@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.
@@ -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 { Button } from 'twenty-ui/primitives/input';
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 ORDER_BY: Record<TaskSort, string> = {
49
- newest: 'createdAt[DescNullsLast]',
50
- oldest: 'createdAt[AscNullsLast]',
51
- dueSoon: 'dueAt[AscNullsLast]',
52
- nameAsc: 'title[AscNullsLast]',
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<TaskSort>('newest');
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: ORDER_BY[sort],
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
- <div style={{ flex: '0 1 240px' }}>
226
- <TextField
227
- type="search"
228
- variant="bare"
229
- placeholder="Поиск заданий"
230
- value={searchInput}
231
- onChange={setSearchInput}
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
- <SelectField<TaskSort>
250
- variant="bare"
251
- value={sort}
252
- options={[
253
- { value: 'newest', label: 'Сначала новые' },
254
- { value: 'oldest', label: 'Сначала старые' },
255
- { value: 'dueSoon', label: 'По сроку' },
256
- { value: 'nameAsc', label: 'По названию' },
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="outline" size="sm" onClick={() => setPanel({ kind: 'create' })}>
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: '40%' },
277
- { key: 'assignee', label: 'Исполнитель', width: '24%' },
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)