@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.
- package/manifest.json +15 -15
- package/package.json +1 -1
- package/public/app/docflow-app.js +21 -21
- package/src/front-components/docflow-documents.front-component.mjs +1 -1
- package/src/front-components/docflow-processes.front-component.mjs +41 -41
- package/src/front-components/docflow-processes.front-component.tsx +60 -90
- package/src/front-components/docflow-tasks.front-component.mjs +1 -1
- 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 +1 -1
- 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,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[] }>(
|
|
143
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
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:
|
|
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
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
643
|
-
//
|
|
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
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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>
|