Skip to content

Commit 32aaa70

Browse files
authored
feat: UILD-299: STORY: Repeatable fields | Delete function (#38)
* feat: UILD-299: STORY: Repeatable fields | Delete function * minor refactor * unit tests
1 parent 510e1ad commit 32aaa70

File tree

24 files changed

+425
-173
lines changed

24 files changed

+425
-173
lines changed

src/common/constants/bibframe.constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export const IDENTIFIER_AS_VALUE: Record<string, { field: string; value: string
113113
export const LOC_GOV_URI = 'http://id.loc.gov/';
114114

115115
export const PREV_ENTRY_PATH_INDEX = 2;
116+
export const MIN_AMT_OF_SIBLING_ENTRIES_TO_BE_DELETABLE = 2;
116117
export const GRANDPARENT_ENTRY_PATH_INDEX = PREV_ENTRY_PATH_INDEX + 1;
117118

118119
export const PROVISION_ACTIVITY_OPTIONS = [

src/common/helpers/common.helper.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,11 @@ export const getAdvancedFieldType = (struct: Record<string, unknown>): AdvancedF
7979

8080
return AdvancedFieldType.__fallback;
8181
};
82+
83+
export const deleteFromSetImmutable = <T = unknown>(set: Set<T>, toDelete: T[]) => {
84+
const clone = new Set([...set]);
85+
86+
toDelete.forEach(entry => clone.delete(entry));
87+
88+
return clone;
89+
};
Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
11
import { useRecoilState, useSetRecoilState } from 'recoil';
22
import state from '@state';
33
import { useServicesContext } from './useServicesContext';
4+
import { deleteFromSetImmutable } from '@common/helpers/common.helper';
45

56
export const useProfileSchema = () => {
67
const { selectedEntriesService, schemaWithDuplicatesService } = useServicesContext() as Required<ServicesParams>;
78
const [schema, setSchema] = useRecoilState(state.config.schema);
89
const setSelectedEntries = useSetRecoilState(state.config.selectedEntries);
9-
const setClonePrototypes = useSetRecoilState(state.config.clonePrototypes);
10+
const setCollapsibleEntries = useSetRecoilState(state.ui.collapsibleEntries);
11+
const setIsEdited = useSetRecoilState(state.status.recordIsEdited);
12+
const setUserValues = useSetRecoilState(state.inputs.userValues);
1013

1114
const getSchemaWithCopiedEntries = (entry: SchemaEntry, selectedEntries: string[]) => {
1215
selectedEntriesService.set(selectedEntries);
1316
schemaWithDuplicatesService.set(schema);
14-
schemaWithDuplicatesService.duplicateEntry(entry);
17+
const newUuid = schemaWithDuplicatesService.duplicateEntry(entry);
1518

1619
setSelectedEntries(selectedEntriesService.get());
17-
setClonePrototypes(prev => [...prev, entry.uuid]);
20+
setCollapsibleEntries(prev => new Set(newUuid ? [...prev, entry.uuid, newUuid] : [...prev, entry.uuid]));
1821
setSchema(schemaWithDuplicatesService.get());
22+
23+
setIsEdited(true);
24+
};
25+
26+
const getSchemaWithDeletedEntries = (entry: SchemaEntry) => {
27+
schemaWithDuplicatesService.set(schema);
28+
const deletedUuids = schemaWithDuplicatesService.deleteEntry(entry);
29+
30+
setCollapsibleEntries(prev => deleteFromSetImmutable(prev, [entry.uuid]));
31+
setSchema(schemaWithDuplicatesService.get());
32+
setUserValues(prev => Object.fromEntries(Object.entries(prev).filter(([key]) => !deletedUuids?.includes(key))));
33+
34+
setIsEdited(true);
1935
};
2036

21-
return { getSchemaWithCopiedEntries };
37+
return { getSchemaWithCopiedEntries, getSchemaWithDeletedEntries };
2238
};

src/common/services/recordToSchemaMapping/recordToSchemaMapping.service.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -423,17 +423,6 @@ export class RecordToSchemaMappingService implements IRecordToSchemaMapping {
423423
const newEntryUuid = this.repeatableFieldsService?.duplicateEntry(schemaUiElem, false) ?? '';
424424
this.updatedSchema = this.repeatableFieldsService?.get();
425425

426-
// Parameters are defined for further proper duplication of repeatable subcomponents
427-
const duplicatedElem = this.updatedSchema.get(newEntryUuid);
428-
429-
if (duplicatedElem) {
430-
duplicatedElem.cloneOf = schemaUiElem.uuid;
431-
duplicatedElem.clonedBy = [];
432-
schemaUiElem.clonedBy = Array.isArray(schemaUiElem.clonedBy)
433-
? [...schemaUiElem.clonedBy, newEntryUuid]
434-
: [newEntryUuid];
435-
}
436-
437426
this.schemaArray = Array.from(this.updatedSchema?.values() || []);
438427

439428
return { newEntryUuid };

src/common/services/schema/schemaWithDuplicates.interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ export interface ISchemaWithDuplicates {
44
set: (schema: Schema) => void;
55

66
duplicateEntry: (entry: SchemaEntry, isManualDuplication?: boolean) => string | undefined;
7+
8+
deleteEntry: (entry: SchemaEntry) => string[] | undefined;
79
}

src/common/services/schema/schemaWithDuplicates.service.ts

Lines changed: 74 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,15 @@ import { ISelectedEntries } from '../selectedEntries/selectedEntries.interface';
44
import { getParentEntryUuid, getUdpatedAssociatedEntries } from '@common/helpers/schema.helper';
55
import { generateEmptyValueUuid } from '@common/helpers/complexLookup.helper';
66
import { IEntryPropertiesGeneratorService } from './entryPropertiesGenerator.interface';
7+
import { MIN_AMT_OF_SIBLING_ENTRIES_TO_BE_DELETABLE } from '@common/constants/bibframe.constants';
78

89
export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService {
9-
private isManualDuplication: boolean;
10-
1110
constructor(
1211
private schema: Map<string, SchemaEntry>,
1312
private readonly selectedEntriesService: ISelectedEntries,
1413
private readonly entryPropertiesGeneratorService?: IEntryPropertiesGeneratorService,
1514
) {
1615
this.set(schema);
17-
this.isManualDuplication = true;
1816
}
1917

2018
get() {
@@ -25,14 +23,13 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService
2523
this.schema = cloneDeep(schema);
2624
}
2725

28-
duplicateEntry(entry: SchemaEntry, isManualDuplication = true) {
29-
this.isManualDuplication = isManualDuplication;
30-
const { uuid, path, children, constraints, clonedBy, cloneOf } = entry;
26+
duplicateEntry(entry: SchemaEntry) {
27+
const { uuid, path, children, constraints, uri = '' } = entry;
3128

3229
if (!constraints?.repeatable) return;
3330

3431
const updatedEntryUuid = uuidv4();
35-
const updatedEntry = this.getCopiedEntry(entry, updatedEntryUuid, undefined, true);
32+
const updatedEntry = this.getCopiedEntry(entry, updatedEntryUuid);
3633
updatedEntry.children = this.getUpdatedChildren(children, updatedEntry);
3734

3835
const parentEntryUuid = getParentEntryUuid(path);
@@ -41,56 +38,82 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService
4138
parentEntry,
4239
originalEntryUuid: uuid,
4340
updatedEntryUuid: updatedEntryUuid,
41+
childEntryId: uri,
4442
});
4543

4644
if (updatedParentEntry) {
4745
this.schema.set(parentEntryUuid, updatedParentEntry);
4846
this.schema.set(updatedEntryUuid, updatedEntry);
4947

50-
if (this.isManualDuplication && cloneOf) {
51-
// dupicating the field that's a clone
52-
// got to set the initial prototype's properties
53-
const initialPrototype = this.schema.get(cloneOf)!;
54-
55-
this.schema.set(initialPrototype.uuid, {
56-
...initialPrototype,
57-
clonedBy: [...(initialPrototype.clonedBy ?? []), updatedEntryUuid],
58-
});
59-
} else {
60-
this.schema.set(uuid, {
61-
...entry,
62-
clonedBy: this.isManualDuplication ? [...(clonedBy ?? []), updatedEntryUuid] : undefined,
63-
});
64-
}
65-
48+
this.updateDeletabilityAndPositioning(updatedParentEntry?.twinChildren?.[uri]);
6649
this.entryPropertiesGeneratorService?.applyHtmlIdToEntries(this.schema);
6750
}
6851

69-
this.isManualDuplication = true;
7052
return updatedEntryUuid;
7153
}
7254

73-
private getCopiedEntry(entry: SchemaEntry, updatedUuid: string, parentElemPath?: string[], includeCloneInfo = false) {
74-
const { path, uuid, cloneIndex = 0, htmlId } = entry;
75-
const copiedEntry = cloneDeep(entry);
55+
deleteEntry(entry: SchemaEntry) {
56+
const { deletable, uuid, path, uri = '' } = entry;
7657

77-
copiedEntry.uuid = updatedUuid;
78-
copiedEntry.path = this.getUpdatedPath(path, updatedUuid, parentElemPath);
58+
if (!deletable) return;
7959

80-
if (includeCloneInfo) {
81-
copiedEntry.cloneIndex = cloneIndex + 1;
82-
}
60+
const parent = this.schema.get(getParentEntryUuid(path));
61+
const twinSiblings = parent?.twinChildren?.[uri];
8362

84-
if (htmlId) {
85-
this.entryPropertiesGeneratorService?.addEntryWithHtmlId(updatedUuid);
63+
if (twinSiblings) {
64+
const updatedTwinSiblings = twinSiblings?.filter(twinUuid => twinUuid !== uuid);
65+
66+
this.schema.set(parent.uuid, {
67+
...parent,
68+
twinChildren: {
69+
...parent.twinChildren,
70+
[uri]: updatedTwinSiblings,
71+
},
72+
children: parent.children?.filter(child => child !== uuid),
73+
});
74+
75+
this.updateDeletabilityAndPositioning(updatedTwinSiblings);
8676
}
8777

88-
if (this.isManualDuplication && includeCloneInfo) {
89-
if (!copiedEntry.cloneOf) {
90-
copiedEntry.cloneOf = uuid;
78+
const deletedUuids: string[] = [];
79+
80+
this.deleteEntryAndChildren(entry, deletedUuids);
81+
82+
return deletedUuids;
83+
}
84+
85+
private deleteEntryAndChildren(entry?: SchemaEntry, deletedUuids?: string[]) {
86+
if (!entry) return;
87+
88+
const { children, uuid } = entry;
89+
90+
if (children) {
91+
for (const child of children) {
92+
this.deleteEntryAndChildren(this.schema.get(child), deletedUuids);
9193
}
94+
}
95+
96+
deletedUuids?.push(uuid);
97+
this.schema.delete(uuid);
98+
}
9299

93-
copiedEntry.clonedBy = undefined;
100+
private updateDeletabilityAndPositioning(uuids: string[] = []) {
101+
const deletable = uuids.length >= MIN_AMT_OF_SIBLING_ENTRIES_TO_BE_DELETABLE;
102+
103+
uuids.forEach((uuid, cloneIndex) =>
104+
this.schema.set(uuid, { ...(this.schema.get(uuid) ?? {}), deletable, cloneIndex } as SchemaEntry),
105+
);
106+
}
107+
108+
private getCopiedEntry(entry: SchemaEntry, updatedUuid: string, parentElemPath?: string[]) {
109+
const { path, htmlId } = entry;
110+
const copiedEntry = cloneDeep(entry);
111+
112+
copiedEntry.uuid = updatedUuid;
113+
copiedEntry.path = this.getUpdatedPath(path, updatedUuid, parentElemPath);
114+
115+
if (htmlId) {
116+
this.entryPropertiesGeneratorService?.addEntryWithHtmlId(updatedUuid);
94117
}
95118

96119
return copiedEntry;
@@ -104,7 +127,7 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService
104127
children?.forEach((entryUuid: string, index: number) => {
105128
const entry = this.schema.get(entryUuid);
106129

107-
if (!entry || entry.cloneOf) return;
130+
if (!entry) return;
108131

109132
const { children } = entry;
110133
let updatedEntryUuid = newUuids?.[index] ?? uuidv4();
@@ -119,7 +142,6 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService
119142
this.schema.set(updatedEntryUuid, copiedEntry);
120143

121144
copiedEntry.children = this.getUpdatedChildren(children, copiedEntry);
122-
copiedEntry.clonedBy = [];
123145

124146
const { updatedEntry, controlledByEntry } = this.getUpdatedAssociatedEntries({
125147
initialEntry: copiedEntry,
@@ -148,17 +170,29 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService
148170
parentEntry,
149171
originalEntryUuid,
150172
updatedEntryUuid,
173+
childEntryId,
151174
}: {
152175
parentEntry?: SchemaEntry;
153176
originalEntryUuid: string;
154177
updatedEntryUuid: string;
178+
childEntryId?: string;
155179
}) {
156180
if (!parentEntry) return;
157181

158182
const updatedParentEntry = cloneDeep(parentEntry);
159183
const { children } = updatedParentEntry;
160184
const originalEntryIndex = children?.indexOf(originalEntryUuid);
161185

186+
if (childEntryId) {
187+
if (!updatedParentEntry.twinChildren) {
188+
updatedParentEntry.twinChildren = {};
189+
}
190+
191+
updatedParentEntry.twinChildren[childEntryId] = [
192+
...new Set([...(updatedParentEntry.twinChildren[childEntryId] ?? []), originalEntryUuid, updatedEntryUuid]),
193+
];
194+
}
195+
162196
if (originalEntryIndex !== undefined && originalEntryIndex >= 0) {
163197
// Add the UUID of the copied entry to the parent element's array of children,
164198
// saving the order of the elements
Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
11
import { FC, memo } from 'react';
22
import classNames from 'classnames';
33
import { Button, ButtonType } from '@components/Button';
4-
import { IS_DISABLED_FOR_ALPHA } from '@common/constants/feature.constants';
54
import Plus16 from '@src/assets/plus-16.svg?react';
65
import Trash16 from '@src/assets/trash-16.svg?react';
76
import { getHtmlIdForSchemaControl } from '@common/helpers/schema.helper';
87
import { SchemaControlType } from '@common/constants/uiControls.constants';
98
import './DuplicateGroup.scss';
109

1110
interface Props {
12-
onClick?: VoidFunction;
11+
onClickDuplicate?: VoidFunction;
12+
onClickDelete?: VoidFunction;
1313
hasDeleteButton?: boolean;
14+
deleteDisabled?: boolean;
1415
className?: string;
1516
htmlId?: string;
1617
}
1718

18-
export const DuplicateGroup: FC<Props> = memo(({ onClick, hasDeleteButton = true, className, htmlId }) => (
19-
<div className={classNames(['duplicate-group', className])}>
20-
<Button
21-
data-testid={getHtmlIdForSchemaControl(SchemaControlType.Duplicate, htmlId)}
22-
type={ButtonType.Icon}
23-
onClick={onClick}
24-
>
25-
<Plus16 />
26-
</Button>
27-
{hasDeleteButton && (
19+
export const DuplicateGroup: FC<Props> = memo(
20+
({ onClickDuplicate, onClickDelete, hasDeleteButton = true, className, htmlId, deleteDisabled = true }) => (
21+
<div className={classNames(['duplicate-group', className])}>
2822
<Button
29-
data-testid={getHtmlIdForSchemaControl(SchemaControlType.RemoveDuplicate, htmlId)}
23+
data-testid={getHtmlIdForSchemaControl(SchemaControlType.Duplicate, htmlId)}
3024
type={ButtonType.Icon}
31-
disabled={IS_DISABLED_FOR_ALPHA}
25+
onClick={onClickDuplicate}
3226
>
33-
<Trash16 />
27+
<Plus16 />
3428
</Button>
35-
)}
36-
</div>
37-
));
29+
{hasDeleteButton && (
30+
<Button
31+
data-testid={getHtmlIdForSchemaControl(SchemaControlType.RemoveDuplicate, htmlId)}
32+
type={ButtonType.Icon}
33+
disabled={deleteDisabled}
34+
onClick={onClickDelete}
35+
>
36+
<Trash16 />
37+
</Button>
38+
)}
39+
</div>
40+
),
41+
);

0 commit comments

Comments
 (0)