@stackbit/cms-core 0.8.4-develop.1 → 0.8.4-develop.2

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.
@@ -1,5 +1,5 @@
1
1
  import _ from 'lodash';
2
- import { Config, mapModelFieldsRecursively } from '@stackbit/sdk';
2
+ import { Config, PageModel, DataModel, FieldObjectProps, FieldSpecificProps, mapModelFieldsRecursively, ObjectModel } from '@stackbit/sdk';
3
3
  import * as StackbitTypes from '@stackbit/types';
4
4
  import { mapPromise, omitByNil } from '@stackbit/utils';
5
5
  import * as ContentStoreTypes from '../types';
@@ -13,16 +13,14 @@ import {
13
13
  APIGetCustomActionRequest,
14
14
  APIRunCustomActionRequest,
15
15
  APIRunCustomActionRequestBulk,
16
- ContentStoreCustomActionMap,
17
- ContentStoreCustomActionGlobal,
18
- ContentStoreCustomActionBulk,
19
- ContentStoreCustomActionDocument,
20
- ContentStoreCustomActionObjectModel,
21
- ContentStoreCustomActionField,
16
+ CustomActionRunStateMap,
17
+ ExtendedCustomActionDocument,
18
+ ExtendedCustomActionObjectModel,
19
+ ExtendedCustomActionField,
22
20
  APICustomActionGlobal,
23
21
  APICustomActionBulk,
24
- ContentStoreCustomAction,
25
- ContentStoreCustomActionObjectField
22
+ ExtendedCustomAction,
23
+ ExtendedCustomActionObjectField
26
24
  } from '../types';
27
25
  import { createConfigDelegate } from './config-delegate';
28
26
  import { getContentSourceActionsForSourceThunk } from './document-hooks';
