diff --git a/src/app/app.config.ts b/src/app/app.config.ts index d913ba378..ac3980d36 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -24,7 +24,7 @@ import * as Sentry from '@sentry/angular'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(routes, withInMemoryScrolling({ scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled' })), + provideRouter(routes, withInMemoryScrolling({ scrollPositionRestoration: 'top', anchorScrolling: 'enabled' })), provideStore(STATES, withNgxsReduxDevtoolsPlugin({ disabled: false })), providePrimeNG({ theme: { diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.html b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.html index 64258dcb7..515a58a39 100644 --- a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.html +++ b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.html @@ -13,66 +13,68 @@

{{ 'project.overview.dialog.fork.forksMessage' | translate }}

@for (duplicate of duplicates(); track duplicate.id) { - @if (duplicate.currentUserPermissions.includes(UserPermissions.Read)) { -
-
-

- - {{ duplicate.title }} -

+
+
+

+ + {{ duplicate.title }} +

-
- @if (duplicate.currentUserPermissions.includes(UserPermissions.Write)) { - - - } +
+ @if (showMoreOptions(duplicate)) { + + + } - - - {{ item.label | translate }} - - -
+ + + {{ item.label | translate }} + +
+
-
-
- {{ 'common.labels.forked' | translate }}: -

{{ duplicate.dateCreated | date: 'MMM d, y, h:mm a' }}

-
- -
- {{ 'common.labels.lastUpdated' | translate }}: -

{{ duplicate.dateModified | date: 'MMM d, y, h:mm a' }}

-
+
+
+ {{ 'common.labels.forked' | translate }}: +

{{ duplicate.dateCreated | date: 'MMM d, y, h:mm a' }}

+
-
- {{ 'common.labels.contributors' | translate }}: - @for (contributor of duplicate.contributors; track contributor.id) { -
- {{ contributor.fullName }} - {{ $last ? '' : ',' }} -
- } -
+
+ {{ 'common.labels.lastUpdated' | translate }}: +

{{ duplicate.dateModified | date: 'MMM d, y, h:mm a' }}

+
-
-
- {{ 'common.labels.description' | translate }}: - +
+ {{ 'common.labels.contributors' | translate }}: + @for (contributor of duplicate.contributors; track contributor.id) { +
+ {{ contributor.fullName }} + {{ $last ? '' : ',' }}
+ } +
+ +
+
+ {{ 'common.labels.description' | translate }}: +
-
- } + +
} @if (totalDuplicates() > pageSize) { diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.scss b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.scss index 878a3d10c..32a20a323 100644 --- a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.scss +++ b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.scss @@ -1,6 +1,3 @@ -@use "styles/variables" as var; -@use "styles/mixins" as mix; - :host { display: flex; flex-direction: column; @@ -8,7 +5,7 @@ } .duplicate-wrapper { - border: 1px solid var.$grey-2; - border-radius: mix.rem(12px); - color: var.$dark-blue-1; + border: 1px solid var(--grey-2); + border-radius: 0.75rem; + color: var(--dark-blue-1); } diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts index 50b567184..bea1cb9c1 100644 --- a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts +++ b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.ts @@ -40,6 +40,7 @@ import { import { ResourceType, UserPermissions } from '@osf/shared/enums'; import { IS_SMALL } from '@osf/shared/helpers'; import { ToolbarResource } from '@osf/shared/models'; +import { Duplicate } from '@osf/shared/models/duplicates'; import { ClearDuplicates, DuplicatesSelectors, GetAllDuplicates } from '@osf/shared/stores'; @Component({ @@ -165,6 +166,13 @@ export class ViewDuplicatesComponent { return null; }); + showMoreOptions(duplicate: Duplicate) { + return ( + duplicate.currentUserPermissions.includes(UserPermissions.Admin) || + duplicate.currentUserPermissions.includes(UserPermissions.Write) + ); + } + handleForkResource(): void { const toolbarResource = this.toolbarResource(); const dialogWidth = !this.isSmall() ? '95vw' : '450px'; diff --git a/src/app/features/files/pages/files/files.component.html b/src/app/features/files/pages/files/files.component.html index 235609f3a..c23a06b7c 100644 --- a/src/app/features/files/pages/files/files.component.html +++ b/src/app/features/files/pages/files/files.component.html @@ -59,7 +59,7 @@ - @if (!isReadonly() && !hasViewOnly()) { + @if (canEdit() && !hasViewOnly()) { { DialogService, provideMockStore({ signals: [ + { + selector: CurrentResourceSelectors.getResourceDetails, + value: testNode, + }, { selector: FilesSelectors.getRootFolders, value: getNodeFilesMappedData(), diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index 274020e0d..eb6f6bdcd 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -178,11 +178,15 @@ export class FilesComponent { readonly hasViewOnly = computed(() => hasViewOnlyParam(this.router)); - readonly isReadonly = computed( - () => - this.resourceDetails().isRegistration || - this.resourceDetails().currentUserPermissions.includes(UserPermissions.Read) - ); + readonly canEdit = computed(() => { + const details = this.resourceDetails(); + const hasAdminOrWrite = details.currentUserPermissions.some( + (permission) => permission === UserPermissions.Admin || permission === UserPermissions.Write + ); + + return !details.isRegistration && hasAdminOrWrite; + }); + readonly isViewOnlyDownloadable = computed(() => this.resourceType() === ResourceType.Registration); isButtonDisabled = computed(() => this.fileIsUploading() || this.isFilesLoading()); diff --git a/src/app/features/files/store/files.model.ts b/src/app/features/files/store/files.model.ts index 895245488..c00cdbe00 100644 --- a/src/app/features/files/store/files.model.ts +++ b/src/app/features/files/store/files.model.ts @@ -24,7 +24,7 @@ export interface FilesStateModel { isAnonymous: boolean; } -export const filesStateDefaults: FilesStateModel = { +export const FILES_STATE_DEFAULTS: FilesStateModel = { files: { data: [], isLoading: false, diff --git a/src/app/features/files/store/files.state.ts b/src/app/features/files/store/files.state.ts index 1bc51a8df..b5e2f7f8e 100644 --- a/src/app/features/files/store/files.state.ts +++ b/src/app/features/files/store/files.state.ts @@ -33,12 +33,12 @@ import { SetSort, UpdateTags, } from './files.actions'; -import { filesStateDefaults, FilesStateModel } from './files.model'; +import { FILES_STATE_DEFAULTS, FilesStateModel } from './files.model'; @Injectable() @State({ - name: 'filesState', - defaults: filesStateDefaults, + name: 'files', + defaults: FILES_STATE_DEFAULTS, }) export class FilesState { filesService = inject(FilesService); @@ -328,6 +328,6 @@ export class FilesState { @Action(ResetState) resetState(ctx: StateContext) { - ctx.patchState(filesStateDefaults); + ctx.patchState(FILES_STATE_DEFAULTS); } } diff --git a/src/app/features/profile/components/profile-information/profile-information.component.html b/src/app/features/profile/components/profile-information/profile-information.component.html index 7b1d7509a..37308942a 100644 --- a/src/app/features/profile/components/profile-information/profile-information.component.html +++ b/src/app/features/profile/components/profile-information/profile-information.component.html @@ -62,52 +62,52 @@

@if (currentUser()?.social?.github) { - + github } @if (currentUser()?.social?.scholar) { - + scholar } @if (currentUser()?.social?.twitter) { - + x(twitter) } @if (currentUser()?.social?.linkedIn) { - + linkedin } @if (currentUser()?.social?.impactStory) { - + impactstory } @if (currentUser()?.social?.baiduScholar) { - + baidu } @if (currentUser()?.social?.researchGate) { - + researchGate } @if (currentUser()?.social?.researcherId) { - + researchId } @if (currentUser()?.social?.ssrn) { - + ssrn } @if (currentUser()?.social?.academiaProfileID) { - + academia } @@ -118,7 +118,7 @@

} diff --git a/src/app/shared/mappers/search/search.mapper.ts b/src/app/shared/mappers/search/search.mapper.ts index 334cde403..59099d6d5 100644 --- a/src/app/shared/mappers/search/search.mapper.ts +++ b/src/app/shared/mappers/search/search.mapper.ts @@ -4,6 +4,7 @@ import { IndexCardDataJsonApi, ResourceModel } from '@shared/models'; export function MapResources(indexCardData: IndexCardDataJsonApi): ResourceModel { const resourceMetadata = indexCardData.attributes.resourceMetadata; const resourceIdentifier = indexCardData.attributes.resourceIdentifier; + return { absoluteUrl: resourceMetadata['@id'], resourceType: ResourceType[resourceMetadata.resourceType[0]['@id'] as keyof typeof ResourceType], diff --git a/src/app/shared/mocks/addon.mock.ts b/src/app/shared/mocks/addon.mock.ts index 068725a85..f547d3250 100644 --- a/src/app/shared/mocks/addon.mock.ts +++ b/src/app/shared/mocks/addon.mock.ts @@ -10,4 +10,5 @@ export const MOCK_ADDON: AddonModel = { supportedFeatures: ['ACCESS', 'UPDATE'], credentialsFormat: CredentialsFormat.ACCESS_SECRET_KEYS, providerName: 'Test Provider', + wbKey: 'github', }; diff --git a/src/app/shared/mocks/base-node.mock.ts b/src/app/shared/mocks/base-node.mock.ts new file mode 100644 index 000000000..808bf3203 --- /dev/null +++ b/src/app/shared/mocks/base-node.mock.ts @@ -0,0 +1,27 @@ +import { BaseNodeModel } from '../models'; + +export const testNode: BaseNodeModel = { + id: 'abc123', + title: 'Long-Term Effects of Climate Change', + description: + 'This project collects and analyzes climate change data across multiple regions to understand long-term environmental impacts.', + category: 'project', + customCitation: 'Doe, J. (2024). Long-Term Effects of Climate Change. OSF.', + dateCreated: '2024-05-10T14:23:00Z', + dateModified: '2025-09-01T09:45:00Z', + isRegistration: false, + isPreprint: true, + isFork: false, + isCollection: false, + isPublic: true, + tags: ['climate', 'environment', 'data-analysis'], + accessRequestsEnabled: true, + nodeLicense: { + copyrightHolders: ['CC0 1.0 Universal'], + year: '2025', + }, + currentUserPermissions: ['admin', 'read', 'write'], + currentUserIsContributor: true, + wikiEnabled: true, + rootParentId: 'nt29k', +}; diff --git a/src/app/shared/mocks/data.mock.ts b/src/app/shared/mocks/data.mock.ts index 1aeb92a45..60584caa3 100644 --- a/src/app/shared/mocks/data.mock.ts +++ b/src/app/shared/mocks/data.mock.ts @@ -11,6 +11,7 @@ export const MOCK_USER: User = { middleNames: '', suffix: '', dateRegistered: new Date('2024-01-01'), + acceptedTermsOfService: true, employment: [ { title: 'Software Engineer', diff --git a/src/app/shared/mocks/index.ts b/src/app/shared/mocks/index.ts index 9468edc46..6e4f9e011 100644 --- a/src/app/shared/mocks/index.ts +++ b/src/app/shared/mocks/index.ts @@ -1,5 +1,6 @@ export { MOCK_ADDON } from './addon.mock'; export * from './analytics.mock'; +export * from './base-node.mock'; export { CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK } from './cedar-metadata-data-template-json-api.mock'; export * from './contributors.mock'; export * from './custom-сonfirmation.service.mock'; diff --git a/src/app/shared/mocks/project-overview.mock.ts b/src/app/shared/mocks/project-overview.mock.ts index dcbecc95a..a513cde33 100644 --- a/src/app/shared/mocks/project-overview.mock.ts +++ b/src/app/shared/mocks/project-overview.mock.ts @@ -54,7 +54,6 @@ export const MOCK_PROJECT_OVERVIEW: ProjectOverview = { currentUserIsContributor: true, currentUserIsContributorOrGroupMember: true, wikiEnabled: false, - subjects: [], contributors: [], customCitation: null, forksCount: 0, diff --git a/src/app/shared/mocks/resource.mock.ts b/src/app/shared/mocks/resource.mock.ts index 7de6b6947..9c44df257 100644 --- a/src/app/shared/mocks/resource.mock.ts +++ b/src/app/shared/mocks/resource.mock.ts @@ -3,43 +3,75 @@ import { ResourceType } from '@shared/enums'; import { ResourceModel, ResourceOverview } from '@shared/models'; export const MOCK_RESOURCE: ResourceModel = { - id: 'https://api.osf.io/v2/resources/resource-123', + absoluteUrl: 'https://api.osf.io/v2/resources/resource-123', resourceType: ResourceType.Registration, title: 'Test Resource', description: 'This is a test resource', dateCreated: new Date('2024-01-15'), dateModified: new Date('2024-01-20'), creators: [ - { id: 'https://api.osf.io/v2/users/user1', name: 'John Doe' }, - { id: 'https://api.osf.io/v2/users/user2', name: 'Jane Smith' }, + { absoluteUrl: 'https://api.osf.io/v2/users/user1', name: 'John Doe' }, + { absoluteUrl: 'https://api.osf.io/v2/users/user2', name: 'Jane Smith' }, ], - from: { id: 'https://api.osf.io/v2/projects/project1', name: 'Test Project' }, - provider: { id: 'https://api.osf.io/v2/providers/provider1', name: 'Test Provider' }, - license: { id: 'https://api.osf.io/v2/licenses/license1', name: 'MIT License' }, + provider: { absoluteUrl: 'https://api.osf.io/v2/providers/provider1', name: 'Test Provider' }, + license: { absoluteUrl: 'https://api.osf.io/v2/licenses/license1', name: 'MIT License' }, registrationTemplate: 'Test Template', - identifier: '10.1234/test.123', - conflictOfInterestResponse: 'no-conflict-of-interest', - orcid: 'https://orcid.org/0000-0000-0000-0000', - hasDataResource: true, + identifiers: ['https://staging4.osf.io/a42ysd'], + doi: ['10.1234/abcd.5678'], + addons: ['github', 'dropbox'], + hasDataResource: 'true', hasAnalyticCodeResource: false, hasMaterialsResource: true, hasPapersResource: false, hasSupplementalResource: true, + language: 'en', + isPartOfCollection: { absoluteUrl: 'https://staging4.osf.io/123asd', name: 'collection' }, + funders: [ + { + absoluteUrl: 'https://funder.org/nasa/', + name: 'NASA', + }, + ], + affiliations: [{ absoluteUrl: 'https://university.edu/', name: 'Example University' }], + qualifiedAttribution: [ + { + agentId: 'agentId', + order: 1, + }, + ], }; export const MOCK_AGENT_RESOURCE: ResourceModel = { - id: 'https://api.osf.io/v2/users/user-123', + absoluteUrl: 'https://api.osf.io/v2/users/user-123', resourceType: ResourceType.Agent, title: 'Test User', description: 'This is a test user', dateCreated: new Date('2024-01-15'), dateModified: new Date('2024-01-20'), creators: [], - hasDataResource: false, + hasDataResource: 'false', hasAnalyticCodeResource: false, hasMaterialsResource: false, hasPapersResource: false, hasSupplementalResource: false, + identifiers: ['https://staging4.osf.io/123xca'], + language: 'en', + isPartOfCollection: { absoluteUrl: 'https://staging4.osf.io/123asd', name: 'collection' }, + doi: ['10.1234/abcd.5678'], + addons: ['github', 'dropbox'], + funders: [ + { + absoluteUrl: 'https://funder.org/nasa/', + name: 'NASA', + }, + ], + affiliations: [{ absoluteUrl: 'https://university.edu/', name: 'Example University' }], + qualifiedAttribution: [ + { + agentId: 'agentId', + order: 1, + }, + ], }; export const MOCK_RESOURCE_OVERVIEW: ResourceOverview = { diff --git a/src/styles/components/md-editor.scss b/src/styles/components/md-editor.scss index 844fb67ab..2d2be958a 100644 --- a/src/styles/components/md-editor.scss +++ b/src/styles/components/md-editor.scss @@ -5,6 +5,7 @@ position: relative; display: inline-flex; vertical-align: middle; + flex-wrap: wrap; .btn { padding: 0.3rem 0.5rem;