@haiiro/docuflow 0.0.1 → 0.0.3

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.
Files changed (136) hide show
  1. package/{.twenty/output/manifest.json → manifest.json} +9 -9
  2. package/package.json +15 -3
  3. package/{.twenty/output/src → src}/front-components/docflow-documents.front-component.mjs +24 -24
  4. package/src/front-components/docflow-documents.front-component.tsx +21 -6
  5. package/{.twenty/output/src → src}/front-components/docflow-processes.front-component.mjs +18 -18
  6. package/{.twenty/output/src → src}/front-components/docflow-tasks.front-component.mjs +18 -18
  7. package/{.twenty/output/src → src}/front-components/process-steps.front-component.mjs +19 -19
  8. package/src/front-components/process-steps.front-component.tsx +6 -1
  9. package/{.twenty/output/src → src}/front-components/task-form.front-component.mjs +24 -24
  10. package/{.twenty/output/src → src}/logic-functions/launch-process.logic-function.mjs +14 -0
  11. package/src/logic-functions/launch-process.logic-function.ts +5 -0
  12. package/{.twenty/output/src → src}/logic-functions/save-launch-fields.logic-function.mjs +14 -0
  13. package/src/logic-functions/save-launch-fields.logic-function.ts +5 -0
  14. package/.gitignore +0 -43
  15. package/.nvmrc +0 -1
  16. package/.oxlintrc.json +0 -37
  17. package/.twenty/output/README.md +0 -128
  18. package/.twenty/output/package.json +0 -49
  19. package/.twenty/output/public/cover.svg +0 -15
  20. package/.twenty/output/public/logo.svg +0 -9
  21. package/.twenty/output/src/front-components/docflow-documents.front-component.tsx +0 -516
  22. package/.twenty/output/src/front-components/docflow-processes.front-component.tsx +0 -266
  23. package/.twenty/output/src/front-components/docflow-tasks.front-component.tsx +0 -303
  24. package/.twenty/output/src/front-components/process-steps.front-component.tsx +0 -286
  25. package/.twenty/output/src/front-components/task-form.front-component.tsx +0 -29
  26. package/.twenty/output/src/logic-functions/bitrix-app.logic-function.ts +0 -143
  27. package/.twenty/output/src/logic-functions/bitrix-event.logic-function.ts +0 -54
  28. package/.twenty/output/src/logic-functions/bitrix-install.logic-function.ts +0 -81
  29. package/.twenty/output/src/logic-functions/capture-file.logic-function.ts +0 -32
  30. package/.twenty/output/src/logic-functions/complete-task.logic-function.ts +0 -46
  31. package/.twenty/output/src/logic-functions/hand-out-form-step.logic-function.ts +0 -30
  32. package/.twenty/output/src/logic-functions/health-check.ts +0 -31
  33. package/.twenty/output/src/logic-functions/launch-parameters-step.logic-function.ts +0 -33
  34. package/.twenty/output/src/logic-functions/launch-process.logic-function.ts +0 -96
  35. package/.twenty/output/src/logic-functions/notify-bitrix-assignee.logic-function.ts +0 -85
  36. package/.twenty/output/src/logic-functions/on-attachment-created.logic-function.ts +0 -67
  37. package/.twenty/output/src/logic-functions/on-task-updated.logic-function.ts +0 -71
  38. package/.twenty/output/src/logic-functions/on-workflow-run-updated.logic-function.ts +0 -70
  39. package/.twenty/output/src/logic-functions/save-launch-fields.logic-function.ts +0 -73
  40. package/.twenty/output/src/logic-functions/save-step-settings.logic-function.ts +0 -90
  41. package/.twenty/output/src/logic-functions/save-task-form.logic-function.ts +0 -62
  42. package/.twenty/output/yarn.lock +0 -3292
  43. package/.yarn/install-state.gz +0 -0
  44. package/.yarnrc.yml +0 -1
  45. package/dist/tsconfig.spec.tsbuildinfo +0 -1
  46. package/src/__tests__/api-and-bitrix.test.ts +0 -89
  47. package/src/__tests__/form-fields.test.ts +0 -73
  48. package/src/__tests__/launch-parameters.test.ts +0 -75
  49. package/src/__tests__/step-settings.test.ts +0 -39
  50. package/src/__tests__/workflow-run.test.ts +0 -113
  51. package/src/application-config.ts +0 -58
  52. package/src/fields/company-documents.field.ts +0 -29
  53. package/src/fields/document-author.field.ts +0 -32
  54. package/src/fields/document-counterparty.field.ts +0 -32
  55. package/src/fields/document-folder-child-folders.field.ts +0 -23
  56. package/src/fields/document-folder-documents.field.ts +0 -24
  57. package/src/fields/document-folder-parent-folder.field.ts +0 -31
  58. package/src/fields/document-folder.field.ts +0 -32
  59. package/src/fields/document-responsible.field.ts +0 -32
  60. package/src/fields/member-authored-documents.field.ts +0 -29
  61. package/src/fields/member-responsible-documents.field.ts +0 -29
  62. package/src/fields/task-form-schema.field.ts +0 -21
  63. package/src/fields/task-form-values.field.ts +0 -21
  64. package/src/fields/task-instruction.field.ts +0 -21
  65. package/src/fields/task-section.field.ts +0 -36
  66. package/src/fields/task-workflow-run-id.field.ts +0 -21
  67. package/src/fields/task-workflow-step-id.field.ts +0 -21
  68. package/src/fields/task-workflow-thread-id.field.ts +0 -21
  69. package/src/fields/workflow-launch-fields.field.ts +0 -21
  70. package/src/fields/workflow-section.field.ts +0 -36
  71. package/src/front/api.ts +0 -56
  72. package/src/front/format.ts +0 -60
  73. package/src/front/hooks/use-async.ts +0 -54
  74. package/src/front/hooks/use-workspace-members.ts +0 -22
  75. package/src/front/types.ts +0 -48
  76. package/src/front/ui/components.tsx +0 -436
  77. package/src/front/ui/form-field-inputs.tsx +0 -187
  78. package/src/front/ui/form-fields-builder.tsx +0 -183
  79. package/src/front/ui/task-work-panel.tsx +0 -218
  80. package/src/navigation-menu-items/docflow.navigation-menu-item.ts +0 -15
  81. package/src/objects/document-folder.object.ts +0 -80
  82. package/src/objects/document.object.ts +0 -312
  83. package/src/page-layouts/docflow.page-layout.ts +0 -86
  84. package/src/page-layouts/task-docflow.page-layout-tab.ts +0 -36
  85. package/src/page-layouts/workflow-steps.page-layout-tab.ts +0 -37
  86. package/src/roles/default-role.ts +0 -21
  87. package/src/roles/docflow-member.role.ts +0 -23
  88. package/src/server/bitrix-page.ts +0 -52
  89. package/src/server/bitrix-portal.ts +0 -67
  90. package/src/server/document-capture.ts +0 -102
  91. package/src/server/http-response.ts +0 -18
  92. package/src/server/launch-step.ts +0 -166
  93. package/src/server/process-tasks.ts +0 -116
  94. package/src/server/records.ts +0 -132
  95. package/src/server/task-completion.ts +0 -100
  96. package/src/server/workflow-engine.ts +0 -105
  97. package/src/shared/api/core-graphql.util.ts +0 -98
  98. package/src/shared/api/object-summaries.ts +0 -79
  99. package/src/shared/api/rest-filter.util.ts +0 -37
  100. package/src/shared/api/rest-response.util.ts +0 -43
  101. package/src/shared/bitrix/bitrix-client.ts +0 -114
  102. package/src/shared/bitrix/bitrix.type.ts +0 -25
  103. package/src/shared/bitrix/bitrix.util.ts +0 -139
  104. package/src/shared/docflow.constants.ts +0 -31
  105. package/src/shared/documents/attachment-target.util.ts +0 -64
  106. package/src/shared/engine.ts +0 -11
  107. package/src/shared/form/form-field-builder.util.ts +0 -122
  108. package/src/shared/form/form-field.type.ts +0 -23
  109. package/src/shared/form/form-fields.util.ts +0 -138
  110. package/src/shared/form/step-settings.util.ts +0 -104
  111. package/src/shared/workflow/launch-step.util.ts +0 -56
  112. package/src/shared/workflow/run-target-record.util.ts +0 -70
  113. package/src/shared/workflow/workflow-run.type.ts +0 -35
  114. package/src/shared/workflow/workflow-run.util.ts +0 -174
  115. package/src/universal-identifiers.ts +0 -199
  116. package/src/views/all-document-folders.view.ts +0 -50
  117. package/src/views/all-documents.view.ts +0 -91
  118. package/src/views/documents-by-status.view.ts +0 -76
  119. package/tsconfig.json +0 -37
  120. package/tsconfig.spec.json +0 -9
  121. package/vitest.unit.config.ts +0 -14
  122. /package/{.twenty/output/front-component-shared-dependencies.mjs → front-component-shared-dependencies.mjs} +0 -0
  123. /package/{.twenty/output/src → src}/logic-functions/bitrix-app.logic-function.mjs +0 -0
  124. /package/{.twenty/output/src → src}/logic-functions/bitrix-event.logic-function.mjs +0 -0
  125. /package/{.twenty/output/src → src}/logic-functions/bitrix-install.logic-function.mjs +0 -0
  126. /package/{.twenty/output/src → src}/logic-functions/capture-file.logic-function.mjs +0 -0
  127. /package/{.twenty/output/src → src}/logic-functions/complete-task.logic-function.mjs +0 -0
  128. /package/{.twenty/output/src → src}/logic-functions/hand-out-form-step.logic-function.mjs +0 -0
  129. /package/{.twenty/output/src → src}/logic-functions/health-check.mjs +0 -0
  130. /package/{.twenty/output/src → src}/logic-functions/launch-parameters-step.logic-function.mjs +0 -0
  131. /package/{.twenty/output/src → src}/logic-functions/notify-bitrix-assignee.logic-function.mjs +0 -0
  132. /package/{.twenty/output/src → src}/logic-functions/on-attachment-created.logic-function.mjs +0 -0
  133. /package/{.twenty/output/src → src}/logic-functions/on-task-updated.logic-function.mjs +0 -0
  134. /package/{.twenty/output/src → src}/logic-functions/on-workflow-run-updated.logic-function.mjs +0 -0
  135. /package/{.twenty/output/src → src}/logic-functions/save-step-settings.logic-function.mjs +0 -0
  136. /package/{.twenty/output/src → src}/logic-functions/save-task-form.logic-function.mjs +0 -0
