@haiiro/docuflow 0.0.5 → 0.0.6
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 -10
- package/manifest.json +25 -31
- package/package.json +1 -1
- package/src/front-components/docflow-documents.front-component.mjs +35 -35
- package/src/front-components/docflow-documents.front-component.tsx +90 -300
- package/src/front-components/docflow-processes.front-component.mjs +27 -27
- package/src/front-components/docflow-processes.front-component.tsx +122 -26
- package/src/front-components/docflow-tasks.front-component.mjs +19 -19
- package/src/front-components/process-steps.front-component.mjs +18 -18
- package/src/front-components/task-form.front-component.mjs +26 -26
- package/src/logic-functions/capture-file.logic-function.mjs +15 -1
|
@@ -8,10 +8,12 @@ import { callAppRoute, createRecord, getErrorMessage, listRecords } from 'src/fr
|
|
|
8
8
|
import { useAsync, useDebouncedValue } from 'src/front/hooks/use-async';
|
|
9
9
|
import { DataTable, DocflowRoot, Message, SplitView, TextField, Toolbar } from 'src/front/ui/components';
|
|
10
10
|
import { FormFieldInputs } from 'src/front/ui/form-field-inputs';
|
|
11
|
+
import { FormFieldsBuilder } from 'src/front/ui/form-fields-builder';
|
|
11
12
|
import { restAnd, restEq, restIlike, restIn } from 'src/shared/api/rest-filter.util';
|
|
12
|
-
import { DOCFLOW_SECTION } from 'src/shared/docflow.constants';
|
|
13
|
+
import { DOCFLOW_SECTION, LAUNCH_PARAMETERS_STEP_NAME } from 'src/shared/docflow.constants';
|
|
13
14
|
import { type FormField, type FormValues } from 'src/shared/form/form-field.type';
|
|
14
|
-
import { findMissingRequiredFields } from 'src/shared/form/form-fields.util';
|
|
15
|
+
import { findMissingRequiredFields, getFormFieldsError } from 'src/shared/form/form-fields.util';
|
|
16
|
+
import { logDocflow, logDocflowError } from 'src/shared/log.util';
|
|
15
17
|
import {
|
|
16
18
|
buildLaunchParameters,
|
|
17
19
|
isLaunchParametersStep,
|
|
@@ -22,6 +24,12 @@ import { PROCESSES_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/universal-id
|
|
|
22
24
|
|
|
23
25
|
const PROCESSES_LIMIT = 200;
|
|
24
26
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
27
|
+
const DRAFT_WAIT_ATTEMPTS = 10;
|
|
28
|
+
const DRAFT_WAIT_MS = 400;
|
|
29
|
+
|
|
30
|
+
type ProcessKind = 'plain' | 'parameters';
|
|
31
|
+
|
|
32
|
+
type Panel = { kind: 'closed' } | { kind: 'process'; workflowVersionId: string } | { kind: 'new' };
|
|
25
33
|
|
|
26
34
|
type WorkflowRecord = { id: string; name?: string | null; smbLaunchFields?: unknown };
|
|
27
35
|
type WorkflowVersionRecord = {
|
|
@@ -153,52 +161,140 @@ const ProcessLaunchForm = ({
|
|
|
153
161
|
);
|
|
154
162
|
};
|
|
155
163
|
|
|
156
|
-
|
|
164
|
+
// The server gives a new workflow its first draft version right after the
|
|
165
|
+
// record; the launch step can only be put into an existing draft.
|
|
166
|
+
const waitForDraft = async (workflowId: string) => {
|
|
167
|
+
for (let attempt = 0; attempt < DRAFT_WAIT_ATTEMPTS; attempt++) {
|
|
168
|
+
const { records } = await listRecords<{ id: string }>('workflowVersions', {
|
|
169
|
+
filter: restEq('workflowId', workflowId),
|
|
170
|
+
limit: 1,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (records.length > 0) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await new Promise((resolve) => setTimeout(resolve, DRAFT_WAIT_MS));
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const NewProcessForm = () => {
|
|
157
182
|
const theme = useTheme();
|
|
158
|
-
const [
|
|
159
|
-
const
|
|
160
|
-
const [
|
|
183
|
+
const [name, setName] = useState('');
|
|
184
|
+
const [kind, setKind] = useState<ProcessKind>('parameters');
|
|
185
|
+
const [fields, setFields] = useState<FormField[]>([]);
|
|
161
186
|
const [isCreating, setIsCreating] = useState(false);
|
|
162
|
-
const
|
|
163
|
-
const selectedProcess = processes?.find((process) => process.workflowVersionId === selectedVersionId);
|
|
187
|
+
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
|
164
188
|
|
|
165
|
-
// Created in the docflow section
|
|
189
|
+
// Created in the docflow section with a manual trigger; "with parameters"
|
|
190
|
+
// also gets the "Launch with parameters" step right after the trigger, its
|
|
191
|
+
// fields being variables for every later step. Then the standard editor.
|
|
166
192
|
const handleCreate = async () => {
|
|
193
|
+
const launchFields = kind === 'parameters' ? fields : [];
|
|
194
|
+
const fieldsError = getFormFieldsError(launchFields);
|
|
195
|
+
|
|
196
|
+
if (kind === 'parameters' && launchFields.length === 0) {
|
|
197
|
+
setErrorMessage('Добавьте хотя бы одно поле или выберите обычный процесс');
|
|
198
|
+
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (fieldsError !== undefined) {
|
|
203
|
+
setErrorMessage(fieldsError);
|
|
204
|
+
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
167
208
|
setIsCreating(true);
|
|
209
|
+
setErrorMessage(undefined);
|
|
210
|
+
logDocflow('process: create', { kind, fieldCount: launchFields.length });
|
|
168
211
|
|
|
169
212
|
try {
|
|
170
213
|
const workflow = await createRecord<WorkflowRecord>('workflows', {
|
|
171
|
-
name: 'Новый процесс',
|
|
214
|
+
name: name.trim() || 'Новый процесс',
|
|
172
215
|
smbSection: DOCFLOW_SECTION,
|
|
173
216
|
});
|
|
174
217
|
|
|
175
|
-
if (workflow
|
|
176
|
-
|
|
177
|
-
objectNameSingular: 'workflow',
|
|
178
|
-
objectRecordId: workflow.id,
|
|
179
|
-
});
|
|
218
|
+
if (workflow === undefined) {
|
|
219
|
+
throw new Error('процесс создан, но сервер его не вернул');
|
|
180
220
|
}
|
|
181
|
-
|
|
182
|
-
await
|
|
183
|
-
|
|
184
|
-
|
|
221
|
+
|
|
222
|
+
await waitForDraft(workflow.id);
|
|
223
|
+
await callAppRoute('/docflow/processes/launch-fields', {
|
|
224
|
+
workflowId: workflow.id,
|
|
225
|
+
fields: launchFields,
|
|
226
|
+
});
|
|
227
|
+
await navigate(AppPath.RecordShowPage, {
|
|
228
|
+
objectNameSingular: 'workflow',
|
|
229
|
+
objectRecordId: workflow.id,
|
|
185
230
|
});
|
|
231
|
+
} catch (createError) {
|
|
232
|
+
logDocflowError('process: create failed', createError);
|
|
233
|
+
setErrorMessage(getErrorMessage(createError, 'Не удалось создать процесс'));
|
|
186
234
|
} finally {
|
|
187
235
|
setIsCreating(false);
|
|
188
236
|
}
|
|
189
237
|
};
|
|
190
238
|
|
|
239
|
+
const kindButton = (value: ProcessKind, label: string) => (
|
|
240
|
+
<Button
|
|
241
|
+
variant={kind === value ? 'solid' : 'outline'}
|
|
242
|
+
size="sm"
|
|
243
|
+
disabled={isCreating}
|
|
244
|
+
onClick={() => setKind(value)}
|
|
245
|
+
>
|
|
246
|
+
{label}
|
|
247
|
+
</Button>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[3] }}>
|
|
252
|
+
<TextField label="Название" placeholder="Новый процесс" value={name} onChange={setName} />
|
|
253
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: theme.spacing[2] }}>
|
|
254
|
+
{kindButton('parameters', LAUNCH_PARAMETERS_STEP_NAME)}
|
|
255
|
+
{kindButton('plain', 'Обычный процесс')}
|
|
256
|
+
</div>
|
|
257
|
+
{kind === 'parameters' ? (
|
|
258
|
+
<>
|
|
259
|
+
<Message tone="hint">
|
|
260
|
+
Поля, которые человек заполняет перед запуском. В процессе появится шаг «
|
|
261
|
+
{LAUNCH_PARAMETERS_STEP_NAME}», и следующие шаги берут значения из его переменных.
|
|
262
|
+
</Message>
|
|
263
|
+
<FormFieldsBuilder fields={fields} onChange={setFields} disabled={isCreating} />
|
|
264
|
+
</>
|
|
265
|
+
) : (
|
|
266
|
+
<Message tone="hint">Ручной запуск без параметров. Шаги добавляются в редакторе.</Message>
|
|
267
|
+
)}
|
|
268
|
+
{errorMessage !== undefined && <Message tone="danger">{errorMessage}</Message>}
|
|
269
|
+
<Button variant="solid" size="md" fullWidth loading={isCreating} onClick={() => void handleCreate()}>
|
|
270
|
+
Создать и открыть редактор
|
|
271
|
+
</Button>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const DocflowProcesses = () => {
|
|
277
|
+
const theme = useTheme();
|
|
278
|
+
const [searchInput, setSearchInput] = useState('');
|
|
279
|
+
const searchQuery = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
|
|
280
|
+
const [panel, setPanel] = useState<Panel>({ kind: 'closed' });
|
|
281
|
+
const { data: processes, loading, error } = useAsync(() => loadProcesses(searchQuery), [searchQuery]);
|
|
282
|
+
const selectedVersionId = panel.kind === 'process' ? panel.workflowVersionId : undefined;
|
|
283
|
+
const selectedProcess = processes?.find((process) => process.workflowVersionId === selectedVersionId);
|
|
284
|
+
|
|
191
285
|
return (
|
|
192
286
|
<DocflowRoot>
|
|
193
287
|
<SplitView
|
|
194
|
-
panelTitle={selectedProcess?.name}
|
|
195
|
-
onClosePanel={() =>
|
|
288
|
+
panelTitle={panel.kind === 'new' ? 'Новый процесс' : selectedProcess?.name}
|
|
289
|
+
onClosePanel={() => setPanel({ kind: 'closed' })}
|
|
196
290
|
panel={
|
|
197
|
-
|
|
291
|
+
panel.kind === 'new' ? (
|
|
292
|
+
<NewProcessForm />
|
|
293
|
+
) : selectedProcess !== undefined ? (
|
|
198
294
|
<ProcessLaunchForm
|
|
199
295
|
key={selectedProcess.workflowVersionId}
|
|
200
296
|
process={selectedProcess}
|
|
201
|
-
onLaunched={() =>
|
|
297
|
+
onLaunched={() => setPanel({ kind: 'closed' })}
|
|
202
298
|
/>
|
|
203
299
|
) : undefined
|
|
204
300
|
}
|
|
@@ -214,7 +310,7 @@ const DocflowProcesses = () => {
|
|
|
214
310
|
/>
|
|
215
311
|
</div>
|
|
216
312
|
<div style={{ marginLeft: 'auto' }}>
|
|
217
|
-
<Button variant="
|
|
313
|
+
<Button variant="solid" size="sm" onClick={() => setPanel({ kind: 'new' })}>
|
|
218
314
|
Создать
|
|
219
315
|
</Button>
|
|
220
316
|
</div>
|
|
@@ -227,7 +323,7 @@ const DocflowProcesses = () => {
|
|
|
227
323
|
{loading && processes === undefined && <Message>Загрузка…</Message>}
|
|
228
324
|
{!loading && error === undefined && processes?.length === 0 && (
|
|
229
325
|
<Message>
|
|
230
|
-
Процессов для запуска нет. Здесь видны опубликованные процессы с ручным запуском и разделом «Документооборот». Параметры запуска задаются
|
|
326
|
+
Процессов для запуска нет. Здесь видны опубликованные процессы с ручным запуском и разделом «Документооборот». Параметры запуска задаются командой процесса «Запуск и задания» (кнопка вверху карточки процесса).
|
|
231
327
|
</Message>
|
|
232
328
|
)}
|
|
233
329
|
{processes !== undefined && processes.length > 0 && (
|
|
@@ -238,7 +334,7 @@ const DocflowProcesses = () => {
|
|
|
238
334
|
{ key: 'stages', label: 'Этапы' },
|
|
239
335
|
]}
|
|
240
336
|
selectedRowId={selectedVersionId}
|
|
241
|
-
onRowClick={
|
|
337
|
+
onRowClick={(workflowVersionId) => setPanel({ kind: 'process', workflowVersionId })}
|
|
242
338
|
rows={processes.map((process) => {
|
|
243
339
|
const parameterCount = process.launchFields.length;
|
|
244
340
|
|