+
+
{{ '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) {
-
- }
-
+
+
{{ '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) {
+
+ }
+
+
+
+
+ {{ '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) {
-
+
}
@if (currentUser()?.social?.scholar) {
-
+
}
@if (currentUser()?.social?.twitter) {
-
+
}
@if (currentUser()?.social?.linkedIn) {
-
+
}
@if (currentUser()?.social?.impactStory) {
-
+
}
@if (currentUser()?.social?.baiduScholar) {
-
+
}
@if (currentUser()?.social?.researchGate) {
-
+
}
@if (currentUser()?.social?.researcherId) {
-
+
}
@if (currentUser()?.social?.ssrn) {
-
+
}
@if (currentUser()?.social?.academiaProfileID) {
-
+
}
@@ -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;