Skip to content

Commit

Permalink
UILD-410: Record generation refactoring (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
SKarolFolio authored Nov 26, 2024
1 parent 5b0b7b8 commit 6b737ce
Show file tree
Hide file tree
Showing 16 changed files with 1,051 additions and 240 deletions.
229 changes: 2 additions & 227 deletions src/common/helpers/profile.helper.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,7 @@
// https://redux.js.org/usage/structuring-reducers/normalizing-state-shape

import {
COMPLEX_GROUPS,
FORCE_INCLUDE_WHEN_DEPARSING,
GROUPS_WITHOUT_ROOT_WRAPPER,
GROUP_BY_LEVEL,
IGNORE_HIDDEN_PARENT_OR_RECORD_SELECTION,
LOOKUPS_WITH_SIMPLE_STRUCTURE,
NONARRAY_DROPDOWN_OPTIONS,
FORCE_EXCLUDE_WHEN_DEPARSING,
IDENTIFIER_AS_VALUE,
LOC_GOV_URI,
KEEP_VALUE_AS_IS,
OUTGOING_RECORD_IDENTIFIERS_TO_SWAP,
} from '@common/constants/bibframe.constants';
import { LOOKUPS_WITH_SIMPLE_STRUCTURE, LOC_GOV_URI } from '@common/constants/bibframe.constants';
import { BFLITE_URIS, TYPE_MAP } from '@common/constants/bibframeMapping.constants';
import { AdvancedFieldType } from '@common/constants/uiControls.constants';
import {
checkGroupIsNonBFMapped,
generateAdvancedFieldObject,
getAdvancedValuesField,
getLookupLabelKey,
selectNonBFMappedGroupData,
} from './schema.helper';

type TraverseSchema = {
schema: Map<string, SchemaEntry>;
userValues: UserValues;
selectedEntries?: string[];
container: Record<string, any>;
key: string;
index?: number;
shouldHaveRootWrapper?: boolean;
parentEntryType?: string;
nonBFMappedGroup?: NonBFMappedGroup;
};

const getNonArrayTypes = () => [AdvancedFieldType.hidden, AdvancedFieldType.dropdownOption, AdvancedFieldType.profile];
import { getLookupLabelKey } from './schema.helper';

export const hasElement = (collection: string[], uri?: string) => !!uri && collection.includes(uri);

Expand Down Expand Up @@ -95,174 +60,6 @@ export const getMappedLookupValue = ({
return mappedUri;
};

const traverseSchema = ({
schema,
userValues,
selectedEntries = [],
container,
key,
index = 0,
shouldHaveRootWrapper = false,
parentEntryType,
nonBFMappedGroup,
}: TraverseSchema) => {
const { children, uri, uriBFLite, bfid, type } = schema.get(key) || {};
const uriSelector = uriBFLite || uri;
const selector = (uriSelector && OUTGOING_RECORD_IDENTIFIERS_TO_SWAP[uriSelector]) || uriSelector || bfid;
const userValueMatch = userValues[key];
const shouldProceed = Object.keys(userValues)
.map(uuid => schema.get(uuid)?.path)
.flat()
.includes(key);

const isArray = !getNonArrayTypes().includes(type as AdvancedFieldType);
const isArrayContainer = !!selector && Array.isArray(container[selector]);
let updatedNonBFMappedGroup = nonBFMappedGroup;

if (
checkGroupIsNonBFMapped({
propertyURI: uri as string,
parentEntryType: parentEntryType as AdvancedFieldType,
type: type as AdvancedFieldType,
})
) {
const { nonBFMappedGroup: generatedNonBFMappedGroup } = selectNonBFMappedGroupData({
propertyURI: uri as string,
type: type as AdvancedFieldType,
parentEntryType: parentEntryType as AdvancedFieldType,
});

if (generatedNonBFMappedGroup) {
updatedNonBFMappedGroup = generatedNonBFMappedGroup as NonBFMappedGroup;
}
}

if (userValueMatch && uri && selector) {
const advancedValueField = getAdvancedValuesField(uriBFLite);

const withFormat = userValueMatch.contents.map(
({ id, label, meta: { uri, parentUri, type, basicLabel, srsId } = {} }) => {
if (KEEP_VALUE_AS_IS.includes(selector) || type === AdvancedFieldType.complex) {
return { id, label, srsId };
} else if (
((parentUri || uri) && (!advancedValueField || updatedNonBFMappedGroup)) ||
type === AdvancedFieldType.simple
) {
return generateLookupValue({
uriBFLite,
label,
basicLabel,
uri: uri ?? parentUri,
type: type as AdvancedFieldType,
nonBFMappedGroup: updatedNonBFMappedGroup,
});
} else if (advancedValueField) {
return generateAdvancedFieldObject({ advancedValueField, label });
} else {
return type ? { label } : label;
}
},
);

if (isArrayContainer && container[selector].length) {
// Add duplicated group
container[selector].push(...withFormat);
} else {
container[selector] = withFormat;
}
} else if (selector && (shouldProceed || index < GROUP_BY_LEVEL)) {
let containerSelector: RecursiveRecordSchema | RecursiveRecordSchema[] | string[];
let hasRootWrapper = shouldHaveRootWrapper;

const { profile: profileType, block, dropdownOption, groupComplex, hidden } = AdvancedFieldType;
const isGroupWithoutRootWrapper = hasElement(GROUPS_WITHOUT_ROOT_WRAPPER, uri);
const identifierAsValueSelection = IDENTIFIER_AS_VALUE[selector];

if (type === profileType) {
containerSelector = container;
} else if (
(type === block ||
(type === groupComplex && hasElement(COMPLEX_GROUPS, uri)) ||
(type === groupComplex && updatedNonBFMappedGroup) ||
shouldHaveRootWrapper ||
(FORCE_INCLUDE_WHEN_DEPARSING.includes(selector) && type !== hidden)) &&
!FORCE_EXCLUDE_WHEN_DEPARSING.includes(selector)
) {
if (type === dropdownOption && !selectedEntries.includes(key)) {
// Only fields from the selected option should be processed and saved
return;
}

// Groups like "Provision Activity" don't have "block" wrapper,
// their child elements like "dropdown options" are placed at the top level,
// where any other blocks are placed.
containerSelector = {};

if (isArrayContainer) {
// Add duplicated group
container[selector].push(containerSelector);
} else {
container[selector] = type === block ? containerSelector : [containerSelector];
}
} else if (type === dropdownOption) {
if (!selectedEntries.includes(key)) {
// Only fields from the selected option should be processed and saved
return;
}

containerSelector = {};

if (NONARRAY_DROPDOWN_OPTIONS.includes(selector)) {
container[selector] = containerSelector;
} else if (identifierAsValueSelection) {
containerSelector = {
[identifierAsValueSelection.field]: [identifierAsValueSelection.value],
};

container.push(containerSelector);
} else {
container.push({ [selector]: containerSelector });
}
} else if (
isGroupWithoutRootWrapper ||
type === hidden ||
type === groupComplex ||
IGNORE_HIDDEN_PARENT_OR_RECORD_SELECTION.includes(selector)
) {
// Some groups like "Provision Activity" should not have a root node,
// and they put their children directly in the block node.
containerSelector = container;

if (isGroupWithoutRootWrapper) {
hasRootWrapper = true;
}
} else {
containerSelector = isArray ? [] : {};

if (container[selector] && isArrayContainer) {
// Add duplicated group
containerSelector = container[selector];
} else {
container[selector] = containerSelector;
}
}

children?.forEach(uuid =>
traverseSchema({
schema,
userValues,
selectedEntries,
container: containerSelector,
key: uuid,
index: index + 1,
shouldHaveRootWrapper: hasRootWrapper,
parentEntryType: type,
nonBFMappedGroup: updatedNonBFMappedGroup,
}),
);
}
};

export const filterUserValues = (userValues: UserValues) =>
Object.values(userValues).reduce((accum, current) => {
const { contents, uuid } = current;
Expand All @@ -277,25 +74,3 @@ export const filterUserValues = (userValues: UserValues) =>

return accum;
}, {} as UserValues);

export const applyUserValues = (
schema: Map<string, SchemaEntry>,
initKey: string | null,
userInput: {
userValues: UserValues;
selectedEntries: string[];
},
) => {
const { userValues, selectedEntries } = userInput;

if (!Object.keys(userValues).length || !schema.size || !initKey) {
return;
}

const filteredValues = filterUserValues(userValues);
const result: Record<string, RecordEntry> = {};

traverseSchema({ schema, userValues: filteredValues, selectedEntries, container: result, key: initKey });

return result;
};
14 changes: 6 additions & 8 deletions src/common/hooks/useRecordControls.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { flushSync } from 'react-dom';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { applyUserValues } from '@common/helpers/profile.helper';
import { useRecoilState, useSetRecoilState } from 'recoil';
import {
postRecord,
putRecord,
Expand Down Expand Up @@ -32,6 +31,7 @@ import state from '@state';
import { useContainerEvents } from './useContainerEvents';
import { ApiErrorCodes, ExternalResourceIdType } from '@common/constants/api.constants';
import { checkHasErrorOfCodeType } from '@common/helpers/api.helper';
import { useRecordGeneration } from './useRecordGeneration';

type SaveRecordProps = {
asRefToNewRecord?: boolean;
Expand All @@ -49,11 +49,8 @@ type IBaseFetchRecord = {
export const useRecordControls = () => {
const [searchParams, setSearchParams] = useSearchParams();
const setIsLoading = useSetRecoilState(state.loadingState.isLoading);
const [userValues, setUserValues] = useRecoilState(state.inputs.userValues);
const schema = useRecoilValue(state.config.schema);
const setUserValues = useSetRecoilState(state.inputs.userValues);
const setSelectedProfile = useSetRecoilState(state.config.selectedProfile);
const initialSchemaKey = useRecoilValue(state.config.initialSchemaKey);
const selectedEntries = useRecoilValue(state.config.selectedEntries);
const [record, setRecord] = useRecoilState(state.inputs.record);
const setIsEdited = useSetRecoilState(state.status.recordIsEdited);
const setRecordStatus = useSetRecoilState(state.status.recordStatus);
Expand All @@ -72,6 +69,7 @@ export const useRecordControls = () => {
const { dispatchUnblockEvent, dispatchNavigateToOriginEventWithFallback } = useContainerEvents();
const [queryParams] = useSearchParams();
const isClone = queryParams.get(QueryParams.CloneOf);
const { generateRecord } = useRecordGeneration();

const fetchRecord = async (recordId: string, previewParams?: PreviewParams) => {
const profile = PROFILE_BFIDS.MONOGRAPH;
Expand Down Expand Up @@ -105,7 +103,7 @@ export const useRecordControls = () => {
isNavigatingBack = true,
shouldSetSearchParams = true,
}: SaveRecordProps = {}) => {
const parsed = applyUserValues(schema, initialSchemaKey, { selectedEntries, userValues });
const parsed = generateRecord();
const currentRecordId = record?.id;

if (!parsed) return;
Expand Down Expand Up @@ -190,7 +188,7 @@ export const useRecordControls = () => {
};

const saveLocalRecord = () => {
const parsed = applyUserValues(schema, initialSchemaKey, { userValues, selectedEntries });
const parsed = generateRecord();

if (!parsed) return;

Expand Down
24 changes: 24 additions & 0 deletions src/common/hooks/useRecordGeneration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useRecoilValue } from 'recoil';
import { useServicesContext } from './useServicesContext';
import state from '@state';

export const useRecordGeneration = () => {
const { recordGeneratorService } = useServicesContext();
const schema = useRecoilValue(state.config.schema);
const userValues = useRecoilValue(state.inputs.userValues);
const selectedEntries = useRecoilValue(state.config.selectedEntries);
const initialSchemaKey = useRecoilValue(state.config.initialSchemaKey);

const generateRecord = () => {
recordGeneratorService?.init({
schema,
initKey: initialSchemaKey,
userValues,
selectedEntries,
});

return recordGeneratorService?.generate();
};

return { generateRecord };
};
2 changes: 2 additions & 0 deletions src/common/services/record/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { SchemaTraverser } from './schemaTraverser';
export { RecordGenerator } from './record';
9 changes: 9 additions & 0 deletions src/common/services/record/record.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface IRecordGenerator {
init: (params: {
schema: Map<string, SchemaEntry>;
initKey: string | null;
userValues: UserValues;
selectedEntries: string[];
}) => void;
generate: () => Record<string, RecordEntry<RecursiveRecordSchema>> | undefined;
}
57 changes: 57 additions & 0 deletions src/common/services/record/record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { filterUserValues } from '@common/helpers/profile.helper';
import { SchemaTraverser } from './schemaTraverser';
import { IRecordGenerator } from './record.interface';

export class RecordGenerator implements IRecordGenerator {
private schema: Map<string, SchemaEntry>;
private initKey: string | null;
private userValues: UserValues;
private selectedEntries: string[];

constructor(private readonly schemaTraverser: SchemaTraverser) {
this.schemaTraverser = schemaTraverser;
this.schema = new Map();
this.initKey = null;
this.userValues = {};
this.selectedEntries = [];
}

init({
schema,
initKey,
userValues,
selectedEntries,
}: {
schema: Map<string, SchemaEntry>;
initKey: string | null;
userValues: UserValues;
selectedEntries: string[];
}) {
this.schema = schema;
this.initKey = initKey;
this.userValues = userValues;
this.selectedEntries = selectedEntries;
}

public generate() {
if (!Object.keys(this.userValues).length || !this.schema.size || !this.initKey) {
return;
}

const filteredValues = filterUserValues(this.userValues);
const result: Record<string, RecordEntry> = {};

this.schemaTraverser
.init({
schema: this.schema,
userValues: filteredValues,
selectedEntries: this.selectedEntries,
initialContainer: result,
})
.traverse({
key: this.initKey,
});

return result;
}
}
Loading

0 comments on commit 6b737ce

Please sign in to comment.