@kubex/zinc 1.0.4 → 1.0.6

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.
Files changed (158) hide show
  1. package/dist/custom-elements.json +1278 -191
  2. package/dist/vscode.html-custom-data.json +93 -16
  3. package/dist/web-types.json +235 -25
  4. package/dist/zn.d.ts +1313 -941
  5. package/dist/zn.min.css +1 -1
  6. package/dist/zn.min.js +2049 -931
  7. package/docs/_includes/default.njk +1 -0
  8. package/docs/assets/scripts/docs.js +4 -5
  9. package/docs/assets/scripts/search.js +0 -9
  10. package/docs/assets/styles/docs.css +3 -3
  11. package/docs/data/data-table.json +79 -0
  12. package/docs/data/products-table.json +36 -0
  13. package/docs/eleventy.config.cjs +3 -0
  14. package/docs/pages/components/data-select.md +10 -0
  15. package/docs/pages/components/data-table.md +84 -45
  16. package/docs/pages/components/dialog.md +1 -1
  17. package/docs/pages/components/editor.md +1 -3
  18. package/docs/pages/components/filter-wrapper.md +20 -0
  19. package/docs/pages/components/icon.md +2 -0
  20. package/docs/pages/components/item.md +2 -2
  21. package/docs/pages/components/menu-item.md +12 -3
  22. package/docs/pages/components/panel.md +17 -0
  23. package/docs/pages/components/progress-tile.md +17 -1
  24. package/docs/pages/components/settings-container.md +137 -0
  25. package/docs/pages/components/textarea.md +1 -1
  26. package/package.json +3 -1
  27. package/scss/_root.scss +26 -0
  28. package/scss/boot.scss +27 -1
  29. package/scss/mixins/index.scss +30 -1
  30. package/scss/shared/_base.scss +1 -1
  31. package/scss/shared/_button.scss +1 -1
  32. package/scss/shared/layout.scss +7 -0
  33. package/scss/themes/_dark.scss +5 -5
  34. package/scss/themes/_light.scss +6 -5
  35. package/scss/variables/index.scss +9 -0
  36. package/src/components/alert/alert.component.ts +1 -1
  37. package/src/components/alert/alert.scss +18 -4
  38. package/src/components/bulk-actions/bulk-actions.component.ts +152 -84
  39. package/src/components/bulk-actions/bulk-actions.scss +2 -0
  40. package/src/components/button/button.component.ts +6 -2
  41. package/src/components/button/button.scss +16 -1
  42. package/src/components/button-menu/button-menu.component.ts +23 -15
  43. package/src/components/button-menu/button-menu.scss +4 -4
  44. package/src/components/chart/chart.component.ts +14 -12
  45. package/src/components/checkbox/checkbox.component.ts +5 -1
  46. package/src/components/checkbox/checkbox.scss +1 -1
  47. package/src/components/chip/chip.component.ts +7 -3
  48. package/src/components/chip/chip.scss +9 -1
  49. package/src/components/collapsible/collapsible.component.ts +48 -2
  50. package/src/components/cols/cols.component.ts +7 -3
  51. package/src/components/cols/cols.scss +22 -14
  52. package/src/components/confirm/confirm.component.ts +1 -2
  53. package/src/components/content-block/content-block.component.ts +104 -14
  54. package/src/components/content-block/content-block.scss +8 -0
  55. package/src/components/data-select/data-select.component.ts +1 -1
  56. package/src/components/data-table/data-table.component.ts +394 -430
  57. package/src/components/data-table/data-table.scss +6 -1
  58. package/src/components/datepicker/datepicker.scss +1 -1
  59. package/src/components/dialog/dialog.component.ts +52 -33
  60. package/src/components/dialog/dialog.scss +15 -1
  61. package/src/components/dropdown/dropdown.component.ts +1 -1
  62. package/src/components/editor/editor.component.ts +196 -158
  63. package/src/components/editor/editor.scss +25 -5
  64. package/src/components/editor/modules/ai/index.ts +281 -0
  65. package/src/components/editor/modules/ai/panel/ai-panel.component.ts +102 -0
  66. package/src/components/editor/modules/ai/panel/ai-panel.scss +83 -0
  67. package/src/components/editor/modules/ai/tooltip/ai-tooltip.component.ts +30 -0
  68. package/src/components/editor/modules/ai/tooltip/ai-tooltip.scss +28 -0
  69. package/src/components/editor/modules/{attachment-module.ts → attachment/attachment.ts} +7 -7
  70. package/src/components/editor/modules/context-menu/context-menu-component.ts +115 -0
  71. package/src/components/editor/modules/context-menu/context-menu.scss +49 -0
  72. package/src/components/editor/modules/context-menu/context-menu.ts +296 -0
  73. package/src/components/editor/modules/context-menu/quick-action/index.ts +12 -0
  74. package/src/components/editor/modules/context-menu/quick-action/quick-action.component.ts +18 -0
  75. package/src/components/editor/modules/date-picker/date-picker.ts +77 -0
  76. package/src/components/editor/modules/dialog/dialog.component.ts +115 -0
  77. package/src/components/editor/modules/dialog/dialog.scss +43 -0
  78. package/src/components/editor/modules/dialog/dialog.ts +46 -0
  79. package/src/components/editor/modules/{drag-drop-module.ts → drag-drop/drag-drop.ts} +6 -6
  80. package/src/components/editor/modules/emoji/emoji.ts +106 -0
  81. package/src/components/editor/modules/emoji/headless/headless-emoji.component.ts +122 -0
  82. package/src/components/editor/modules/emoji/headless/headless-emoji.scss +52 -0
  83. package/src/components/editor/modules/emoji/headless/headless-emoji.ts +223 -0
  84. package/src/components/editor/modules/{image-resize-module/image-resize-module.ts → image-resize/image-resize.ts} +5 -5
  85. package/src/components/editor/modules/{time-tracking-module.ts → time-tracking/time-tracking.ts} +5 -5
  86. package/src/components/editor/modules/toolbar/tool/index.ts +12 -0
  87. package/src/components/editor/modules/toolbar/tool/tool.component.ts +30 -0
  88. package/src/components/editor/modules/toolbar/tool/tool.scss +5 -0
  89. package/src/components/editor/modules/toolbar/toolbar.component.ts +738 -0
  90. package/src/components/editor/modules/toolbar/toolbar.scss +98 -0
  91. package/src/components/editor/modules/toolbar/toolbar.ts +328 -0
  92. package/src/components/expanding-action/expanding-action.component.ts +2 -2
  93. package/src/components/expanding-action/expanding-action.scss +2 -2
  94. package/src/components/filter-wrapper/filter-wrapper.component.ts +91 -0
  95. package/src/components/filter-wrapper/filter-wrapper.scss +11 -0
  96. package/src/components/filter-wrapper/filter-wrapper.test.ts +10 -0
  97. package/src/components/filter-wrapper/index.ts +12 -0
  98. package/src/components/header/header.component.ts +1 -1
  99. package/src/components/header/header.scss +8 -1
  100. package/src/components/hover-container/hover-container.scss +39 -36
  101. package/src/components/icon/icon.component.ts +11 -5
  102. package/src/components/icon/icon.scss +15 -2
  103. package/src/components/inline-edit/inline-edit.component.ts +52 -6
  104. package/src/components/inline-edit/inline-edit.scss +15 -2
  105. package/src/components/input/input.scss +1 -1
  106. package/src/components/item/item.component.ts +30 -19
  107. package/src/components/item/item.scss +57 -23
  108. package/src/components/linked-select/linked-select.component.ts +5 -1
  109. package/src/components/menu/menu.component.ts +1 -0
  110. package/src/components/menu/menu.scss +1 -1
  111. package/src/components/menu-item/menu-item.component.ts +22 -4
  112. package/src/components/menu-item/menu-item.scss +54 -7
  113. package/src/components/navbar/navbar.component.ts +7 -1
  114. package/src/components/navbar/navbar.scss +26 -16
  115. package/src/components/option/option.component.ts +4 -1
  116. package/src/components/option/option.scss +9 -0
  117. package/src/components/page-nav/page-nav.component.ts +2 -2
  118. package/src/components/page-nav/page-nav.scss +4 -4
  119. package/src/components/panel/panel.component.ts +20 -9
  120. package/src/components/panel/panel.scss +5 -1
  121. package/src/components/popup/popup.component.ts +16 -2
  122. package/src/components/progress-tile/progress-tile.component.ts +10 -4
  123. package/src/components/progress-tile/progress-tile.scss +7 -4
  124. package/src/components/query-builder/query-builder.component.ts +1 -1
  125. package/src/components/radio/radio.scss +1 -1
  126. package/src/components/scroll-container/scroll-container.component.ts +56 -9
  127. package/src/components/scroll-container/scroll-container.scss +37 -2
  128. package/src/components/select/select.component.ts +3 -0
  129. package/src/components/select/select.scss +1 -1
  130. package/src/components/settings-container/index.ts +12 -0
  131. package/src/components/settings-container/settings-container.component.ts +226 -0
  132. package/src/components/settings-container/settings-container.scss +99 -0
  133. package/src/components/settings-container/settings-container.test.ts +10 -0
  134. package/src/components/skeleton/skeleton.scss +5 -4
  135. package/src/components/sp/sp.component.ts +2 -0
  136. package/src/components/sp/sp.scss +2 -0
  137. package/src/components/split-button/split-button.component.ts +24 -18
  138. package/src/components/split-pane/split-pane.component.ts +5 -5
  139. package/src/components/stat/stat.component.ts +5 -1
  140. package/src/components/stat/stat.scss +24 -1
  141. package/src/components/style/style.component.ts +25 -1
  142. package/src/components/tabs/tabs.component.ts +12 -1
  143. package/src/components/textarea/textarea.component.ts +3 -1
  144. package/src/components/textarea/textarea.scss +21 -2
  145. package/src/components/vertical-stepper/vertical-stepper.scss +1 -1
  146. package/src/internal/form.ts +3 -1
  147. package/src/internal/slot.ts +29 -3
  148. package/src/utilities/on.ts +7 -8
  149. package/src/utilities/query.ts +5 -1
  150. package/src/wc.scss +7 -0
  151. package/src/zinc.ts +5 -0
  152. package/src/components/editor/modules/dialog-module/dialog-module.component.ts +0 -289
  153. package/src/components/editor/modules/dialog-module/dialog-module.scss +0 -115
  154. package/src/components/editor/modules/dialog-module/dialog-module.ts +0 -99
  155. package/src/components/editor/modules/events/zn-show-canned-response-dialog.ts +0 -15
  156. package/src/components/editor/modules/menu-module/menu-module.component.ts +0 -149
  157. package/src/components/editor/modules/menu-module/menu-module.scss +0 -17
  158. package/src/components/editor/modules/menu-module/menu-module.ts +0 -203
