@haiiro/docuflow 0.0.8 → 0.0.10
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 +13 -1
- package/manifest.json +108 -25
- package/package.json +1 -1
- package/src/front-components/docflow-documents.front-component.mjs +35 -35
- package/src/front-components/docflow-documents.front-component.tsx +216 -88
- package/src/front-components/docflow-processes.front-component.mjs +42 -42
- package/src/front-components/docflow-processes.front-component.tsx +170 -61
- package/src/front-components/docflow-tasks.front-component.mjs +42 -42
- package/src/front-components/docflow-tasks.front-component.tsx +69 -47
- package/src/front-components/process-steps.front-component.mjs +36 -36
- package/src/front-components/process-steps.front-component.tsx +28 -2
- package/src/front-components/task-form.front-component.mjs +36 -36
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { 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 { IconExternalLink, IconEye, IconRestore, IconTrash } from 'twenty-ui/icon';
|
|
5
5
|
import { Tag } from 'twenty-ui/primitives/data-display';
|
|
6
6
|
import { useTheme } from 'twenty-ui/theme';
|
|
7
7
|
|
|
8
|
-
import { createRecord, getErrorMessage, listRecords } from 'src/front/api';
|
|
8
|
+
import { createRecord, deleteRecord, getErrorMessage, listRecords } from 'src/front/api';
|
|
9
9
|
import { formatDate, formatMemberName, isOverdue, TASK_STATUS_LABELS } from 'src/front/format';
|
|
10
10
|
import { useAsync, useDebouncedValue } from 'src/front/hooks/use-async';
|
|
11
11
|
import { useWorkspaceMembers } from 'src/front/hooks/use-workspace-members';
|
|
@@ -19,9 +19,12 @@ import {
|
|
|
19
19
|
SplitView,
|
|
20
20
|
TextAreaField,
|
|
21
21
|
TextField,
|
|
22
|
+
type TableSort,
|
|
23
|
+
toRestOrderBy,
|
|
22
24
|
Toolbar,
|
|
23
25
|
} from 'src/front/ui/components';
|
|
24
26
|
import { FormFieldsBuilder } from 'src/front/ui/form-fields-builder';
|
|
27
|
+
import { Button, SearchWithFilters } from 'src/front/ui/kit';
|
|
25
28
|
import { TaskWorkPanel } from 'src/front/ui/task-work-panel';
|
|
26
29
|
import {
|
|
27
30
|
restAnd,
|
|
@@ -39,23 +42,26 @@ import { getFormFieldsError } from 'src/shared/form/form-fields.util';
|
|
|
39
42
|
import { TASKS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/universal-identifiers';
|
|
40
43
|
|
|
41
44
|
type TaskFilter = 'open' | 'mine' | 'overdue' | 'done' | 'archived';
|
|
42
|
-
type TaskSort = 'newest' | 'oldest' | 'dueSoon' | 'nameAsc';
|
|
43
45
|
type Panel = { kind: 'closed' } | { kind: 'create' } | { kind: 'task'; taskId: string };
|
|
44
46
|
|
|
45
47
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
46
48
|
const NO_ASSIGNEE = '';
|
|
47
49
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
50
|
+
const TASK_FILTER_OPTIONS: { value: TaskFilter; label: string }[] = [
|
|
51
|
+
{ value: 'open', label: 'Открытые' },
|
|
52
|
+
{ value: 'mine', label: 'Мои' },
|
|
53
|
+
{ value: 'overdue', label: 'Просроченные' },
|
|
54
|
+
{ value: 'done', label: 'Выполненные' },
|
|
55
|
+
{ value: 'archived', label: 'Архив (удалённые)' },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const DEFAULT_SORT: TableSort = { key: 'createdAt', direction: 'desc' };
|
|
54
59
|
|
|
55
60
|
const OPEN_FILTER = restOr(restNeq('status', TASK_DONE_STATUS), restIsNull('status'));
|
|
56
61
|
|
|
57
62
|
// An archived task opens in its standard card: Twenty shows it as deleted
|
|
58
63
|
// there and offers to restore it.
|
|
64
|
+
// A task opens in its standard card too; a deleted one shows "Восстановить" there
|
|
59
65
|
const openArchivedTask = (taskId: string) =>
|
|
60
66
|
openSidePanelPage({
|
|
61
67
|
to: AppPath.RecordShowPage,
|
|
@@ -184,7 +190,7 @@ const DocflowTasks = () => {
|
|
|
184
190
|
const [searchInput, setSearchInput] = useState('');
|
|
185
191
|
const searchQuery = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
|
|
186
192
|
const [taskFilter, setTaskFilter] = useState<TaskFilter>('open');
|
|
187
|
-
const [sort, setSort] = useState<
|
|
193
|
+
const [sort, setSort] = useState<TableSort>(DEFAULT_SORT);
|
|
188
194
|
const [panel, setPanel] = useState<Panel>({ kind: 'closed' });
|
|
189
195
|
const [pages, setPages] = useState(1);
|
|
190
196
|
// Fixed for the life of the list, so "overdue" does not refetch by itself.
|
|
@@ -194,11 +200,22 @@ const DocflowTasks = () => {
|
|
|
194
200
|
() =>
|
|
195
201
|
listRecords<DocflowTask>('tasks', {
|
|
196
202
|
filter: buildFilter({ taskFilter, searchQuery, currentMemberId: currentMember?.id, now }),
|
|
197
|
-
order_by:
|
|
203
|
+
order_by: toRestOrderBy(sort),
|
|
198
204
|
limit: PAGE_SIZE * pages,
|
|
199
205
|
}),
|
|
200
206
|
[taskFilter, searchQuery, currentMember?.id, sort, pages, now],
|
|
201
207
|
);
|
|
208
|
+
|
|
209
|
+
const handleDeleteTask = async (taskId: string) => {
|
|
210
|
+
try {
|
|
211
|
+
await deleteRecord('tasks', taskId);
|
|
212
|
+
setPanel((current) => (current.kind === 'task' && current.taskId === taskId ? { kind: 'closed' } : current));
|
|
213
|
+
await enqueueSnackbar({ message: 'Задание удалено: оно в фильтре «Архив»', variant: 'success' });
|
|
214
|
+
await reload();
|
|
215
|
+
} catch (deleteError) {
|
|
216
|
+
await enqueueSnackbar({ message: getErrorMessage(deleteError, 'Не удалось удалить задание'), variant: 'error' });
|
|
217
|
+
}
|
|
218
|
+
};
|
|
202
219
|
const tasks = data?.records ?? [];
|
|
203
220
|
|
|
204
221
|
return (
|
|
@@ -222,43 +239,30 @@ const DocflowTasks = () => {
|
|
|
222
239
|
}
|
|
223
240
|
>
|
|
224
241
|
<Toolbar>
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
/>
|
|
233
|
-
</div>
|
|
234
|
-
<SelectField<TaskFilter>
|
|
235
|
-
variant="bare"
|
|
236
|
-
value={taskFilter}
|
|
237
|
-
options={[
|
|
238
|
-
{ value: 'open', label: 'Открытые' },
|
|
239
|
-
{ value: 'mine', label: 'Мои' },
|
|
240
|
-
{ value: 'overdue', label: 'Просроченные' },
|
|
241
|
-
{ value: 'done', label: 'Выполненные' },
|
|
242
|
-
{ value: 'archived', label: 'Архив' },
|
|
243
|
-
]}
|
|
244
|
-
onChange={(value) => {
|
|
245
|
-
setTaskFilter(value);
|
|
242
|
+
<SearchWithFilters
|
|
243
|
+
value={searchInput}
|
|
244
|
+
onChange={setSearchInput}
|
|
245
|
+
placeholder="Поиск заданий"
|
|
246
|
+
activeFilterCount={taskFilter === 'open' ? 0 : 1}
|
|
247
|
+
onResetFilters={() => {
|
|
248
|
+
setTaskFilter('open');
|
|
246
249
|
setPages(1);
|
|
247
250
|
}}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
filters={[
|
|
252
|
+
{
|
|
253
|
+
key: 'tasks',
|
|
254
|
+
title: 'Показать',
|
|
255
|
+
value: taskFilter,
|
|
256
|
+
options: TASK_FILTER_OPTIONS,
|
|
257
|
+
onChange: (value) => {
|
|
258
|
+
setTaskFilter(value as TaskFilter);
|
|
259
|
+
setPages(1);
|
|
260
|
+
},
|
|
261
|
+
},
|
|
257
262
|
]}
|
|
258
|
-
onChange={setSort}
|
|
259
263
|
/>
|
|
260
264
|
<div style={{ marginLeft: 'auto' }}>
|
|
261
|
-
<Button variant="
|
|
265
|
+
<Button variant="solid" size="sm" onClick={() => setPanel({ kind: 'create' })}>
|
|
262
266
|
Создать
|
|
263
267
|
</Button>
|
|
264
268
|
</div>
|
|
@@ -273,12 +277,30 @@ const DocflowTasks = () => {
|
|
|
273
277
|
{tasks.length > 0 && (
|
|
274
278
|
<DataTable
|
|
275
279
|
columns={[
|
|
276
|
-
{ key: 'title', label: 'Задание', width: '
|
|
277
|
-
{ key: 'assignee', label: 'Исполнитель', width: '
|
|
278
|
-
{ key: 'dueAt', label: 'Срок', width: '16%' },
|
|
279
|
-
{ key: 'status', label: 'Статус' },
|
|
280
|
+
{ key: 'title', label: 'Задание', width: '38%', sortKey: 'title' },
|
|
281
|
+
{ key: 'assignee', label: 'Исполнитель', width: '22%' },
|
|
282
|
+
{ key: 'dueAt', label: 'Срок', width: '16%', sortKey: 'dueAt' },
|
|
283
|
+
{ key: 'status', label: 'Статус', sortKey: 'status' },
|
|
280
284
|
]}
|
|
285
|
+
sort={sort}
|
|
286
|
+
onSortChange={setSort}
|
|
281
287
|
selectedRowId={panel.kind === 'task' ? panel.taskId : undefined}
|
|
288
|
+
rowActions={(taskId) =>
|
|
289
|
+
taskFilter === 'archived'
|
|
290
|
+
? [{ key: 'open', label: 'Открыть и восстановить', icon: IconRestore, onSelect: () => void openArchivedTask(taskId) }]
|
|
291
|
+
: [
|
|
292
|
+
{ key: 'open', label: 'Открыть', icon: IconEye, onSelect: () => setPanel({ kind: 'task', taskId }) },
|
|
293
|
+
{ key: 'card', label: 'Карточка задачи', icon: IconExternalLink, onSelect: () => void openArchivedTask(taskId) },
|
|
294
|
+
{
|
|
295
|
+
key: 'delete',
|
|
296
|
+
label: 'Удалить',
|
|
297
|
+
confirmLabel: 'Точно удалить?',
|
|
298
|
+
icon: IconTrash,
|
|
299
|
+
tone: 'danger',
|
|
300
|
+
onSelect: () => void handleDeleteTask(taskId),
|
|
301
|
+
},
|
|
302
|
+
]
|
|
303
|
+
}
|
|
282
304
|
onRowClick={(taskId) =>
|
|
283
305
|
taskFilter === 'archived'
|
|
284
306
|
? void openArchivedTask(taskId)
|