@@ -1,266 +0,0 @@
1
- import { useState } from 'react';
2
- import { defineFrontComponent } from 'twenty-sdk/define';
3
- import { AppPath, enqueueSnackbar, navigate } from 'twenty-sdk/front-component';
4
- import { Button } from 'twenty-ui/primitives/input';
5
- import { useTheme } from 'twenty-ui/theme';
6
-
7
- import { callAppRoute, createRecord, getErrorMessage, listRecords } from 'src/front/api';
8
- import { useAsync, useDebouncedValue } from 'src/front/hooks/use-async';
9
- import { DataTable, DocflowRoot, Message, SplitView, TextField, Toolbar } from 'src/front/ui/components';
10
- import { FormFieldInputs } from 'src/front/ui/form-field-inputs';
11
- import { restAnd, restEq, restIlike, restIn } from 'src/shared/api/rest-filter.util';
12
- import { DOCFLOW_SECTION } from 'src/shared/docflow.constants';
13
- import { type FormField, type FormValues } from 'src/shared/form/form-field.type';
14
- import { findMissingRequiredFields } from 'src/shared/form/form-fields.util';
15
- import {
16
- buildLaunchParameters,
17
- isLaunchParametersStep,
18
- parseLaunchFields,
19
- } from 'src/shared/workflow/launch-step.util';
20
- import { type WorkflowStep, type WorkflowTrigger } from 'src/shared/workflow/workflow-run.type';
21
- import { PROCESSES_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/universal-identifiers';
22
-
23
- const PROCESSES_LIMIT = 200;
24
- const SEARCH_DEBOUNCE_MS = 300;
25
-
26
- type WorkflowRecord = { id: string; name?: string | null; smbLaunchFields?: unknown };
27
- type WorkflowVersionRecord = {
28
- id: string;
29
- workflowId: string;
30
- trigger?: WorkflowTrigger | null;
31
- steps?: WorkflowStep[] | null;
32
- };
33
-
34
- type DocflowProcess = {
35
- workflowId: string;
36
- workflowVersionId: string;
37
- name: string;
38
- launchFields: FormField[];
39
- stageNames: string[];
40
- };
41
-
42
- // Docflow processes are the standard workflows with the docflow section,
43
- // published, started by hand. Both lists are filtered by the server.
44
- const loadProcesses = async (searchQuery: string): Promise<DocflowProcess[]> => {
45
- const { records: workflows } = await listRecords<WorkflowRecord>('workflows', {
46
- filter: restAnd(
47
- restEq('smbSection', DOCFLOW_SECTION),
48
- searchQuery.trim().length > 0 ? restIlike('name', searchQuery) : undefined,
49
- ),
50
- order_by: 'name[AscNullsLast]',
51
- limit: PROCESSES_LIMIT,
52
- });
53
-
54
- if (workflows.length === 0) {
55
- return [];
56
- }
57
-
58
- const { records: versions } = await listRecords<WorkflowVersionRecord>('workflowVersions', {
59
- filter: restAnd(
60
- restEq('status', 'ACTIVE'),
61
- restIn('workflowId', workflows.map(({ id }) => id)),
62
- ),
63
- limit: PROCESSES_LIMIT,
64
- });
65
-
66
- return workflows.flatMap((workflow) => {
67
- const version = versions.find((candidate) => candidate.workflowId === workflow.id);
68
-
69
- if (version === undefined || version.trigger?.type !== 'MANUAL') {
70
- return [];
71
- }
72
-
73
- return [
74
- {
75
- workflowId: workflow.id,
76
- workflowVersionId: version.id,
77
- name: workflow.name?.trim() || 'Без названия',
78
- launchFields: parseLaunchFields(workflow.smbLaunchFields),
79
- stageNames: (version.steps ?? [])
80
- .filter((step) => !isLaunchParametersStep(step))
81
- .map((step) => step.name),
82
- },
83
- ];
84
- });
85
- };
86
-
87
- const ProcessLaunchForm = ({
88
- process,
89
- onLaunched,
90
- }: {
91
- process: DocflowProcess;
92
- onLaunched: () => void;
93
- }) => {
94
- const theme = useTheme();
95
- const [values, setValues] = useState<FormValues>({});
96
- const [isLaunching, setIsLaunching] = useState(false);
97
- const [errorMessage, setErrorMessage] = useState<string | undefined>();
98
-
99
- const handleLaunch = async () => {
100
- const parameters = buildLaunchParameters({ fields: process.launchFields, values });
101
- const missingFields = findMissingRequiredFields({ fields: process.launchFields, values: parameters });
102
-
103
- if (missingFields.length > 0) {
104
- setErrorMessage(`Заполните: ${missingFields.map(({ label }) => label).join(', ')}`);
105
-
106
- return;
107
- }
108
-
109
- setIsLaunching(true);
110
- setErrorMessage(undefined);
111
-
112
- try {
113
- await callAppRoute('/docflow/processes/launch', {
114
- workflowVersionId: process.workflowVersionId,
115
- values: parameters,
116
- });
117
- await enqueueSnackbar({ message: 'Процесс запущен', variant: 'success' });
118
- onLaunched();
119
- } catch (error) {
120
- setErrorMessage(getErrorMessage(error, 'Не удалось запустить процесс'));
121
- } finally {
122
- setIsLaunching(false);
123
- }
124
- };
125
-
126
- return (
127
- <div style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[3] }}>
128
- {process.stageNames.length > 0 && (
129
- <Message>Этапы: {process.stageNames.join(' → ')}</Message>
130
- )}
131
- {process.launchFields.length > 0 ? (
132
- <FormFieldInputs fields={process.launchFields} values={values} onChange={setValues} disabled={isLaunching} />
133
- ) : (
134
- <Message>Процесс запускается без параметров.</Message>
135
- )}
136
- {errorMessage !== undefined && <Message tone="danger">{errorMessage}</Message>}
137
- <Button variant="solid" size="md" fullWidth loading={isLaunching} onClick={() => void handleLaunch()}>
138
- Запустить
139
- </Button>
140
- <Button
141
- variant="ghost"
142
- size="sm"
143
- onClick={() =>
144
- void navigate(AppPath.RecordShowPage, {
145
- objectNameSingular: 'workflow',
146
- objectRecordId: process.workflowId,
147
- })
148
- }
149
- >
150
- Изменить процесс
151
- </Button>
152
- </div>
153
- );
154
- };
155
-
156
- const DocflowProcesses = () => {
157
- const theme = useTheme();
158
- const [searchInput, setSearchInput] = useState('');
159
- const searchQuery = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
160
- const [selectedVersionId, setSelectedVersionId] = useState<string | undefined>();
161
- const [isCreating, setIsCreating] = useState(false);
162
- const { data: processes, loading, error } = useAsync(() => loadProcesses(searchQuery), [searchQuery]);
163
- const selectedProcess = processes?.find((process) => process.workflowVersionId === selectedVersionId);
164
-
165
- // Created in the docflow section, then opened in the standard editor.
166
- const handleCreate = async () => {
167
- setIsCreating(true);
168
-
169
- try {
170
- const workflow = await createRecord<WorkflowRecord>('workflows', {
171
- name: 'Новый процесс',
172
- smbSection: DOCFLOW_SECTION,
173
- });
174
-
175
- if (workflow !== undefined) {
176
- await navigate(AppPath.RecordShowPage, {
177
- objectNameSingular: 'workflow',
178
- objectRecordId: workflow.id,
179
- });
180
- }
181
- } catch (createError) {
182
- await enqueueSnackbar({
183
- message: getErrorMessage(createError, 'Не удалось создать процесс'),
184
- variant: 'error',
185
- });
186
- } finally {
187
- setIsCreating(false);
188
- }
189
- };
190
-
191
- return (
192
- <DocflowRoot>
193
- <SplitView
194
- panelTitle={selectedProcess?.name}
195
- onClosePanel={() => setSelectedVersionId(undefined)}
196
- panel={
197
- selectedProcess !== undefined ? (
198
- <ProcessLaunchForm
199
- key={selectedProcess.workflowVersionId}
200
- process={selectedProcess}
201
- onLaunched={() => setSelectedVersionId(undefined)}
202
- />
203
- ) : undefined
204
- }
205
- >
206
- <Toolbar>
207
- <div style={{ flex: '0 1 240px' }}>
208
- <TextField
209
- type="search"
210
- variant="bare"
211
- placeholder="Поиск процессов"
212
- value={searchInput}
213
- onChange={setSearchInput}
214
- />
215
- </div>
216
- <div style={{ marginLeft: 'auto' }}>
217
- <Button variant="outline" size="sm" loading={isCreating} onClick={() => void handleCreate()}>
218
- Создать
219
- </Button>
220
- </div>
221
- </Toolbar>
222
- {error !== undefined && (
223
- <div style={{ padding: theme.spacing[4] }}>
224
- <Message tone="danger">{getErrorMessage(error, 'Не удалось загрузить процессы')}</Message>
225
- </div>
226
- )}
227
- {loading && processes === undefined && <Message>Загрузка…</Message>}
228
- {!loading && error === undefined && processes?.length === 0 && (
229
- <Message>
230
- Процессов для запуска нет. Здесь видны опубликованные процессы с ручным запуском и разделом «Документооборот». Параметры запуска задаются на вкладке процесса «Запуск и задания».
231
- </Message>
232
- )}
233
- {processes !== undefined && processes.length > 0 && (
234
- <DataTable
235
- columns={[
236
- { key: 'name', label: 'Процесс', width: '40%' },
237
- { key: 'parameters', label: 'Параметры', width: '18%' },
238
- { key: 'stages', label: 'Этапы' },
239
- ]}
240
- selectedRowId={selectedVersionId}
241
- onRowClick={setSelectedVersionId}
242
- rows={processes.map((process) => {
243
- const parameterCount = process.launchFields.length;
244
-
245
- return {
246
- id: process.workflowVersionId,
247
- cells: {
248
- name: process.name,
249
- parameters: parameterCount > 0 ? String(parameterCount) : 'нет',
250
- stages: process.stageNames.join(' → '),
251
- },
252
- };
253
- })}
254
- />
255
- )}
256
- </SplitView>
257
- </DocflowRoot>
258
- );
259
- };
260
-
261
- export default defineFrontComponent({
262
- universalIdentifier: PROCESSES_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
263
- name: 'docflow-processes',
264
- description: 'Процессы документооборота: запуск с параметрами',
265
- component: DocflowProcesses,
266
- });
@@ -1,303 +0,0 @@
1
- import { useState } from 'react';
2
- import { defineFrontComponent } from 'twenty-sdk/define';
3
- import { enqueueSnackbar } from 'twenty-sdk/front-component';
4
- import { Button } from 'twenty-ui/primitives/input';
5
- import { Tag } from 'twenty-ui/primitives/data-display';
6
- import { useTheme } from 'twenty-ui/theme';
7
-
8
- import { createRecord, getErrorMessage, listRecords } from 'src/front/api';
9
- import { formatDate, formatMemberName, isOverdue, TASK_STATUS_LABELS } from 'src/front/format';
10
- import { useAsync, useDebouncedValue } from 'src/front/hooks/use-async';
11
- import { useWorkspaceMembers } from 'src/front/hooks/use-workspace-members';
12
- import { type DocflowTask } from 'src/front/types';
13
- import {
14
- DataTable,
15
- DocflowRoot,
16
- Message,
17
- SectionTitle,
18
- SelectField,
19
- SplitView,
20
- TextAreaField,
21
- TextField,
22
- Toolbar,
23
- } from 'src/front/ui/components';
24
- import { FormFieldsBuilder } from 'src/front/ui/form-fields-builder';
25
- import { TaskWorkPanel } from 'src/front/ui/task-work-panel';
26
- import {
27
- restAnd,
28
- restEq,
29
- restIlike,
30
- restIsNull,
31
- restLt,
32
- restNeq,
33
- restOr,
34
- } from 'src/shared/api/rest-filter.util';
35
- import { DOCFLOW_SECTION, PAGE_SIZE, TASK_DONE_STATUS, TASK_TODO_STATUS } from 'src/shared/docflow.constants';
36
- import { type FormField } from 'src/shared/form/form-field.type';
37
- import { getFormFieldsError } from 'src/shared/form/form-fields.util';
38
- import { TASKS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/universal-identifiers';
39
-
40
- type TaskFilter = 'open' | 'mine' | 'overdue' | 'done';
41
- type TaskSort = 'newest' | 'oldest' | 'dueSoon' | 'nameAsc';
42
- type Panel = { kind: 'closed' } | { kind: 'create' } | { kind: 'task'; taskId: string };
43
-
44
- const SEARCH_DEBOUNCE_MS = 300;
45
- const NO_ASSIGNEE = '';
46
-
47
- const ORDER_BY: Record<TaskSort, string> = {
48
- newest: 'createdAt[DescNullsLast]',
49
- oldest: 'createdAt[AscNullsLast]',
50
- dueSoon: 'dueAt[AscNullsLast]',
51
- nameAsc: 'title[AscNullsLast]',
52
- };
53
-
54
- const OPEN_FILTER = restOr(restNeq('status', TASK_DONE_STATUS), restIsNull('status'));
55
-
56
- // The list is filtered by the server, so a long history of tasks never
57
- // travels to the browser.
58
- const buildFilter = ({
59
- taskFilter,
60
- searchQuery,
61
- currentMemberId,
62
- now,
63
- }: {
64
- taskFilter: TaskFilter;
65
- searchQuery: string;
66
- currentMemberId: string | undefined;
67
- now: string;
68
- }) =>
69
- restAnd(
70
- restEq('smbSection', DOCFLOW_SECTION),
71
- taskFilter === 'done' ? restEq('status', TASK_DONE_STATUS) : OPEN_FILTER,
72
- taskFilter === 'mine'
73
- ? currentMemberId !== undefined
74
- ? restEq('assigneeId', currentMemberId)
75
- : restIsNull('assigneeId')
76
- : undefined,
77
- taskFilter === 'overdue' ? restLt('dueAt', now) : undefined,
78
- searchQuery.trim().length > 0 ? restIlike('title', searchQuery) : undefined,
79
- );
80
-
81
- const TaskCreateForm = ({
82
- currentMemberId,
83
- onCreated,
84
- onCancel,
85
- }: {
86
- currentMemberId: string | undefined;
87
- onCreated: (taskId: string) => void;
88
- onCancel: () => void;
89
- }) => {
90
- const theme = useTheme();
91
- const { members } = useWorkspaceMembers();
92
- const [title, setTitle] = useState('');
93
- const [assigneeId, setAssigneeId] = useState(currentMemberId ?? NO_ASSIGNEE);
94
- const [dueAt, setDueAt] = useState('');
95
- const [instruction, setInstruction] = useState('');
96
- const [fields, setFields] = useState<FormField[]>([]);
97
- const [isSaving, setIsSaving] = useState(false);
98
- const [errorMessage, setErrorMessage] = useState<string | undefined>();
99
-
100
- const handleCreate = async () => {
101
- const fieldsError = getFormFieldsError(fields);
102
-
103
- if (fieldsError !== undefined) {
104
- setErrorMessage(fieldsError);
105
-
106
- return;
107
- }
108
-
109
- setIsSaving(true);
110
- setErrorMessage(undefined);
111
-
112
- try {
113
- const task = await createRecord<DocflowTask>('tasks', {
114
- title: title.trim(),
115
- assigneeId: assigneeId === NO_ASSIGNEE ? null : assigneeId,
116
- dueAt: /^\d{4}-\d{2}-\d{2}$/.test(dueAt) ? dueAt : null,
117
- status: TASK_TODO_STATUS,
118
- smbSection: DOCFLOW_SECTION,
119
- smbInstruction: instruction.trim().length > 0 ? instruction.trim() : null,
120
- smbFormSchema: fields.length > 0 ? fields : null,
121
- smbFormValues: fields.length > 0 ? {} : null,
122
- });
123
-
124
- if (task === undefined) {
125
- throw new Error('The created task was not returned');
126
- }
127
-
128
- await enqueueSnackbar({ message: 'Задание создано', variant: 'success' });
129
- onCreated(task.id);
130
- } catch (error) {
131
- setErrorMessage(getErrorMessage(error, 'Не удалось создать задание'));
132
- } finally {
133
- setIsSaving(false);
134
- }
135
- };
136
-
137
- return (
138
- <div style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[3] }}>
139
- <TextField label="Название" value={title} onChange={setTitle} />
140
- <SelectField
141
- label="Исполнитель"
142
- value={assigneeId}
143
- options={[
144
- { value: NO_ASSIGNEE, label: 'Не назначен' },
145
- ...members.map((member) => ({ value: member.id, label: formatMemberName(member) || member.id })),
146
- ]}
147
- onChange={setAssigneeId}
148
- />
149
- <TextField label="Срок" type="date" value={dueAt} onChange={setDueAt} />
150
- <TextAreaField label="Инструкция" value={instruction} onChange={setInstruction} rows={4} />
151
- <SectionTitle>Поля для заполнения</SectionTitle>
152
- <FormFieldsBuilder fields={fields} onChange={setFields} disabled={isSaving} />
153
- {errorMessage !== undefined && <Message tone="danger">{errorMessage}</Message>}
154
- <div style={{ display: 'flex', gap: theme.spacing[2] }}>
155
- <Button variant="solid" size="sm" loading={isSaving} disabled={title.trim().length === 0} onClick={() => void handleCreate()}>
156
- Создать
157
- </Button>
158
- <Button variant="ghost" size="sm" onClick={onCancel}>
159
- Отмена
160
- </Button>
161
- </div>
162
- </div>
163
- );
164
- };
165
-
166
- const DocflowTasks = () => {
167
- const theme = useTheme();
168
- const { members, currentMember } = useWorkspaceMembers();
169
- const [searchInput, setSearchInput] = useState('');
170
- const searchQuery = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
171
- const [taskFilter, setTaskFilter] = useState<TaskFilter>('open');
172
- const [sort, setSort] = useState<TaskSort>('newest');
173
- const [panel, setPanel] = useState<Panel>({ kind: 'closed' });
174
- const [pages, setPages] = useState(1);
175
- // Fixed for the life of the list, so "overdue" does not refetch by itself.
176
- const [now] = useState(() => new Date().toISOString());
177
-
178
- const { data, loading, error, reload } = useAsync(
179
- () =>
180
- listRecords<DocflowTask>('tasks', {
181
- filter: buildFilter({ taskFilter, searchQuery, currentMemberId: currentMember?.id, now }),
182
- order_by: ORDER_BY[sort],
183
- limit: PAGE_SIZE * pages,
184
- }),
185
- [taskFilter, searchQuery, currentMember?.id, sort, pages, now],
186
- );
187
- const tasks = data?.records ?? [];
188
-
189
- return (
190
- <DocflowRoot>
191
- <SplitView
192
- panelTitle={panel.kind === 'create' ? 'Новое задание' : 'Задание'}
193
- onClosePanel={() => setPanel({ kind: 'closed' })}
194
- panel={
195
- panel.kind === 'create' ? (
196
- <TaskCreateForm
197
- currentMemberId={currentMember?.id}
198
- onCancel={() => setPanel({ kind: 'closed' })}
199
- onCreated={(taskId) => {
200
- setPanel({ kind: 'task', taskId });
201
- void reload();
202
- }}
203
- />
204
- ) : panel.kind === 'task' ? (
205
- <TaskWorkPanel key={panel.taskId} taskId={panel.taskId} onChanged={() => void reload()} />
206
- ) : undefined
207
- }
208
- >
209
- <Toolbar>
210
- <div style={{ flex: '0 1 240px' }}>
211
- <TextField
212
- type="search"
213
- variant="bare"
214
- placeholder="Поиск заданий"
215
- value={searchInput}
216
- onChange={setSearchInput}
217
- />
218
- </div>
219
- <SelectField<TaskFilter>
220
- variant="bare"
221
- value={taskFilter}
222
- options={[
223
- { value: 'open', label: 'Открытые' },
224
- { value: 'mine', label: 'Мои' },
225
- { value: 'overdue', label: 'Просроченные' },
226
- { value: 'done', label: 'Выполненные' },
227
- ]}
228
- onChange={(value) => {
229
- setTaskFilter(value);
230
- setPages(1);
231
- }}
232
- />
233
- <SelectField<TaskSort>
234
- variant="bare"
235
- value={sort}
236
- options={[
237
- { value: 'newest', label: 'Сначала новые' },
238
- { value: 'oldest', label: 'Сначала старые' },
239
- { value: 'dueSoon', label: 'По сроку' },
240
- { value: 'nameAsc', label: 'По названию' },
241
- ]}
242
- onChange={setSort}
243
- />
244
- <div style={{ marginLeft: 'auto' }}>
245
- <Button variant="outline" size="sm" onClick={() => setPanel({ kind: 'create' })}>
246
- Создать
247
- </Button>
248
- </div>
249
- </Toolbar>
250
- {error !== undefined && (
251
- <div style={{ padding: theme.spacing[4] }}>
252
- <Message tone="danger">{getErrorMessage(error, 'Не удалось загрузить задания')}</Message>
253
- </div>
254
- )}
255
- {loading && tasks.length === 0 && <Message>Загрузка…</Message>}
256
- {!loading && error === undefined && tasks.length === 0 && <Message>Заданий нет.</Message>}
257
- {tasks.length > 0 && (
258
- <DataTable
259
- columns={[
260
- { key: 'title', label: 'Задание', width: '40%' },
261
- { key: 'assignee', label: 'Исполнитель', width: '24%' },
262
- { key: 'dueAt', label: 'Срок', width: '16%' },
263
- { key: 'status', label: 'Статус' },
264
- ]}
265
- selectedRowId={panel.kind === 'task' ? panel.taskId : undefined}
266
- onRowClick={(taskId) => setPanel({ kind: 'task', taskId })}
267
- rows={tasks.map((task) => ({
268
- id: task.id,
269
- cells: {
270
- title: task.title?.trim() || 'Без названия',
271
- assignee: formatMemberName(members.find((member) => member.id === task.assigneeId)),
272
- dueAt: (
273
- <span style={{ color: isOverdue(task.dueAt, task.status, new Date(now)) ? theme.font.color.danger : undefined }}>
274
- {formatDate(task.dueAt)}
275
- </span>
276
- ),
277
- status: (
278
- <Tag color={task.status === TASK_DONE_STATUS ? 'green' : 'gray'}>
279
- {TASK_STATUS_LABELS[task.status ?? ''] ?? task.status ?? 'Без статуса'}
280
- </Tag>
281
- ),
282
- },
283
- }))}
284
- />
285
- )}
286
- {data?.pageInfo.hasNextPage === true && (
287
- <div style={{ display: 'flex', justifyContent: 'center', padding: theme.spacing[3] }}>
288
- <Button variant="ghost" size="sm" loading={loading} onClick={() => setPages((current) => current + 1)}>
289
- Показать ещё
290
- </Button>
291
- </div>
292
- )}
293
- </SplitView>
294
- </DocflowRoot>
295
- );
296
- };
297
-
298
- export default defineFrontComponent({
299
- universalIdentifier: TASKS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
300
- name: 'docflow-tasks',
301
- description: 'Задания документооборота: штатные задачи с разделом «Документооборот»',
302
- component: DocflowTasks,
303
- });