@stackbit/cms-core 3.0.7 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/content-store.d.ts +3 -2
  3. package/dist/content-store.d.ts.map +1 -1
  4. package/dist/content-store.js +16 -10
  5. package/dist/content-store.js.map +1 -1
  6. package/dist/types/content-store-types.d.ts +1 -0
  7. package/dist/types/content-store-types.d.ts.map +1 -1
  8. package/dist/types/custom-actions.d.ts +104 -20
  9. package/dist/types/custom-actions.d.ts.map +1 -1
  10. package/dist/utils/create-update-csi-docs.d.ts +6 -6
  11. package/dist/utils/create-update-csi-docs.d.ts.map +1 -1
  12. package/dist/utils/create-update-csi-docs.js +7 -14
  13. package/dist/utils/create-update-csi-docs.js.map +1 -1
  14. package/dist/utils/custom-actions.d.ts +13 -10
  15. package/dist/utils/custom-actions.d.ts.map +1 -1
  16. package/dist/utils/custom-actions.js +530 -339
  17. package/dist/utils/custom-actions.js.map +1 -1
  18. package/dist/utils/document-hooks.d.ts.map +1 -1
  19. package/dist/utils/document-hooks.js +18 -0
  20. package/dist/utils/document-hooks.js.map +1 -1
  21. package/dist/utils/field-path-utils.d.ts.map +1 -1
  22. package/dist/utils/field-path-utils.js +1 -1
  23. package/dist/utils/field-path-utils.js.map +1 -1
  24. package/dist/utils/model-utils.d.ts.map +1 -1
  25. package/dist/utils/model-utils.js +7 -3
  26. package/dist/utils/model-utils.js.map +1 -1
  27. package/package.json +5 -5
  28. package/src/content-store.ts +21 -12
  29. package/src/types/content-store-types.ts +1 -0
  30. package/src/types/custom-actions.ts +123 -20
  31. package/src/utils/create-update-csi-docs.ts +7 -15
  32. package/src/utils/custom-actions.ts +658 -403
  33. package/src/utils/document-hooks.ts +18 -0
  34. package/src/utils/field-path-utils.ts +3 -1
  35. package/src/utils/model-utils.ts +9 -5
@@ -1,30 +1,34 @@
1
1
  import _ from 'lodash';
2
- import { Config } from '@stackbit/sdk';
2
+ import { Config, PresetMap } from '@stackbit/sdk';
3
3
  import * as StackbitTypes from '@stackbit/types';
4
- import { mapPromise, omitByNil } from '@stackbit/utils';
4
+ import { omitByNil } from '@stackbit/utils';
5
+ import { v4 as uuid } from 'uuid';
5
6
  import * as ContentStoreTypes from '../types';
6
7
  import {
7
8
  CustomActionStateChange,
9
+ CustomActionRunStateMap,
10
+ CustomActionRunState,
8
11
  APICustomAction,
12
+ APICustomActionGlobal,
13
+ APICustomActionBulk,
14
+ APICustomActionModel,
15
+ APICustomActionModelSpecifier,
9
16
  APICustomActionDocumentSpecifier,
10
17
  APIGetCustomActionRequest,
18
+ APIGetRunningCustomActionsRequest,
11
19
  APIRunCustomActionRequest,
12
- APIRunCustomActionRequestBulk,
13
- CustomActionRunStateMap,
14
- ExtendedCustomActionObjectModel,
15
- ExtendedCustomActionField,
16
- APICustomActionGlobal,
17
- APICustomActionBulk,
20
+ APIRunCustomActionRequestModel,
21
+ APIRunCustomActionResponse,
18
22
  ExtendedCustomAction,
19
- ExtendedCustomActionObjectField,
20
- ExtendedCustomActionDocument
23
+ ExtendedCustomActionGlobal,
24
+ ExtendedCustomActionBulk,
25
+ ExtendedCustomActionModelGlobal
21
26
  } from '../types';
22
27
  import { createConfigDelegate } from './config-delegate';
23
28
  import { getContentSourceActionsForSourceThunk } from './document-hooks';
24
29
  import { mapStoreDocumentToCSIDocumentWithSource, mapStoreFieldToCSIField } from './store-to-csi-docs-converter';
25
30
  import { getModelAndDocumentFieldForLocalizedFieldPath } from './field-path-utils';
26
31
  import { getContentSourceDataByTypeAndProjectIdOrThrow, getUserContextForSrcTypeThunk } from '../content-store-utils';
27
- import { CustomActionBulk, CustomActionGlobal, CustomActionPermissions } from '@stackbit/types';
28
32
 
