From cab81c3e7965f5a785bdf3a38a666fee9e5c371a Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Wed, 8 Dec 2021 15:11:24 -0800 Subject: [PATCH 1/2] Client: * Pass in objectType and updataData to ObjectDetails, so that we can implement custom publishing behaviors for subjects, separate from scenes * When publishing subjects, first use updateData, if provided, to persist metadata edits * Rename labels, buttons, and modify actions for publishing for the subject use case Collections: * When publishing subjects, fetch & update / create system object version to record published state * Fix license text field usage (descriptiveNonRepeating.metadata_usage.content instead of descriptiveNonRepeating.metadata_usage.text) GraphQL: * Only publish subjects to Edan when we're creating a subject ... don't always do it on subject edits * Treat subjects as always publishable, for now --- .../components/DetailsView/ObjectDetails.tsx | 28 +++++++++-- .../components/DetailsView/index.tsx | 3 +- client/src/types/graphql.tsx | 4 +- server/collections/impl/PublishSubject.ts | 46 +++++++++++++++++-- .../api/mutations/systemobject/publish.ts | 1 + server/graphql/schema.graphql | 1 + .../schema/systemobject/mutations.graphql | 1 + .../mutations/createSubjectWithIdentifiers.ts | 10 +++- .../resolvers/mutations/publish.ts | 4 +- .../mutations/updateObjectDetails.ts | 12 ----- .../queries/getSystemObjectDetails.ts | 31 ++++++++----- server/types/graphql.ts | 1 + 12 files changed, 103 insertions(+), 39 deletions(-) diff --git a/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx b/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx index 13777c494..e13c62e2b 100644 --- a/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx @@ -16,7 +16,7 @@ import { clearLicenseAssignment, assignLicense, publish } from '../../hooks/useD import { getTermForSystemObjectType } from '../../../../utils/repository'; import { LoadingButton } from '../../../../components'; import { toast } from 'react-toastify'; -import { ePublishedState } from '../../../../types/server'; +import { eSystemObjectType, ePublishedState } from '../../../../types/server'; const useStyles = makeStyles(({ palette, typography }) => ({ detail: { @@ -89,11 +89,12 @@ interface ObjectDetailsProps { publishable: boolean; retired: boolean; hideRetired?: boolean; - hidePublishState?: boolean; + objectType?: number; originalFields?: GetSystemObjectDetailsResult; onRetiredUpdate?: (event: React.ChangeEvent, checked: boolean) => void; onLicenseUpdate?: (event) => void; path?: RepositoryPath[][] | null; + updateData?: () => Promise; idSystemObject: number; license?: number; licenseInheritance?: number | null; @@ -110,7 +111,7 @@ function ObjectDetails(props: ObjectDetailsProps): React.ReactElement { publishable, retired, hideRetired, - hidePublishState, + objectType, disabled, originalFields, onRetiredUpdate, @@ -118,7 +119,8 @@ function ObjectDetails(props: ObjectDetailsProps): React.ReactElement { idSystemObject, license, licenseInheritance, - path + path, + updateData } = props; const [licenseList, setLicenseList] = useState([]); const [loading, setLoading] = useState(false); @@ -179,10 +181,15 @@ function ObjectDetails(props: ObjectDetailsProps): React.ReactElement { const onPublish = async () => { onPublishWorker(ePublishedState.ePublished, 'Publish'); }; const onAPIOnly = async () => { onPublishWorker(ePublishedState.eAPIOnly, 'Publish for API Only'); }; const onUnpublish = async () => { onPublishWorker(ePublishedState.eNotPublished, 'Unpublish'); }; + const onSyncToEdan = async () => { onPublishWorker(ePublishedState.ePublished, 'Sync to Edan'); }; const onPublishWorker = async (eState: number, action: string) => { setLoading(true); + // if we're attempting to publish a subject, call the passed in update method first to persist metadata edits + if (objectType === eSystemObjectType.eSubject && updateData !== undefined) + await updateData(); + const { data } = await publish(idSystemObject, eState); if (data?.publish?.success) toast.success(`${action} succeeded`); @@ -198,7 +205,7 @@ function ObjectDetails(props: ObjectDetailsProps): React.ReactElement { - {!hidePublishState && ( + {(objectType === eSystemObjectType.eScene) && ( )} + {(objectType === eSystemObjectType.eSubject) && ( + + {publishedState} +  Sync to Edan + + } + /> + )} {!hideRetired && ( ; message?: Maybe; }; @@ -2879,7 +2880,7 @@ export type PublishMutation = ( { __typename?: 'Mutation' } & { publish: ( { __typename?: 'PublishResult' } - & Pick + & Pick ) } ); @@ -4602,6 +4603,7 @@ export const PublishDocument = gql` mutation publish($input: PublishInput!) { publish(input: $input) { success + eState message } } diff --git a/server/collections/impl/PublishSubject.ts b/server/collections/impl/PublishSubject.ts index b3c66d55a..72cea06a9 100644 --- a/server/collections/impl/PublishSubject.ts +++ b/server/collections/impl/PublishSubject.ts @@ -38,12 +38,16 @@ export class PublishSubject { return res; this.edanRecord = await ICol.createEdanMDM(this.edanMDM, 0, true); - if (this.edanRecord) - LOG.info(`PublishSubject.publish ${this.edanRecord.url} succeeded with Edan status ${this.edanRecord.status}, publicSearch ${this.edanRecord.publicSearch}`, LOG.LS.eCOLL); // :\nEdanMDM=${JSON.stringify(this.edanMDM, H.Helpers.saferStringify)}\nEdan Record=${JSON.stringify(this.edanRecord, H.Helpers.saferStringify)}`, LOG.LS.eCOLL); // `, LOG.LS.eCOLL); // - else + if (!this.edanRecord) { LOG.error(`PublishSubject.publish ${JSON.stringify(this.edanMDM, H.Helpers.saferStringify)} failed`, LOG.LS.eCOLL); + return PublishSubject.returnResults(false, 'Edan publishing failed'); + } - return PublishSubject.returnResults(this.edanRecord !== null, this.edanRecord !== null ? undefined : 'Edan publishing failed'); + // update SystemObjectVersion.PublishedState + res = await this.updatePublishedState(DBAPI.ePublishedState.ePublished); + if (!res.success) + return res; + return PublishSubject.returnResults(true); } private async analyze(): Promise { @@ -91,7 +95,7 @@ export class PublishSubject { case 'record id': this.edanMDM.descriptiveNonRepeating.record_ID = values[0]; nonRepeating = true; break; case 'unit': await this.handleUnit(values[0]); nonRepeating = true; break; case 'license': await this.handleLicense(values[0]); nonRepeating = true; break; - case 'license text': this.edanMDM.descriptiveNonRepeating.metadata_usage!.text = values[0]; nonRepeating = true; break; // eslint-disable-line @typescript-eslint/no-non-null-assertion + case 'license text': this.edanMDM.descriptiveNonRepeating.metadata_usage!.content = values[0]; nonRepeating = true; break; // eslint-disable-line @typescript-eslint/no-non-null-assertion case 'object type': this.edanMDM.indexedStructured!.object_type = values; break; // eslint-disable-line @typescript-eslint/no-non-null-assertion case 'date': this.edanMDM.indexedStructured!.date = values; break; // eslint-disable-line @typescript-eslint/no-non-null-assertion @@ -163,4 +167,36 @@ export class PublishSubject { } return retValue; } + + private async updatePublishedState(ePublishedStateIntended: DBAPI.ePublishedState): Promise { + let systemObjectVersion: DBAPI.SystemObjectVersion | null = await DBAPI.SystemObjectVersion.fetchLatestFromSystemObject(this.idSystemObject); + if (systemObjectVersion) { + if (systemObjectVersion.publishedStateEnum() !== ePublishedStateIntended) { + systemObjectVersion.setPublishedState(ePublishedStateIntended); + if (!await systemObjectVersion.update()) { + const error: string = `PublishSubject.updatePublishedState unable to update published state for ${JSON.stringify(systemObjectVersion, H.Helpers.saferStringify)}`; + LOG.error(error, LOG.LS.eCOLL); + return { success: false, error }; + } + } + + return { success: true }; + } + + systemObjectVersion = new DBAPI.SystemObjectVersion({ + idSystemObject: this.idSystemObject, + PublishedState: ePublishedStateIntended, + DateCreated: new Date(), + Comment: 'Created by Packrat', + idSystemObjectVersion: 0 + }); + + if (!await systemObjectVersion.create()) { + const error: string = `PublishSubject.updatePublishedState could not create SystemObjectVersion for idSystemObject ${this.idSystemObject}`; + LOG.error(error, LOG.LS.eCOLL); + return { success: false, error }; + } + + return { success: true }; + } } \ No newline at end of file diff --git a/server/graphql/api/mutations/systemobject/publish.ts b/server/graphql/api/mutations/systemobject/publish.ts index d0d0f37ba..22966122b 100644 --- a/server/graphql/api/mutations/systemobject/publish.ts +++ b/server/graphql/api/mutations/systemobject/publish.ts @@ -4,6 +4,7 @@ const publish = gql` mutation publish($input: PublishInput!) { publish(input: $input) { success + eState message } } diff --git a/server/graphql/schema.graphql b/server/graphql/schema.graphql index 90d337a0e..9deea88f1 100644 --- a/server/graphql/schema.graphql +++ b/server/graphql/schema.graphql @@ -1276,6 +1276,7 @@ input PublishInput { type PublishResult { success: Boolean! + eState: Int message: String } diff --git a/server/graphql/schema/systemobject/mutations.graphql b/server/graphql/schema/systemobject/mutations.graphql index e7f1c319c..eac79af94 100644 --- a/server/graphql/schema/systemobject/mutations.graphql +++ b/server/graphql/schema/systemobject/mutations.graphql @@ -258,5 +258,6 @@ input PublishInput { type PublishResult { success: Boolean! + eState: Int message: String } diff --git a/server/graphql/schema/systemobject/resolvers/mutations/createSubjectWithIdentifiers.ts b/server/graphql/schema/systemobject/resolvers/mutations/createSubjectWithIdentifiers.ts index 07c1f8fe4..7d1cef7a8 100644 --- a/server/graphql/schema/systemobject/resolvers/mutations/createSubjectWithIdentifiers.ts +++ b/server/graphql/schema/systemobject/resolvers/mutations/createSubjectWithIdentifiers.ts @@ -1,6 +1,6 @@ import { CreateSubjectWithIdentifiersResult, MutationCreateSubjectWithIdentifiersArgs } from '../../../../../types/graphql'; import { Parent, Context } from '../../../../../types/resolvers'; -import { handleMetadata, publishSubject } from './updateObjectDetails'; +import { handleMetadata } from './updateObjectDetails'; import * as DBAPI from '../../../../../db'; import * as COL from '../../../../../collections/interface'; import * as LOG from '../../../../../utils/logger'; @@ -98,4 +98,10 @@ function sendResult(success: boolean, message?: string): CreateSubjectWithIdenti if (!success) LOG.error(`createSubjectWithIdentifier: ${message}`, LOG.LS.eGQL); return { success, message: message ?? '' }; -} \ No newline at end of file +} + +async function publishSubject(idSystemObject: number): Promise { + const ICol: COL.ICollection = COL.CollectionFactory.getInstance(); + const success: boolean = await ICol.publish(idSystemObject, DBAPI.ePublishedState.ePublished); + return { success, error: success ? '' : 'Error encountered during publishing' }; +} diff --git a/server/graphql/schema/systemobject/resolvers/mutations/publish.ts b/server/graphql/schema/systemobject/resolvers/mutations/publish.ts index 2b0ee67ce..ba5e22d5e 100644 --- a/server/graphql/schema/systemobject/resolvers/mutations/publish.ts +++ b/server/graphql/schema/systemobject/resolvers/mutations/publish.ts @@ -9,5 +9,7 @@ export default async function publish(_: Parent, args: MutationPublishArgs): Pro const ICol: COL.ICollection = COL.CollectionFactory.getInstance(); const success: boolean = await ICol.publish(idSystemObject, eState); - return { success, message: success ? '' : 'Error encountered during publishing' }; + if (success) + return { success, eState }; + return { success, message: 'Error encountered during publishing' }; } diff --git a/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts b/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts index 12e5aa133..05100f328 100644 --- a/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts +++ b/server/graphql/schema/systemobject/resolvers/mutations/updateObjectDetails.ts @@ -444,12 +444,6 @@ export default async function updateObjectDetails(_: Parent, args: MutationUpdat break; } - if (objectType === eSystemObjectType.eSubject) { - const publishRes: H.IOResults = await publishSubject(idSystemObject); - if (!publishRes.success) - return sendResult(false, publishRes.error); - } - return { success: true, message: '' }; } @@ -503,9 +497,3 @@ export async function handleMetadata(idSystemObject: number, metadatas: Metadata } return { success: true }; } - -export async function publishSubject(idSystemObject: number): Promise { - const ICol: COL.ICollection = COL.CollectionFactory.getInstance(); - const success: boolean = await ICol.publish(idSystemObject, DBAPI.ePublishedState.ePublished); - return { success, error: success ? '' : 'Error encountered during publishing' }; -} diff --git a/server/graphql/schema/systemobject/resolvers/queries/getSystemObjectDetails.ts b/server/graphql/schema/systemobject/resolvers/queries/getSystemObjectDetails.ts index 08f91b2e9..1deaea5f5 100644 --- a/server/graphql/schema/systemobject/resolvers/queries/getSystemObjectDetails.ts +++ b/server/graphql/schema/systemobject/resolvers/queries/getSystemObjectDetails.ts @@ -103,19 +103,26 @@ async function getPublishedState(idSystemObject: number, oID: DBAPI.ObjectIDAndT const publishedEnum: DBAPI.ePublishedState = systemObjectVersion ? systemObjectVersion.publishedStateEnum() : DBAPI.ePublishedState.eNotPublished; const publishedState: string = DBAPI.PublishedStateEnumToString(publishedEnum); - const mayBePublished: boolean = (LR != null) && - (LR.License != null) && - (DBAPI.LicenseRestrictLevelToPublishedStateEnum(LR.License.RestrictLevel) !== DBAPI.ePublishedState.eNotPublished); - let publishable: boolean = false; - if (oID && oID.eObjectType == DBAPI.eSystemObjectType.eScene) { - const scene: DBAPI.Scene | null = await DBAPI.Scene.fetch(oID.idObject); - if (scene) - publishable = scene.ApprovedForPublication && // Approved for Publication - scene.PosedAndQCd && // Posed and QCd - mayBePublished; // License defined and allows publishing - else - LOG.error(`Unable to compute scene for ${JSON.stringify(oID)}`, LOG.LS.eGQL); + if (oID) { + switch (oID.eObjectType) { + case DBAPI.eSystemObjectType.eScene: { + const scene: DBAPI.Scene | null = await DBAPI.Scene.fetch(oID.idObject); + if (scene) { + const mayBePublished: boolean = (LR != null) && + (LR.License != null) && + (DBAPI.LicenseRestrictLevelToPublishedStateEnum(LR.License.RestrictLevel) !== DBAPI.ePublishedState.eNotPublished); + publishable = scene.ApprovedForPublication && // Approved for Publication + scene.PosedAndQCd && // Posed and QCd + mayBePublished; // License defined and allows publishing + } else + LOG.error(`Unable to compute scene for ${JSON.stringify(oID)}`, LOG.LS.eGQL); + } break; + + case DBAPI.eSystemObjectType.eSubject: + publishable = true; + break; + } } return { publishedState, publishedEnum, publishable }; } diff --git a/server/types/graphql.ts b/server/types/graphql.ts index c53afa869..d57d82b2b 100644 --- a/server/types/graphql.ts +++ b/server/types/graphql.ts @@ -1725,6 +1725,7 @@ export type PublishInput = { export type PublishResult = { __typename?: 'PublishResult'; success: Scalars['Boolean']; + eState?: Maybe; message?: Maybe; }; From 628871272122319105f5df8c4b909cd63341e2cf Mon Sep 17 00:00:00 2001 From: Jon Tyson <6943745+jahjedtieson@users.noreply.github.com> Date: Wed, 8 Dec 2021 15:48:03 -0800 Subject: [PATCH 2/2] Client: * Return success/failure when updating data in detail views * Handle update failures by providing an error and not publishing --- .../components/DetailsView/ObjectDetails.tsx | 11 ++++++++--- .../Repository/components/DetailsView/index.tsx | 15 ++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx b/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx index e13c62e2b..d7ef3ae64 100644 --- a/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx +++ b/client/src/pages/Repository/components/DetailsView/ObjectDetails.tsx @@ -94,7 +94,7 @@ interface ObjectDetailsProps { onRetiredUpdate?: (event: React.ChangeEvent, checked: boolean) => void; onLicenseUpdate?: (event) => void; path?: RepositoryPath[][] | null; - updateData?: () => Promise; + updateData?: () => Promise; idSystemObject: number; license?: number; licenseInheritance?: number | null; @@ -187,8 +187,13 @@ function ObjectDetails(props: ObjectDetailsProps): React.ReactElement { setLoading(true); // if we're attempting to publish a subject, call the passed in update method first to persist metadata edits - if (objectType === eSystemObjectType.eSubject && updateData !== undefined) - await updateData(); + if (objectType === eSystemObjectType.eSubject && updateData !== undefined) { + if (!await updateData()) { + toast.error(`${action} failed while updating object`); + setLoading(false); + return; + } + } const { data } = await publish(idSystemObject, eState); if (data?.publish?.success) diff --git a/client/src/pages/Repository/components/DetailsView/index.tsx b/client/src/pages/Repository/components/DetailsView/index.tsx index bf38a66d1..474a567a9 100644 --- a/client/src/pages/Repository/components/DetailsView/index.tsx +++ b/client/src/pages/Repository/components/DetailsView/index.tsx @@ -322,14 +322,14 @@ function DetailsView(): React.ReactElement { setUpdatedData(updatedDataFields); }; - const updateData = async (): Promise => { + const updateData = async (): Promise => { toast.dismiss(); setIsUpdatingData(true); const identifierCheck = checkIdentifiersBeforeUpdate(); if (identifierCheck.length) { identifierCheck.forEach(error => toast.error(error)); setIsUpdatingData(false); - return; + return false; } const stateIdentifiersWithIdSystemObject: UpdateIdentifier[] = stateIdentifiers.map(({ id, identifier, identifierType, idIdentifier, preferred }) => { @@ -350,7 +350,7 @@ function DetailsView(): React.ReactElement { const invalidMetadata = validateMetadataFields(); if (invalidMetadata.length) { invalidMetadata.forEach(message => toast.error(message, { autoClose: false })); - return; + return false; } // Create another validation here to make sure that the appropriate SO types are being checked @@ -358,7 +358,7 @@ function DetailsView(): React.ReactElement { if (errors.length) { errors.forEach(error => toast.error(`${error}`, { autoClose: false })); setIsUpdatingData(false); - return; + return false; } try { @@ -455,18 +455,19 @@ function DetailsView(): React.ReactElement { } const metadata = getAllMetadataEntries().filter(entry => entry.Name); - console.log('metadata', metadata); + // console.log('metadata', metadata); updatedData.Metadata = metadata; const { data } = await updateDetailsTabData(idSystemObject, idObject, objectType, updatedData); if (data?.updateObjectDetails?.success) { toast.success('Data saved successfully'); - } else { + return true; + } else throw new Error(data?.updateObjectDetails?.message ?? ''); - } } catch (error) { if (error instanceof Error) toast.error(error.toString() || 'Failed to save updated data'); + return false; } finally { setIsUpdatingData(false); }