@@ -59,7 +57,7 @@ export function stripModelActions({ modelMap }: { modelMap: StackbitTypes.ModelM
59
57
 
60
58
  export async function getGlobalAndBulkAPIActions({
61
59
  stackbitConfig,
62
- customActionMap,
60
+ customActionRunStateMap,
63
61
  contentSourceDataById,
64
62
  userLogger,
65
63
  pageUrl,
@@ -68,7 +66,7 @@ export async function getGlobalAndBulkAPIActions({
68
66
  currentPageDocument
69
67
  }: {
70
68
  stackbitConfig: Config | null;
71
- customActionMap: ContentStoreCustomActionMap;
69
+ customActionRunStateMap: CustomActionRunStateMap;
72
70
  contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
73
71
  userLogger: StackbitTypes.Logger;
74
72
  pageUrl?: string;
@@ -86,25 +84,15 @@ export async function getGlobalAndBulkAPIActions({
86
84
  });
87
85
 
88
86
  return mapPromise(stackbitConfig.actions, async (action) => {
89
- const actionId = `config.actions.${action.name}`;
90
- const prevStoreAction = customActionMap[actionId] as ContentStoreCustomActionGlobal | ContentStoreCustomActionBulk | undefined;
91
- const storeAction: ContentStoreCustomActionGlobal | ContentStoreCustomActionBulk = {
92
- // if configuration is updated, the new action properties will override the stored action properties
93
- ...action,
94
- actionId,
95
- label: action.label ?? _.startCase(action.name),
96
- runningHandler: prevStoreAction?.runningHandler,
97
- lastResultState: prevStoreAction?.lastResultState
98
- };
99
- customActionMap[actionId] = storeAction;
100
-
87
+ const actionId = globalActionId(action);
88
+ const actionRunState = customActionRunStateMap[actionId];
101
89
  let state: StackbitTypes.CustomActionState | 'unknown';
102
90
 
103
- if (storeAction.runningHandler) {
91
+ if (actionRunState?.runningHandler) {
104
92
  state = 'running';
105
- } else if (storeAction.state) {
93
+ } else if (action.state) {
106
94
  const pageDocument = getCSIDocumentWithSourceFromDocumentSpec(currentPageDocument, configDelegate);
107
- state = await storeAction.state({
95
+ state = await action.state({
108
96
  actionId: actionId,
109
97
  currentLocale: locale,
110
98
  currentUser: user ? { name: user.name, email: user.email } : undefined,
@@ -113,189 +101,230 @@ export async function getGlobalAndBulkAPIActions({
113
101
  ...configDelegate
114
102
  });
115
103
  } else {
116
- state = storeAction.lastResultState ?? 'enabled';
104
+ state = actionRunState?.lastResultState ?? 'enabled';
117
105
  }
118
106
 
119
107
  return omitByNil({
108
+ type: action.type,
120
109
  actionId: actionId,
121
- type: storeAction.type,
122
- name: storeAction.name,
123
- label: storeAction.label,
124
- icon: storeAction.icon,
110
+ name: action.name,
111
+ label: action.label ?? _.startCase(action.name),
112
+ icon: action.icon,
125
113
  state: state,
126
- inputFields: storeAction.inputFields
114
+ inputFields: action.inputFields
127
115
  });
128
116
  });
129
117
  }
130
118
 
131
- export function convertDocumentActionToAPICustomDocumentAction({
132
- action,
133
- customActionMap,
134
- csiDocument
119
+ export function getDocumentActionsThunk({
120
+ csiDocument,
121
+ model,
122
+ srcType,
123
+ srcProjectId,
124
+ customActionRunStateMap
135
125
  }: {
136
- action: StackbitTypes.CustomActionDocument;
137
- customActionMap: ContentStoreCustomActionMap;
138
- csiDocument: StackbitTypes.DocumentWithSource;
139
- }): APICustomActionDocument {
140
- const actionId = `${csiDocument.srcType}:${csiDocument.srcProjectId}:${csiDocument.id}:${action.name}`;
141
-
142
- const prevStoreAction = customActionMap[actionId] as ContentStoreCustomActionDocument | undefined;
143
- const storeAction: ContentStoreCustomActionDocument = {
144
- // if configuration is updated, the new action properties will override the stored action properties
145
- ...action,
146
- type: 'document',
147
- actionId,
148
- label: action.label ?? _.startCase(action.name),
149
- runningHandler: prevStoreAction?.runningHandler,
150
- lastResultState: prevStoreAction?.lastResultState,
151
- documentSpec: {
152
- srcType: csiDocument.srcType,
153
- srcProjectId: csiDocument.srcProjectId,
154
- srcDocumentId: csiDocument.id
155
- }
156
- };
157
- customActionMap[actionId] = storeAction;
158
-
159
- let state: StackbitTypes.CustomActionState | 'unknown';
160
- let needsResolving = false;
161
- if (storeAction?.runningHandler) {
162
- state = 'running';
163
- } else if (action.state) {
164
- state = 'unknown';
165
- needsResolving = true;
166
- } else {
167
- state = storeAction?.lastResultState ?? 'enabled';
126
+ csiDocument: StackbitTypes.Document;
127
+ model: PageModel | DataModel;
128
+ srcType: string;
129
+ srcProjectId: string;
130
+ customActionRunStateMap: ContentStoreTypes.CustomActionRunStateMap;
131
+ }): () => APICustomActionDocument[] | undefined {
132
+ if (!('actions' in model) || !Array.isArray(model.actions)) {
133
+ return () => undefined;
168
134
  }
169
135
 
170
- return omitByNil({
171
- actionId: actionId,
172
- type: 'document',
173
- name: storeAction.name,
174
- label: storeAction.label,
175
- icon: storeAction.icon,
176
- state: state,
177
- inputFields: storeAction.inputFields,
178
- needsResolving: needsResolving
136
+ const extendedDocumentActions = model.actions.map((action): ExtendedCustomActionDocument => {
137
+ const documentSpec = {
138
+ srcType: srcType,
139
+ srcProjectId: srcProjectId,
140
+ srcDocumentId: csiDocument.id
141
+ };
142
+ return {
143
+ // if configuration is updated, the new action properties will override the stored action properties
144
+ ...action,
145
+ type: 'document',
146
+ actionId: documentActionId({ ...documentSpec, actionName: action.name }),
147
+ label: action.label ?? _.startCase(action.name),
148
+ documentSpec: documentSpec
149
+ };
179
150
  });
151
+
152
+ return () =>
153
+ extendedDocumentActions.map((extendedAction) => {
154
+ return omitByNil({
155
+ type: 'document',
156
+ actionId: extendedAction.actionId,
157
+ name: extendedAction.name,
158
+ label: extendedAction.label,
159
+ icon: extendedAction.icon,
160
+ inputFields: extendedAction.inputFields,
161
+ ...getAPIActionState(extendedAction, customActionRunStateMap)
162
+ });
163
+ });
180
164
  }
181
165
 
182
- export function convertObjectModelActionToAPICustomObjectAction({
183
- action,
184
- customActionMap,
166
+ export function getObjectModelActionsThunk({
167
+ model,
185
168
  csiParentDocument,
169
+ srcType,
170
+ srcProjectId,
171
+ customActionRunStateMap,
186
172
  fieldPath
187
173
  }: {
188
- action: StackbitTypes.CustomActionObjectModel;
189
- customActionMap: ContentStoreCustomActionMap;
190
- csiParentDocument: StackbitTypes.DocumentWithSource;
174
+ model: ObjectModel;
175
+ csiParentDocument: StackbitTypes.Document;
176
+ srcType: string;
177
+ srcProjectId: string;
178
+ customActionRunStateMap: ContentStoreTypes.CustomActionRunStateMap;
191
179
  fieldPath: (string | number)[];
192
- }): APICustomActionObject {
193
- const actionId = `${csiParentDocument.srcType}:${csiParentDocument.srcProjectId}:${csiParentDocument.id}:${fieldPath.join('.')}:${action.name}`;
194
-
195
- const prevStoreAction = customActionMap[actionId] as ContentStoreCustomActionObjectModel | undefined;
196
- const storeAction: ContentStoreCustomActionObjectModel = {
197
- // if configuration is updated, the new action properties will override the stored action properties
198
- ...action,
199
- type: 'objectModel',
200
- actionId,
201
- label: action.label ?? _.startCase(action.name),
202
- runningHandler: prevStoreAction?.runningHandler,
203
- lastResultState: prevStoreAction?.lastResultState,
204
- documentSpec: {
205
- srcType: csiParentDocument.srcType,
206
- srcProjectId: csiParentDocument.srcProjectId,
207
- srcDocumentId: csiParentDocument.id
208
- },
209
- fieldPath
210
- };
211
- customActionMap[actionId] = storeAction;
212
-
213
- let state: StackbitTypes.CustomActionState | 'unknown';
214
- let needsResolving = false;
215
- if (storeAction?.runningHandler) {
216
- state = 'running';
217
- } else if (action.state) {
218
- state = 'unknown';
219
- needsResolving = true;
220
- } else {
221
- state = storeAction?.lastResultState ?? 'enabled';
180
+ }): () => ContentStoreTypes.APICustomActionObject[] | undefined {
181
+ if (!('actions' in model) || !Array.isArray(model.actions)) {
182
+ return () => undefined;
222
183
  }
223
184
 
224
- return omitByNil({
225
- actionId: actionId,
226
- type: 'object',
227
- name: storeAction.name,
228
- label: storeAction.label,
229
- icon: storeAction.icon,
230
- state: state,
231
- inputFields: storeAction.inputFields,
232
- needsResolving: needsResolving
185
+ const extendedObjectModelActions = model.actions.map((action): ExtendedCustomActionObjectModel => {
186
+ const documentSpec = {
187
+ srcType: srcType,
188
+ srcProjectId: srcProjectId,
189
+ srcDocumentId: csiParentDocument.id
190
+ };
191
+ return {
192
+ // if configuration is updated, the new action properties will override the stored action properties
193
+ ...action,
194
+ type: 'objectModel',
195
+ actionId: fieldPathActionId({ ...documentSpec, fieldPath, actionName: action.name }),
196
+ label: action.label ?? _.startCase(action.name),
197
+ documentSpec: documentSpec,
198
+ fieldPath
199
+ };
233
200
  });
201
+
202
+ return () =>
203
+ extendedObjectModelActions.map((extendedAction) => {
204
+ return omitByNil({
205
+ type: 'object',
206
+ actionId: extendedAction.actionId,
207
+ name: extendedAction.name,
208
+ label: extendedAction.label,
209
+ icon: extendedAction.icon,
210
+ inputFields: extendedAction.inputFields,
211
+ ...getAPIActionState(extendedAction, customActionRunStateMap)
212
+ });
213
+ });
234
214
  }
235
215
 
236
- export function convertFieldActionToAPICustomAction<T extends StackbitTypes.CustomActionField | StackbitTypes.CustomActionObjectField>({
237
- action,
238
- customActionMap,
216
+ export function getObjectFieldActionsThunk({
217
+ modelField,
239
218
  csiParentDocument,
219
+ srcType,
220
+ srcProjectId,
221
+ customActionRunStateMap,
240
222
  fieldPath
241
223
  }: {
242
- action: T;
243
- customActionMap: ContentStoreCustomActionMap;
244
- csiParentDocument: StackbitTypes.DocumentWithSource;
224
+ modelField: FieldObjectProps;
225
+ csiParentDocument: StackbitTypes.Document;
226
+ srcType: string;
227
+ srcProjectId: string;
228
+ customActionRunStateMap: ContentStoreTypes.CustomActionRunStateMap;
245
229
  fieldPath: (string | number)[];
246
- }): T extends StackbitTypes.CustomActionField ? APICustomActionField : APICustomActionObject {
247
- const actionId = `${csiParentDocument.srcType}:${csiParentDocument.srcProjectId}:${csiParentDocument.id}:${fieldPath.join('.')}:${action.name}`;
248
-
249
- const prevStoreAction = customActionMap[actionId] as ContentStoreCustomActionField | ContentStoreCustomActionObjectField | undefined;
250
- const storeAction = {
251
- // if configuration is updated, the new action properties will override the stored action properties
252
- ...action,
253
- type: action.type === 'object' ? 'objectField' : 'field',
254
- actionId,
255
- label: action.label ?? _.startCase(action.name),
256
- runningHandler: prevStoreAction?.runningHandler,
257
- lastResultState: prevStoreAction?.lastResultState,
258
- documentSpec: {
259
- srcType: csiParentDocument.srcType,
260
- srcProjectId: csiParentDocument.srcProjectId,
230
+ }): () => APICustomActionObject[] | undefined {
231
+ if (!('actions' in modelField) || !Array.isArray(modelField.actions)) {
232
+ return () => undefined;
233
+ }
234
+ const objectFieldActions = modelField.actions.filter((action): action is StackbitTypes.CustomActionObjectField => action.type === 'object');
235
+
236
+ const extendedObjectFieldActions = objectFieldActions.map((action): ExtendedCustomActionObjectField => {
237
+ const documentSpec = {
238
+ srcType: srcType,
239
+ srcProjectId: srcProjectId,
261
240
  srcDocumentId: csiParentDocument.id
262
- },
263
- fieldPath
264
- } as ContentStoreCustomActionField | ContentStoreCustomActionObjectField;
265
- customActionMap[actionId] = storeAction;
241
+ };
242
+ return {
243
+ // if configuration is updated, the new action properties will override the stored action properties
244
+ ...action,
245
+ type: 'objectField',
246
+ actionId: fieldPathActionId({ ...documentSpec, fieldPath, actionName: action.name }),
247
+ label: action.label ?? _.startCase(action.name),
248
+ documentSpec: documentSpec,
249
+ fieldPath
250
+ };
251
+ });
266
252
 
267
- let state: StackbitTypes.CustomActionState | 'unknown';
268
- let needsResolving = false;
269
- if (storeAction?.runningHandler) {
270
- state = 'running';
271
- } else if (action.state) {
272
- state = 'unknown';
273
- needsResolving = true;
274
- } else {
275
- state = storeAction?.lastResultState ?? 'enabled';
253
+ return () =>
254
+ extendedObjectFieldActions.map((extendedAction) => {
255
+ return omitByNil({
256
+ type: 'object',
257
+ actionId: extendedAction.actionId,
258
+ name: extendedAction.name,
259
+ label: extendedAction.label,
260
+ icon: extendedAction.icon,
261
+ inputFields: extendedAction.inputFields,
262
+ ...getAPIActionState(extendedAction, customActionRunStateMap)
263
+ });
264
+ });
265
+ }
266
+
267
+ export function getFieldActions({
268
+ modelField,
269
+ csiParentDocument,
270
+ srcType,
271
+ srcProjectId,
272
+ customActionRunStateMap,
273
+ fieldPath
274
+ }: {
275
+ modelField: FieldSpecificProps;
276
+ csiParentDocument: StackbitTypes.Document;
277
+ srcType: string;
278
+ srcProjectId: string;
279
+ customActionRunStateMap: ContentStoreTypes.CustomActionRunStateMap;
280
+ fieldPath: (string | number)[];
281
+ }): APICustomActionField[] | undefined {
282
+ if (!('actions' in modelField) || !Array.isArray(modelField.actions)) {
283
+ return undefined;
276
284
  }
285
+ const fieldActions = modelField.actions.filter((action): action is StackbitTypes.CustomActionField => action.type !== 'object');
286
+
287
+ const extendedFieldActions = fieldActions.map((action): ExtendedCustomActionField => {
288
+ const documentSpec = {
289
+ srcType: srcType,
290
+ srcProjectId: srcProjectId,
291
+ srcDocumentId: csiParentDocument.id
292
+ };
293
+ return {
294
+ // if configuration is updated, the new action properties will override the stored action properties
295
+ ...action,
296
+ type: 'field',
297
+ actionId: fieldPathActionId({ ...documentSpec, fieldPath, actionName: action.name }),
298
+ label: action.label ?? _.startCase(action.name),
299
+ documentSpec: documentSpec,
300
+ fieldPath
301
+ };
302
+ });
277
303
 
278
- return omitByNil({
279
- actionId: actionId,
280
- type: action.type ?? 'field',
281
- name: storeAction.name,
282
- label: storeAction.label,
283
- icon: storeAction.icon,
284
- state: state,
285
- inputFields: storeAction.inputFields,
286
- needsResolving: needsResolving
287
- }) as T extends StackbitTypes.CustomActionField ? APICustomActionField : APICustomActionObject;
304
+ return extendedFieldActions.map((extendedAction) => {
305
+ return omitByNil({
306
+ type: 'field',
307
+ actionId: extendedAction.actionId,
308
+ name: extendedAction.name,
309
+ label: extendedAction.label,
310
+ icon: extendedAction.icon,
311
+ inputFields: extendedAction.inputFields,
312
+ ...getAPIActionState(extendedAction, customActionRunStateMap)
313
+ });
314
+ });
288
315
  }
289
316
 
290
317
  export async function resolveCustomActionsById({
291
318
  getActionRequest,
292
- customActionMap,
319
+ customActionRunStateMap,
293
320
  contentSourceDataById,
321
+ stackbitConfig,
294
322
  userLogger
295
323
  }: {
296
324
  getActionRequest: APIGetCustomActionRequest;
297
- customActionMap: ContentStoreCustomActionMap;
325
+ customActionRunStateMap: CustomActionRunStateMap;
298
326
  contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
327
+ stackbitConfig: Config | null;
299
328
  userLogger: StackbitTypes.Logger;
300
329
  }): Promise<APICustomAction[]> {
301
330
  const result: APICustomAction[] = [];
@@ -307,17 +336,22 @@ export async function resolveCustomActionsById({
307
336
  });
308
337
 
309
338
  for (const actionId of customActionIds) {
310
- const storeAction = customActionMap[actionId];
311
- if (!storeAction) {
312
- userLogger.debug(`getCustomActionsById: custom action not found, id: '${actionId}'`);
339
+ const extendedAction = findCustomActionById({
340
+ actionId,
341
+ customActionRunStateMap,
342
+ contentSourceDataById,
343
+ stackbitConfig
344
+ });
345
+ if (!extendedAction) {
346
+ userLogger.debug(`custom action with id: '${actionId}' was not found`);
313
347
  continue;
314
348
  }
315
349
 
316
350
  try {
317
351
  let state: StackbitTypes.CustomActionState;
318
- if (storeAction.runningHandler) {
352
+ if (extendedAction.runningHandler) {
319
353
  state = 'running';
320
- } else if (storeAction.state) {
354
+ } else if (extendedAction.state) {
321
355
  const pageDocument = getCSIDocumentWithSourceFromDocumentSpec(currentPageDocument, configDelegate);
322
356
  const commonStateOptions: StackbitTypes.CustomActionStateCommonOptions = {
323
357
  actionId: actionId,
@@ -332,61 +366,61 @@ export async function resolveCustomActionsById({
332
366
  currentPageDocument: pageDocument,
333
367
  ...configDelegate
334
368
  };
335
- if (storeAction.type === 'global' || storeAction.type === 'bulk') {
336
- state = await storeAction.state(commonStateOptions);
337
- } else if (storeAction.type === 'document') {
369
+ if (extendedAction.type === 'global' || extendedAction.type === 'bulk') {
370
+ state = await extendedAction.state(commonStateOptions);
371
+ } else if (extendedAction.type === 'document') {
338
372
  const { document, model } = getCSIDocumentAndModelWithSourceFromDocumentSpec({
339
- documentSpec: storeAction.documentSpec,
373
+ documentSpec: extendedAction.documentSpec,
340
374
  contentSourceDataById
341
375
  });
342
- state = await storeAction.state({
376
+ state = await extendedAction.state({
343
377
  ...commonStateOptions,
344
378
  document: document,
345
379
  model: model
346
380
  });
347
- } else if (storeAction.type === 'objectModel') {
381
+ } else if (extendedAction.type === 'objectModel') {
348
382
  const stateObjectParams = getHandlerParamsForObjectModelAction({
349
- storeAction,
383
+ extendedAction,
350
384
  contentSourceDataById
351
385
  });
352
- state = await storeAction.state({
386
+ state = await extendedAction.state({
353
387
  ...commonStateOptions,
354
388
  ...stateObjectParams
355
389
  });
356
- } else if (storeAction.type === 'objectField') {
390
+ } else if (extendedAction.type === 'objectField') {
357
391
  const stateObjectParams = getHandlerParamsForObjectFieldAction({
358
- storeAction,
392
+ extendedAction,
359
393
  contentSourceDataById
360
394
  });
361
- state = await storeAction.state({
395
+ state = await extendedAction.state({
362
396
  ...commonStateOptions,
363
397
  ...stateObjectParams
364
398
  });
365
- } else if (storeAction.type === 'field') {
399
+ } else if (extendedAction.type === 'field') {
366
400
  const stateFieldParams = getHandlerParamsForFieldAction({
367
- storeAction,
401
+ extendedAction,
368
402
  contentSourceDataById
369
403
  });
370
- state = await storeAction.state({
404
+ state = await extendedAction.state({
371
405
  ...commonStateOptions,
372
406
  ...stateFieldParams
373
407
  });
374
408
  } else {
375
- const _exhaustiveCheck: never = storeAction;
409
+ const _exhaustiveCheck: never = extendedAction;
376
410
  continue;
377
411
  }
378
412
  } else {
379
- state = storeAction?.lastResultState ?? 'enabled';
413
+ state = extendedAction.lastResultState ?? 'enabled';
380
414
  }
381
415
  result.push(
382
416
  omitByNil({
383
417
  actionId: actionId,
384
- type: storeActionTypeToAPIActionType(storeAction.type),
385
- name: storeAction.name,
386
- label: storeAction.label,
387
- icon: storeAction.icon,
418
+ type: storeActionTypeToAPIActionType(extendedAction.type),
419
+ name: extendedAction.name,
420
+ label: extendedAction.label,
421
+ icon: extendedAction.icon,
388
422
  state: state,
389
- inputFields: storeAction.inputFields
423
+ inputFields: extendedAction.inputFields
390
424
  })
391
425
  );
392
426
  } catch (error: any) {
@@ -399,38 +433,45 @@ export async function resolveCustomActionsById({
399
433
 
400
434
  export function runCustomAction({
401
435
  runActionRequest,
402
- customActionMap,
436
+ customActionRunStateMap,
403
437
  contentSourceDataById,
404
- userLogger,
405
- stackbitConfig
438
+ stackbitConfig,
439
+ userLogger
406
440
  }: {
407
441
  runActionRequest: APIRunCustomActionRequest;
408
- customActionMap: ContentStoreCustomActionMap;
442
+ customActionRunStateMap: CustomActionRunStateMap;
409
443
  contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
410
- userLogger: StackbitTypes.Logger;
411
444
  stackbitConfig: Config | null;
445
+ userLogger: StackbitTypes.Logger;
412
446
  }): Promise<CustomActionStateChange> {
413
- const storeAction = customActionMap[runActionRequest.actionId];
414
- if (!storeAction) {
447
+ const extendedAction = findCustomActionById({
448
+ actionId: runActionRequest.actionId,
449
+ customActionRunStateMap,
450
+ contentSourceDataById,
451
+ stackbitConfig
452
+ });
453
+ if (!extendedAction) {
415
454
  throw new Error(`Error running action: action not found, action name: '${runActionRequest.actionName}' action ID: '${runActionRequest.actionId}'.`);
416
455
  }
417
- const prevResultState = storeAction.lastResultState;
418
- if (storeAction.lastResultState && storeAction.lastResultState !== 'enabled') {
456
+ const prevResultState = extendedAction.lastResultState;
457
+ if (extendedAction.lastResultState && extendedAction.lastResultState !== 'enabled') {
419
458
  throw new Error(
420
459
  `Error running action: action is not enabled, action name: '${runActionRequest.actionName}' action ID: '${runActionRequest.actionId}'.`
421
460
  );
422
461
  }
423
462
 
424
463
  try {
425
- const actionLogger = userLogger.createLogger({ label: `action:${storeAction.name}` });
464
+ const actionLogger = userLogger.createLogger({ label: `action:${extendedAction.name}` });
426
465
  const configDelegate = createConfigDelegate({ contentSourceDataById: contentSourceDataById, logger: actionLogger });
427
466
  const currentPageDocument = getCSIDocumentWithSourceFromDocumentSpec(runActionRequest.currentPageDocument, configDelegate);
428
467
 
429
- storeAction.runningHandler = true;
430
- storeAction.lastResultState = 'running';
468
+ customActionRunStateMap[runActionRequest.actionId] = {
469
+ runningHandler: true,
470
+ lastResultState: 'running'
471
+ };
431
472
 
432
473
  const commonRunOptions: StackbitTypes.CustomActionRunCommonOptions = {
433
- actionId: storeAction.actionId,
474
+ actionId: extendedAction.actionId,
434
475
  inputData: runActionRequest.inputData,
435
476
  currentLocale: runActionRequest.locale,
436
477
  currentUser: runActionRequest.user,
@@ -448,9 +489,9 @@ export function runCustomAction({
448
489
 
449
490
  let promise;
450
491
 
451
- if (storeAction.type === 'global') {
452
- promise = storeAction.run(commonRunOptions);
453
- } else if (storeAction.type === 'bulk') {
492
+ if (extendedAction.type === 'global') {
493
+ promise = extendedAction.run(commonRunOptions);
494
+ } else if (extendedAction.type === 'bulk') {
454
495
  const documents = (runActionRequest as APIRunCustomActionRequestBulk).documents.map((documentSpec) => {
455
496
  const { document } = getCSIDocumentAndModelWithSourceFromDocumentSpec({
456
497
  documentSpec,
@@ -458,74 +499,76 @@ export function runCustomAction({
458
499
  });
459
500
  return document;
460
501
  });
461
- promise = storeAction.run({
502
+ promise = extendedAction.run({
462
503
  ...commonRunOptions,
463
504
  documents
464
505
  });
465
- } else if (storeAction.type === 'document') {
506
+ } else if (extendedAction.type === 'document') {
466
507
  const { document, model } = getCSIDocumentAndModelWithSourceFromDocumentSpec({
467
- documentSpec: storeAction.documentSpec,
508
+ documentSpec: extendedAction.documentSpec,
468
509
  contentSourceDataById
469
510
  });
470
- promise = storeAction.run({
511
+ promise = extendedAction.run({
471
512
  ...commonRunOptions,
472
513
  document,
473
514
  model,
474
515
  contentSourceActions: commonRunOptions.getContentSourceActionsForSource({
475
- srcType: storeAction.documentSpec.srcType,
476
- srcProjectId: storeAction.documentSpec.srcProjectId
516
+ srcType: extendedAction.documentSpec.srcType,
517
+ srcProjectId: extendedAction.documentSpec.srcProjectId
477
518
  })!
478
519
  });
479
- } else if (storeAction.type === 'objectModel') {
520
+ } else if (extendedAction.type === 'objectModel') {
480
521
  const handlerObjectParams = getHandlerParamsForObjectModelAction({
481
- storeAction,
522
+ extendedAction,
482
523
  contentSourceDataById
483
524
  });
484
- promise = storeAction.run({
525
+ promise = extendedAction.run({
485
526
  ...commonRunOptions,
486
527
  ...handlerObjectParams,
487
528
  contentSourceActions: commonRunOptions.getContentSourceActionsForSource({
488
- srcType: storeAction.documentSpec.srcType,
489
- srcProjectId: storeAction.documentSpec.srcProjectId
529
+ srcType: extendedAction.documentSpec.srcType,
530
+ srcProjectId: extendedAction.documentSpec.srcProjectId
490
531
  })!
491
532
  });
492
- } else if (storeAction.type === 'objectField') {
533
+ } else if (extendedAction.type === 'objectField') {
493
534
  const handlerObjectParams = getHandlerParamsForObjectFieldAction({
494
- storeAction,
535
+ extendedAction,
495
536
  contentSourceDataById
496
537
  });
497
- promise = storeAction.run({
538
+ promise = extendedAction.run({
498
539
  ...commonRunOptions,
499
540
  ...handlerObjectParams,
500
541
  contentSourceActions: commonRunOptions.getContentSourceActionsForSource({
501
- srcType: storeAction.documentSpec.srcType,
502
- srcProjectId: storeAction.documentSpec.srcProjectId
542
+ srcType: extendedAction.documentSpec.srcType,
543
+ srcProjectId: extendedAction.documentSpec.srcProjectId
503
544
  })!
504
545
  });
505
- } else if (storeAction.type === 'field') {
506
- const handlerFieldParams = getHandlerParamsForFieldAction({ storeAction, contentSourceDataById });
507
- promise = storeAction.run({
546
+ } else if (extendedAction.type === 'field') {
547
+ const handlerFieldParams = getHandlerParamsForFieldAction({ extendedAction, contentSourceDataById });
548
+ promise = extendedAction.run({
508
549
  ...commonRunOptions,
509
550
  ...handlerFieldParams,
510
551
  contentSourceActions: commonRunOptions.getContentSourceActionsForSource({
511
- srcType: storeAction.documentSpec.srcType,
512
- srcProjectId: storeAction.documentSpec.srcProjectId
552
+ srcType: extendedAction.documentSpec.srcType,
553
+ srcProjectId: extendedAction.documentSpec.srcProjectId
513
554
  })!
514
555
  });
515
556
  } else {
516
- throw new Error(`action type ${(storeAction as any).type} not supported`);
557
+ throw new Error(`action type ${(extendedAction as any).type} not supported`);
517
558
  }
518
559
 
519
560
  return promise
520
561
  .then((actionResult) => {
521
- storeAction.runningHandler = false;
522
- storeAction.lastResultState = actionResult?.state;
523
- userLogger.debug(`Action completed: ${storeAction.actionId}`);
562
+ customActionRunStateMap[runActionRequest.actionId] = {
563
+ runningHandler: false,
564
+ lastResultState: actionResult?.state
565
+ };
566
+ userLogger.debug(`Action completed: ${extendedAction.actionId}`);
524
567
  return Promise.resolve(
525
568
  omitByNil({
526
- actionId: storeAction.actionId,
527
- actionName: storeAction.name,
528
- actionType: storeActionTypeToAPIActionType(storeAction.type),
569
+ actionId: extendedAction.actionId,
570
+ actionName: extendedAction.name,
571
+ actionType: storeActionTypeToAPIActionType(extendedAction.type),
529
572
  // TODO: resolve the state if state function is defined
530
573
  state: actionResult?.state ?? 'enabled',
531
574
  success: actionResult?.success,
@@ -534,28 +577,32 @@ export function runCustomAction({
534
577
  );
535
578
  })
536
579
  .catch((error: any) => {
537
- storeAction.runningHandler = false;
538
- storeAction.lastResultState = prevResultState;
580
+ customActionRunStateMap[runActionRequest.actionId] = {
581
+ runningHandler: false,
582
+ lastResultState: prevResultState
583
+ };
539
584
  userLogger.debug(`Error running action: ${error.message}`);
540
585
  return Promise.resolve({
541
- actionId: storeAction.actionId,
542
- actionName: storeAction.name,
543
- actionType: storeActionTypeToAPIActionType(storeAction.type),
586
+ actionId: extendedAction.actionId,
587
+ actionName: extendedAction.name,
588
+ actionType: storeActionTypeToAPIActionType(extendedAction.type),
544
589
  // TODO: resolve the state if state function is defined
545
590
  state: prevResultState ?? 'enabled',
546
591
  error: `Error running action: ${error.message}`
547
592
  });
548
593
  });
549
594
  } catch (error: any) {
550
- if (storeAction) {
551
- storeAction.runningHandler = false;
552
- storeAction.lastResultState = prevResultState;
595
+ if (customActionRunStateMap[runActionRequest.actionId]) {
596
+ customActionRunStateMap[runActionRequest.actionId] = {
597
+ runningHandler: false,
598
+ lastResultState: prevResultState
599
+ };
553
600
  }
554
601
  userLogger.debug(`Error running action: ${error.message}`);
555
602
  return Promise.resolve({
556
603
  actionId: runActionRequest.actionId,
557
- actionName: storeAction?.name ?? runActionRequest.actionName,
558
- actionType: storeActionTypeToAPIActionType(storeAction?.type) ?? runActionRequest.actionType,
604
+ actionName: extendedAction?.name ?? runActionRequest.actionName,
605
+ actionType: storeActionTypeToAPIActionType(extendedAction?.type) ?? runActionRequest.actionType,
559
606
  // TODO: resolve the state if state function is defined
560
607
  state: prevResultState ?? 'enabled',
561
608
  error: `Error running action: ${error.message}`
@@ -576,24 +623,46 @@ function getCSIDocumentWithSourceFromDocumentSpec(
576
623
  : undefined;
577
624
  }
578
625
 
626
+ function getAPIActionState(
627
+ extendedAction: ExtendedCustomAction,
628
+ customActionRunStateMap: CustomActionRunStateMap
629
+ ): {
630
+ state: StackbitTypes.CustomActionState | 'unknown';
631
+ needsResolving: boolean;
632
+ } {
633
+ const actionId = extendedAction.actionId;
634
+ const actionRunState = customActionRunStateMap[actionId];
635
+ let state: StackbitTypes.CustomActionState | 'unknown';
636
+ let needsResolving = false;
637
+ if (actionRunState?.runningHandler) {
638
+ state = 'running';
639
+ } else if (extendedAction.state) {
640
+ state = 'unknown';
641
+ needsResolving = true;
642
+ } else {
643
+ state = actionRunState?.lastResultState ?? 'enabled';
644
+ }
645
+ return { state, needsResolving };
646
+ }
647
+
579
648
  function getHandlerParamsForObjectModelAction({
580
- storeAction,
649
+ extendedAction,
581
650
  contentSourceDataById
582
651
  }: {
583
- storeAction: Pick<ContentStoreCustomActionObjectModel, 'documentSpec' | 'fieldPath'>;
652
+ extendedAction: Pick<ExtendedCustomActionObjectModel, 'documentSpec' | 'fieldPath'>;
584
653
  contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
585
654
  }): StackbitTypes.CustomActionObjectModelStateParams {
586
655
  const fieldActionCommonParams = getHandlerParamsForFieldAction({
587
- storeAction,
656
+ extendedAction,
588
657
  contentSourceDataById
589
658
  });
590
659
 
591
660
  if (!fieldActionCommonParams.documentField) {
592
- throw new Error(`object document field not found at field path: ${storeAction.fieldPath.join('.')}`);
661
+ throw new Error(`object document field not found at field path: ${extendedAction.fieldPath.join('.')}`);
593
662
  }
594
663
 
595
664
  const documentField = fieldActionCommonParams.documentField as StackbitTypes.DocumentModelFieldNonLocalized;
596
- const documentSpec = storeAction.documentSpec;
665
+ const documentSpec = extendedAction.documentSpec;
597
666
  const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(documentSpec.srcType, documentSpec.srcProjectId, contentSourceDataById);
598
667
  const objectModel = contentSourceData.modelMap[documentField.modelName];
599
668
  if (!objectModel || objectModel.type !== 'object') {
@@ -613,19 +682,19 @@ function getHandlerParamsForObjectModelAction({
613
682
  }
614
683
 
615
684
  function getHandlerParamsForObjectFieldAction({
616
- storeAction,
685
+ extendedAction,
617
686
  contentSourceDataById
618
687
  }: {
619
- storeAction: Pick<ContentStoreCustomActionObjectField, 'documentSpec' | 'fieldPath'>;
688
+ extendedAction: Pick<ExtendedCustomActionObjectField, 'documentSpec' | 'fieldPath'>;
620
689
  contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
621
690
  }): StackbitTypes.CustomActionObjectFieldStateParams {
622
691
  const fieldActionCommonParams = getHandlerParamsForFieldAction({
623
- storeAction,
692
+ extendedAction,
624
693
  contentSourceDataById
625
694
  });
626
695
 
627
696
  if (!fieldActionCommonParams.documentField) {
628
- throw new Error(`object document field not found at field path: ${storeAction.fieldPath.join('.')}`);
697
+ throw new Error(`object document field not found at field path: ${extendedAction.fieldPath.join('.')}`);
629
698
  }
630
699
 
631
700
  return {
@@ -636,13 +705,13 @@ function getHandlerParamsForObjectFieldAction({
636
705
  }
637
706
 
638
707
  function getHandlerParamsForFieldAction({
639
- storeAction,
708
+ extendedAction,
640
709
  contentSourceDataById
641
710
  }: {
642
- storeAction: Pick<ContentStoreCustomActionField, 'documentSpec' | 'fieldPath'>;
711
+ extendedAction: Pick<ExtendedCustomActionField, 'documentSpec' | 'fieldPath'>;
643
712
  contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
644
713
  }): StackbitTypes.CustomActionFieldStateParams {
645
- const documentSpec = storeAction.documentSpec;
714
+ const documentSpec = extendedAction.documentSpec;
646
715
  const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(documentSpec.srcType, documentSpec.srcProjectId, contentSourceDataById);
647
716
  const document = contentSourceData.documentMap[documentSpec.srcDocumentId];
648
717
  const csiDocument = contentSourceData.csiDocumentMap[documentSpec.srcDocumentId];
@@ -663,7 +732,7 @@ function getHandlerParamsForFieldAction({
663
732
  const { modelField, documentField } = getModelAndDocumentFieldForLocalizedFieldPath({
664
733
  document,
665
734
  model,
666
- fieldPath: storeAction.fieldPath,
735
+ fieldPath: extendedAction.fieldPath,
667
736
  modelMap: contentSourceData.modelMap
668
737
  }) as {
669
738
  // list items cannot have actions, therefore we can safely reduce the type to non list item fields
@@ -682,7 +751,7 @@ function getHandlerParamsForFieldAction({
682
751
  },
683
752
  documentField: csiDocumentField,
684
753
  modelField: modelField,
685
- fieldPath: storeAction.fieldPath
754
+ fieldPath: extendedAction.fieldPath
686
755
  };
687
756
  }
688
757
 
@@ -722,9 +791,191 @@ function getCSIDocumentAndModelWithSourceFromDocumentSpec({
722
791
  };
723
792
  }
724
793
 
725
- function storeActionTypeToAPIActionType(storeActionType: ContentStoreCustomAction['type']): APICustomAction['type'] {
794
+ function storeActionTypeToAPIActionType(storeActionType: ExtendedCustomAction['type']): APICustomAction['type'] {
726
795
  if (storeActionType === 'objectModel' || storeActionType === 'objectField') {
727
796
  return 'object';
728
797
  }
729
798
  return storeActionType;
730
799
  }
800
+
801
+ function findCustomActionById({
802
+ actionId,
803
+ customActionRunStateMap,
804
+ contentSourceDataById,
805
+ stackbitConfig
806
+ }: {
807
+ actionId: string;
808
+ customActionRunStateMap: CustomActionRunStateMap;
809
+ contentSourceDataById: Record<string, ContentStoreTypes.ContentSourceData>;
810
+ stackbitConfig: Config | null;
811
+ }): ExtendedCustomAction | undefined {
812
+ const actionRunState = customActionRunStateMap[actionId];
813
+ if (isGlobalActionId(actionId)) {
814
+ if (!stackbitConfig || !Array.isArray(stackbitConfig.actions)) {
815
+ return undefined;
816
+ }
817
+ const actionName = getGlobalActionNameFromId(actionId);
818
+ const action = stackbitConfig.actions.find((action) => action.name === actionName);
819
+ if (!action) {
820
+ return undefined;
821
+ }
822
+ return {
823
+ ...action,
824
+ actionId,
825
+ label: action.label ?? _.startCase(action.name),
826
+ runningHandler: actionRunState?.runningHandler,
827
+ lastResultState: actionRunState?.lastResultState
828
+ };
829
+ }
830
+ const { srcType, srcProjectId, srcDocumentId, actionName, fieldPath } = parseActionId(actionId) ?? {};
831
+ if (!srcType || !srcProjectId || !srcDocumentId || !actionName) {
832
+ return undefined;
833
+ }
834
+ const documentSpec: APICustomActionDocumentSpecifier = { srcType, srcProjectId, srcDocumentId };
835
+ const contentSourceData = getContentSourceDataByTypeAndProjectIdOrThrow(srcType, srcProjectId, contentSourceDataById);
836
+ const document = contentSourceData.documentMap[srcDocumentId];
837
+ if (!document) {
838
+ return undefined;
839
+ }
840
+ const modelName = document.srcModelName;
841
+ const model = contentSourceData.modelMap[modelName];
842
+ // The model of a document is always 'page' or 'data',
843
+ // this condition helps TS to infer the right type of model.actions
844
+ if (!model || (model.type !== 'page' && model.type !== 'data')) {
845
+ return undefined;
846
+ }
847
+ if (!('actions' in model) || !Array.isArray(model.actions)) {
848
+ return undefined;
849
+ }
850
+ if (typeof fieldPath === 'undefined') {
851
+ // fieldPath was not provided, therefore the model must be of type "page" or "data",
852
+ // and the action type must be 'document'
853
+ const action = model.actions.find((action) => action.name === actionName);
854
+ if (!action) {
855
+ return undefined;
856
+ }
857
+ return {
858
+ // if configuration is updated, the new action properties will override the stored action properties
859
+ ...action,
860
+ type: 'document',
861
+ actionId,
862
+ label: action.label ?? _.startCase(action.name),
863
+ documentSpec,
864
+ runningHandler: actionRunState?.runningHandler,
865
+ lastResultState: actionRunState?.lastResultState
866
+ };
867
+ } else {
868
+ const { modelField, documentField } = getModelAndDocumentFieldForLocalizedFieldPath({
869
+ document,
870
+ model,
871
+ fieldPath,
872
+ modelMap: contentSourceData.modelMap
873
+ });
874
+
875
+ if ('actions' in modelField && Array.isArray(modelField.actions)) {
876
+ const action = modelField.actions.find((action) => action.name === actionName);
877
+ if (action) {
878
+ return {
879
+ // if configuration is updated, the new action properties will override the stored action properties
880
+ ...action,
881
+ type: action.type === 'object' ? 'objectField' : 'field',
882
+ actionId,
883
+ label: action.label ?? _.startCase(action.name),
884
+ documentSpec,
885
+ fieldPath,
886
+ runningHandler: actionRunState?.runningHandler,
887
+ lastResultState: actionRunState?.lastResultState
888
+ } as ExtendedCustomActionField | ExtendedCustomActionObjectField;
889
+ }
890
+ }
891
+
892
+ if (modelField.type === 'model') {
893
+ if (documentField.type !== 'model' || documentField.localized || documentField.isUnset) {
894
+ return undefined;
895
+ }
896
+ const modelName = documentField.srcModelName;
897
+ const model = contentSourceData.modelMap[modelName];
898
+ if (!model || model.type !== 'object') {
899
+ return undefined;
900
+ }
901
+ if (!('actions' in model && Array.isArray(model.actions))) {
902
+ return undefined;
903
+ }
904
+ // This is a nested model of type "object", so the action must be CustomActionObjectModel
905
+ const action = model.actions.find((action) => action.name === actionName);
906
+ if (!action) {
907
+ return undefined;
908
+ }
909
+ return {
910
+ // if configuration is updated, the new action properties will override the stored action properties
911
+ ...action,
912
+ type: 'objectModel',
913
+ actionId,
914
+ label: action.label ?? _.startCase(action.name),
915
+ documentSpec,
916
+ fieldPath,
917
+ runningHandler: actionRunState?.runningHandler,
918
+ lastResultState: actionRunState?.lastResultState
919
+ };
920
+ }
921
+ }
922
+ }
923
+
924
+ function globalActionId(action: StackbitTypes.CustomActionGlobal | StackbitTypes.CustomActionBulk): string {
925
+ return `config.actions.${action.name}`;
926
+ }
927
+
928
+ function isGlobalActionId(actionId: string) {
929
+ return actionId.startsWith('config.actions.');
930
+ }
931
+
932
+ function getGlobalActionNameFromId(actionId: string) {
933
+ return actionId.substring('config.actions.'.length);
934
+ }
935
+
936
+ function documentActionId({ srcType, srcProjectId, srcDocumentId, actionName }: APICustomActionDocumentSpecifier & { actionName: string }): string {
937
+ return `${srcType}:${srcProjectId}:${srcDocumentId}:${actionName}`;
938
+ }
939
+
940
+ function fieldPathActionId({
941
+ srcType,
942
+ srcProjectId,
943
+ srcDocumentId,
944
+ fieldPath,
945
+ actionName
946
+ }: APICustomActionDocumentSpecifier & { fieldPath: (string | number)[]; actionName: string }): string {
947
+ return `${srcType}:${srcProjectId}:${srcDocumentId}:${fieldPath.join('.')}:${actionName}`;
948
+ }
949
+
950
+ function parseActionId(actionId: string):
951
+ | {
952
+ srcType?: string;
953
+ srcProjectId?: string;
954
+ srcDocumentId?: string;
955
+ actionName?: string;
956
+ fieldPath?: string[];
957
+ }
958
+ | undefined {
959
+ const parts = actionId.split(':');
960
+ if (parts.length < 4 || parts.length > 5) {
961
+ return undefined;
962
+ }
963
+ const srcType = parts[0];
964
+ const srcProjectId = parts[1];
965
+ const srcDocumentId = parts[2];
966
+ let fieldPath: string[] | undefined;
967
+ let actionName: string | undefined;
968
+ if (parts.length === 5) {
969
+ fieldPath = parts[3]?.split('.');
970
+ actionName = parts[4];
971
+ } else {
972
+ actionName = parts[3];
973
+ }
974
+ return {
975
+ srcType,
976
+ srcProjectId,
977
+ srcDocumentId,
978
+ actionName,
979
+ fieldPath
980
+ };
981
+ }