@haiiro/docuflow 0.0.9 → 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,7 +1,7 @@
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
 
@@ -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,7 +200,7 @@ 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],
@@ -233,43 +239,30 @@ const DocflowTasks = () => {
233
239
  }
234
240
  >
235
241
  <Toolbar>
236
- <div style={{ flex: '0 1 240px' }}>
237
- <TextField
238
- type="search"
239
- variant="bare"
240
- placeholder="Поиск заданий"
241
- value={searchInput}
242
- onChange={setSearchInput}
243
- />
244
- </div>
245
- <SelectField<TaskFilter>
246
- variant="bare"
247
- value={taskFilter}
248
- options={[
249
- { value: 'open', label: 'Открытые' },
250
- { value: 'mine', label: 'Мои' },
251
- { value: 'overdue', label: 'Просроченные' },
252
- { value: 'done', label: 'Выполненные' },
253
- { value: 'archived', label: 'Архив' },
254
- ]}
255
- onChange={(value) => {
256
- setTaskFilter(value);
242
+ <SearchWithFilters
243
+ value={searchInput}
244
+ onChange={setSearchInput}
245
+ placeholder="Поиск заданий"
246
+ activeFilterCount={taskFilter === 'open' ? 0 : 1}
247
+ onResetFilters={() => {
248
+ setTaskFilter('open');
257
249
  setPages(1);
258
250
  }}
259
- />
260
- <SelectField<TaskSort>
261
- variant="bare"
262
- value={sort}
263
- options={[
264
- { value: 'newest', label: 'Сначала новые' },
265
- { value: 'oldest', label: 'Сначала старые' },
266
- { value: 'dueSoon', label: 'По сроку' },
267
- { 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
+ },
268
262
  ]}
269
- onChange={setSort}
270
263
  />
271
264
  <div style={{ marginLeft: 'auto' }}>
272
- <Button variant="outline" size="sm" onClick={() => setPanel({ kind: 'create' })}>
265
+ <Button variant="solid" size="sm" onClick={() => setPanel({ kind: 'create' })}>
273
266
  Создать
274
267
  </Button>
275
268
  </div>
@@ -284,13 +277,30 @@ const DocflowTasks = () => {
284
277
  {tasks.length > 0 && (
285
278
  <DataTable
286
279
  columns={[
287
- { key: 'title', label: 'Задание', width: '34%' },
288
- { key: 'assignee', label: 'Исполнитель', width: '20%' },
289
- { key: 'dueAt', label: 'Срок', width: '14%' },
290
- { 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' },
291
284
  ]}
285
+ sort={sort}
286
+ onSortChange={setSort}
292
287
  selectedRowId={panel.kind === 'task' ? panel.taskId : undefined}
293
- onDeleteRow={taskFilter === 'archived' ? undefined : handleDeleteTask}
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
+ }
294
304
  onRowClick={(taskId) =>
295
305
  taskFilter === 'archived'
296
306
  ? void openArchivedTask(taskId)