@xy-planning-network/trees 0.11.5 → 0.11.6-dev-2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xy-planning-network/trees",
3
- "version": "0.11.5",
3
+ "version": "0.11.6-dev-2",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "repository": "github:xy-planning-network/trees",
@@ -8,7 +8,9 @@ import type {
8
8
  DynamicTableAPI,
9
9
  DynamicTableOptions,
10
10
  TableActions,
11
+ TableBulkActions,
11
12
  TableColumns,
13
+ TableRowData,
12
14
  } from "@/composables/table"
13
15
  import { useAppFlasher } from "@/composables/useFlashes"
14
16
  import { TrailsRespPaged } from "@/api/client"
@@ -19,11 +21,13 @@ import TableActionButtons from "./TableActionButtons.vue"
19
21
  const props = withDefaults(
20
22
  defineProps<{
21
23
  tableActions?: TableActions<any>
24
+ tableBulkActions?: TableBulkActions<any>
22
25
  tableColumns: TableColumns<any>
23
26
  tableOptions: DynamicTableOptions
24
27
  }>(),
25
28
  {
26
29
  tableActions: () => ({ type: "dropdown", actions: [] }),
30
+ tableBulkActions: () => ({ actions: [] }),
27
31
  }
28
32
  )
29
33
 
@@ -42,6 +46,10 @@ const loadAndRender = (): void => {
42
46
  q: query.value,
43
47
  }
44
48
 
49
+ if (!selectionsPersisted.value) {
50
+ clearSelections()
51
+ }
52
+
45
53
  BaseAPI.get<TrailsRespPaged<unknown>>(
46
54
  props.tableOptions.url,
47
55
  { skipLoader: false },
@@ -54,7 +62,7 @@ const loadAndRender = (): void => {
54
62
  totalItems: success.data.totalItems,
55
63
  totalPages: success.data.totalPages,
56
64
  }
57
- tableData.value = success.data.items as Record<string, any>[]
65
+ tableData.value = success.data.items as TableRowData[]
58
66
  },
59
67
  () => {
60
68
  useAppFlasher.genericError()
@@ -81,19 +89,7 @@ const setDateRange = (): void => {
81
89
  }
82
90
  }
83
91
 
84
- const tableData = ref<Record<string, any>[]>([])
85
-
86
- const publicMethods: DynamicTableAPI = {
87
- refresh: loadAndRender,
88
- reset: reloadTable,
89
- }
90
-
91
- const { columns, hasActions, isEmptyCellValue, rows } = useTable(
92
- tableData,
93
- toRef(props, "tableColumns"),
94
- toRef(props, "tableActions"),
95
- publicMethods
96
- )
92
+ const tableData = ref<TableRowData[]>([])
97
93
 
