import _ from 'lodash'; import { Config, PresetMap } from '@stackbit/sdk'; import * as StackbitTypes from '@stackbit/types'; import { omitByNil } from '@stackbit/utils'; import { v4 as uuid } from 'uuid'; import * as ContentStoreTypes from '../types'; import { CustomActionStateChange, CustomActionRunStateMap, CustomActionRunState, APICustomAction, APICustomActionGlobal, APICustomActionBulk, APICustomActionModel, APICustomActionModelSpecifier, APICustomActionDocumentSpecifier, APIGetCustomActionRequest, APIGetRunningCustomActionsRequest, APIRunCustomActionRequest, APIRunCustomActionRequestModel, APIRunCustomActionResponse, ExtendedCustomAction, ExtendedCustomActionGlobal, ExtendedCustomActionBulk, ExtendedCustomActionModelGlobal } from '../types'; import { createConfigDelegate } from './config-delegate'; import { getContentSourceActionsForSourceThunk } from './document-hooks'; import { mapStoreDocumentToCSIDocumentWithSource, mapStoreFieldToCSIField } from './store-to-csi-docs-converter'; import { getModelAndDocumentFieldForLocalizedFieldPath } from './field-path-utils'; import { getContentSourceDataByTypeAndProjectIdOrThrow, getUserContextForSrcTypeThunk } from '../content-store-utils'; export async function getGlobalAndBulkAPIActions({ stackbitConfig, customActionRunStateMap, contentSourceDataById, userLogger, pageUrl, user, locale, currentPageDocument }: { stackbitConfig: Config | null; customActionRunStateMap: CustomActionRunStateMap; contentSourceDataById: Record; userLogger: StackbitTypes.Logger; pageUrl?: string; user?: ContentStoreTypes.User; locale?: string; currentPageDocument?: APICustomActionDocumentSpecifier; }): Promise<(APICustomActionGlobal | APICustomActionBulk | APICustomActionModel)[]> { if (!stackbitConfig || !Array.isArray(stackbitConfig.actions)) { return []; } const configDelegate = createConfigDelegate({ contentSourceDataById: contentSourceDataById, logger: userLogger }); const apiCustomActions: (APICustomActionGlobal | APICustomActionBulk | APICustomActionModel)[] = []; for (const action of stackbitConfig.actions) { const actionId = globalActionId(action); const pageDocument = getCSIDocumentWithSourceFromDocumentSpec(currentPageDocument, configDelegate); const commonStateOptions: StackbitTypes.CustomActionStateCommonOptions = { actionId: actionId, currentLocale: locale, currentUser: user, currentPageUrl: pageUrl, currentPageDocument: pageDocument, ...configDelegate }; const actionRunState = customActionRunStateMap[actionId]; const extendedAction = getExtendedActionFromGlobalAction({ action, actionId, actionRunState, generateNewActionId: false }); const state = await resolveCustomActionState({ extendedAction, commonStateOptions }); const permissions = resolveCustomActionPermissions({ extendedAction, commonStateOptions }); apiCustomActions.push( extendedActionToApiAction({ extendedAction, state, permissions }) ); } return apiCustomActions; } export async function resolveCustomActionsById({ getActionRequest, customActionRunStateMap, contentSourceDataById, stackbitConfig, userLogger }: { getActionRequest: APIGetCustomActionRequest; customActionRunStateMap: CustomActionRunStateMap; contentSourceDataById: Record; stackbitConfig: Config | null; userLogger: StackbitTypes.Logger; }): Promise { const result: APICustomAction[] = []; const { customActionIds, locale, user, pageUrl, currentPageDocument } = getActionRequest; const configDelegate = createConfigDelegate({ contentSourceDataById, logger: userLogger }); for (const actionId of customActionIds) { const extendedAction = getExtendedActionById({ actionId, customActionRunStateMap, contentSourceDataById, stackbitConfig }); if (!extendedAction) { userLogger.debug(`custom action with id: '${actionId}' was not found`); continue; } const pageDocument = getCSIDocumentWithSourceFromDocumentSpec(currentPageDocument, configDelegate); const commonStateOptions: StackbitTypes.CustomActionStateCommonOptions = { actionId: actionId, currentLocale: locale, currentUser: user, currentPageUrl: pageUrl, currentPageDocument: pageDocument, ...configDelegate }; try { const state = await resolveCustomActionState({ extendedAction, commonStateOptions }); const permissions = resolveCustomActionPermissions({ extendedAction, commonStateOptions }); result.push( extendedActionToApiAction({ extendedAction, state, permissions }) ); } catch (error: any) { userLogger.warn(`getCustomActionsById: error resolving custom action, id: '${actionId}', error: ${error.message}`); } } return result; } async function resolveCustomActionState({ extendedAction, commonStateOptions }: { extendedAction: ExtendedCustomAction; commonStateOptions: StackbitTypes.CustomActionStateCommonOptions; }): Promise { if (extendedAction.isRunning) { return 'running'; } if (!('state' in extendedAction) || typeof extendedAction.state !== 'function') { return 'enabled'; } switch (extendedAction.extendedType) { case 'global': case 'bulk': return await extendedAction.state(commonStateOptions); case 'document': return await extendedAction.state({ ...commonStateOptions, document: extendedAction.documentWithSource, model: extendedAction.modelWithSource }); case 'objectModel': return await extendedAction.state({ ...commonStateOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, documentField: extendedAction.documentField, modelField: extendedAction.modelField, objectModel: extendedAction.objectModel, fieldPath: extendedAction.fieldPath }); case 'objectField': return await extendedAction.state({ ...commonStateOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, documentField: extendedAction.documentField, modelField: extendedAction.modelField, fieldPath: extendedAction.fieldPath }); case 'field': return await extendedAction.state({ ...commonStateOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, documentField: extendedAction.documentField, modelField: extendedAction.modelField, fieldPath: extendedAction.fieldPath }); default: { // actions of type 'model' do not support 'state' function const _exhaustiveCheck: never = extendedAction; throw new Error(`error getting action state, action of type '${_exhaustiveCheck['type']}' does not support states`); } } } function resolveCustomActionPermissions({ extendedAction, commonStateOptions }: { extendedAction: ExtendedCustomAction; commonStateOptions: StackbitTypes.CustomActionStateCommonOptions; }): StackbitTypes.CustomActionPermissions | undefined { if (typeof extendedAction?.permissions !== 'function' || !commonStateOptions.currentUser) { return undefined; } const { currentUser, ...restOptions } = commonStateOptions; const commonPermissionsOptions: StackbitTypes.CustomActionPermissionsCommonOptions = { ...restOptions, userContext: currentUser }; switch (extendedAction.extendedType) { case 'global': case 'bulk': case 'modelGlobal': case 'model': case 'modelObject': { return extendedAction.permissions?.(commonPermissionsOptions); } case 'document': { return extendedAction.permissions?.({ ...commonPermissionsOptions, document: extendedAction.documentWithSource, model: extendedAction.modelWithSource }); } case 'objectModel': { return extendedAction.permissions?.({ ...commonPermissionsOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, documentField: extendedAction.documentField, modelField: extendedAction.modelField, objectModel: extendedAction.objectModel, fieldPath: extendedAction.fieldPath }); } case 'objectField': { return extendedAction.permissions?.({ ...commonPermissionsOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, documentField: extendedAction.documentField, modelField: extendedAction.modelField, fieldPath: extendedAction.fieldPath }); } case 'field': { return extendedAction.permissions?.({ ...commonPermissionsOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, documentField: extendedAction.documentField, modelField: extendedAction.modelField, fieldPath: extendedAction.fieldPath }); } default: { const _exhaustiveCheck: never = extendedAction; return _exhaustiveCheck; } } } export function getRunningActions({ getRunningActionsRequest, customActionRunStateMap, contentSourceDataById, stackbitConfig, userLogger }: { getRunningActionsRequest: APIGetRunningCustomActionsRequest; customActionRunStateMap: CustomActionRunStateMap; contentSourceDataById: Record; stackbitConfig: Config | null; userLogger: StackbitTypes.Logger; }): APICustomAction[] { if (!getRunningActionsRequest.user) { return []; } const runningActions: APICustomAction[] = []; for (const [actionId, actionRunState] of Object.entries(customActionRunStateMap)) { if (actionRunState.isRunning && actionRunState.userId === getRunningActionsRequest.user.id) { const extendedAction = getExtendedActionById({ actionId, customActionRunStateMap, contentSourceDataById, stackbitConfig }); if (!extendedAction) { userLogger.debug(`custom action with id: '${actionId}' was not found`); continue; } runningActions.push( extendedActionToApiAction({ extendedAction, state: 'running' }) ); } } return runningActions; } type ActionType = ExtendedCustomAction['type']; type ExtendedActionForActionType = U extends { type: Type } ? U : never; type APICustomActionForType = U extends { type: Type } ? U : never; function extendedActionToApiAction({ extendedAction, state, permissions }: { extendedAction: ExtendedActionForActionType; state: StackbitTypes.CustomActionState; permissions?: StackbitTypes.CustomActionPermissions; }): APICustomActionForType { const apiAction = omitByNil({ type: extendedAction.type, name: extendedAction.name, label: extendedAction.label, actionId: extendedAction.actionId, userId: extendedAction.userId, icon: extendedAction.icon, state: state, permissions: permissions, models: extendedAction.extendedType === 'modelGlobal' && 'models' in extendedAction ? extendedAction.models : undefined, inputFields: extendedAction.inputFields, hidden: extendedAction.hidden }); return apiAction as APICustomActionForType; } export async function runCustomAction({ runActionRequest, customActionRunStateMap, contentSourceDataById, stackbitConfig, userLogger, presets, onProgress }: { runActionRequest: APIRunCustomActionRequest; customActionRunStateMap: CustomActionRunStateMap; contentSourceDataById: Record; stackbitConfig: Config | null; userLogger: StackbitTypes.Logger; presets: PresetMap; onProgress: (options: CustomActionStateChange) => void; }): Promise { const extendedAction = getExtendedActionById({ actionId: runActionRequest.actionId, customActionRunStateMap, contentSourceDataById, stackbitConfig, generateNewActionId: true }); if (!extendedAction) { throw new Error(`Error running action: action not found, action name: '${runActionRequest.actionName}' action ID: '${runActionRequest.actionId}'.`); } if (extendedAction.isRunning) { throw new Error(`Error running action: action '${runActionRequest.actionName}' with ID '${runActionRequest.actionId}' is already running.`); } const actionId = extendedAction.actionId; let isRunning = false; try { const actionLogger = userLogger.createLogger({ label: `action:${extendedAction.name}` }); const configDelegate = createConfigDelegate({ contentSourceDataById: contentSourceDataById, logger: actionLogger }); const currentPageDocument = getCSIDocumentWithSourceFromDocumentSpec(runActionRequest.currentPageDocument, configDelegate); const actionRunState: CustomActionRunState = { isRunning: true, userId: runActionRequest.user?.id }; customActionRunStateMap[actionId] = actionRunState; const inputData = runActionRequest.inputData ?? {}; if (runActionRequest.actionType === 'model' && 'presetId' in runActionRequest) { inputData.presetData = presets[runActionRequest.presetId!]?.data; } const commonRunOptions: StackbitTypes.CustomActionRunCommonOptions = { actionId: actionId, inputData: inputData, currentLocale: runActionRequest.locale, currentUser: runActionRequest.user, currentPageUrl: runActionRequest.pageUrl, currentPageDocument: currentPageDocument, progress: ({ message, percent }) => { if (!isRunning) { return; } onProgress( omitByNil({ actionId: actionId, actionName: extendedAction.name, actionType: extendedAction.type, userId: runActionRequest.user?.id, state: 'running', message, percent }) ); }, getContentSourceActionsForSource: getContentSourceActionsForSourceThunk({ getContentSourceDataById: () => contentSourceDataById, logger: userLogger, user: runActionRequest.user, stackbitConfig: stackbitConfig }), getUserContextForContentSourceType: getUserContextForSrcTypeThunk(runActionRequest.user), ...configDelegate }; let promise: Promise; isRunning = true; onProgress( omitByNil({ actionId: actionId, actionName: extendedAction.name, actionType: extendedAction.type, userId: runActionRequest.user?.id, state: 'running' }) ); if (extendedAction.extendedType === 'global') { promise = extendedAction.run(commonRunOptions); } else if (extendedAction.extendedType === 'bulk') { if (!('documents' in runActionRequest) || !Array.isArray(runActionRequest.documents)) { throw new Error( `Bulk action run request must contain array of documents, action name: '${runActionRequest.actionName}', action ID: '${runActionRequest.actionId}'.` ); } const documents = runActionRequest.documents.map((documentSpec) => { const { document } = getCSIDocumentAndModelWithSourceFromDocumentSpec({ documentSpec, contentSourceDataById }); return document; }); promise = extendedAction.run({ ...commonRunOptions, documents }); } else if (extendedAction.extendedType === 'modelGlobal') { if (!('srcType' in runActionRequest) || !('srcProjectId' in runActionRequest) || !('modelName' in runActionRequest)) { throw new Error( `Global model action run request must contain srcType, srcProjectId and modelName, action name: '${runActionRequest.actionName}', action ID: '${runActionRequest.actionId}'.` ); } actionRunState.modelSpec = { srcType: runActionRequest.srcType, srcProjectId: runActionRequest.srcProjectId, modelName: runActionRequest.modelName }; const runOptions = getRunParamsForModelAction({ modelSpec: actionRunState.modelSpec, location: runActionRequest.location, contentSourceDataById, getContentSourceActionsForSource: commonRunOptions.getContentSourceActionsForSource }); promise = extendedAction.run({ ...commonRunOptions, ...runOptions }); } else if (extendedAction.extendedType === 'model') { const runOptions = getRunParamsForModelAction({ modelSpec: extendedAction.modelSpec, location: (runActionRequest as APIRunCustomActionRequestModel).location, contentSourceDataById, getContentSourceActionsForSource: commonRunOptions.getContentSourceActionsForSource }); promise = extendedAction.run({ ...commonRunOptions, ...runOptions }); } else if (extendedAction.extendedType === 'document') { promise = extendedAction.run({ ...commonRunOptions, document: extendedAction.documentWithSource, model: extendedAction.modelWithSource, contentSourceActions: commonRunOptions.getContentSourceActionsForSource({ srcType: extendedAction.documentWithSource.srcType, srcProjectId: extendedAction.documentWithSource.srcProjectId })! }); } else if (extendedAction.extendedType === 'modelObject') { promise = extendedAction.run({ ...commonRunOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, modelField: extendedAction.modelField, objectModel: extendedAction.objectModel, fieldPath: extendedAction.fieldPath, location: (runActionRequest as APIRunCustomActionRequestModel).location, contentSourceActions: commonRunOptions.getContentSourceActionsForSource({ srcType: extendedAction.documentWithSource.srcType, srcProjectId: extendedAction.documentWithSource.srcProjectId })! }); } else if (extendedAction.extendedType === 'objectModel') { promise = extendedAction.run({ ...commonRunOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, documentField: extendedAction.documentField, modelField: extendedAction.modelField, objectModel: extendedAction.objectModel, fieldPath: extendedAction.fieldPath, contentSourceActions: commonRunOptions.getContentSourceActionsForSource({ srcType: extendedAction.documentWithSource.srcType, srcProjectId: extendedAction.documentWithSource.srcProjectId })! }); } else if (extendedAction.extendedType === 'objectField') { promise = extendedAction.run({ ...commonRunOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, documentField: extendedAction.documentField, modelField: extendedAction.modelField, fieldPath: extendedAction.fieldPath, contentSourceActions: commonRunOptions.getContentSourceActionsForSource({ srcType: extendedAction.documentWithSource.srcType, srcProjectId: extendedAction.documentWithSource.srcProjectId })! }); } else if (extendedAction.extendedType === 'field') { promise = extendedAction.run({ ...commonRunOptions, parentDocument: extendedAction.documentWithSource, parentModel: extendedAction.modelWithSource, documentField: extendedAction.documentField, modelField: extendedAction.modelField, fieldPath: extendedAction.fieldPath, contentSourceActions: commonRunOptions.getContentSourceActionsForSource({ srcType: extendedAction.documentWithSource.srcType, srcProjectId: extendedAction.documentWithSource.srcProjectId })! }); } else { const _exhaustiveCheck: never = extendedAction; throw new Error(`action of type ${_exhaustiveCheck['type']} is not supported`); } promise .then(async (actionResult) => { isRunning = false; delete customActionRunStateMap[actionId]; userLogger.debug(`Action completed: ${actionId}`); const state = await resolveCustomActionState({ extendedAction, commonStateOptions: { actionId: actionId, currentLocale: runActionRequest.locale, currentUser: runActionRequest.user, currentPageUrl: runActionRequest.pageUrl, currentPageDocument: currentPageDocument, ...configDelegate } }); onProgress( omitByNil({ actionId: actionId, actionName: extendedAction.name, actionType: extendedAction.type, userId: runActionRequest.user?.id, state: extendedAction.type === 'model' ? 'finished' : state, success: actionResult?.success, error: actionResult?.error, result: actionResult?.result }) ); }) .catch((error: any) => { isRunning = false; delete customActionRunStateMap[actionId]; userLogger.warn(`Error running action: ${error.stack ?? error.message}`); onProgress( omitByNil({ actionId: extendedAction.actionId, actionName: extendedAction.name, actionType: extendedAction.type, userId: runActionRequest.user?.id, state: extendedAction.type === 'model' ? 'failed' : 'enabled', error: `Error running action: ${error.message}` }) ); }); } catch (error: any) { if (isRunning) { onProgress( omitByNil({ actionId: extendedAction.actionId, actionName: extendedAction.name, actionType: extendedAction.type, userId: runActionRequest.user?.id, state: extendedAction.type === 'model' ? 'failed' : 'enabled', error: `Error running action: ${error.message}` }) ); } isRunning = false; delete customActionRunStateMap[actionId]; userLogger.warn(`Error running action: ${error.stack ?? error.message}`); // rethrow the error to return erroneous response throw error; } return { actionId }; } function getCSIDocumentWithSourceFromDocumentSpec( documentSpec: APICustomActionDocumentSpecifier | undefined, configDelegate: StackbitTypes.ConfigDelegate ): StackbitTypes.DocumentWithSource | undefined { return documentSpec ? configDelegate.getDocumentById({ id: documentSpec.srcDocumentId, srcType: documentSpec.srcType, srcProjectId: documentSpec.srcProjectId }) : undefined; } function getRunParamsForModelAction({ modelSpec, location, contentSourceDataById, getContentSourceActionsForSource }: { modelSpec: APICustomActionModelSpecifier; location: 'new-card' | 'preset-card'; contentSourceDataById: Record; getContentSourceActionsForSource: ReturnType; }): StackbitTypes.CustomActionModelRunOptions { const { srcType, srcProjectId, modelName } = modelSpec; const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(srcType, srcProjectId, contentSourceDataById); const actionModel = contentSourceData.modelMap[modelName]; if (!actionModel) { throw new Error(`model '${modelName}' not found`); } return { actionModel: { ...actionModel, srcType, srcProjectId }, location, contentSourceActions: getContentSourceActionsForSource({ srcType, srcProjectId })! }; } function getCSIDocumentAndModelWithSourceFromDocumentSpec({ documentSpec, contentSourceDataById }: { documentSpec: APICustomActionDocumentSpecifier; contentSourceDataById: Record; }): { document: StackbitTypes.DocumentWithSource; model: StackbitTypes.ModelWithSource; } { const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(documentSpec.srcType, documentSpec.srcProjectId, contentSourceDataById); const document = contentSourceData.documentMap[documentSpec.srcDocumentId]; const csiDocument = contentSourceData.csiDocumentMap[documentSpec.srcDocumentId]; if (!document || !csiDocument) { throw new Error( `document not found, srcType: ${documentSpec.srcType}, srcProjectId: ${documentSpec.srcProjectId}, srcDocumentId: ${documentSpec.srcDocumentId}` ); } const documentWithSource = mapStoreDocumentToCSIDocumentWithSource({ document, csiDocument }); const model = contentSourceData.modelMap[documentWithSource.modelName]; if (!model) { throw new Error(`model '${documentWithSource.modelName}' not found`); } return { document: documentWithSource, model: { ...model, srcType: documentSpec.srcType, srcProjectId: documentSpec.srcProjectId } }; } function getExtendedActionById({ actionId, customActionRunStateMap, contentSourceDataById, stackbitConfig, generateNewActionId }: { actionId: string; customActionRunStateMap: CustomActionRunStateMap; contentSourceDataById: Record; stackbitConfig: Config | null; generateNewActionId?: boolean; }): ExtendedCustomAction | undefined { const actionRunState = customActionRunStateMap[actionId]; // Check if this is a global action defined in the actions array of stackbit.config. // Their types can be "global", "bulk" or "model" if (isGlobalActionId(actionId)) { if (!stackbitConfig || !Array.isArray(stackbitConfig.actions)) { return undefined; } const actionName = getGlobalActionNameFromId(actionId); const action = stackbitConfig.actions.find((action) => action.name === actionName); if (!action) { return undefined; } return getExtendedActionFromGlobalAction({ action, actionId, actionRunState, generateNewActionId }); } const { srcType, srcProjectId, modelName, srcDocumentId, fieldPath, actionName } = parseActionId(actionId) ?? {}; if (!srcType || !srcProjectId || !actionName) { return undefined; } const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(srcType, srcProjectId, contentSourceDataById); // If "modelName" was specified without "srcDocumentId" then it has to be a // "model" action defined in a model of type 'page' or 'data'. if (!srcDocumentId && modelName) { const model = contentSourceData.modelMap[modelName]; if (!model || (model.type !== 'page' && model.type !== 'data')) { return undefined; } const action = model.actions?.find((action) => action.name === actionName); if (!action || action.type !== 'model') { return undefined; } return { ...action, extendedType: 'model', actionId: generateNewActionId && !actionRunState ? JSON.stringify({ srcType, srcProjectId, modelName, actionName, actionRunId: uuid().substring(0, 8) }) : actionId, label: getActionLabel(action), modelSpec: { srcType, srcProjectId, modelName }, userId: actionRunState?.userId, isRunning: actionRunState?.isRunning }; } if (!srcDocumentId) { return undefined; } const document = contentSourceData.documentMap[srcDocumentId]; const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId]; if (!document || !csiDocument) { return undefined; } const model = contentSourceData.modelMap[document.srcModelName]; // The model of a document is always 'page' or 'data', // this condition helps Typescript infer the right type of model.actions if (!model || (model.type !== 'page' && model.type !== 'data')) { return undefined; } const documentWithSource = mapStoreDocumentToCSIDocumentWithSource({ document, csiDocument }); const modelWithSource = { ...model, srcType, srcProjectId }; if (typeof fieldPath === 'undefined') { // If fieldPath was not provided, then the model must be of type "page" or "data", // and the action type must be of type 'document' const action = model.actions?.find((action) => action.name === actionName); if (!action || action.type !== 'document') { return undefined; } return { // if configuration is updated, the new action properties will override the stored action properties ...action, type: 'document', extendedType: 'document', actionId, label: getActionLabel(action), documentWithSource, modelWithSource, userId: actionRunState?.userId, isRunning: actionRunState?.isRunning }; } else { const { modelField, documentField } = getModelAndDocumentFieldForLocalizedFieldPath({ document, fieldPath, modelMap: contentSourceData.modelMap }) as { // list items cannot have actions, therefore we can safely omit the list item types modelField: StackbitTypes.Field; documentField?: ContentStoreTypes.DocumentField; }; const csiDocumentField = documentField ? mapStoreFieldToCSIField(documentField) : undefined; // Find an action with the provided 'actionName' in model.field.actions if ('actions' in modelField && Array.isArray(modelField.actions)) { const action = modelField.actions.find((action) => action.name === actionName); if (action) { if (action.type === 'object') { if (!csiDocumentField) { throw new Error(`object document field not found at field path: ${fieldPath.join('.')}`); } if (modelField.type !== 'object' || csiDocumentField.type !== 'object') { // action of type 'object' cannot be defined on field other than 'object' return undefined; } return { // if configuration is updated, the new action properties will override the stored action properties ...action, extendedType: 'objectField', actionId, label: getActionLabel(action), documentWithSource, modelWithSource, documentField: csiDocumentField as StackbitTypes.DocumentObjectFieldNonLocalized, modelField: modelField as StackbitTypes.FieldObject, fieldPath, userId: actionRunState?.userId, isRunning: actionRunState?.isRunning }; } return { // if configuration is updated, the new action properties will override the stored action properties ...action, type: 'field', extendedType: 'field', actionId, label: getActionLabel(action), documentWithSource, modelWithSource, documentField: csiDocumentField, modelField, fieldPath, userId: actionRunState?.userId, isRunning: actionRunState?.isRunning }; } } // If the field is of type "model", find an action with the provided 'actionName' in model.actions if (modelField.type === 'model') { if (modelName) { const objectModel = contentSourceData.modelMap[modelName]; if (!objectModel || objectModel.type !== 'object') { return undefined; } // This is a nested model of type "object", so the action must be CustomActionModelObject const action = objectModel.actions?.find((action) => action.name === actionName); if (!action || action.type !== 'model') { return undefined; } return { // if configuration is updated, the new action properties will override the stored action properties ...action, extendedType: 'modelObject', actionId, label: getActionLabel(action), documentWithSource, modelWithSource, modelField, objectModel: { ...objectModel, srcType, srcProjectId }, fieldPath, userId: actionRunState?.userId, isRunning: actionRunState?.isRunning }; } else { if (!csiDocumentField || csiDocumentField.type !== 'model' || csiDocumentField.localized) { return undefined; } const modelName = csiDocumentField.modelName; const objectModel = contentSourceData.modelMap[modelName]; if (!objectModel || objectModel.type !== 'object') { return undefined; } // This is a nested model of type "object", so the action must be CustomActionObjectModel const action = objectModel.actions?.find((action) => action.name === actionName); if (!action || action.type !== 'object') { return undefined; } return { // if configuration is updated, the new action properties will override the stored action properties ...action, type: 'object', extendedType: 'objectModel', actionId, label: getActionLabel(action), documentWithSource, modelWithSource, documentField: csiDocumentField, modelField, objectModel: { ...objectModel, srcType, srcProjectId }, fieldPath, userId: actionRunState?.userId, isRunning: actionRunState?.isRunning }; } } } } function getExtendedActionFromGlobalAction({ action, actionId, actionRunState, generateNewActionId }: { action: StackbitTypes.CustomActionGlobal | StackbitTypes.CustomActionBulk | StackbitTypes.CustomActionModel; actionId: string; actionRunState?: CustomActionRunState; generateNewActionId?: boolean; }): ExtendedCustomActionGlobal | ExtendedCustomActionBulk | ExtendedCustomActionModelGlobal { if (action.type === 'model') { return { ...action, extendedType: 'modelGlobal', actionId: generateNewActionId && !actionRunState ? `${actionId}.${uuid().substring(0, 8)}` : actionId, label: getActionLabel(action), modelSpec: actionRunState?.modelSpec, userId: actionRunState?.userId, isRunning: actionRunState?.isRunning }; } return { ...action, extendedType: action.type, actionId, label: getActionLabel(action), userId: actionRunState?.userId, isRunning: actionRunState?.isRunning } as ExtendedCustomActionGlobal | ExtendedCustomActionBulk; } function globalActionId(action: StackbitTypes.CustomActionGlobal | StackbitTypes.CustomActionBulk | StackbitTypes.CustomActionModel): string { return `config.actions.${action.name}`; } function isGlobalActionId(actionId: string) { return actionId.startsWith('config.actions.'); } function getGlobalActionNameFromId(actionId: string) { const result = actionId.match(/^config\.actions\.(.+?)(?:\.([\w-]+))?$/); return result ? result[1] : undefined; } function parseActionId(actionId: string): | { srcType?: string; srcProjectId?: string; srcDocumentId?: string; modelName?: string; actionName?: string; fieldPath?: string[]; } | undefined { try { return JSON.parse(actionId); } catch (e) { return undefined; } } function getActionLabel( action: | StackbitTypes.CustomActionGlobal | StackbitTypes.CustomActionBulk | StackbitTypes.CustomActionModel | StackbitTypes.CustomActionDocument | StackbitTypes.CustomActionModelObject | StackbitTypes.CustomActionObjectModel | StackbitTypes.CustomActionObjectField | StackbitTypes.CustomActionField ): string { return action.label ?? _.startCase(action.name); }