Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/folio-org/ui-linked-data
Browse files Browse the repository at this point in the history
…into refactor/UILD-405-change-state-management-library
  • Loading branch information
SKarolFolio committed Dec 10, 2024
2 parents bd5ceaa + 3d6e798 commit 562a99e
Show file tree
Hide file tree
Showing 32 changed files with 725 additions and 183 deletions.
57 changes: 37 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# @folio/linked-data

This project is a new MARVA editor.
© 2024 EBSCO Information Services.

This software is distributed under the terms of the Apache License, Version 2.0. See the file "[LICENSE](LICENSE)" for more information.

## Introduction
UI application designed for performing operations on a library's linked data graph. This application can run standalone outside of the FOLIO platform. The [ui-ld-folio-wrapper](https://github.com/folio-org/ui-ld-folio-wrapper) module integrates this application for use within FOLIO.

## Table of Contents

Expand All @@ -24,39 +29,45 @@ or Yarn:
yarn install
```

### 2. Provide API config:

#### a) For an external API:
### 2. Configure the application:

Create a JSON object with required data, then use it in such cases:

- For development or using MARVA as a standalone application, in a browser's localstorage create a record with a key `okapi_config` and stringified JSON value (see "JSON config example");
- For development or using Linked data as a standalone application, in a browser's localstorage create a record with a key `okapi_config` and stringified JSON value (see "JSON config example");

- For an embedded application:
Use JSON in the `config` attribute, see [Usage](#usage) section.

###### JSON config example:
###### Configuration options:
* `basePath`: Backend URI to which requests from the frontend are going to be directed.
* `tenant`: Okapi tenant.
* `token`: Okapi token.
* `customEvents`: A dictionary with custom event names. The keys of this dictionary have to be specific while the values can be arbitrary but unique. Events are used to communicate between this application and its container when running in embedded mode.
* `TRIGGER_MODAL`: Root application can dispatch this event to open a prompt in Linked data application which will inform a user about unsaved changes before leaving an Edit or Create page.
* `PROCEED_NAVIGATION`: Linked data application dispatches this event when a user clicks in the prompt "Save and continue" button or closes the prompt.
* `BLOCK_NAVIGATION`: Linked data application dispatches this event when user makes changes in a work form ("Create" or "Edit" page).
* `UNBLOCK_NAVIGATION`: Root application can dispatch this event to allow Linked data to proceed its navigation after it's been blocked.
* `NAVIGATE_TO_ORIGIN`: Linked data application dispatches this event when there is a need to navigate to the entrypoint from where the navigation to the Linked data application happened.
* `DROP_NAVIGATE_TO_ORIGIN`: Linked data application dispatches this event when there is no longer a need to navigate to the entrypoint from where the navigation to the Linked data application happened. Subsequent `NAVIGATE_TO_ORIGIN` calls have no effect unless a new navigation origin is set within the root application.

###### Configuration example:

```json
{
"basePath": "YOUR_API_URI",
"tenant": "YOUR_TENANT",
"token": "YOUR_TOKEN",
// For embedded application only. Events also should be dispatched or listened in the root application.
"customEvents": {
"TRIGGER_MODAL": "triggermodal", // Root application can dispatch this event to open a prompt in MARVA which will inform a user about unsaved changes before leaving an Edit or Create page.
"PROCEED_NAVIGATION": "proceednavigation", // MARVA dispatches this event when a user clicks in the prompt "Save and continue" button or closes the prompt.
"BLOCK_NAVIGATION": "blocknavigation" // MARVA dispatches this event when user makes changes in a work form ("Create" or "Edit" page).
"TRIGGER_MODAL": "TRIGGER_MODAL",
"PROCEED_NAVIGATION": "PROCEED_NAVIGATION",
"BLOCK_NAVIGATION": "BLOCK_NAVIGATION",
"UNBLOCK_NAVIGATION": "UNBLOCK_NAVIGATION",
"NAVIGATE_TO_ORIGIN": "NAVIGATE_TO_ORIGIN",
"DROP_NAVIGATE_TO_ORIGIN": "DROP_NAVIGATE_TO_ORIGIN"
}
}
```

#### b) For an opened API (e.g. locally started. Can be used for development purposes):

1. Rename `.env` file to `.env.local`.

2. In that file change `EDITOR_API_BASE_PATH` variable's value.

## Scripts

The following scripts are available:
Expand Down Expand Up @@ -85,13 +96,19 @@ The following scripts are available:
1. Build the code as an embedded application using `npm run build:lib` command. The built code will be placed in `./dist` folder.
2. Add the script on a page:

```html
<script src="[PATH_TO_SCRIPT]/linked-data.es.js"></script>
```
1. As a package in the files where you plan to use the application if it was added to your project via package management tools:
```js
import '@folio/linked-data';
```
2. Or as a script:
```html
<script src="[PATH_TO_SCRIPT]/linked-data.es.js"></script>
```


3. Add a web component in the required HTML container on the page.

Use a config with a required API config for passing it in the MARVA application through the web component (see JSON config example in [Installation](#installation) section):
Use a config with a required API config for passing it in the Linked data application through the web component (see JSON config example in [Installation](#installation) section):

```html
<div id="linked-data-container">
Expand Down
8 changes: 8 additions & 0 deletions src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ body > #editor-root,
// font-size: 16px;
}

