@haiiro/docuflow 0.0.19 → 0.0.21

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.
@@ -7,16 +7,14 @@ import { useTheme } from 'twenty-ui/theme';
7
7
  import {
8
8
  callAppRoute,
9
9
  countRecords,
10
- createRecord,
11
- deleteRecords,
12
10
  getErrorMessage,
13
11
  listRecords,
14
- updateRecord,
15
12
  } from 'src/front/api';
16
13
  import {
17
14
  formatDate,
18
15
  formatDateTime,
19
16
  formatMemberName,
17
+ formatRunStarter,
20
18
  RUN_STATUS_COLORS,
21
19
  RUN_STATUS_LABELS,
22
20
  STEP_TYPE_LABELS,
@@ -24,6 +22,14 @@ import {
24
22
  import { useAsync, useDebouncedValue } from 'src/front/hooks/use-async';
25
23
  import { useBulkAction } from 'src/front/hooks/use-bulk-action';
26
24
  import { useWorkspaceMembers } from 'src/front/hooks/use-workspace-members';
25
+ import {
26
+ createProcess,
27
+ type ProcessEditing,
28
+ publishProcess as publishProcessIn,
29
+ removeProcesses as removeProcessesIn,
30
+ renameProcess as renameProcessIn,
31
+ resolveProcessEditing,
32
+ } from 'src/front/process-editing';
27
33
  import { type DocflowWorkflowRun, type WorkspaceMember } from 'src/front/types';
28
34
  import { ListSkeleton, TableSkeleton } from 'src/front/ui/skeleton';
29
35
  import {
@@ -40,10 +46,10 @@ import {
40
46
  useRowSelection,
41
47
  } from 'src/front/ui/components';
42
48
  import { FormFieldInputs } from 'src/front/ui/form-field-inputs';
49
+ import { ProcessSettings, StandalonePageHint } from 'src/front/ui/process-settings';
43
50
  import { Button, type MenuAction, MenuButton, PromptDialog, QuickViews, StatusChip, ValueDialog } from 'src/front/ui/kit';
44
51
  import { Timeline } from 'src/front/ui/timeline';
45
52
  import { restAnd, restEq } from 'src/shared/api/rest-filter.util';
46
- import { DOCFLOW_SECTION } from 'src/shared/docflow.constants';
47
53
  import { type FormField, type FormValues } from 'src/shared/form/form-field.type';
48
54
  import { findMissingRequiredFields } from 'src/shared/form/form-fields.util';
49
55
  import { logDocflow, logDocflowError } from 'src/shared/log.util';
@@ -59,8 +65,6 @@ import { getFormStepAssignment, getFormStepFields, isFormStep } from 'src/shared
59
65
  import { PROCESSES_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/universal-identifiers';
60
66
 
61
67
  const SEARCH_DEBOUNCE_MS = 300;
62
- const DRAFT_WAIT_ATTEMPTS = 10;
63
- const DRAFT_WAIT_MS = 400;
64
68
  const RECENT_RUNS_LIMIT = 5;
65
69
  const PROCESS_NOUN: [string, string, string] = ['процесс', 'процесса', 'процессов'];
66
70
 
@@ -68,14 +72,15 @@ type Panel = { kind: 'closed' } | { kind: 'process'; workflowId: string };
68
72
  type ProcessView = 'all' | 'active' | 'draft';
69
73
  type ParametersFilter = 'any' | 'with' | 'without';
70
74
 
71
- type WorkflowRecord = { id: string };
72
-
73
75
  type DocflowProcess = {
74
76
  // The process engine's id: the editor page, runs and launching take it
75
77
  workflowId: string;
76
78
  // The process record of Twenty 2.45: renaming and deleting go through it.
77
79
  // Twenty 2.50 has none, those are done in its editor.
78
80
  workspaceWorkflowId: string | null;
81
+ draftVersionId?: string;
82
+ activeVersionId?: string;
83
+ deactivatedVersionId?: string;
79
84
  // The published version people launch; none while the process is a draft
80
85
  workflowVersionId: string | undefined;
81
86
  name: string;
@@ -122,6 +127,9 @@ const toDocflowProcess = (process: CatalogProcess): DocflowProcess => {
122
127
  return {
123
128
  workflowId: process.workflowId,
124
129
  workspaceWorkflowId: process.workspaceWorkflowId,
130
+ draftVersionId: draft?.id,
131
+ activeVersionId: active?.id,
132
+ deactivatedVersionId: deactivated?.id,
125
133
  workflowVersionId: isLaunchable ? active.id : undefined,
126
134
  name: process.name,
127
135
  isPublished: isLaunchable,
@@ -137,15 +145,19 @@ const toDocflowProcess = (process: CatalogProcess): DocflowProcess => {
137
145
  // Docflow processes come from the app's route, which reads them through the
138
146
  // Twenty process engine (2.50 keeps no process records) and knows which are
139
147
  // docflow ones. The list is short: searched here.
140
- const loadProcesses = async (searchQuery: string): Promise<DocflowProcess[]> => {
141
- const { processes } = await callAppRoute<{ processes: CatalogProcess[] }>('/docflow/processes/catalog', {
142
- action: 'list',
143
- });
148
+ const loadProcesses = async (searchQuery: string): Promise<{ processes: DocflowProcess[]; editing: ProcessEditing }> => {
149
+ const { processes, canAppEdit } = await callAppRoute<{ processes: CatalogProcess[]; canAppEdit: boolean }>(
150
+ '/docflow/processes/catalog',
151
+ { action: 'list' },
152
+ );
144
153
  const query = searchQuery.trim().toLowerCase();
145
154
 
146
- return processes
147
- .map(toDocflowProcess)
148
- .filter((process) => query.length === 0 || process.name.toLowerCase().includes(query));
155
+ return {
156
+ processes: processes
157
+ .map(toDocflowProcess)
158
+ .filter((process) => query.length === 0 || process.name.toLowerCase().includes(query)),
159
+ editing: resolveProcessEditing(canAppEdit),
160
+ };
149
161
  };
150
162
 
151
163
  // Runs per process: all of them and the ones still going. A run names its
@@ -166,8 +178,6 @@ const loadRunCounts = async (workflowIds: string[]): Promise<Record<string, RunC
166
178
 
167
179
  const openEditor = (workflowId: string) => navigate(AppPath.WorkflowCoreShowPage, { coreWorkflowId: workflowId });
168
180
 
169
- // What Twenty 2.50 does only in its own editor
170
- const EDITOR_ONLY_MESSAGE = 'В этой версии Twenty это делается в редакторе процесса: «Открыть редактор»';
171
181
 
172
182
  const openRun = (runId: string) =>
173
183
  navigate(AppPath.RecordShowPage, { objectNameSingular: 'workflowRun', objectRecordId: runId });
@@ -203,7 +213,7 @@ const ProcessStages = ({ process, members }: { process: DocflowProcess; members:
203
213
  );
204
214
  };
205
215
 
206
- const RecentRuns = ({ workflowId }: { workflowId: string }) => {
216
+ const RecentRuns = ({ workflowId, members }: { workflowId: string; members: WorkspaceMember[] }) => {
207
217
  const theme = useTheme();
208
218
  const { data: runs, loading } = useAsync(
209
219
  async () =>
@@ -250,7 +260,7 @@ const RecentRuns = ({ workflowId }: { workflowId: string }) => {
250
260
  >
251
261
  <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
252
262
  {formatDateTime(run.startedAt ?? run.createdAt)}
253
- {run.createdBy?.name ? ` · ${run.createdBy.name}` : ''}
263
+ {formatRunStarter(run, members) ? ` · ${formatRunStarter(run, members)}` : ''}
254
264
  </span>
255
265
  <StatusChip color={RUN_STATUS_COLORS[run.status ?? ''] ?? 'gray'}>
256
266
  {RUN_STATUS_LABELS[run.status ?? ''] ?? run.status}
@@ -334,6 +344,7 @@ const ProcessPanel = ({
334
344
  onLaunched,
335
345
  onRename,
336
346
  onPublish,
347
+ onSettingsSaved,
337
348
  }: {
338
349
  process: DocflowProcess;
339
350
  members: WorkspaceMember[];
@@ -341,11 +352,13 @@ const ProcessPanel = ({
341
352
  onLaunched: () => void;
342
353
  onRename: (name: string) => Promise<void>;
343
354
  onPublish: (publish: boolean) => void;
355
+ onSettingsSaved: () => void;
344
356
  }) => {
345
357
  const theme = useTheme();
346
358
  const [values, setValues] = useState<FormValues>({});
347
359
  const [isLaunching, setIsLaunching] = useState(false);
348
360
  const [errorMessage, setErrorMessage] = useState<string | undefined>();
361
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
349
362
 
350
363
  const handleLaunch = async () => {
351
364
  const parameters = buildLaunchParameters({ fields: process.launchFields, values });
@@ -423,13 +436,22 @@ const ProcessPanel = ({
423
436
  : 'Процесс ещё не опубликован: проверьте этапы и нажмите «Опубликовать».'}
424
437
  </Message>
425
438
  )}
439
+ <section style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[3] }}>
440
+ <div style={{ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }}>
441
+ <SectionTitle>Параметры запуска и задания</SectionTitle>
442
+ <Button size="sm" icon={IconSettings} onClick={() => setIsSettingsOpen((current) => !current)}>
443
+ {isSettingsOpen ? 'Свернуть' : 'Настроить'}
444
+ </Button>
445
+ </div>
446
+ {isSettingsOpen && <ProcessSettings workflowId={process.workflowId} onLaunchSaved={onSettingsSaved} />}
447
+ </section>
426
448
  <section style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[3] }}>
427
449
  <SectionTitle>Этапы</SectionTitle>
428
450
  <ProcessStages process={process} members={members} />
429
451
  </section>
430
452
  <section style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[1] }}>
431
453
  <SectionTitle>Последние запуски</SectionTitle>
432
- <RecentRuns workflowId={process.workflowId} />
454
+ <RecentRuns workflowId={process.workflowId} members={members} />
433
455
  </section>
434
456
  </div>
435
457
  );
@@ -499,23 +521,6 @@ const AddFromTwentyDialog = ({ onClose, onAdded }: { onClose: () => void; onAdde
499
521
  );
500
522
  };
501
523
 
502
- // The server gives a new workflow its first draft version right after the
503
- // record; the launch step can only be put into an existing draft.
504
- const waitForDraft = async (workflowId: string) => {
505
- for (let attempt = 0; attempt < DRAFT_WAIT_ATTEMPTS; attempt++) {
506
- const { records } = await listRecords<{ id: string }>('workflowVersions', {
507
- filter: restEq('workflowId', workflowId),
508
- limit: 1,
509
- });
510
-
511
- if (records.length > 0) {
512
- return;
513
- }
514
-
515
- await new Promise((resolve) => setTimeout(resolve, DRAFT_WAIT_MS));
516
- }
517
- };
518
-
519
524
  const DocflowProcesses = () => {
520
525
  const theme = useTheme();
521
526
  const { members } = useWorkspaceMembers();
@@ -529,7 +534,10 @@ const DocflowProcesses = () => {
529
534
  const [isCreating, setIsCreating] = useState(false);
530
535
  const [isAdding, setIsAdding] = useState(false);
531
536
  const [hiddenColumns, setHiddenColumns] = useHiddenColumns(['updatedAt']);
532
- const { data: loaded, loading, error, reload } = useAsync(() => loadProcesses(searchQuery), [searchQuery]);
537
+ const { data: catalog, loading, error, reload } = useAsync(() => loadProcesses(searchQuery), [searchQuery]);
538
+ const loaded = catalog?.processes;
539
+ // Until the list loads, changes go the app's way: it answers if it cannot
540
+ const editing: ProcessEditing = catalog?.editing ?? 'app';
533
541
  const workflowIdsKey = (loaded ?? []).map(({ workflowId }) => workflowId).join();
534
542
  const { data: runCounts, reload: reloadRunCounts } = useAsync(
535
543
  () => loadRunCounts(workflowIdsKey.length > 0 ? workflowIdsKey.split(',') : []),
@@ -577,26 +585,15 @@ const DocflowProcesses = () => {
577
585
  },
578
586
  });
579
587
 
580
- // The 2.45 process records of the given processes; none on Twenty 2.50,
581
- // which renames and deletes processes only in its editor
582
- const recordIdsOf = (ids: string[]): string[] | undefined => {
583
- const recordIds = ids.map((id) => processes.find(({ workflowId }) => workflowId === id)?.workspaceWorkflowId);
584
-
585
- return recordIds.every((recordId): recordId is string => typeof recordId === 'string') ? recordIds : undefined;
586
- };
588
+ const processesOf = (ids: string[]) =>
589
+ ids.flatMap((id) => processes.filter(({ workflowId }) => workflowId === id));
587
590
 
588
591
  // The processes go to the Twenty trash ("Рабочие процессы → Удалённые"),
589
592
  // their runs and tasks stay.
590
593
  const removeProcesses = (ids: string[]) =>
591
594
  bulk.run(
592
595
  async () => {
593
- const recordIds = recordIdsOf(ids);
594
-
595
- if (recordIds === undefined) {
596
- throw new Error(EDITOR_ONLY_MESSAGE);
597
- }
598
-
599
- await deleteRecords('workflows', recordIds);
596
+ await removeProcessesIn(editing, processesOf(ids));
600
597
  setPanel((current) => (current.kind === 'process' && ids.includes(current.workflowId) ? { kind: 'closed' } : current));
601
598
  },
602
599
  ids.length === 1 ? 'Процесс удалён' : `Удалено процессов: ${ids.length}`,
@@ -614,21 +611,16 @@ const DocflowProcesses = () => {
614
611
  'Не удалось убрать',
615
612
  );
616
613
 
617
- // Publishing goes through the app's route, one process at a time: the
618
- // workflow engine takes a version per call.
614
+ // One process at a time: the workflow engine takes a version per call
619
615
  const publishProcesses = (ids: string[], publish: boolean) =>
620
- bulk.runEach(ids, (workflowId) => callAppRoute('/docflow/processes/publish', { workflowId, publish }), {
616
+ bulk.runEach(ids, (workflowId) => publishProcessIn(editing, processesOf([workflowId])[0] ?? { workflowId, workspaceWorkflowId: null }, publish), {
621
617
  done: (count) => (publish ? `Опубликовано: ${count}` : `Снято с публикации: ${count}`),
622
618
  failed: publish ? 'Не опубликованы' : 'Не сняты с публикации',
623
619
  });
624
620
 
625
621
  const handleRenameProcess = async (process: DocflowProcess, name: string) => {
626
622
  try {
627
- if (process.workspaceWorkflowId === null) {
628
- throw new Error(EDITOR_ONLY_MESSAGE);
629
- }
630
-
631
- await updateRecord('workflows', process.workspaceWorkflowId, { name });
623
+ await renameProcessIn(editing, process, name);
632
624
  await enqueueSnackbar({ message: 'Процесс переименован', variant: 'success' });
633
625
  setRenaming(undefined);
634
626
  await reload();
@@ -638,45 +630,18 @@ const DocflowProcesses = () => {
638
630
  };
639
631
 
640
632
  // As "Создать" of the standard list: a process with a manual trigger in the
641
- // docflow section, opened in the editor. Launch parameters are set later
642
- // with the process's "Параметры запуска" command.
633
+ // docflow section, opened in the editor. Launch parameters are set in the
634
+ // process's panel ("Параметры запуска и задания").
643
635
  const handleCreate = async () => {
644
636
  setIsCreating(true);
645
- logDocflow('process: create');
637
+ logDocflow('process: create', { editing });
646
638
 
647
639
  try {
648
- const workflow = await createRecord<WorkflowRecord>('workflows', {
649
- name: 'Новый процесс',
650
- smbSection: DOCFLOW_SECTION,
651
- });
652
-
653
- if (workflow === undefined) {
654
- throw new Error('процесс создан, но сервер его не вернул');
655
- }
656
-
657
- await waitForDraft(workflow.id);
658
- await callAppRoute('/docflow/processes/launch-fields', { workflowId: workflow.id, fields: [] });
659
- await navigate(AppPath.RecordShowPage, {
660
- objectNameSingular: 'workflow',
661
- objectRecordId: workflow.id,
662
- });
640
+ await createProcess(editing, 'Новый процесс');
641
+ await reload();
663
642
  } catch (createError) {
664
643
  logDocflowError('process: create failed', createError);
665
- // Twenty 2.50 lets no app create a process: it is made in the Twenty
666
- // editor and added here
667
- const message = getErrorMessage(createError, 'Не удалось создать процесс');
668
-
669
- if (/Method not allowed/i.test(message)) {
670
- await enqueueSnackbar({
671
- message: 'В этой версии Twenty процесс создаётся в «Рабочих процессах» Twenty, затем — «Добавить из Twenty»',
672
- variant: 'info',
673
- });
674
- await navigate(AppPath.RecordIndexPage, { objectNamePlural: 'workflows' });
675
-
676
- return;
677
- }
678
-
679
- await enqueueSnackbar({ message, variant: 'error' });
644
+ await enqueueSnackbar({ message: getErrorMessage(createError, 'Не удалось создать процесс'), variant: 'error' });
680
645
  } finally {
681
646
  setIsCreating(false);
682
647
  }
@@ -740,6 +705,7 @@ const DocflowProcesses = () => {
740
705
  onLaunched={() => void reloadRunCounts()}
741
706
  onRename={(name) => handleRenameProcess(selectedProcess, name)}
742
707
  onPublish={(publish) => void publishProcesses([selectedProcess.workflowId], publish)}
708
+ onSettingsSaved={() => void reload()}
743
709
  />
744
710
  ) : undefined
745
711
  }
@@ -786,6 +752,11 @@ const DocflowProcesses = () => {
786
752
  onCreate={() => void handleCreate()}
787
753
  isCreating={isCreating}
788
754
  />
755
+ {editing === 'standalone' && (
756
+ <div style={{ padding: `0 ${theme.spacing[5]} ${theme.spacing[3]}` }}>
757
+ <StandalonePageHint />
758
+ </div>
759
+ )}
789
760
  {error !== undefined && (
790
761
  <div style={{ padding: theme.spacing[4] }}>
791
762
  <Message tone="danger">{getErrorMessage(error, 'Не удалось загрузить процессы')}</Message>