98
94
  const currentSort = ref(
99
95
  props.tableOptions.defaultSort ? props.tableOptions.defaultSort : ""
@@ -145,6 +141,120 @@ const hasContent = computed((): boolean => {
145
141
  return rows.value.length ? true : false
146
142
  })
147
143
 
144
+ const selectionsPersisted = computed(() => {
145
+ return props.tableBulkActions.persistent ?? false
146
+ })
147
+
148
+ const clearSelections = () => {
149
+ selected.value = []
150
+ }
151
+
152
+ const selected = defineModel<number[]>("selected", {
153
+ required: false,
154
+ default: [],
155
+ })
156
+
157
+ const selectedData = computed<TableRowData[]>(() => {
158
+ return tableData.value.filter((data) => {
159
+ return selected.value.includes(data.id)
160
+ })
161
+ })
162
+
163
+ const selectedOnPage = computed(() => {
164
+ return selected.value.filter((id) => {
165
+ return selectableIds.value.includes(id)
166
+ }).length
167
+ })
168
+
169
+ const bulkActions = computed(() => {
170
+ return props.tableBulkActions.actions
171
+ .filter((action) => {
172
+ return action.show ?? true
173
+ })
174
+ .map((action) => {
175
+ return {
176
+ ...action,
177
+ disabled: selected.value.length === 0 || action.disabled,
178
+ onClick: () =>
179
+ action.onClick.apply(undefined, [
180
+ selected.value,
181
+ selectedData.value,
182
+ publicMethods,
183
+ ]),
184
+ }
185
+ })
186
+ })
187
+
188
+ const hasBulkActions = computed(() => bulkActions.value.length > 0)
189
+
190
+ const selectableIds = computed(() => {
191
+ return tableData.value
192
+ .filter((row) => {
193
+ if (props.tableBulkActions.isSelectable === undefined) {
194
+ return true
195
+ }
196
+
197
+ if (props.tableBulkActions.isSelectable(row)) {
198
+ return true
199
+ }
200
+
201
+ return false
202
+ })
203
+ .map((d) => d["id"])
204
+ })
205
+
206
+ const bulkSelectChecked = computed(
207
+ () =>
208
+ selected.value.length > 0 &&
209
+ selected.value.length === selectableIds.value.length
210
+ )
211
+
212
+ const bulkSelectIndeterminate = computed(
213
+ () =>
214
+ selected.value.length > 0 &&
215
+ selected.value.length < selectableIds.value.length
216
+ )
217
+
218
+ const bulkSelectOnChange = (e: Event) => {
219
+ const isChecked = (e.target as HTMLInputElement).checked
220
+
221
+ // append all records on current page to existing selection
222
+ if (selectionsPersisted.value && isChecked) {
223
+ selected.value = [...selected.value, ...selectableIds.value.map((id) => id)]
224
+ return
225
+ }
226
+
227
+ // remove all records on current page from existing selection
228
+ if (selectionsPersisted.value && !isChecked) {
229
+ selected.value = selected.value.filter((id) => {
230
+ return !selectableIds.value.includes(id)
231
+ })
232
+
233
+ return
234
+ }
235
+
236
+ // set all records on current page to selection
237
+ if (isChecked) {
238
+ selected.value = selectableIds.value.map((id) => id)
239
+ return
240
+ }
241
+
242
+ clearSelections()
243
+ }
244
+
245
+ const publicMethods: DynamicTableAPI = {
246
+ clearSelection: clearSelections,
247
+ refresh: loadAndRender,
248
+ reset: reloadTable,
249
+ }
250
+
251
+ const { columns, hasActions, isEmptyCellValue, rows } = useTable(
252
+ tableData,
253
+ toRef(props, "tableColumns"),
254
+ toRef(props, "tableActions"),
255
+ publicMethods
256
+ )
257
+
148
258
  watch(
149
259
  () => props.tableOptions.refreshTrigger,
150
260
  () => {
@@ -237,6 +347,21 @@ loadAndRender()
237
347
  <table class="min-w-full divide-y divide-gray-200">
238
348
  <thead class="bg-gray-100">
239
349
  <tr>
350
+ <th v-if="hasBulkActions" scope="col" class="pl-6 w-4 leading-none">
351
+ <input
352
+ :class="[
353
+ 'h-4 w-4 rounded cursor-pointer',
354
+ 'disabled:bg-gray-100 disabled:border-gray-200 disabled:cursor-not-allowed disabled:opacity-100',
355
+ 'checked:disabled:bg-xy-blue checked:disabled:border-xy-blue checked:disabled:opacity-50',
356
+ 'border-gray-300 focus:ring-xy-blue-500',
357
+ ]"
358
+ :checked="bulkSelectChecked"
359
+ :indeterminate="bulkSelectIndeterminate"
360
+ type="checkbox"
361
+ @change="bulkSelectOnChange"
362
+ />
363
+ </th>
364
+
240
365
  <th
241
366
  v-for="(col, idx) in columns"
242
367
  :key="idx"
@@ -303,6 +428,23 @@ loadAndRender()
303
428
  class="px-6 py-3 text-xs font-medium tracking-wider text-gray-900 uppercase leading-4"
304
429
  />
305
430
  </tr>
431
+
432
+ <tr v-if="hasBulkActions && selected.length > 0">
433
+ <td colspan="100%" class="px-6 py-3 border-t bg-neutral-50">
434
+ <div class="flex items-center space-x-3">
435
+ <div v-if="selectionsPersisted" class="text-sm font-semibold">
436
+ Selected: {{ selectedOnPage }} ({{ selected.length }} total)
437
+ </div>
438
+
439
+ <div v-else class="text-sm font-semibold">
440
+ {{ selected.length }}
441
+ selected
442
+ </div>
443
+
444
+ <TableActionButtons :actions="bulkActions" />
445
+ </div>
446
+ </td>
447
+ </tr>
306
448
  </thead>
307
449
 
308
450
  <tbody class="bg-white">
@@ -312,6 +454,21 @@ loadAndRender()
312
454
  class="even:bg-gray-50"
313
455
  @click="$emit('click:row', row.rowData)"
314
456
  >
457
+ <td v-if="hasBulkActions" class="pl-6 w-4 leading-none">
458
+ <input
459
+ v-model="selected"
460
+ :class="[
461
+ 'h-4 w-4 rounded cursor-pointer',
462
+ 'disabled:bg-gray-100 disabled:border-gray-200 disabled:cursor-not-allowed disabled:opacity-100',
463
+ 'checked:disabled:bg-xy-blue checked:disabled:border-xy-blue checked:disabled:opacity-50',
464
+ 'border-gray-300 focus:ring-xy-blue-500',
465
+ ]"
466
+ :disabled="!selectableIds.includes(row.rowData.id)"
467
+ type="checkbox"
468
+ :value="row.rowData.id"
469
+ />
470
+ </td>
471
+
315
472
  <template v-for="(cell, cellIdx) in row.cells" :key="cellIdx">
316
473
  <component
317
474
  :is="'td'"
@@ -5,13 +5,16 @@ import type { ActionItem } from "@/composables/nav"
5
5
  import { useActionItems } from "@/composables/useActionItems"
6
6
  import { toRef, useTemplateRef } from "vue"
7
7
  import { useFloating, autoUpdate } from "@floating-ui/vue"
8
+ import type { Placement } from "@floating-ui/vue"
8
9
 
9
10
  const props = withDefaults(
10
11
  defineProps<{
11
12
  actions?: ActionItem[]
13
+ placement?: Placement
12
14
  }>(),
13
15
  {
14
16
  actions: () => [],
17
+ placement: "bottom-end",
15
18
  }
16
19
  )
17
20
 
@@ -22,7 +25,7 @@ const { actions, hasActions } = useActionItems(toRef(props, "actions"))
22
25
  const triggerRef = useTemplateRef<InstanceType<typeof MenuButton>>("trigger")
23
26
  const wrapperRef = useTemplateRef<HTMLElement | null>("wrapper")
24
27
  const { floatingStyles } = useFloating(triggerRef, wrapperRef, {
25
- placement: "bottom-end",
28
+ placement: props.placement,
26
29
  strategy: "fixed",
27
30
  whileElementsMounted: autoUpdate,
28
31
  })
@@ -11,6 +11,7 @@ export interface DynamicTableOptions {
11
11
  url: string;
12
12
  }
13
13
  export interface DynamicTableAPI {
14
+ clearSelection: () => void;
14
15
  /**
15
16
  * Force refresh the table data with the current api params state
16
17
  * @returns void
@@ -48,6 +49,11 @@ export interface TableActionItem<T = TableRowData> extends ActionItem {
48
49
  */
49
50
  show?: boolean | ((rowData: T, rowIndex: number) => boolean);
50
51
  }
52
+ export interface TableBulkActionItem<T = TableRowData> extends ActionItem {
53
+ disabled?: boolean;
54
+ onClick: (selected: number[], selectedRows: T[], tableAPI: DynamicTableAPI) => void;
55
+ show?: boolean;
56
+ }
51
57
  export interface TableActions<T = TableRowData> {
52
58
  /**
53
59
  * an array of TableActionItem definitions
@@ -58,6 +64,14 @@ export interface TableActions<T = TableRowData> {
58
64
  */
59
65
  type: "dropdown" | "buttons";
60
66
  }
67
+ export interface TableBulkActions<T extends TableRowData = TableRowData> {
68
+ /**
69
+ * an array of TableActionItem definitions
70
+ */
71
+ actions: TableBulkActionItem<T>[];
72
+ persistent?: boolean;
73
+ isSelectable?: (data: T) => boolean;
74
+ }
61
75
  export interface TableColumn<T = TableRowData> {
62
76
  /**
63
77
  * The alignment the table cell should have
@@ -89,6 +103,12 @@ export interface TableColumn<T = TableRowData> {
89
103
  sort?: string;
90
104
  }
91
105
  export type TableCellAlignment = "left" | "center" | "right";
92
- export type TableRowData = Record<string, any>;
106
+ export type TableRowData<T extends {
107
+ [key: string]: any;
108
+ id: number;
109
+ } = {
110
+ [key: string]: any;
111
+ id: number;
112
+ }> = T;
93
113
  export type TableColumns<T = TableRowData> = TableColumn<T>[];
94
114
  export type TableRowsData = TableRowData[];
@@ -3,9 +3,15 @@ import { TableColumns, TableRowsData, TableActions, DynamicTableAPI } from "./ta
3
3
  export declare const useTable: (rowData: TableRowsData | Ref<TableRowsData>, cols: TableColumns | Ref<TableColumns>, acts: TableActions | Ref<TableActions>, exposedAPI?: DynamicTableAPI) => {
4
4
  columns: import("vue").ComputedRef<{
5
5
  alignment: string;
6
- classNames?: string | ((rowData: import("./table").TableRowData, rowIndex: number) => string) | undefined;
6
+ classNames?: string | ((rowData: {
7
+ [key: string]: any;
8
+ id: number;
9
+ }, rowIndex: number) => string) | undefined;
7
10
  title: string;
8
- render: string | ((rowData: import("./table").TableRowData, rowIndex: number) => import("vue").VNodeChild);
11
+ render: string | number | ((rowData: {
12
+ [key: string]: any;
13
+ id: number;
14
+ }, rowIndex: number) => import("vue").VNodeChild);
9
15
  show?: boolean | undefined;
10
16
  sort?: string | undefined;
11
17
  }[]>;
@@ -19,14 +25,20 @@ export declare const useTable: (rowData: TableRowsData | Ref<TableRowsData>, col
19
25
  icon?: import("vue").RenderFunction | import("vue").FunctionalComponent<{}, {}, any, {}> | undefined;
20
26
  label: string;
21
27
  }[];
22
- rowData: import("./table").TableRowData;
28
+ rowData: {
29
+ [key: string]: any;
30
+ id: number;
31
+ };
23
32
  cells: {
24
33
  isComponent: boolean;
25
34
  classNames: string;
26
35
  val: any;
27
36
  alignment: string;
28
37
  title: string;
29
- render: string | ((rowData: import("./table").TableRowData, rowIndex: number) => import("vue").VNodeChild);
38
+ render: string | number | ((rowData: {
39
+ [key: string]: any;
40
+ id: number;
41
+ }, rowIndex: number) => import("vue").VNodeChild);
30
42
  show?: boolean | undefined;
31
43
  sort?: string | undefined;
32
44
  }[];
@@ -0,0 +1,24 @@
1
+ import type { Component, FunctionalComponent, RenderFunction } from "vue";
2
+ type __VLS_Props = {
3
+ bgColor?: `bg-${string}`;
4
+ iconColor?: `text-${string}`;
5
+ icon: FunctionalComponent | Component | RenderFunction;
6
+ indicator?: string | number | boolean;
7
+ size?: "xs" | "sm" | "base" | "lg" | "xl";
8
+ type?: "circle" | "square";
9
+ };
10
+ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
+ type: "circle" | "square";
12
+ size: "base" | "xs" | "sm" | "lg" | "xl";
13
+ bgColor: `bg-${string}`;
14
+ iconColor: `text-${string}`;
15
+ indicator: string | number | boolean;
16
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, HTMLDivElement>, {
17
+ default?(_: {}): any;
18
+ }>;
19
+ export default _default;
20
+ type __VLS_WithTemplateSlots<T, S> = T & {
21
+ new (): {
22
+ $slots: S;
23
+ };
24
+ };
@@ -0,0 +1,8 @@
1
+ import { UserMenuItem } from "../../composables/nav";
2
+ type __VLS_Props = {
3
+ menu?: UserMenuItem[];
4
+ };
5
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
6
+ menu: UserMenuItem[];
7
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ export default _default;
@@ -1,17 +1,26 @@
1
- import type { DynamicTableOptions, TableActions, TableColumns } from "../../composables/table";
1
+ import type { DynamicTableOptions, TableActions, TableBulkActions, TableColumns } from "../../composables/table";
2
2
  type __VLS_Props = {
3
3
  tableActions?: TableActions<any>;
4
+ tableBulkActions?: TableBulkActions<any>;
4
5
  tableColumns: TableColumns<any>;
5
6
  tableOptions: DynamicTableOptions;
6
7
  };
7
- declare const _default: import("vue").DefineComponent<__VLS_Props, {
8
+ type __VLS_PublicProps = {
9
+ "selected"?: number[];
10
+ } & __VLS_Props;
11
+ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {
12
+ clearSelection: () => void;
8
13
  refresh: () => void;
9
14
  reset: () => void;
10
- }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
16
+ "update:selected": (value: number[]) => any;
17
+ } & {
11
18
  "click:row": (v: any) => any;
12
- }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
19
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
20
  "onClick:row"?: ((v: any) => any) | undefined;
21
+ "onUpdate:selected"?: ((value: number[]) => any) | undefined;
14
22
  }>, {
15
23
  tableActions: TableActions<any>;
24
+ tableBulkActions: TableBulkActions<any>;
16
25
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, HTMLDivElement>;
17
26
  export default _default;
@@ -3,6 +3,9 @@ type __VLS_Props = {
3
3
  actions?: TableActionItem[];
4
4
  };
5
5
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
6
- actions: TableActionItem<import("../../composables").TableRowData>[];
6
+ actions: TableActionItem<{
7
+ [key: string]: any;
8
+ id: number;
9
+ }>[];
7
10
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
11
  export default _default;
@@ -1,8 +1,11 @@
1
1
  import type { ActionItem } from "../../composables/nav";
2
+ import type { Placement } from "@floating-ui/vue";
2
3
  type __VLS_Props = {
3
4
  actions?: ActionItem[];
5
+ placement?: Placement;
4
6
  };
5
7
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
6
8
  actions: ActionItem[];
9
+ placement: Placement;
7
10
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
11
  export default _default;
@@ -0,0 +1,18 @@
1
+ import { type Placement } from "@floating-ui/vue";
2
+ type __VLS_Props = {
3
+ as?: string;
4
+ position?: Placement | "auto";
5
+ tooltip: string;
6
+ };
7
+ declare const _default: __VLS_WithTemplateSlots<import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
8
+ as: string;
9
+ position: Placement | "auto";
10
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>, {
11
+ default?(_: {}): any;
12
+ }>;
13
+ export default _default;
14
+ type __VLS_WithTemplateSlots<T, S> = T & {
15
+ new (): {
16
+ $slots: S;
17
+ };
18
+ };