@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,516 +0,0 @@
1
- import { useState } from 'react';
2
- import { defineFrontComponent } from 'twenty-sdk/define';
3
- import { AppPath, enqueueSnackbar, navigate, openSidePanelPage } 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, getRecord, listRecords } from 'src/front/api';
9
- import {
10
- DOCUMENT_ORIGIN_LABELS,
11
- DOCUMENT_STATUS_LABELS,
12
- DOCUMENT_TYPE_LABELS,
13
- formatDate,
14
- formatMemberName,
15
- } from 'src/front/format';
16
- import { useAsync, useDebouncedValue } from 'src/front/hooks/use-async';
17
- import { type DocflowDocument, type DocumentFolder } from 'src/front/types';
18
- import {
19
- DataTable,
20
- Details,
21
- DocflowRoot,
22
- Message,
23
- SelectField,
24
- SplitView,
25
- TextAreaField,
26
- TextField,
27
- Toolbar,
28
- } from 'src/front/ui/components';
29
- import { getObjectSummaries } from 'src/shared/api/object-summaries';
30
- import { restAnd, restEq, restIlike, restIn, restOr } from 'src/shared/api/rest-filter.util';
31
- import { DOCUMENT_OBJECT_NAME, PAGE_SIZE } from 'src/shared/docflow.constants';
32
- import { DOCUMENTS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/universal-identifiers';
33
-
34
- type DocumentFilter = 'all' | 'collected' | 'DRAFT' | 'IN_REVIEW' | 'APPROVED' | 'ARCHIVED';
35
- type Panel = { kind: 'closed' } | { kind: 'upload' } | { kind: 'document'; documentId: string };
36
-
37
- const SEARCH_DEBOUNCE_MS = 300;
38
- const FOLDERS_LIMIT = 500;
39
- const COMPANY_OPTIONS_LIMIT = 8;
40
- const ALL_FOLDERS = '';
41
- const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
42
-
43
- // The sandbox of a front component gets only the name and size of a chosen
44
- // file, never its content, so the file goes in through the standard "File"
45
- // field of the document's own card, opened in the side panel.
46
- const openDocumentCard = (documentId: string) =>
47
- openSidePanelPage({
48
- to: AppPath.RecordShowPage,
49
- params: { objectNameSingular: DOCUMENT_OBJECT_NAME, objectRecordId: documentId },
50
- pageTitle: 'Документ',
51
- });
52
-
53
- const buildFilter = ({
54
- folderId,
55
- documentFilter,
56
- searchQuery,
57
- }: {
58
- folderId: string;
59
- documentFilter: DocumentFilter;
60
- searchQuery: string;
61
- }) =>
62
- restAnd(
63
- folderId !== ALL_FOLDERS ? restEq('folderId', folderId) : undefined,
64
- documentFilter === 'collected'
65
- ? restIn('origin', ['RECORD', 'PROCESS'])
66
- : documentFilter !== 'all'
67
- ? restEq('status', documentFilter)
68
- : undefined,
69
- searchQuery.trim().length > 0
70
- ? restOr(
71
- restIlike('name', searchQuery),
72
- restIlike('number', searchQuery),
73
- restIlike('sourceRecordName', searchQuery),
74
- )
75
- : undefined,
76
- );
77
-
78
- const CompanyPicker = ({
79
- value,
80
- onChange,
81
- }: {
82
- value: { id: string; name: string } | null;
83
- onChange: (company: { id: string; name: string } | null) => void;
84
- }) => {
85
- const theme = useTheme();
86
- const [searchInput, setSearchInput] = useState('');
87
- const searchQuery = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
88
- const { data } = useAsync(
89
- async () =>
90
- searchQuery.trim().length === 0
91
- ? []
92
- : (
93
- await listRecords<{ id: string; name?: string | null }>('companies', {
94
- filter: restIlike('name', searchQuery),
95
- limit: COMPANY_OPTIONS_LIMIT,
96
- })
97
- ).records,
98
- [searchQuery],
99
- );
100
-
101
- if (value !== null) {
102
- return (
103
- <div style={{ alignItems: 'center', display: 'flex', gap: theme.spacing[2], justifyContent: 'space-between' }}>
104
- <span>Контрагент: {value.name}</span>
105
- <Button variant="ghost" size="sm" onClick={() => onChange(null)}>
106
- Убрать
107
- </Button>
108
- </div>
109
- );
110
- }
111
-
112
- return (
113
- <div style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[1] }}>
114
- <TextField label="Контрагент" placeholder="Начните вводить название" value={searchInput} onChange={setSearchInput} />
115
- {(data ?? []).map((company) => (
116
- <Button
117
- key={company.id}
118
- variant="ghost"
119
- size="sm"
120
- onClick={() => {
121
- onChange({ id: company.id, name: company.name?.trim() || 'Без названия' });
122
- setSearchInput('');
123
- }}
124
- >
125
- {company.name?.trim() || 'Без названия'}
126
- </Button>
127
- ))}
128
- </div>
129
- );
130
- };
131
-
132
- const DocumentUploadForm = ({
133
- folderId,
134
- onUploaded,
135
- onCancel,
136
- }: {
137
- folderId: string;
138
- onUploaded: (documentId: string) => void;
139
- onCancel: () => void;
140
- }) => {
141
- const theme = useTheme();
142
- const [name, setName] = useState('');
143
- const [documentType, setDocumentType] = useState('OTHER');
144
- const [status, setStatus] = useState('DRAFT');
145
- const [number, setNumber] = useState('');
146
- const [documentDate, setDocumentDate] = useState('');
147
- const [validUntil, setValidUntil] = useState('');
148
- const [description, setDescription] = useState('');
149
- const [counterparty, setCounterparty] = useState<{ id: string; name: string } | null>(null);
150
- const [isSaving, setIsSaving] = useState(false);
151
- const [errorMessage, setErrorMessage] = useState<string | undefined>();
152
-
153
- const handleCreate = async () => {
154
- if (name.trim().length === 0) {
155
- return;
156
- }
157
-
158
- setIsSaving(true);
159
- setErrorMessage(undefined);
160
-
161
- try {
162
- const document = await createRecord<DocflowDocument>('smbDocuments', {
163
- name: name.trim(),
164
- documentType,
165
- status,
166
- number: number.trim() || null,
167
- documentDate: DATE_PATTERN.test(documentDate) ? documentDate : null,
168
- validUntil: DATE_PATTERN.test(validUntil) ? validUntil : null,
169
- description: description.trim() || null,
170
- counterpartyId: counterparty?.id ?? null,
171
- folderId: folderId === ALL_FOLDERS ? null : folderId,
172
- origin: 'UPLOAD',
173
- });
174
-
175
- if (document === undefined) {
176
- throw new Error('документ создан, но сервер его не вернул');
177
- }
178
-
179
- await enqueueSnackbar({ message: 'Документ создан: прикрепите файл в поле «Файл»', variant: 'success' });
180
- onUploaded(document.id);
181
- await openDocumentCard(document.id);
182
- } catch (error) {
183
- setErrorMessage(getErrorMessage(error, 'Не удалось создать документ'));
184
- } finally {
185
- setIsSaving(false);
186
- }
187
- };
188
-
189
- return (
190
- <div style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[3] }}>
191
- <TextField label="Название" value={name} onChange={setName} />
192
- <SelectField
193
- label="Тип"
194
- value={documentType}
195
- options={Object.entries(DOCUMENT_TYPE_LABELS).map(([value, label]) => ({ value, label }))}
196
- onChange={setDocumentType}
197
- />
198
- <SelectField
199
- label="Статус"
200
- value={status}
201
- options={Object.entries(DOCUMENT_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
202
- onChange={setStatus}
203
- />
204
- <TextField label="Номер" value={number} onChange={setNumber} />
205
- <TextField label="Дата документа" type="date" value={documentDate} onChange={setDocumentDate} />
206
- <TextField label="Действует до" type="date" value={validUntil} onChange={setValidUntil} />
207
- <CompanyPicker value={counterparty} onChange={setCounterparty} />
208
- <TextAreaField label="Описание" value={description} onChange={setDescription} />
209
- <Message tone="hint">Файл прикрепляется в карточке документа: она откроется справа после создания.</Message>
210
- {errorMessage !== undefined && <Message tone="danger">{errorMessage}</Message>}
211
- <div style={{ display: 'flex', gap: theme.spacing[2] }}>
212
- <Button
213
- variant="solid"
214
- size="sm"
215
- loading={isSaving}
216
- disabled={name.trim().length === 0}
217
- onClick={() => void handleCreate()}
218
- >
219
- Создать и прикрепить файл
220
- </Button>
221
- <Button variant="ghost" size="sm" onClick={onCancel}>
222
- Отмена
223
- </Button>
224
- </div>
225
- </div>
226
- );
227
- };
228
-
229
- const DocumentCard = ({ documentId }: { documentId: string }) => {
230
- const theme = useTheme();
231
- const { data: document, loading, error, reload } = useAsync(
232
- () => getRecord<DocflowDocument>('smbDocuments', documentId),
233
- [documentId],
234
- );
235
- const { data: objectSummaries } = useAsync(() => getObjectSummaries(), []);
236
-
237
- if (document === undefined) {
238
- return loading ? (
239
- <Message>Загрузка…</Message>
240
- ) : (
241
- <Message tone="danger">{getErrorMessage(error, 'Документ не найден или нет доступа.')}</Message>
242
- );
243
- }
244
-
245
- const file = document.file?.[0];
246
- const sourceSummary =
247
- typeof document.sourceObjectName === 'string'
248
- ? objectSummaries?.get(document.sourceObjectName)
249
- : undefined;
250
- const linkStyle = { color: theme.color.blue, cursor: 'pointer' };
251
-
252
- return (
253
- <div style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[3] }}>
254
- <strong style={{ fontSize: theme.font.size.lg }}>{document.name?.trim() || 'Без названия'}</strong>
255
- <Details
256
- rows={[
257
- { label: 'Тип', value: DOCUMENT_TYPE_LABELS[document.documentType ?? ''] },
258
- { label: 'Номер', value: document.number },
259
- { label: 'Дата', value: formatDate(document.documentDate) },
260
- { label: 'Действует до', value: formatDate(document.validUntil) },
261
- { label: 'Контрагент', value: document.counterparty?.name },
262
- {
263
- label: 'Статус',
264
- value: document.status && <Tag color="gray">{DOCUMENT_STATUS_LABELS[document.status] ?? document.status}</Tag>,
265
- },
266
- { label: 'Автор', value: formatMemberName(document.author ?? undefined) },
267
- { label: 'Добавлен', value: formatDate(document.createdAt) },
268
- { label: 'Источник', value: DOCUMENT_ORIGIN_LABELS[document.origin ?? ''] },
269
- {
270
- label: 'Где загружен',
271
- value: sourceSummary !== undefined && typeof document.sourceRecordId === 'string' && (
272
- <span
273
- role="link"
274
- tabIndex={0}
275
- style={linkStyle}
276
- onClick={() =>
277
- void navigate(AppPath.RecordShowPage, {
278
- objectNameSingular: sourceSummary.nameSingular,
279
- objectRecordId: document.sourceRecordId as string,
280
- })
281
- }
282
- >
283
- {document.sourceRecordName ?? sourceSummary.labelSingular}
284
- </span>
285
- ),
286
- },
287
- {
288
- label: 'Процесс',
289
- value: typeof document.workflowRunId === 'string' && (
290
- <span
291
- role="link"
292
- tabIndex={0}
293
- style={linkStyle}
294
- onClick={() =>
295
- void navigate(AppPath.RecordShowPage, {
296
- objectNameSingular: 'workflowRun',
297
- objectRecordId: document.workflowRunId as string,
298
- })
299
- }
300
- >
301
- Открыть запуск
302
- </span>
303
- ),
304
- },
305
- ]}
306
- />
307
- {typeof document.description === 'string' && document.description.length > 0 && (
308
- <div style={{ whiteSpace: 'pre-wrap' }}>{document.description}</div>
309
- )}
310
- {file?.url !== undefined ? (
311
- <a href={file.url} target="_blank" rel="noopener noreferrer" download={file.label} style={linkStyle}>
312
- Скачать {file.label}
313
- </a>
314
- ) : (
315
- <Message tone="hint">Файл не прикреплён.</Message>
316
- )}
317
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: theme.spacing[2] }}>
318
- <Button variant="outline" size="sm" onClick={() => void openDocumentCard(documentId)}>
319
- {file === undefined ? 'Прикрепить файл' : 'Открыть карточку'}
320
- </Button>
321
- <Button variant="ghost" size="sm" loading={loading} onClick={() => void reload()}>
322
- Обновить
323
- </Button>
324
- </div>
325
- </div>
326
- );
327
- };
328
-
329
- const DocflowDocuments = () => {
330
- const theme = useTheme();
331
- const [searchInput, setSearchInput] = useState('');
332
- const searchQuery = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
333
- const [folderId, setFolderId] = useState(ALL_FOLDERS);
334
- const [documentFilter, setDocumentFilter] = useState<DocumentFilter>('all');
335
- const [panel, setPanel] = useState<Panel>({ kind: 'closed' });
336
- const [pages, setPages] = useState(1);
337
- const [newFolderName, setNewFolderName] = useState('');
338
-
339
- const { data: folders, reload: reloadFolders } = useAsync(
340
- async () =>
341
- (
342
- await listRecords<DocumentFolder>('smbDocumentFolders', {
343
- order_by: 'name[AscNullsLast]',
344
- limit: FOLDERS_LIMIT,
345
- })
346
- ).records,
347
- [],
348
- );
349
- const { data, loading, error, reload } = useAsync(
350
- () =>
351
- listRecords<DocflowDocument>('smbDocuments', {
352
- filter: buildFilter({ folderId, documentFilter, searchQuery }),
353
- order_by: 'createdAt[DescNullsLast]',
354
- limit: PAGE_SIZE * pages,
355
- depth: 1,
356
- }),
357
- [folderId, documentFilter, searchQuery, pages],
358
- );
359
- const documents = data?.records ?? [];
360
-
361
- const handleCreateFolder = async () => {
362
- const folderName = newFolderName.trim();
363
-
364
- if (folderName.length === 0) {
365
- return;
366
- }
367
-
368
- try {
369
- await createRecord('smbDocumentFolders', {
370
- name: folderName,
371
- parentFolderId: folderId === ALL_FOLDERS ? null : folderId,
372
- });
373
- setNewFolderName('');
374
- await reloadFolders();
375
- } catch (folderError) {
376
- await enqueueSnackbar({ message: getErrorMessage(folderError, 'Не удалось создать папку'), variant: 'error' });
377
- }
378
- };
379
-
380
- return (
381
- <DocflowRoot>
382
- <SplitView
383
- panelTitle={panel.kind === 'upload' ? 'Новый документ' : 'Документ'}
384
- onClosePanel={() => setPanel({ kind: 'closed' })}
385
- panel={
386
- panel.kind === 'upload' ? (
387
- <DocumentUploadForm
388
- folderId={folderId}
389
- onCancel={() => setPanel({ kind: 'closed' })}
390
- onUploaded={(documentId) => {
391
- setPanel({ kind: 'document', documentId });
392
- void reload();
393
- }}
394
- />
395
- ) : panel.kind === 'document' ? (
396
- <DocumentCard key={panel.documentId} documentId={panel.documentId} />
397
- ) : undefined
398
- }
399
- >
400
- <div style={{ display: 'flex', flex: 1, flexWrap: 'wrap', minHeight: 0 }}>
401
- <nav
402
- aria-label="Папки"
403
- style={{
404
- boxSizing: 'border-box',
405
- display: 'flex',
406
- flex: '1 1 200px',
407
- flexDirection: 'column',
408
- gap: theme.spacing[1],
409
- maxWidth: '240px',
410
- padding: theme.spacing[2],
411
- }}
412
- >
413
- {[{ id: ALL_FOLDERS, name: 'Все документы' }, ...(folders ?? [])].map((folder) => (
414
- <Button
415
- key={folder.id}
416
- variant={folder.id === folderId ? 'solid' : 'ghost'}
417
- size="sm"
418
- fullWidth
419
- onClick={() => {
420
- setFolderId(folder.id);
421
- setPages(1);
422
- }}
423
- >
424
- {folder.name?.trim() || 'Без названия'}
425
- </Button>
426
- ))}
427
- <div style={{ display: 'flex', flexDirection: 'column', gap: theme.spacing[1], marginTop: theme.spacing[3] }}>
428
- <TextField placeholder="Новая папка" value={newFolderName} onChange={setNewFolderName} />
429
- <Button variant="outline" size="sm" disabled={newFolderName.trim().length === 0} onClick={() => void handleCreateFolder()}>
430
- Создать папку
431
- </Button>
432
- </div>
433
- </nav>
434
- <div style={{ display: 'flex', flex: '999 1 420px', flexDirection: 'column', minWidth: 0 }}>
435
- <Toolbar>
436
- <div style={{ flex: '0 1 240px' }}>
437
- <TextField
438
- type="search"
439
- variant="bare"
440
- placeholder="Название или номер"
441
- value={searchInput}
442
- onChange={setSearchInput}
443
- />
444
- </div>
445
- <SelectField<DocumentFilter>
446
- variant="bare"
447
- value={documentFilter}
448
- options={[
449
- { value: 'all', label: 'Все документы' },
450
- { value: 'collected', label: 'Из записей и процессов' },
451
- { value: 'DRAFT', label: 'Черновики' },
452
- { value: 'IN_REVIEW', label: 'На согласовании' },
453
- { value: 'APPROVED', label: 'Согласованные' },
454
- { value: 'ARCHIVED', label: 'В архиве' },
455
- ]}
456
- onChange={(value) => {
457
- setDocumentFilter(value);
458
- setPages(1);
459
- }}
460
- />
461
- <div style={{ marginLeft: 'auto' }}>
462
- <Button variant="solid" size="sm" onClick={() => setPanel({ kind: 'upload' })}>
463
- Добавить документ
464
- </Button>
465
- </div>
466
- </Toolbar>
467
- {error !== undefined && (
468
- <div style={{ padding: theme.spacing[4] }}>
469
- <Message tone="danger">{getErrorMessage(error, 'Не удалось загрузить документы')}</Message>
470
- </div>
471
- )}
472
- {loading && documents.length === 0 && <Message>Загрузка…</Message>}
473
- {!loading && error === undefined && documents.length === 0 && <Message>Документов нет.</Message>}
474
- {documents.length > 0 && (
475
- <DataTable
476
- columns={[
477
- { key: 'name', label: 'Название', width: '34%' },
478
- { key: 'type', label: 'Тип', width: '14%' },
479
- { key: 'number', label: 'Номер', width: '12%' },
480
- { key: 'source', label: 'Где загружен', width: '24%' },
481
- { key: 'createdAt', label: 'Добавлен' },
482
- ]}
483
- selectedRowId={panel.kind === 'document' ? panel.documentId : undefined}
484
- onRowClick={(documentId) => setPanel({ kind: 'document', documentId })}
485
- rows={documents.map((document) => ({
486
- id: document.id,
487
- cells: {
488
- name: document.name?.trim() || 'Без названия',
489
- type: DOCUMENT_TYPE_LABELS[document.documentType ?? ''],
490
- number: document.number,
491
- source: document.sourceRecordName,
492
- createdAt: formatDate(document.createdAt),
493
- },
494
- }))}
495
- />
496
- )}
497
- {data?.pageInfo.hasNextPage === true && (
498
- <div style={{ display: 'flex', justifyContent: 'center', padding: theme.spacing[3] }}>
499
- <Button variant="ghost" size="sm" loading={loading} onClick={() => setPages((current) => current + 1)}>
500
- Показать ещё
501
- </Button>
502
- </div>
503
- )}
504
- </div>
505
- </div>
506
- </SplitView>
507
- </DocflowRoot>
508
- );
509
- };
510
-
511
- export default defineFrontComponent({
512
- universalIdentifier: DOCUMENTS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER,
513
- name: 'docflow-documents',
514
- description: 'Документы компании: загрузка и файлы со всех записей и процессов',
515
- component: DocflowDocuments,
516
- });