{
return (
{content}
+
);
}
diff --git a/app/containers/ProjectPage/ProjectPage.js b/app/containers/ProjectPage/ProjectPage.js
index e9ba6876..6266889c 100644
--- a/app/containers/ProjectPage/ProjectPage.js
+++ b/app/containers/ProjectPage/ProjectPage.js
@@ -596,7 +596,7 @@ class ProjectPage extends Component {
const { projects } = prevState;
const foundIndex = projects.findIndex((x) => x.id === project.id);
projects[foundIndex] = project;
- return { projects };
+ return { projects, selectedProject: project };
});
// The update project request will handle logging if it succeeds. No additional logging call is
diff --git a/app/services/project.js b/app/services/project.js
index bf0a768a..cfadb224 100644
--- a/app/services/project.js
+++ b/app/services/project.js
@@ -471,6 +471,9 @@ export default class ProjectService {
asset.attributes = {};
}
asset.attributes[details.name] = details.value;
+ if (details.clearDescendants) {
+ AssetUtil.clearArchivedAttributeForDescendants(asset);
+ }
} else {
return null;
}
diff --git a/app/utils/asset.js b/app/utils/asset.js
index dad475d5..45924d1e 100644
--- a/app/utils/asset.js
+++ b/app/utils/asset.js
@@ -207,8 +207,8 @@ export default class AssetUtil {
const notes =
asset && asset.notes
? asset.notes.map((n) => {
- return { ...n, uri: asset.uri };
- })
+ return { ...n, uri: asset.uri };
+ })
: [];
if (!asset || !asset.children) {
return notes.flat();
@@ -493,4 +493,44 @@ export default class AssetUtil {
}
return !FILE_IGNORE_LIST.includes(fileName);
}
+
+ /**
+ * Determine if an asset has any descendants that are explicitly archived.
+ * @param {object} asset The asset to check
+ * @returns true if any descendant is explicitly archived
+ */
+ static hasArchivedDescendants(asset) {
+ if (!asset || !asset.children) {
+ return false;
+ }
+
+ // Check if any direct child is archived
+ const hasArchivedChild = asset.children.some(child =>
+ child.attributes && child.attributes.archived
+ );
+
+ if (hasArchivedChild) {
+ return true;
+ }
+
+ // Recursively check children
+ return asset.children.some(child => AssetUtil.hasArchivedDescendants(child));
+ }
+
+ /**
+ * Recursively clear the archived attribute for all descendants of an asset.
+ * @param {object} asset The asset to clear descendants for
+ */
+ static clearArchivedAttributeForDescendants(asset) {
+ if (!asset || !asset.children) {
+ return;
+ }
+
+ asset.children.forEach(child => {
+ if (child.attributes && child.attributes.archived) {
+ child.attributes.archived = false;
+ }
+ AssetUtil.clearArchivedAttributeForDescendants(child);
+ });
+ }
}
diff --git a/test/utils/asset.spec.js b/test/utils/asset.spec.js
index 54898cb4..303ae043 100644
--- a/test/utils/asset.spec.js
+++ b/test/utils/asset.spec.js
@@ -1446,17 +1446,17 @@ describe('utils', () => {
expect(AssetUtil.isExternalAsset(undefined)).toBeFalse();
});
it('should return false for a null/undefined type', () => {
- expect(AssetUtil.isExternalAsset({type: null})).toBeFalse();
- expect(AssetUtil.isExternalAsset({type: undefined})).toBeFalse();
+ expect(AssetUtil.isExternalAsset({ type: null })).toBeFalse();
+ expect(AssetUtil.isExternalAsset({ type: undefined })).toBeFalse();
expect(AssetUtil.isExternalAsset({})).toBeFalse();
});
it('should return true for a URL', () => {
- expect(AssetUtil.isExternalAsset({type: Constants.AssetType.URL})).toBeTrue();
+ expect(AssetUtil.isExternalAsset({ type: Constants.AssetType.URL })).toBeTrue();
});
it('should return false for other types', () => {
- expect(AssetUtil.isExternalAsset({type: Constants.AssetType.FILE})).toBeFalse();
- expect(AssetUtil.isExternalAsset({type: 'not a url'})).toBeFalse();
- expect(AssetUtil.isExternalAsset({type: ''})).toBeFalse();
+ expect(AssetUtil.isExternalAsset({ type: Constants.AssetType.FILE })).toBeFalse();
+ expect(AssetUtil.isExternalAsset({ type: 'not a url' })).toBeFalse();
+ expect(AssetUtil.isExternalAsset({ type: '' })).toBeFalse();
});
});
@@ -1465,17 +1465,17 @@ describe('utils', () => {
expect(AssetUtil.getAssetNameForTree(null)).toBe('');
});
it('should return a name normally for a regular asset', () => {
- expect(AssetUtil.getAssetNameForTree({type: Constants.AssetType.FOLDER, uri: 'Test'})).toBe('Test');
+ expect(AssetUtil.getAssetNameForTree({ type: Constants.AssetType.FOLDER, uri: 'Test' })).toBe('Test');
});
it('should return the URL for an external asset with no name parameter', () => {
- expect(AssetUtil.getAssetNameForTree({type: Constants.AssetType.URL, uri: 'http://test.com'})).toBe('http://test.com');
- expect(AssetUtil.getAssetNameForTree({type: Constants.AssetType.URL, uri: 'http://test.com', name: null})).toBe('http://test.com');
- expect(AssetUtil.getAssetNameForTree({type: Constants.AssetType.URL, uri: 'http://test.com', name: undefined})).toBe('http://test.com');
- expect(AssetUtil.getAssetNameForTree({type: Constants.AssetType.URL, uri: 'http://test.com', name: ''})).toBe('http://test.com');
- expect(AssetUtil.getAssetNameForTree({type: Constants.AssetType.URL, uri: 'http://test.com', name: ' '})).toBe('http://test.com');
+ expect(AssetUtil.getAssetNameForTree({ type: Constants.AssetType.URL, uri: 'http://test.com' })).toBe('http://test.com');
+ expect(AssetUtil.getAssetNameForTree({ type: Constants.AssetType.URL, uri: 'http://test.com', name: null })).toBe('http://test.com');
+ expect(AssetUtil.getAssetNameForTree({ type: Constants.AssetType.URL, uri: 'http://test.com', name: undefined })).toBe('http://test.com');
+ expect(AssetUtil.getAssetNameForTree({ type: Constants.AssetType.URL, uri: 'http://test.com', name: '' })).toBe('http://test.com');
+ expect(AssetUtil.getAssetNameForTree({ type: Constants.AssetType.URL, uri: 'http://test.com', name: ' ' })).toBe('http://test.com');
});
it('should return a formatted name for a URL with a name', () => {
- expect(AssetUtil.getAssetNameForTree({type: Constants.AssetType.URL, uri: 'http://test.com', name: 'Test'})).toBe('Test (http://test.com)');
+ expect(AssetUtil.getAssetNameForTree({ type: Constants.AssetType.URL, uri: 'http://test.com', name: 'Test' })).toBe('Test (http://test.com)');
});
});
});
@@ -1509,19 +1509,75 @@ describe('utils', () => {
it('should return false when the asset has no attributes', () => {
expect(AssetUtil.isArchived({})).toBeFalsy();
- expect(AssetUtil.isArchived({attributes: null})).toBeFalsy();
- expect(AssetUtil.isArchived({attributes: undefined})).toBeFalsy();
+ expect(AssetUtil.isArchived({ attributes: null })).toBeFalsy();
+ expect(AssetUtil.isArchived({ attributes: undefined })).toBeFalsy();
});
it('should return false when the asset is not set as archived', () => {
expect(AssetUtil.isArchived({})).toBeFalsy();
- expect(AssetUtil.isArchived({attributes: null})).toBeFalsy();
- expect(AssetUtil.isArchived({attributes: {archived: null}})).toBeFalsy();
- expect(AssetUtil.isArchived({attributes: {archived: false}})).toBeFalsy();
+ expect(AssetUtil.isArchived({ attributes: null })).toBeFalsy();
+ expect(AssetUtil.isArchived({ attributes: { archived: null } })).toBeFalsy();
+ expect(AssetUtil.isArchived({ attributes: { archived: false } })).toBeFalsy();
});
it('should return false when the asset is set as archived', () => {
- expect(AssetUtil.isArchived({attributes: {archived: true}})).toBeTrue();
+ expect(AssetUtil.isArchived({ attributes: { archived: true } })).toBeTrue();
+ });
+ });
+
+ describe('hasArchivedDescendants', () => {
+ it('returns false for null/undefined', () => {
+ expect(AssetUtil.hasArchivedDescendants(null)).toBeFalsy();
+ expect(AssetUtil.hasArchivedDescendants(undefined)).toBeFalsy();
+ expect(AssetUtil.hasArchivedDescendants({})).toBeFalsy();
+ });
+
+ it('returns false when there are no explicitly archived descendants', () => {
+ const asset = {
+ children: [
+ { attributes: { archived: false } },
+ { children: [{ attributes: { archived: false } }] }
+ ]
+ };
+ expect(AssetUtil.hasArchivedDescendants(asset)).toBeFalsy();
+ });
+
+ it('returns true when a direct child is explicitly archived', () => {
+ const asset = {
+ children: [
+ { attributes: { archived: true } }
+ ]
+ };
+ expect(AssetUtil.hasArchivedDescendants(asset)).toBeTrue();
+ });
+
+ it('returns true when a deeper descendant is explicitly archived', () => {
+ const asset = {
+ children: [
+ { children: [{ attributes: { archived: true } }] }
+ ]
+ };
+ expect(AssetUtil.hasArchivedDescendants(asset)).toBeTrue();
+ });
+ });
+
+ describe('clearArchivedAttributeForDescendants', () => {
+ it('handles null/undefined gracefully', () => {
+ expect(() => { AssetUtil.clearArchivedAttributeForDescendants(null); }).not.toThrow();
+ expect(() => { AssetUtil.clearArchivedAttributeForDescendants(undefined); }).not.toThrow();
+ expect(() => { AssetUtil.clearArchivedAttributeForDescendants({}); }).not.toThrow();
+ });
+
+ it('clears archived attributes on all descendants', () => {
+ const asset = {
+ children: [
+ { attributes: { archived: true } },
+ { children: [{ attributes: { archived: true } }, { attributes: { archived: false } }] }
+ ]
+ };
+ AssetUtil.clearArchivedAttributeForDescendants(asset);
+ expect(asset.children[0].attributes.archived).toBeFalsy();
+ expect(asset.children[1].children[0].attributes.archived).toBeFalsy();
});
});
});