@haiiro/docuflow 0.0.11 → 0.0.12
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 +7 -1
- package/manifest.json +27 -12
- package/package.json +1 -1
- package/src/front-components/docflow-documents.front-component.mjs +42 -42
- package/src/front-components/docflow-documents.front-component.tsx +336 -128
- package/src/front-components/docflow-processes.front-component.mjs +35 -35
- package/src/front-components/docflow-processes.front-component.tsx +511 -131
- package/src/front-components/docflow-tasks.front-component.mjs +46 -41
- package/src/front-components/docflow-tasks.front-component.tsx +489 -132
- package/src/front-components/process-steps.front-component.mjs +23 -23
- package/src/front-components/process-steps.front-component.tsx +1 -1
- package/src/front-components/task-form.front-component.mjs +46 -41
- package/src/logic-functions/bitrix-app.logic-function.mjs +295 -142
- package/src/logic-functions/bitrix-app.logic-function.ts +43 -34
- package/src/logic-functions/capture-file.logic-function.mjs +2 -1
- package/src/logic-functions/hand-out-form-step.logic-function.mjs +2 -1
- package/src/logic-functions/on-workflow-run-updated.logic-function.mjs +2 -1
- package/src/logic-functions/publish-process.logic-function.mjs +119519 -0
- package/src/logic-functions/publish-process.logic-function.ts +91 -0
- package/src/logic-functions/save-launch-fields.logic-function.mjs +2 -1
|
@@ -1,23 +1,55 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { defineFrontComponent } from 'twenty-sdk/define';
|
|
3
3
|
import { AppPath, enqueueSnackbar, openSidePanelPage } from 'twenty-sdk/front-component';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
IconExternalLink,
|
|
6
|
+
IconFileText,
|
|
7
|
+
IconFolderSymlink,
|
|
8
|
+
IconPencil,
|
|
9
|
+
IconRestore,
|
|
10
|
+
IconTrash,
|
|
11
|
+
} from 'twenty-ui/icon';
|
|
5
12
|
import { useTheme } from 'twenty-ui/theme';
|
|
6
13
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
14
|
+
import {
|
|
15
|
+
countRecords,
|
|
16
|
+
createRecord,
|
|
17
|
+
deleteRecord,
|
|
18
|
+
deleteRecords,
|
|
19
|
+
getErrorMessage,
|
|
20
|
+
listRecords,
|
|
21
|
+
restoreRecords,
|
|
22
|
+
updateRecord,
|
|
23
|
+
updateRecords,
|
|
24
|
+
} from 'src/front/api';
|
|
25
|
+
import {
|
|
26
|
+
DOCUMENT_ORIGIN_LABELS,
|
|
27
|
+
DOCUMENT_STATUS_COLORS,
|
|
28
|
+
DOCUMENT_STATUS_LABELS,
|
|
29
|
+
DOCUMENT_TYPE_LABELS,
|
|
30
|
+
formatDate,
|
|
31
|
+
formatMemberName,
|
|
32
|
+
} from 'src/front/format';
|
|
9
33
|
import { useAsync, useDebouncedValue } from 'src/front/hooks/use-async';
|
|
34
|
+
import { useBulkAction } from 'src/front/hooks/use-bulk-action';
|
|
10
35
|
import { type DocflowDocument, type DocumentFolder } from 'src/front/types';
|
|
11
36
|
import {
|
|
12
37
|
DataTable,
|
|
13
38
|
DocflowRoot,
|
|
39
|
+
formatTotal,
|
|
40
|
+
ListToolbar,
|
|
14
41
|
Message,
|
|
42
|
+
pluralize,
|
|
43
|
+
SelectionBar,
|
|
44
|
+
ShowMore,
|
|
45
|
+
type TableColumn,
|
|
15
46
|
type TableSort,
|
|
16
47
|
toRestOrderBy,
|
|
17
|
-
|
|
48
|
+
useHiddenColumns,
|
|
49
|
+
useRowSelection,
|
|
18
50
|
} from 'src/front/ui/components';
|
|
19
|
-
import { type FolderAppearance, FolderList } from 'src/front/ui/folders';
|
|
20
|
-
import { Button, type MenuAction, PromptDialog,
|
|
51
|
+
import { type FolderAppearance, FolderList, useFolderColor } from 'src/front/ui/folders';
|
|
52
|
+
import { BulkChangeDialog, Button, type MenuAction, MenuButton, PromptDialog, StatusChip, ViewTabs } from 'src/front/ui/kit';
|
|
21
53
|
import { restAnd, restEq, restIlike, restIn, restIsNotNull, restOr } from 'src/shared/api/rest-filter.util';
|
|
22
54
|
import { DOCUMENT_OBJECT_NAME, PAGE_SIZE } from 'src/shared/docflow.constants';
|
|
23
55
|
import { logDocflow, logDocflowError } from 'src/shared/log.util';
|
|
@@ -29,12 +61,40 @@ const SEARCH_DEBOUNCE_MS = 300;
|
|
|
29
61
|
const FOLDERS_LIMIT = 500;
|
|
30
62
|
const ALL_FOLDERS = '';
|
|
31
63
|
const ANY = 'any';
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
const DOCUMENT_NOUN: [string, string, string] = ['документ', 'документа', 'документов'];
|
|
65
|
+
|
|
66
|
+
// The quick views: a status, all of them, or the trash
|
|
67
|
+
type DocumentView = 'all' | 'DRAFT' | 'IN_REVIEW' | 'APPROVED' | 'ARCHIVED' | 'deleted';
|
|
68
|
+
type DocumentFilters = { origin: string; type: string };
|
|
69
|
+
|
|
70
|
+
const VIEWS: { value: DocumentView; label: string }[] = [
|
|
71
|
+
{ value: 'all', label: 'Все' },
|
|
72
|
+
{ value: 'DRAFT', label: 'Черновики' },
|
|
73
|
+
{ value: 'IN_REVIEW', label: 'На согласовании' },
|
|
74
|
+
{ value: 'APPROVED', label: 'Согласованные' },
|
|
75
|
+
{ value: 'ARCHIVED', label: 'В архиве' },
|
|
76
|
+
{ value: 'deleted', label: 'Удалённые' },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const DEFAULT_FILTERS: DocumentFilters = { origin: ANY, type: ANY };
|
|
36
80
|
const DEFAULT_SORT: TableSort = { key: 'createdAt', direction: 'desc' };
|
|
37
81
|
|
|
82
|
+
const COLUMNS: TableColumn[] = [
|
|
83
|
+
{ key: 'name', label: 'Название', width: '24%', sortKey: 'name' },
|
|
84
|
+
{ key: 'type', label: 'Тип', width: '10%', sortKey: 'documentType' },
|
|
85
|
+
{ key: 'status', label: 'Статус', width: '13%', sortKey: 'status' },
|
|
86
|
+
{ key: 'number', label: 'Номер', width: '9%', sortKey: 'number' },
|
|
87
|
+
{ key: 'documentDate', label: 'Дата документа', width: '10%', sortKey: 'documentDate' },
|
|
88
|
+
{ key: 'counterparty', label: 'Контрагент', width: '13%' },
|
|
89
|
+
{ key: 'responsible', label: 'Ответственный', width: '12%' },
|
|
90
|
+
{ key: 'folder', label: 'Папка', width: '11%' },
|
|
91
|
+
{ key: 'source', label: 'Где загружен', width: '14%', sortKey: 'sourceRecordName' },
|
|
92
|
+
{ key: 'file', label: 'Файл', width: '7%' },
|
|
93
|
+
{ key: 'createdAt', label: 'Добавлен', sortKey: 'createdAt' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const STATUS_VALUES = ['DRAFT', 'IN_REVIEW', 'APPROVED', 'ARCHIVED'];
|
|
97
|
+
|
|
38
98
|
// The sandbox of a front component gets only the name and size of a chosen
|
|
39
99
|
// file, never its content, so the file goes in through the standard "File"
|
|
40
100
|
// field of the document's own card, opened in the side panel.
|
|
@@ -56,39 +116,45 @@ const openDocumentCard = async (documentId: string) => {
|
|
|
56
116
|
}
|
|
57
117
|
};
|
|
58
118
|
|
|
119
|
+
const viewFilter = (view: DocumentView) =>
|
|
120
|
+
// Deleted = in the Twenty trash, restored from here or the document's card
|
|
121
|
+
view === 'deleted' ? restIsNotNull('deletedAt') : view !== 'all' ? restEq('status', view) : undefined;
|
|
122
|
+
|
|
123
|
+
const searchFilter = (searchQuery: string) =>
|
|
124
|
+
searchQuery.trim().length > 0
|
|
125
|
+
? restOr(restIlike('name', searchQuery), restIlike('number', searchQuery), restIlike('sourceRecordName', searchQuery))
|
|
126
|
+
: undefined;
|
|
127
|
+
|
|
59
128
|
const buildFilter = ({
|
|
60
129
|
folderId,
|
|
130
|
+
view,
|
|
61
131
|
filters,
|
|
62
132
|
searchQuery,
|
|
63
133
|
}: {
|
|
64
134
|
folderId: string;
|
|
135
|
+
view: DocumentView;
|
|
65
136
|
filters: DocumentFilters;
|
|
66
137
|
searchQuery: string;
|
|
67
138
|
}) =>
|
|
68
139
|
restAnd(
|
|
69
140
|
folderId !== ALL_FOLDERS ? restEq('folderId', folderId) : undefined,
|
|
70
|
-
|
|
71
|
-
filters.
|
|
72
|
-
filters.status !== ANY ? restEq('status', filters.status) : undefined,
|
|
141
|
+
viewFilter(view),
|
|
142
|
+
filters.type !== ANY ? restEq('documentType', filters.type) : undefined,
|
|
73
143
|
filters.origin === 'collected'
|
|
74
144
|
? restIn('origin', ['RECORD', 'PROCESS'])
|
|
75
145
|
: filters.origin === 'UPLOAD'
|
|
76
146
|
? restEq('origin', 'UPLOAD')
|
|
77
147
|
: undefined,
|
|
78
|
-
searchQuery
|
|
79
|
-
? restOr(
|
|
80
|
-
restIlike('name', searchQuery),
|
|
81
|
-
restIlike('number', searchQuery),
|
|
82
|
-
restIlike('sourceRecordName', searchQuery),
|
|
83
|
-
)
|
|
84
|
-
: undefined,
|
|
148
|
+
searchFilter(searchQuery),
|
|
85
149
|
);
|
|
86
150
|
|
|
87
151
|
const DocflowDocuments = () => {
|
|
88
152
|
const theme = useTheme();
|
|
153
|
+
const folderColor = useFolderColor();
|
|
89
154
|
const [searchInput, setSearchInput] = useState('');
|
|
90
155
|
const searchQuery = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
|
|
91
156
|
const [folderId, setFolderId] = useState(ALL_FOLDERS);
|
|
157
|
+
const [view, setView] = useState<DocumentView>('all');
|
|
92
158
|
const [filters, setFilters] = useState<DocumentFilters>(DEFAULT_FILTERS);
|
|
93
159
|
const [sort, setSort] = useState<TableSort>(DEFAULT_SORT);
|
|
94
160
|
const [openedDocumentId, setOpenedDocumentId] = useState<string | undefined>();
|
|
@@ -96,6 +162,8 @@ const DocflowDocuments = () => {
|
|
|
96
162
|
const [watchStartedAt, setWatchStartedAt] = useState<number | undefined>();
|
|
97
163
|
const [pages, setPages] = useState(1);
|
|
98
164
|
const [isCreating, setIsCreating] = useState(false);
|
|
165
|
+
const [bulkDialogOpen, setBulkDialogOpen] = useState(false);
|
|
166
|
+
const [hiddenColumns, setHiddenColumns] = useHiddenColumns(['documentDate', 'responsible', 'file']);
|
|
99
167
|
|
|
100
168
|
const { data: folders, reload: reloadFolders } = useAsync(
|
|
101
169
|
async () =>
|
|
@@ -109,20 +177,54 @@ const DocflowDocuments = () => {
|
|
|
109
177
|
);
|
|
110
178
|
const listQuery = useMemo(
|
|
111
179
|
() => ({
|
|
112
|
-
filter: buildFilter({ folderId, filters, searchQuery }),
|
|
180
|
+
filter: buildFilter({ folderId, view, filters, searchQuery }),
|
|
113
181
|
order_by: toRestOrderBy(sort),
|
|
114
182
|
limit: PAGE_SIZE * pages,
|
|
115
183
|
depth: 1,
|
|
116
184
|
}),
|
|
117
|
-
[folderId, filters, searchQuery, sort, pages],
|
|
185
|
+
[folderId, view, filters, searchQuery, sort, pages],
|
|
118
186
|
);
|
|
119
187
|
const { data, setData, loading, error, reload } = useAsync(
|
|
120
188
|
() => listRecords<DocflowDocument>('smbDocuments', listQuery),
|
|
121
189
|
[listQuery],
|
|
122
190
|
);
|
|
191
|
+
// The counts of the views follow the folder and the search
|
|
192
|
+
const { data: counts, reload: reloadCounts } = useAsync(
|
|
193
|
+
async () =>
|
|
194
|
+
Object.fromEntries(
|
|
195
|
+
await Promise.all(
|
|
196
|
+
VIEWS.map(async ({ value }) => [
|
|
197
|
+
value,
|
|
198
|
+
await countRecords(
|
|
199
|
+
'smbDocuments',
|
|
200
|
+
restAnd(
|
|
201
|
+
folderId !== ALL_FOLDERS ? restEq('folderId', folderId) : undefined,
|
|
202
|
+
viewFilter(value),
|
|
203
|
+
searchFilter(searchQuery),
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
]),
|
|
207
|
+
),
|
|
208
|
+
) as Record<DocumentView, number>,
|
|
209
|
+
[folderId, searchQuery],
|
|
210
|
+
);
|
|
123
211
|
const documents = data?.records ?? [];
|
|
124
|
-
const
|
|
125
|
-
|
|
212
|
+
const selection = useRowSelection(documents.map(({ id }) => id));
|
|
213
|
+
const selectedIds = selection.selectedIds;
|
|
214
|
+
const activeFilterCount = (filters.origin !== ANY ? 1 : 0) + (filters.type !== ANY ? 1 : 0);
|
|
215
|
+
const folderName = (id: string | null | undefined) => folders?.find((folder) => folder.id === id)?.name?.trim();
|
|
216
|
+
|
|
217
|
+
const reloadAll = async () => {
|
|
218
|
+
await Promise.all([reload(), reloadCounts()]);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const bulk = useBulkAction({
|
|
222
|
+
onDone: async () => {
|
|
223
|
+
selection.clear();
|
|
224
|
+
setBulkDialogOpen(false);
|
|
225
|
+
await reloadAll();
|
|
226
|
+
},
|
|
227
|
+
});
|
|
126
228
|
|
|
127
229
|
const changeFilters = (patch: Partial<DocumentFilters>) => {
|
|
128
230
|
setFilters((current) => ({ ...current, ...patch }));
|
|
@@ -183,7 +285,7 @@ const DocflowDocuments = () => {
|
|
|
183
285
|
throw new Error('документ создан, но сервер его не вернул');
|
|
184
286
|
}
|
|
185
287
|
|
|
186
|
-
void
|
|
288
|
+
void reloadAll();
|
|
187
289
|
await openDocument(document.id);
|
|
188
290
|
} catch (createError) {
|
|
189
291
|
logDocflowError('documents: create failed', createError);
|
|
@@ -193,27 +295,20 @@ const DocflowDocuments = () => {
|
|
|
193
295
|
}
|
|
194
296
|
};
|
|
195
297
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await reload();
|
|
201
|
-
} catch (updateError) {
|
|
202
|
-
logDocflowError('documents: update failed', updateError, { documentId });
|
|
203
|
-
await notifyError(updateError, 'Не удалось изменить документ');
|
|
204
|
-
}
|
|
205
|
-
};
|
|
298
|
+
// One change for the given documents: a row's menu passes one id, the
|
|
299
|
+
// selection bar all checked ones.
|
|
300
|
+
const changeDocuments = (ids: string[], patch: Record<string, unknown>, done: string) =>
|
|
301
|
+
bulk.run(() => updateRecords('smbDocuments', ids, patch), done, 'Не удалось изменить документы');
|
|
206
302
|
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
};
|
|
303
|
+
const removeDocuments = (ids: string[]) =>
|
|
304
|
+
bulk.run(
|
|
305
|
+
() => deleteRecords('smbDocuments', ids),
|
|
306
|
+
ids.length === 1 ? 'Документ удалён: он в «Удалённых»' : `Удалено документов: ${ids.length}, они в «Удалённых»`,
|
|
307
|
+
'Не удалось удалить',
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const restoreDocuments = (ids: string[]) =>
|
|
311
|
+
bulk.run(() => restoreRecords('smbDocuments', ids), `Восстановлено: ${ids.length}`, 'Не удалось восстановить');
|
|
217
312
|
|
|
218
313
|
const handleCreateFolder = async (appearance: FolderAppearance) => {
|
|
219
314
|
try {
|
|
@@ -257,6 +352,35 @@ const DocflowDocuments = () => {
|
|
|
257
352
|
}
|
|
258
353
|
};
|
|
259
354
|
|
|
355
|
+
// The menus that change several documents at once: status, type, folder
|
|
356
|
+
const statusActions = (ids: string[], current?: string | null): MenuAction[] =>
|
|
357
|
+
STATUS_VALUES.map((status) => ({
|
|
358
|
+
key: `status-${status}`,
|
|
359
|
+
section: 'Статус',
|
|
360
|
+
label: DOCUMENT_STATUS_LABELS[status],
|
|
361
|
+
isSelected: status === current,
|
|
362
|
+
onSelect: () => void changeDocuments(ids, { status }, `Статус: ${DOCUMENT_STATUS_LABELS[status]}`),
|
|
363
|
+
}));
|
|
364
|
+
|
|
365
|
+
const folderActions = (ids: string[], current?: string | null): MenuAction[] => [
|
|
366
|
+
...(folders ?? []).map((folder) => ({
|
|
367
|
+
key: `folder-${folder.id}`,
|
|
368
|
+
section: 'В папку',
|
|
369
|
+
label: folder.name?.trim() || 'Без названия',
|
|
370
|
+
icon: IconFolderSymlink,
|
|
371
|
+
isSelected: folder.id === current,
|
|
372
|
+
onSelect: () =>
|
|
373
|
+
void changeDocuments(ids, { folderId: folder.id }, `Перемещено в «${folder.name?.trim() || 'Без названия'}»`),
|
|
374
|
+
})),
|
|
375
|
+
{
|
|
376
|
+
key: 'unfile',
|
|
377
|
+
section: 'В папку',
|
|
378
|
+
label: 'Без папки',
|
|
379
|
+
icon: IconFolderSymlink,
|
|
380
|
+
onSelect: () => void changeDocuments(ids, { folderId: null }, 'Убрано из папки'),
|
|
381
|
+
},
|
|
382
|
+
];
|
|
383
|
+
|
|
260
384
|
const documentActions = (documentId: string): MenuAction[] => {
|
|
261
385
|
const document = documents.find(({ id }) => id === documentId);
|
|
262
386
|
|
|
@@ -264,54 +388,39 @@ const DocflowDocuments = () => {
|
|
|
264
388
|
return [];
|
|
265
389
|
}
|
|
266
390
|
|
|
267
|
-
if (
|
|
391
|
+
if (view === 'deleted') {
|
|
268
392
|
return [
|
|
269
|
-
{ key: 'restore', label: '
|
|
393
|
+
{ key: 'restore', label: 'Восстановить', icon: IconRestore, onSelect: () => void restoreDocuments([documentId]) },
|
|
394
|
+
{ key: 'open', label: 'Открыть карточку', icon: IconExternalLink, onSelect: () => void openDocument(documentId) },
|
|
270
395
|
];
|
|
271
396
|
}
|
|
272
397
|
|
|
273
398
|
return [
|
|
274
399
|
{ key: 'open', label: 'Открыть карточку', icon: IconExternalLink, onSelect: () => void openDocument(documentId) },
|
|
275
400
|
{ key: 'rename', label: 'Переименовать', icon: IconPencil, onSelect: () => setRenamingDocument(document) },
|
|
276
|
-
...(folders ?? [])
|
|
277
|
-
.filter((folder) => folder.id !== document.folderId)
|
|
278
|
-
.map((folder) => ({
|
|
279
|
-
key: `move-${folder.id}`,
|
|
280
|
-
label: `В папку «${folder.name?.trim() || 'Без названия'}»`,
|
|
281
|
-
icon: IconFolderSymlink,
|
|
282
|
-
onSelect: () =>
|
|
283
|
-
void handleUpdateDocument(documentId, { folderId: folder.id }, `Документ перемещён в «${folder.name ?? ''}»`),
|
|
284
|
-
})),
|
|
285
|
-
...(typeof document.folderId === 'string' && document.folderId.length > 0
|
|
286
|
-
? [
|
|
287
|
-
{
|
|
288
|
-
key: 'unfile',
|
|
289
|
-
label: 'Убрать из папки',
|
|
290
|
-
icon: IconFolderSymlink,
|
|
291
|
-
onSelect: () => void handleUpdateDocument(documentId, { folderId: null }, 'Документ убран из папки'),
|
|
292
|
-
},
|
|
293
|
-
]
|
|
294
|
-
: []),
|
|
295
401
|
{
|
|
296
402
|
key: 'delete',
|
|
297
403
|
label: 'Удалить',
|
|
298
404
|
confirmLabel: 'Точно удалить?',
|
|
299
405
|
icon: IconTrash,
|
|
300
|
-
tone: 'danger'
|
|
301
|
-
onSelect: () => void
|
|
406
|
+
tone: 'danger',
|
|
407
|
+
onSelect: () => void removeDocuments([documentId]),
|
|
302
408
|
},
|
|
409
|
+
...statusActions([documentId], document.status),
|
|
410
|
+
...folderActions([documentId], document.folderId),
|
|
303
411
|
];
|
|
304
412
|
};
|
|
305
413
|
|
|
306
414
|
return (
|
|
307
415
|
<DocflowRoot>
|
|
308
416
|
<div style={{ display: 'flex', flex: 1, flexDirection: 'column', minHeight: 0, overflowY: 'auto' }}>
|
|
309
|
-
<div style={{ display: 'flex', flex: 1, flexWrap: 'wrap', minHeight: 0 }}>
|
|
417
|
+
<div style={{ alignItems: 'flex-start', display: 'flex', flex: 1, flexWrap: 'wrap', minHeight: 0 }}>
|
|
310
418
|
<div
|
|
311
419
|
style={{
|
|
312
420
|
boxSizing: 'border-box',
|
|
313
421
|
flex: '1 1 200px',
|
|
314
422
|
maxWidth: '240px',
|
|
423
|
+
minWidth: 0,
|
|
315
424
|
padding: `${theme.spacing[4]} ${theme.spacing[3]} ${theme.spacing[4]} ${theme.spacing[5]}`,
|
|
316
425
|
}}
|
|
317
426
|
>
|
|
@@ -322,6 +431,7 @@ const DocflowDocuments = () => {
|
|
|
322
431
|
onSelect={(id) => {
|
|
323
432
|
setFolderId(id);
|
|
324
433
|
setPages(1);
|
|
434
|
+
selection.clear();
|
|
325
435
|
}}
|
|
326
436
|
onCreate={handleCreateFolder}
|
|
327
437
|
onUpdate={handleUpdateFolder}
|
|
@@ -329,26 +439,23 @@ const DocflowDocuments = () => {
|
|
|
329
439
|
/>
|
|
330
440
|
</div>
|
|
331
441
|
<div style={{ display: 'flex', flex: '999 1 420px', flexDirection: 'column', minWidth: 0 }}>
|
|
332
|
-
<
|
|
333
|
-
|
|
334
|
-
value
|
|
335
|
-
onChange
|
|
336
|
-
placeholder
|
|
337
|
-
activeFilterCount
|
|
338
|
-
onResetFilters
|
|
339
|
-
filters
|
|
442
|
+
<ListToolbar
|
|
443
|
+
search={{
|
|
444
|
+
value: searchInput,
|
|
445
|
+
onChange: setSearchInput,
|
|
446
|
+
placeholder: 'Название, номер, где загружен',
|
|
447
|
+
activeFilterCount,
|
|
448
|
+
onResetFilters: () => changeFilters(DEFAULT_FILTERS),
|
|
449
|
+
filters: [
|
|
340
450
|
{
|
|
341
|
-
key: '
|
|
342
|
-
title: '
|
|
343
|
-
value: filters.
|
|
451
|
+
key: 'type',
|
|
452
|
+
title: 'Тип',
|
|
453
|
+
value: filters.type,
|
|
344
454
|
options: [
|
|
345
455
|
{ value: ANY, label: 'Любой' },
|
|
346
|
-
{ value
|
|
347
|
-
{ value: 'IN_REVIEW', label: 'На согласовании' },
|
|
348
|
-
{ value: 'APPROVED', label: 'Согласованные' },
|
|
349
|
-
{ value: 'ARCHIVED', label: 'В архиве' },
|
|
456
|
+
...Object.entries(DOCUMENT_TYPE_LABELS).map(([value, label]) => ({ value, label })),
|
|
350
457
|
],
|
|
351
|
-
onChange: (value) => changeFilters({
|
|
458
|
+
onChange: (value) => changeFilters({ type: value }),
|
|
352
459
|
},
|
|
353
460
|
{
|
|
354
461
|
key: 'origin',
|
|
@@ -361,70 +468,171 @@ const DocflowDocuments = () => {
|
|
|
361
468
|
],
|
|
362
469
|
onChange: (value) => changeFilters({ origin: value }),
|
|
363
470
|
},
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
471
|
+
],
|
|
472
|
+
}}
|
|
473
|
+
views={
|
|
474
|
+
<ViewTabs
|
|
475
|
+
value={view}
|
|
476
|
+
onChange={(next) => {
|
|
477
|
+
setView(next);
|
|
478
|
+
setPages(1);
|
|
479
|
+
selection.clear();
|
|
480
|
+
}}
|
|
481
|
+
options={VIEWS.map((option) => ({ ...option, count: counts?.[option.value] }))}
|
|
482
|
+
/>
|
|
483
|
+
}
|
|
484
|
+
total={data === undefined ? undefined : formatTotal(documents.length, data.totalCount, DOCUMENT_NOUN)}
|
|
485
|
+
columns={{ columns: COLUMNS, hiddenKeys: hiddenColumns, onChange: setHiddenColumns }}
|
|
486
|
+
onRefresh={() => void reloadAll()}
|
|
487
|
+
isRefreshing={loading}
|
|
488
|
+
onCreate={() => void handleCreateDocument()}
|
|
489
|
+
isCreating={isCreating}
|
|
490
|
+
/>
|
|
382
491
|
{error !== undefined && (
|
|
383
492
|
<div style={{ padding: theme.spacing[4] }}>
|
|
384
493
|
<Message tone="danger">{getErrorMessage(error, 'Не удалось загрузить документы')}</Message>
|
|
385
494
|
</div>
|
|
386
495
|
)}
|
|
387
|
-
{loading &&
|
|
496
|
+
{loading && data === undefined && <Message>Загрузка…</Message>}
|
|
388
497
|
{!loading && error === undefined && documents.length === 0 && <Message>Документов нет.</Message>}
|
|
389
498
|
{documents.length > 0 && (
|
|
390
499
|
<DataTable
|
|
391
|
-
columns={
|
|
392
|
-
|
|
393
|
-
{ key: 'type', label: 'Тип', width: '14%', sortKey: 'documentType' },
|
|
394
|
-
{ key: 'number', label: 'Номер', width: '12%', sortKey: 'number' },
|
|
395
|
-
{ key: 'source', label: 'Где загружен', width: '24%', sortKey: 'sourceRecordName' },
|
|
396
|
-
{ key: 'createdAt', label: 'Добавлен', sortKey: 'createdAt' },
|
|
397
|
-
]}
|
|
500
|
+
columns={COLUMNS}
|
|
501
|
+
hiddenColumnKeys={hiddenColumns}
|
|
398
502
|
sort={sort}
|
|
399
503
|
onSortChange={(next) => {
|
|
400
504
|
setSort(next);
|
|
401
505
|
setPages(1);
|
|
402
506
|
}}
|
|
403
507
|
selectedRowId={openedDocumentId}
|
|
508
|
+
selectedIds={selectedIds}
|
|
509
|
+
onSelectionChange={selection.setSelectedIds}
|
|
404
510
|
onRowClick={(documentId) => void openDocument(documentId)}
|
|
405
511
|
rowActions={documentActions}
|
|
406
|
-
rows={documents.map((document) =>
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
512
|
+
rows={documents.map((document) => {
|
|
513
|
+
const fileCount = document.file?.length ?? 0;
|
|
514
|
+
const folder = folders?.find(({ id }) => id === document.folderId);
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
id: document.id,
|
|
518
|
+
cells: {
|
|
519
|
+
name: (
|
|
520
|
+
<span style={{ alignItems: 'center', display: 'inline-flex', gap: theme.spacing[1], maxWidth: '100%' }}>
|
|
521
|
+
<IconFileText size={theme.icon.size.md} stroke={theme.icon.stroke.sm} color={theme.font.color.tertiary} />
|
|
522
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{document.name?.trim() || 'Без названия'}</span>
|
|
523
|
+
</span>
|
|
524
|
+
),
|
|
525
|
+
type: DOCUMENT_TYPE_LABELS[document.documentType ?? ''] ?? '—',
|
|
526
|
+
status: document.status ? (
|
|
527
|
+
<StatusChip color={DOCUMENT_STATUS_COLORS[document.status] ?? 'gray'}>
|
|
528
|
+
{DOCUMENT_STATUS_LABELS[document.status] ?? document.status}
|
|
529
|
+
</StatusChip>
|
|
530
|
+
) : (
|
|
531
|
+
'—'
|
|
532
|
+
),
|
|
533
|
+
number: document.number || '—',
|
|
534
|
+
documentDate: formatDate(document.documentDate) || '—',
|
|
535
|
+
counterparty: document.counterparty?.name || '—',
|
|
536
|
+
responsible: formatMemberName(document.responsible ?? undefined) || '—',
|
|
537
|
+
folder: folder ? (
|
|
538
|
+
<span style={{ alignItems: 'center', display: 'inline-flex', gap: theme.spacing[1] }}>
|
|
539
|
+
<span style={{ background: folderColor(folder.color), flexShrink: 0, height: '8px', width: '8px' }} />
|
|
540
|
+
{folderName(folder.id) || 'Без названия'}
|
|
541
|
+
</span>
|
|
542
|
+
) : (
|
|
543
|
+
'—'
|
|
544
|
+
),
|
|
545
|
+
source: document.sourceRecordName || DOCUMENT_ORIGIN_LABELS[document.origin ?? ''] || '—',
|
|
546
|
+
file: fileCount > 0 ? `${fileCount} шт.` : 'нет',
|
|
547
|
+
createdAt: formatDate(document.createdAt),
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
})}
|
|
416
551
|
/>
|
|
417
552
|
)}
|
|
418
|
-
{data?.pageInfo.hasNextPage === true
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
553
|
+
<ShowMore isVisible={data?.pageInfo.hasNextPage === true} loading={loading} onClick={() => setPages((current) => current + 1)} />
|
|
554
|
+
<SelectionBar count={selectedIds.length} noun={DOCUMENT_NOUN} onClear={selection.clear}>
|
|
555
|
+
{view === 'deleted' ? (
|
|
556
|
+
<Button icon={IconRestore} loading={bulk.isBusy} onClick={() => void restoreDocuments(selectedIds)}>
|
|
557
|
+
Восстановить
|
|
422
558
|
</Button>
|
|
423
|
-
|
|
424
|
-
|
|
559
|
+
) : (
|
|
560
|
+
<>
|
|
561
|
+
<Button icon={IconPencil} disabled={bulk.isBusy} onClick={() => setBulkDialogOpen(true)}>
|
|
562
|
+
Изменить
|
|
563
|
+
</Button>
|
|
564
|
+
<MenuButton
|
|
565
|
+
label="Удалить"
|
|
566
|
+
icon={IconTrash}
|
|
567
|
+
disabled={bulk.isBusy}
|
|
568
|
+
actions={[
|
|
569
|
+
{
|
|
570
|
+
key: 'delete',
|
|
571
|
+
label: `Удалить выбранные (${selectedIds.length})`,
|
|
572
|
+
confirmLabel: 'Точно удалить?',
|
|
573
|
+
icon: IconTrash,
|
|
574
|
+
tone: 'danger',
|
|
575
|
+
onSelect: () => void removeDocuments(selectedIds),
|
|
576
|
+
},
|
|
577
|
+
]}
|
|
578
|
+
/>
|
|
579
|
+
</>
|
|
580
|
+
)}
|
|
581
|
+
</SelectionBar>
|
|
425
582
|
</div>
|
|
426
583
|
</div>
|
|
427
584
|
</div>
|
|
585
|
+
{bulkDialogOpen && (
|
|
586
|
+
<BulkChangeDialog
|
|
587
|
+
title="Изменить поле"
|
|
588
|
+
hint={`Для ${selectedIds.length} ${pluralize(selectedIds.length, DOCUMENT_NOUN)}`}
|
|
589
|
+
fields={[
|
|
590
|
+
{
|
|
591
|
+
key: 'status',
|
|
592
|
+
label: 'Статус',
|
|
593
|
+
options: STATUS_VALUES.map((status) => ({ value: status, label: DOCUMENT_STATUS_LABELS[status] })),
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
key: 'documentType',
|
|
597
|
+
label: 'Тип',
|
|
598
|
+
options: Object.entries(DOCUMENT_TYPE_LABELS).map(([type, label]) => ({ value: type, label })),
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
key: 'folderId',
|
|
602
|
+
label: 'Папка',
|
|
603
|
+
options: [
|
|
604
|
+
...(folders ?? []).map((folder) => ({
|
|
605
|
+
value: folder.id,
|
|
606
|
+
label: folder.name?.trim() || 'Без названия',
|
|
607
|
+
})),
|
|
608
|
+
{ value: '', label: 'Без папки' },
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
]}
|
|
612
|
+
onClose={() => setBulkDialogOpen(false)}
|
|
613
|
+
onSubmit={(fieldKey, value) => {
|
|
614
|
+
if (fieldKey === 'status') {
|
|
615
|
+
return changeDocuments(selectedIds, { status: value }, `Статус: ${DOCUMENT_STATUS_LABELS[value]}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (fieldKey === 'documentType') {
|
|
619
|
+
return changeDocuments(
|
|
620
|
+
selectedIds,
|
|
621
|
+
{ documentType: value },
|
|
622
|
+
`Тип: ${DOCUMENT_TYPE_LABELS[value] ?? value}`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return changeDocuments(
|
|
627
|
+
selectedIds,
|
|
628
|
+
{ folderId: value.length > 0 ? value : null },
|
|
629
|
+
value.length > 0
|
|
630
|
+
? `Перемещено в «${(folders ?? []).find((folder) => folder.id === value)?.name?.trim() || 'Без названия'}»`
|
|
631
|
+
: 'Убрано из папки',
|
|
632
|
+
);
|
|
633
|
+
}}
|
|
634
|
+
/>
|
|
635
|
+
)}
|
|
428
636
|
{renamingDocument !== undefined && (
|
|
429
637
|
<PromptDialog
|
|
430
638
|
title="Переименовать документ"
|
|
@@ -432,7 +640,7 @@ const DocflowDocuments = () => {
|
|
|
432
640
|
initialValue={renamingDocument.name ?? ''}
|
|
433
641
|
onClose={() => setRenamingDocument(undefined)}
|
|
434
642
|
onSubmit={async (name) => {
|
|
435
|
-
await
|
|
643
|
+
await changeDocuments([renamingDocument.id], { name }, 'Документ переименован');
|
|
436
644
|
setRenamingDocument(undefined);
|
|
437
645
|
}}
|
|
438
646
|
/>
|