@kubex/zinc 1.1.66 → 1.1.68

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": "@kubex/zinc",
3
- "version": "1.1.66",
3
+ "version": "1.1.68",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -36,6 +36,7 @@ const DEFAULT_PER_PAGE = 10;
36
36
 
37
37
  interface Cell {
38
38
  text: string;
39
+ subText?: string;
39
40
  column: string;
40
41
  color?: string;
41
42
  style?: string;
@@ -51,6 +52,7 @@ interface Cell {
51
52
  uri?: string;
52
53
  target?: string;
53
54
  copyable?: boolean;
55
+ title?: string;
54
56
  }
55
57
 
56
58
  interface Row {
@@ -123,7 +125,7 @@ const defaultTemplates = {
123
125
  dateTime: ((cell) => {
124
126
  const t = cell.text || '';
125
127
  return html`
126
- <div class="table__collum--datetime">
128
+ <div class="table__collum--datetime" title="${ifDefined(cell.title)}">
127
129
  <span><strong>${t.slice(0, 10)}</strong></span>
128
130
  <zn-style size="s" muted>${t.slice(11)}</zn-style>
129
131
  </div>`
@@ -178,6 +180,7 @@ type AllowedInputElement =
178
180
  * @slot create-action - Slot for create action button.
179
181
  * @slot inputs - Slot for additional input controls.
180
182
  * @slot empty-state - Slot for custom empty state.
183
+ * @slot no-results - Slot for a custom no-results state, shown when a search on a no-initial-load table returns no rows.
181
184
  *
182
185
  * @csspart base - The component's base wrapper.
183
186
  *
@@ -288,7 +291,8 @@ export default class ZnDataTable extends ZincElement {
288
291
  ActionSlots.filter.valueOf(),
289
292
  ActionSlots.sort.valueOf(),
290
293
  ActionSlots.inputs.valueOf(),
291
- 'empty-state'
294
+ 'empty-state',
295
+ 'no-results'
292
296
  );
293
297
 
294
298
  private _dataTask = new Task(this, {
@@ -501,6 +505,22 @@ export default class ZnDataTable extends ZincElement {
501
505
  }
502
506
 
503
507
  emptyState() {
508
+ if (this.noInitialLoad && !this._initialLoad) {
509
+ if (this.hasSlotController.test('no-results')) {
510
+ return html`
511
+ <div class="table--empty">
512
+ <slot name="no-results"></slot>
513
+ </div>`;
514
+ }
515
+
516
+ return html`
517
+ <div class="table--empty">
518
+ <zn-empty-state caption="No Results Found" icon="search">
519
+ <p>Nothing matched your search. Try adjusting your filters.</p>
520
+ </zn-empty-state>
521
+ </div>`;
522
+ }
523
+
504
524
  if (this.hasSlotController.test('empty-state')) {
505
525
  return html`
506
526
  <div class="table--empty">
@@ -1019,7 +1039,7 @@ export default class ZnDataTable extends ZincElement {
1019
1039
  if (header?.cellTemplate !== undefined) {
1020
1040
  data = {...header.cellTemplate, ...data}
1021
1041
  }
1022
-
1042
+
1023
1043
  let content: TemplateResult | ZincElement = html`${data.text}`;
1024
1044
 
1025
1045
  if (data.style || data.color) {
@@ -1045,55 +1065,25 @@ export default class ZnDataTable extends ZincElement {
1045
1065
  border="${isBorder || nothing}"
1046
1066
  center="${isCenter || nothing}"
1047
1067
  muted="${isMuted || nothing}"
1048
- color="${ifDefined(data.color)}">${content}
1068
+ color="${ifDefined(data.color)}"
1069
+ title="${ifDefined(data.title)}"
1070
+ >${content}
1049
1071
  </zn-style>`;
1050
1072
  }
1051
1073
 
1052
1074
  if (data.uri) {
1053
1075
  content = html`
1054
1076
  <a href="${data.uri}"
1077
+ title="${ifDefined(data.title)}"
1055
1078
  data-target="${ifDefined(data.target || nothing)}"
1056
1079
  gaid="${ifDefined(data.gaid || nothing)}">${content}</a>`;
1057
1080
  }
1058
1081
 
1059
1082
  if (data.chipColor) {
1060
- return html`
1061
- <zn-chip type="${data.chipColor}">${content}</zn-chip>`;
1083
+ content = html`
1084
+ <zn-chip type="${data.chipColor}" title="${ifDefined(data.title)}">${content}</zn-chip>`;
1062
1085
  }
1063
1086
 
1064
- if (data.hoverContent) {
1065
- const placement = data.hoverPlacement ?? 'top';
1066
-
1067
- if (data.iconSrc) {
1068
- const src = data.iconSrc;
1069
- const color = data.iconColor;
1070
- const size = data.iconSize;
1071
- const tokens = (data.iconStyle ?? '').split(',').map(s => s.trim()).filter(Boolean);
1072
-
1073
- return html`
1074
- ${content}
1075
- <zn-hover-container placement="${placement}" flip>
1076
- <zn-icon
1077
- src="${src}"
1078
- size="${ifDefined(size)}"
1079
- color="${ifDefined(color)}"
1080
- ${ref(el => {
1081
- if (!el) return;
1082
- tokens.forEach(t => el.setAttribute(t, ''));
1083
- })}
1084
- ></zn-icon>
1085
- <div slot="content">
1086
- ${unsafeHTML(data.hoverContent)}
1087
- </div>
1088
- </zn-hover-container>`;
1089
- }
1090
-
1091
- return html`
1092
- <zn-hover-container placement="${placement}" flip>
1093
- <div slot="anchor">${content}</div>
1094
- ${unsafeHTML(data.hoverContent)}
1095
- </zn-hover-container>`;
1096
- }
1097
1087
 
1098
1088
  if (data.iconSrc) {
1099
1089
  const src = data.iconSrc;
@@ -1101,24 +1091,57 @@ export default class ZnDataTable extends ZincElement {
1101
1091
  const size = data.iconSize;
1102
1092
  const tokens = (data.iconStyle ?? '').split(',').map(s => s.trim()).filter(Boolean);
1103
1093
 
1104
- return html`
1094
+ content = html`
1105
1095
  <zn-icon
1106
1096
  src="${src}"
1107
1097
  size="${ifDefined(size)}"
1108
1098
  color="${ifDefined(color)}"
1109
1099
  ${ref(el => {
1110
- if (!el) return;
1111
- tokens.forEach(t => el.setAttribute(t, ''));
1112
- })}
1100
+ if (!el) return;
1101
+ tokens.forEach(t => el.setAttribute(t, ''));
1102
+ })}
1103
+ title="${ifDefined(data.title)}"
1113
1104
  ></zn-icon> ${content}`;
1114
1105
  }
1115
1106
 
1107
+ if (data.hoverContent) {
1108
+ const placement = data.hoverPlacement ?? 'top';
1109
+ if (data.iconSrc) {
1110
+ content = html`
1111
+ <zn-hover-container placement="${placement}" flip>
1112
+ ${content}
1113
+ <div slot="content">
1114
+ ${unsafeHTML(data.hoverContent)}
1115
+ </div>
1116
+ </zn-hover-container>`;
1117
+ } else {
1118
+ content = html`
1119
+ <zn-hover-container placement="${placement}" flip>
1120
+ ${content}
1121
+ <div slot="content">
1122
+ ${unsafeHTML(data.hoverContent)}
1123
+ </div>
1124
+ </zn-hover-container>`;
1125
+ }
1126
+ }
1127
+
1116
1128
  if (data.copyable) {
1117
- return html`
1129
+ content = html`
1118
1130
  ${content}
1119
1131
  <zn-copy-button value="${data.text}"></zn-copy-button>`;
1120
1132
  }
1121
1133
 
1134
+
1135
+ if (data.subText) {
1136
+ content = html`
1137
+ <div>
1138
+ <div>${content}</div>
1139
+ <span class="table__cell--subtext">${data.subText}</span>
1140
+ </div>
1141
+ `
1142
+ }
1143
+
1144
+
1122
1145
  return content;
1123
1146
 
1124
1147
  }
@@ -1199,7 +1222,7 @@ export default class ZnDataTable extends ZincElement {
1199
1222
  return !header.secondary;
1200
1223
  });
1201
1224
  const header: HeaderConfig | undefined = filteredHeaders[index];
1202
- const headerKey: string = header?.key;
1225
+ const headerKey: string | undefined = header?.key;
1203
1226
 
1204
1227
  return html`
1205
1228
  <td
@@ -444,3 +444,7 @@ tbody tr.table__row--error td {
444
444
  align-items: flex-end;
445
445
  gap:2px;
446
446
  }
447
+
448
+ .table__cell--subtext{
449
+ @include wc.text-style(subheading-small);
450
+ }
@@ -1,5 +1,6 @@
1
1
  import '../../../dist/zn.min.js';
2
2
  import { expect, fixture, html, waitUntil } from '@open-wc/testing';
3
+ import { render } from 'lit';
3
4
  import type ZnDataTable from './data-table.component';
4
5
 
5
6
  describe('<zn-data-table>', () => {
@@ -73,9 +74,96 @@ describe('<zn-data-table>', () => {
73
74
  }
74
75
  });
75
76
 
77
+ it('shows the empty-state slot before any load on a no-initial-load table, then a no-results state after an empty search', async () => {
78
+ const originalFetch = window.fetch;
79
+ window.fetch = () => Promise.resolve(new Response(JSON.stringify({rows: [], page: 1, perPage: 10, total: 0}),
80
+ {status: 200, headers: {'Content-Type': 'application/json'}}));
81
+
82
+ try {
83
+ const el = await fixture<ZnDataTable>(html`
84
+ <zn-data-table data-uri="/test-data" no-initial-load headers='{"name": {"key": "name", "label": "Name"}}'>
85
+ <div slot="empty-state" id="filters-prompt">No Filters Selected</div>
86
+ </zn-data-table>`);
87
+
88
+ expect(el.shadowRoot!.querySelector('slot[name="empty-state"]')).to.exist;
89
+
90
+ el.refresh();
91
+ await waitUntil(() => el.shadowRoot?.querySelector('zn-empty-state'));
92
+
93
+ expect(el.shadowRoot!.querySelector('slot[name="empty-state"]')).to.not.exist;
94
+ const emptyState = el.shadowRoot!.querySelector('zn-empty-state')!;
95
+ expect(emptyState.getAttribute('caption')).to.equal('No Results Found');
96
+ } finally {
97
+ window.fetch = originalFetch;
98
+ }
99
+ });
100
+
101
+ it('prefers the no-results slot after an empty search on a no-initial-load table', async () => {
102
+ const originalFetch = window.fetch;
103
+ window.fetch = () => Promise.resolve(new Response(JSON.stringify({rows: [], page: 1, perPage: 10, total: 0}),
104
+ {status: 200, headers: {'Content-Type': 'application/json'}}));
105
+
106
+ try {
107
+ const el = await fixture<ZnDataTable>(html`
108
+ <zn-data-table data-uri="/test-data" no-initial-load headers='{"name": {"key": "name", "label": "Name"}}'>
109
+ <div slot="empty-state">No Filters Selected</div>
110
+ <div slot="no-results">No Customers Found</div>
111
+ </zn-data-table>`);
112
+
113
+ el.refresh();
114
+ await waitUntil(() => el.shadowRoot?.querySelector('slot[name="no-results"]'));
115
+
116
+ expect(el.shadowRoot!.querySelector('slot[name="no-results"]')).to.exist;
117
+ expect(el.shadowRoot!.querySelector('slot[name="empty-state"]')).to.not.exist;
118
+ } finally {
119
+ window.fetch = originalFetch;
120
+ }
121
+ });
122
+
76
123
  it('exposes displayTemplates as an object property', async () => {
77
124
  const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
78
125
  const templates = (el as unknown as {displayTemplates: Record<string, unknown>}).displayTemplates;
79
126
  expect(templates).to.be.an('object');
80
127
  });
128
+
129
+ it('renders subText as muted secondary text', async () => {
130
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
131
+ const container = document.createElement('div');
132
+ render(el.renderCell({text: 'Main', column: 'name', subText: 'Secondary'}), container);
133
+
134
+ const subtext = container.querySelector('.table__cell--subtext');
135
+ expect(subtext).to.exist;
136
+ expect(subtext?.textContent).to.contain('Secondary');
137
+ expect(container.textContent).to.contain('Main');
138
+ });
139
+
140
+ it('applies title as a tooltip on link cells', async () => {
141
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
142
+ const container = document.createElement('div');
143
+ render(el.renderCell({text: 'Go', column: 'name', uri: 'https://example.com', title: 'Tip'}), container);
144
+
145
+ const link = container.querySelector('a');
146
+ expect(link).to.exist;
147
+ expect(link?.getAttribute('title')).to.equal('Tip');
148
+ });
149
+
150
+ it('applies title as a tooltip on styled cells', async () => {
151
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
152
+ const container = document.createElement('div');
153
+ render(el.renderCell({text: 'Val', column: 'name', style: 'bold', title: 'Info'}), container);
154
+
155
+ const styled = container.querySelector('zn-style');
156
+ expect(styled).to.exist;
157
+ expect(styled?.getAttribute('title')).to.equal('Info');
158
+ });
159
+
160
+ it('omits the title attribute when no title is provided', async () => {
161
+ const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
162
+ const container = document.createElement('div');
163
+ render(el.renderCell({text: 'Plain', column: 'name', uri: 'https://example.com'}), container);
164
+
165
+ const link = container.querySelector('a');
166
+ expect(link).to.exist;
167
+ expect(link?.hasAttribute('title')).to.be.false;
168
+ });
81
169
  });
@@ -1,15 +1,23 @@
1
- import {type CSSResultGroup, html, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
1
+ import {type CSSResultGroup, html, nothing, type PropertyValues, type TemplateResult, unsafeCSS} from 'lit';
2
2
  import {FormControlController} from '../../internal/form';
3
3
  import {ifDefined} from 'lit/directives/if-defined.js';
4
4
  import {property, query} from 'lit/decorators.js';
5
5
  import {watch} from '../../internal/watch';
6
6
  import ZincElement from '../../internal/zinc-element';
7
+ import ZnButton from '../button';
8
+ import ZnDropdown from '../dropdown';
9
+ import ZnInput from '../input';
10
+ import ZnOption from '../option';
11
+ import ZnSelect from '../select';
7
12
  import type {ZincFormControl} from '../../internal/zinc-element';
8
- import type ZnDropdown from '../dropdown';
9
- import type ZnInput from '../input';
10
13
 
11
14
  import styles from './defined-label.scss';
12
15
 
16
+ interface PredefinedLabel {
17
+ name: string;
18
+ options?: string[];
19
+ }
20
+
13
21
  /**
14
22
  * @summary This component provides a labeled input with support for predefined and custom labels,
15
23
  * allowing users to select or enter label-value pairs within a dropdown interface.
@@ -21,31 +29,51 @@ import styles from './defined-label.scss';
21
29
  * @dependency zn-dropdown
22
30
  * @dependency zn-input
23
31
  * @dependency zn-option
24
- * @dependency zn-panel
25
32
  * @dependency zn-select
26
- * @dependency zn-sp
27
33
  *
28
34
  * @csspart input - The component's main input.
29
35
  * @csspart input-value - The label's value inputs.
30
36
  */
31
37
  export default class ZnDefinedLabel extends ZincElement implements ZincFormControl {
32
38
  static styles: CSSResultGroup = unsafeCSS(styles);
39
+ static dependencies = {
40
+ 'zn-button': ZnButton,
41
+ 'zn-dropdown': ZnDropdown,
42
+ 'zn-input': ZnInput,
43
+ 'zn-option': ZnOption,
44
+ 'zn-select': ZnSelect
45
+ };
33
46
 
34
47
  private readonly formControlController = new FormControlController(this, {
35
48
  value: (control: this) => control.value + (control.inputValue ? `:${control.inputValue}` : ''),
36
49
  });
37
50
 
38
- @query('.input__control') input: ZnInput;
51
+ @query('.defined-label__input') input: ZnInput;
39
52
  @query('.defined-label__dropdown') dropdown: ZnDropdown;
40
53
 
54
+ /** The selected label key. Also acts as the filter while typing. */
41
55
  @property() value: string = '';
56
+
57
+ /** The value entered for the selected label. Submitted as `label:value` when present. */
42
58
  @property() inputValue: string = '';
59
+
60
+ /** The size of the main input. */
43
61
  @property({attribute: 'input-size', reflect: true}) inputSize: 'x-small' | 'small' | 'medium' | 'large' = 'medium';
62
+
63
+ /** The name of the control. Used for form submission. */
44
64
  @property() name: string = 'label';
65
+
66
+ /** The title of the main input. */
45
67
  @property() title: string;
68
+
69
+ /** Disables the control. */
46
70
  @property({type: Boolean}) disabled: boolean = false;
71
+
72
+ /** Allows labels that aren't in the predefined list. */
47
73
  @property({attribute: 'allow-custom', type: Boolean}) allowCustom: boolean = false;
48
- @property({type: Array, attribute: 'predefined-labels'}) predefinedLabels = [];
74
+
75
+ /** The predefined labels. Entries are either a string or `{name, options}`. */
76
+ @property({type: Array, attribute: 'predefined-labels'}) predefinedLabels: (PredefinedLabel | string)[] = [];
49
77
 
50
78
  get validationMessage() {
51
79
  return this.input.validationMessage;
@@ -83,6 +111,14 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
83
111
  this.formControlController.updateValidity();
84
112
  }
85
113
 
114
+ private getFilteredLabels(): PredefinedLabel[] {
115
+ const filter = this.value.toLowerCase();
116
+ return this.predefinedLabels
117
+ .map(label => typeof label === 'string' ? {name: label} : label)
118
+ .filter((label): label is PredefinedLabel => Boolean(label?.name))
119
+ .filter(label => !filter || label.name.toLowerCase().includes(filter));
120
+ }
121
+
86
122
  private handleChange() {
87
123
  if (!this.dropdown.open) {
88
124
  this.dropdown.show().then(r => r);
@@ -117,14 +153,18 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
117
153
  if (target.hasAttribute('data-label')) this.value = target.getAttribute('data-label') ?? "";
118
154
  }
119
155
 
120
- private handleInputValueInput(e: Event) {
121
- const target = e.target as HTMLInputElement | HTMLSelectElement;
122
- this.inputValue = target.value.toLowerCase();
123
-
124
- if (target.hasAttribute('data-label')) this.value = target.getAttribute('data-label') ?? "";
125
- }
156
+ private handleFormSubmit(e: Event, label?: string) {
157
+ // Sync from the submitted row so a partially typed filter never wins over
158
+ // the row's actual label key, and stale values from other rows are discarded
159
+ const row = (e.currentTarget as HTMLElement).closest('.defined-label__row');
160
+ const control = row?.querySelector<ZnInput | ZnSelect>('.defined-label__value');
161
+ if (label) {
162
+ this.value = label;
163
+ }
164
+ if (control) {
165
+ this.inputValue = String(control.value ?? '').toLowerCase();
166
+ }
126
167
 
127
- private handleFormSubmit() {
128
168
  const form = this.formControlController.getForm();
129
169
 
130
170
  if (form && form.reportValidity()) {
@@ -135,106 +175,89 @@ export default class ZnDefinedLabel extends ZincElement implements ZincFormContr
135
175
  }
136
176
  }
137
177
 
138
- render() {
139
- let predefinedLabels = html``;
140
-
141
- if (this.predefinedLabels.length > 0) {
142
- this.predefinedLabels.forEach((label: { [key: string]: any } | string | null) => {
143
- // label = ['name' => 'label', 'options'=>['one', 'two', 'three']]
144
- let options: string[] | undefined;
145
- if (label && typeof label !== 'string') {
146
- options = label.options as string[];
147
- label = label.name as string;
148
- }
149
-
150
- if (this.value && !label?.toLowerCase().includes(this.value.toLowerCase())) {
151
- return;
152
- }
153
-
154
- let selector: TemplateResult<1>;
155
- if (options && options.length > 0) {
156
- selector = html`
157
- <zn-select
158
- part="input-value"
159
- id="input-value input-value-${label}"
160
- class="input__control-value input__control-value--${label}"
161
- data-label="${label}"
162
- @zn-change="${this.handleInputValueChange}"
163
- @zn-input="${this.handleInputValueInput}"
164
- size="small">
165
- <zn-option value="">Select ${label}</zn-option>
166
- ${options.map((option: string) => html`
167
- <zn-option value="${option}">${option}</zn-option>
168
- `)}
169
- </zn-select>`;
170
- } else {
171
- selector = html`
172
- <zn-input
173
- part="input-value"
174
- id="input-value input-value-${label}"
175
- class="input__control-value input__control-value--${label}"
176
- type="text"
177
- data-label="${label}"
178
- @zn-change="${this.handleInputValueChange}"
179
- @zn-input="${this.handleInputValueInput}"
180
- size="small"></zn-input>`;
181
- }
182
-
183
- predefinedLabels = html`
184
- <div class="defined-label__input">
185
- <small>${label}</small>
186
- <div class="defined-label__input-wrap">
187
- ${selector}
188
- <zn-button type="submit" icon="add" @click="${this.handleFormSubmit}"></zn-button>
189
- </div>
190
- </div>`;
191
- });
178
+ private renderValueControl(label: PredefinedLabel): TemplateResult {
179
+ if (label.options && label.options.length > 0) {
180
+ return html`
181
+ <zn-select
182
+ part="input-value"
183
+ class="defined-label__value"
184
+ data-label="${label.name}"
185
+ size="small"
186
+ @zn-change="${this.handleInputValueChange}"
187
+ @zn-input="${this.handleInputValueChange}">
188
+ <zn-option value="">Select ${label.name}</zn-option>
189
+ ${label.options.map(option => html`
190
+ <zn-option value="${option}">${option}</zn-option>`)}
191
+ </zn-select>`;
192
192
  }
193
193
 
194
194
  return html`
195
- <zn-dropdown class="defined-label__dropdown">
195
+ <zn-input
196
+ part="input-value"
197
+ class="defined-label__value"
198
+ type="text"
199
+ placeholder="Label Value"
200
+ data-label="${label.name}"
201
+ size="small"
202
+ @zn-change="${this.handleInputValueChange}"
203
+ @zn-input="${this.handleInputValueChange}"></zn-input>`;
204
+ }
205
+
206
+ private renderRow(name: string, control: TemplateResult, label?: string): TemplateResult {
207
+ return html`
208
+ <div class="defined-label__row">
209
+ <small class="defined-label__row-label">${name}</small>
210
+ <div class="defined-label__row-controls">
211
+ ${control}
212
+ <zn-button
213
+ type="submit"
214
+ icon="add"
215
+ @click="${(e: Event) => this.handleFormSubmit(e, label)}"></zn-button>
216
+ </div>
217
+ </div>`;
218
+ }
219
+
220
+ render() {
221
+ const labels = this.getFilteredLabels();
222
+
223
+ return html`
224
+ <zn-dropdown class="defined-label__dropdown" sync="width">
196
225
  <zn-input
197
226
  part="input"
198
227
  id="input"
199
- class="input__control"
228
+ class="defined-label__input"
200
229
  type="text"
201
- title="${this.title}"
230
+ title="${ifDefined(this.title)}"
202
231
  value="${this.value}"
203
232
  name="${ifDefined(this.name)}"
204
233
  placeholder="Add a Label"
205
234
  maxlength="60"
206
235
  autocomplete="off"
207
236
  size="${this.inputSize}"
237
+ ?disabled="${this.disabled}"
208
238
  @zn-change="${this.handleChange}"
209
239
  @zn-input="${this.handleInput}"
210
240
  @click="${this.handleClick}"
211
241
  slot="trigger"
212
242
  ></zn-input>
213
243
 
214
- <zn-panel class="defined-label__container">
215
- <zn-sp flush divide>
216
- ${predefinedLabels.values.length > 0 ? predefinedLabels : html`
217
- <div class="defined-label__input">
218
- <small>Cannot find any predefined labels</small>
219
- </div>`}
220
-
221
- ${this.allowCustom && this.value !== '' ? html`
222
- <div class="defined-label__input">
223
- <small>${this.value}</small>
224
- <div class="defined-label__input-wrap">
225
- <zn-input part="input-value"
226
- placeholder="Label Value"
227
- type="text"
228
- size="small"
229
- maxlength="60"
230
- @zn-change="${this.handleInputValueChange}"
231
- @zn-input="${this.handleInputValueInput}"></zn-input>
232
- <zn-button type="submit" icon="add" @click="${this.handleFormSubmit}"></zn-button>
233
- </div>
234
- </div>` : ''}
235
-
236
- </zn-sp>
237
- </zn-panel>
244
+ <div class="defined-label__panel">
245
+ ${labels.length > 0
246
+ ? labels.map(label => this.renderRow(label.name, this.renderValueControl(label), label.name))
247
+ : html`
248
+ <div class="defined-label__empty">Cannot find any predefined labels</div>`}
249
+
250
+ ${this.allowCustom && this.value !== '' ? this.renderRow(this.value, html`
251
+ <zn-input
252
+ part="input-value"
253
+ class="defined-label__value"
254
+ placeholder="Label Value"
255
+ type="text"
256
+ size="small"
257
+ maxlength="60"
258
+ @zn-change="${this.handleInputValueChange}"
259
+ @zn-input="${this.handleInputValueChange}"></zn-input>`) : nothing}
260
+ </div>
238
261
  </zn-dropdown>
239
262
  `;
240
263
  }