@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.
- package/README.md +10 -0
- package/manifest.json +16 -16
- package/package.json +1 -1
- package/public/app/docflow-app.js +21 -21
- package/src/front-components/docflow-documents.front-component.mjs +20 -20
- package/src/front-components/docflow-processes.front-component.mjs +41 -41
- package/src/front-components/docflow-processes.front-component.tsx +64 -93
- package/src/front-components/docflow-tasks.front-component.mjs +22 -22
- package/src/front-components/launch-process.front-component.mjs +18 -18
- package/src/front-components/process-steps.front-component.mjs +25 -25
- package/src/front-components/process-steps.front-component.tsx +15 -316
- package/src/front-components/task-form.front-component.mjs +22 -22
- package/src/logic-functions/bitrix-app.logic-function.mjs +1 -1
- package/src/logic-functions/docflow-app.logic-function.mjs +1 -1
- package/src/logic-functions/launch-process.logic-function.mjs +21 -6
- package/src/logic-functions/process-catalog.logic-function.mjs +42 -8
- package/src/logic-functions/process-catalog.logic-function.ts +8 -3
- package/src/logic-functions/publish-process.logic-function.mjs +21 -6
- package/src/logic-functions/save-launch-fields.logic-function.mjs +528 -266
- package/src/logic-functions/save-launch-fields.logic-function.ts +40 -12
- package/src/logic-functions/save-step-settings.logic-function.mjs +123 -47
- package/src/logic-functions/save-step-settings.logic-function.ts +11 -28
|
@@ -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[] }>(
|
|
142
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
642
|
-
//
|
|
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
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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>
|