@xy-planning-network/trees 0.11.6-dev-1 → 0.11.6-dev-4

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.6-dev-1",
3
+ "version": "0.11.6-dev-4",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "repository": "github:xy-planning-network/trees",
@@ -1,8 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, toRef, watch } from "vue"
3
- import { ActionsDropdown } from "@/lib-components"
3
+ import { ActionsDropdown, TablePaginator } from "@/lib-components"
4
4
  import DateRangePicker from "../forms/DateRangePicker.vue"
5
- import Paginator from "../navigation/Paginator.vue"
6
5
  import BaseAPI from "../../api/base"
7
6
  import type {
8
7
  DynamicTableAPI,
@@ -46,6 +45,10 @@ const loadAndRender = (): void => {
46
45
  q: query.value,
47
46
  }
48
47
 
48
+ if (!selectionsPersisted.value) {
49
+ clearSelections()
50
+ }
51
+
49
52
  BaseAPI.get<TrailsRespPaged<unknown>>(
50
53
  props.tableOptions.url,
51
54
  { skipLoader: false },
@@ -137,7 +140,11 @@ const hasContent = computed((): boolean => {
137
140
  return rows.value.length ? true : false
138
141
  })
139
142
 
140
- const deselectAll = () => {
143
+ const selectionsPersisted = computed(() => {
144
+ return props.tableBulkActions.persistent ?? false
145
+ })
146
+
147
+ const clearSelections = () => {
141
148
  selected.value = []
142
149
  }
143
150
 
@@ -146,9 +153,9 @@ const selected = defineModel<number[]>("selected", {
146
153
  default: [],
147
154
  })
148
155
 
149
- const selectedData = computed<TableRowData[]>(() => {
150
- return tableData.value.filter((data) => {
151
- return selected.value.includes(data.id)
156
+ const selectedOnPage = computed(() => {
157
+ return selected.value.filter((id) => {
158
+ return selectable.value.includes(id)
152
159
  })
153
160
  })
154
161
 
@@ -162,20 +169,21 @@ const bulkActions = computed(() => {
162
169
  ...action,
163
170
  disabled: selected.value.length === 0 || action.disabled,
164
171
  onClick: () =>
165
- action.onClick.apply(undefined, [
166
- selected.value,
167
- selectedData.value,
168
- publicMethods,
169
- ]),
172
+ action.onClick.apply(undefined, [selected.value, publicMethods]),
170
173
  }
171
174
  })
172
175
  })
173
176
 
174
177
  const hasBulkActions = computed(() => bulkActions.value.length > 0)
175
178
 
176
- const selectableIds = computed(() => {
179
+ const selectable = computed(() => {
177
180
  return tableData.value
178
181
  .filter((row) => {
182
+ // NOTE(spk): table data must have an "id" key for bulk actions.
183
+ if (row.id === undefined) {
184
+ return false
185
+ }
186
+
179
187
  if (props.tableBulkActions.isSelectable === undefined) {
180
188
  return true
181
189
  }
@@ -189,8 +197,47 @@ const selectableIds = computed(() => {
189
197
  .map((d) => d["id"])
190
198
  })
191
199
 
200
+ const bulkSelectChecked = computed(
201
+ () =>
202
+ selectedOnPage.value.length > 0 &&
203
+ selectedOnPage.value.length === selectable.value.length
204
+ )
205
+
206
+ const bulkSelectIndeterminate = computed(
207
+ () =>
208
+ selectedOnPage.value.length > 0 &&
209
+ selectedOnPage.value.length < selectable.value.length
210
+ )
211
+
212
+ const bulkSelectOnChange = (e: Event) => {
213
+ const isChecked = (e.target as HTMLInputElement).checked
214
+
215
+ // append all records on current page to existing selection
216
+ if (selectionsPersisted.value && isChecked) {
217
+ selected.value = [...selected.value, ...selectable.value.map((id) => id)]
218
+ return
219
+ }
220
+
221
+ // remove all records on current page from existing selection
222
+ if (selectionsPersisted.value && !isChecked) {
223
+ selected.value = selected.value.filter((id) => {
224
+ return !selectable.value.includes(id)
225
+ })
226
+
227
+ return
228
+ }
229
+
230
+ // set all records on current page to selection
231
+ if (isChecked) {
232
+ selected.value = selectable.value.map((id) => id)
233
+ return
234
+ }
235
+
236
+ clearSelections()
237
+ }
238
+
192
239
  const publicMethods: DynamicTableAPI = {
193
- deselectAll: deselectAll,
240
+ clearSelection: clearSelections,
194
241
  refresh: loadAndRender,
195
242
  reset: reloadTable,
196
243
  }
@@ -302,16 +349,10 @@ loadAndRender()
302
349
  'checked:disabled:bg-xy-blue checked:disabled:border-xy-blue checked:disabled:opacity-50',
303
350
  'border-gray-300 focus:ring-xy-blue-500',
304
351
  ]"
305
- :checked="selected.length === selectableIds.length"
306
- :indeterminate="
307
- selected.length > 0 && selected.length < selectableIds.length
308
- "
352
+ :checked="bulkSelectChecked"
353
+ :indeterminate="bulkSelectIndeterminate"
309
354
  type="checkbox"
310
- @change="
311
- selected = ($event.target as HTMLInputElement).checked
312
- ? selectableIds.map((id) => id)
313
- : []
314
- "
355
+ @change="bulkSelectOnChange"
315
356
  />
316
357
  </th>
317
358
 
@@ -382,12 +423,19 @@ loadAndRender()
382
423
  />
383
424
  </tr>
384
425
 
385
- <tr v-if="selected.length > 0">
386
- <td colspan="100%" class="px-6 py-3 border-t bg-neutral-50">
387
- <div class="flex items-center space-x-3">
388
- <div class="text-sm font-semibold">
389
- {{ selected.length }}
390
- selected
426
+ <tr v-if="hasBulkActions && selected.length > 0">
427
+ <td colspan="100%" class="px-6 py-2.5 border-t bg-neutral-50">
428
+ <div class="flex items-center gap-x-3">
429
+ <div class="text-sm shrink-0">
430
+ Selected
431
+ <span class="font-medium">{{ selectedOnPage.length }}</span>
432
+ of
433
+ <span class="font-medium">{{ selectable.length }}</span>
434
+ <span v-if="selectionsPersisted">
435
+ /
436
+ <span class="font-medium">{{ selected.length }}</span>
437
+ total
438
+ </span>
391
439
  </div>
392
440
 
393
441
  <TableActionButtons :actions="bulkActions" />
@@ -412,9 +460,9 @@ loadAndRender()
412
460
  'checked:disabled:bg-xy-blue checked:disabled:border-xy-blue checked:disabled:opacity-50',
413
461
  'border-gray-300 focus:ring-xy-blue-500',
414
462
  ]"
415
- :disabled="!selectableIds.includes(row.rowData.id)"
463
+ :disabled="!selectable.includes(row.rowData?.id)"
416
464
  type="checkbox"
417
- :value="row.rowData.id"
465
+ :value="row.rowData?.id"
418
466
  />
419
467
  </td>
420
468
 
@@ -461,10 +509,11 @@ loadAndRender()
461
509
  </table>
462
510
  </div>
463
511
 
464
- <Paginator
465
- v-if="hasContent"
466
- v-model="pagination"
467
- @update:model-value="loadAndRender()"
468
- />
512
+ <div v-if="hasContent" class="mt-4">
513
+ <TablePaginator
514
+ v-model="pagination"
515
+ @update:model-value="loadAndRender()"
516
+ />
517
+ </div>
469
518
  </div>
470
519
  </template>
@@ -0,0 +1,140 @@
1
+ <script setup lang="ts">
2
+ import { Pagination } from "@/composables/nav"
3
+ import { debounceFn } from "@/entry"
4
+ import { NumberInput, Select } from "@/lib-components"
5
+ import { computed } from "vue"
6
+
7
+ const props = defineProps<{
8
+ pageOptions?: { label: string; value: number }[]
9
+ }>()
10
+
11
+ const pagination = defineModel<Pagination>({ required: true })
12
+
13
+ const page = computed({
14
+ get: () => pagination.value.page,
15
+ set: (v: number) => {
16
+ pagination.value = {
17
+ ...pagination.value,
18
+ page: v,
19
+ }
20
+ },
21
+ })
22
+
23
+ const perPage = computed({
24
+ get: () => pagination.value.perPage,
25
+ set: (v: number) => {
26
+ pagination.value = {
27
+ ...pagination.value,
28
+ perPage: v,
29
+ }
30
+ },
31
+ })
32
+
33
+ const pageSelectOpts = computed(() => {
34
+ if (props.pageOptions) {
35
+ return props.pageOptions
36
+ }
37
+
38
+ return [
39
+ { label: "5 per page", value: 5 },
40
+ { label: "10 per page", value: 10 },
41
+ { label: "20 per page", value: 20 },
42
+ { label: "50 per page", value: 50 },
43
+ ]
44
+ })
45
+
46
+ const debouncePageInput = debounceFn((val: number | null) => {
47
+ if (val === null) {
48
+ return
49
+ }
50
+
51
+ if (val === page.value) {
52
+ return
53
+ }
54
+
55
+ page.value = val
56
+ }, 350)
57
+
58
+ const onDown = debounceFn(() => {
59
+ if (page.value <= 1) {
60
+ return
61
+ }
62
+
63
+ page.value = page.value - 1
64
+ }, 250)
65
+
66
+ const onUp = debounceFn(() => {
67
+ if (page.value >= pagination.value.totalPages) {
68
+ return
69
+ }
70
+
71
+ page.value = page.value + 1
72
+ }, 250)
73
+
74
+ const range = computed(() => {
75
+ const { page, perPage, totalItems } = pagination.value
76
+ return {
77
+ start: totalItems > 0 ? (page - 1) * perPage + 1 : 0,
78
+ end: Math.min(page * perPage, totalItems),
79
+ }
80
+ })
81
+ </script>
82
+
83
+ <template>
84
+ <div
85
+ class="flex flex-col items-center space-y-3.5 sm:flex-row sm:space-y-0 sm:gap-x-3 sm:justify-center"
86
+ >
87
+ <!--Range details-->
88
+ <p class="text-center text-sm text-neutral-700 sm:text-left sm:mr-auto">
89
+ Showing
90
+ <span class="font-medium">{{ range.start }}</span>
91
+ to
92
+ <span class="font-medium">{{ range.end }}</span>
93
+ of
94
+ <span class="font-medium">{{ pagination.totalItems }}</span>
95
+ results
96
+ </p>
97
+
98
+ <!--Pager-->
99
+ <div class="flex gap-3 items-center justify-center shrink-0">
100
+ <button
101
+ class="xy-btn-neutral"
102
+ :disabled="page <= 1"
103
+ type="button"
104
+ @click.prevent="page--"
105
+ >
106
+ &larr; <span class="sr-only">Previous</span>
107
+ </button>
108
+
109
+ <div class="max-w-[50px]">
110
+ <NumberInput
111
+ :model-value="page"
112
+ :min="1"
113
+ :max="pagination.totalPages"
114
+ type="number"
115
+ @update:model-value="debouncePageInput"
116
+ @keydown.down="onDown"
117
+ @keydown.up="onUp"
118
+ />
119
+ </div>
120
+
121
+ <div class="text-sm">
122
+ of <span class="font-medium">{{ pagination.totalPages }}</span>
123
+ </div>
124
+
125
+ <button
126
+ class="xy-btn-neutral"
127
+ :disabled="page >= pagination.totalPages"
128
+ type="button"
129
+ @click.prevent="page++"
130
+ >
131
+ <span class="sr-only">Next</span> &rarr;
132
+ </button>
133
+ </div>
134
+
135
+ <!--Per Page Selector-->
136
+ <div class="max-w-[150px] sm:ml-auto">
137
+ <Select v-model="perPage" :options="pageSelectOpts" />
138
+ </div>
139
+ </div>
140
+ </template>
@@ -11,7 +11,7 @@ export interface DynamicTableOptions {
11
11
  url: string;
12
12
  }
13
13
  export interface DynamicTableAPI {
14
- deselectAll: () => void;
14
+ clearSelection: () => void;
15
15
  /**
16
16
  * Force refresh the table data with the current api params state
17
17
  * @returns void
@@ -49,9 +49,23 @@ export interface TableActionItem<T = TableRowData> extends ActionItem {
49
49
  */
50
50
  show?: boolean | ((rowData: T, rowIndex: number) => boolean);
51
51
  }
52
- export interface TableBulkActionItem<T = TableRowData> extends ActionItem {
52
+ export interface TableBulkActionItem extends ActionItem {
53
+ /**
54
+ * Whether or not the bulk action item is enabled. Disabled actions are
55
+ * visible in the UI, but do not trigger click events.
56
+ */
53
57
  disabled?: boolean;
54
- onClick: (selected: number[], selectedRows: T[], tableAPI: DynamicTableAPI) => void;
58
+ /**
59
+ * The callback method triggered by the action item buttons click event.
60
+ * @param selected the array of selected rows by the primary key `id`
61
+ * @param tableAPI DynamicTableAPI
62
+ * @returns void
63
+ */
64
+ onClick: (selected: number[], tableAPI: DynamicTableAPI) => void;
65
+ /**
66
+ * Whether or not to visible show the action item in the UI. When all action items
67
+ * on a table a hidden with show: false, bulk selections are disabled for the table.
68
+ */
55
69
  show?: boolean;
56
70
  }
57
71
  export interface TableActions<T = TableRowData> {
@@ -64,11 +78,18 @@ export interface TableActions<T = TableRowData> {
64
78
  */
65
79
  type: "dropdown" | "buttons";
66
80
  }
67
- export interface TableBulkActions<T extends TableRowData = TableRowData> {
81
+ export interface TableBulkActions<T = TableRowData> {
68
82
  /**
69
83
  * an array of TableActionItem definitions
70
84
  */
71
- actions: TableBulkActionItem<T>[];
85
+ actions: TableBulkActionItem[];
86
+ /**
87
+ * whether to persist the selections across pagination, searching, sorting, and filtering
88
+ */
89
+ persistent?: boolean;
90
+ /**
91
+ * a function that determines if the row can be selected for bulk actions
92
+ */
72
93
  isSelectable?: (data: T) => boolean;
73
94
  }
74
95
  export interface TableColumn<T = TableRowData> {
@@ -102,12 +123,6 @@ export interface TableColumn<T = TableRowData> {
102
123
  sort?: string;
103
124
  }
104
125
  export type TableCellAlignment = "left" | "center" | "right";
105
- export type TableRowData<T extends {
106
- [key: string]: any;
107
- id: number;
108
- } = {
109
- [key: string]: any;
110
- id: number;
111
- }> = T;
126
+ export type TableRowData = Record<string, any>;
112
127
  export type TableColumns<T = TableRowData> = TableColumn<T>[];
113
128
  export type TableRowsData = TableRowData[];
File without changes
@@ -3,15 +3,9 @@ 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: {
7
- [key: string]: any;
8
- id: number;
9
- }, rowIndex: number) => string) | undefined;
6
+ classNames?: string | ((rowData: import("./table").TableRowData, rowIndex: number) => string) | undefined;
10
7
  title: string;
11
- render: string | number | ((rowData: {
12
- [key: string]: any;
13
- id: number;
14
- }, rowIndex: number) => import("vue").VNodeChild);
8
+ render: string | ((rowData: import("./table").TableRowData, rowIndex: number) => import("vue").VNodeChild);
15
9
  show?: boolean | undefined;
16
10
  sort?: string | undefined;
17
11
  }[]>;
@@ -25,20 +19,14 @@ export declare const useTable: (rowData: TableRowsData | Ref<TableRowsData>, col
25
19
  icon?: import("vue").RenderFunction | import("vue").FunctionalComponent<{}, {}, any, {}> | undefined;
26
20
  label: string;
27
21
  }[];
28
- rowData: {
29
- [key: string]: any;
30
- id: number;
31
- };
22
+ rowData: import("./table").TableRowData;
32
23
  cells: {
33
24
  isComponent: boolean;
34
25
  classNames: string;
35
26
  val: any;
36
27
  alignment: string;
37
28
  title: string;
38
- render: string | number | ((rowData: {
39
- [key: string]: any;
40
- id: number;
41
- }, rowIndex: number) => import("vue").VNodeChild);
29
+ render: string | ((rowData: import("./table").TableRowData, rowIndex: number) => import("vue").VNodeChild);
42
30
  show?: boolean | undefined;
43
31
  sort?: string | undefined;
44
32
  }[];
@@ -19,6 +19,7 @@ import { default as Spinner } from "./overlays/Spinner.vue";
19
19
  import { default as DataTable } from "./lists/DataTable.vue";
20
20
  import { default as Steps } from "./navigation/Steps.vue";
21
21
  import { default as DynamicTable } from "./lists/DynamicTable.vue";
22
+ import { default as TablePaginator } from "./navigation/TablePaginator.vue";
22
23
  import { default as Tabs } from "./navigation/Tabs.vue";
23
24
  import { default as Toggle } from "./forms/Toggle.vue";
24
25
  import { default as XYSpinner } from "./indicators/XYSpinner.vue";
@@ -40,7 +41,7 @@ import { default as Select } from "./forms/Select.vue";
40
41
  import { default as TextArea } from "./forms/TextArea.vue";
41
42
  import { default as YesOrNoRadio } from "./forms/YesOrNoRadio.vue";
42
43
  export { ActionsDropdown, Cards, ContentModal, DateFilter, DetailList, DownloadCell, Flash, InlineAlert, Modal, SidebarLayout, Slideover, StackedLayout, Popover, PopoverContent, PopoverPosition, // Type export
43
- Paginator, Spinner, DataTable, Steps, DynamicTable, Tabs, Toggle, Tooltip, BaseInput, Checkbox, DateRangePicker, DateTime, InputError, InputHelp, InputLabel, FieldsetLegend, MultiCheckboxes, NumberInput, Radio, RadioCards, Select, TextArea, YesOrNoRadio, XYSpinner, ProgressCircles, ProgressCirclesLabeled, };
44
+ Paginator, Spinner, DataTable, Steps, DynamicTable, Tabs, TablePaginator, Toggle, Tooltip, BaseInput, Checkbox, DateRangePicker, DateTime, InputError, InputHelp, InputLabel, FieldsetLegend, MultiCheckboxes, NumberInput, Radio, RadioCards, Select, TextArea, YesOrNoRadio, XYSpinner, ProgressCircles, ProgressCirclesLabeled, };
44
45
  /**
45
46
  * declare global component types for App.use(Trees)
46
47
  */
@@ -64,6 +65,7 @@ export interface TreesComponents {
64
65
  DynamicTable: typeof DynamicTable;
65
66
  Steps: typeof Steps;
66
67
  Tabs: typeof Tabs;
68
+ TablePaginator: typeof TablePaginator;
67
69
  Toggle: typeof Toggle;
68
70
  Tooltip: typeof Tooltip;
69
71
  BaseInput: typeof BaseInput;
@@ -9,7 +9,7 @@ type __VLS_PublicProps = {
9
9
  "selected"?: number[];
10
10
  } & __VLS_Props;
11
11
  declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {
12
- deselectAll: () => void;
12
+ clearSelection: () => void;
13
13
  refresh: () => void;
14
14
  reset: () => void;
15
15
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
@@ -3,9 +3,6 @@ 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<{
7
- [key: string]: any;
8
- id: number;
9
- }>[];
6
+ actions: TableActionItem<import("../../composables").TableRowData>[];
10
7
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
8
  export default _default;
@@ -0,0 +1,16 @@
1
+ import { Pagination } from "../../composables/nav";
2
+ type __VLS_Props = {
3
+ pageOptions?: {
4
+ label: string;
5
+ value: number;
6
+ }[];
7
+ };
8
+ type __VLS_PublicProps = {
9
+ modelValue: Pagination;
10
+ } & __VLS_Props;
11
+ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
+ "update:modelValue": (value: Pagination) => any;
13
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
14
+ "onUpdate:modelValue"?: ((value: Pagination) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, HTMLDivElement>;
16
+ export default _default;