@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.
@@ -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
- const DocflowProcesses = () => {
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 [searchInput, setSearchInput] = useState('');
159
- const searchQuery = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
160
- const [selectedVersionId, setSelectedVersionId] = useState<string | undefined>();
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 { data: processes, loading, error } = useAsync(() => loadProcesses(searchQuery), [searchQuery]);
163
- const selectedProcess = processes?.find((process) => process.workflowVersionId === selectedVersionId);
187
+ const [errorMessage, setErrorMessage] = useState<string | undefined>();
164
188
 
165
- // Created in the docflow section, then opened in the standard editor.
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 !== undefined) {
176
- await navigate(AppPath.RecordShowPage, {
177
- objectNameSingular: 'workflow',
178
- objectRecordId: workflow.id,
179
- });
218
+ if (workflow === undefined) {
219
+ throw new Error('процесс создан, но сервер его не вернул');
180
220
  }
181
- } catch (createError) {
182
- await enqueueSnackbar({
183
- message: getErrorMessage(createError, 'Не удалось создать процесс'),
184
- variant: 'error',
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={() => setSelectedVersionId(undefined)}
288
+ panelTitle={panel.kind === 'new' ? 'Новый процесс' : selectedProcess?.name}
289
+ onClosePanel={() => setPanel({ kind: 'closed' })}
196
290
  panel={
197
- selectedProcess !== undefined ? (
291
+ panel.kind === 'new' ? (
292
+ <NewProcessForm />
293
+ ) : selectedProcess !== undefined ? (
198
294
  <ProcessLaunchForm
199
295
  key={selectedProcess.workflowVersionId}
200
296
  process={selectedProcess}
201
- onLaunched={() => setSelectedVersionId(undefined)}
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="outline" size="sm" loading={isCreating} onClick={() => void handleCreate()}>
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={setSelectedVersionId}
337
+ onRowClick={(workflowVersionId) => setPanel({ kind: 'process', workflowVersionId })}
242
338
  rows={processes.map((process) => {
243
339
  const parameterCount = process.launchFields.length;
244
340