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