- {{#if institutionalUser}}
-
-
- {{institutionalUser.userName}} ({{institutionalUser.userGuid}})
+
+
+
+ {{this.totalUsers}}
+
+ {{t 'institutions.dashboard.users_list.total_users'}}
+
+
+
+
+ {{t 'institutions.dashboard.users_list.has_orcid'}}
+
+
+
+
+
+
+ {{department}}
+
+
+
+
+
+
+ {{t 'institutions.dashboard.users_list.select_columns'}}
+
+ {{#if dd.isOpen}}
+
+
+ {{#each this.columns as |column|}}
+
+
+ {{column.label}}
+
+ {{/each}}
+
+
+
+ {{t 'general.cancel'}}
+
+
+ {{t 'general.apply'}}
+
+
+
+ {{/if}}
+
+
+ {{#if @institution.linkToExternalReportsArchive}}
+
+
+ {{t 'institutions.dashboard.download_past_reports_label'}}
+
+
+ {{/if}}
+
+
+
+
+
+
+
+ {{t 'institutions.dashboard.format_labels.csv'}}
+
+
+ {{t 'institutions.dashboard.format_labels.tsv'}}
+
+
+ {{t 'institutions.dashboard.format_labels.json_table'}}
+
+
+ {{t 'institutions.dashboard.format_labels.json_direct'}}
+
+
+
+
+
+
+
+
+
+
+ {{#let (component 'sort-arrow'
+ class=(local-class 'sort-arrow')
+ sortAction=this.sortInstitutionalUsers
+ sort=this.sort
+ ) as |SortArrow|}}
+
+ {{#each this.columns as |column|}}
+ {{#if (includes column.key this.selectedColumns)}}
+
+
+ {{column.label}}
+ {{#if column.sort_key}}
+
+
+
+ {{/if}}
+
+
+ {{/if}}
+ {{/each}}
+
+ {{/let}}
+
+
+ {{#each this.columns as |column|}}
+ {{#if (includes column.key this.selectedColumns)}}
+
+ {{#if (eq column.type 'user_name')}}
+
+ {{institutionalUser.userName}}
+
+ {{else if (eq column.type 'osf_link')}}
+
+ {{institutionalUser.userGuid}}
+
+ {{else if (eq column.type 'orcid')}}
+ {{#if institutionalUser.orcidId}}
+
+ {{institutionalUser.orcidId}}
+
+ {{else}}
+ {{t 'institutions.dashboard.object-list.table-items.missing-info'}}
+ {{/if}}
+ {{else if (eq column.type 'date_by_month')}}
+ {{#if (get institutionalUser column.key)}}
+ {{moment-format (get institutionalUser column.key) 'MM/YYYY'}}
+ {{else}}
+ {{t 'institutions.dashboard.users_list.not_found'}}
+ {{/if}}
+ {{else}}
+ {{get institutionalUser column.key}}
+ {{/if}}
- {{institutionalUser.department}}
- {{institutionalUser.publicProjects}}
- {{institutionalUser.privateProjects}}
- {{else}}
- {{placeholder.text lines=1}}
- {{placeholder.text lines=1}}
- {{placeholder.text lines=1}}
- {{placeholder.text lines=1}}
{{/if}}
-
-
- {{t 'institutions.dashboard.users_list.empty'}}
-
-
-
+ {{/each}}
+
+
+ {{t 'institutions.dashboard.users_list.empty'}}
+
+
{{/if}}
diff --git a/app/institutions/dashboard/-components/object-list/component-test.ts b/app/institutions/dashboard/-components/object-list/component-test.ts
new file mode 100644
index 00000000000..4c1ae753cb3
--- /dev/null
+++ b/app/institutions/dashboard/-components/object-list/component-test.ts
@@ -0,0 +1,168 @@
+import { click, render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { setupIntl } from 'ember-intl/test-support';
+import { setupRenderingTest } from 'ember-qunit';
+import { TestContext } from 'ember-test-helpers';
+import { module, test } from 'qunit';
+
+import { OsfLinkRouterStub } from 'ember-osf-web/tests/integration/helpers/osf-link-router-stub';
+
+module('Integration | institutions | dashboard | -components | object-list', hooks => {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+ setupIntl(hooks);
+
+ hooks.beforeEach(function(this: TestContext) {
+ this.owner.unregister('service:router');
+ this.owner.register('service:router', OsfLinkRouterStub);
+ const columns = [
+ {
+ name: 'Title',
+ sortKey: 'title',
+ getValue: () => 'Title of some object',
+ },
+ {
+ name: 'Description',
+ getValue: () => 'Description of some object',
+ },
+ {
+ name: 'Contributors',
+ type: 'contributors',
+ },
+ {
+ name: 'DOI',
+ type: 'doi',
+ },
+ ];
+ const institution = server.create('institution', {
+ id: 'my-institution',
+ });
+ const defaultQueryOptions = {
+ cardSearchFilter: {
+ resourceType: 'Project,ProjectComponent',
+ },
+ };
+ this.setProperties({
+ columns,
+ institution,
+ defaultQueryOptions,
+ objectType: 'thingies',
+ });
+ });
+
+ test('the table headers are correct', async function(assert) {
+ await render(hbs`
+
+ `);
+
+ // Elements from InstitutionDashboarWrapper are present
+ assert.dom('[data-test-page-tab="summary"]').exists('Summary tab exists');
+
+ // Elements in the top bar are present
+ assert.dom('[data-test-object-count]').containsText('10 total thingies', 'Object count is correct');
+ assert.dom('[data-test-toggle-filter-button]').exists('Filter button exists');
+ assert.dom('[data-test-customize-columns-button]').exists('Columns button exists');
+
+ assert.dom('[data-test-object-list-table]').exists('Object list exists');
+
+ // The table headers are correct
+ assert.dom('[data-test-column-header]').exists({ count: 4 }, 'There are 4 columns');
+ assert.dom('[data-test-column-header="Title"]').containsText('Title');
+ assert.dom('[data-test-column-header="Title"] [data-test-sort="title"]').exists('Title is sortable');
+ assert.dom('[data-test-column-header="Description"]').containsText('Description');
+ assert.dom('[data-test-column-header="Description"] [data-test-sort="description"]')
+ .doesNotExist('Description is not sortable');
+
+ // The table data is not blatantly incorrect
+ assert.dom('[data-test-object-table-body-row]').exists({ count: 10 }, 'There are 10 rows');
+ });
+
+ test('the table supports filtering', async function(assert) {
+ await render(hbs`
+
+ `);
+
+ await click('[data-test-toggle-filter-button]');
+
+ assert.dom('[data-test-filter-facet-toggle]').exists({ count: 3 }, '3 filters available');
+
+ // Open the filter facet and load the values and select the first filter value
+ await click('[data-test-filter-facet-toggle]');
+ await click('[data-test-filter-facet-value] button');
+
+ assert.dom('[data-test-active-filter]').exists({ count: 1 }, '1 filter active');
+ assert.dom('[data-test-remove-active-filter]').exists('Remove filter button exists');
+
+ await click('[data-test-remove-active-filter]');
+ assert.dom('[data-test-active-filter]').doesNotExist('Filter removed');
+ });
+
+ test('the table supports customizing columns', async function(assert) {
+ await render(hbs`
+
+ `);
+
+ assert.dom('[data-test-column-header]').exists({ count: 4 }, '4 columns available');
+ const titleColumn = document.querySelector('[data-test-column-header="Title"]');
+ assert.ok(titleColumn, 'Title column is visible');
+
+ // Open the column customization menu
+ await click('[data-test-customize-columns-button]');
+ assert.dom('[data-test-column-toggle-input]').exists({ count: 4 }, '4 columns available to show/hide');
+ assert.dom('[data-test-column-toggle-input="Title"]').isChecked('Title column checkbox is checked');
+ assert.dom('[data-test-column-toggle-input="Description"]').isChecked('Description column checkbox is checked');
+
+ // Toggle off the first column
+ await click('[data-test-column-toggle-input="Title"]');
+ assert.ok(titleColumn, 'Title column still visible after toggling off');
+
+ // Save changes
+ await click('[data-test-save-columns-button]');
+ assert.dom('[data-test-column-toggle-input]').doesNotExist('Column toggle menu hidden');
+ assert.dom('[data-test-column-header="Title"]').doesNotExist('Title column removed');
+ assert.dom('[data-test-column-header]').exists({ count: 3 }, '3 columns available');
+
+ // Open the menu again
+ await click('[data-test-customize-columns-button]');
+ assert.dom('[data-test-column-toggle-input]').exists({ count: 4 }, 'Column toggle menu reopened');
+ assert.dom('[data-test-column-toggle-input="Title"]').isNotChecked('Title column checkbox is not checked');
+ assert.dom('[data-test-column-toggle-input="Description"]')
+ .isChecked('Description column checkbox is still checked');
+
+ // Toggle off all columns, but reset
+ await click('[data-test-column-toggle-input="Description"]');
+ await click('[data-test-column-toggle-input="Contributors"]');
+ await click('[data-test-column-toggle-input="DOI"]');
+ await click('[data-test-reset-columns-button]');
+ assert.dom('[data-test-column-toggle-input]').doesNotExist('Column toggle menu hidden');
+ assert.dom('[data-test-column-header]').exists({ count: 3 }, '3 columns available, as we did not save changes');
+
+ // Open the menu again
+ await click('[data-test-customize-columns-button]');
+ assert.dom('[data-test-column-toggle-input]').exists({ count: 4 }, 'Column toggle menu reopened');
+ assert.dom('[data-test-column-toggle-input="Title"]').isNotChecked('Title column checkbox is not checked');
+ assert.dom('[data-test-column-toggle-input="Description"]')
+ .isChecked('Description column checkbox is still checked');
+
+ // Toggle title back on
+ await click('[data-test-column-toggle-input="Title"]');
+ await click('[data-test-save-columns-button]');
+ assert.ok(titleColumn, 'Title column visible again');
+ });
+});
diff --git a/app/institutions/dashboard/-components/object-list/component.ts b/app/institutions/dashboard/-components/object-list/component.ts
new file mode 100644
index 00000000000..0333bfb59ad
--- /dev/null
+++ b/app/institutions/dashboard/-components/object-list/component.ts
@@ -0,0 +1,120 @@
+import { action } from '@ember/object';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+import InstitutionModel from 'ember-osf-web/models/institution';
+import { SuggestedFilterOperators } from 'ember-osf-web/models/related-property-path';
+import SearchResultModel from 'ember-osf-web/models/search-result';
+import { Filter } from 'osf-components/components/search-page/component';
+
+interface Column {
+ name: string;
+ sortKey?: string;
+ sortParam?: string;
+}
+interface ValueColumn extends Column {
+ getValue(searchResult: SearchResultModel): string;
+}
+
+interface LinkColumn extends Column {
+ getHref(searchResult: SearchResultModel): string;
+ getLinkText(searchResult: SearchResultModel): string;
+ type: 'link';
+}
+
+interface ComponentColumn extends Column {
+ type: 'doi' | 'contributors';
+}
+
+export type ObjectListColumn = ValueColumn | LinkColumn | ComponentColumn;
+
+interface InstitutionalObjectListArgs {
+ institution: InstitutionModel;
+ defaultQueryOptions: Record<'cardSearchFilter', Record>;
+ columns: ObjectListColumn[];
+ objectType: string;
+}
+
+export default class InstitutionalObjectList extends Component {
+ @tracked activeFilters: Filter[] = [];
+ @tracked page = '';
+ @tracked sort = '-dateModified';
+ @tracked sortParam?: string;
+ @tracked visibleColumns = this.args.columns.map(column => column.name);
+ @tracked dirtyVisibleColumns = [...this.visibleColumns]; // track changes to visible columns before they are saved
+
+ get queryOptions() {
+ const options = {
+ cardSearchFilter: {
+ ...this.args.defaultQueryOptions.cardSearchFilter,
+ },
+ 'page[cursor]': this.page,
+ 'page[size]': 10,
+ // sort can look like `sort=dateFieldName` or `sort[integer-value]=fieldName` if sortParam is provided
+ sort: this.sortParam ? { [this.sortParam]: this.sort } : this.sort,
+ };
+ const fullQueryOptions = this.activeFilters.reduce((acc, filter: Filter) => {
+ if (filter.suggestedFilterOperator === SuggestedFilterOperators.IsPresent) {
+ acc.cardSearchFilter[filter.propertyPathKey] = {};
+ acc.cardSearchFilter[filter.propertyPathKey][filter.value] = true;
+ return acc;
+ }
+ const currentValue = acc.cardSearchFilter[filter.propertyPathKey];
+ acc.cardSearchFilter[filter.propertyPathKey] =
+ currentValue ? currentValue.concat(filter.value) : [filter.value];
+ return acc;
+ }, options);
+ return fullQueryOptions;
+ }
+
+ get valueSearchQueryOptions() {
+ return {
+ ...this.queryOptions.cardSearchFilter,
+ };
+ }
+
+ @action
+ updateVisibleColumns() {
+ this.visibleColumns = [...this.dirtyVisibleColumns];
+ }
+
+ @action
+ resetColumns() {
+ this.dirtyVisibleColumns = [...this.visibleColumns];
+ }
+
+ @action
+ toggleColumnVisibility(columnName: string) {
+ if (this.dirtyVisibleColumns.includes(columnName)) {
+ this.dirtyVisibleColumns.removeObject(columnName);
+ } else {
+ this.dirtyVisibleColumns.pushObject(columnName);
+ }
+ }
+
+ @action
+ toggleFilter(property: Filter) {
+ this.page = '';
+ if (this.activeFilters.includes(property)) {
+ this.activeFilters.removeObject(property);
+ } else {
+ this.activeFilters.pushObject(property);
+ }
+ }
+
+ @action
+ updateSortKey(newSortKey: string, newSortParam?: string) {
+ this.sortParam = newSortParam;
+ this.page = '';
+ if (this.sort === newSortKey) {
+ this.sort = '-' + newSortKey;
+ } else {
+ this.sort = newSortKey;
+ }
+ }
+
+ @action
+ updatePage(newPage: string) {
+ this.page = newPage;
+ }
+}
diff --git a/app/institutions/dashboard/-components/object-list/contributors-field/component.ts b/app/institutions/dashboard/-components/object-list/contributors-field/component.ts
new file mode 100644
index 00000000000..d4923162959
--- /dev/null
+++ b/app/institutions/dashboard/-components/object-list/contributors-field/component.ts
@@ -0,0 +1,75 @@
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import Intl from 'ember-intl/services/intl';
+
+import InstitutionModel from 'ember-osf-web/models/institution';
+import SearchResultModel from 'ember-osf-web/models/search-result';
+import { AttributionRoleIris } from 'ember-osf-web/models/index-card';
+import { getOsfmapObjects, getSingleOsfmapValue, hasOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld';
+
+interface ContributorsFieldArgs {
+ searchResult: SearchResultModel;
+ institution: InstitutionModel;
+}
+
+const roleIriToTranslationKey: Record = {
+ [AttributionRoleIris.Admin]: 'general.permissions.admin',
+ [AttributionRoleIris.Write]: 'general.permissions.write',
+ [AttributionRoleIris.Read]: 'general.permissions.read',
+};
+
+
+export default class InstitutionalObjectListContributorsField extends Component {
+ @service intl!: Intl;
+
+ // Return two contributors affiliated with the institution given with highest permission levels
+ get topInstitutionAffiliatedContributors() {
+ const { searchResult, institution } = this.args;
+ const {resourceMetadata} = searchResult;
+ const attributions: any[] = getOsfmapObjects(resourceMetadata, ['qualifiedAttribution']);
+ const contributors = getOsfmapObjects(resourceMetadata, ['creator']);
+ const institutionIris = institution.iris;
+
+ const affiliatedAttributions = attributions
+ .filter((attribution: any) => hasInstitutionAffiliation(contributors, attribution, institutionIris));
+ const adminAttributions = affiliatedAttributions.filter(
+ attribution => hasOsfmapValue(attribution, ['hadRole'], AttributionRoleIris.Admin),
+ );
+ const writeAttributions = affiliatedAttributions.filter(
+ attribution => hasOsfmapValue(attribution, ['hadRole'], AttributionRoleIris.Write),
+ );
+ const readAttributions = affiliatedAttributions.filter(
+ attribution => hasOsfmapValue(attribution, ['hadRole'], AttributionRoleIris.Read),
+ );
+
+ const prioritizedAttributions = adminAttributions.concat(writeAttributions, readAttributions);
+
+ return prioritizedAttributions.slice(0, 2).map(attribution => {
+ const contributor = getContributorById(contributors, getSingleOsfmapValue(attribution, ['agent']));
+ const roleIri: AttributionRoleIris = getSingleOsfmapValue(attribution, ['hadRole']);
+ return {
+ name: getSingleOsfmapValue(contributor,['name']),
+ url: getSingleOsfmapValue(contributor, ['identifier']),
+ permissionLevel: this.intl.t(roleIriToTranslationKey[roleIri]),
+ };
+ });
+ }
+}
+
+function hasInstitutionAffiliation(contributors: any[], attribution: any, institutionIris: string[]) {
+ const attributedContributor = getContributorById(contributors, getSingleOsfmapValue(attribution, ['agent']));
+
+ if (!attributedContributor.affiliation) {
+ return false;
+ }
+
+ return attributedContributor.affiliation.some(
+ (affiliation: any) => affiliation.identifier.some(
+ (affiliationIdentifier: any) => institutionIris.includes(affiliationIdentifier['@value']),
+ ),
+ );
+}
+
+function getContributorById(contributors: any[], contributorId: string) {
+ return contributors.find(contributor => contributor['@id'] === contributorId);
+}
diff --git a/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs b/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs
new file mode 100644
index 00000000000..992e6be26a3
--- /dev/null
+++ b/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs
@@ -0,0 +1,14 @@
+{{#each this.topInstitutionAffiliatedContributors as |contributor|}}
+
+
+ {{contributor.name}}
+
+ {{t 'institutions.dashboard.object-list.table-items.permission-level' permissionLevel=contributor.permissionLevel}}
+
+{{else}}
+
+ {{t 'institutions.dashboard.object-list.table-items.no-contributors'}}
+
+{{/each}}
diff --git a/app/institutions/dashboard/-components/object-list/doi-field/component.ts b/app/institutions/dashboard/-components/object-list/doi-field/component.ts
new file mode 100644
index 00000000000..5a1e2733b16
--- /dev/null
+++ b/app/institutions/dashboard/-components/object-list/doi-field/component.ts
@@ -0,0 +1,15 @@
+import Component from '@glimmer/component';
+
+import SearchResultModel from 'ember-osf-web/models/search-result';
+import { extractDoi } from 'ember-osf-web/utils/doi';
+
+interface DoiFieldArgs {
+ searchResult: SearchResultModel;
+}
+
+export default class InstitutionalObjectListDoiField extends Component {
+ get dois() {
+ const dois = this.args.searchResult.doi;
+ return dois.map((doi: string) => ({ fullLink: doi, displayText: extractDoi(doi) }));
+ }
+}
diff --git a/app/institutions/dashboard/-components/object-list/doi-field/template.hbs b/app/institutions/dashboard/-components/object-list/doi-field/template.hbs
new file mode 100644
index 00000000000..59496096bbc
--- /dev/null
+++ b/app/institutions/dashboard/-components/object-list/doi-field/template.hbs
@@ -0,0 +1,5 @@
+{{#each this.dois as |doi|}}
+ {{doi.displayText}}
+{{else}}
+ {{t 'institutions.dashboard.object-list.table-items.missing-info'}}
+{{/each}}
diff --git a/app/institutions/dashboard/-components/object-list/styles.scss b/app/institutions/dashboard/-components/object-list/styles.scss
new file mode 100644
index 00000000000..5eabae93733
--- /dev/null
+++ b/app/institutions/dashboard/-components/object-list/styles.scss
@@ -0,0 +1,130 @@
+@import 'app/styles/layout';
+
+.top-bar-wrapper {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 1rem 0;
+}
+
+.total-object-count {
+ align-self: center;
+ font-size: large;
+
+ .total-object-number {
+ font-weight: bold;
+ }
+}
+
+.top-bar-button-wrapper {
+ display: flex;
+
+ button {
+ margin-right: 0.5rem;
+ }
+}
+
+.customize-menu-wrapper {
+ display: flex;
+ flex-direction: column;
+ padding: 0.5rem;
+ border: 1px solid $color-border-gray;
+ width: 240px;
+
+ label {
+ text-wrap: nowrap;
+ }
+}
+
+.customize-menu-button-wrapper {
+ display: flex;
+ justify-content: end;
+}
+
+.table-wrapper {
+ overflow: auto;
+}
+
+.object-table {
+ border-collapse: collapse;
+
+ th {
+ padding: 10px 15px;
+
+ span {
+ display: flex;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+ }
+
+ td {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ padding: 10px 15px;
+
+ border: 1px solid $color-border-gray;
+ }
+}
+
+.object-table-head {
+ background: $color-bg-gray-blue-dark;
+ color: $color-text-white;
+}
+
+.bottom-bar-wrapper {
+ display: flex;
+ justify-content: end;
+ margin: 1rem 0;
+
+ button {
+ margin-left: 0.5rem;
+ }
+}
+
+.right-wrapper {
+ min-width: 300px;
+ padding: 0.5rem;
+}
+
+.right-panel-header {
+ font-size: 1.5rem;
+}
+
+.close-button {
+ float: right;
+}
+
+.active-filter-list {
+ list-style: none;
+ padding-left: 0;
+ margin-top: 1rem;
+ border-top: 1px solid $color-border-gray;
+ border-bottom: 1px solid $color-border-gray;
+}
+
+.active-filter-item {
+ display: flex;
+ margin: 0.5rem 0.2rem;
+ justify-content: space-between;
+
+ button {
+ margin-right: -5px;
+ }
+}
+
+.blue-text-button {
+ color: $color-link-dark;
+}
+
+.download-dropdown-trigger {
+ color: $color-bg-blue-dark;
+}
+
+.download-button-group {
+ align-content: center;
+ display: inline-flex;
+ padding-left: 10px;
+}
diff --git a/app/institutions/dashboard/-components/object-list/template.hbs b/app/institutions/dashboard/-components/object-list/template.hbs
new file mode 100644
index 00000000000..2015b283e94
--- /dev/null
+++ b/app/institutions/dashboard/-components/object-list/template.hbs
@@ -0,0 +1,259 @@
+
+
+
+ {{#if list.searchObjectsTask.isRunning}}
+
+ {{else}}
+
+
+ {{list.totalResultCount}}
+ {{t 'institutions.dashboard.object-list.total-objects' objectType=@objectType}}
+
+
+
+
+ {{t 'institutions.dashboard.object-list.filter-button-label'}}
+
+
+
+
+ {{t 'institutions.dashboard.object-list.customize'}}
+
+
+ {{#each @columns as |column|}}
+
+
+ {{column.name}}
+
+ {{/each}}
+
+
+ {{t 'general.reset'}}
+
+
+ {{t 'general.save'}}
+
+
+
+
+
+ {{#if @institution.linkToExternalReportsArchive}}
+
+
+
+ {{t 'institutions.dashboard.download_past_reports_label'}}
+
+
+
+
+ {{/if}}
+
+
+
+
+
+
+ {{t 'institutions.dashboard.format_labels.csv'}}
+
+
+ {{t 'institutions.dashboard.format_labels.tsv'}}
+
+
+
+
+
+
+
+
+
+
+ {{#let (component 'sort-arrow'
+ sort=this.sort
+ ) as |SortArrow|
+ }}
+ {{#each @columns as |column|}}
+ {{#if (includes column.name this.visibleColumns)}}
+
+
+ {{column.name}}
+ {{#if column.sortKey}}
+
+ {{/if}}
+
+
+ {{/if}}
+ {{/each}}
+ {{/let}}
+
+
+
+
+ {{#each list.searchResults as |result|}}
+
+ {{#each @columns as |column|}}
+ {{#if (includes column.name this.visibleColumns)}}
+
+ {{#if (eq column.type 'link')}}
+
+ {{call (fn column.getLinkText result)}}
+
+ {{else if (eq column.type 'doi')}}
+
+ {{else if (eq column.type 'contributors')}}
+
+ {{else}}
+ {{call (fn column.getValue result)}}
+ {{/if}}
+
+ {{/if}}
+ {{/each}}
+
+ {{/each}}
+
+
+
+
+ {{#if list.showFirstPageOption}}
+
+ {{t 'institutions.dashboard.object-list.first'}}
+
+ {{/if}}
+ {{#if list.hasPrevPage}}
+
+ {{t 'institutions.dashboard.object-list.prev'}}
+
+ {{/if}}
+ {{#if list.hasNextPage}}
+
+ {{t 'institutions.dashboard.object-list.next'}}
+
+ {{/if}}
+
+ {{/if}}
+
+ {{#if list.relatedProperties}}
+
+
+ {{t 'institutions.dashboard.object-list.filter-heading'}}
+
+
+
+
+ {{#if this.activeFilters}}
+
+ {{#each this.activeFilters as |filter|}}
+
+
+ {{filter.propertyVisibleLabel}}:
+ {{filter.label}}
+
+
+
+
+
+ {{/each}}
+
+ {{/if}}
+
+ {{#each list.relatedProperties as |property|}}
+
+ {{/each}}
+ {{#if list.booleanFilters.length}}
+
+ {{/if}}
+
+ {{/if}}
+
+
diff --git a/app/institutions/dashboard/-components/panel/styles.scss b/app/institutions/dashboard/-components/panel/styles.scss
deleted file mode 100644
index cba6f52f7da..00000000000
--- a/app/institutions/dashboard/-components/panel/styles.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-.panel {
- .panel-overall {
- border: 0;
- margin-bottom: 30px;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
- }
-
- .panel-heading {
- background: #365063;
- padding: 15px;
- border: 0;
- }
-
- .panel-title {
- display: block;
- float: none;
- color: #fff;
- font-size: 14px;
- font-weight: bold;
- text-align: center;
- text-transform: uppercase;
- line-height: 20px;
- }
-
- .panel-body {
- color: #263947;
- text-align: center;
-
- h3 {
- font-size: 18pt;
- font-weight: 800;
- }
- }
-}
diff --git a/app/institutions/dashboard/-components/panel/template.hbs b/app/institutions/dashboard/-components/panel/template.hbs
deleted file mode 100644
index d8d1e4fa52f..00000000000
--- a/app/institutions/dashboard/-components/panel/template.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
- {{@title}}
-
-
- {{#if @isLoading}}
-
- {{else}}
- {{yield}}
- {{/if}}
-
-
\ No newline at end of file
diff --git a/app/institutions/dashboard/-components/projects-panel/component.ts b/app/institutions/dashboard/-components/projects-panel/component.ts
deleted file mode 100644
index 1e30b36108f..00000000000
--- a/app/institutions/dashboard/-components/projects-panel/component.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import Component from '@ember/component';
-import { computed } from '@ember/object';
-import { alias } from '@ember/object/computed';
-import { inject as service } from '@ember/service';
-import { ChartData, ChartOptions } from 'ember-cli-chart';
-import Intl from 'ember-intl/services/intl';
-
-import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric';
-
-export default class ProjectsPanel extends Component {
- summaryMetrics!: InstitutionSummaryMetricModel;
- @alias('summaryMetrics.privateProjectCount') numPrivateProjects!: number;
- @alias('summaryMetrics.publicProjectCount') numPublicProjects!: number;
- @service intl!: Intl;
-
- chartOptions: ChartOptions = {
- aspectRatio: 1,
- legend: {
- display: false,
- },
- };
-
- @computed('numPrivateProjects', 'numPublicProjects')
- get numProjects(): number {
- return this.numPublicProjects + this.numPrivateProjects;
- }
-
- @computed('numPrivateProjects', 'numPublicProjects')
- get chartData(): ChartData {
- return {
- labels: [
- this.intl.t('institutions.dashboard.public'),
- this.intl.t('institutions.dashboard.private'),
- ],
- datasets: [{
- data: [
- this.numPublicProjects,
- this.numPrivateProjects,
- ],
- backgroundColor: [
- '#36b183',
- '#a5b3bd',
- ],
- }],
- };
- }
-}
diff --git a/app/institutions/dashboard/-components/projects-panel/styles.scss b/app/institutions/dashboard/-components/projects-panel/styles.scss
deleted file mode 100644
index 51603b6aad1..00000000000
--- a/app/institutions/dashboard/-components/projects-panel/styles.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.ember-chart {
- max-width: 200px;
- max-height: 200px;
- margin: 0 auto 15px;
-}
-
-.projects-count {
- font-size: 16px;
-
- span:first-of-type {
- margin-right: 15px;
- }
-}
diff --git a/app/institutions/dashboard/-components/projects-panel/template.hbs b/app/institutions/dashboard/-components/projects-panel/template.hbs
deleted file mode 100644
index a9a5bc68fd6..00000000000
--- a/app/institutions/dashboard/-components/projects-panel/template.hbs
+++ /dev/null
@@ -1,25 +0,0 @@
-{{#if this.summaryMetrics}}
-
-
-
- {{this.numProjects}}
-
-
- {{this.numPublicProjects}}
- {{t 'institutions.dashboard.public'}}
-
-
- {{this.numPrivateProjects}}
- {{t 'institutions.dashboard.private'}}
-
-
-{{else}}
- {{t 'institutions.dashboard.empty'}}
-{{/if}}
\ No newline at end of file
diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/component-test.ts b/app/institutions/dashboard/-components/total-count-kpi-wrapper/component-test.ts
new file mode 100644
index 00000000000..7df5335449e
--- /dev/null
+++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/component-test.ts
@@ -0,0 +1,118 @@
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { setupIntl } from 'ember-intl/test-support';
+import { setupRenderingTest } from 'ember-qunit';
+import { TestContext } from 'ember-test-helpers';
+import { module, test } from 'qunit';
+
+module('Integration | institutions | dashboard | -components | total-count-kpi-wrapper', hooks => {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+ setupIntl(hooks);
+
+ hooks.beforeEach(function(this: TestContext) {
+ const model = Object({
+ summaryMetrics: {
+ publicProjectCount: 10,
+ privateProjectCount: 10,
+ userCount: 10,
+ publicRegistrationCount: 100,
+ publishedPreprintCount: 1000,
+ embargoedRegistrationCount: 200,
+ storageByteCount: 104593230,
+ publicFileCount: 1567,
+ monthlyLoggedInUserCount: 300,
+ monthlyActiveUserCount:40,
+ convertedStorageCount: '104 GB',
+ },
+ });
+
+ this.set('model', model);
+ });
+
+ test('it renders the dashboard total kpis correctly', async assert => {
+ // Given the component is rendered
+ await render(hbs`
+
+`);
+
+ let parentContainer = '[data-test-total-count-kpi="0"]';
+ // Then the total users kpi is tested
+ assert.dom(parentContainer)
+ .exists('The User Widget exists');
+ assert.dom(parentContainer)
+ .hasText('10 Total Users');
+ assert.dom(`${parentContainer} [data-test-kpi-icon]`)
+ .hasAttribute('data-icon', 'users');
+
+ // And the total logged in user kpi is tested
+ parentContainer = '[data-test-total-count-kpi="1"]';
+ assert.dom(parentContainer)
+ .exists('The Total Monthly Logged in Users Widget exists');
+ assert.dom(parentContainer)
+ .hasText('300 Total Monthly Logged in Users');
+ assert.dom(`${parentContainer} [data-test-kpi-icon]`)
+ .hasAttribute('data-icon', 'users');
+
+ // And the total active in user kpi is tested
+ parentContainer = '[data-test-total-count-kpi="2"]';
+ assert.dom(parentContainer)
+ .exists('The Total Monthly Active Users Widget exists');
+ assert.dom(parentContainer)
+ .hasText('40 Total Monthly Active Users');
+ assert.dom(`${parentContainer} [data-test-kpi-icon]`)
+ .hasAttribute('data-icon', 'users');
+
+ // And the total project kpi is tested
+ parentContainer = '[data-test-total-count-kpi="3"]';
+ assert.dom(parentContainer)
+ .exists('The Project Widget exists');
+ assert.dom(parentContainer)
+ .hasText('20 OSF Public and Private Projects');
+ assert.dom(`${parentContainer} [data-test-kpi-icon]`)
+ .hasAttribute('data-icon', 'flask');
+
+ // And the total registration kpi is tested
+ parentContainer = '[data-test-total-count-kpi="4"]';
+ assert.dom(parentContainer)
+ .exists('The Total Registration Widget exists');
+ assert.dom(parentContainer)
+ .hasText('300 OSF Public and Embargoed Registrations');
+ assert.dom(`${parentContainer} [data-test-kpi-icon]`)
+ .hasAttribute('data-icon', 'archive');
+
+ // And the total preprint kpi is tested
+ parentContainer = '[data-test-total-count-kpi="5"]';
+ assert.dom(parentContainer)
+ .exists('The Total Preprint Widget exists');
+ assert.dom(parentContainer)
+ .hasText('1000 OSF Preprints');
+ assert.dom(`${parentContainer} [data-test-kpi-icon]`)
+ .hasAttribute('data-icon', 'file-alt');
+
+ // And the total file count kpi is tested
+ parentContainer = '[data-test-total-count-kpi="6"]';
+ assert.dom(parentContainer)
+ .exists('The Total File Widget exists');
+ assert.dom(parentContainer)
+ .hasText('1567 Total Public File Count');
+ assert.dom(`${parentContainer} [data-test-kpi-icon]`)
+ .hasAttribute('data-icon', 'file-alt');
+
+ // And the total storage kpi is tested
+ parentContainer = '[data-test-total-count-kpi="7"]';
+ assert.dom(parentContainer)
+ .exists('The Total Storage Widget exists');
+ assert.dom(parentContainer)
+ .hasText('104 Total Storage in GB');
+ assert.dom(`${parentContainer} [data-test-kpi-icon]`)
+ .hasAttribute('data-icon', 'database');
+
+ // Finally there are only 8 widgets
+ assert.dom('[data-test-total-count-kpi="8"]')
+ .doesNotExist('There are only 8 widgets');
+ });
+});
diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/component.ts b/app/institutions/dashboard/-components/total-count-kpi-wrapper/component.ts
new file mode 100644
index 00000000000..ccc9c62e102
--- /dev/null
+++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/component.ts
@@ -0,0 +1,101 @@
+import { waitFor } from '@ember/test-waiters';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { task } from 'ember-concurrency';
+import { taskFor } from 'ember-concurrency-ts';
+import Intl from 'ember-intl/services/intl';
+import { inject as service } from '@ember/service';
+import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric';
+
+interface TotalCountKpiWrapperArgs {
+ model: any;
+}
+
+export interface TotalCountKpiModel {
+ title: string;
+ total: number | string;
+ icon: string;
+}
+
+export default class TotalCountKpiWrapperComponent extends Component {
+ @service intl!: Intl;
+ @tracked model = this.args.model;
+ @tracked totalCountKpis = [] as TotalCountKpiModel[];
+ @tracked isLoading = true;
+
+ constructor(owner: unknown, args: TotalCountKpiWrapperArgs) {
+ super(owner, args);
+
+ taskFor(this.loadData).perform();
+ }
+
+ /**
+ * calculateProjects
+ *
+ * @description Abstracted method to calculate the private and public projects
+ * @param summaryMetrics The institutional summary metrics object
+ *
+ * @returns The total of private and public projects
+ */
+ private calculateProjects(summaryMetrics: InstitutionSummaryMetricModel): number {
+ return summaryMetrics.privateProjectCount + summaryMetrics.publicProjectCount;
+ }
+
+ private calculateRegistrations(summaryMetrics: InstitutionSummaryMetricModel): number {
+ return summaryMetrics.embargoedRegistrationCount + summaryMetrics.publicRegistrationCount;
+ }
+
+ @task
+ @waitFor
+ private async loadData(): Promise {
+ const metrics: { summaryMetrics: InstitutionSummaryMetricModel } = await this.model;
+ const [storageAmount, storageUnit] = metrics.summaryMetrics.convertedStorageCount.split(' ');
+
+ this.totalCountKpis.push(
+ {
+ title: this.intl.t('institutions.dashboard.kpi-panel.users'),
+ total: metrics.summaryMetrics.userCount,
+ icon: 'users',
+ },
+ {
+ title: this.intl.t('institutions.dashboard.kpi-panel.logged-in-users'),
+ total: metrics.summaryMetrics.monthlyLoggedInUserCount,
+ icon: 'users',
+ },
+ {
+ title: this.intl.t('institutions.dashboard.kpi-panel.active-users'),
+ total: metrics.summaryMetrics.monthlyActiveUserCount,
+ icon: 'users',
+ },
+ {
+ title: this.intl.t('institutions.dashboard.kpi-panel.projects'),
+ total: this.calculateProjects(metrics.summaryMetrics),
+ icon: 'flask',
+ },
+ {
+ title: this.intl.t('institutions.dashboard.kpi-panel.registrations'),
+ total: this.calculateRegistrations(metrics.summaryMetrics),
+ icon: 'archive',
+ },
+ {
+ title: this.intl.t('institutions.dashboard.kpi-panel.preprints'),
+ total: metrics.summaryMetrics.publishedPreprintCount,
+ icon: 'file-alt',
+ },
+ {
+ title: this.intl.t('institutions.dashboard.kpi-panel.file-count'),
+ total: metrics.summaryMetrics.publicFileCount,
+ icon: 'file-alt',
+ },
+ {
+ title: this.intl.t('institutions.dashboard.kpi-panel.storage', { unit: storageUnit }),
+ total: storageAmount,
+ icon: 'database',
+ },
+ );
+
+ this.isLoading = false;
+ }
+}
+
+
diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/styles.scss b/app/institutions/dashboard/-components/total-count-kpi-wrapper/styles.scss
new file mode 100644
index 00000000000..853bdec6f24
--- /dev/null
+++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/styles.scss
@@ -0,0 +1,29 @@
+.wrapper-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: flex-start;
+ width: calc(100% - 24px);
+ min-height: 145px;
+ height: fit-content;
+ margin-left: 12px;
+ margin-right: 12px;
+ margin-bottom: 12px;
+
+ .loading {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 145px;
+ }
+
+ &.mobile {
+ flex-direction: column;
+ height: fit-content;
+ align-items: center;
+ margin-bottom: 0;
+ }
+}
diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/template.hbs b/app/institutions/dashboard/-components/total-count-kpi-wrapper/template.hbs
new file mode 100644
index 00000000000..943bca40371
--- /dev/null
+++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/template.hbs
@@ -0,0 +1,14 @@
+
+ {{#if this.isLoading}}
+
+
+
+ {{else}}
+ {{#each this.totalCountKpis as |totalCountKpi index|}}
+
+ {{/each}}
+ {{/if}}
+
\ No newline at end of file
diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/component-test.ts b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/component-test.ts
new file mode 100644
index 00000000000..69897968789
--- /dev/null
+++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/component-test.ts
@@ -0,0 +1,64 @@
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { EnginesIntlTestContext } from 'ember-engines/test-support';
+import { setupIntl } from 'ember-intl/test-support';
+import { setupRenderingTest } from 'ember-qunit';
+import { TestContext } from 'ember-test-helpers';
+import { module, test } from 'qunit';
+
+module('Integration | institutions | dashboard | -components | total-count-kpi', hooks => {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+ setupIntl(hooks);
+
+ hooks.beforeEach(function(this: TestContext) {
+ const data = Object({
+ total: 200,
+ title: 'This is the title',
+ icon: 'building',
+ });
+
+ this.set('data', data);
+ });
+
+ test('it renders the data correctly', async assert => {
+
+ await render(hbs`
+
+`);
+
+ assert.dom('[data-test-kpi-title]')
+ .hasText('This is the title');
+ assert.dom('[data-test-kpi-data]')
+ .hasText('200');
+ assert.dom('[data-test-kpi-icon]')
+ .hasAttribute('data-icon', 'building');
+ });
+
+ test('it renders the without data correctly', async function(this: EnginesIntlTestContext, assert) {
+ const data = Object({
+ total: 0,
+ title: 'This is the title',
+ icon: 'building',
+ });
+
+ this.set('data', data);
+
+
+ await render(hbs`
+
+`);
+
+ assert.dom('[data-test-kpi-title]')
+ .hasText('This is the title');
+ assert.dom('[data-test-kpi-data]')
+ .hasText('No data for institution found.');
+ assert.dom('[data-test-kpi-icon]')
+ .hasAttribute('data-icon', 'building');
+ });
+});
diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/styles.scss b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/styles.scss
new file mode 100644
index 00000000000..ad500d44b1b
--- /dev/null
+++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/styles.scss
@@ -0,0 +1,67 @@
+// stylelint-disable max-nesting-depth, selector-max-compound-selectors
+
+.kpi-container {
+ margin-right: 12px;
+ margin-bottom: 12px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ width: 350px;
+ height: 140px;
+ background-color: $color-bg-white;
+
+ .top-container {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ padding: 0 15px;
+ margin-bottom: 10px;
+
+ .left-container {
+ padding-left: 5px;
+ height: 75px;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+
+ .total-container {
+ font-size: 84px;
+ font-style: normal;
+ font-weight: bolder;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+ }
+
+ .right-container {
+ padding-right: 10px;
+ height: 75px;
+ width: 75px;
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+
+ .icon {
+ font-size: 60px;
+ color: $color-text-slate-gray;
+ }
+ }
+
+ }
+
+ .title {
+ padding-left: 15px;
+ width: calc(100% - 15px);
+ font-size: 14px;
+ font-weight: normal;
+ height: 25px;
+ }
+
+ &.mobile {
+ margin-right: 0;
+ margin-bottom: 12px;
+ }
+}
diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/template.hbs b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/template.hbs
new file mode 100644
index 00000000000..59484f26916
--- /dev/null
+++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/template.hbs
@@ -0,0 +1,20 @@
+
+
+
+ {{#if @data.total}}
+
+ {{@data.total}}
+
+ {{else}}
+ {{t 'institutions.dashboard.empty'}}
+ {{/if}}
+
+
+
+
+
+
{{@data.title}}
+
diff --git a/app/institutions/dashboard/controller.ts b/app/institutions/dashboard/controller.ts
deleted file mode 100644
index 785f77100c5..00000000000
--- a/app/institutions/dashboard/controller.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { alias } from '@ember/object/computed';
-import Controller from '@ember/controller';
-import { computed } from '@ember/object';
-import { inject as service } from '@ember/service';
-
-import { InstitutionsDashboardModel } from 'ember-osf-web/institutions/dashboard/route';
-import InstitutionModel from 'ember-osf-web/models/institution';
-import InstitutionDepartmentModel from 'ember-osf-web/models/institution-department';
-import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric';
-import CurrentUser from 'ember-osf-web/services/current-user';
-import { addQueryParam } from 'ember-osf-web/utils/url-parts';
-
-export default class InstitutionsDashboardController extends Controller {
- @service currentUser!: CurrentUser;
-
- @alias('model.taskInstance.value') modelValue?: InstitutionsDashboardModel;
- @alias('modelValue.institution') institution?: InstitutionModel;
- @alias('modelValue.summaryMetrics') summaryMetrics?: InstitutionSummaryMetricModel;
- @alias('modelValue.departmentMetrics') departmentMetrics?: InstitutionDepartmentModel[];
- @alias('modelValue.totalUsers') totalUsers?: number;
-
- @computed('institution')
- get csvHref(): string {
- const { institution } = this;
- if (institution) {
- const url = institution.hasMany('userMetrics').link();
- return addQueryParam(url, 'format', 'csv');
- }
- return '';
- }
-}
-
-declare module '@ember/controller' {
- interface Registry {
- 'institutions-dashboard': InstitutionsDashboardController;
- }
-}
diff --git a/app/institutions/dashboard/index/styles.scss b/app/institutions/dashboard/index/styles.scss
new file mode 100644
index 00000000000..5df5cd3eeaf
--- /dev/null
+++ b/app/institutions/dashboard/index/styles.scss
@@ -0,0 +1,24 @@
+.main-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ padding-top: 12px;
+ background-color: $color-bg-gray;
+ width: 100%;
+
+ .kpi-container {
+ margin-bottom: 12px;
+ }
+
+ .chart-container {
+ margin-bottom: 20px;
+ }
+
+ &.mobile {
+ .kpi-container,
+ .chart-container {
+ margin-bottom: 0;
+ }
+ }
+}
diff --git a/app/institutions/dashboard/index/template.hbs b/app/institutions/dashboard/index/template.hbs
new file mode 100644
index 00000000000..36145181db9
--- /dev/null
+++ b/app/institutions/dashboard/index/template.hbs
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/institutions/dashboard/preprints/controller.ts b/app/institutions/dashboard/preprints/controller.ts
new file mode 100644
index 00000000000..c51f5c42de4
--- /dev/null
+++ b/app/institutions/dashboard/preprints/controller.ts
@@ -0,0 +1,78 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+import Intl from 'ember-intl/services/intl';
+import { getSingleOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld';
+
+import { ResourceTypeFilterValue } from 'osf-components/components/search-page/component';
+import { ObjectListColumn } from '../-components/object-list/component';
+
+export default class InstitutionDashboardPreprints extends Controller {
+ @service intl!: Intl;
+
+ missingItemPlaceholder = this.intl.t('institutions.dashboard.object-list.table-items.missing-info');
+
+ columns: ObjectListColumn[] = [
+ { // Title
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.title'),
+ type: 'link',
+ getHref: searchResult => searchResult.indexCard.get('osfIdentifier'),
+ getLinkText: searchResult => searchResult.displayTitle,
+ },
+ { // Link
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.link'),
+ type: 'link',
+ getHref: searchResult => searchResult.indexCard.get('osfIdentifier'),
+ getLinkText: searchResult => searchResult.indexCard.get('osfGuid'),
+ },
+ { // Date created
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.created_date'),
+ getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateCreated']),
+ sortKey: 'dateCreated',
+ },
+ { // Date modified
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.modified_date'),
+ getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateModified']),
+ sortKey: 'dateModified',
+ },
+ { // DOI
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.doi'),
+ type: 'doi',
+ },
+ { // License
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.license'),
+ getValue: searchResult => searchResult.license?.name || this.missingItemPlaceholder,
+ },
+ { // Contributor name + permissions
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.contributor_name'),
+ type: 'contributors',
+ },
+ { // View count
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.view_count'),
+ getValue: searchResult => {
+ const metrics = searchResult.usageMetrics;
+ return metrics ? metrics.viewCount : this.missingItemPlaceholder;
+ },
+ sortKey: 'usage.viewCount',
+ sortParam: 'integer-value',
+ },
+ { // Download count
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.download_count'),
+ getValue: searchResult => {
+ const metrics = searchResult.usageMetrics;
+ return metrics ? metrics.downloadCount : this.missingItemPlaceholder;
+ },
+ sortKey: 'usage.downloadCount',
+ sortParam: 'integer-value',
+ },
+ ];
+
+ get defaultQueryOptions() {
+ const identifiers = this.model.institution.iris.join(',');
+ return {
+ cardSearchFilter: {
+ affiliation: [identifiers],
+ resourceType: ResourceTypeFilterValue.Preprints,
+ },
+ };
+ }
+}
diff --git a/app/institutions/dashboard/preprints/route.ts b/app/institutions/dashboard/preprints/route.ts
new file mode 100644
index 00000000000..b083dbe9831
--- /dev/null
+++ b/app/institutions/dashboard/preprints/route.ts
@@ -0,0 +1,4 @@
+import Route from '@ember/routing/route';
+
+export default class InstitutionsDashboardPreprintsRoute extends Route {
+}
diff --git a/app/institutions/dashboard/preprints/template.hbs b/app/institutions/dashboard/preprints/template.hbs
new file mode 100644
index 00000000000..3a7ffd7838a
--- /dev/null
+++ b/app/institutions/dashboard/preprints/template.hbs
@@ -0,0 +1,6 @@
+
diff --git a/app/institutions/dashboard/projects/controller.ts b/app/institutions/dashboard/projects/controller.ts
new file mode 100644
index 00000000000..22e27b6d25e
--- /dev/null
+++ b/app/institutions/dashboard/projects/controller.ts
@@ -0,0 +1,100 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+import Intl from 'ember-intl/services/intl';
+import { getSingleOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld';
+
+import humanFileSize from 'ember-osf-web/utils/human-file-size';
+import { ObjectListColumn } from '../-components/object-list/component';
+
+export default class InstitutionDashboardProjects extends Controller {
+ @service intl!: Intl;
+
+ missingItemPlaceholder = this.intl.t('institutions.dashboard.object-list.table-items.missing-info');
+
+ columns: ObjectListColumn[] = [
+ { // Title
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.title'),
+ type: 'link',
+ getHref: searchResult => searchResult.indexCard.get('osfIdentifier'),
+ getLinkText: searchResult => searchResult.displayTitle,
+ },
+ { // Link
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.link'),
+ type: 'link',
+ getHref: searchResult => searchResult.indexCard.get('osfIdentifier'),
+ getLinkText: searchResult => searchResult.indexCard.get('osfGuid'),
+ },
+ { // Date created
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.created_date'),
+ getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateCreated']),
+ sortKey: 'dateCreated',
+ },
+ { // Date modified
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.modified_date'),
+ getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateModified']),
+ sortKey: 'dateModified',
+ },
+ { // DOI
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.doi'),
+ type: 'doi',
+ },
+ { // Storage location
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.storage_location'),
+ getValue: searchResult => searchResult.storageRegion,
+ },
+ { // Total data stored
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.total_data_stored'),
+ getValue: searchResult => {
+ const byteCount = getSingleOsfmapValue(searchResult.resourceMetadata, ['storageByteCount']);
+ return byteCount ? humanFileSize(byteCount) : this.missingItemPlaceholder;
+ },
+ sortKey: 'storageByteCount',
+ sortParam: 'integer-value',
+ },
+ { // Contributor name + permissions
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.contributor_name'),
+ type: 'contributors',
+ },
+ { // View count
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.view_count'),
+ getValue: searchResult => {
+ const metrics = searchResult.usageMetrics;
+ return metrics ? metrics.viewCount : this.missingItemPlaceholder;
+ },
+ sortKey: 'usage.viewCount',
+ sortParam: 'integer-value',
+ },
+ { // Resource type
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.resource_nature'),
+ getValue: searchResult => searchResult.resourceNature || this.missingItemPlaceholder,
+ },
+ { // License
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.license'),
+ getValue: searchResult => searchResult.license?.name || this.missingItemPlaceholder,
+ },
+ { // addons associated
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.addons'),
+ getValue: searchResult => searchResult.configuredAddonNames.length ? searchResult.configuredAddonNames :
+ this.missingItemPlaceholder,
+ },
+ { // Funder name
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.funder_name'),
+ getValue: searchResult => {
+ if (!searchResult.funders) {
+ return this.missingItemPlaceholder;
+ }
+ return searchResult.funders.map((funder: { name: string }) => funder.name).join(', ');
+ },
+ },
+ ];
+
+ get defaultQueryOptions() {
+ const identifiers = this.model.institution.iris.join(',');
+ return {
+ cardSearchFilter: {
+ affiliation: [identifiers],
+ resourceType: 'Project',
+ },
+ };
+ }
+}
diff --git a/app/institutions/dashboard/projects/route.ts b/app/institutions/dashboard/projects/route.ts
new file mode 100644
index 00000000000..92028943388
--- /dev/null
+++ b/app/institutions/dashboard/projects/route.ts
@@ -0,0 +1,4 @@
+import Route from '@ember/routing/route';
+
+export default class InstitutionsDashboardRoute extends Route {
+}
diff --git a/app/institutions/dashboard/projects/template.hbs b/app/institutions/dashboard/projects/template.hbs
new file mode 100644
index 00000000000..64a49591bea
--- /dev/null
+++ b/app/institutions/dashboard/projects/template.hbs
@@ -0,0 +1,6 @@
+
diff --git a/app/institutions/dashboard/registrations/controller.ts b/app/institutions/dashboard/registrations/controller.ts
new file mode 100644
index 00000000000..d991172f29f
--- /dev/null
+++ b/app/institutions/dashboard/registrations/controller.ts
@@ -0,0 +1,98 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+import Intl from 'ember-intl/services/intl';
+import { getSingleOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld';
+
+import humanFileSize from 'ember-osf-web/utils/human-file-size';
+import { ObjectListColumn } from '../-components/object-list/component';
+
+export default class InstitutionDashboardRegistrations extends Controller {
+ @service intl!: Intl;
+
+ missingItemPlaceholder = this.intl.t('institutions.dashboard.object-list.table-items.missing-info');
+ columns: ObjectListColumn[] = [
+ { // Title
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.title'),
+ type: 'link',
+ getHref: searchResult => searchResult.indexCard.get('osfIdentifier'),
+ getLinkText: searchResult => searchResult.displayTitle,
+ },
+ { // Link
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.link'),
+ type: 'link',
+ getHref: searchResult => searchResult.indexCard.get('osfIdentifier'),
+ getLinkText: searchResult => searchResult.indexCard.get('osfGuid'),
+ },
+ { // Date created
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.created_date'),
+ getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateCreated']),
+ sortKey: 'dateCreated',
+ },
+ { // Date modified
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.modified_date'),
+ getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateModified']),
+ sortKey: 'dateModified',
+ },
+ { // DOI
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.doi'),
+ type: 'doi',
+ },
+ { // Storage location
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.storage_location'),
+ getValue: searchResult => searchResult.storageRegion,
+ },
+ { // Total data stored
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.total_data_stored'),
+ getValue: searchResult => {
+ const byteCount = getSingleOsfmapValue(searchResult.resourceMetadata, ['storageByteCount']);
+ return byteCount ? humanFileSize(byteCount) : this.missingItemPlaceholder;
+ },
+ sortKey: 'storageByteCount',
+ sortParam: 'integer-value',
+ },
+ { // Contributor name + permissions
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.contributor_name'),
+ type: 'contributors',
+ },
+ { // View count
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.view_count'),
+ getValue: searchResult => {
+ const metrics = searchResult.usageMetrics;
+ return metrics ? metrics.viewCount : this.missingItemPlaceholder;
+ },
+ sortKey: 'usage.viewCount',
+ sortParam: 'integer-value',
+ },
+ { // Resource type
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.resource_nature'),
+ getValue: searchResult => searchResult.resourceNature || this.missingItemPlaceholder,
+ },
+ { // License
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.license'),
+ getValue: searchResult => searchResult.license?.name || this.missingItemPlaceholder,
+ },
+ { // Funder name
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.funder_name'),
+ getValue: searchResult => {
+ if (!searchResult.funders) {
+ return this.missingItemPlaceholder;
+ }
+ return searchResult.funders.map((funder: { name: string }) => funder.name).join(', ');
+ },
+ },
+ { // schema
+ name: this.intl.t('institutions.dashboard.object-list.table-headers.registration_schema'),
+ getValue: searchResult => searchResult.registrationTemplate,
+ },
+ ];
+
+ get defaultQueryOptions() {
+ const identifiers = this.model.institution.iris.join(',');
+ return {
+ cardSearchFilter: {
+ affiliation: [identifiers],
+ resourceType: 'Registration',
+ },
+ };
+ }
+}
diff --git a/app/institutions/dashboard/registrations/route.ts b/app/institutions/dashboard/registrations/route.ts
new file mode 100644
index 00000000000..460700e098e
--- /dev/null
+++ b/app/institutions/dashboard/registrations/route.ts
@@ -0,0 +1,4 @@
+import Route from '@ember/routing/route';
+
+export default class InstitutionsDashboardRegistrationsRoute extends Route {
+}
diff --git a/app/institutions/dashboard/registrations/template.hbs b/app/institutions/dashboard/registrations/template.hbs
new file mode 100644
index 00000000000..afbe7f88a6b
--- /dev/null
+++ b/app/institutions/dashboard/registrations/template.hbs
@@ -0,0 +1,6 @@
+
diff --git a/app/institutions/dashboard/route.ts b/app/institutions/dashboard/route.ts
index e03dc35352d..16ab7b53501 100644
--- a/app/institutions/dashboard/route.ts
+++ b/app/institutions/dashboard/route.ts
@@ -1,16 +1,14 @@
import Route from '@ember/routing/route';
import RouterService from '@ember/routing/router-service';
import { inject as service } from '@ember/service';
-import { waitFor } from '@ember/test-waiters';
import Store from '@ember-data/store';
-import { task } from 'ember-concurrency';
-import { taskFor } from 'ember-concurrency-ts';
import InstitutionModel from 'ember-osf-web/models/institution';
import InstitutionDepartmentModel from 'ember-osf-web/models/institution-department';
import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric';
import { QueryHasManyResult } from 'ember-osf-web/models/osf-model';
import captureException from 'ember-osf-web/utils/capture-exception';
+import { notFoundURL } from 'ember-osf-web/utils/clean-url';
export interface InstitutionsDashboardModel {
institution: InstitutionModel;
@@ -21,16 +19,16 @@ export default class InstitutionsDashboardRoute extends Route {
@service router!: RouterService;
@service store!: Store;
- @task
- @waitFor
- async modelTask(institutionId: string) {
+ // eslint-disable-next-line camelcase
+ async model(params: { institution_id: string }) {
try {
- const institution = await this.store.findRecord('institution', institutionId, {
+ const institution = await this.store.findRecord('institution', params.institution_id, {
adapterOptions: {
include: ['summary_metrics'],
},
});
const departmentMetrics = await institution.queryHasMany('departmentMetrics');
+
const summaryMetrics = await institution.summaryMetrics;
const userMetricInfo: QueryHasManyResult = await institution.queryHasMany(
'userMetrics',
@@ -45,15 +43,8 @@ export default class InstitutionsDashboardRoute extends Route {
};
} catch (error) {
captureException(error);
- this.transitionTo('not-found', this.router.get('currentURL').slice(1));
+ this.transitionTo('not-found', notFoundURL(window.location.pathname));
return undefined;
}
}
-
- // eslint-disable-next-line camelcase
- model(params: { institution_id: string }) {
- return {
- taskInstance: taskFor(this.modelTask).perform(params.institution_id),
- };
- }
}
diff --git a/app/institutions/dashboard/styles.scss b/app/institutions/dashboard/styles.scss
deleted file mode 100644
index 3ee33c804b3..00000000000
--- a/app/institutions/dashboard/styles.scss
+++ /dev/null
@@ -1,100 +0,0 @@
-.banner {
- padding: 15px 0;
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- div {
- color: #263947;
- }
-}
-
-.dashboard-wrapper {
- display: flex;
-}
-
-.table-wrapper {
- padding-right: 15px;
- flex-grow: 1;
-}
-
-.panel-wrapper {
- padding-left: 15px;
- text-align: right;
-}
-
-.csv-button {
- display: inline-block;
- width: 40px;
- height: 40px;
- background: #fff;
- padding: 7px 0;
- border: 1px solid #ddd;
- margin-bottom: 15px;
- text-align: center;
-
- &:active,
- &:hover {
- background: #15a5eb;
- border-color: #15a5eb;
- }
-}
-
-.sso-users-connected {
- :global(.panel-body) {
- h3 {
- margin: 0;
- font-size: 6vw;
- font-weight: bold;
- }
- }
-}
-
-.projects {
- :global(.panel-body) {
- h3 {
- margin: 0 0 10px;
- font-size: 3.75vw;
- font-weight: bold;
- }
-
- p {
- margin: 0;
- font-size: 16px;
- }
- }
-}
-
-// Extra large devices (large desktops, 1200px and up)
-@media (min-width: 1200px) {
- .sso-users-connected {
- :global(.panel-body) {
- h3 {
- font-size: 96px;
- }
- }
- }
-
- .projects {
- :global(.panel-body) {
- h3 {
- font-size: 72px;
- }
- }
- }
-}
-
-@media (max-width: 767px) {
- .dashboard-wrapper {
- flex-wrap: wrap-reverse;
- }
-
- .panel-wrapper {
- padding-left: 0;
- width: 100%;
- }
-
- .table-wrapper {
- padding-right: 0;
- }
-}
diff --git a/app/institutions/dashboard/template.hbs b/app/institutions/dashboard/template.hbs
index fd5e7b6a496..1980fe88290 100644
--- a/app/institutions/dashboard/template.hbs
+++ b/app/institutions/dashboard/template.hbs
@@ -1,59 +1,2 @@
-{{page-title (t 'institutions.dashboard.title' institutionName=this.institution.unsafeName)}}
-
-
-
-
- {{t 'institutions.dashboard.last_update'}}
-
-
-
-
-
-
-
- {{#if this.csvHref}}
-
-
-
- {{/if}}
-
- {{#if this.summaryMetrics}}
- {{this.summaryMetrics.userCount}}
- {{else}}
- {{t 'institutions.dashboard.empty'}}
- {{/if}}
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+{{page-title (t 'institutions.dashboard.title' institutionName=this.model.institution.unsafeName)}}
+{{outlet}}
diff --git a/app/institutions/dashboard/users/styles.scss b/app/institutions/dashboard/users/styles.scss
new file mode 100644
index 00000000000..93ca0026725
--- /dev/null
+++ b/app/institutions/dashboard/users/styles.scss
@@ -0,0 +1,3 @@
+.panel-wrapper {
+ margin-top: 12px;
+}
diff --git a/app/institutions/dashboard/users/template.hbs b/app/institutions/dashboard/users/template.hbs
new file mode 100644
index 00000000000..9bcdf36be8f
--- /dev/null
+++ b/app/institutions/dashboard/users/template.hbs
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/app/models/index-card.ts b/app/models/index-card.ts
index 3f64d81888a..15fcd514373 100644
--- a/app/models/index-card.ts
+++ b/app/models/index-card.ts
@@ -16,6 +16,26 @@ export interface LanguageText {
'@value': string;
}
+export enum OsfmapResourceTypes {
+ Project = 'Project',
+ ProjectComponent = 'ProjectComponent',
+ Registration = 'Registration',
+ RegistrationComponent = 'RegistrationComponent',
+ Preprint = 'Preprint',
+ File = 'File',
+ Person = 'Person',
+ Agent = 'Agent',
+ Organization = 'Organization',
+ Concept = 'Concept',
+ ConceptScheme = 'Concept:Scheme',
+}
+
+export enum AttributionRoleIris {
+ Admin = 'osf:admin-contributor',
+ Write = 'osf:write-contributor',
+ Read = 'osf:readonly-contributor',
+}
+
export default class IndexCardModel extends Model {
@service intl!: IntlService;
@@ -36,7 +56,8 @@ export default class IndexCardModel extends Model {
}
get osfModelType() {
- const types = this.resourceMetadata.resourceType.map( (item: any) => item['@id']);
+ const types: OsfmapResourceTypes = this.resourceMetadata.resourceType
+ .map((item: Record<'@id', OsfmapResourceTypes>) => item['@id']);
if (types.includes('Project') || types.includes('ProjectComponent')) {
return 'node';
} else if (types.includes('Registration') || types.includes('RegistrationComponent')) {
@@ -74,7 +95,7 @@ export default class IndexCardModel extends Model {
async getOsfModel(options?: object) {
const identifier = this.resourceIdentifier;
if (identifier && this.osfModelType) {
- const guid = this.guidFromIdentifierList(identifier);
+ const guid = this.osfGuid;
if (guid) {
const osfModel = await this.store.findRecord(this.osfModelType, guid, options);
this.osfModel = osfModel;
@@ -82,16 +103,16 @@ export default class IndexCardModel extends Model {
}
}
- guidFromIdentifierList() {
- for (const iri of this.resourceIdentifier) {
- if (iri && iri.startsWith(osfUrl)) {
- const pathSegments = iri.slice(osfUrl.length).split('/').filter(Boolean);
- if (pathSegments.length === 1) {
- return pathSegments[0]; // one path segment; looks like osf-id
- }
- }
+ get osfIdentifier() {
+ return this.resourceIdentifier.find(iri => iri.startsWith(osfUrl)) || '';
+ }
+
+ get osfGuid() {
+ const pathSegments = this.osfIdentifier.slice(osfUrl.length).split('/').filter(Boolean);
+ if (pathSegments.length === 1) {
+ return pathSegments[0]; // one path segment; looks like osf-id
}
- return null;
+ return '';
}
}
diff --git a/app/models/institution-summary-metric.ts b/app/models/institution-summary-metric.ts
index ac3675fafe6..9e84ede5b0d 100644
--- a/app/models/institution-summary-metric.ts
+++ b/app/models/institution-summary-metric.ts
@@ -1,10 +1,23 @@
import { attr } from '@ember-data/model';
+import humanFileSize from 'ember-osf-web/utils/human-file-size';
import OsfModel from './osf-model';
export default class InstitutionSummaryMetricModel extends OsfModel {
@attr('number') publicProjectCount!: number;
@attr('number') privateProjectCount!: number;
@attr('number') userCount!: number;
+ @attr('number') publicRegistrationCount!: number;
+ @attr('number') publishedPreprintCount!: number;
+ @attr('number') embargoedRegistrationCount!: number;
+ @attr('number') storageByteCount!: number;
+ @attr('number') publicFileCount!: number;
+ @attr('number') monthlyLoggedInUserCount!: number;
+ @attr('number') monthlyActiveUserCount!: number;
+
+
+ get convertedStorageCount(): string {
+ return humanFileSize(parseFloat(this.storageByteCount.toFixed(1)));
+ }
}
declare module 'ember-data/types/registries/model' {
diff --git a/app/models/institution-user.ts b/app/models/institution-user.ts
index 0ce79fde66a..2fb19f778d3 100644
--- a/app/models/institution-user.ts
+++ b/app/models/institution-user.ts
@@ -1,6 +1,6 @@
import { attr, belongsTo, AsyncBelongsTo } from '@ember-data/model';
-
import UserModel from 'ember-osf-web/models/user';
+import humanFileSize from 'ember-osf-web/utils/human-file-size';
import OsfModel from './osf-model';
@@ -9,6 +9,16 @@ export default class InstitutionUserModel extends OsfModel {
@attr('fixstring') department?: string;
@attr('number') publicProjects!: number;
@attr('number') privateProjects!: number;
+ @attr('number') publicRegistrationCount!: number;
+ @attr('number') embargoedRegistrationCount!: number;
+ @attr('number') publishedPreprintCount!: number;
+ @attr('number') publicFileCount!: number;
+ @attr('number') storageByteCount!: number;
+ @attr('number') totalObjectCount!: number;
+ @attr('string') monthLastLogin!: string; // YYYY-MM
+ @attr('string') monthLastActive!: string; // YYYY-MM
+ @attr('string') accountCreationDate!: string; // YYYY-MM
+ @attr('fixstring') orcidId?: string;
@belongsTo('user', { async: true })
user!: AsyncBelongsTo & UserModel;
@@ -16,6 +26,10 @@ export default class InstitutionUserModel extends OsfModel {
get userGuid() {
return (this as InstitutionUserModel).belongsTo('user').id();
}
+
+ get userDataUsage() {
+ return humanFileSize(this.storageByteCount);
+ }
}
declare module 'ember-data/types/registries/model' {
diff --git a/app/models/institution.ts b/app/models/institution.ts
index 9e2fa60ce7a..9b0432faf02 100644
--- a/app/models/institution.ts
+++ b/app/models/institution.ts
@@ -29,6 +29,7 @@ export default class InstitutionModel extends OsfModel {
@attr('string') authUrl!: string;
@attr('object') assets?: Assets;
@attr('boolean', { defaultValue: false }) currentUserIsAdmin!: boolean;
+ @attr('fixstring') linkToExternalReportsArchive?: string; // only serialized when currentUserIsAdmin
@attr('date') lastUpdated!: Date;
@attr('fixstring') rorIri!: string;
// identifier_domain in the admin app
diff --git a/app/models/search-result.ts b/app/models/search-result.ts
index 5d02d1c9c63..45a41c9c43c 100644
--- a/app/models/search-result.ts
+++ b/app/models/search-result.ts
@@ -2,6 +2,7 @@ import Model, { attr, belongsTo } from '@ember-data/model';
import { inject as service } from '@ember/service';
import { htmlSafe } from '@ember/template';
import IntlService from 'ember-intl/services/intl';
+import { getOsfmapValues, getSingleOsfmapObject, getSingleOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld';
import { languageFromLanguageCode } from 'osf-components/components/file-metadata-manager/component';
import IndexCardModel from './index-card';
@@ -20,6 +21,17 @@ export interface TextMatchEvidence {
osfmapPropertyPath: string[];
}
+export const CardLabelTranslationKeys = {
+ project: 'osf-components.search-result-card.project',
+ project_component: 'osf-components.search-result-card.project_component',
+ registration: 'osf-components.search-result-card.registration',
+ registration_component: 'osf-components.search-result-card.registration_component',
+ preprint: 'osf-components.search-result-card.preprint',
+ file: 'osf-components.search-result-card.file',
+ user: 'osf-components.search-result-card.user',
+ unknown: 'osf-components.search-result-card.unknown',
+};
+
export default class SearchResultModel extends Model {
@service intl!: IntlService;
@@ -51,22 +63,22 @@ export default class SearchResultModel extends Model {
get displayTitle() {
if (this.resourceType === 'user') {
- return this.resourceMetadata['name'][0]['@value'];
+ return getSingleOsfmapValue(this.resourceMetadata, ['name']);
} else if (this.resourceType === 'file') {
- return this.resourceMetadata['fileName'][0]['@value'];
+ return getSingleOsfmapValue(this.resourceMetadata, ['fileName']);
}
- return this.resourceMetadata['title']?.[0]['@value'];
+ return getSingleOsfmapValue(this.resourceMetadata, ['title']);
}
get fileTitle() {
if (this.resourceType === 'file') {
- return this.resourceMetadata.title?.[0]['@value'];
+ return getSingleOsfmapValue(this.resourceMetadata, ['title']);
}
return null;
}
get description() {
- return this.resourceMetadata.description?.[0]?.['@value'];
+ return getSingleOsfmapValue(this.resourceMetadata, ['description']);
}
get absoluteUrl() {
@@ -75,19 +87,18 @@ export default class SearchResultModel extends Model {
// returns list of affilated institutions for users
// returns list of contributors for osf objects
- // returns list of affiliated institutions for osf users
get affiliatedEntities() {
if (this.resourceType === 'user') {
if (this.resourceMetadata.affiliation) {
return this.resourceMetadata.affiliation.map((item: any) =>
- ({ name: item.name[0]['@value'], absoluteUrl: item['@id'] }));
+ ({ name: getSingleOsfmapValue(item, ['name']), absoluteUrl: item['@id'] }));
}
} else if (this.resourceMetadata.creator) {
return this.resourceMetadata.creator?.map((item: any) =>
- ({ name: item.name[0]['@value'], absoluteUrl: item['@id'] }));
+ ({ name: getSingleOsfmapValue(item, ['name']), absoluteUrl: item['@id'] }));
} else if (this.isContainedBy?.[0]?.creator) {
return this.isContainedBy[0].creator.map((item: any) =>
- ({ name: item.name?.[0]?.['@value'], absoluteUrl: item['@id'] }));
+ ({ name: getSingleOsfmapValue(item, ['name']), absoluteUrl: item['@id'] }));
}
}
@@ -100,22 +111,22 @@ export default class SearchResultModel extends Model {
return [
{
label: this.intl.t('osf-components.search-result-card.date_registered'),
- date: this.resourceMetadata.dateCreated?.[0]['@value'],
+ date: getSingleOsfmapValue(this.resourceMetadata, ['dateCreated']),
},
{
label: this.intl.t('osf-components.search-result-card.date_modified'),
- date: this.resourceMetadata.dateModified?.[0]['@value'],
+ date: getSingleOsfmapValue(this.resourceMetadata, ['dateModified']),
},
];
default:
return [
{
label: this.intl.t('osf-components.search-result-card.date_created'),
- date: this.resourceMetadata.dateCreated?.[0]['@value'],
+ date: getSingleOsfmapValue(this.resourceMetadata, ['dateCreated']),
},
{
label: this.intl.t('osf-components.search-result-card.date_modified'),
- date: this.resourceMetadata.dateModified?.[0]['@value'],
+ date: getSingleOsfmapValue(this.resourceMetadata, ['dateModified']),
},
];
}
@@ -153,8 +164,8 @@ export default class SearchResultModel extends Model {
const isPartOfCollection = this.resourceMetadata.isPartOfCollection;
if (isPartOfCollection) {
return {
- title: this.resourceMetadata.isPartOfCollection?.[0]?.title?.[0]?.['@value'],
- absoluteUrl: this.resourceMetadata.isPartOfCollection?.[0]?.['@id'],
+ title: getSingleOsfmapValue(this.resourceMetadata, ['isPartOfCollection', 'title']),
+ absoluteUrl: getSingleOsfmapValue(this.resourceMetadata, ['isPartOfCollection']),
};
}
return null;
@@ -162,7 +173,8 @@ export default class SearchResultModel extends Model {
get languageFromCode() {
if (this.resourceMetadata.language) {
- return languageFromLanguageCode(this.resourceMetadata.language[0]['@value']);
+ const language = getSingleOsfmapValue(this.resourceMetadata, ['language']);
+ return languageFromLanguageCode(language);
}
return null;
}
@@ -170,8 +182,8 @@ export default class SearchResultModel extends Model {
get funders() {
if (this.resourceMetadata.funder) {
return this.resourceMetadata.funder.map( (item: any) => ({
- name: item.name[0]['@value'],
- identifier: item.identifier?.[0]['@value'],
+ name: getSingleOsfmapValue(item, ['name']),
+ identifier: getSingleOsfmapValue(item, ['identifier']),
}));
}
return null;
@@ -180,8 +192,8 @@ export default class SearchResultModel extends Model {
get nodeFunders() {
if (this.resourceMetadata.isContainedBy?.[0]?.funder) {
return this.resourceMetadata.isContainedBy[0].funder.map( (item: any) => ({
- name: item.name[0]['@value'],
- identifier: item.identifier?.[0]['@value'],
+ name: getSingleOsfmapValue(item, ['name']),
+ identifier: getSingleOsfmapValue(item, ['identifier']),
}));
}
return null;
@@ -190,8 +202,8 @@ export default class SearchResultModel extends Model {
get provider() {
if (this.resourceMetadata.publisher) {
return {
- name: this.resourceMetadata.publisher[0]?.name?.[0]['@value'],
- identifier: this.resourceMetadata.publisher[0]['@id'],
+ name: getSingleOsfmapValue(this.resourceMetadata, ['publisher', 'name']),
+ identifier: getSingleOsfmapValue(this.resourceMetadata, ['publisher']),
};
}
return null;
@@ -204,8 +216,8 @@ export default class SearchResultModel extends Model {
get license() {
if (this.resourceMetadata.rights) {
return {
- name: this.resourceMetadata.rights?.[0]?.name?.[0]?.['@value'],
- identifier: this.resourceMetadata.rights?.[0]?.['@id'],
+ name: getSingleOsfmapValue(this.resourceMetadata, ['rights', 'name']),
+ identifier: getSingleOsfmapValue(this.resourceMetadata, ['rights']),
};
}
return null;
@@ -214,9 +226,9 @@ export default class SearchResultModel extends Model {
get nodeLicense() {
if (this.resourceMetadata.isContainedBy?.[0]?.rights) {
return {
- name: this.resourceMetadata.isContainedBy[0].rights?.[0]?.name?.[0]?.['@value'],
- identifier: this.resourceMetadata.rights?.[0]?.['@id'] ||
- this.resourceMetadata.isContainedBy[0].rights[0]?.['@id'],
+ name: getSingleOsfmapValue(this.resourceMetadata, ['isContainedBy', 'rights', 'name']),
+ identifier: getSingleOsfmapValue(this.resourceMetadata, ['rights']) ||
+ getSingleOsfmapValue(this.resourceMetadata, ['isContainedBy', 'rights']),
};
}
return null;
@@ -242,6 +254,10 @@ export default class SearchResultModel extends Model {
return 'unknown';
}
+ get intlResourceType() {
+ return this.intl.t(CardLabelTranslationKeys[this.resourceType]);
+ }
+
get orcids() {
if (this.resourceMetadata.identifier) {
const orcids = this.resourceMetadata.identifier.filter(
@@ -253,7 +269,7 @@ export default class SearchResultModel extends Model {
}
get resourceNature() {
- return this.resourceMetadata.resourceNature?.[0]?.displayLabel?.[0]?.['@value'];
+ return getSingleOsfmapValue(this.resourceMetadata, ['resourceNature','displayLabel']);
}
get hasDataResource() {
@@ -277,12 +293,32 @@ export default class SearchResultModel extends Model {
}
get registrationTemplate() {
- return this.resourceMetadata.conformsTo?.[0]?.title?.[0]?.['@value'];
+ return getSingleOsfmapValue(this.resourceMetadata, ['conformsTo', 'title']);
}
get isWithdrawn() {
return this.resourceMetadata.dateWithdrawn || this.resourceMetadata['https://osf.io/vocab/2022/withdrawal'];
}
+
+ get configuredAddonNames() {
+ return getOsfmapValues(this.resourceMetadata, ['hasOsfAddon', 'prefLabel']);
+ }
+
+ get storageRegion() {
+ return getSingleOsfmapValue(this.resourceMetadata, ['storageRegion', 'prefLabel']);
+ }
+
+ get usageMetrics() {
+ const usage = getSingleOsfmapObject(this.resourceMetadata, ['usage']);
+ if (!usage) {
+ return null;
+ }
+ return {
+ period: getSingleOsfmapValue(usage, ['temporalCoverage']),
+ viewCount: getSingleOsfmapValue(usage, ['viewCount']),
+ downloadCount: getSingleOsfmapValue(usage, ['downloadCount']),
+ };
+ }
}
declare module 'ember-data/types/registries/model' {
diff --git a/app/packages/osfmap/jsonld.ts b/app/packages/osfmap/jsonld.ts
new file mode 100644
index 00000000000..9711ea177ca
--- /dev/null
+++ b/app/packages/osfmap/jsonld.ts
@@ -0,0 +1,37 @@
+export function *iterOsfmapObjects(osfmapObject: any, propertyPath: string[]): IterableIterator {
+ const [property, ...remainingPath] = propertyPath;
+ const innerObjArray = osfmapObject[property] || [];
+ if (remainingPath.length) {
+ for (const innerObj of innerObjArray) {
+ yield* iterOsfmapObjects(innerObj, remainingPath);
+ }
+ } else {
+ yield* innerObjArray;
+ }
+}
+
+export function *iterOsfmapValues(osfmapObject: any, propertyPath: string[]): IterableIterator {
+ for (const obj of iterOsfmapObjects(osfmapObject, propertyPath)) {
+ yield (Object.hasOwn(obj, '@id') ? obj['@id'] : obj['@value']);
+ }
+}
+
+export function getOsfmapValues(osfmapObject: any, propertyPath: string[]) {
+ return Array.from(iterOsfmapValues(osfmapObject, propertyPath));
+}
+
+export function getSingleOsfmapValue(osfmapObject: any, propertyPath: string[]) {
+ return iterOsfmapValues(osfmapObject, propertyPath).next().value;
+}
+
+export function getOsfmapObjects(osfmapObject: any, propertyPath: string[]) {
+ return Array.from(iterOsfmapObjects(osfmapObject, propertyPath));
+}
+
+export function getSingleOsfmapObject(osfmapObject: any, propertyPath: string[]) {
+ return iterOsfmapObjects(osfmapObject, propertyPath).next().value;
+}
+
+export function hasOsfmapValue(osfmapObject: any, propertyPath: string[], expectedValue: any) {
+ return Array.from(iterOsfmapValues(osfmapObject, propertyPath)).some(value => value === expectedValue);
+}
diff --git a/app/router.ts b/app/router.ts
index 02a2f8aa6c3..be2416a6e37 100644
--- a/app/router.ts
+++ b/app/router.ts
@@ -23,7 +23,12 @@ Router.map(function() {
this.route('search');
this.route('institutions', function() {
this.route('discover', { path: '/:institution_id' });
- this.route('dashboard', { path: '/:institution_id/dashboard' });
+ this.route('dashboard', { path: '/:institution_id/dashboard' }, function() {
+ this.route('projects');
+ this.route('registrations');
+ this.route('preprints');
+ this.route('users');
+ });
});
this.route('preprints', function() {
diff --git a/app/styles/_components.scss b/app/styles/_components.scss
index 7aa123fa52c..0e1db0303ad 100644
--- a/app/styles/_components.scss
+++ b/app/styles/_components.scss
@@ -902,3 +902,38 @@ button.nav-user-dropdown {
.logoutLink {
cursor: pointer;
}
+
+@mixin tab-list {
+ margin-bottom: 10px;
+ border-bottom: 1px solid $color-border-gray;
+ box-sizing: border-box;
+ color: $color-text-black;
+ display: block;
+ line-height: 20px;
+ list-style-image: none;
+ list-style-position: outside;
+ list-style-type: none;
+ height: 41px;
+ padding: 0;
+
+ li {
+ display: block;
+ position: relative;
+ margin-bottom: -1px;
+ float: left;
+ height: 41px;
+ padding: 10px 15px;
+ }
+
+ li.ember-tabs__tab--selected {
+ background-color: $bg-light;
+ border-bottom: 2px solid $color-blue;
+ }
+
+ li:hover {
+ border-color: transparent;
+ text-decoration: none;
+ background-color: $bg-light;
+ color: var(--primary-color);
+ }
+}
diff --git a/app/styles/_variables.scss b/app/styles/_variables.scss
index adcc9ab19ff..06441451109 100644
--- a/app/styles/_variables.scss
+++ b/app/styles/_variables.scss
@@ -130,6 +130,7 @@ $color-grey: #333;
$color-filter-bg: #a4b3bd;
$color-red: #f00;
$color-green: #090;
+$color-green-light: #90ee90;
$color-yellow: #ff0;
$color-turquoise: rgb(64, 224, 211);
$color-purple: rgb(154, 0, 192);
diff --git a/ember-cli-build.js b/ember-cli-build.js
index ebbc8fe6086..b6b9e0abb1e 100644
--- a/ember-cli-build.js
+++ b/ember-cli-build.js
@@ -32,7 +32,9 @@ module.exports = function(defaults) {
],
},
'ember-composable-helpers': {
- only: ['compose', 'contains', 'flatten', 'includes', 'range', 'queue', 'map-by', 'without', 'find-by'],
+ only: [
+ 'call', 'compose', 'contains', 'find-by', 'flatten', 'includes', 'map-by', 'queue', 'range', 'without',
+ ],
},
fingerprint: {
enabled: true,
diff --git a/lib/app-components/addon/components/project-contributors/list/item/template.hbs b/lib/app-components/addon/components/project-contributors/list/item/template.hbs
index d7b13fa1c58..4211d83e60b 100644
--- a/lib/app-components/addon/components/project-contributors/list/item/template.hbs
+++ b/lib/app-components/addon/components/project-contributors/list/item/template.hbs
@@ -48,11 +48,11 @@
@selected={{@contributor.permission}}
as |option|
>
- {{t (concat 'app_components.project_contributors.list.item.permissions.' option)}}
+ {{t (concat 'general.permissions.' option)}}
{{else}}
- {{t (concat 'app_components.project_contributors.list.item.permissions.' @contributor.permission)}}
+ {{t (concat 'general.permissions.' @contributor.permission)}}
{{/if}}
diff --git a/lib/osf-components/addon/components/adjustable-paginator/component.ts b/lib/osf-components/addon/components/adjustable-paginator/component.ts
new file mode 100644
index 00000000000..fedf34a6bad
--- /dev/null
+++ b/lib/osf-components/addon/components/adjustable-paginator/component.ts
@@ -0,0 +1,92 @@
+import { classNames, tagName } from '@ember-decorators/component';
+import Component from '@ember/component';
+import { action, computed } from '@ember/object';
+import { gt } from '@ember/object/computed';
+import { layout } from 'ember-osf-web/decorators/component';
+import styles from './styles';
+import template from './template';
+
+@layout(template, styles)
+@tagName('span')
+@classNames('sort-group')
+export default class AdjustablePaginator extends Component {
+ page?: number;
+ maxPage?: number;
+ totalCount?: number;
+ previousPage?: () => unknown;
+ nextPage?: () => unknown;
+ selectedPageSize = 10;
+
+ defaultPageSizeOptions = [10, 25, 50, 100];
+
+ @computed('totalCount', 'defaultPageSizeOptions')
+ get pageSizeOptions(): number[] {
+ if (this.totalCount) {
+ // Filter options smaller or equal to totalCount and include the next higher option
+ const filteredOptions = this.defaultPageSizeOptions.filter(option => option <= this.totalCount);
+
+ // Find the first option greater than totalCount and include it as well
+ const nextHigherOption = this.defaultPageSizeOptions.find(option => option > this.totalCount);
+
+ if (nextHigherOption) {
+ filteredOptions.push(nextHigherOption); // Include the next higher option
+ }
+
+ return filteredOptions;
+ }
+
+ return this.defaultPageSizeOptions;
+ }
+
+ @computed('page', 'maxPage')
+ get hasNext(): boolean {
+ return Boolean(this.page && this.maxPage && this.page < this.maxPage);
+ }
+ @computed('page')
+ get prevPage(): number {
+ return this.page - 1;
+ }
+
+ @computed('page')
+ get nextPage2(): number {
+ return this.page + 1;
+ }
+
+ @computed('maxPage')
+ get finalPage(): number {
+ return this.maxPage + 1;
+ }
+
+ @gt('page', 1) hasPrev!: boolean;
+
+ @gt('maxPage', 1) hasMultiplePages!: boolean;
+
+ @action
+ _previous() {
+ if (this.previousPage) {
+ this.previousPage();
+ }
+ }
+
+ @action
+ _next() {
+ if (this.nextPage) {
+ this.nextPage();
+ }
+ }
+
+ @action
+ onPageSizeChange(value: int) {
+ this.set('pageSize', value);
+ if (this.doReload) {
+ this.doReload();
+ }
+ }
+
+ @action
+ setPage(page: number) {
+ if (this.doReload) {
+ this.doReload(page);
+ }
+ }
+}
diff --git a/lib/osf-components/addon/components/adjustable-paginator/styles.scss b/lib/osf-components/addon/components/adjustable-paginator/styles.scss
new file mode 100644
index 00000000000..066079586da
--- /dev/null
+++ b/lib/osf-components/addon/components/adjustable-paginator/styles.scss
@@ -0,0 +1,76 @@
+.paginator__control {
+ display: inline-flex;
+ align-items: center;
+ margin: 0;
+
+ &:first-of-type {
+ padding-left: 0;
+ }
+}
+
+.paginator__button,
+.paginator__select {
+ background-color: $color-bg-white;
+ border: $border-light;
+ color: $color-text-blue-dark;
+ padding: 7px 16px;
+ font-size: 14px;
+ border-radius: $radius;
+ cursor: pointer;
+ transition: background-color 0.3s ease, border-color 0.3s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 40px; // Setting height to ensure the same for both buttons and select
+ box-sizing: border-box; // Ensures padding and borders are included in the height
+
+ &:hover {
+ background-color: $color-bg-gray-light;
+ border-color: $color-border-gray-dark;
+ }
+
+ &:disabled {
+ color: $color-text-blue-dark;
+ background-color: $bg-light;
+ cursor: not-allowed;
+ border-color: $color-border-gray;
+ }
+
+ &:focus {
+ outline: none;
+ border-color: $secondary-blue;
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
+ }
+}
+
+.paginator__button--current {
+ background-color: $color-bg-gray-lighter;
+ border: 1px solid $color-osf-primary;
+ color: $color-text-blue-dark;
+ font-weight: bold;
+ cursor: default;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Special styling for next/previous buttons */
+.paginator__button--prev,
+.paginator__button--next {
+ font-weight: bolder;
+ padding: 5px 12px;
+}
+
+.paginator__select {
+ max-width: 160px;
+ margin-top: -5px;
+ font-weight: bolder;
+}
+
+.paginator__ellipsis {
+ color: $color-text-blue-dark;
+ margin: 0 8px;
+ font-size: 14px;
+ display: inline-flex;
+ align-items: center;
+}
diff --git a/lib/osf-components/addon/components/adjustable-paginator/template.hbs b/lib/osf-components/addon/components/adjustable-paginator/template.hbs
new file mode 100644
index 00000000000..4d04950503e
--- /dev/null
+++ b/lib/osf-components/addon/components/adjustable-paginator/template.hbs
@@ -0,0 +1,140 @@
+
+
+
+ {{t 'paginator.itemsPerPage'}}
+
+ {{#each this.pageSizeOptions as |page_size_option|}}
+
+ {{page_size_option}}
+
+ {{/each}}
+
+
+
+
+
+
+{{#if this.hasMultiplePages}}
+
+
+ {{t 'paginator.previous'}}
+
+
+
+ {{!-- Always show the first page button --}}
+
+
+ {{t 'paginator.first'}}
+
+
+
+ {{#if (lte this.maxPage 3)}}
+ {{!-- If fewer than 3 pages, show all pages --}}
+ {{#each (range 2 3) as |page|}}
+
+
+ {{page}}
+
+
+ {{/each}}
+ {{else}}
+ {{#if (gt this.prevPage 2)}}
+
+
+ ...
+
+
+ {{/if}}
+
+ {{!-- Conditionally show previous and current pages --}}
+ {{#if (not (eq this.prevPage 1))}}
+ {{#if (not (eq this.page 1))}}
+
+
+ {{this.prevPage}}
+
+
+ {{/if}}
+ {{/if}}
+
+ {{#if (not (eq this.page 1))}}
+
+
+ {{this.page}}
+
+
+ {{/if}}
+
+ {{!-- Show nextPage only if it differs from maxPage --}}
+ {{#if (and this.hasNext (not (eq this.nextPage2 this.maxPage)))}}
+
+
+ {{this.nextPage2}}
+
+
+ {{/if}}
+
+ {{#if (not (gte this.nextPage2 this.maxPage))}}
+ {{#if (not (and (eq this.page 3) (eq this.maxPage 5))) }}
+
+
+ ...
+
+
+ {{/if}}
+ {{/if}}
+
+ {{!-- Always show the maxPage button --}}
+ {{#if (not (eq this.page this.maxPage))}}
+
+
+ {{this.maxPage}}
+
+
+ {{/if}}
+ {{/if}}
+
+
+
+ {{t 'paginator.next'}}
+
+
+{{/if}}
diff --git a/lib/osf-components/addon/components/contributors/add-unregistered-modal/template.hbs b/lib/osf-components/addon/components/contributors/add-unregistered-modal/template.hbs
index a64301b3c5c..be16b23125c 100644
--- a/lib/osf-components/addon/components/contributors/add-unregistered-modal/template.hbs
+++ b/lib/osf-components/addon/components/contributors/add-unregistered-modal/template.hbs
@@ -42,7 +42,7 @@
data-test-select-permission
as |permission|
>
- {{t (concat 'osf-components.contributors.permissions.' permission)}}
+ {{t (concat 'general.permissions.' permission)}}
{{#let (unique-id 'citation-checkbox') as |id|}}
diff --git a/lib/osf-components/addon/components/contributors/card/editable/template.hbs b/lib/osf-components/addon/components/contributors/card/editable/template.hbs
index fbe8de0c898..c26e7f2985c 100644
--- a/lib/osf-components/addon/components/contributors/card/editable/template.hbs
+++ b/lib/osf-components/addon/components/contributors/card/editable/template.hbs
@@ -49,7 +49,7 @@
as |option|
>
- {{t (concat 'osf-components.contributors.permissions.' option)}}
+ {{t (concat 'general.permissions.' option)}}
diff --git a/lib/osf-components/addon/components/contributors/card/readonly/template.hbs b/lib/osf-components/addon/components/contributors/card/readonly/template.hbs
index 74476313327..21d17402bfe 100644
--- a/lib/osf-components/addon/components/contributors/card/readonly/template.hbs
+++ b/lib/osf-components/addon/components/contributors/card/readonly/template.hbs
@@ -39,7 +39,7 @@
{{t 'osf-components.contributors.permissionsNotEditable' }}
- {{t (concat 'osf-components.contributors.permissions.' @contributor.permission)}}
+ {{t (concat 'general.permissions.' @contributor.permission)}}
- {{t (concat 'osf-components.contributors.permissions.' option)}}
+ {{t (concat 'general.permissions.' option)}}
{{/unless}}
diff --git a/lib/osf-components/addon/components/index-card-searcher/component.ts b/lib/osf-components/addon/components/index-card-searcher/component.ts
new file mode 100644
index 00000000000..c0b36fc6672
--- /dev/null
+++ b/lib/osf-components/addon/components/index-card-searcher/component.ts
@@ -0,0 +1,85 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { waitFor } from '@ember/test-waiters';
+import { restartableTask, timeout } from 'ember-concurrency';
+import Store from '@ember-data/store';
+import Toast from 'ember-toastr/services/toast';
+
+import SearchResultModel from 'ember-osf-web/models/search-result';
+import { taskFor } from 'ember-concurrency-ts';
+import RelatedPropertyPathModel, { SuggestedFilterOperators } from 'ember-osf-web/models/related-property-path';
+
+interface IndexCardSearcherArgs {
+ queryOptions: Record
;
+ debounceTime?: number;
+}
+
+export default class IndexCardSearcher extends Component {
+ @service store!: Store;
+ @service toast!: Toast;
+
+ debounceTime = this.args.debounceTime || 1000;
+
+ @tracked searchResults: SearchResultModel[] = [];
+ @tracked totalResultCount = 0;
+
+ @tracked relatedProperties?: RelatedPropertyPathModel[] = [];
+ @tracked booleanFilters?: RelatedPropertyPathModel[] = [];
+
+ @tracked firstPageCursor?: string;
+ @tracked nextPageCursor?: string;
+ @tracked prevPageCursor?: string;
+
+ get showFirstPageOption() {
+ return this.hasPrevPage && this.prevPageCursor !== this.firstPageCursor;
+ }
+
+ get hasNextPage() {
+ return Boolean(this.nextPageCursor);
+ }
+
+ get hasPrevPage() {
+ return Boolean(this.prevPageCursor);
+ }
+
+ constructor(owner: unknown, args: IndexCardSearcherArgs) {
+ super(owner, args);
+ taskFor(this.searchObjectsTask).perform();
+ }
+
+ get isLoading() {
+ return taskFor(this.searchObjectsTask).isRunning;
+ }
+
+ @restartableTask
+ @waitFor
+ async searchObjectsTask() {
+ try {
+ const searchResult = await this.store.queryRecord('index-card-search', this.args.queryOptions);
+
+ this.booleanFilters = searchResult.relatedProperties
+ .filterBy('suggestedFilterOperator', SuggestedFilterOperators.IsPresent);
+ this.relatedProperties = searchResult.relatedProperties.filter(
+ (property: RelatedPropertyPathModel) =>
+ property.suggestedFilterOperator !== SuggestedFilterOperators.IsPresent, // AnyOf or AtDate
+ );
+ this.firstPageCursor = searchResult.firstPageCursor;
+ this.nextPageCursor = searchResult.nextPageCursor;
+ this.prevPageCursor = searchResult.prevPageCursor;
+ this.searchResults = searchResult.searchResultPage.toArray();
+ this.totalResultCount = searchResult.totalResultCount;
+
+ return searchResult;
+ } catch (error) {
+ this.toast.error(error);
+ }
+ }
+
+ @restartableTask
+ @waitFor
+ async debouceSearchObjectsTask() {
+ await timeout(this.debounceTime);
+ return taskFor(this.searchObjectsTask).perform();
+ }
+}
diff --git a/lib/osf-components/addon/components/index-card-searcher/template.hbs b/lib/osf-components/addon/components/index-card-searcher/template.hbs
new file mode 100644
index 00000000000..76d1db68bd9
--- /dev/null
+++ b/lib/osf-components/addon/components/index-card-searcher/template.hbs
@@ -0,0 +1,16 @@
+{{yield (hash
+ searchResults=this.searchResults
+ relatedProperties=this.relatedProperties
+ booleanFilters=this.booleanFilters
+ totalResultCount=this.totalResultCount
+
+ debouceSearchObjectsTask=this.debouceSearchObjectsTask
+ searchObjectsTask=this.searchObjectsTask
+
+ firstPageCursor=this.firstPageCursor
+ nextPageCursor=this.nextPageCursor
+ prevPageCursor=this.prevPageCursor
+ showFirstPageOption=this.showFirstPageOption
+ hasNextPage=this.hasNextPage
+ hasPrevPage=this.hasPrevPage
+)}}
diff --git a/lib/osf-components/addon/components/metadata/metadata-tabs/styles.scss b/lib/osf-components/addon/components/metadata/metadata-tabs/styles.scss
index 38429c3b567..507ef4712cf 100644
--- a/lib/osf-components/addon/components/metadata/metadata-tabs/styles.scss
+++ b/lib/osf-components/addon/components/metadata/metadata-tabs/styles.scss
@@ -1,4 +1,5 @@
// stylelint-disable max-nesting-depth, selector-max-compound-selectors
+@import 'app/styles/components';
.metadata-tab-container {
width: 100%;
@@ -31,51 +32,20 @@
/* stylelint-disable selector-no-qualifying-type */
ul.tab-list {
- margin-bottom: 10px;
- border-bottom: 1px solid #ddd;
- box-sizing: border-box;
- color: rgb(51, 51, 51);
- display: block;
- line-height: 20px;
- list-style-image: none;
- list-style-position: outside;
- list-style-type: none;
+ @include tab-list;
height: 42px;
- padding: 0;
-
- &.showMore {
- height: fit-content;
- }
- }
-
- /* stylelint-enable selector-no-qualifying-type */
- .tab-list {
overflow: hidden;
.dark {
font-weight: bold;
}
- li {
- cursor: pointer;
- display: block;
- position: relative;
- margin-bottom: -1px;
- float: left;
- height: 41px;
- padding: 10px 15px;
- }
-
- li:global(.ember-tabs__tab--selected) {
- background-color: #f8f8f8;
- border-bottom: 2px solid #204762;
+ &.showMore {
+ height: fit-content;
}
- li:hover {
- border-color: transparent;
- text-decoration: none;
- background-color: #f8f8f8;
- color: var(--primary-color);
+ li {
+ cursor: pointer;
}
}
}
diff --git a/lib/osf-components/addon/components/osf-layout/component.ts b/lib/osf-components/addon/components/osf-layout/component.ts
index 775193dd3ea..7ea3499e2b8 100644
--- a/lib/osf-components/addon/components/osf-layout/component.ts
+++ b/lib/osf-components/addon/components/osf-layout/component.ts
@@ -17,14 +17,19 @@ export default class OsfLayout extends Component {
sidenavGutterClosed = true;
metadataGutterClosed = true;
backgroundClass?: string;
+ forceMetadataGutterMode?: 'page' | 'drawer' | 'column';
+ forceSidenavGutterMode?: 'page' | 'drawer' | 'column';
init() {
super.init();
assert('@backgroundClass is required!', Boolean(this.backgroundClass));
}
- @computed('media.{isMobile,isTablet,isDesktop}')
+ @computed('media.{isMobile,isTablet,isDesktop}', 'forceMetadataGutterMode')
get metadataGutterMode() {
+ if (this.forceMetadataGutterMode) {
+ return this.forceMetadataGutterMode;
+ }
if (this.media.isMobile) {
return 'page';
}
@@ -34,8 +39,11 @@ export default class OsfLayout extends Component {
return 'column';
}
- @computed('media.{isMobile,isTablet,isDesktop}')
+ @computed('media.{isMobile,isTablet,isDesktop}', 'forceSidenavGutterMode')
get sidenavGutterMode() {
+ if (this.forceSidenavGutterMode) {
+ return this.forceSidenavGutterMode;
+ }
if (this.media.isDesktop) {
return 'column';
}
diff --git a/lib/osf-components/addon/components/osf-layout/main-column/template.hbs b/lib/osf-components/addon/components/osf-layout/main-column/template.hbs
index ce1bf493eb5..b15f3b982db 100644
--- a/lib/osf-components/addon/components/osf-layout/main-column/template.hbs
+++ b/lib/osf-components/addon/components/osf-layout/main-column/template.hbs
@@ -1,5 +1,7 @@
<@gutters.body ...attributes local-class='Main'>
{{yield (hash
main=(element 'div')
+ toggleMetadata=(action @toggleMetadata)
+ metadataGutterClosed=@metadataGutterClosed
)}}
@gutters.body>
diff --git a/lib/osf-components/addon/components/osf-layout/template.hbs b/lib/osf-components/addon/components/osf-layout/template.hbs
index 0bf4dc3d098..067ecfba4e6 100644
--- a/lib/osf-components/addon/components/osf-layout/template.hbs
+++ b/lib/osf-components/addon/components/osf-layout/template.hbs
@@ -26,7 +26,11 @@
leftNavOld=(component 'osf-layout/left-nav-old' gutters=gutters toggleSidenav=(action this.toggleSidenav))
leftNav=(component 'osf-layout/left-nav' gutters=gutters toggleSidenav=(action this.toggleSidenav))
left=(component 'osf-layout/left-column' gutters=gutters toggleSidenav=(action this.toggleSidenav))
- main=(component 'osf-layout/main-column' gutters=gutters)
+ main=(component 'osf-layout/main-column'
+ gutters=gutters
+ toggleMetadata=(action this.toggleMetadata)
+ metadataGutterClosed=this.metadataGutterClosed
+ )
right=(component 'osf-layout/right-column' gutters=gutters toggleMetadata=(action this.toggleMetadata))
)}}
diff --git a/lib/osf-components/addon/components/osf-link/component.ts b/lib/osf-components/addon/components/osf-link/component.ts
index 610506a1cf0..10507995497 100644
--- a/lib/osf-components/addon/components/osf-link/component.ts
+++ b/lib/osf-components/addon/components/osf-link/component.ts
@@ -35,6 +35,7 @@ export default class OsfLink extends Component {
href?: string;
queryParams?: Record;
fragment?: string;
+ fakeButton?: boolean;
rel: AnchorRel = 'noopener noreferrer';
target: AnchorTarget = '_self';
diff --git a/lib/osf-components/addon/components/osf-link/styles.scss b/lib/osf-components/addon/components/osf-link/styles.scss
index 6385b4bf190..6251ba32b6f 100644
--- a/lib/osf-components/addon/components/osf-link/styles.scss
+++ b/lib/osf-components/addon/components/osf-link/styles.scss
@@ -6,3 +6,9 @@
text-decoration: underline;
}
}
+
+.Button {
+ composes: Button from '../button/styles.scss';
+ composes: MediumButton from '../button/styles.scss';
+ composes: SecondaryButton from '../button/styles.scss';
+}
diff --git a/lib/osf-components/addon/components/osf-link/template.hbs b/lib/osf-components/addon/components/osf-link/template.hbs
index da22168c274..92c5a5f1a2e 100644
--- a/lib/osf-components/addon/components/osf-link/template.hbs
+++ b/lib/osf-components/addon/components/osf-link/template.hbs
@@ -3,7 +3,7 @@
target={{this.target}}
rel={{this.rel}}
class='{{this.class}} {{if this.isActive 'active'}}'
- local-class='OsfLink'
+ local-class='OsfLink {{if this.fakeButton 'Button'}}'
onclick={{action this._onClick}}
...attributes
>
diff --git a/lib/osf-components/addon/components/paginated-list/has-many/component.ts b/lib/osf-components/addon/components/paginated-list/has-many/component.ts
index 9df7d12651f..0e385096d80 100644
--- a/lib/osf-components/addon/components/paginated-list/has-many/component.ts
+++ b/lib/osf-components/addon/components/paginated-list/has-many/component.ts
@@ -19,6 +19,7 @@ export default class PaginatedHasMany extends BaseDataComponent {
// Either model xor modelTaskInstance is required
model?: OsfModel;
modelTaskInstance?: TaskInstance;
+ pagination?: string;
@or('model', 'modelTaskInstance.value')
modelInstance?: OsfModel;
diff --git a/lib/osf-components/addon/components/paginated-list/has-many/template.hbs b/lib/osf-components/addon/components/paginated-list/has-many/template.hbs
index 21e10e07392..f2ead15c1b8 100644
--- a/lib/osf-components/addon/components/paginated-list/has-many/template.hbs
+++ b/lib/osf-components/addon/components/paginated-list/has-many/template.hbs
@@ -9,6 +9,7 @@
next=(action this.next)
previous=(action this.previous)
doReload=(action this._doReload)
+ pagination=this.pagination
as |list|
}}
{{yield list}}
diff --git a/lib/osf-components/addon/components/paginated-list/layout/component.ts b/lib/osf-components/addon/components/paginated-list/layout/component.ts
index ee89fdbb0b2..18d9219fdcd 100644
--- a/lib/osf-components/addon/components/paginated-list/layout/component.ts
+++ b/lib/osf-components/addon/components/paginated-list/layout/component.ts
@@ -14,6 +14,7 @@ export default class PaginatedList extends Component {
@requiredAction next!: () => void;
@requiredAction previous!: () => void;
@requiredAction doReload!: () => void;
+ pagination?: string;
// Optional arguments
loading = false;
diff --git a/lib/osf-components/addon/components/paginated-list/layout/styles.scss b/lib/osf-components/addon/components/paginated-list/layout/styles.scss
index c5072d71dc2..de8477d8395 100644
--- a/lib/osf-components/addon/components/paginated-list/layout/styles.scss
+++ b/lib/osf-components/addon/components/paginated-list/layout/styles.scss
@@ -2,6 +2,10 @@
text-align: center;
}
+.align-right {
+ text-align: right;
+}
+
.m-md {
margin: 15px;
}
diff --git a/lib/osf-components/addon/components/paginated-list/layout/template.hbs b/lib/osf-components/addon/components/paginated-list/layout/template.hbs
index 625345df1c4..f19670a01c5 100644
--- a/lib/osf-components/addon/components/paginated-list/layout/template.hbs
+++ b/lib/osf-components/addon/components/paginated-list/layout/template.hbs
@@ -50,14 +50,28 @@
{{/if}}
{{#if this.paginatorShown}}
-
- {{simple-paginator
- maxPage=this.maxPage
- nextPage=(action @next)
- previousPage=(action @previous)
- curPage=@page
- }}
-
+ {{#if (eq this.pagination 'adjustable')}}
+
+ {{adjustable-paginator
+ maxPage=this.maxPage
+ nextPage=(action @next)
+ previousPage=(action @previous)
+ page=@page
+ doReload=@doReload
+ pageSize=@pageSize
+ totalCount=@totalCount
+ }}
+
+ {{else}}
+
+ {{simple-paginator
+ maxPage=this.maxPage
+ nextPage=(action @next)
+ previousPage=(action @previous)
+ curPage=@page
+ }}
+
+ {{/if}}
{{/if}}
{{else if this.loading}}
{{loading-indicator dark=true}}
diff --git a/lib/osf-components/addon/components/search-result-card/component.ts b/lib/osf-components/addon/components/search-result-card/component.ts
index 2b9bf7b9d8a..ed497eb9982 100644
--- a/lib/osf-components/addon/components/search-result-card/component.ts
+++ b/lib/osf-components/addon/components/search-result-card/component.ts
@@ -9,18 +9,6 @@ import SearchResultModel from 'ember-osf-web/models/search-result';
import PreprintProviderModel from 'ember-osf-web/models/preprint-provider';
import InstitutionModel from 'ember-osf-web/models/institution';
-
-const CardLabelTranslationKeys = {
- project: 'osf-components.search-result-card.project',
- project_component: 'osf-components.search-result-card.project_component',
- registration: 'osf-components.search-result-card.registration',
- registration_component: 'osf-components.search-result-card.registration_component',
- preprint: 'osf-components.search-result-card.preprint',
- file: 'osf-components.search-result-card.file',
- user: 'osf-components.search-result-card.user',
- unknown: 'osf-components.search-result-card.unknown',
-};
-
interface Args {
result: SearchResultModel;
provider?: PreprintProviderModel;
@@ -39,8 +27,8 @@ export default class SearchResultCard extends Component {
}
get cardTypeLabel() {
- const { provider, institution } = this.args;
- const resourceType = this.args.result.resourceType;
+ const { provider, institution, result } = this.args;
+ const resourceType = result.resourceType;
if (resourceType === 'preprint') {
if (institution?.id === 'yls') {
return this.intl.t('documentType.paper.singularCapitalized');
@@ -49,7 +37,7 @@ export default class SearchResultCard extends Component {
return provider.documentType.singularCapitalized;
}
}
- return this.intl.t(CardLabelTranslationKeys[resourceType]);
+ return result.intlResourceType;
}
get secondaryMetadataComponent() {
diff --git a/lib/osf-components/addon/components/sort-arrow/component.ts b/lib/osf-components/addon/components/sort-arrow/component.ts
new file mode 100644
index 00000000000..08d1ca2b2ab
--- /dev/null
+++ b/lib/osf-components/addon/components/sort-arrow/component.ts
@@ -0,0 +1,29 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+interface SortArrowArgs {
+ sort: string;
+ sortBy: string;
+ sortAction: (sortField: string) => void;
+}
+
+export default class SortArrow extends Component {
+
+ get isCurrentAscending() {
+ return this.args.sort === this.args.sortBy;
+ }
+
+ get isCurrentDescending() {
+ return this.args.sort === `-${this.args.sortBy}`;
+ }
+
+ get isSelected() {
+ return this.isCurrentAscending || this.isCurrentDescending;
+ }
+
+ @action
+ handleSort() {
+ this.args.sortAction(this.args.sortBy);
+ }
+}
+
diff --git a/lib/osf-components/addon/components/sort-arrow/styles.scss b/lib/osf-components/addon/components/sort-arrow/styles.scss
new file mode 100644
index 00000000000..db354890b61
--- /dev/null
+++ b/lib/osf-components/addon/components/sort-arrow/styles.scss
@@ -0,0 +1,31 @@
+.sort-arrow-container {
+ vertical-align: middle;
+
+ .selected {
+ color: $color-bg-gray-light;
+ }
+
+ .not-selected {
+ color: $color-bg-blue-dark;
+ }
+}
+
+
+.arrow-button,
+.arrow-button:active,
+.arrow-button:focus,
+.arrow-button:focus:active {
+ outline: none;
+ background: transparent;
+ border: 0;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ color: $color-bg-gray-light;
+ width: 100%;
+ height: 100%;
+}
+
+.arrow-button:hover {
+ color: $color-bg-gray-lighter;
+}
diff --git a/lib/osf-components/addon/components/sort-arrow/template.hbs b/lib/osf-components/addon/components/sort-arrow/template.hbs
new file mode 100644
index 00000000000..58e88101ea7
--- /dev/null
+++ b/lib/osf-components/addon/components/sort-arrow/template.hbs
@@ -0,0 +1,17 @@
+
+
+ {{#if this.isCurrentAscending}}
+
+ {{else}}
+
+ {{/if}}
+
+
diff --git a/lib/osf-components/addon/components/subjects/widget/styles.scss b/lib/osf-components/addon/components/subjects/widget/styles.scss
index ce72b1bac90..7f7a59aa1a5 100644
--- a/lib/osf-components/addon/components/subjects/widget/styles.scss
+++ b/lib/osf-components/addon/components/subjects/widget/styles.scss
@@ -1,39 +1,8 @@
+@import 'app/styles/components';
+
.Tabs {
/* stylelint-disable selector-no-qualifying-type */
ul.TabList {
- margin-bottom: 10px;
- border-bottom: 1px solid #ddd;
- box-sizing: border-box;
- color: rgb(51, 51, 51);
- display: block;
- line-height: 20px;
- list-style-image: none;
- list-style-position: outside;
- list-style-type: none;
- height: 41px;
- padding: 0;
- }
- /* stylelint-enable selector-no-qualifying-type */
- .TabList {
- li {
- display: block;
- position: relative;
- margin-bottom: -1px;
- float: left;
- height: 41px;
- padding: 10px 15px;
- }
-
- li:global(.ember-tabs__tab--selected) {
- background-color: #f8f8f8;
- border-bottom: 2px solid #204762;
- }
-
- li:hover {
- border-color: transparent;
- text-decoration: none;
- background-color: #f8f8f8;
- color: var(--primary-color);
- }
+ @include tab-list;
}
}
diff --git a/lib/osf-components/app/components/adjustable-paginator/component.js b/lib/osf-components/app/components/adjustable-paginator/component.js
new file mode 100644
index 00000000000..68bd15c9615
--- /dev/null
+++ b/lib/osf-components/app/components/adjustable-paginator/component.js
@@ -0,0 +1 @@
+export { default } from 'osf-components/components/adjustable-paginator/component';
diff --git a/lib/osf-components/app/components/index-card-searcher/component.js b/lib/osf-components/app/components/index-card-searcher/component.js
new file mode 100644
index 00000000000..bd6c5fbde2e
--- /dev/null
+++ b/lib/osf-components/app/components/index-card-searcher/component.js
@@ -0,0 +1 @@
+export { default } from 'osf-components/components/index-card-searcher/component';
diff --git a/lib/osf-components/app/components/index-card-searcher/template.js b/lib/osf-components/app/components/index-card-searcher/template.js
new file mode 100644
index 00000000000..e45506dfbfe
--- /dev/null
+++ b/lib/osf-components/app/components/index-card-searcher/template.js
@@ -0,0 +1 @@
+export { default } from 'osf-components/components/index-card-searcher/template';
diff --git a/lib/osf-components/app/components/sort-arrow/component.js b/lib/osf-components/app/components/sort-arrow/component.js
new file mode 100644
index 00000000000..1dd776decb6
--- /dev/null
+++ b/lib/osf-components/app/components/sort-arrow/component.js
@@ -0,0 +1 @@
+export { default } from 'osf-components/components/sort-arrow/component';
diff --git a/lib/osf-components/app/components/sort-arrow/template.js b/lib/osf-components/app/components/sort-arrow/template.js
new file mode 100644
index 00000000000..e9d4355e272
--- /dev/null
+++ b/lib/osf-components/app/components/sort-arrow/template.js
@@ -0,0 +1 @@
+export { default } from 'osf-components/components/sort-arrow/template';
diff --git a/mirage/config.ts b/mirage/config.ts
index a8e595fa164..bfabf667589 100644
--- a/mirage/config.ts
+++ b/mirage/config.ts
@@ -66,13 +66,9 @@ export default function(this: Server) {
// SHARE-powered search endpoints
this.urlPrefix = shareBaseUrl;
- this.namespace = '/api/v3/';
+ this.namespace = '/trove/'; // /api/v3/ works as well, but /trove/ is the preferred URL
this.get('/index-card-search', cardSearch);
this.get('/index-value-search', valueSearch);
- // this.get('/index-card/:id', Detail);
- this.get('/index-card-searches', cardSearch);
- this.get('/index-value-searches', valueSearch);
- // this.get('/index-cards/:id', Detail);
this.urlPrefix = osfUrl;
this.namespace = '/api/v1/';
diff --git a/mirage/factories/institution-department.ts b/mirage/factories/institution-department.ts
index a2e2cf3b682..201e3c806f3 100644
--- a/mirage/factories/institution-department.ts
+++ b/mirage/factories/institution-department.ts
@@ -5,7 +5,7 @@ import InstitutionDepartmentModel from 'ember-osf-web/models/institution-departm
export default Factory.extend({
name() {
- return faker.lorem.word();
+ return faker.random.arrayElement(['Architecture', 'Biology', 'Psychology']);
},
numberOfUsers() {
return faker.random.number({ min: 100, max: 1000 });
diff --git a/mirage/factories/institution-summary-metric.ts b/mirage/factories/institution-summary-metric.ts
index 9531b488136..123c422d7d7 100644
--- a/mirage/factories/institution-summary-metric.ts
+++ b/mirage/factories/institution-summary-metric.ts
@@ -11,7 +11,28 @@ export default Factory.extend({
return faker.random.number({ min: 10, max: 100 });
},
userCount() {
- return faker.random.number({ min: 100, max: 1000 });
+ return faker.random.number({ min: 10, max: 50});
+ },
+ publicRegistrationCount() {
+ return faker.random.number({ min: 1000, max: 10000 });
+ },
+ embargoedRegistrationCount() {
+ return faker.random.number({ min: 0, max: 25});
+ },
+ publishedPreprintCount() {
+ return faker.random.number({ min: 15, max: 175});
+ },
+ storageByteCount() {
+ return faker.random.number({ min: 1000 * 100, max: 1000 * 1000 * 100 });
+ },
+ publicFileCount() {
+ return faker.random.number({ min: 15, max: 1000 });
+ },
+ monthlyActiveUserCount() {
+ return faker.random.number({ min: 10, max: 100 * 10 });
+ },
+ monthlyLoggedInUserCount() {
+ return faker.random.number({ min: 10, max: 100 * 100 });
},
});
diff --git a/mirage/factories/institution-user.ts b/mirage/factories/institution-user.ts
index ce3b3e276c8..beac8d9db6d 100644
--- a/mirage/factories/institution-user.ts
+++ b/mirage/factories/institution-user.ts
@@ -14,6 +14,36 @@ export default Factory.extend({
privateProjects() {
return faker.random.number({ min: 0, max: 99 });
},
+ publicRegistrationCount() {
+ return faker.random.number({ min: 0, max: 50 });
+ },
+ embargoedRegistrationCount() {
+ return faker.random.number({ min: 0, max: 50 });
+ },
+ publishedPreprintCount() {
+ return faker.random.number({ min: 0, max: 50 });
+ },
+ publicFileCount() {
+ return faker.random.number({ min: 0, max: 100 });
+ },
+ storageByteCount() {
+ return faker.random.number({ min: 1e7, max: 1e9 }); // Between 10MB and 1GB
+ },
+ totalObjectCount() {
+ return faker.random.number({ min: 0, max: 1000 });
+ },
+ monthLastLogin() {
+ return faker.date.past(1); // Any date within the past year
+ },
+ monthLastActive() {
+ return faker.date.past(1); // Any date within the past year
+ },
+ accountCreationDate() {
+ return faker.date.past(10); // Any date within the past 10 years
+ },
+ orcidId() {
+ return faker.random.uuid(); // Simulate an ORCID ID
+ },
afterCreate(institutionUser, server) {
if (!institutionUser.userName && !institutionUser.userGuid) {
const user = server.create('user');
diff --git a/mirage/factories/institution.ts b/mirage/factories/institution.ts
index fa2259e8e75..15de4602562 100644
--- a/mirage/factories/institution.ts
+++ b/mirage/factories/institution.ts
@@ -23,6 +23,7 @@ export default Factory.extend({
};
},
currentUserIsAdmin: true,
+ linkToExternalReportsArchive: faker.internet.url,
lastUpdated() {
return faker.date.recent();
},
@@ -33,16 +34,13 @@ export default Factory.extend({
withMetrics: trait({
afterCreate(institution, server) {
const userMetrics = server.createList('institution-user', 15);
- const departmentMetrics = server.createList('institution-department', 12);
- const userCount = userMetrics.length;
- let publicProjectCount = 0;
- let privateProjectCount = 0;
- userMetrics.forEach(({ publicProjects, privateProjects }) => {
- publicProjectCount += publicProjects;
- privateProjectCount += privateProjects;
- });
+ const departmentNames = ['Architecture', 'Biology', 'Psychology'];
+
+ const departmentMetrics = departmentNames.map(name =>
+ server.create('institution-department', { name }));
+
const summaryMetrics = server.create('institution-summary-metric', { id: institution.id });
- summaryMetrics.update({ publicProjectCount, privateProjectCount, userCount });
+
institution.update({ userMetrics, departmentMetrics, summaryMetrics });
},
}),
diff --git a/mirage/scenarios/dashboard.ts b/mirage/scenarios/dashboard.ts
index 029908aa506..18f108f86ed 100644
--- a/mirage/scenarios/dashboard.ts
+++ b/mirage/scenarios/dashboard.ts
@@ -83,6 +83,7 @@ export function dashboardScenario(server: Server, currentUser: ModelInstance> = {
+ Registration: () => {
+ const contributor = _sharePersonField();
+ return {
+ '@id': faker.random.uuid(),
+ // accessService: [{}],
+ archivedAt: [{}], // archive.org URL
+ conformsTo: [{ // Registration Schema
+ '@id': 'https://api.osf.io/v2/schemas/registrations/564d31db8c5e4a7c9694b2be/',
+ title: [{
+ '@value': 'Open-Ended Registration',
+ }],
+ }],
+ creator: [contributor],
+ dateAvailable: [_shareDateField()],
+ dateCopyrighted: [_shareDateField()],
+ dateCreated: [_shareDateField()],
+ dateModified: [_shareDateField()],
+ description: [{
+ '@value': faker.lorem.sentence(),
+ }],
+ funder: [_shareOrganizationField()],
+ hasPart: [{}], // RegistrationComponent
+ hostingInstition: [_shareOrganizationField()],
+ identifier: [_shareIdentifierField(), _shareOsfIdentifier()],
+ isVersionOf: [{}], // if this is from a project
+ keyword: [{ // tags
+ '@value': faker.random.word(),
+ }],
+ publisher: [_shareOrganizationField()], // Registration Provider
+ qualifiedAttribution: [{
+ agent: [contributor],
+ hadRole: [{
+ '@id': faker.random.arrayElement(Object.values(AttributionRoleIris)),
+ }],
+ }],
+ resourceNature: [{ // Registration Category
+ '@id': 'https://schema.datacite.org/meta/kernel-4/#StudyRegistration',
+ displayLabel: [{
+ '@value': 'StudyRegistration',
+ '@language': 'en',
+ }],
+ }],
+ resourceType: [{ '@id': 'Registration' }],
+ rights: [_shareLicenseField()],
+ sameAs: [{}], // some DOI
+ storageByteCount: [{
+ '@value': faker.random.number(),
+ }],
+ storageRegion: [{
+ prefLabel: [{
+ '@value': faker.random.arrayElement(sampleStorageRegions),
+ }],
+ }],
+ subject: [_shareSubjectField()],
+ title: [{
+ '@value': faker.lorem.words(3),
+ }],
+ usage: [_shareUsageReportField()],
+ };
+ },
+ Project: () => {
+ const contributor = _sharePersonField();
+ return {
+ '@id': faker.internet.url(),
+ // accessService: [{}],
+ creator: [contributor],
+ dateCopyrighted: [_shareDateField()],
+ dateCreated: [_shareDateField()],
+ dateModified: [_shareDateField()],
+ description: [{
+ '@value': faker.lorem.sentence(),
+ }],
+ funder: [_shareOrganizationField()],
+ hasOsfAddon: [
+ {
+ prefLabel: [{
+ '@value': 'Box',
+ }],
+ },
+ ],
+ hasPart: [{}], // ProjectComponent
+ hostingInstition: [_shareOrganizationField()],
+ identifier: [_shareIdentifierField(), _shareOsfIdentifier()],
+ keyword: [{ // tags
+ '@value': faker.random.word(),
+ }],
+ publisher: [_shareOrganizationField()],
+ qualifiedAttribution: [{
+ agent: [contributor],
+ hadRole: [{
+ '@id': faker.random.arrayElement(Object.values(AttributionRoleIris)),
+ }],
+ }],
+ resourceNature: [{
+ '@id': 'https://schema.datacite.org/meta/kernel-4/#Dataset',
+ displayLabel: [
+ {
+ '@value': faker.random.arrayElement(['Dataset', 'JournalArticle', 'Book']),
+ '@language': 'en',
+ },
+ ],
+ }],
+ resourceType: [{ '@id': 'Project' }],
+ rights: [_shareLicenseField()],
+ sameAs: [{}], // some DOI
+ storageByteCount: [{
+ '@value': faker.random.number(),
+ }],
+ storageRegion: [{
+ prefLabel: [{
+ '@value': faker.random.arrayElement(sampleStorageRegions),
+ }],
+ }],
+ subject: [_shareSubjectField()],
+ title: [{
+ '@value': faker.lorem.words(3),
+ }],
+ usage: [_shareUsageReportField()],
+ };
+ },
+ Preprint: () => {
+ const contributor = _sharePersonField();
+ return {
+ '@id': faker.internet.url(),
+ // accessService: [{}],
+ creator: [contributor],
+ dateAccepted: [_shareDateField()],
+ dateCopyrighted: [_shareDateField()],
+ dateCreated: [_shareDateField()],
+ dateSubmitted: [_shareDateField()],
+ dateModified: [_shareDateField()],
+ description: [{
+ '@value': faker.lorem.sentence(),
+ }],
+ hasPart: [{}], // File
+ hostingInstition: [_shareOrganizationField()],
+ identifier: [_shareIdentifierField(), _shareOsfIdentifier()],
+ // isSupplementedBy: [{}], // if this links a project
+ keyword: [{ // tags
+ '@value': faker.random.word(),
+ }],
+ omits: [{
+ ommittedMetadataProperty: [
+ { '@id': 'hasPreregisteredStudyDesign' },
+ { '@id': 'hasPreregisteredAnalysisPlan' },
+ ],
+ }],
+ publisher: [_shareOrganizationField()], // Preprint Provider
+ qualifiedAttribution: [{
+ agent: [contributor],
+ hadRole: [{
+ '@id': faker.random.arrayElement(Object.values(AttributionRoleIris)),
+ }],
+ }],
+ resourceNature: [{
+ '@id': 'https://schema.datacite.org/meta/kernel-4/#Preprint',
+ displayLabel: [{
+ '@value': 'Preprint',
+ '@language': 'en',
+ }],
+ }],
+ resourceType: [{ '@id': 'Preprint' }],
+ rights: [_shareLicenseField()],
+ sameAs: [{}], // some DOI
+ statedConflictOfInterest: [{
+ '@id': 'no-confict-of-interest',
+ }],
+ subject: [_shareSubjectField()],
+ title: [{
+ '@value': faker.lorem.words(3),
+ }],
+ usage: [_shareUsageReportField()],
+ };
+ },
+ Agent: () => ({
+ '@id': faker.internet.url(),
+ // accessService: [{}],
+ affiliation: [_shareOrganizationField()],
+ identifier: [
+ _shareIdentifierField(),
+ {
+ '@value': 'https://orcid.org/0000-0000-0000-0000',
+ },
+ _shareOsfIdentifier(),
+ ],
+ name: [{
+ '@value': faker.name.findName(),
+ }],
+ resourceType: [{ '@id': OsfmapResourceTypes.Person }, { '@id': OsfmapResourceTypes.Agent }],
+ sameAs: [{ '@id': 'https://orcid.org/0000-0000-0000-0000' }], // some ORCID
+ }),
+};
+
+resourceMetadataByType.ProjectComponent = function() {
+ return {
+ ...resourceMetadataByType.Project(),
+ resourceType: [{ '@id': 'ProjectComponent' }],
+ isPartOf: [{ // Parent Project
+ ...resourceMetadataByType.Project(),
+ }],
+ hasRoot: [{ // Root Project
+ ...resourceMetadataByType.Project(),
+ }],
+ };
+};
+resourceMetadataByType.RegistrationComponent = function() {
return {
+ ...resourceMetadataByType.Registration(),
+ resourceType: [{ '@id': 'RegistrationComponent' }],
+ isPartOf: [{ // Parent Registration
+ ...resourceMetadataByType.Registration(),
+ }],
+ hasRoot: [{ // Root Registration
+ ...resourceMetadataByType.Registration(),
+ }],
+ };
+};
+resourceMetadataByType.File = function() {
+ return {
+ '@id': faker.internet.url(),
+ // accessService: [{}],
+ dateCreated: [_shareDateField()],
+ dateModified: [_shareDateField()],
+ description: [{
+ '@value': faker.lorem.sentence(),
+ }],
+ fileName: [{
+ '@value': faker.system.fileName(),
+ }],
+ filePath: [{
+ '@value': faker.system.filePath(),
+ }],
+ identifier: [_shareIdentifierField(), _shareOsfIdentifier()],
+ isContainedBy: [{ // Parent Project
+ ...resourceMetadataByType.Project(),
+ }],
+ language: [{
+ '@value': 'eng',
+ }],
+ // 'osf:hasFileVersion': [{}], // FileVersion
+ resourceNature: [{
+ '@id': 'https://schema.datacite.org/meta/kernel-4/#Dataset',
+ displayLabel: [{
+ '@value': 'Dataset',
+ '@language': 'en',
+ }],
+ }],
+ resourceType: [{ '@id': 'File' }],
+ title: [{
+ '@value': faker.lorem.words(3),
+ }],
+ };
+};
+
+export function cardSearch(_: Schema, request: Request) {
+ const {queryParams} = request;
+ const pageCursor = queryParams['page[cursor]'];
+ const pageSize = queryParams['page[size]'] ? parseInt(queryParams['page[size]'], 10) : 10;
+
+ // cardSearchFilter[resourceType] is a comma-separated list (e.g. 'Project,ProjectComponent') or undefined
+ let requestedResourceTypes = queryParams['cardSearchFilter[resourceType]']?.split(',') as OsfmapResourceTypes[];
+ if (!requestedResourceTypes) {
+ requestedResourceTypes = Object.keys(resourceMetadataByType) as OsfmapResourceTypes[];
+ }
+
+ const indexCardSearch = {
data: {
type: 'index-card-search',
- id: 'zzzzzz',
- attributes:{
+ id: faker.random.uuid(),
+ attributes: {
cardSearchText: 'hello',
cardSearchFilter: [
{
@@ -25,30 +299,9 @@ export function cardSearch(_: Schema, __: Request) {
],
},
],
- totalResultCount: 3,
+ totalResultCount: 10,
},
relationships: {
- searchResultPage: {
- data: [
- {
- type: 'search-result',
- id: 'abc',
- },
- {
- type: 'search-result',
- id: 'def',
- },
- {
- type: 'search-result',
- id: 'ghi',
- },
- ],
- links: {
- next: {
- href: 'https://staging-share.osf.io/api/v3/index-card-search?page%5Bcursor%5D=lmnop',
- },
- },
- },
relatedProperties: {
data: [
{
@@ -66,238 +319,14 @@ export function cardSearch(_: Schema, __: Request) {
],
links: {
next: {
- href: 'https://staging-share.osf.io/api/v3/index-card-search?page%5Bcursor%5D=lmnop',
+ href: 'https://staging-share.osf.io/trove/index-card-search?page%5Bcursor%5D=lmnop',
},
},
},
+ searchResultPage: {},
},
},
included: [
- {
- type: 'search-result',
- id: 'abc',
- attributes: {
- matchEvidence: [
- {
- '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'],
- osfmapPropertyPath: 'description',
- matchingHighlight: '... say hello !',
- },
- {
- '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'],
- osfmapPropertyPath: 'title',
- matchingHighlight: '... shout hello !',
- },
- ],
- },
- relationships: {
- indexCard: {
- data: {
- type: 'index-card',
- id: 'abc',
- },
- links: {
- related: 'https://share.osf.io/api/v2/index-card/abc',
- },
- },
- },
- },
- {
- type: 'search-result',
- id: 'def',
- attributes: {
- matchEvidence: [
- {
- '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'],
- osfmapPropertyPath: 'description',
- matchingHighlight: '... computer said hello world!',
- },
- ],
- },
- relationships: {
- indexCard: {
- data: {
- type: 'index-card',
- id: 'def',
- },
- links: {
- related: 'https://share.osf.io/api/v2/index-card/def',
- },
- },
- },
- },
- {
- type: 'search-result',
- id: 'ghi',
- attributes: {
- matchEvidence: [
- {
- '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'],
- osfmapPropertyPath: 'title',
- matchingHighlight: '... you said hello !',
- },
- ],
- },
- relationships: {
- indexCard: {
- data: {
- type: 'index-card',
- id: 'ghi',
- },
- links: {
- related: 'https://share.osf.io/api/v2/index-card/abc',
- },
- },
- },
- },
- {
- type: 'index-card',
- id: 'abc',
- attributes: {
- resourceIdentifier: [
- 'https://osf.example/abcfoo',
- 'https://doi.org/10.0000/osf.example/abcfoo',
- ],
- resourceMetadata: {
- resourceType: [
- 'osf:Registration',
- 'dcterms:Dataset',
- ],
- '@id': 'https://osf.example/abcfoo',
- '@type': 'osf:Registration',
- title: [
- {
- '@value': 'I shout hello!',
- '@language': 'en',
- },
- ],
- description: [
- {
- '@value': 'I say hello!',
- '@language': 'en',
- },
- ],
- isPartOf: [
- {
- '@id': 'https://osf.example/xyzfoo',
- '@type': 'osf:Registration',
- title: [
- {
- '@value': 'a parent!',
- '@language': 'en',
- },
- ],
- },
- ],
- hasPart: [
- {
- '@id': 'https://osf.example/deffoo',
- '@type': 'osf:Registration',
- title: [
- {
- '@value': 'a child!',
- '@language': 'en',
- },
- ],
- },
- {
- '@id': 'https://osf.example/ghifoo',
- '@type': 'osf:Registration',
- title: [
- {
- '@value': 'another child!',
- '@language': 'en',
- },
- ],
- },
- ],
- subject: [
- {
- '@id': 'https://subjects.org/subjectId',
- '@type': 'dcterms:Subject',
- label: [
- {
- '@value': 'wibbleplop',
- '@language': 'wi-bl',
- },
- ],
- },
- ],
- creator: [{
- '@id': 'https://osf.example/person',
- '@type': 'dcterms:Agent',
- specificType: 'foaf:Person',
- name: 'person person, prsn',
- }],
- },
- },
- links: {
- self: 'https://share.osf.io/api/v2/index-card/abc',
- resource: 'https://osf.example/abcfoo',
- },
- },
- {
- type: 'index-card',
- id: 'def',
- attributes: {
- resourceIdentifier: [
- 'https://osf.example/abcfoo',
- 'https://doi.org/10.0000/osf.example/abcfoo',
- ],
- resourceMetadata: {
- resourceType: [
- 'osf:Registration',
- 'dcterms:Dataset',
- ],
- '@id': 'https://osf.example/abcfoo',
- '@type': 'osf:Registration',
- title: [
- {
- '@value': 'Hi!',
- '@language': 'en',
- },
- ],
- },
- },
- links: {
- self: 'https://share.osf.io/api/v2/index-card/ghi',
- resource: 'https://osf.example/abcfoo',
- },
- },
- {
- type: 'index-card',
- id: 'ghi',
- attributes: {
- resourceIdentifier: [
- 'https://osf.example/abcfoo',
- 'https://doi.org/10.0000/osf.example/abcfoo',
- ],
- resourceMetadata: {
- resourceType: [
- 'osf:Registration',
- 'dcterms:Dataset',
- ],
- '@id': 'https://osf.example/abcfoo',
- '@type': 'osf:Registration',
- title: [
- {
- '@value': 'Ahoj! That\'s hello in Czech!',
- '@language': 'en',
- },
- ],
- description: [
- {
- '@value': 'Some description',
- '@language': 'en',
- },
- ],
- },
- },
- links: {
- self: 'https://share.osf.io/api/v2/index-card/ghi',
- resource: 'https://osf.example/abcfoo',
- },
- },
// Related properties
{
type: 'related-property-path',
@@ -415,6 +444,85 @@ export function cardSearch(_: Schema, __: Request) {
},
],
};
+
+ const searchResultPageRelationship = { data: [] as any[], links: {} as PaginationLinks};
+ const includedSearchResultPage: any[] = [];
+ const includedIndexCard: any[] = [];
+ Array.from({ length: pageSize }).forEach(() => {
+ const searchResultId = faker.random.uuid();
+ const indexCardId = faker.random.uuid();
+ const indexCardURL = `https://share.osf.io/api/v2/index-card/${indexCardId}`;
+ searchResultPageRelationship.data.push({
+ type: 'search-result',
+ id: searchResultId,
+ });
+ includedSearchResultPage.push({
+ type: 'search-result',
+ id: searchResultId,
+ attributes: {
+ matchEvidence: [
+ {
+ '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'],
+ evidenceCardIdentifier: indexCardURL,
+ matchingHighlight: [`...${faker.lorem.word()} ...`],
+ osfmapPropertyPath: ['description'],
+ propertyPathKey: 'description',
+ },
+ ],
+ },
+ relationships: {
+ indexCard: {
+ data: {
+ type: 'index-card',
+ id: indexCardId,
+ },
+ links: {
+ related: indexCardURL,
+ },
+ },
+ },
+ });
+ // pick a random resource type among the possible ones requested
+ const requestedResourceType: OsfmapResourceTypes
+ = requestedResourceTypes[Math.floor(Math.random() * requestedResourceTypes.length)];
+ const resourceTypeMetadata = resourceMetadataByType[requestedResourceType];
+ const osfGuid = fakeOsfIdentifier();
+ includedIndexCard.push({
+ type: 'index-card',
+ id: indexCardId,
+ attributes: {
+ resourceIdentifier: [
+ indexCardURL,
+ `https://doi.org/10.0000/osf.example/${indexCardId}`,
+ osfGuid,
+ ],
+ resourceMetadata: resourceTypeMetadata(),
+ },
+ links: {
+ self: indexCardURL,
+ resource: `https://osf.example/${indexCardId}`,
+ },
+ });
+ });
+
+ const cursorizedUrl = new URL(request.url);
+ cursorizedUrl.searchParams.set('page[cursor]', faker.random.uuid());
+ searchResultPageRelationship.links = {
+ next: {
+ href: cursorizedUrl.toString(),
+ },
+ };
+ if (pageCursor) {
+ searchResultPageRelationship.links.prev = {
+ href: cursorizedUrl.toString(),
+ };
+ searchResultPageRelationship.links.first = {
+ href: cursorizedUrl.toString(),
+ };
+ }
+ indexCardSearch.data.relationships.searchResultPage = searchResultPageRelationship;
+ indexCardSearch.included.push(...includedSearchResultPage, ...includedIndexCard);
+ return indexCardSearch;
}
export function valueSearch(_: Schema, __: Request) {
@@ -451,7 +559,7 @@ export function valueSearch(_: Schema, __: Request) {
],
links: {
next: {
- href: 'https://staging-share.osf.io/api/v3/index-value-search?page%5Bcursor%5D=lmnop',
+ href: 'https://staging-share.osf.io/trove/index-value-search?page%5Bcursor%5D=lmnop',
},
},
},
@@ -503,7 +611,7 @@ export function valueSearch(_: Schema, __: Request) {
id: property1Id,
attributes: {
resourceType: 'osf:Funder',
- resourceIdentifier: 'http://dx.doi.org/10.10000/505000005050',
+ resourceIdentifier: ['http://dx.doi.org/10.10000/505000005050'],
resourceMetadata: {
'@id': 'http://dx.doi.org/10.10000/505000005050',
'@type': 'datacite:Funder',
@@ -516,7 +624,7 @@ export function valueSearch(_: Schema, __: Request) {
id: property2Id,
attributes: {
resourceType: 'osf:Funder',
- resourceIdentifier: 'https://doi.org/10.10000/100000001',
+ resourceIdentifier: ['https://doi.org/10.10000/100000001'],
resourceMetadata: {
'@id': 'http://dx.doi.org/10.10000/100000001',
'@type': 'datacite:Funder',
@@ -527,3 +635,112 @@ export function valueSearch(_: Schema, __: Request) {
],
};
}
+
+function _sharePersonField() {
+ const fakeIdentifier = faker.internet.url();
+ return {
+ '@id': fakeIdentifier,
+ resourceType: [{ '@id': OsfmapResourceTypes.Person }, { '@id': OsfmapResourceTypes.Agent }],
+ identifier: [
+ {
+ '@value': 'https://orcid.org/0000-0000-0000-0000', // hard-coded as search-result looks for orcid URL
+ },
+ _shareIdentifierField(fakeIdentifier),
+ ],
+ // Pass an IRI to the _shareOrganizationField to create an organization with the same IRI
+ // as one specified in your mirage scenario
+ // e.g. in mirage scenario: server.create('institution', { iris: ['http://ror.org/has-users']});
+ affiliation: [_shareOrganizationField('http://ror.org/has-users')],
+ name: [{
+ '@value': faker.name.findName(),
+ }],
+ };
+}
+
+function _shareOrganizationField(orgId?: string) {
+ const identifier = orgId || faker.internet.url();
+ return {
+ '@id': identifier,
+ resourceType: [{ '@id': OsfmapResourceTypes.Organization }, { '@id': OsfmapResourceTypes.Agent }],
+ identifier: [_shareIdentifierField(identifier)],
+ name: [{
+ '@value': faker.company.companyName(),
+ }],
+ // sameAs: [{}], // some ROR
+ };
+}
+
+function _shareIdentifierField(idValue?: string) {
+ return {
+ '@value': idValue || faker.internet.url(),
+ };
+}
+function fakeOsfIdentifier() {
+ const id = guid('share-result')(Math.random());
+ return osfUrl + '/' + id;
+}
+
+function _shareOsfIdentifier(identifier?: string) {
+ return {
+ '@value': identifier || fakeOsfIdentifier(),
+ };
+}
+
+function _shareDateField() {
+ return {
+ '@value': _randomPastYearMonthDay(),
+ };
+}
+
+function _shareLicenseField() {
+ return {
+ '@id': 'http://creativecommons.org/licenses/by/4.0/',
+ identifier: [{
+ '@value': 'http://creativecommons.org/licenses/by/4.0/',
+ }],
+ name: [{
+ '@value': 'CC-BY-4.0',
+ }],
+ };
+}
+
+function _shareSubjectField() {
+ return {
+ '@id': 'https://api.osf.io/v2/subjects/584240da54be81056cecac48',
+ resourceType: [{ '@id': OsfmapResourceTypes.Concept }],
+ inScheme: [{
+ '@id': 'https://api.osf.io/v2/schemas/subjects/',
+ resourceType: [{ '@id': OsfmapResourceTypes.ConceptScheme }],
+ title: [{
+ '@value': 'bepress Digital Commons Three-Tiered Taxonomy',
+ }],
+ }],
+ prefLabel: [{
+ '@value': 'Social and Behavioral Sciences',
+ }],
+ };
+}
+
+function _shareUsageReportField() {
+ return {
+ temporalCoverage: [{
+ '@value': _randomPastYearMonthDay().slice(0, 7), // YYYY-MM
+ }],
+ viewCount: [{
+ '@value': faker.random.number(),
+ }],
+ downloadCount: [{
+ '@value': faker.random.number(),
+ }],
+ viewSessionCount: [{
+ '@value': faker.random.number(),
+ }],
+ downloadSessionCount: [{
+ '@value': faker.random.number(),
+ }],
+ };
+}
+
+function _randomPastYearMonthDay(): string {
+ return faker.date.past().toISOString().split('T')[0];
+}
diff --git a/tests/acceptance/institutions/dashboard-test.ts b/tests/acceptance/institutions/dashboard-test.ts
index b90ea119ca0..419ff7f172d 100644
--- a/tests/acceptance/institutions/dashboard-test.ts
+++ b/tests/acceptance/institutions/dashboard-test.ts
@@ -11,7 +11,7 @@ module(moduleName, hooks => {
setupOSFApplicationTest(hooks);
setupMirage(hooks);
- test('institutions dashboard', async function(assert) {
+ test('institutions dashboard: page layout', async function(assert) {
server.create('institution', {
id: 'has-users',
}, 'withMetrics');
@@ -21,9 +21,62 @@ module(moduleName, hooks => {
'/institutions/has-users/dashboard',
"Still at '/institutions/has-users/dashboard'.",
);
- await percySnapshot(`${moduleName} - default`);
- assert.dom('[data-test-next-page-button]').exists({ count: 1 }, 'next page button exists!?');
- await click('[data-test-next-page-button]');
- await percySnapshot(`${moduleName} - next page`);
+
+ assert.dom('[data-test-page-tab="summary"]').exists('Summary tab exists');
+ assert.dom('[data-test-page-tab="users"]').exists('Users tab exists');
+ assert.dom('[data-test-page-tab="projects"]').exists('Projects tab exists');
+ assert.dom('[data-test-page-tab="registrations"]').exists('Regitrations tab exists');
+ assert.dom('[data-test-page-tab="preprints"]').exists('Preprints tab exists');
+
+ // Summary tab
+ await percySnapshot(`${moduleName} - summary`);
+ assert.dom('[data-test-page-tab="summary"]').hasClass('active', 'Summary tab is active by default');
+
+ // Users tab
+ await click('[data-test-page-tab="users"]');
+ await percySnapshot(`${moduleName} - users`);
+ assert.dom('[data-test-page-tab="users"]').hasClass('active', 'Users tab is active');
+ assert.dom('[data-test-link-to-reports-archive]').exists('Link to download prior reports exists');
+ assert.dom('[data-test-download-dropdown]').exists('Link to download file formats');
+
+ // Projects tab
+ await click('[data-test-page-tab="projects"]');
+ await percySnapshot(`${moduleName} - projects`);
+ assert.dom('[data-test-page-tab="projects"]').hasClass('active', 'Projects tab is active');
+ assert.dom('[data-test-link-to-reports-archive]').exists('Link to download prior reports exists');
+ assert.dom('[data-test-download-dropdown]').exists('Link to download file formats');
+
+ // Registrations tab
+ await click('[data-test-page-tab="registrations"]');
+ await percySnapshot(`${moduleName} - registrations`);
+ assert.dom('[data-test-page-tab="registrations"]').hasClass('active', 'Registrations tab is active');
+ assert.dom('[data-test-link-to-reports-archive]').exists('Link to download prior reports exists');
+ assert.dom('[data-test-download-dropdown]').exists('Link to download file formats');
+
+ // Preprints tab
+ await click('[data-test-page-tab="preprints"]');
+ await percySnapshot(`${moduleName} - preprints`);
+ assert.dom('[data-test-page-tab="preprints"]').hasClass('active', 'Preprints tab is active');
+ assert.dom('[data-test-link-to-reports-archive]').exists('Link to download prior reports exists');
+ assert.dom('[data-test-download-dropdown]').exists('Link to download file formats');
+ });
+
+ test('institutions dashboard: projects, registrations, and preprints tab', async function(assert) {
+ server.create('institution', {
+ id: 'has-users',
+ }, 'withMetrics');
+
+ await visit('/institutions/has-users/dashboard');
+
+ for (const tab of ['projects', 'registrations', 'preprints']) {
+ await click(`[data-test-page-tab=${tab}]`);
+
+ assert.dom(`[data-test-page-tab=${tab}]`).hasClass('active', `${tab} tab is active`);
+ assert.dom('[data-test-object-list-table]').exists('Object list exists');
+ assert.dom('[data-test-object-count]').exists('Object count exists');
+ assert.dom('[data-test-toggle-filter-button]').exists('Filter button exists');
+ assert.dom('[data-test-customize-columns-button]').exists('Customize columns button exists');
+ }
});
});
+
diff --git a/tests/integration/components/contributors/component-test.ts b/tests/integration/components/contributors/component-test.ts
index 378f0826182..f3b3e783d55 100644
--- a/tests/integration/components/contributors/component-test.ts
+++ b/tests/integration/components/contributors/component-test.ts
@@ -64,7 +64,7 @@ module('Integration | Component | contributors', hooks => {
hbs` `,
);
contributors.forEach(contributor => {
- const userPermission = t(`osf-components.contributors.permissions.${contributor.permission}`);
+ const userPermission = t(`general.permissions.${contributor.permission}`);
const userCitation = t(`osf-components.contributors.citation.${contributor.bibliographic}`);
assert.dom('[data-test-contributor-card]').exists();
@@ -91,7 +91,7 @@ module('Integration | Component | contributors', hooks => {
await render(
hbs` `,
);
- const userPermission = t(`osf-components.contributors.permissions.${unregContributor.permission}`);
+ const userPermission = t(`general.permissions.${unregContributor.permission}`);
const userCitation = t(`osf-components.contributors.citation.${unregContributor.bibliographic}`);
assert.dom('[data-test-contributor-card]').exists();
diff --git a/tests/integration/components/sort-arrow/component-test.ts b/tests/integration/components/sort-arrow/component-test.ts
new file mode 100644
index 00000000000..9dfb5a6be9f
--- /dev/null
+++ b/tests/integration/components/sort-arrow/component-test.ts
@@ -0,0 +1,86 @@
+import { render, click } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupRenderingTest } from 'ember-qunit';
+import { module, test } from 'qunit';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { setupIntl } from 'ember-intl/test-support';
+
+module('Integration | Component | sort-arrow', function(hooks) {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+ setupIntl(hooks);
+
+ hooks.beforeEach(async function(this: TestContext) {
+ this.store = this.owner.lookup('service:store');
+ this.intl = this.owner.lookup('service:intl');
+ this.set('noop', () => {/* noop */});
+ });
+
+ test('it renders the correct icon when ascending', async function(assert) {
+ // Set properties before rendering the component
+ this.set('sortBy', 'user_name');
+ this.set('sort', 'user_name');
+
+ // Render the component
+ await render(hbs` `);
+
+ // Assert that the component is rendered with the correct data-test-sort attribute
+ assert
+ .dom('[data-test-sort="user_name"]')
+ .exists('The sort arrow button is rendered with the correct data-test-sort attribute');
+
+ // Assert that the correct icon is displayed for ascending sort
+ assert
+ .dom('[data-test-sort="user_name"] [data-icon="arrow-up"]')
+ .exists('Displays the correct arrow-up icon when sort is ascending');
+ });
+
+ test('it renders the correct icon when descending', async function(assert) {
+ this.set('sortBy', 'user_name');
+ this.set('sort', '-user_name');
+
+ await render(hbs` `);
+
+ assert
+ .dom('[data-test-sort="user_name"]')
+ .exists('The sort arrow button is rendered with the correct data-test-sort attribute');
+
+ assert
+ .dom('[data-test-sort="user_name"] [data-icon="arrow-down"]')
+ .exists('Displays the correct arrow-down icon when sort is descending');
+ });
+
+ test('it triggers the sort action on click', async function(assert) {
+ assert.expect(1);
+
+ this.set('sortBy', 'user_name');
+ this.set('sortAction', sortField => {
+ assert.strictEqual(
+ sortField,
+ 'user_name',
+ 'sortAction was called with the correct sort field',
+ );
+ });
+
+ await render(hbs` `);
+
+ await click('[data-test-sort="user_name"]');
+ });
+
+ test('it applies the correct attributes and classes', async function(assert) {
+ this.set('sortBy', 'user_name');
+ this.set('sort', 'user_name');
+
+ await render(hbs` `);
+
+ assert
+ .dom('[data-test-sort="user_name"]')
+ .hasAttribute('data-analytics-name', 'Sort user_name', 'The correct data-analytics-name is applied');
+ assert
+ .dom('[data-test-sort="user_name"]')
+ .hasAttribute('title', 'Sort descending', 'The correct title attribute is applied when ascending');
+ assert
+ .dom('[data-test-sort="user_name"]')
+ .hasAttribute('aria-label', 'Sort descending', 'The correct aria-label is applied when ascending');
+ });
+});
diff --git a/tests/integration/routes/institutions/dashboard/-components/institutional-users-list/component-test.ts b/tests/integration/routes/institutions/dashboard/-components/institutional-users-list/component-test.ts
index 17509c6ad2c..7ea72714ede 100644
--- a/tests/integration/routes/institutions/dashboard/-components/institutional-users-list/component-test.ts
+++ b/tests/integration/routes/institutions/dashboard/-components/institutional-users-list/component-test.ts
@@ -18,7 +18,7 @@ module('Integration | routes | institutions | dashboard | -components | institut
this.owner.register('service:router', OsfLinkRouterStub);
});
- test('it renders and paginates', async function(assert) {
+ test('it renders and paginates 9 default columns', async function(assert) {
server.create('institution', {
id: 'testinstitution',
}, 'withMetrics');
@@ -40,33 +40,35 @@ module('Integration | routes | institutions | dashboard | -components | institut
@institution={{this.model.taskInstance.institution}}
/>
`);
- assert.dom('[data-test-header-name]')
- .exists({ count: 1 }, '1 name header');
- assert.dom('[data-test-header-department]')
+ assert.dom('[data-test-header]')
+ .exists({ count: 9 }, '9 default headers');
+ assert.dom('[data-test-header="department"]')
.exists({ count: 1 }, '1 departments header');
- assert.dom('[data-test-header-public-projects]')
+ assert.dom('[data-test-header="orcid"]')
+ .exists('1 orcid header');
+ assert.dom('[data-test-header="publicProjects"]')
.exists({ count: 1 }, '1 public projects header');
- assert.dom('[data-test-header-private-projects]')
+ assert.dom('[data-test-header="privateProjects"]')
.exists({ count: 1 }, '1 private projects header');
- assert.dom('[data-test-item-name]')
- .exists({ count: 10 }, '10 in the list with a name');
- assert.dom('[data-test-item-department]')
+ assert.dom('[data-test-item]')
+ .exists({ count: 90 }, '90 items 10 rows and 9 columns by default');
+ assert.dom('[data-test-item="department"]')
.exists({ count: 10 }, '10 in the list with department');
- assert.dom('[data-test-item-public-projects]')
+ assert.dom('[data-test-item="publicProjects"]')
.exists({ count: 10 }, '10 in the list with public project');
- assert.dom('[data-test-item-private-projects]')
+ assert.dom('[data-test-item="privateProjects"]')
.exists({ count: 10 }, '10 in the list with private projects');
await click('[data-test-next-page-button]');
- assert.dom('[data-test-item-name]')
+ assert.dom('[data-test-item="user_name"]')
.exists({ count: 5 }, '5 in the list with a name');
- assert.dom('[data-test-item-department]')
+ assert.dom('[data-test-item="department"]')
.exists({ count: 5 }, '5 in the list with department');
- assert.dom('[data-test-item-public-projects]')
+ assert.dom('[data-test-item="publicProjects"]')
.exists({ count: 5 }, '5 in the list with public project');
- assert.dom('[data-test-item-private-projects]')
+ assert.dom('[data-test-item="privateProjects"]')
.exists({ count: 5 }, '5 in the list with private projects');
});
@@ -108,26 +110,26 @@ module('Integration | routes | institutions | dashboard | -components | institut
@institution={{this.model.taskInstance.institution}}
/>
`);
- assert.dom('[data-test-item-name]')
+ assert.dom('[data-test-item="user_name"]')
.exists({ count: 3 }, '3 users');
- await click('[data-test-ascending-sort="user_name"]');
- assert.dom('[data-test-item-name]')
- .containsText('Hulk Hogan', 'Sorts by name ascendening');
+ assert.dom('[data-test-item="user_name"]')
+ .containsText('Hulk Hogan', 'Sorts by name ascending by default');
- assert.dom('[data-test-item-name] a:first-of-type')
+ assert.dom('[data-test-item] a:first-of-type')
.hasAttribute('href');
- await click('[data-test-descending-sort="user_name"]');
- assert.dom('[data-test-item-name]')
- .containsText('John Doe', 'Sorts by name descendening');
+ await click('[data-test-sort="user_name"]');
+ assert.dom('[data-test-item]')
+ .containsText('John Doe', 'Sorts by name descending');
- await click('[data-test-ascending-sort="department"]');
- assert.dom('[data-test-item-department]')
- .hasText('Architecture', 'Sorts by department ascendening');
+ await click('[data-test-sort="department"]');
+ assert.dom('[data-test-item="department"]')
+ .hasText('Psychology', 'Sorts by department descending');
+
+ await click('[data-test-sort="department"]');
+ assert.dom('[data-test-item="department"]')
+ .hasText('Architecture', 'Sorts by department ascending');
- await click('[data-test-descending-sort="department"]');
- assert.dom('[data-test-item-department]')
- .hasText('Psychology', 'Sorts by department descendening');
});
});
diff --git a/tests/integration/routes/institutions/dashboard/-components/panel/component-test.ts b/tests/integration/routes/institutions/dashboard/-components/panel/component-test.ts
deleted file mode 100644
index 53ad4199aac..00000000000
--- a/tests/integration/routes/institutions/dashboard/-components/panel/component-test.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { render } from '@ember/test-helpers';
-import { hbs } from 'ember-cli-htmlbars';
-import { setupMirage } from 'ember-cli-mirage/test-support';
-import { setupRenderingTest } from 'ember-qunit';
-import { TestContext } from 'ember-test-helpers';
-import { module, test } from 'qunit';
-
-module('Integration | routes | institutions | dashboard | -components | panel', hooks => {
- setupRenderingTest(hooks);
- setupMirage(hooks);
-
- hooks.beforeEach(function(this: TestContext) {
- this.store = this.owner.lookup('service:store');
- });
-
- test('it renders while loading', async function(assert) {
- await render(hbs`
-
- Hello, World!
- `);
-
- assert.dom('[data-test-panel-title]')
- .exists({ count: 1 }, '1 title');
- assert.dom('[data-test-panel-title]')
- .hasText('Test');
- assert.dom('[data-test-panel-body]')
- .exists({ count: 1 }, '1 body');
- assert.dom('[data-test-panel-body]')
- .hasText('');
- assert.dom('[data-test-loading-indicator]')
- .exists({ count: 1 }, '1 loading indicator');
- });
-
- test('it renders after loading', async function(assert) {
- await render(hbs`
-
- Hello, World!
- `);
-
- assert.dom('[data-test-panel-title]')
- .exists({ count: 1 }, '1 title');
- assert.dom('[data-test-panel-title]')
- .hasText('Test');
- assert.dom('[data-test-panel-body]')
- .exists({ count: 1 }, '1 body');
- assert.dom('[data-test-panel-body]')
- .hasText('Hello, World!');
- assert.dom('[data-test-loading-indicator]')
- .doesNotExist();
- });
-});
diff --git a/translations/en-us.yml b/translations/en-us.yml
index 62dbc33b0f1..0416c52097d 100644
--- a/translations/en-us.yml
+++ b/translations/en-us.yml
@@ -77,6 +77,10 @@ general:
optional: Optional
optional_paren: (optional)
period: .
+ permissions:
+ admin: Administrator
+ read: Read
+ write: 'Read + Write'
please_confirm: 'Please confirm'
presented_by_osf: 'Presented by OSF'
previous: previous
@@ -87,6 +91,7 @@ general:
registration: registration
rename: Rename
required: Required
+ reset: Reset
revert: Revert
revisions: Revisions
save: Save
@@ -774,9 +779,11 @@ delete_modal:
type_this: 'Type the following to continue:'
input_label: 'Scientist name verification'
paginator:
+ first: '1'
next: 'Next page'
previous: 'Previous page'
page: 'Page {page} of {max}'
+ itemsPerPage: 'Items per page'
social:
email: Email
twitter: Twitter
@@ -792,9 +799,28 @@ institutions:
search_placeholder: 'Search institutions'
load_more: 'Load more institutions'
dashboard:
+ tabs:
+ summary: Summary
+ users: Users
+ projects: Projects
+ registrations: Registrations
+ preprints: Preprints
+ object-type-word:
+ projects: projects
+ registrations: registrations
+ preprints: preprints
+ content-placeholder: Content coming soon # Delete this eventually pls
title: '{institutionName} Dashboard'
- last_update: 'Updated every 24 hours'
+ download_past_reports_label: 'Previous reports'
+ download_dropdown_label: 'Download data'
download_csv: 'Download CSV'
+ download_tsv: 'Download TSV'
+ format_labels:
+ csv: '.csv'
+ tsv: '.tsv'
+ json_table: 'JSON (table)'
+ json_direct: 'JSON (direct)'
+ download: 'Download'
select_default: 'All Departments'
users_list:
name: Name
@@ -803,7 +829,82 @@ institutions:
public_projects: 'Public Projects'
private_projects: 'Private Projects'
empty: 'No users found matching search criteria.'
- users_connected_panel: 'SSO Users Connected'
+ public_registration_count: 'Public Registrations'
+ private_registration_count: 'Private Registrations'
+ published_preprint_count: 'Preprints'
+ orcid: ORCID
+ osf_link: 'OSF Link'
+ public_file_count: 'Files on OSF'
+ storage_byte_count: 'Total Data Stored on OSF'
+ account_created: 'Account Created'
+ month_last_login: 'Last Login'
+ month_last_active: 'Last Action'
+ has_orcid: 'Has ORCID'
+ select_columns: 'Customize'
+ total_users: 'total users'
+ not_found: 'N/A'
+ kpi-panel:
+ users: 'Total Users'
+ projects: 'OSF Public and Private Projects'
+ registrations: 'OSF Public and Embargoed Registrations'
+ preprints: 'OSF Preprints'
+ storage: 'Total Storage in {unit}'
+ file-count: 'Total Public File Count'
+ fileCount: 'Total Public File Count'
+ logged-in-users: 'Total Monthly Logged in Users'
+ active-users: 'Total Monthly Active Users'
+ kpi-chart:
+ open-expanded-data: 'Expand Additionnal Data'
+ close-expanded-data: 'Collapse Additionnal Data'
+ users-by-department: 'Total Users by Department'
+ public-vs-private-projects:
+ title: 'Public vs Private Projects'
+ public: 'Public Projects'
+ private: 'Private Projects'
+ public-vs-private-registrations:
+ title: 'Public vs Private Registrations'
+ public: 'Public Registrations'
+ private: 'Private Registrations'
+ total-osf-objects:
+ title: 'Total OSF Objects'
+ preprints: 'Preprints'
+ licenses: 'Top 10 Licenses'
+ add-ons: 'Top 10 Add-ons'
+ storage-regions: 'Top Storage Regions'
+ public-vs-private-data-storage:
+ title: 'Public vs Private Data Storage'
+ public: 'Public Data Storage'
+ private: 'Private Data Storage'
+ error: 'Error loading data'
+ object-list:
+ filter-button-label: 'Filter'
+ filter-heading: 'Filter by:'
+ total-objects: 'total {objectType}'
+ customize: 'Customize'
+ first: First
+ prev: Prev
+ next: Next
+ table-headers:
+ title: Title
+ link: Link
+ created_date: 'Created Date'
+ modified_date: 'Modified Date'
+ doi: DOI
+ storage_location: 'Storage Location'
+ total_data_stored: 'Total Data Stored on OSF'
+ contributor_name: 'Contributor Name'
+ contributor_permissions: 'Contributor Permissions'
+ view_count: Views (last 30 days)
+ download_count: Downloads (last 30 days)
+ resource_nature: 'Resource Type'
+ license: License
+ addons: Add-ons
+ funder_name: Funder Name
+ registration_schema: 'Registration Schema'
+ table-items:
+ missing-info: '-'
+ no-contributors: 'Contributor no longer affiliated'
+ permission-level: '({permissionLevel})'
projects_panel: 'Total Projects'
departments_panel: Departments
public: Public
@@ -886,10 +987,6 @@ app_components:
img_alt: Gravatar
in_citation_label: 'In citation:'
permissions_label: 'Permissions:'
- permissions:
- admin: Administrator
- write: 'Read + Write'
- read: Read
remove: Remove
remove_author: 'Remove author'
load_more_contributors: 'Load more contributors'
@@ -2663,10 +2760,6 @@ osf-components:
name: 'Name'
permission: 'Permission'
citation: 'Citation'
- permissions:
- admin: 'Administrator'
- write: 'Read + Write'
- read: 'Read'
citation:
true: 'Yes'
false: 'No'
diff --git a/types/ember-cli-chart.d.ts b/types/ember-cli-chart.d.ts
index 23c208ad83d..6d027a67b11 100644
--- a/types/ember-cli-chart.d.ts
+++ b/types/ember-cli-chart.d.ts
@@ -2,6 +2,7 @@ declare module 'ember-cli-chart' {
interface DataSet {
data?: number[];
backgroundColor?: string[];
+ fill?: boolean;
}
export interface ChartData {
@@ -15,6 +16,24 @@ declare module 'ember-cli-chart' {
display?: boolean,
};
onHover?: (_: MouseEvent, shapes: Shape[]) => void;
+ scales?: {
+ xAxes?: [
+ {
+ display?: boolean
+ ticks?: {
+ min?: number
+ }
+ }
+ ],
+ yAxes?: [
+ {
+ display?: boolean
+ ticks?: {
+ min?: number
+ }
+ }
+ ]
+ }
}
export interface Shape {