Skip to content

Commit

Permalink
feat: UILD-299: STORY: Repeatable fields | Delete function (#38)
Browse files Browse the repository at this point in the history
* feat: UILD-299: STORY: Repeatable fields | Delete function

* minor refactor

* unit tests
  • Loading branch information
s3fs authored Nov 21, 2024
1 parent 510e1ad commit 32aaa70
Show file tree
Hide file tree
Showing 24 changed files with 425 additions and 173 deletions.
1 change: 1 addition & 0 deletions src/common/constants/bibframe.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const IDENTIFIER_AS_VALUE: Record<string, { field: string; value: string
export const LOC_GOV_URI = 'http://id.loc.gov/';

export const PREV_ENTRY_PATH_INDEX = 2;
export const MIN_AMT_OF_SIBLING_ENTRIES_TO_BE_DELETABLE = 2;
export const GRANDPARENT_ENTRY_PATH_INDEX = PREV_ENTRY_PATH_INDEX + 1;

export const PROVISION_ACTIVITY_OPTIONS = [
Expand Down
8 changes: 8 additions & 0 deletions src/common/helpers/common.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,11 @@ export const getAdvancedFieldType = (struct: Record<string, unknown>): AdvancedF

return AdvancedFieldType.__fallback;
};

export const deleteFromSetImmutable = <T = unknown>(set: Set<T>, toDelete: T[]) => {
const clone = new Set([...set]);

toDelete.forEach(entry => clone.delete(entry));

return clone;
};
24 changes: 20 additions & 4 deletions src/common/hooks/useProfileSchema.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import { useRecoilState, useSetRecoilState } from 'recoil';
import state from '@state';
import { useServicesContext } from './useServicesContext';
import { deleteFromSetImmutable } from '@common/helpers/common.helper';

export const useProfileSchema = () => {
const { selectedEntriesService, schemaWithDuplicatesService } = useServicesContext() as Required<ServicesParams>;
const [schema, setSchema] = useRecoilState(state.config.schema);
const setSelectedEntries = useSetRecoilState(state.config.selectedEntries);
const setClonePrototypes = useSetRecoilState(state.config.clonePrototypes);
const setCollapsibleEntries = useSetRecoilState(state.ui.collapsibleEntries);
const setIsEdited = useSetRecoilState(state.status.recordIsEdited);
const setUserValues = useSetRecoilState(state.inputs.userValues);

const getSchemaWithCopiedEntries = (entry: SchemaEntry, selectedEntries: string[]) => {
selectedEntriesService.set(selectedEntries);
schemaWithDuplicatesService.set(schema);
schemaWithDuplicatesService.duplicateEntry(entry);
const newUuid = schemaWithDuplicatesService.duplicateEntry(entry);

setSelectedEntries(selectedEntriesService.get());
setClonePrototypes(prev => [...prev, entry.uuid]);
setCollapsibleEntries(prev => new Set(newUuid ? [...prev, entry.uuid, newUuid] : [...prev, entry.uuid]));
setSchema(schemaWithDuplicatesService.get());

setIsEdited(true);
};

const getSchemaWithDeletedEntries = (entry: SchemaEntry) => {
schemaWithDuplicatesService.set(schema);
const deletedUuids = schemaWithDuplicatesService.deleteEntry(entry);

setCollapsibleEntries(prev => deleteFromSetImmutable(prev, [entry.uuid]));
setSchema(schemaWithDuplicatesService.get());
setUserValues(prev => Object.fromEntries(Object.entries(prev).filter(([key]) => !deletedUuids?.includes(key))));

setIsEdited(true);
};

return { getSchemaWithCopiedEntries };
return { getSchemaWithCopiedEntries, getSchemaWithDeletedEntries };
};
Original file line number Diff line number Diff line change
Expand Up @@ -423,17 +423,6 @@ export class RecordToSchemaMappingService implements IRecordToSchemaMapping {
const newEntryUuid = this.repeatableFieldsService?.duplicateEntry(schemaUiElem, false) ?? '';
this.updatedSchema = this.repeatableFieldsService?.get();

// Parameters are defined for further proper duplication of repeatable subcomponents
const duplicatedElem = this.updatedSchema.get(newEntryUuid);

if (duplicatedElem) {
duplicatedElem.cloneOf = schemaUiElem.uuid;
duplicatedElem.clonedBy = [];
schemaUiElem.clonedBy = Array.isArray(schemaUiElem.clonedBy)
? [...schemaUiElem.clonedBy, newEntryUuid]
: [newEntryUuid];
}

this.schemaArray = Array.from(this.updatedSchema?.values() || []);

return { newEntryUuid };
Expand Down
2 changes: 2 additions & 0 deletions src/common/services/schema/schemaWithDuplicates.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export interface ISchemaWithDuplicates {
set: (schema: Schema) => void;

duplicateEntry: (entry: SchemaEntry, isManualDuplication?: boolean) => string | undefined;

deleteEntry: (entry: SchemaEntry) => string[] | undefined;
}
114 changes: 74 additions & 40 deletions src/common/services/schema/schemaWithDuplicates.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ import { ISelectedEntries } from '../selectedEntries/selectedEntries.interface';
import { getParentEntryUuid, getUdpatedAssociatedEntries } from '@common/helpers/schema.helper';
import { generateEmptyValueUuid } from '@common/helpers/complexLookup.helper';
import { IEntryPropertiesGeneratorService } from './entryPropertiesGenerator.interface';
import { MIN_AMT_OF_SIBLING_ENTRIES_TO_BE_DELETABLE } from '@common/constants/bibframe.constants';

export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService {
private isManualDuplication: boolean;

constructor(
private schema: Map<string, SchemaEntry>,
private readonly selectedEntriesService: ISelectedEntries,
private readonly entryPropertiesGeneratorService?: IEntryPropertiesGeneratorService,
) {
this.set(schema);
this.isManualDuplication = true;
}

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

duplicateEntry(entry: SchemaEntry, isManualDuplication = true) {
this.isManualDuplication = isManualDuplication;
const { uuid, path, children, constraints, clonedBy, cloneOf } = entry;
duplicateEntry(entry: SchemaEntry) {
const { uuid, path, children, constraints, uri = '' } = entry;

if (!constraints?.repeatable) return;

const updatedEntryUuid = uuidv4();
const updatedEntry = this.getCopiedEntry(entry, updatedEntryUuid, undefined, true);
const updatedEntry = this.getCopiedEntry(entry, updatedEntryUuid);
updatedEntry.children = this.getUpdatedChildren(children, updatedEntry);

const parentEntryUuid = getParentEntryUuid(path);
Expand All @@ -41,56 +38,82 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService
parentEntry,
originalEntryUuid: uuid,
updatedEntryUuid: updatedEntryUuid,
childEntryId: uri,
});

if (updatedParentEntry) {
this.schema.set(parentEntryUuid, updatedParentEntry);
this.schema.set(updatedEntryUuid, updatedEntry);

if (this.isManualDuplication && cloneOf) {
// dupicating the field that's a clone
// got to set the initial prototype's properties
const initialPrototype = this.schema.get(cloneOf)!;

this.schema.set(initialPrototype.uuid, {
...initialPrototype,
clonedBy: [...(initialPrototype.clonedBy ?? []), updatedEntryUuid],
});
} else {
this.schema.set(uuid, {
...entry,
clonedBy: this.isManualDuplication ? [...(clonedBy ?? []), updatedEntryUuid] : undefined,
});
}

this.updateDeletabilityAndPositioning(updatedParentEntry?.twinChildren?.[uri]);
this.entryPropertiesGeneratorService?.applyHtmlIdToEntries(this.schema);
}

this.isManualDuplication = true;
return updatedEntryUuid;
}

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

copiedEntry.uuid = updatedUuid;
copiedEntry.path = this.getUpdatedPath(path, updatedUuid, parentElemPath);
if (!deletable) return;

if (includeCloneInfo) {
copiedEntry.cloneIndex = cloneIndex + 1;
}
const parent = this.schema.get(getParentEntryUuid(path));
const twinSiblings = parent?.twinChildren?.[uri];

if (htmlId) {
this.entryPropertiesGeneratorService?.addEntryWithHtmlId(updatedUuid);
if (twinSiblings) {
const updatedTwinSiblings = twinSiblings?.filter(twinUuid => twinUuid !== uuid);

this.schema.set(parent.uuid, {
...parent,
twinChildren: {
...parent.twinChildren,
[uri]: updatedTwinSiblings,
},
children: parent.children?.filter(child => child !== uuid),
});

this.updateDeletabilityAndPositioning(updatedTwinSiblings);
}

if (this.isManualDuplication && includeCloneInfo) {
if (!copiedEntry.cloneOf) {
copiedEntry.cloneOf = uuid;
const deletedUuids: string[] = [];

this.deleteEntryAndChildren(entry, deletedUuids);

return deletedUuids;
}

private deleteEntryAndChildren(entry?: SchemaEntry, deletedUuids?: string[]) {
if (!entry) return;

const { children, uuid } = entry;

if (children) {
for (const child of children) {
this.deleteEntryAndChildren(this.schema.get(child), deletedUuids);
}
}

deletedUuids?.push(uuid);
this.schema.delete(uuid);
}

copiedEntry.clonedBy = undefined;
private updateDeletabilityAndPositioning(uuids: string[] = []) {
const deletable = uuids.length >= MIN_AMT_OF_SIBLING_ENTRIES_TO_BE_DELETABLE;

uuids.forEach((uuid, cloneIndex) =>
this.schema.set(uuid, { ...(this.schema.get(uuid) ?? {}), deletable, cloneIndex } as SchemaEntry),
);
}

private getCopiedEntry(entry: SchemaEntry, updatedUuid: string, parentElemPath?: string[]) {
const { path, htmlId } = entry;
const copiedEntry = cloneDeep(entry);

copiedEntry.uuid = updatedUuid;
copiedEntry.path = this.getUpdatedPath(path, updatedUuid, parentElemPath);

if (htmlId) {
this.entryPropertiesGeneratorService?.addEntryWithHtmlId(updatedUuid);
}

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

if (!entry || entry.cloneOf) return;
if (!entry) return;

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

copiedEntry.children = this.getUpdatedChildren(children, copiedEntry);
copiedEntry.clonedBy = [];

const { updatedEntry, controlledByEntry } = this.getUpdatedAssociatedEntries({
initialEntry: copiedEntry,
Expand Down Expand Up @@ -148,17 +170,29 @@ export class SchemaWithDuplicatesService implements ISchemaWithDuplicatesService
parentEntry,
originalEntryUuid,
updatedEntryUuid,
childEntryId,
}: {
parentEntry?: SchemaEntry;
originalEntryUuid: string;
updatedEntryUuid: string;
childEntryId?: string;
}) {
if (!parentEntry) return;

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

if (childEntryId) {
if (!updatedParentEntry.twinChildren) {
updatedParentEntry.twinChildren = {};
}

updatedParentEntry.twinChildren[childEntryId] = [
...new Set([...(updatedParentEntry.twinChildren[childEntryId] ?? []), originalEntryUuid, updatedEntryUuid]),
];
}

if (originalEntryIndex !== undefined && originalEntryIndex >= 0) {
// Add the UUID of the copied entry to the parent element's array of children,
// saving the order of the elements
Expand Down
40 changes: 22 additions & 18 deletions src/components/DuplicateGroup/DuplicateGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
import { FC, memo } from 'react';
import classNames from 'classnames';
import { Button, ButtonType } from '@components/Button';
import { IS_DISABLED_FOR_ALPHA } from '@common/constants/feature.constants';
import Plus16 from '@src/assets/plus-16.svg?react';
import Trash16 from '@src/assets/trash-16.svg?react';
import { getHtmlIdForSchemaControl } from '@common/helpers/schema.helper';
import { SchemaControlType } from '@common/constants/uiControls.constants';
import './DuplicateGroup.scss';

interface Props {
onClick?: VoidFunction;
onClickDuplicate?: VoidFunction;
onClickDelete?: VoidFunction;
hasDeleteButton?: boolean;
deleteDisabled?: boolean;
className?: string;
htmlId?: string;
}

export const DuplicateGroup: FC<Props> = memo(({ onClick, hasDeleteButton = true, className, htmlId }) => (
<div className={classNames(['duplicate-group', className])}>
<Button
data-testid={getHtmlIdForSchemaControl(SchemaControlType.Duplicate, htmlId)}
type={ButtonType.Icon}
onClick={onClick}
>
<Plus16 />
</Button>
{hasDeleteButton && (
export const DuplicateGroup: FC<Props> = memo(
({ onClickDuplicate, onClickDelete, hasDeleteButton = true, className, htmlId, deleteDisabled = true }) => (
<div className={classNames(['duplicate-group', className])}>
<Button
data-testid={getHtmlIdForSchemaControl(SchemaControlType.RemoveDuplicate, htmlId)}
data-testid={getHtmlIdForSchemaControl(SchemaControlType.Duplicate, htmlId)}
type={ButtonType.Icon}
disabled={IS_DISABLED_FOR_ALPHA}
onClick={onClickDuplicate}
>
<Trash16 />
<Plus16 />
</Button>
)}
</div>
));
{hasDeleteButton && (
<Button
data-testid={getHtmlIdForSchemaControl(SchemaControlType.RemoveDuplicate, htmlId)}
type={ButtonType.Icon}
disabled={deleteDisabled}
onClick={onClickDelete}
>
<Trash16 />
</Button>
)}
</div>
),
);
Loading

0 comments on commit 32aaa70

Please sign in to comment.