h2,
h3,
h4,
h5,
h6 {
margin: 0;
}

#app-root {
display: flex;
flex-direction: column;
Expand Down
20 changes: 16 additions & 4 deletions src/common/constants/bibframeMapping.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const BFLITE_URIS = {
CLASSIFICATION: 'http://bibfra.me/vocab/lite/classification',
PROVISION_ACTIVITY: 'https://bibfra.me/vocab/marc/provisionActivity',
TITLE: 'http://bibfra.me/vocab/marc/title',
TITLE_CONTAINER: 'http://bibfra.me/vocab/marc/Title',
MAIN_TITLE: 'http://bibfra.me/vocab/marc/mainTitle',
PRODUCTION: 'http://bibfra.me/vocab/marc/production',
PUBLICATION: 'http://bibfra.me/vocab/marc/publication',
Expand Down Expand Up @@ -215,29 +216,37 @@ export const BF_URIS = {
LABEL: 'http://www.w3.org/2000/01/rdf-schema#label',
};

export const INSTANCE_REF_KEY = "_instanceReference"
export const WORK_REF_KEY = "_workReference"

export const BLOCKS_BFLITE = {
INSTANCE: {
uri: BFLITE_URIS.INSTANCE,
referenceKey: '_instanceReference',
referenceKey: INSTANCE_REF_KEY,
resourceType: ResourceType.instance,
reference: {
key: '_workReference',
key: WORK_REF_KEY,
uri: BFLITE_URIS.WORK,
name: ResourceType.work,
},
},
WORK: {
uri: BFLITE_URIS.WORK,
referenceKey: '_workReference',
referenceKey: WORK_REF_KEY,
resourceType: ResourceType.work,
reference: {
key: '_instanceReference',
key: INSTANCE_REF_KEY,
uri: BFLITE_URIS.INSTANCE,
name: ResourceType.instance,
},
},
};

export const REF_TO_NAME = {
[INSTANCE_REF_KEY]: ResourceType.instance,
[WORK_REF_KEY]: ResourceType.work,
}

export const BFLITE_BFID_TO_BLOCK = {
'lc:RT:bf2:Monograph:Instance': BLOCKS_BFLITE.INSTANCE,
'lc:RT:bf2:Monograph:Work': BLOCKS_BFLITE.WORK,
Expand Down Expand Up @@ -609,6 +618,9 @@ export const TYPE_MAP = {

export const NEW_BF2_TO_BFLITE_MAPPING = {
[BFLITE_URIS.INSTANCE]: {
[BFLITE_URIS.INSTANCE]: {
container: { bf2Uri: 'http://id.loc.gov/ontologies/bibframe/Instance' }
},
'http://bibfra.me/vocab/marc/title': {
container: { bf2Uri: 'http://id.loc.gov/ontologies/bibframe/title' },
options: {
Expand Down
30 changes: 24 additions & 6 deletions src/common/helpers/record.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
TYPE_URIS,
} from '@common/constants/bibframe.constants';
import { formatRecord } from './recordFormatting.helper';
import { BFLITE_URI_TO_BLOCK, BFLITE_URIS, BLOCKS_BFLITE } from '@common/constants/bibframeMapping.constants';
import {
BFLITE_URI_TO_BLOCK,
BFLITE_URIS,
BLOCKS_BFLITE,
REF_TO_NAME,
} from '@common/constants/bibframeMapping.constants';
import { ResourceType } from '@common/constants/record.constants';
import { QueryParams } from '@common/constants/routes.constants';
import { cloneDeep } from 'lodash';
Expand Down Expand Up @@ -216,7 +221,7 @@ export const getPreviewFieldsConditions = ({
schema,
isOnBranchWithUserValue,
altDisplayNames,
hideActions,
hideEntities,
isEntity,
forceRenderAllTopLevelEntities,
}: {
Expand All @@ -227,7 +232,7 @@ export const getPreviewFieldsConditions = ({
schema: Schema;
isOnBranchWithUserValue: boolean;
altDisplayNames?: Record<string, string>;
hideActions?: boolean;
hideEntities?: boolean;
isEntity: boolean;
forceRenderAllTopLevelEntities?: boolean;
}) => {
Expand All @@ -241,7 +246,7 @@ export const getPreviewFieldsConditions = ({
const isBranchEndWithoutValues = !selectedUserValues && isBranchEnd;
const isBranchEndWithValues = !!selectedUserValues;
const shouldRenderLabelOrPlaceholders =
(isPreviewable && isGroupable) ||
(!(isEntity && hideEntities) && isPreviewable && isGroupable) ||
type === AdvancedFieldType.dropdown ||
(isBranchEndWithValues && type !== AdvancedFieldType.complex) ||
isBranchEndWithoutValues;
Expand All @@ -257,7 +262,6 @@ export const getPreviewFieldsConditions = ({
const isBlock = level === GROUP_BY_LEVEL && shouldRenderLabelOrPlaceholders;
const isBlockContents = level === GROUP_CONTENTS_LEVEL;
const isInstance = bfid === PROFILE_BFIDS.INSTANCE;
const showEntityActions = !hideActions && isEntity;
const wrapEntities = forceRenderAllTopLevelEntities && isEntity;

return {
Expand All @@ -270,7 +274,21 @@ export const getPreviewFieldsConditions = ({
isBlock,
isBlockContents,
isInstance,
showEntityActions,
wrapEntities,
};
};

export const getRecordDependencies = (record?: RecordEntry | null) => {
if (!record) return;

const contents = unwrapRecordValuesFromCommonContainer(record);
const { block, reference } = getEditingRecordBlocks(contents);

if (block && reference) {
return {
keys: reference,
type: REF_TO_NAME[reference.key as keyof typeof REF_TO_NAME],
entries: contents[block][reference.key] as unknown as RecursiveRecordSchema[],
};
}
};
34 changes: 34 additions & 0 deletions src/common/helpers/recordFormatting.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
NON_BF_RECORD_ELEMENTS,
} from '@common/constants/bibframeMapping.constants';
import { getRecordPropertyData } from './record.helper';
import { Row } from '@components/Table';

export const formatRecord = ({
parsedRecord,
Expand Down Expand Up @@ -232,3 +233,36 @@ export const applyIntlToTemplates = ({
...rest,
template: Object.fromEntries(Object.entries(template).map(([k, v]) => [k, format({ id: v })])),
}));

export const formatDependeciesTable = (deps: Record<string, unknown>[]): Row[] => {
return deps.map(({ id, ...rest }) => {
const selectedPublication = (rest?.[BFLITE_URIS.PUBLICATION] as Record<string, unknown>)?.[0] as Record<
string,
unknown[]
>;
const selectedTitle = (rest?.[BFLITE_URIS.TITLE] as Record<string, unknown>)?.[0] as Record<
string,
Record<string, unknown[]>
>;

return {
__meta: {
id,
key: id,
...rest,
},
title: {
label: selectedTitle?.[BFLITE_URIS.TITLE_CONTAINER]?.[BFLITE_URIS.MAIN_TITLE]?.[0],
className: 'title',
},
publisher: {
label: selectedPublication?.[BFLITE_URIS.NAME]?.[0],
className: 'publisher',
},
pubDate: {
label: selectedPublication?.[BFLITE_URIS.DATE]?.[0],
className: 'publication-date',
},
};
}) as Row[];
};
47 changes: 29 additions & 18 deletions src/common/hooks/useConfig.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,25 @@ import { useProcessedRecordAndSchema } from './useProcessedRecordAndSchema.hook'
import { useServicesContext } from './useServicesContext';

export type PreviewParams = {
noStateUpdate?: boolean;
singular?: boolean;
};

type GetProfiles = {
type IGetProfiles = {
record?: RecordEntry;
recordId?: string;
previewParams?: PreviewParams;
asClone?: boolean;
};

type IBuildSchema = {
profile: ProfileEntry;
templates: ResourceTemplates;
record: Record<string, unknown> | Array<unknown>;
asClone?: boolean;
noStateUpdate?: boolean;
};

export const useConfig = () => {
const { schemaCreatorService, userValuesService, selectedEntriesService } =
useServicesContext() as Required<ServicesParams>;
Expand Down Expand Up @@ -57,12 +66,7 @@ export const useConfig = () => {
return preparedFields;
};

const buildSchema = async (
profile: ProfileEntry,
templates: ResourceTemplates,
record: Record<string, unknown> | Array<unknown>,
asClone = false,
) => {
const buildSchema = async ({ profile, templates, record, asClone = false, noStateUpdate = false }: IBuildSchema) => {
const initKey = uuidv4();
const userValues: UserValues = {};

Expand All @@ -76,18 +80,21 @@ export const useConfig = () => {
record,
userValues,
asClone,
noStateUpdate,
});

setUserValues(updatedUserValues || userValues);
setInitialSchemaKey(initKey);
setSelectedEntries(selectedEntriesService.get());
setSchema(updatedSchema);
setSelectedRecordBlocks(selectedRecordBlocks);
if (!noStateUpdate) {
setUserValues(updatedUserValues || userValues);
setInitialSchemaKey(initKey);
setSelectedEntries(selectedEntriesService.get());
setSchema(updatedSchema);
setSelectedRecordBlocks(selectedRecordBlocks);
}

return { updatedSchema, initKey };
};

const getProfiles = async ({ record, recordId, previewParams, asClone }: GetProfiles): Promise<unknown> => {
const getProfiles = async ({ record, recordId, previewParams, asClone }: IGetProfiles): Promise<unknown> => {
if (isProcessingProfiles.current && (record || recordId)) return;

try {
Expand All @@ -103,16 +110,20 @@ export const useConfig = () => {
setProfiles(response);
}

setUserValues({});

const recordData = record?.resource || {};
const recordTitle = getRecordTitle(recordData as RecordEntry);
const entities = getPrimaryEntitiesFromRecord(record as RecordEntry);

if (selectedProfile) {
setSelectedProfile(selectedProfile);

const { updatedSchema, initKey } = await buildSchema(selectedProfile, templates, recordData, asClone);
!previewParams?.noStateUpdate && setSelectedProfile(selectedProfile);

const { updatedSchema, initKey } = await buildSchema({
profile: selectedProfile,
templates,
record: recordData,
asClone,
noStateUpdate: previewParams?.noStateUpdate,
});

if (previewParams && recordId) {
setPreviewContent([
Expand Down
Loading

0 comments on commit 562a99e

Please sign in to comment.