@@ -19,20 +19,46 @@ import ZnHoverContainer from "../hover-container";
19
19
  import ZnMenu from "../menu";
20
20
  import ZnMenuItem from "../menu-item";
21
21
  import ZnSkeleton from "../skeleton";
22
+ import ZnStyle from "../style";
23
+ import type ZnDataSelect from "../data-select";
24
+ import type ZnInput from "../input";
25
+ import type ZnQueryBuilder from "../query-builder";
22
26
  import type ZnSelect from "../select";
23
27
 
24
28
  import styles from './data-table.scss';
25
29
 
26
30
  const DEFAULT_PAGE = 1;
27
31
  const DEFAULT_PER_PAGE = 10;
28
- const DEFAULT_TOTAL_PAGES = 10;
29
32
 
30
- interface TableData {
31
- page: number;
32
- per_page: number;
33
+ interface Cell {
34
+ text: string;
35
+ column: string;
36
+ color?: string;
37
+ style?: string;
38
+ iconSrc?: string;
39
+ iconColor?: string;
40
+ hoverContent?: string;
41
+ hoverPlacement?: string;
42
+ chipColor?: string;
43
+ gaid?: string;
44
+ sortValue?: string;
45
+ uri?: string;
46
+ target?: string;
47
+ }
48
+
49
+ interface Row {
50
+ id: string;
51
+ uri?: string;
52
+ target?: string;
53
+ actions?: ActionConfig[];
54
+ cells: Cell[];
55
+ }
56
+
57
+ interface Response {
58
+ rows: Row[];
59
+ perPage: number;
33
60
  total: number;
34
- total_pages: number;
35
- data: any[];
61
+ page: number;
36
62
  }
37
63
 
38
64
  export enum ActionSlots {
@@ -40,101 +66,48 @@ export enum ActionSlots {
40
66
  modify = 'modify-action',
41
67
  create = 'create-action',
42
68
  filter = 'filter',
69
+ filter_top = 'filter-top',
43
70
  sort = 'sort',
71
+ inputs = 'inputs'
44
72
  }
45
73
 
46
- interface RowData {
47
- [key: string]: string | number | object | undefined;
48
- }
49
-
50
- interface RenderDataValue {
51
- value: string | number;
52
- type: string;
53
- tag: string;
54
- url: string;
55
- target: string;
56
- gaid?: string;
57
- html?: string;
58
- color?: string;
59
- caption: CaptionConfig;
60
- buttons: ButtonConfig[];
61
- menu: MenuConfig[];
62
- icon: IconConfig;
63
- hover: HoverContainerConfig;
64
- }
65
-
66
- interface CaptionConfig {
67
- title: string;
68
- summary: string;
74
+ interface ActionConfig {
75
+ text: string;
69
76
  uri: string;
70
77
  target: string;
71
78
  gaid: string;
72
- icon: IconConfig;
73
- }
74
-
75
- interface IconConfig {
76
- src: string;
77
- size: number;
78
- color: string;
79
- }
80
-
81
- interface HoverContainerConfig {
82
- label: string;
83
- content: string;
84
- anchor: string;
85
- placement:
86
- | 'top'
87
- | 'top-start'
88
- | 'top-end'
89
- | 'bottom'
90
- | 'bottom-start'
91
- | 'bottom-end'
92
- | 'right'
93
- | 'right-start'
94
- | 'right-end'
95
- | 'left'
96
- | 'left-start'
97
- | 'left-end';
98
- icon: IconConfig;
99
- }
100
-
101
- interface ButtonConfig {
102
- id: string;
103
- href: string;
104
- gaid: string;
105
- uri: string;
106
- size: string;
107
- color: string;
79
+ confirmType: string;
80
+ confirmTitle: string;
81
+ confirmContent: string;
108
82
  icon: string;
109
- iconSize: string;
110
- tooltip: string;
111
- target: string;
112
- label: string;
113
- outline: boolean;
114
- confirm: ConfirmConfig;
83
+ type: string;
115
84
  }
116
85
 
117
- interface MenuConfig {
118
- id: string;
119
- href: string;
120
- gaid: string;
121
- target: string;
86
+ interface HeaderConfig {
87
+ key: string;
122
88
  label: string;
89
+ required?: boolean;
90
+ default?: boolean;
91
+ sortable?: boolean;
92
+ filterable?: boolean;
123
93
  }
124
94
 
125
- interface ConfirmConfig {
126
- type: string;
127
- trigger: string;
128
- fid: string;
129
- caption: string;
130
- content: string;
131
- action: string;
95
+ interface DataRequest {
96
+ page: number;
97
+ perPage: number;
98
+ sortColumn: string;
99
+ sortDirection: string;
100
+ filter: string;
132
101
  }
133
102
 
134
- interface HeaderConfig {
135
- title: string;
136
- position: string;
137
- }
103
+ type AllowedInputElement =
104
+ HTMLInputElement
105
+ | HTMLSelectElement
106
+ | HTMLTextAreaElement
107
+ | ZnInput
108
+ | ZnSelect
109
+ | ZnDataSelect
110
+ | ZnQueryBuilder
138
111
 
139
112
  /**
140
113
  * @summary Short summary of the component's intended use.
@@ -175,19 +148,17 @@ export default class ZnDataTable extends ZincElement {
175
148
  'zn-button-group': ZnButtonGroup,
176
149
  'zn-confirm': ZnConfirm,
177
150
  'zn-skeleton': ZnSkeleton,
151
+ 'zn-style': ZnStyle,
178
152
  };
179
153
 
180
154
  @property({attribute: 'data-uri'}) dataUri: string;
181
155
  @property({attribute: 'data', type: Object, reflect: true}) data: any;
182
156
  @property({attribute: 'sort-column'}) sortColumn: string;
183
- @property({attribute: 'sort-direction'}) sortDirection: string;
157
+ @property({attribute: 'sort-direction'}) sortDirection: string = "asc";
184
158
  @property({attribute: 'filter'}) filter: string = '';
185
-
186
159
  @property({attribute: 'wide-column'}) wideColumn: string;
187
160
  @property({attribute: 'key'}) key: string = 'id';
188
-
189
- @property({attribute: 'headers', type: Object})
190
- headers: Record<string, string | HeaderConfig> = {};
161
+ @property({attribute: 'headers', type: Object}) headers: Record<string, HeaderConfig> = {};
191
162
 
192
163
  // Hide header text keeping the content - e.g. Action buttons without a header
193
164
  @property({attribute: 'hide-headers', type: Object}) hiddenHeaders = '{}';
@@ -208,10 +179,19 @@ export default class ZnDataTable extends ZincElement {
208
179
 
209
180
  @property() caption: string;
210
181
 
182
+ @property({attribute: "empty-state-caption"}) emptyStateCaption: string;
183
+
184
+ @property({attribute: "empty-state-icon"}) emptyStateIcon: string = "data_alert";
185
+
211
186
  // Hide the checkbox column
212
187
  @property({attribute: 'hide-checkboxes', type: Boolean}) hideCheckboxes: boolean;
188
+
213
189
  @property() filters: [] = [];
214
190
 
191
+ @property() method: 'GET' | 'POST' = 'POST';
192
+
193
+ @property({attribute: "no-initial-load", type: Boolean}) noInitialLoad: boolean = false;
194
+
215
195
  // Data Table Properties
216
196
  private _initialLoad = true;
217
197
  private _lastTableContent: TemplateResult = html``;
@@ -223,7 +203,6 @@ export default class ZnDataTable extends ZincElement {
223
203
  private totalPages: number;
224
204
 
225
205
  private _rows: any[] = [];
226
- private _filteredRows: any[] = [];
227
206
 
228
207
  private numberOfRowsSelected: number = 0;
229
208
  private selectedRows: any[] = [];
@@ -237,98 +216,130 @@ export default class ZnDataTable extends ZincElement {
237
216
  ActionSlots.modify.valueOf(),
238
217
  ActionSlots.create.valueOf(),
239
218
  ActionSlots.filter.valueOf(),
240
- ActionSlots.sort.valueOf()
219
+ ActionSlots.sort.valueOf(),
220
+ ActionSlots.inputs.valueOf()
241
221
  );
242
222
 
243
223
  private _dataTask = new Task(this, {
244
- task: async ([dataUri], {signal}) => {
245
- let url = dataUri;
246
-
247
- const params = new URLSearchParams();
248
- params.append('page', this.page.toString());
249
- params.append('per_page', this.itemsPerPage.toString());
250
- if (this.sortColumn) {
251
- params.append('sort_column', this.sortColumn);
252
- }
253
- if (this.sortDirection) {
254
- params.append('sort_direction', this.sortDirection);
255
- }
256
- if (this.filter) {
257
- params.append('filter', this.filter);
224
+ task: async ([dataUri, requestParams], {signal}) => {
225
+ if (dataUri === undefined || this.noInitialLoad && this._initialLoad) {
226
+ return {rows: [], page: 1, perPage: this.itemsPerPage, total: 0};
258
227
  }
259
- url += '?' + params.toString();
260
-
261
- if (!this.dataUri) {
262
- return new Promise<TableData>((resolve) => {
263
- if (!this.data) throw new Error('No data provided, please provide data or dataUri');
264
- let localData = Array.isArray(this.data?.data) ? this.data.data as RowData[] : [];
265
-
266
- if (this.sortColumn) {
267
- localData = localData.sort((a, b) => {
268
- const aValue = a?.[this.sortColumn];
269
- const bValue = b?.[this.sortColumn];
270
- return this.sortData(aValue, bValue);
271
- });
272
- }
273
228
 
274
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
275
- const totalPages = Math.ceil(localData.length / (this.data.per_page as number ?? this.itemsPerPage));
276
- const start = (this.page - 1) * this.itemsPerPage;
277
- const end = start + this.itemsPerPage;
278
- const paginatedData = localData.slice(start, end);
279
-
280
- resolve({
281
- page: this.page,
282
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
283
- per_page: this.data.per_page as number ?? this.itemsPerPage,
284
- total: localData.length,
285
- total_pages: totalPages,
286
- data: paginatedData,
287
- } satisfies TableData);
229
+ const requestData: DataRequest = {
230
+ page: this.page,
231
+ perPage: this.itemsPerPage,
232
+ sortColumn: this.sortColumn,
233
+ sortDirection: this.sortDirection,
234
+ filter: this.filter,
235
+ };
236
+
237
+ // get all inputs that are in the inputs slot and add them to the
238
+ const inputs = this.hasSlotController.getSlots(ActionSlots.inputs.valueOf());
239
+ const params: Record<string, any> = {};
240
+ if (inputs) {
241
+ inputs.forEach((input) => {
242
+ const allowedInputs = ['zn-input', 'zn-select', 'zn-query-builder', 'zn-multiselect', 'zn-params-select', 'input', 'select', 'textarea'];
243
+ if (allowedInputs.includes(input.tagName.toLowerCase())) {
244
+ const value = (input as AllowedInputElement).value as string || input.getAttribute('value');
245
+ const name = (input as AllowedInputElement).name || input.getAttribute('name');
246
+ if (name) {
247
+ params[name] = value;
248
+ }
249
+ }
288
250
  });
251
+ Object.assign(requestData, params);
252
+ }
253
+
254
+
255
+ // Add any extra request params
256
+ if (requestParams && typeof requestParams === 'object') {
257
+ Object.assign(requestData, requestParams);
289
258
  }
290
259
 
291
260
  // This is also used for Rubix, so it may not work for your application.
292
- const response = await fetch(url, {
261
+ const response = await fetch(dataUri, {
262
+ method: this.method,
263
+ headers: {
264
+ 'x-kx-fetch-style': 'zn-data-table',
265
+ },
293
266
  signal,
294
267
  credentials: 'same-origin',
268
+ body: this.method === 'POST' ? JSON.stringify(requestData) : undefined
295
269
  });
296
270
 
297
271
  if (!response.ok) throw new Error(response.statusText);
298
272
  return response.json();
299
273
  },
300
- args: () => [this.dataUri]
274
+ args: () => [this.dataUri, this.requestParams]
301
275
  });
302
276
 
277
+ private rowHasActions: boolean = false;
278
+
279
+ requestParams: Record<string, any> = {};
280
+
281
+ builtinRequestParams: Record<string, any> = {};
282
+
283
+ refresh() {
284
+ // Allow manual refresh to trigger the first data load when no-initial-load is set
285
+ this._initialLoad = false;
286
+ this._dataTask.run().then(r => r);
287
+ }
288
+
303
289
  render() {
304
- const tableBody = this._dataTask.render({
305
- pending: () => {
306
- if (this._initialLoad) {
290
+ // If no-initial-load is set, do not invoke the Task on the first render
291
+ let tableBody: TemplateResult;
292
+ if (this.noInitialLoad && this._initialLoad) {
293
+ tableBody = html`
294
+ <slot name="empty-state"></slot>`;
295
+ } else {
296
+ tableBody = this._dataTask.render({
297
+ pending: () => {
298
+ if (this._initialLoad) {
299
+ return html`
300
+ <div>${this.loadingTable()}</div>`;
301
+ }
307
302
  return html`
308
- <div>${this.loadingTable()}</div>`;
303
+ <div class="reduced-opacity">${this._lastTableContent}</div>`;
304
+ },
305
+ complete: (data) => {
306
+ this._initialLoad = false;
307
+ this._lastTableContent = html`
308
+ <div>${this.renderTable(data as Response)}</div>`;
309
+ return this._lastTableContent;
310
+ },
311
+ error: (error) => {
312
+ if (error instanceof Error) {
313
+ if (error.name === "SyntaxError") {
314
+ console.debug(error.message)
315
+ return html`
316
+ <zn-sp>
317
+ <zn-alert level="error">Unable to load data</zn-alert>
318
+ </zn-sp>`;
319
+ } else if (error.message === "Not Found") {
320
+ return this.emptyState();
321
+ }
322
+ }
323
+ return html`
324
+ <div>${error}</div>`
309
325
  }
310
- return html`
311
- <div class="reduced-opacity">${this._lastTableContent}</div>`;
312
- },
313
- complete: (data) => {
314
- this._initialLoad = false;
315
- this._lastTableContent = html`
316
- <div>${this.renderTable(data as TableData)}</div>`;
317
- return this._lastTableContent;
318
- },
319
- error: (error) => html`
320
- <div>${error}</div>`
321
- }) as TemplateResult;
326
+ }) as TemplateResult;
327
+ }
322
328
 
323
329
  const hasActions = this.hasSlotController.test(ActionSlots.delete.valueOf())
324
330
  || this.hasSlotController.test(ActionSlots.modify.valueOf())
325
331
  || this.hasSlotController.test(ActionSlots.create.valueOf())
326
332
  || this.hasSlotController.test(ActionSlots.sort.valueOf())
327
- || this.hasSlotController.test(ActionSlots.filter.valueOf());
333
+ || this.hasSlotController.test(ActionSlots.filter.valueOf())
334
+ || this.hasSlotController.test(ActionSlots.filter_top.valueOf());
335
+
336
+ const hasInputs = this.hasSlotController.test(ActionSlots.inputs.valueOf());
328
337
 
329
338
  // Headers do not need to be re-rendered with new data
330
339
  return html`
331
340
  <div class="table-container" ${ref((el) => (this.tableContainer = el))}>
341
+ ${hasInputs ? html`
342
+ <slot name="${ActionSlots.inputs.valueOf()}" style="display: none"></slot>` : null}
332
343
  ${hasActions ? this.getTableHeader() : html``}
333
344
  ${tableBody}
334
345
  </div>
@@ -362,28 +373,50 @@ export default class ZnDataTable extends ZincElement {
362
373
  }
363
374
  }
364
375
 
365
- renderTable(data: TableData) {
366
- this.itemsPerPage = data.per_page ?? DEFAULT_PER_PAGE;
367
- this.page = data.page ?? DEFAULT_PAGE;
368
- this.totalPages = data.total_pages ?? DEFAULT_TOTAL_PAGES;
376
+ emptyState() {
377
+ return html`
378
+ <div class="table--empty">
379
+ <zn-empty-state
380
+ caption="${this.emptyStateCaption ? this.emptyStateCaption : (this.caption ? "No " + this.caption.toLowerCase() + " found" : "No data found")}"
381
+ icon="${this.emptyStateIcon}">
382
+ </zn-empty-state>
383
+ </div>`;
384
+ }
369
385
 
370
- if (!data?.data || data.data.length === 0) {
371
- return html`
372
- <div class="table--empty">
373
- <zn-empty-state
374
- caption="${this.caption ? "No " + this.caption.toLowerCase() + " found" : "No data found"}"
375
- icon="data_alert">
376
- </zn-empty-state>
377
- </div>`;
386
+ renderTable(data: Response) {
387
+ this.itemsPerPage = Math.max(1, data.perPage ?? DEFAULT_PER_PAGE);
388
+ this.page = Math.max(1, data.page ?? DEFAULT_PAGE);
389
+ this.totalPages = Math.ceil(Math.max(1, data.total) / this.itemsPerPage);
390
+
391
+ if (!data?.rows || data.rows.length === 0) {
392
+ return this.emptyState();
378
393
  }
379
394
 
380
- const keys = Object.entries(this.headers).map(([key, _]) => key);
381
- const filteredKeys = keys.filter((key) => !Object.values(this.hiddenColumns).includes(key));
395
+ const filteredHeaders = Object.values(this.headers).filter((header) => {
396
+ return !Object.values(this.hiddenColumns).includes(header.key);
397
+ });
382
398
 
383
- this._filteredRows = this.getRows(filteredKeys, data);
384
- this._rows = this.getRows(keys, data);
399
+ this._rows = this.getRows(data);
385
400
 
386
401
  const hasSelectedRows = this.selectedRows.length > 0;
402
+ this.rowHasActions = this._rows.some((row: Row) => row.actions && row.actions.length > 0);
403
+
404
+ // To make sure the cells are in the same order as the headers, we need to map them cell.col to heading.key
405
+ this._rows.forEach((row: Row) => {
406
+ const orderedCells: Cell[] = [];
407
+ // const unorderedCells = row.cells.filter((c: Cell) => !filteredHeaders.some((h: HeaderConfig) => h.key === c.column));
408
+
409
+ filteredHeaders.forEach((header: HeaderConfig) => {
410
+ const cell = row.cells.find((c: Cell) => c.column === header.key);
411
+ if (cell) {
412
+ orderedCells.push(cell);
413
+ } else {
414
+ orderedCells.push({text: '', column: header.key});
415
+ }
416
+ });
417
+
418
+ row.cells = orderedCells;//.concat(unorderedCells);
419
+ });
387
420
 
388
421
  return html`
389
422
  <div style="overflow-x: auto">
@@ -391,7 +424,7 @@ export default class ZnDataTable extends ZincElement {
391
424
  'table': true,
392
425
  'table--standalone': this.standalone,
393
426
  'with-hover': !this.unsortable,
394
- 'table-with--checkboxes': !this.hideCheckboxes,
427
+ 'table--with-checkboxes': !this.hideCheckboxes,
395
428
  })}">
396
429
  <thead>
397
430
  <tr>
@@ -399,17 +432,20 @@ export default class ZnDataTable extends ZincElement {
399
432
  <th>
400
433
  <div><input type="checkbox" @change="${this.selectAll}"></div>
401
434
  </th>`}
402
- ${filteredKeys.map((key: any) => this.renderCellHeader(key))}
435
+ ${filteredHeaders.map((header: HeaderConfig) => this.renderCellHeader(header))}
436
+ ${this.rowHasActions ? html`
437
+ <th></th>` : html``}
403
438
  </tr>
404
439
  </thead>
405
440
  <tbody>
406
- ${this._filteredRows.map((row: any) => html`
441
+ ${this._rows.map((row: Row) => html`
407
442
  <tr>
408
443
  ${this.hideCheckboxes ? html`` : html`
409
444
  <td class="${classMap({'hidden': !hasSelectedRows})}">
410
445
  <div><input type="checkbox" @change="${this.selectRow}"></div>
411
446
  </td>`}
412
- ${row.map((value: RenderDataValue, index: number) => this.renderCellBody(index, value))}
447
+ ${row.cells.map((value: Cell, index: number) => this.renderCellBody(index, value))}
448
+ ${this.rowHasActions ? this.renderActions(row) : null}
413
449
  </tr>`)}
414
450
  </tbody>
415
451
  </table>
@@ -421,6 +457,7 @@ export default class ZnDataTable extends ZincElement {
421
457
 
422
458
  getTableHeader() {
423
459
  return html`
460
+ <slot name="${ActionSlots.filter_top.valueOf()}"></slot>
424
461
  <div class="table__header">
425
462
  <div class="table__header__actions">
426
463
  ${this.getActions()}
@@ -460,7 +497,7 @@ export default class ZnDataTable extends ZincElement {
460
497
  }
461
498
 
462
499
  getPagination() {
463
- if (this.hidePagination || (this.totalPages <= 1 && this._rows.length <= 10)) return null;
500
+ if (this.hidePagination || (this.totalPages <= 1 && this._rows.length <= this.itemsPerPage)) return null;
464
501
 
465
502
  const optionsRowsPerPage = [10, 20, 30, 40, 50];
466
503
  optionsRowsPerPage.filter((option) => option <= this._rows.length);
@@ -597,45 +634,33 @@ export default class ZnDataTable extends ZincElement {
597
634
  this.requestUpdate();
598
635
  }
599
636
 
600
-
601
- private updateActionKeys(slotName: string) {
602
- if (this.hasSlotController.test(slotName)) {
603
- // we need to look into the slot controller for the keys input
604
- const slots = this.hasSlotController.getSlots(slotName);
605
- if (slots) {
606
- slots.forEach((slot) => {
607
- const input = slot.querySelector('input[name="keys"]');
608
- if (input && input instanceof HTMLInputElement) {
609
- input.value = this.getSelectedKeys().join(',');
610
- }
611
- });
612
- }
613
- }
614
- }
615
-
616
- selectRow(e: PointerEvent) {
637
+ selectRow(e: Event) {
617
638
  if (!(e.target && (e.target instanceof Element))) {
618
639
  return;
619
640
  }
620
641
 
621
- let target: HTMLElement = e.target as HTMLElement;
642
+ const target: Element | null = e.target;
622
643
  // if is a button or a link continue
623
- if (target.tagName === 'ZN-BUTTON' || target.tagName === 'A' || target.tagName === 'BUTTON') {
624
- return
644
+ if (target?.tagName === 'ZN-BUTTON' || target?.tagName === 'A' || target?.tagName === 'BUTTON') {
645
+ return;
625
646
  }
626
647
 
627
- // traverse parent until we reach a tr
628
- while (target && target.tagName !== 'TR') {
629
- target = target.parentElement as HTMLElement;
648
+ // Find the parent row
649
+ let parent: Element | null = target.closest ? target.closest('tr') : target.parentElement;
650
+ while (parent && parent.tagName !== 'TR') {
651
+ parent = parent.parentElement;
630
652
  }
631
653
 
632
- if (target === null) {
654
+ if (parent === null) {
633
655
  return;
634
656
  }
635
657
 
636
- const checkbox = target.querySelector('input[type="checkbox"]') as HTMLInputElement;
658
+ const checkbox: HTMLInputElement | null = parent.querySelector('input[type="checkbox"]');
637
659
  if (checkbox) {
638
- checkbox.checked = !checkbox.checked;
660
+ const isCheckboxTarget = target instanceof HTMLInputElement && target.type === 'checkbox';
661
+ if (!isCheckboxTarget) {
662
+ checkbox.checked = !checkbox.checked;
663
+ }
639
664
  }
640
665
 
641
666
  this.selectedRows = this._rows.filter((_, index) => {
@@ -670,181 +695,79 @@ export default class ZnDataTable extends ZincElement {
670
695
  };
671
696
  }
672
697
 
673
- renderData(data: RenderDataValue) {
698
+ renderCell(data: Cell) {
674
699
  if (data && typeof data === 'object') {
675
- let content: TemplateResult = html`${data['value']}`;
700
+ let content: TemplateResult | ZincElement = html`${data.text}`;
676
701
 
677
- if (data['html']) {
678
- return html`${unsafeHTML(data['html'])}`;
679
- }
702
+ if (data.style || data.color) {
703
+ const styleStr = typeof data.style === 'string' ? data.style : '';
704
+ const tokens = new Set(styleStr.split(',').filter(Boolean));
680
705
 
681
- if (data['type'] === 'timestamp') {
682
- const timestamp = typeof data['value'] === 'string' ? parseInt(data['value']) : data['value'];
683
- content = html`${new Intl.DateTimeFormat('en-GB', {
684
- dateStyle: 'medium',
685
- timeStyle: 'short',
686
- hourCycle: 'h12',
687
- timeZone: 'UTC'
688
- }).format(new Date(timestamp < 10000000000 ? timestamp * 1000 : timestamp))}`;
689
- }
706
+ const isMono = tokens.has('mono') || tokens.has('code');
707
+ const isBorder = tokens.has('border');
708
+ const isCenter = tokens.has('center');
690
709
 
691
- if (data['tag'] === 'zn-chip') {
692
- const type = data['type'];
693
- content = html`
694
- <zn-chip type="${type}">${content}</zn-chip>`;
695
- }
710
+ if (tokens.has('bold') || tokens.has('strong')) {
711
+ content = html`<strong>${content}</strong>`;
712
+ }
713
+
714
+ if (tokens.has('italic')) {
715
+ content = html`<em>${content}</em>`;
716
+ }
696
717
 
697
- if (data['tag'] === 'code') {
698
718
  content = html`
699
- <code>${content}</code>`;
719
+ <zn-style
720
+ font="${isMono ? 'mono' : nothing}"
721
+ border="${isBorder || nothing}"
722
+ center="${isCenter || nothing}"
723
+ color="${ifDefined(data.color)}">${content}
724
+ </zn-style>`;
700
725
  }
701
726
 
702
- if (data['url']) {
727
+ if (data.uri) {
703
728
  content = html`
704
- <a href="${data['url']}"
705
- data-target="${ifDefined(data['target'])}"
706
- gaid="${ifDefined(data['gaid'])}">${content}</a>`;
729
+ <a href="${data.uri}"
730
+ data-target="${ifDefined(data.target || nothing)}"
731
+ gaid="${ifDefined(data.gaid || nothing)}">${content}</a>`;
707
732
  }
708
733
 
709
- if (data['caption']) {
710
- let title = html`<span class="title">${data['caption'].title}</span>`;
711
- let captionIcon = html``;
712
-
713
- if (data['caption'].icon) {
714
- const icon = data['caption'].icon;
715
- const src = icon['src'] ?? icon;
716
- const size = icon['size'] ?? 16;
717
- const color = icon['color'] ?? '';
718
-
719
- captionIcon = html`
720
- <zn-icon src="${src}" size="${size}" color="${color}"></zn-icon>`;
721
- }
722
-
723
- if (data['caption'].uri) {
724
- title = html` <a data-target="${ifDefined(data['caption'].target || nothing)}"
725
- href="${data['caption'].uri}"
726
- gaid="${ifDefined(data['caption'].gaid)}"
727
- class="title">
728
- ${data['caption'].title}
729
- </a>`
730
- }
731
-
732
- content = html`
733
- <div class="${classMap({
734
- 'caption': true,
735
- 'caption--icon': data['caption'].icon !== undefined,
736
- })}">
737
- ${captionIcon}
738
- <div>
739
- ${title}
740
- <span class="summary">${data['caption'].summary}</span>
741
- </div>
742
- </div>`;
734
+ if (data.chipColor) {
735
+ return html`
736
+ <zn-chip type="${data.chipColor}">${content}</zn-chip>`;
743
737
  }
744
738
 
745
- if (data['hover']) {
746
- const placement = data['hover'].placement ?? 'top';
739
+ if (data.hoverContent) {
740
+ const placement = data.hoverPlacement ?? 'top';
747
741
 
748
- if (data['hover'].icon) {
749
- const icon = data['hover'].icon;
750
- const src = icon['src'] ?? icon;
751
- const size = icon['size'] ?? 16;
752
- const color = icon['color'] ?? '';
742
+ if (data.iconSrc) {
743
+ const src = data.iconSrc;
744
+ const color = data.iconColor ?? '';
753
745
 
754
746
  return html`
755
- ${data['hover'].label}
747
+ ${content}
756
748
  <zn-hover-container placement="${placement}"
757
749
  flip>
758
- <zn-icon src="${src}" size="${size}" color="${color}"></zn-icon>
750
+ <zn-icon src="${src}" color="${color}"></zn-icon>
759
751
  <div slot="content">
760
- ${unsafeHTML(data['hover'].content)}
752
+ ${unsafeHTML(data.hoverContent)}
761
753
  </div>
762
754
  </zn-hover-container>`;
763
755
  }
764
756
 
765
757
  return html`
766
- ${data['hover'].label}
767
758
  <zn-hover-container placement="${placement}"
768
759
  flip>
769
- <div slot="anchor">${data['hover'].anchor}</div>
770
- ${unsafeHTML(data['hover'].content)}
760
+ <div slot="anchor">${content}</div>
761
+ ${unsafeHTML(data.hoverContent)}
771
762
  </zn-hover-container>`;
772
763
  }
773
764
 
774
- if (data['menu']) {
775
- return html`
776
- <zn-dropdown>
777
- <zn-button slot="trigger" icon="more_horiz" icon-size="24" color="transparent" size="content"></zn-button>
778
- <zn-menu>
779
- ${data['menu'].map((item: MenuConfig) => {
780
- return html`
781
- <zn-menu-item id="${item.id}"
782
- href="${item.href}"
783
- gaid="${item.gaid}"
784
- data-target="${ifDefined(item.target)}">
785
- ${item.label}
786
- </zn-menu-item>`;
787
- })}
788
- </zn-menu>
789
- </zn-dropdown>`
790
- }
791
-
792
- if (data['buttons']) {
793
- content = html`
794
- ${Object.values(data['buttons']).map((button) => {
795
- if (button.confirm) {
796
- return html`
797
- <div>
798
- <zn-button
799
- id="${button.id}"
800
- size="${button.size}"
801
- color="${button.color}"
802
- icon="${button.icon}"
803
- icon-size="${button.iconSize}"
804
- tooltip="${button.tooltip}"
805
- outline="${ifDefined(button.outline)}">
806
- ${button.label || nothing}
807
- </zn-button>
808
- <zn-confirm
809
- fid="${button.confirm.fid}"
810
- trigger="${button.confirm.trigger}"
811
- type="${button.confirm.type}"
812
- caption="${button.confirm.caption}"
813
- content="${button.confirm.content}"
814
- action="${button.confirm.action}"></zn-confirm>
815
- </div>`;
816
- }
817
- return html`
818
- <zn-button
819
- id="${button.id}"
820
- href="${button.href}"
821
- gaid="${button.gaid}"
822
- size="${button.size}"
823
- color="${button.color}"
824
- icon="${button.icon}"
825
- icon-size="${button.iconSize}"
826
- tooltip="${button.tooltip}"
827
- data-uri="${button.uri}"
828
- data-target="${['modal', 'slide'].includes(button.target) ? button.target : nothing}"
829
- target="${!['modal', 'slide'].includes(button.target) ? button.target : nothing}"
830
- outline="${ifDefined(button.outline)}">
831
- ${button.label || nothing}
832
- </zn-button>`;
833
- })}`;
834
- }
835
-
836
- if (data['icon']) {
837
- const icon = data['icon']?.['src'] ?? data['icon'];
838
- const size = data['icon']?.['size'] ?? 16;
839
- const color = data['icon']?.['color'] ?? '';
840
-
841
- content = html`
842
- <zn-icon src="${icon}" size="${size}" color="${color}"></zn-icon> ${content}`;
843
- }
765
+ if (data.iconSrc) {
766
+ const src = data.iconSrc;
767
+ const color = data.iconColor ?? '';
844
768
 
845
- if (data['color']) {
846
- content = html`
847
- <zn-style color="${data['color']}">${content}</zn-style>`;
769
+ return html`
770
+ <zn-icon src="${src}" color="${color}"></zn-icon> ${content}`;
848
771
  }
849
772
 
850
773
  return content;
@@ -853,6 +776,21 @@ export default class ZnDataTable extends ZincElement {
853
776
  return data;
854
777
  }
855
778
 
779
+ private updateActionKeys(slotName: string) {
780
+ if (this.hasSlotController.test(slotName)) {
781
+ // we need to look into the slot controller for the keys input
782
+ const slots = this.hasSlotController.getSlots(slotName);
783
+ if (slots) {
784
+ slots.forEach((slot) => {
785
+ const input = slot.querySelector('input[name="keys"]');
786
+ if (input && input instanceof HTMLInputElement) {
787
+ input.value = this.getSelectedKeys().join(',');
788
+ }
789
+ });
790
+ }
791
+ }
792
+ }
793
+
856
794
  private getTableSortIcon(key: any) {
857
795
  if (this.sortColumn !== key) {
858
796
  return html`
@@ -874,68 +812,33 @@ export default class ZnDataTable extends ZincElement {
874
812
  </div>`;
875
813
  }
876
814
 
877
- private renderCellHeader(key: string) {
878
- const sortable = !Object.values(this.unsortableHeaders).includes(key) && !Object.values(this.hiddenHeaders).includes(key) && !this.unsortable;
879
- let headerKeys = Object.keys(this.headers);
880
- headerKeys = headerKeys.filter((k) => !Object.values(this.hiddenColumns).includes(k));
881
- const header: string | HeaderConfig = this.headers[key];
882
-
883
- if (this.hasHeaderPosition(header)) {
884
- return html`
885
- <th
886
- class="${classMap({
887
- 'table__head': true,
888
- 'table__head--wide': key === this.wideColumn,
889
- 'table__head--last': key === headerKeys[headerKeys.length - 1],
890
- 'table__head--hidden': Object.values(this.hiddenHeaders).includes(key),
891
- [`table__head--${header.position}`]: true
892
- })}"
893
- @click="${sortable ? this.updateSort(key) : undefined}">
894
- <div>
895
- ${header.title}
896
- ${sortable ? this.getTableSortIcon(key) : nothing}
897
- </div>
898
- </th>
899
- `;
900
- }
815
+ private renderCellHeader(header: HeaderConfig) {
816
+ const sortable = !Object.values(this.unsortableHeaders).includes(header.key) && !Object.values(this.hiddenHeaders).includes(header.key) && !this.unsortable;
817
+ const lastHeader = Object.keys(this.headers).filter((key) => !Object.values(this.hiddenColumns).includes(key)).slice(-1)[0];
818
+ const lastHeaderKey = lastHeader ? this.headers[lastHeader].key : '';
901
819
 
902
820
  return html`
903
821
  <th
904
822
  class="${classMap({
905
823
  'table__head': true,
906
- 'table__head--wide': key === this.wideColumn,
907
- 'table__head--last': key === headerKeys[headerKeys.length - 1],
908
- 'table__head--hidden': Object.values(this.hiddenHeaders).includes(key),
824
+ 'table__head--wide': header.key === this.wideColumn,
825
+ 'table__head--last': header.key === lastHeaderKey,
826
+ 'table__head--hidden': Object.values(this.hiddenHeaders).includes(header.key),
909
827
  })}"
910
- @click="${sortable ? this.updateSort(key) : undefined}">
828
+ @click="${sortable ? this.updateSort(header.key) : undefined}">
911
829
  <div>
912
- ${header}
913
- ${sortable ? this.getTableSortIcon(key) : nothing}
830
+ ${header.label}
831
+ ${sortable ? this.getTableSortIcon(header.key) : nothing}
914
832
  </div>
915
833
  </th>
916
834
  `;
917
835
  }
918
836
 
919
- private renderCellBody(index: number, value: RenderDataValue) {
837
+ private renderCellBody(index: number, value: Cell) {
920
838
  const headerKeys: string[] = Object.keys(this.headers).filter(
921
839
  (key) => !Object.values(this.hiddenColumns).includes(key)
922
840
  );
923
841
  const headerKey: string = headerKeys[index];
924
- const header = this.headers[headerKey];
925
-
926
- if (this.hasHeaderPosition(header)) {
927
- return html`
928
- <td
929
- @click="${this.selectRow}"
930
- class="${classMap({
931
- 'table__cell': true,
932
- 'table__cell--wide': headerKey === this.wideColumn,
933
- 'table__cell--last': headerKey === headerKeys[headerKeys.length - 1],
934
- [`table__cell--${header.position}`]: true
935
- })}">
936
- <div>${this.renderData(value)}</div>
937
- </td>`;
938
- }
939
842
 
940
843
  return html`
941
844
  <td
@@ -945,32 +848,49 @@ export default class ZnDataTable extends ZincElement {
945
848
  'table__cell--wide': headerKey === this.wideColumn,
946
849
  'table__cell--last': headerKey === headerKeys[headerKeys.length - 1],
947
850
  })}">
948
- <div>${this.renderData(value)}</div>
851
+ <div>${this.renderCell(value)}</div>
949
852
  </td>`;
950
853
  }
951
854
 
952
- private hasHeaderPosition(header: string | HeaderConfig) {
953
- return typeof header === 'object' && header !== null && 'position' in header && 'title' in header;
954
- }
855
+ private getRows(data: Response): Row[] {
856
+ const sourceRows = data.rows;
955
857
 
956
- private getRows(keys: string[], data: TableData) {
957
- let rows = Object.values(data.data).map((row: any) => {
958
- return keys.map((header: any) => row[header]);
959
- });
858
+ if (this.sortColumn) {
859
+ const headerKeys = Object.keys(this.headers);
860
+ const sortIndex = headerKeys.indexOf(this.sortColumn);
960
861
 
961
- if (this.dataUri && this.sortColumn) {
962
- // sort the rows by the key
963
- rows = rows.sort((a, b) => {
964
- return this.sortData(a[keys.indexOf(this.sortColumn)], b[keys.indexOf(this.sortColumn)]);
862
+ sourceRows.sort((rowA: Row, rowB: Row) => {
863
+ const getCellForSort = (row: Row): Cell => {
864
+ const byHeading = row.cells.find((cell: Cell) => cell.column === this.sortColumn);
865
+ if (byHeading !== undefined) return byHeading as unknown as Cell;
866
+ return row.cells[sortIndex];
867
+ };
868
+
869
+ const aVal = getCellForSort(rowA);
870
+ const bVal = getCellForSort(rowB);
871
+ return this.sortData(aVal, bVal);
965
872
  });
966
873
  }
967
874
 
968
- return rows;
875
+ return sourceRows;
969
876
  }
970
877
 
971
- private getSelectedKeys() {
878
+ private getSelectedKeys(): (string)[] {
972
879
  const headerKeys = Object.keys(this.headers);
973
- return this.selectedRows.map((row) => row[headerKeys.indexOf(this.key)]);
880
+ const keyIndex = headerKeys.indexOf(this.key);
881
+
882
+ return this.selectedRows
883
+ .map((row: Row) => {
884
+ if (this.key === 'id' || keyIndex === -1) {
885
+ return row.id;
886
+ }
887
+ const cell = row.cells?.[keyIndex] as Cell;
888
+ if (cell.sortValue) {
889
+ return cell.sortValue;
890
+ }
891
+ return cell.text as string;
892
+ })
893
+ .filter((v): v is string => v !== undefined && v !== null);
974
894
  }
975
895
 
976
896
  private updateKeys() {
@@ -986,36 +906,27 @@ export default class ZnDataTable extends ZincElement {
986
906
  this.updateActionKeys('delete-action');
987
907
  }
988
908
 
989
- private sortData(a: RenderDataValue | string | number | object | undefined, b: RenderDataValue | string | number | object | undefined) {
990
- if (a === undefined || b === undefined) {
991
- return 0;
909
+ private extractComparable(cell: Cell): string {
910
+ const sortValue = cell.sortValue;
911
+ if (sortValue) {
912
+ return sortValue;
992
913
  }
993
914
 
994
- if (typeof a === 'object' && 'caption' in a && 'title' in a.caption) {
995
- a = a.caption.title as string;
996
- } else if (typeof a === 'object' && a && 'value' in a) {
997
- a = a.value as string | number;
998
- }
915
+ return cell.text;
916
+ }
999
917
 
1000
- if (typeof b === 'object' && 'caption' in b && 'title' in b.caption) {
1001
- b = b.caption.title as string;
1002
- } else if (typeof b === 'object' && b && 'value' in b) {
1003
- b = b.value as string | number;
1004
- }
918
+ private sortData(a: Cell, b: Cell) {
919
+ const aCompare = this.extractComparable(a);
920
+ const bCompare = this.extractComparable(b);
1005
921
 
1006
- if (typeof a === 'string') {
1007
- a = a.toLowerCase();
1008
- }
1009
-
1010
- if (typeof b === 'string') {
1011
- b = b.toLowerCase();
1012
- }
922
+ const aNormal = aCompare.toLowerCase();
923
+ const bNormal = bCompare.toLowerCase();
1013
924
 
1014
925
  if (this.sortDirection === 'asc') {
1015
- return a > b ? 1 : a < b ? -1 : 0;
926
+ return aNormal > bNormal ? 1 : aNormal < bNormal ? -1 : 0;
1016
927
  }
1017
928
 
1018
- return a < b ? 1 : a > b ? -1 : 0;
929
+ return aNormal < bNormal ? 1 : aNormal > bNormal ? -1 : 0;
1019
930
  }
1020
931
 
1021
932
  private loadingTable() {
@@ -1090,4 +1001,57 @@ export default class ZnDataTable extends ZincElement {
1090
1001
  </table>
1091
1002
  </div>`;
1092
1003
  }
1004
+
1005
+ private renderActions(row: Row) {
1006
+ if ((row.actions === null || row.actions === undefined) && this.rowHasActions) {
1007
+ // return an empty cell to keep the table structure
1008
+ return html`
1009
+ <td></td>`;
1010
+ }
1011
+
1012
+ if (!row.actions || row.actions.length === 0) {
1013
+ return html``;
1014
+ }
1015
+
1016
+ return html`
1017
+ <td class="table__cell table__cell--actions">
1018
+ <zn-dropdown placement="bottom-end">
1019
+ <zn-button slot="trigger"
1020
+ icon="more_vert"
1021
+ size="small"
1022
+ color="transparent"
1023
+ icon-size="24"
1024
+ aria-label="Row actions">
1025
+ </zn-button>
1026
+ <zn-menu>
1027
+ ${row.actions?.map((action: ActionConfig) => {
1028
+ if (action.confirmContent) {
1029
+ const triggerId = 'confirm-action-' + Math.random().toString(36).substring(2, 15);
1030
+ return html`
1031
+ <zn-confirm trigger="${triggerId}"
1032
+ type="${ifDefined(action.confirmType)}"
1033
+ caption="${action.confirmTitle}"
1034
+ content="${action.confirmContent}"
1035
+ action="${action.uri}"></zn-confirm>
1036
+ <zn-menu-item id="${triggerId}">
1037
+ ${(action.icon) ? html`
1038
+ <zn-icon src="${action.icon}" size="20" slot="prefix"></zn-icon>` : html``}
1039
+ ${action.text}
1040
+ </zn-menu-item>`;
1041
+ } else {
1042
+ return html`
1043
+ <zn-menu-item value="${action.text}"
1044
+ href="${action.uri}"
1045
+ gaid="${ifDefined(action.gaid || nothing)}"
1046
+ data-target="${ifDefined(action.target || nothing)}">
1047
+ ${(action.icon) ? html`
1048
+ <zn-icon src="${action.icon}" size="20" slot="prefix"></zn-icon>` : html``}
1049
+ ${action.text}
1050
+ </zn-menu-item>`;
1051
+ }
1052
+ })}
1053
+ </zn-menu>
1054
+ </zn-dropdown>
1055
+ </td>`;
1056
+ }
1093
1057
  }