29
33
  export async function getGlobalAndBulkAPIActions({
30
34
  stackbitConfig,
@@ -44,7 +48,7 @@ export async function getGlobalAndBulkAPIActions({
44
48
  user?: ContentStoreTypes.User;
45
49
  locale?: string;
46
50
  currentPageDocument?: APICustomActionDocumentSpecifier;
47
- }): Promise<(APICustomActionGlobal | APICustomActionBulk)[]> {
51
+ }): Promise<(APICustomActionGlobal | APICustomActionBulk | APICustomActionModel)[]> {
48
52
  if (!stackbitConfig || !Array.isArray(stackbitConfig.actions)) {
49
53
  return [];
50
54
  }
@@ -54,10 +58,9 @@ export async function getGlobalAndBulkAPIActions({
54
58
  logger: userLogger
55
59
  });
56
60
 
57
- return mapPromise(stackbitConfig.actions, async (action) => {
61
+ const apiCustomActions: (APICustomActionGlobal | APICustomActionBulk | APICustomActionModel)[] = [];
62
+ for (const action of stackbitConfig.actions) {
58
63
  const actionId = globalActionId(action);
59
- const actionRunState = customActionRunStateMap[actionId];
60
- let state: StackbitTypes.CustomActionState | 'unknown';
61
64
  const pageDocument = getCSIDocumentWithSourceFromDocumentSpec(currentPageDocument, configDelegate);
62
65
  const commonStateOptions: StackbitTypes.CustomActionStateCommonOptions = {
63
66
  actionId: actionId,
@@ -68,36 +71,35 @@ export async function getGlobalAndBulkAPIActions({
68
71
  ...configDelegate
69
72
  };
70
73
 
71
- if (actionRunState?.runningHandler) {
72
- state = 'running';
73
- } else if (action.state) {
74
- state = await action.state(commonStateOptions);
75
- } else {
76
- state = actionRunState?.lastResultState ?? 'enabled';
77
- }
74
+ const actionRunState = customActionRunStateMap[actionId];
78
75
 
79
- let permissionsResult;
80
- if (commonStateOptions.currentUser) {
81
- const commonPermissionsOptions: StackbitTypes.CustomActionPermissionsCommonOptions = {
82
- ..._.omit(commonStateOptions, ['currentUser']),
83
- userContext: commonStateOptions.currentUser
84
- };
76
+ const extendedAction = getExtendedActionFromGlobalAction({
77
+ action,
78
+ actionId,
79
+ actionRunState,
80
+ generateNewActionId: false
81
+ });
85
82
 
86
- permissionsResult = resolveCustomActionPermissions({ action, commonPermissionsOptions, contentSourceDataById });
87
- }
83
+ const state = await resolveCustomActionState({
84
+ extendedAction,
85
+ commonStateOptions
86
+ });
88
87
 
89
- return omitByNil({
90
- type: action.type,
91
- actionId: actionId,
92
- name: action.name,
93
- label: action.label ?? _.startCase(action.name),
94
- icon: action.icon,
95
- state: state,
96
- inputFields: action.inputFields,
97
- hidden: action.hidden,
98
- permissions: permissionsResult
88
+ const permissions = resolveCustomActionPermissions({
89
+ extendedAction,
90
+ commonStateOptions
99
91
  });
100
- });
92
+
93
+ apiCustomActions.push(
94
+ extendedActionToApiAction({
95
+ extendedAction,
96
+ state,
97
+ permissions
98
+ })
99
+ );
100
+ }
101
+
102
+ return apiCustomActions;
101
103
  }
102
104
 
103
105
  export async function resolveCustomActionsById({
@@ -122,7 +124,7 @@ export async function resolveCustomActionsById({
122
124
  });
123
125
 
124
126
  for (const actionId of customActionIds) {
125
- const extendedAction = findCustomActionById({
127
+ const extendedAction = getExtendedActionById({
126
128
  actionId,
127
129
  customActionRunStateMap,
128
130
  contentSourceDataById,
@@ -144,78 +146,20 @@ export async function resolveCustomActionsById({
144
146
  };
145
147
 
146
148
  try {
147
- let state: StackbitTypes.CustomActionState;
148
- if (extendedAction.runningHandler) {
149
- state = 'running';
150
- } else if (extendedAction.state) {
151
- if (extendedAction.type === 'global' || extendedAction.type === 'bulk') {
152
- state = await extendedAction.state(commonStateOptions);
153
- } else if (extendedAction.type === 'document') {
154
- const { document, model } = getCSIDocumentAndModelWithSourceFromDocumentSpec({
155
- documentSpec: extendedAction.documentSpec,
156
- contentSourceDataById
157
- });
158
- state = await extendedAction.state({
159
- ...commonStateOptions,
160
- document: document,
161
- model: model
162
- });
163
- } else if (extendedAction.type === 'objectModel') {
164
- const stateObjectParams = getHandlerParamsForObjectModelAction({
165
- extendedAction,
166
- contentSourceDataById
167
- });
168
- state = await extendedAction.state({
169
- ...commonStateOptions,
170
- ...stateObjectParams
171
- });
172
- } else if (extendedAction.type === 'objectField') {
173
- const stateObjectParams = getHandlerParamsForObjectFieldAction({
174
- extendedAction,
175
- contentSourceDataById
176
- });
177
- state = await extendedAction.state({
178
- ...commonStateOptions,
179
- ...stateObjectParams
180
- });
181
- } else if (extendedAction.type === 'field') {
182
- const stateFieldParams = getHandlerParamsForFieldAction({
183
- extendedAction,
184
- contentSourceDataById
185
- });
186
- state = await extendedAction.state({
187
- ...commonStateOptions,
188
- ...stateFieldParams
189
- });
190
- } else {
191
- const _exhaustiveCheck: never = extendedAction;
192
- continue;
193
- }
194
- } else {
195
- state = extendedAction.lastResultState ?? 'enabled';
196
- }
197
-
198
- let permissionsResult;
199
- if (commonStateOptions.currentUser) {
200
- const commonPermissionsOptions: StackbitTypes.CustomActionPermissionsCommonOptions = {
201
- ..._.omit(commonStateOptions, ['currentUser']),
202
- userContext: commonStateOptions.currentUser
203
- };
204
-
205
- permissionsResult = resolveCustomActionPermissions({ action: extendedAction, commonPermissionsOptions, contentSourceDataById });
206
- }
149
+ const state = await resolveCustomActionState({
150
+ extendedAction,
151
+ commonStateOptions
152
+ });
153
+ const permissions = resolveCustomActionPermissions({
154
+ extendedAction,
155
+ commonStateOptions
156
+ });
207
157
 
208
158
  result.push(
209
- omitByNil({
210
- actionId: actionId,
211
- type: storeActionTypeToAPIActionType(extendedAction.type),
212
- name: extendedAction.name,
213
- label: extendedAction.label,
214
- icon: extendedAction.icon,
215
- state: state,
216
- inputFields: extendedAction.inputFields,
217
- hidden: extendedAction.hidden,
218
- permissions: permissionsResult
159
+ extendedActionToApiAction({
160
+ extendedAction,
161
+ state,
162
+ permissions
219
163
  })
220
164
  );
221
165
  } catch (error: any) {
@@ -226,103 +170,279 @@ export async function resolveCustomActionsById({
226
170
  return result;
227
171
  }
228
172
 
229
- export function resolveCustomActionPermissions({
230
- action,
231
- commonPermissionsOptions,
232
- contentSourceDataById
173
+ async function resolveCustomActionState({
174
+ extendedAction,
175
+ commonStateOptions
233
176
  }: {
234
- action:
235
- | CustomActionGlobal
236
- | CustomActionBulk
237
- | ExtendedCustomActionDocument
238
- | ExtendedCustomActionObjectModel
239
- | ExtendedCustomActionObjectField
240
- | ExtendedCustomActionField;
241
- commonPermissionsOptions: StackbitTypes.CustomActionPermissionsCommonOptions;
242
- contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
243
- }): CustomActionPermissions | undefined {
244
- if (typeof action?.permissions !== 'function' || !commonPermissionsOptions.userContext) {
245
- return undefined;
177
+ extendedAction: ExtendedCustomAction;
178
+ commonStateOptions: StackbitTypes.CustomActionStateCommonOptions;
179
+ }): Promise<StackbitTypes.CustomActionState> {
180
+ if (extendedAction.isRunning) {
181
+ return 'running';
182
+ }
183
+
184
+ if (!('state' in extendedAction) || typeof extendedAction.state !== 'function') {
185
+ return 'enabled';
246
186
  }
247
187
 
248
- let document, model, stateParams;
249
- switch (action.type) {
188
+ switch (extendedAction.extendedType) {
250
189
  case 'global':
251
190
  case 'bulk':
252
- return action.permissions?.(commonPermissionsOptions);
191
+ return await extendedAction.state(commonStateOptions);
253
192
  case 'document':
254
- ({ document, model } = getCSIDocumentAndModelWithSourceFromDocumentSpec({
255
- documentSpec: action.documentSpec,
256
- contentSourceDataById
257
- }));
258
- return action.permissions?.({ ...commonPermissionsOptions, document, model });
193
+ return await extendedAction.state({
194
+ ...commonStateOptions,
195
+ document: extendedAction.documentWithSource,
196
+ model: extendedAction.modelWithSource
197
+ });
259
198
  case 'objectModel':
260
- stateParams = getHandlerParamsForObjectModelAction({
261
- extendedAction: action,
262
- contentSourceDataById
199
+ return await extendedAction.state({
200
+ ...commonStateOptions,
201
+ parentDocument: extendedAction.documentWithSource,
202
+ parentModel: extendedAction.modelWithSource,
203
+ documentField: extendedAction.documentField,
204
+ modelField: extendedAction.modelField,
205
+ objectModel: extendedAction.objectModel,
206
+ fieldPath: extendedAction.fieldPath
263
207
  });
264
- return action.permissions?.({ ...commonPermissionsOptions, ...stateParams });
265
208
  case 'objectField':
266
- stateParams = getHandlerParamsForObjectFieldAction({
267
- extendedAction: action,
268
- contentSourceDataById
209
+ return await extendedAction.state({
210
+ ...commonStateOptions,
211
+ parentDocument: extendedAction.documentWithSource,
212
+ parentModel: extendedAction.modelWithSource,
213
+ documentField: extendedAction.documentField,
214
+ modelField: extendedAction.modelField,
215
+ fieldPath: extendedAction.fieldPath
269
216
  });
270
- return action.permissions?.({ ...commonPermissionsOptions, ...stateParams });
271
217
  case 'field':
272
- stateParams = getHandlerParamsForFieldAction({
273
- extendedAction: action,
274
- contentSourceDataById
218
+ return await extendedAction.state({
219
+ ...commonStateOptions,
220
+ parentDocument: extendedAction.documentWithSource,
221
+ parentModel: extendedAction.modelWithSource,
222
+ documentField: extendedAction.documentField,
223
+ modelField: extendedAction.modelField,
224
+ fieldPath: extendedAction.fieldPath
275
225
  });
276
- return action.permissions?.({ ...commonPermissionsOptions, ...stateParams });
226
+ default: {
227
+ // actions of type 'model' do not support 'state' function
228
+ const _exhaustiveCheck: never = extendedAction;
229
+ throw new Error(`error getting action state, action of type '${_exhaustiveCheck['type']}' does not support states`);
230
+ }
277
231
  }
278
232
  }
279
233
 
280
- export function runCustomAction({
281
- runActionRequest,
234
+ function resolveCustomActionPermissions({
235
+ extendedAction,
236
+ commonStateOptions
237
+ }: {
238
+ extendedAction: ExtendedCustomAction;
239
+ commonStateOptions: StackbitTypes.CustomActionStateCommonOptions;
240
+ }): StackbitTypes.CustomActionPermissions | undefined {
241
+ if (typeof extendedAction?.permissions !== 'function' || !commonStateOptions.currentUser) {
242
+ return undefined;
243
+ }
244
+
245
+ const { currentUser, ...restOptions } = commonStateOptions;
246
+ const commonPermissionsOptions: StackbitTypes.CustomActionPermissionsCommonOptions = {
247
+ ...restOptions,
248
+ userContext: currentUser
249
+ };
250
+
251
+ switch (extendedAction.extendedType) {
252
+ case 'global':
253
+ case 'bulk':
254
+ case 'modelGlobal':
255
+ case 'model':
256
+ case 'modelObject': {
257
+ return extendedAction.permissions?.(commonPermissionsOptions);
258
+ }
259
+ case 'document': {
260
+ return extendedAction.permissions?.({
261
+ ...commonPermissionsOptions,
262
+ document: extendedAction.documentWithSource,
263
+ model: extendedAction.modelWithSource
264
+ });
265
+ }
266
+ case 'objectModel': {
267
+ return extendedAction.permissions?.({
268
+ ...commonPermissionsOptions,
269
+ parentDocument: extendedAction.documentWithSource,
270
+ parentModel: extendedAction.modelWithSource,
271
+ documentField: extendedAction.documentField,
272
+ modelField: extendedAction.modelField,
273
+ objectModel: extendedAction.objectModel,
274
+ fieldPath: extendedAction.fieldPath
275
+ });
276
+ }
277
+ case 'objectField': {
278
+ return extendedAction.permissions?.({
279
+ ...commonPermissionsOptions,
280
+ parentDocument: extendedAction.documentWithSource,
281
+ parentModel: extendedAction.modelWithSource,
282
+ documentField: extendedAction.documentField,
283
+ modelField: extendedAction.modelField,
284
+ fieldPath: extendedAction.fieldPath
285
+ });
286
+ }
287
+ case 'field': {
288
+ return extendedAction.permissions?.({
289
+ ...commonPermissionsOptions,
290
+ parentDocument: extendedAction.documentWithSource,
291
+ parentModel: extendedAction.modelWithSource,
292
+ documentField: extendedAction.documentField,
293
+ modelField: extendedAction.modelField,
294
+ fieldPath: extendedAction.fieldPath
295
+ });
296
+ }
297
+ default: {
298
+ const _exhaustiveCheck: never = extendedAction;
299
+ return _exhaustiveCheck;
300
+ }
301
+ }
302
+ }
303
+
304
+ export function getRunningActions({
305
+ getRunningActionsRequest,
282
306
  customActionRunStateMap,
283
307
  contentSourceDataById,
284
308
  stackbitConfig,
285
309
  userLogger
310
+ }: {
311
+ getRunningActionsRequest: APIGetRunningCustomActionsRequest;
312
+ customActionRunStateMap: CustomActionRunStateMap;
313
+ contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
314
+ stackbitConfig: Config | null;
315
+ userLogger: StackbitTypes.Logger;
316
+ }): APICustomAction[] {
317
+ if (!getRunningActionsRequest.user) {
318
+ return [];
319
+ }
320
+ const runningActions: APICustomAction[] = [];
321
+ for (const [actionId, actionRunState] of Object.entries(customActionRunStateMap)) {
322
+ if (actionRunState.isRunning && actionRunState.userId === getRunningActionsRequest.user.id) {
323
+ const extendedAction = getExtendedActionById({
324
+ actionId,
325
+ customActionRunStateMap,
326
+ contentSourceDataById,
327
+ stackbitConfig
328
+ });
329
+ if (!extendedAction) {
330
+ userLogger.debug(`custom action with id: '${actionId}' was not found`);
331
+ continue;
332
+ }
333
+ runningActions.push(
334
+ extendedActionToApiAction({
335
+ extendedAction,
336
+ state: 'running'
337
+ })
338
+ );
339
+ }
340
+ }
341
+ return runningActions;
342
+ }
343
+
344
+ type ActionType = ExtendedCustomAction['type'];
345
+ type ExtendedActionForActionType<Type extends ActionType, U extends ExtendedCustomAction = ExtendedCustomAction> = U extends { type: Type } ? U : never;
346
+ type APICustomActionForType<Type extends ActionType, U extends APICustomAction = APICustomAction> = U extends { type: Type } ? U : never;
347
+
348
+ function extendedActionToApiAction<Type extends ActionType>({
349
+ extendedAction,
350
+ state,
351
+ permissions
352
+ }: {
353
+ extendedAction: ExtendedActionForActionType<Type>;
354
+ state: StackbitTypes.CustomActionState;
355
+ permissions?: StackbitTypes.CustomActionPermissions;
356
+ }): APICustomActionForType<Type> {
357
+ const apiAction: APICustomAction = omitByNil({
358
+ type: extendedAction.type,
359
+ name: extendedAction.name,
360
+ label: extendedAction.label,
361
+ actionId: extendedAction.actionId,
362
+ userId: extendedAction.userId,
363
+ icon: extendedAction.icon,
364
+ state: state,
365
+ permissions: permissions,
366
+ models: extendedAction.extendedType === 'modelGlobal' && 'models' in extendedAction ? extendedAction.models : undefined,
367
+ inputFields: extendedAction.inputFields,
368
+ hidden: extendedAction.hidden
369
+ });
370
+ return apiAction as APICustomActionForType<Type>;
371
+ }
372
+
373
+ export async function runCustomAction({
374
+ runActionRequest,
375
+ customActionRunStateMap,
376
+ contentSourceDataById,
377
+ stackbitConfig,
378
+ userLogger,
379
+ presets,
380
+ onProgress
286
381
  }: {
287
382
  runActionRequest: APIRunCustomActionRequest;
288
383
  customActionRunStateMap: CustomActionRunStateMap;
289
384
  contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
290
385
  stackbitConfig: Config | null;
291
386
  userLogger: StackbitTypes.Logger;
292
- }): Promise<CustomActionStateChange> {
293
- const extendedAction = findCustomActionById({
387
+ presets: PresetMap;
388
+ onProgress: (options: CustomActionStateChange) => void;
389
+ }): Promise<APIRunCustomActionResponse> {
390
+ const extendedAction = getExtendedActionById({
294
391
  actionId: runActionRequest.actionId,
295
392
  customActionRunStateMap,
296
393
  contentSourceDataById,
297
- stackbitConfig
394
+ stackbitConfig,
395
+ generateNewActionId: true
298
396
  });
299
397
  if (!extendedAction) {
300
398
  throw new Error(`Error running action: action not found, action name: '${runActionRequest.actionName}' action ID: '${runActionRequest.actionId}'.`);
301
399
  }
302
- const prevResultState = extendedAction.lastResultState;
303
- if (extendedAction.lastResultState && extendedAction.lastResultState !== 'enabled') {
304
- throw new Error(
305
- `Error running action: action is not enabled, action name: '${runActionRequest.actionName}' action ID: '${runActionRequest.actionId}'.`
306
- );
400
+ if (extendedAction.isRunning) {
401
+ throw new Error(`Error running action: action '${runActionRequest.actionName}' with ID '${runActionRequest.actionId}' is already running.`);
307
402
  }
308
403
 
404
+ const actionId = extendedAction.actionId;
405
+ let isRunning = true;
406
+
309
407
  try {
310
408
  const actionLogger = userLogger.createLogger({ label: `action:${extendedAction.name}` });
311
409
  const configDelegate = createConfigDelegate({ contentSourceDataById: contentSourceDataById, logger: actionLogger });
312
410
  const currentPageDocument = getCSIDocumentWithSourceFromDocumentSpec(runActionRequest.currentPageDocument, configDelegate);
313
411
 
314
- customActionRunStateMap[runActionRequest.actionId] = {
315
- runningHandler: true,
316
- lastResultState: 'running'
412
+ const actionRunState: CustomActionRunState = {
413
+ isRunning: true,
414
+ userId: runActionRequest.user?.id
317
415
  };
416
+ customActionRunStateMap[actionId] = actionRunState;
417
+
418
+ const inputData = runActionRequest.inputData ?? {};
419
+ if (runActionRequest.actionType === 'model' && 'presetId' in runActionRequest) {
420
+ inputData.presetData = presets[runActionRequest.presetId!]?.data;
421
+ }
318
422
 
319
423
  const commonRunOptions: StackbitTypes.CustomActionRunCommonOptions = {
320
- actionId: extendedAction.actionId,
321
- inputData: runActionRequest.inputData,
424
+ actionId: actionId,
425
+ inputData: inputData,
322
426
  currentLocale: runActionRequest.locale,
323
427
  currentUser: runActionRequest.user,
324
428
  currentPageUrl: runActionRequest.pageUrl,
325
429
  currentPageDocument: currentPageDocument,
430
+ progress: ({ message, percent }) => {
431
+ if (!isRunning) {
432
+ return;
433
+ }
434
+ onProgress(
435
+ omitByNil({
436
+ actionId: actionId,
437
+ actionName: extendedAction.name,
438
+ actionType: extendedAction.type,
439
+ userId: runActionRequest.user?.id,
440
+ state: 'running',
441
+ message,
442
+ percent
443
+ })
444
+ );
445
+ },
326
446
  getContentSourceActionsForSource: getContentSourceActionsForSourceThunk({
327
447
  getContentSourceDataById: () => contentSourceDataById,
328
448
  logger: userLogger,
@@ -335,10 +455,15 @@ export function runCustomAction({
335
455
 
336
456
  let promise: Promise<StackbitTypes.CustomActionResult | void>;
337
457
 
338
- if (extendedAction.type === 'global') {
458
+ if (extendedAction.extendedType === 'global') {
339
459
  promise = extendedAction.run(commonRunOptions);
340
- } else if (extendedAction.type === 'bulk') {
341
- const documents = (runActionRequest as APIRunCustomActionRequestBulk).documents.map((documentSpec) => {
460
+ } else if (extendedAction.extendedType === 'bulk') {
461
+ if (!('documents' in runActionRequest) || !Array.isArray(runActionRequest.documents)) {
462
+ throw new Error(
463
+ `Bulk action run request must contain array of documents, action name: '${runActionRequest.actionName}', action ID: '${runActionRequest.actionId}'.`
464
+ );
465
+ }
466
+ const documents = runActionRequest.documents.map((documentSpec) => {
342
467
  const { document } = getCSIDocumentAndModelWithSourceFromDocumentSpec({
343
468
  documentSpec,
344
469
  contentSourceDataById
@@ -349,74 +474,140 @@ export function runCustomAction({
349
474
  ...commonRunOptions,
350
475
  documents
351
476
  });
352
- } else if (extendedAction.type === 'document') {
353
- const { document, model } = getCSIDocumentAndModelWithSourceFromDocumentSpec({
354
- documentSpec: extendedAction.documentSpec,
355
- contentSourceDataById
477
+ } else if (extendedAction.extendedType === 'modelGlobal') {
478
+ if (!('srcType' in runActionRequest) || !('srcProjectId' in runActionRequest) || !('modelName' in runActionRequest)) {
479
+ throw new Error(
480
+ `Global model action run request must contain srcType, srcProjectId and modelName, action name: '${runActionRequest.actionName}', action ID: '${runActionRequest.actionId}'.`
481
+ );
482
+ }
483
+ actionRunState.modelSpec = {
484
+ srcType: runActionRequest.srcType,
485
+ srcProjectId: runActionRequest.srcProjectId,
486
+ modelName: runActionRequest.modelName
487
+ };
488
+ const runOptions = getRunParamsForModelAction({
489
+ modelSpec: actionRunState.modelSpec,
490
+ location: runActionRequest.location,
491
+ contentSourceDataById,
492
+ getContentSourceActionsForSource: commonRunOptions.getContentSourceActionsForSource
493
+ });
494
+ promise = extendedAction.run({
495
+ ...commonRunOptions,
496
+ ...runOptions
497
+ });
498
+ } else if (extendedAction.extendedType === 'model') {
499
+ const runOptions = getRunParamsForModelAction({
500
+ modelSpec: extendedAction.modelSpec,
501
+ location: (runActionRequest as APIRunCustomActionRequestModel).location,
502
+ contentSourceDataById,
503
+ getContentSourceActionsForSource: commonRunOptions.getContentSourceActionsForSource
356
504
  });
357
505
  promise = extendedAction.run({
358
506
  ...commonRunOptions,
359
- document,
360
- model,
507
+ ...runOptions
508
+ });
509
+ } else if (extendedAction.extendedType === 'document') {
510
+ promise = extendedAction.run({
511
+ ...commonRunOptions,
512
+ document: extendedAction.documentWithSource,
513
+ model: extendedAction.modelWithSource,
361
514
  contentSourceActions: commonRunOptions.getContentSourceActionsForSource({
362
- srcType: extendedAction.documentSpec.srcType,
363
- srcProjectId: extendedAction.documentSpec.srcProjectId
515
+ srcType: extendedAction.documentWithSource.srcType,
516
+ srcProjectId: extendedAction.documentWithSource.srcProjectId
364
517
  })!
365
518
  });
366
- } else if (extendedAction.type === 'objectModel') {
367
- const handlerObjectParams = getHandlerParamsForObjectModelAction({
368
- extendedAction,
369
- contentSourceDataById
370
- });
519
+ } else if (extendedAction.extendedType === 'modelObject') {
371
520
  promise = extendedAction.run({
372
521
  ...commonRunOptions,
373
- ...handlerObjectParams,
522
+ parentDocument: extendedAction.documentWithSource,
523
+ parentModel: extendedAction.modelWithSource,
524
+ modelField: extendedAction.modelField,
525
+ objectModel: extendedAction.objectModel,
526
+ fieldPath: extendedAction.fieldPath,
527
+ location: (runActionRequest as APIRunCustomActionRequestModel).location,
374
528
  contentSourceActions: commonRunOptions.getContentSourceActionsForSource({
375
- srcType: extendedAction.documentSpec.srcType,
376
- srcProjectId: extendedAction.documentSpec.srcProjectId
529
+ srcType: extendedAction.documentWithSource.srcType,
530
+ srcProjectId: extendedAction.documentWithSource.srcProjectId
377
531
  })!
378
532
  });
379
- } else if (extendedAction.type === 'objectField') {
380
- const handlerObjectParams = getHandlerParamsForObjectFieldAction({
381
- extendedAction,
382
- contentSourceDataById
533
+ } else if (extendedAction.extendedType === 'objectModel') {
534
+ promise = extendedAction.run({
535
+ ...commonRunOptions,
536
+ parentDocument: extendedAction.documentWithSource,
537
+ parentModel: extendedAction.modelWithSource,
538
+ documentField: extendedAction.documentField,
539
+ modelField: extendedAction.modelField,
540
+ objectModel: extendedAction.objectModel,
541
+ fieldPath: extendedAction.fieldPath,
542
+ contentSourceActions: commonRunOptions.getContentSourceActionsForSource({
543
+ srcType: extendedAction.documentWithSource.srcType,
544
+ srcProjectId: extendedAction.documentWithSource.srcProjectId
545
+ })!
383
546
  });
547
+ } else if (extendedAction.extendedType === 'objectField') {
384
548
  promise = extendedAction.run({
385
549
  ...commonRunOptions,
386
- ...handlerObjectParams,
550
+ parentDocument: extendedAction.documentWithSource,
551
+ parentModel: extendedAction.modelWithSource,
552
+ documentField: extendedAction.documentField,
553
+ modelField: extendedAction.modelField,
554
+ fieldPath: extendedAction.fieldPath,
387
555
  contentSourceActions: commonRunOptions.getContentSourceActionsForSource({
388
- srcType: extendedAction.documentSpec.srcType,
389
- srcProjectId: extendedAction.documentSpec.srcProjectId
556
+ srcType: extendedAction.documentWithSource.srcType,
557
+ srcProjectId: extendedAction.documentWithSource.srcProjectId
390
558
  })!
391
559
  });
392
- } else if (extendedAction.type === 'field') {
393
- const handlerFieldParams = getHandlerParamsForFieldAction({ extendedAction, contentSourceDataById });
560
+ } else if (extendedAction.extendedType === 'field') {
394
561
  promise = extendedAction.run({
395
562
  ...commonRunOptions,
396
- ...handlerFieldParams,
563
+ parentDocument: extendedAction.documentWithSource,
564
+ parentModel: extendedAction.modelWithSource,
565
+ documentField: extendedAction.documentField,
566
+ modelField: extendedAction.modelField,
567
+ fieldPath: extendedAction.fieldPath,
397
568
  contentSourceActions: commonRunOptions.getContentSourceActionsForSource({
398
- srcType: extendedAction.documentSpec.srcType,
399
- srcProjectId: extendedAction.documentSpec.srcProjectId
569
+ srcType: extendedAction.documentWithSource.srcType,
570
+ srcProjectId: extendedAction.documentWithSource.srcProjectId
400
571
  })!
401
572
  });
402
573
  } else {
403
- throw new Error(`action type ${(extendedAction as any).type} not supported`);
574
+ const _exhaustiveCheck: never = extendedAction;
575
+ throw new Error(`action of type ${_exhaustiveCheck['type']} is not supported`);
404
576
  }
405
577
 
406
- return promise
407
- .then((actionResult) => {
408
- customActionRunStateMap[runActionRequest.actionId] = {
409
- runningHandler: false,
410
- lastResultState: actionResult?.state
411
- };
412
- userLogger.debug(`Action completed: ${extendedAction.actionId}`);
413
- return Promise.resolve(
578
+ onProgress(
579
+ omitByNil({
580
+ actionId: actionId,
581
+ actionName: extendedAction.name,
582
+ actionType: extendedAction.type,
583
+ userId: runActionRequest.user?.id,
584
+ state: 'running'
585
+ })
586
+ );
587
+
588
+ promise
589
+ .then(async (actionResult) => {
590
+ isRunning = false;
591
+ delete customActionRunStateMap[actionId];
592
+ userLogger.debug(`Action completed: ${actionId}`);
593
+ const state = await resolveCustomActionState({
594
+ extendedAction,
595
+ commonStateOptions: {
596
+ actionId: actionId,
597
+ currentLocale: runActionRequest.locale,
598
+ currentUser: runActionRequest.user,
599
+ currentPageUrl: runActionRequest.pageUrl,
600
+ currentPageDocument: currentPageDocument,
601
+ ...configDelegate
602
+ }
603
+ });
604
+ onProgress(
414
605
  omitByNil({
415
- actionId: extendedAction.actionId,
606
+ actionId: actionId,
416
607
  actionName: extendedAction.name,
417
- actionType: storeActionTypeToAPIActionType(extendedAction.type),
418
- // TODO: resolve the state if state function is defined
419
- state: actionResult?.state ?? 'enabled',
608
+ actionType: extendedAction.type,
609
+ userId: runActionRequest.user?.id,
610
+ state: extendedAction.type === 'model' ? 'finished' : state,
420
611
  success: actionResult?.success,
421
612
  error: actionResult?.error,
422
613
  result: actionResult?.result
@@ -424,37 +615,29 @@ export function runCustomAction({
424
615
  );
425
616
  })
426
617
  .catch((error: any) => {
427
- customActionRunStateMap[runActionRequest.actionId] = {
428
- runningHandler: false,
429
- lastResultState: prevResultState
430
- };
431
- userLogger.debug(`Error running action: ${error.message}`);
432
- return Promise.resolve({
433
- actionId: extendedAction.actionId,
434
- actionName: extendedAction.name,
435
- actionType: storeActionTypeToAPIActionType(extendedAction.type),
436
- // TODO: resolve the state if state function is defined
437
- state: prevResultState ?? 'enabled',
438
- error: `Error running action: ${error.message}`
439
- });
618
+ isRunning = false;
619
+ delete customActionRunStateMap[actionId];
620
+ userLogger.warn(`Error running action: ${error.stack ?? error.message}`);
621
+ onProgress(
622
+ omitByNil({
623
+ actionId: extendedAction.actionId,
624
+ actionName: extendedAction.name,
625
+ actionType: extendedAction.type,
626
+ userId: runActionRequest.user?.id,
627
+ state: extendedAction.type === 'model' ? 'failed' : 'enabled',
628
+ error: `Error running action: ${error.message}`
629
+ })
630
+ );
440
631
  });
441
632
  } catch (error: any) {
442
- if (customActionRunStateMap[runActionRequest.actionId]) {
443
- customActionRunStateMap[runActionRequest.actionId] = {
444
- runningHandler: false,
445
- lastResultState: prevResultState
446
- };
447
- }
448
- userLogger.debug(`Error running action: ${error.message}`);
449
- return Promise.resolve({
450
- actionId: runActionRequest.actionId,
451
- actionName: extendedAction?.name ?? runActionRequest.actionName,
452
- actionType: storeActionTypeToAPIActionType(extendedAction?.type) ?? runActionRequest.actionType,
453
- // TODO: resolve the state if state function is defined
454
- state: prevResultState ?? 'enabled',
455
- error: `Error running action: ${error.message}`
456
- });
633
+ isRunning = false;
634
+ delete customActionRunStateMap[actionId];
635
+ userLogger.warn(`Error running action: ${error.stack ?? error.message}`);
636
+ // rethrow the error to return erroneous response
637
+ throw error;
457
638
  }
639
+
640
+ return { actionId };
458
641
  }
459
642
 
460
643
  function getCSIDocumentWithSourceFromDocumentSpec(
@@ -470,112 +653,34 @@ function getCSIDocumentWithSourceFromDocumentSpec(
470
653
  : undefined;
471
654
  }
472
655
 
473
- function getHandlerParamsForObjectModelAction({
474
- extendedAction,
475
- contentSourceDataById
476
- }: {
477
- extendedAction: Pick<ExtendedCustomActionObjectModel, 'documentSpec' | 'fieldPath'>;
478
- contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
479
- }): StackbitTypes.CustomActionObjectModelStateParams {
480
- const fieldActionCommonParams = getHandlerParamsForFieldAction({
481
- extendedAction,
482
- contentSourceDataById
483
- });
484
-
485
- if (!fieldActionCommonParams.documentField) {
486
- throw new Error(`object document field not found at field path: ${extendedAction.fieldPath.join('.')}`);
487
- }
488
-
489
- const documentField = fieldActionCommonParams.documentField as StackbitTypes.DocumentModelFieldNonLocalized;
490
- const documentSpec = extendedAction.documentSpec;
491
- const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(documentSpec.srcType, documentSpec.srcProjectId, contentSourceDataById);
492
- const objectModel = contentSourceData.modelMap[documentField.modelName];
493
- if (!objectModel || objectModel.type !== 'object') {
494
- throw new Error(`object model '${documentField.modelName}' not found`);
495
- }
496
-
497
- return {
498
- ...fieldActionCommonParams,
499
- documentField,
500
- modelField: fieldActionCommonParams.modelField as StackbitTypes.FieldModel,
501
- objectModel: {
502
- ...objectModel,
503
- srcType: documentSpec.srcType,
504
- srcProjectId: documentSpec.srcProjectId
505
- }
506
- };
507
- }
508
-
509
- function getHandlerParamsForObjectFieldAction({
510
- extendedAction,
511
- contentSourceDataById
512
- }: {
513
- extendedAction: Pick<ExtendedCustomActionObjectField, 'documentSpec' | 'fieldPath'>;
514
- contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
515
- }): StackbitTypes.CustomActionObjectFieldStateParams {
516
- const fieldActionCommonParams = getHandlerParamsForFieldAction({
517
- extendedAction,
518
- contentSourceDataById
519
- });
520
-
521
- if (!fieldActionCommonParams.documentField) {
522
- throw new Error(`object document field not found at field path: ${extendedAction.fieldPath.join('.')}`);
523
- }
524
-
525
- return {
526
- ...fieldActionCommonParams,
527
- documentField: fieldActionCommonParams.documentField as StackbitTypes.DocumentObjectFieldNonLocalized,
528
- modelField: fieldActionCommonParams.modelField as StackbitTypes.FieldObject
529
- };
530
- }
531
-
532
- function getHandlerParamsForFieldAction({
533
- extendedAction,
534
- contentSourceDataById
656
+ function getRunParamsForModelAction({
657
+ modelSpec,
658
+ location,
659
+ contentSourceDataById,
660
+ getContentSourceActionsForSource
535
661
  }: {
536
- extendedAction: Pick<ExtendedCustomActionField, 'documentSpec' | 'fieldPath'>;
662
+ modelSpec: APICustomActionModelSpecifier;
663
+ location: 'new-card' | 'preset-card';
537
664
  contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
538
- }): StackbitTypes.CustomActionFieldStateParams {
539
- const documentSpec = extendedAction.documentSpec;
540
- const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(documentSpec.srcType, documentSpec.srcProjectId, contentSourceDataById);
541
- const document = contentSourceData.documentMap[documentSpec.srcDocumentId];
542
- const csiDocument = contentSourceData.csiDocumentMap[documentSpec.srcDocumentId];
543
- if (!document || !csiDocument) {
544
- throw new Error(
545
- `document not found for srcType: ${documentSpec.srcType}, srcProjectId: ${documentSpec.srcProjectId}, srcDocumentId: ${documentSpec.srcDocumentId}`
546
- );
547
- }
548
-
549
- const model = contentSourceData.modelMap[document.srcModelName];
550
- if (!model) {
551
- throw new Error(`model '${document.srcModelName}' not found`);
665
+ getContentSourceActionsForSource: ReturnType<typeof getContentSourceActionsForSourceThunk>;
666
+ }): StackbitTypes.CustomActionModelRunOptions {
667
+ const { srcType, srcProjectId, modelName } = modelSpec;
668
+ const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(srcType, srcProjectId, contentSourceDataById);
669
+ const actionModel = contentSourceData.modelMap[modelName];
670
+ if (!actionModel) {
671
+ throw new Error(`model '${modelName}' not found`);
552
672
  }
553
-
554
- const mappedCSIDocument = mapStoreDocumentToCSIDocumentWithSource({ document, csiDocument });
555
-
556
- // the documentField should be localized because fieldPath includes locales
557
- const { modelField, documentField } = getModelAndDocumentFieldForLocalizedFieldPath({
558
- document,
559
- fieldPath: extendedAction.fieldPath,
560
- modelMap: contentSourceData.modelMap
561
- }) as {
562
- // list items cannot have actions, therefore we can safely reduce the type to non list item fields
563
- modelField: StackbitTypes.Field;
564
- documentField?: ContentStoreTypes.DocumentField;
565
- };
566
-
567
- const csiDocumentField = documentField ? mapStoreFieldToCSIField(documentField) : undefined;
568
-
569
673
  return {
570
- parentDocument: mappedCSIDocument,
571
- parentModel: {
572
- ...model,
573
- srcType: documentSpec.srcType,
574
- srcProjectId: documentSpec.srcProjectId
674
+ actionModel: {
675
+ ...actionModel,
676
+ srcType,
677
+ srcProjectId
575
678
  },
576
- documentField: csiDocumentField,
577
- modelField: modelField,
578
- fieldPath: extendedAction.fieldPath
679
+ location,
680
+ contentSourceActions: getContentSourceActionsForSource({
681
+ srcType,
682
+ srcProjectId
683
+ })!
579
684
  };
580
685
  }
581
686
 
@@ -597,16 +702,16 @@ function getCSIDocumentAndModelWithSourceFromDocumentSpec({
597
702
  `document not found, srcType: ${documentSpec.srcType}, srcProjectId: ${documentSpec.srcProjectId}, srcDocumentId: ${documentSpec.srcDocumentId}`
598
703
  );
599
704
  }
600
- const mappedDocument = mapStoreDocumentToCSIDocumentWithSource({
705
+ const documentWithSource = mapStoreDocumentToCSIDocumentWithSource({
601
706
  document,
602
707
  csiDocument
603
708
  });
604
- const model = contentSourceData.modelMap[mappedDocument.modelName];
709
+ const model = contentSourceData.modelMap[documentWithSource.modelName];
605
710
  if (!model) {
606
- throw new Error(`model '${mappedDocument.modelName}' not found`);
711
+ throw new Error(`model '${documentWithSource.modelName}' not found`);
607
712
  }
608
713
  return {
609
- document: mappedDocument,
714
+ document: documentWithSource,
610
715
  model: {
611
716
  ...model,
612
717
  srcType: documentSpec.srcType,
@@ -615,25 +720,23 @@ function getCSIDocumentAndModelWithSourceFromDocumentSpec({
615
720
  };
616
721
  }
617
722
 
618
- function storeActionTypeToAPIActionType(storeActionType: ExtendedCustomAction['type']): APICustomAction['type'] {
619
- if (storeActionType === 'objectModel' || storeActionType === 'objectField') {
620
- return 'object';
621
- }
622
- return storeActionType;
623
- }
624
-
625
- function findCustomActionById({
723
+ function getExtendedActionById({
626
724
  actionId,
627
725
  customActionRunStateMap,
628
726
  contentSourceDataById,
629
- stackbitConfig
727
+ stackbitConfig,
728
+ generateNewActionId
630
729
  }: {
631
730
  actionId: string;
632
731
  customActionRunStateMap: CustomActionRunStateMap;
633
732
  contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
634
733
  stackbitConfig: Config | null;
734
+ generateNewActionId?: boolean;
635
735
  }): ExtendedCustomAction | undefined {
636
736
  const actionRunState = customActionRunStateMap[actionId];
737
+
738
+ // Check if this is a global action defined in the actions array of stackbit.config.
739
+ // Their types can be "global", "bulk" or "model"
637
740
  if (isGlobalActionId(actionId)) {
638
741
  if (!stackbitConfig || !Array.isArray(stackbitConfig.actions)) {
639
742
  return undefined;
@@ -643,105 +746,241 @@ function findCustomActionById({
643
746
  if (!action) {
644
747
  return undefined;
645
748
  }
749
+ return getExtendedActionFromGlobalAction({
750
+ action,
751
+ actionId,
752
+ actionRunState,
753
+ generateNewActionId
754
+ });
755
+ }
756
+
757
+ const { srcType, srcProjectId, modelName, srcDocumentId, fieldPath, actionName } = parseActionId(actionId) ?? {};
758
+ if (!srcType || !srcProjectId || !actionName) {
759
+ return undefined;
760
+ }
761
+ const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(srcType, srcProjectId, contentSourceDataById);
762
+
763
+ // If "modelName" was specified without "srcDocumentId" then it has to be a
764
+ // "model" action defined in a model of type 'page' or 'data'.
765
+ if (!srcDocumentId && modelName) {
766
+ const model = contentSourceData.modelMap[modelName];
767
+ if (!model || (model.type !== 'page' && model.type !== 'data')) {
768
+ return undefined;
769
+ }
770
+ const action = model.actions?.find((action) => action.name === actionName);
771
+ if (!action || action.type !== 'model') {
772
+ return undefined;
773
+ }
646
774
  return {
647
775
  ...action,
648
- actionId,
649
- label: action.label ?? _.startCase(action.name),
650
- runningHandler: actionRunState?.runningHandler,
651
- lastResultState: actionRunState?.lastResultState
776
+ extendedType: 'model',
777
+ actionId:
778
+ generateNewActionId && !actionRunState
779
+ ? JSON.stringify({
780
+ srcType,
781
+ srcProjectId,
782
+ modelName,
783
+ actionName,
784
+ actionRunId: uuid().substring(0, 8)
785
+ })
786
+ : actionId,
787
+ label: getActionLabel(action),
788
+ modelSpec: { srcType, srcProjectId, modelName },
789
+ userId: actionRunState?.userId,
790
+ isRunning: actionRunState?.isRunning
652
791
  };
653
792
  }
654
- const { srcType, srcProjectId, srcDocumentId, actionName, fieldPath } = parseActionId(actionId) ?? {};
655
- if (!srcType || !srcProjectId || !srcDocumentId || !actionName) {
793
+
794
+ if (!srcDocumentId) {
656
795
  return undefined;
657
796
  }
658
- const documentSpec: APICustomActionDocumentSpecifier = { srcType, srcProjectId, srcDocumentId };
659
- const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(srcType, srcProjectId, contentSourceDataById);
660
797
  const document = contentSourceData.documentMap[srcDocumentId];
661
- if (!document) {
798
+ const csiDocument = contentSourceData.csiDocumentMap[srcDocumentId];
799
+ if (!document || !csiDocument) {
662
800
  return undefined;
663
801
  }
664
- const modelName = document.srcModelName;
665
- const model = contentSourceData.modelMap[modelName];
802
+
803
+ const model = contentSourceData.modelMap[document.srcModelName];
666
804
  // The model of a document is always 'page' or 'data',
667
- // this condition helps TS to infer the right type of model.actions
805
+ // this condition helps Typescript infer the right type of model.actions
668
806
  if (!model || (model.type !== 'page' && model.type !== 'data')) {
669
807
  return undefined;
670
808
  }
809
+ const documentWithSource = mapStoreDocumentToCSIDocumentWithSource({
810
+ document,
811
+ csiDocument
812
+ });
813
+ const modelWithSource = { ...model, srcType, srcProjectId };
671
814
  if (typeof fieldPath === 'undefined') {
672
- // fieldPath was not provided, therefore the model must be of type "page" or "data",
673
- // and the action type must be 'document'
815
+ // If fieldPath was not provided, then the model must be of type "page" or "data",
816
+ // and the action type must be of type 'document'
674
817
  const action = model.actions?.find((action) => action.name === actionName);
675
- if (!action) {
818
+ if (!action || action.type !== 'document') {
676
819
  return undefined;
677
820
  }
678
821
  return {
679
822
  // if configuration is updated, the new action properties will override the stored action properties
680
823
  ...action,
681
824
  type: 'document',
825
+ extendedType: 'document',
682
826
  actionId,
683
- label: action.label ?? _.startCase(action.name),
684
- documentSpec,
685
- runningHandler: actionRunState?.runningHandler,
686
- lastResultState: actionRunState?.lastResultState
827
+ label: getActionLabel(action),
828
+ documentWithSource,
829
+ modelWithSource,
830
+ userId: actionRunState?.userId,
831
+ isRunning: actionRunState?.isRunning
687
832
  };
688
833
  } else {
689
834
  const { modelField, documentField } = getModelAndDocumentFieldForLocalizedFieldPath({
690
835
  document,
691
836
  fieldPath,
692
837
  modelMap: contentSourceData.modelMap
693
- });
838
+ }) as {
839
+ // list items cannot have actions, therefore we can safely omit the list item types
840
+ modelField: StackbitTypes.Field;
841
+ documentField?: ContentStoreTypes.DocumentField;
842
+ };
843
+ const csiDocumentField = documentField ? mapStoreFieldToCSIField(documentField) : undefined;
694
844
 
845
+ // Find an action with the provided 'actionName' in model.field.actions
695
846
  if ('actions' in modelField && Array.isArray(modelField.actions)) {
696
847
  const action = modelField.actions.find((action) => action.name === actionName);
697
848
  if (action) {
849
+ if (action.type === 'object') {
850
+ if (!csiDocumentField) {
851
+ throw new Error(`object document field not found at field path: ${fieldPath.join('.')}`);
852
+ }
853
+ if (modelField.type !== 'object' || csiDocumentField.type !== 'object') {
854
+ // action of type 'object' cannot be defined on field other than 'object'
855
+ return undefined;
856
+ }
857
+ return {
858
+ // if configuration is updated, the new action properties will override the stored action properties
859
+ ...action,
860
+ extendedType: 'objectField',
861
+ actionId,
862
+ label: getActionLabel(action),
863
+ documentWithSource,
864
+ modelWithSource,
865
+ documentField: csiDocumentField as StackbitTypes.DocumentObjectFieldNonLocalized,
866
+ modelField: modelField as StackbitTypes.FieldObject,
867
+ fieldPath,
868
+ userId: actionRunState?.userId,
869
+ isRunning: actionRunState?.isRunning
870
+ };
871
+ }
698
872
  return {
699
873
  // if configuration is updated, the new action properties will override the stored action properties
700
874
  ...action,
701
- type: action.type === 'object' ? 'objectField' : 'field',
875
+ type: 'field',
876
+ extendedType: 'field',
702
877
  actionId,
703
- label: action.label ?? _.startCase(action.name),
704
- documentSpec,
878
+ label: getActionLabel(action),
879
+ documentWithSource,
880
+ modelWithSource,
881
+ documentField: csiDocumentField,
882
+ modelField,
705
883
  fieldPath,
706
- runningHandler: actionRunState?.runningHandler,
707
- lastResultState: actionRunState?.lastResultState
708
- } as ExtendedCustomActionField | ExtendedCustomActionObjectField;
884
+ userId: actionRunState?.userId,
885
+ isRunning: actionRunState?.isRunning
886
+ };
709
887
  }
710
888
  }
711
889
 
890
+ // If the field is of type "model", find an action with the provided 'actionName' in model.actions
712
891
  if (modelField.type === 'model') {
713
- if (!documentField || documentField.type !== 'model' || documentField.localized || documentField.isUnset) {
714
- return undefined;
715
- }
716
- const modelName = documentField.srcModelName;
717
- const model = contentSourceData.modelMap[modelName];
718
- if (!model || model.type !== 'object') {
719
- return undefined;
720
- }
721
- if (!('actions' in model && Array.isArray(model.actions))) {
722
- return undefined;
723
- }
724
- // This is a nested model of type "object", so the action must be CustomActionObjectModel
725
- const action = model.actions.find((action) => action.name === actionName);
726
- if (!action) {
727
- return undefined;
892
+ if (modelName) {
893
+ const objectModel = contentSourceData.modelMap[modelName];
894
+ if (!objectModel || objectModel.type !== 'object') {
895
+ return undefined;
896
+ }
897
+ // This is a nested model of type "object", so the action must be CustomActionModelObject
898
+ const action = objectModel.actions?.find((action) => action.name === actionName);
899
+ if (!action || action.type !== 'model') {
900
+ return undefined;
901
+ }
902
+ return {
903
+ // if configuration is updated, the new action properties will override the stored action properties
904
+ ...action,
905
+ extendedType: 'modelObject',
906
+ actionId,
907
+ label: getActionLabel(action),
908
+ documentWithSource,
909
+ modelWithSource,
910
+ modelField,
911
+ objectModel: { ...objectModel, srcType, srcProjectId },
912
+ fieldPath,
913
+ userId: actionRunState?.userId,
914
+ isRunning: actionRunState?.isRunning
915
+ };
916
+ } else {
917
+ if (!csiDocumentField || csiDocumentField.type !== 'model' || csiDocumentField.localized) {
918
+ return undefined;
919
+ }
920
+ const modelName = csiDocumentField.modelName;
921
+ const objectModel = contentSourceData.modelMap[modelName];
922
+ if (!objectModel || objectModel.type !== 'object') {
923
+ return undefined;
924
+ }
925
+ // This is a nested model of type "object", so the action must be CustomActionObjectModel
926
+ const action = objectModel.actions?.find((action) => action.name === actionName);
927
+ if (!action || action.type !== 'object') {
928
+ return undefined;
929
+ }
930
+ return {
931
+ // if configuration is updated, the new action properties will override the stored action properties
932
+ ...action,
933
+ type: 'object',
934
+ extendedType: 'objectModel',
935
+ actionId,
936
+ label: getActionLabel(action),
937
+ documentWithSource,
938
+ modelWithSource,
939
+ documentField: csiDocumentField,
940
+ modelField,
941
+ objectModel: { ...objectModel, srcType, srcProjectId },
942
+ fieldPath,
943
+ userId: actionRunState?.userId,
944
+ isRunning: actionRunState?.isRunning
945
+ };
728
946
  }
729
- return {
730
- // if configuration is updated, the new action properties will override the stored action properties
731
- ...action,
732
- type: 'objectModel',
733
- actionId,
734
- label: action.label ?? _.startCase(action.name),
735
- documentSpec,
736
- fieldPath,
737
- runningHandler: actionRunState?.runningHandler,
738
- lastResultState: actionRunState?.lastResultState
739
- };
740
947
  }
741
948
  }
742
949
  }
743
950
 
744
- function globalActionId(action: StackbitTypes.CustomActionGlobal | StackbitTypes.CustomActionBulk): string {
951
+ function getExtendedActionFromGlobalAction({
952
+ action,
953
+ actionId,
954
+ actionRunState,
955
+ generateNewActionId
956
+ }: {
957
+ action: StackbitTypes.CustomActionGlobal | StackbitTypes.CustomActionBulk | StackbitTypes.CustomActionModel;
958
+ actionId: string;
959
+ actionRunState?: CustomActionRunState;
960
+ generateNewActionId?: boolean;
961
+ }): ExtendedCustomActionGlobal | ExtendedCustomActionBulk | ExtendedCustomActionModelGlobal {
962
+ if (action.type === 'model') {
963
+ return {
964
+ ...action,
965
+ extendedType: 'modelGlobal',
966
+ actionId: generateNewActionId && !actionRunState ? `${actionId}.${uuid().substring(0, 8)}` : actionId,
967
+ label: getActionLabel(action),
968
+ modelSpec: actionRunState?.modelSpec,
969
+ userId: actionRunState?.userId,
970
+ isRunning: actionRunState?.isRunning
971
+ };
972
+ }
973
+ return {
974
+ ...action,
975
+ extendedType: action.type,
976
+ actionId,
977
+ label: getActionLabel(action),
978
+ userId: actionRunState?.userId,
979
+ isRunning: actionRunState?.isRunning
980
+ } as ExtendedCustomActionGlobal | ExtendedCustomActionBulk;
981
+ }
982
+
983
+ function globalActionId(action: StackbitTypes.CustomActionGlobal | StackbitTypes.CustomActionBulk | StackbitTypes.CustomActionModel): string {
745
984
  return `config.actions.${action.name}`;
746
985
  }
747
986
 
@@ -750,7 +989,8 @@ function isGlobalActionId(actionId: string) {
750
989
  }
751
990
 
752
991
  function getGlobalActionNameFromId(actionId: string) {
753
- return actionId.substring('config.actions.'.length);
992
+ const result = actionId.match(/^config\.actions\.(.+?)(?:\.([\w-]+))?$/);
993
+ return result ? result[1] : undefined;
754
994
  }
755
995
 
756
996
  function parseActionId(actionId: string):
@@ -758,6 +998,7 @@ function parseActionId(actionId: string):
758
998
  srcType?: string;
759
999
  srcProjectId?: string;
760
1000
  srcDocumentId?: string;
1001
+ modelName?: string;
761
1002
  actionName?: string;
762
1003
  fieldPath?: string[];
763
1004
  }
@@ -768,3 +1009,17 @@ function parseActionId(actionId: string):
768
1009
  return undefined;
769
1010
  }
770
1011
  }
1012
+
1013
+ function getActionLabel(
1014
+ action:
1015
+ | StackbitTypes.CustomActionGlobal
1016
+ | StackbitTypes.CustomActionBulk
1017
+ | StackbitTypes.CustomActionModel
1018
+ | StackbitTypes.CustomActionDocument
1019
+ | StackbitTypes.CustomActionModelObject
1020
+ | StackbitTypes.CustomActionObjectModel
1021
+ | StackbitTypes.CustomActionObjectField
1022
+ | StackbitTypes.CustomActionField
1023
+ ): string {
1024
+ return action.label ?? _.startCase(action.name);
1025
+ }