@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
|
@@ -1,59 +1,14 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
1
|
import { defineFrontComponent } from 'twenty-sdk/define';
|
|
3
|
-
import {
|
|
4
|
-
AppPath,
|
|
5
|
-
closeSidePanel,
|
|
6
|
-
enqueueSnackbar,
|
|
7
|
-
navigate,
|
|
8
|
-
useSelectedRecordIds,
|
|
9
|
-
} from 'twenty-sdk/front-component';
|
|
10
|
-
import { Button } from 'src/front/ui/kit';
|
|
2
|
+
import { AppPath, closeSidePanel, navigate, useSelectedRecordIds } from 'twenty-sdk/front-component';
|
|
11
3
|
import { useTheme } from 'twenty-ui/theme';
|
|
12
4
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
CheckboxField,
|
|
19
|
-
DocflowRoot,
|
|
20
|
-
Message,
|
|
21
|
-
SectionTitle,
|
|
22
|
-
SelectField,
|
|
23
|
-
TextField,
|
|
24
|
-
VariableChip,
|
|
25
|
-
} from 'src/front/ui/components';
|
|
26
|
-
import { FormFieldsBuilder } from 'src/front/ui/form-fields-builder';
|
|
27
|
-
import { ListSkeleton } from 'src/front/ui/skeleton';
|
|
28
|
-
import { listLaunchObjects } from 'src/shared/api/object-summaries';
|
|
29
|
-
import { restAnd, restEq } from 'src/shared/api/rest-filter.util';
|
|
30
|
-
import { LAUNCH_RECORD_KEY, LAUNCH_TRIGGER_NAME, TRIGGER_STEP_ID } from 'src/shared/docflow.constants';
|
|
31
|
-
import { type FormField } from 'src/shared/form/form-field.type';
|
|
32
|
-
import { getFillableFormFields, getFormFieldsError, parseFormFields } from 'src/shared/form/form-fields.util';
|
|
33
|
-
import { readStepSettings, type StepSettings } from 'src/shared/form/step-settings.util';
|
|
34
|
-
import { parseLaunchFields, parseLaunchObject } from 'src/shared/workflow/launch-step.util';
|
|
35
|
-
import { type WorkflowStep } from 'src/shared/workflow/workflow-run.type';
|
|
36
|
-
import { getFormStepFields, isFormStep } from 'src/shared/workflow/workflow-run.util';
|
|
37
|
-
import { logDocflow, logDocflowError } from 'src/shared/log.util';
|
|
5
|
+
import { type ProcessEditing } from 'src/front/process-editing';
|
|
6
|
+
import { DocflowRoot } from 'src/front/ui/components';
|
|
7
|
+
import { ProcessSettings } from 'src/front/ui/process-settings';
|
|
8
|
+
import { logDocflowError } from 'src/shared/log.util';
|
|
9
|
+
import { type CatalogProcess } from 'src/shared/workflow/process-catalog.type';
|
|
38
10
|
import { PROCESS_STEPS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/universal-identifiers';
|
|
39
11
|
|
|
40
|
-
const NO_ASSIGNEE = '';
|
|
41
|
-
const NO_LAUNCH_OBJECT = '';
|
|
42
|
-
// Objects of the workflows themselves: a process is not launched on them
|
|
43
|
-
const WORKFLOW_OBJECTS = ['workflow', 'workflowVersion', 'workflowRun', 'workflowAutomatedTrigger'];
|
|
44
|
-
|
|
45
|
-
type DraftVersion = { id: string; steps?: WorkflowStep[] | null };
|
|
46
|
-
type ProcessRecord = { id: string; smbLaunchFields?: unknown; smbLaunchObject?: string | null };
|
|
47
|
-
type LaunchObjectOption = { value: string; label: string };
|
|
48
|
-
|
|
49
|
-
const loadLaunchObjectOptions = async (): Promise<LaunchObjectOption[]> =>
|
|
50
|
-
(await listLaunchObjects())
|
|
51
|
-
.filter(({ nameSingular }) => !WORKFLOW_OBJECTS.includes(nameSingular))
|
|
52
|
-
.map(({ nameSingular, labelSingular }) => ({ value: nameSingular, label: labelSingular }))
|
|
53
|
-
.sort((left, right) => left.label.localeCompare(right.label, 'ru'));
|
|
54
|
-
|
|
55
|
-
const sectionStyle = { display: 'flex', flexDirection: 'column' as const };
|
|
56
|
-
|
|
57
12
|
// The workflow editor reads a version once when it opens and keeps it: it is
|
|
58
13
|
// not told that the server changed the trigger, so the new parameters would
|
|
59
14
|
// show up as variables only after a reload. Opening the process again (out
|
|
@@ -73,281 +28,25 @@ const reopenWorkflowEditor = async (workflowId: string) => {
|
|
|
73
28
|
}
|
|
74
29
|
};
|
|
75
30
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
initialFields,
|
|
79
|
-
initialLaunchObject,
|
|
80
|
-
onSaved,
|
|
81
|
-
}: {
|
|
82
|
-
workflowId: string;
|
|
83
|
-
initialFields: FormField[];
|
|
84
|
-
initialLaunchObject: string | undefined;
|
|
85
|
-
onSaved: () => void;
|
|
86
|
-
}) => {
|
|
87
|
-
const theme = useTheme();
|
|
88
|
-
const [fields, setFields] = useState(initialFields);
|
|
89
|
-
const [launchObject, setLaunchObject] = useState(initialLaunchObject ?? NO_LAUNCH_OBJECT);
|
|
90
|
-
const { data: objectOptions, error: objectsError } = useAsync(loadLaunchObjectOptions, []);
|
|
91
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
92
|
-
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
|
93
|
-
|
|
94
|
-
const save = async () => {
|
|
95
|
-
const fieldsError = getFormFieldsError(fields);
|
|
96
|
-
|
|
97
|
-
if (fieldsError !== undefined) {
|
|
98
|
-
setErrorMessage(fieldsError);
|
|
99
|
-
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
setIsSaving(true);
|
|
104
|
-
setErrorMessage(undefined);
|
|
105
|
-
logDocflow('process: save launch fields', { workflowId, fields, launchObject });
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const result = await callAppRoute('/docflow/processes/launch-fields', {
|
|
109
|
-
workflowId,
|
|
110
|
-
fields,
|
|
111
|
-
launchObject: launchObject === NO_LAUNCH_OBJECT ? null : launchObject,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
logDocflow('process: launch fields saved', result);
|
|
115
|
-
await enqueueSnackbar({
|
|
116
|
-
message:
|
|
117
|
-
fields.length > 0 || launchObject !== NO_LAUNCH_OBJECT
|
|
118
|
-
? `Параметры сохранены: процесс начинается с «${LAUNCH_TRIGGER_NAME}», опубликуйте его`
|
|
119
|
-
: 'Параметры убраны',
|
|
120
|
-
variant: 'success',
|
|
121
|
-
});
|
|
122
|
-
onSaved();
|
|
123
|
-
await reopenWorkflowEditor(workflowId);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
logDocflowError('process: save launch fields failed', error, { workflowId });
|
|
126
|
-
setErrorMessage(getErrorMessage(error, 'Не удалось сохранить параметры'));
|
|
127
|
-
} finally {
|
|
128
|
-
setIsSaving(false);
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<section style={{ ...sectionStyle, gap: theme.spacing[2] }} aria-label="Параметры запуска">
|
|
134
|
-
<SectionTitle>Параметры запуска</SectionTitle>
|
|
135
|
-
<Message tone="hint">
|
|
136
|
-
Что человек заполняет перед запуском и на какой записи запускает процесс. Первым блоком процесса
|
|
137
|
-
становится «{LAUNCH_TRIGGER_NAME}»: в следующих шагах значения и запись выбираются как его
|
|
138
|
-
переменные, например сотрудник — в поле «Исполнитель». Запускают процесс командой «Запустить
|
|
139
|
-
процесс» в меню (Cmd+K) или на выбранных записях. Без параметров и записи процесс начинается с
|
|
140
|
-
обычного «Запуск вручную».
|
|
141
|
-
</Message>
|
|
142
|
-
<SelectField
|
|
143
|
-
label="Где запускать"
|
|
144
|
-
value={launchObject}
|
|
145
|
-
disabled={isSaving}
|
|
146
|
-
options={[
|
|
147
|
-
{ value: NO_LAUNCH_OBJECT, label: 'Откуда угодно, без записи' },
|
|
148
|
-
// The saved object stays offered while the list loads
|
|
149
|
-
...(objectOptions ??
|
|
150
|
-
(launchObject !== NO_LAUNCH_OBJECT ? [{ value: launchObject, label: launchObject }] : [])),
|
|
151
|
-
].map((option) =>
|
|
152
|
-
option.value === NO_LAUNCH_OBJECT ? option : { ...option, label: `На записи: ${option.label}` },
|
|
153
|
-
)}
|
|
154
|
-
onChange={setLaunchObject}
|
|
155
|
-
/>
|
|
156
|
-
{objectsError !== undefined && (
|
|
157
|
-
<Message tone="danger">{getErrorMessage(objectsError, 'Не удалось прочитать объекты')}</Message>
|
|
158
|
-
)}
|
|
159
|
-
{launchObject !== NO_LAUNCH_OBJECT && (
|
|
160
|
-
<div style={{ alignItems: 'center', display: 'flex', flexWrap: 'wrap', gap: theme.spacing[2] }}>
|
|
161
|
-
<Message tone="hint">Запись, на которой запустили процесс:</Message>
|
|
162
|
-
<VariableChip>{`{{${TRIGGER_STEP_ID}.${LAUNCH_RECORD_KEY}}}`}</VariableChip>
|
|
163
|
-
</div>
|
|
164
|
-
)}
|
|
165
|
-
<FormFieldsBuilder
|
|
166
|
-
fields={fields}
|
|
167
|
-
onChange={setFields}
|
|
168
|
-
variablePrefix={TRIGGER_STEP_ID}
|
|
169
|
-
disabled={isSaving}
|
|
170
|
-
/>
|
|
171
|
-
{errorMessage !== undefined && <Message tone="danger">{errorMessage}</Message>}
|
|
172
|
-
<div>
|
|
173
|
-
<Button variant="solid" tone="accent" size="sm" loading={isSaving} onClick={() => void save()}>
|
|
174
|
-
Сохранить параметры
|
|
175
|
-
</Button>
|
|
176
|
-
</div>
|
|
177
|
-
</section>
|
|
178
|
-
);
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const StepSettingsEditor = ({
|
|
182
|
-
workflowVersionId,
|
|
183
|
-
step,
|
|
184
|
-
}: {
|
|
185
|
-
workflowVersionId: string;
|
|
186
|
-
step: WorkflowStep;
|
|
187
|
-
}) => {
|
|
188
|
-
const theme = useTheme();
|
|
189
|
-
const { members } = useWorkspaceMembers();
|
|
190
|
-
const fields = parseFormFields(getFormStepFields(step));
|
|
191
|
-
const [settings, setSettings] = useState<StepSettings>(readStepSettings(fields));
|
|
192
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
193
|
-
|
|
194
|
-
const save = async () => {
|
|
195
|
-
setIsSaving(true);
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
await callAppRoute('/docflow/processes/step-settings', {
|
|
199
|
-
workflowVersionId,
|
|
200
|
-
stepId: step.id,
|
|
201
|
-
settings,
|
|
202
|
-
});
|
|
203
|
-
await enqueueSnackbar({ message: 'Настройки шага сохранены', variant: 'success' });
|
|
204
|
-
} catch (error) {
|
|
205
|
-
await enqueueSnackbar({
|
|
206
|
-
message: getErrorMessage(error, 'Не удалось сохранить настройки шага'),
|
|
207
|
-
variant: 'error',
|
|
208
|
-
});
|
|
209
|
-
} finally {
|
|
210
|
-
setIsSaving(false);
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const toggleRequired = (fieldId: string, isRequired: boolean) =>
|
|
215
|
-
setSettings((current) => ({
|
|
216
|
-
...current,
|
|
217
|
-
requiredFieldIds: isRequired
|
|
218
|
-
? [...current.requiredFieldIds, fieldId]
|
|
219
|
-
: current.requiredFieldIds.filter((id) => id !== fieldId),
|
|
220
|
-
}));
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<div
|
|
224
|
-
style={{
|
|
225
|
-
background: theme.background.transparent.lighter,
|
|
226
|
-
borderRadius: theme.border.radius.md,
|
|
227
|
-
display: 'flex',
|
|
228
|
-
flexDirection: 'column',
|
|
229
|
-
gap: theme.spacing[2],
|
|
230
|
-
padding: theme.spacing[3],
|
|
231
|
-
}}
|
|
232
|
-
>
|
|
233
|
-
<strong>{step.name}</strong>
|
|
234
|
-
<div style={{ display: 'flex', flexWrap: 'wrap', gap: theme.spacing[2] }}>
|
|
235
|
-
<div style={{ flex: '2 1 200px' }}>
|
|
236
|
-
<SelectField
|
|
237
|
-
label="Кто выполняет задание"
|
|
238
|
-
value={settings.assignee ?? NO_ASSIGNEE}
|
|
239
|
-
options={[
|
|
240
|
-
{ value: NO_ASSIGNEE, label: 'Запустивший процесс' },
|
|
241
|
-
...members.map((member) => ({ value: member.id, label: formatMemberName(member) || member.id })),
|
|
242
|
-
]}
|
|
243
|
-
onChange={(assignee) =>
|
|
244
|
-
setSettings((current) => ({ ...current, assignee: assignee === NO_ASSIGNEE ? null : assignee }))
|
|
245
|
-
}
|
|
246
|
-
/>
|
|
247
|
-
</div>
|
|
248
|
-
<div style={{ flex: '1 1 120px' }}>
|
|
249
|
-
<TextField
|
|
250
|
-
label="Срок в днях"
|
|
251
|
-
type="number"
|
|
252
|
-
value={settings.dueInDays === null ? '' : String(settings.dueInDays)}
|
|
253
|
-
onChange={(value) =>
|
|
254
|
-
setSettings((current) => ({ ...current, dueInDays: value === '' ? null : Number(value) }))
|
|
255
|
-
}
|
|
256
|
-
/>
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
|
-
{getFillableFormFields(fields).map((field) => (
|
|
260
|
-
<div key={field.id} style={{ alignItems: 'center', display: 'flex', flexWrap: 'wrap', gap: theme.spacing[2] }}>
|
|
261
|
-
<CheckboxField
|
|
262
|
-
label={`${field.label} обязательное`}
|
|
263
|
-
checked={settings.requiredFieldIds.includes(field.id)}
|
|
264
|
-
onChange={(isRequired) => toggleRequired(field.id, isRequired)}
|
|
265
|
-
/>
|
|
266
|
-
<VariableChip>{`{{${step.id}.${field.name}}}`}</VariableChip>
|
|
267
|
-
</div>
|
|
268
|
-
))}
|
|
269
|
-
<div>
|
|
270
|
-
<Button variant="outline" size="sm" loading={isSaving} onClick={() => void save()}>
|
|
271
|
-
Сохранить шаг
|
|
272
|
-
</Button>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
);
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
// Tab of a workflow's record page: the parameters asked before the launch and
|
|
279
|
-
// who fills each "Human input" step of the draft, by when, which fields are
|
|
280
|
-
// required. The standard editor has no place for either.
|
|
31
|
+
// Side panel of a workflow's page ("Параметры запуска"): the launch
|
|
32
|
+
// parameters and the tasks of the steps of this process.
|
|
281
33
|
const ProcessSteps = () => {
|
|
282
34
|
const theme = useTheme();
|
|
283
35
|
const [workflowId] = useSelectedRecordIds();
|
|
284
|
-
const [launchEditorKey, setLaunchEditorKey] = useState(0);
|
|
285
|
-
const { data, error, loading, reload } = useAsync(async () => {
|
|
286
|
-
if (workflowId === undefined) {
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const [process, drafts] = await Promise.all([
|
|
291
|
-
getRecord<ProcessRecord>('workflows', workflowId),
|
|
292
|
-
listRecords<DraftVersion>('workflowVersions', {
|
|
293
|
-
filter: restAnd(restEq('workflowId', workflowId), restEq('status', 'DRAFT')),
|
|
294
|
-
limit: 1,
|
|
295
|
-
}),
|
|
296
|
-
]);
|
|
297
|
-
|
|
298
|
-
return { process, draft: drafts.records[0] };
|
|
299
|
-
}, [workflowId]);
|
|
300
|
-
const draft = data?.draft;
|
|
301
|
-
const formSteps = draft?.steps?.filter(isFormStep) ?? [];
|
|
302
|
-
|
|
303
|
-
// The steps are edited in the diagram next to this tab, so what was loaded
|
|
304
|
-
// goes stale; read the draft again whenever the tab gets the focus back.
|
|
305
|
-
useEffect(() => {
|
|
306
|
-
const handleVisible = () => {
|
|
307
|
-
if (document.visibilityState === 'visible') {
|
|
308
|
-
void reload();
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
window.addEventListener('focus', handleVisible);
|
|
313
|
-
document.addEventListener('visibilitychange', handleVisible);
|
|
314
|
-
|
|
315
|
-
return () => {
|
|
316
|
-
window.removeEventListener('focus', handleVisible);
|
|
317
|
-
document.removeEventListener('visibilitychange', handleVisible);
|
|
318
|
-
};
|
|
319
|
-
}, [reload]);
|
|
320
36
|
|
|
321
37
|
return (
|
|
322
38
|
<DocflowRoot>
|
|
323
|
-
<div style={{
|
|
324
|
-
{
|
|
325
|
-
|
|
326
|
-
<Message tone="danger">{getErrorMessage(error, 'Не удалось прочитать процесс')}</Message>
|
|
327
|
-
)}
|
|
328
|
-
{workflowId !== undefined && data?.process !== undefined && (
|
|
329
|
-
<LaunchParametersEditor
|
|
330
|
-
key={`${workflowId}:${launchEditorKey}`}
|
|
39
|
+
<div style={{ padding: theme.spacing[3] }}>
|
|
40
|
+
{workflowId !== undefined && (
|
|
41
|
+
<ProcessSettings
|
|
331
42
|
workflowId={workflowId}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
43
|
+
onLaunchSaved={(process: CatalogProcess, editing: ProcessEditing) => {
|
|
44
|
+
if (editing === 'app' && process.workspaceWorkflowId !== null) {
|
|
45
|
+
void reopenWorkflowEditor(process.workspaceWorkflowId);
|
|
46
|
+
}
|
|
336
47
|
}}
|
|
337
48
|
/>
|
|
338
49
|
)}
|
|
339
|
-
{draft !== undefined && formSteps.length > 0 && (
|
|
340
|
-
<section style={{ ...sectionStyle, gap: theme.spacing[2] }} aria-label="Задания шагов">
|
|
341
|
-
<SectionTitle>Задания шагов «Человеческий ввод»</SectionTitle>
|
|
342
|
-
<Message tone="hint">
|
|
343
|
-
Каждый такой шаг становится заданием. Поля шага задаются в редакторе процесса; заполненные
|
|
344
|
-
значения следующие шаги читают переменными, показанными рядом с полем.
|
|
345
|
-
</Message>
|
|
346
|
-
{formSteps.map((step) => (
|
|
347
|
-
<StepSettingsEditor key={`${draft.id}:${step.id}`} workflowVersionId={draft.id} step={step} />
|
|
348
|
-
))}
|
|
349
|
-
</section>
|
|
350
|
-
)}
|
|
351
50
|
</div>
|
|
352
51
|
</DocflowRoot>
|
|
353
52
|
);
|