@milaboratories/uikit 2.2.95 → 2.2.96

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 (53) hide show
  1. package/.turbo/turbo-build.log +29 -24
  2. package/.turbo/turbo-type-check.log +1 -1
  3. package/CHANGELOG.md +9 -0
  4. package/dist/components/DataTable/TableComponent.vue.js +1 -1
  5. package/dist/components/PlAccordion/PlAccordionSection.vue2.js +21 -21
  6. package/dist/components/PlAccordion/PlAccordionSection.vue2.js.map +1 -1
  7. package/dist/components/PlAutocomplete/PlAutocomplete.vue.js.map +1 -1
  8. package/dist/components/PlDropdown/OptionList.vue.d.ts +77 -0
  9. package/dist/components/PlDropdown/OptionList.vue.d.ts.map +1 -0
  10. package/dist/components/PlDropdown/OptionList.vue.js +88 -0
  11. package/dist/components/PlDropdown/OptionList.vue.js.map +1 -0
  12. package/dist/components/PlDropdown/OptionList.vue2.js +5 -0
  13. package/dist/components/PlDropdown/OptionList.vue2.js.map +1 -0
  14. package/dist/components/PlDropdown/PlDropdown.vue.d.ts.map +1 -1
  15. package/dist/components/PlDropdown/PlDropdown.vue.js +110 -122
  16. package/dist/components/PlDropdown/PlDropdown.vue.js.map +1 -1
  17. package/dist/components/PlDropdown/types.d.ts +7 -0
  18. package/dist/components/PlDropdown/types.d.ts.map +1 -0
  19. package/dist/components/PlDropdown/useGroupBy.d.ts +7 -0
  20. package/dist/components/PlDropdown/useGroupBy.d.ts.map +1 -0
  21. package/dist/components/PlDropdown/useGroupBy.js +36 -0
  22. package/dist/components/PlDropdown/useGroupBy.js.map +1 -0
  23. package/dist/components/PlDropdownRef/PlDropdownRef.vue.d.ts +1 -1
  24. package/dist/components/PlDropdownRef/PlDropdownRef.vue.d.ts.map +1 -1
  25. package/dist/components/PlDropdownRef/PlDropdownRef.vue.js +11 -10
  26. package/dist/components/PlDropdownRef/PlDropdownRef.vue.js.map +1 -1
  27. package/dist/components/PlSlideModal/PlSlideModal.vue.js +1 -1
  28. package/dist/helpers/utils.d.ts +1 -0
  29. package/dist/helpers/utils.d.ts.map +1 -1
  30. package/dist/helpers/utils.js +2 -1
  31. package/dist/helpers/utils.js.map +1 -1
  32. package/dist/sdk/model/dist/index.js +1 -1
  33. package/dist/sdk/model/dist/index.js.map +1 -1
  34. package/dist/types.d.ts +4 -14
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/utils/TextLabel.vue.d.ts +18 -0
  37. package/dist/utils/TextLabel.vue.d.ts.map +1 -0
  38. package/dist/utils/TextLabel.vue.js +26 -0
  39. package/dist/utils/TextLabel.vue.js.map +1 -0
  40. package/dist/utils/TextLabel.vue2.js +13 -0
  41. package/dist/utils/TextLabel.vue2.js.map +1 -0
  42. package/package.json +3 -3
  43. package/src/components/PlAccordion/PlAccordionSection.vue +3 -3
  44. package/src/components/PlAutocomplete/PlAutocomplete.vue +1 -1
  45. package/src/components/PlDropdown/OptionList.vue +71 -0
  46. package/src/components/PlDropdown/PlDropdown.vue +29 -25
  47. package/src/components/PlDropdown/pl-dropdown.scss +4 -0
  48. package/src/components/PlDropdown/types.ts +3 -0
  49. package/src/components/PlDropdown/useGroupBy.ts +63 -0
  50. package/src/components/PlDropdownRef/PlDropdownRef.vue +1 -0
  51. package/src/helpers/utils.ts +1 -0
  52. package/src/types.ts +5 -15
  53. package/src/utils/TextLabel.vue +43 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/uikit",
3
- "version": "2.2.95",
3
+ "version": "2.2.96",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -19,8 +19,8 @@
19
19
  "sortablejs": "^1.15.6",
20
20
  "vue": "^3.5.13",
21
21
  "d3": "^7.9.0",
22
- "@milaboratories/helpers": "^1.6.15",
23
- "@platforma-sdk/model": "^1.37.11"
22
+ "@platforma-sdk/model": "^1.37.14",
23
+ "@milaboratories/helpers": "^1.6.16"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/d3": "^7.4.3",
@@ -4,15 +4,15 @@ import { PlMaskIcon16 } from '../PlMaskIcon16';
4
4
  import { PlSectionSeparator } from '../PlSectionSeparator';
5
5
  import ExpandTransition from './ExpandTransition.vue';
6
6
  import type { Ref } from 'vue';
7
- import { computed, inject } from 'vue';
7
+ import { computed, inject, toRef } from 'vue';
8
8
 
9
- const $m = inject<Ref<string>>('pl-accordion-model');
9
+ const $m = inject<Ref<string>>('pl-accordion-model', () => toRef(''), true);
10
10
 
11
11
  const $p = inject<
12
12
  Ref<{
13
13
  multiple?: boolean;
14
14
  }>
15
- >('pl-accordion-props');
15
+ >('pl-accordion-props', () => toRef({ multiple: false }), true);
16
16
 
17
17
  const model = defineModel<boolean>();
18
18
 
@@ -9,7 +9,7 @@ export default {
9
9
 
10
10
  <script lang="ts" setup generic="M = unknown">
11
11
  import './pl-autocomplete.scss';
12
- import { computed, reactive, ref, unref, useSlots, useTemplateRef, watch, watchPostEffect } from 'vue';
12
+ import { computed, reactive, ref, unref, useTemplateRef, watch, watchPostEffect } from 'vue';
13
13
  import { tap } from '../../helpers/functions';
14
14
  import { PlTooltip } from '../PlTooltip';
15
15
  import DoubleContour from '../../utils/DoubleContour.vue';
@@ -0,0 +1,71 @@
1
+ <script lang="ts" setup>
2
+ import DropdownListItem from '../DropdownListItem.vue';
3
+ import { DropdownOverlay } from '../../utils/DropdownOverlay';
4
+ import TextLabel from '../../utils/TextLabel.vue';
5
+ import { computed, useTemplateRef } from 'vue';
6
+ import type { LOption } from './types';
7
+
8
+ const props = defineProps<{
9
+ rootRef: HTMLElement;
10
+ groups: Map<string, LOption[]>;
11
+ rest: LOption[];
12
+ optionSize: 'small' | 'medium';
13
+ selectOption: (v: unknown) => void;
14
+ }>();
15
+
16
+ const overlay = useTemplateRef('overlay');
17
+
18
+ const listRef = computed(() => overlay.value?.listRef);
19
+
20
+ const hasGroups = computed(() => props.groups.size > 0);
21
+
22
+ const optionsLength = computed(() => {
23
+ let totalGroupItems = 0;
24
+ for (const items of props.groups.values()) {
25
+ totalGroupItems += items.length;
26
+ }
27
+ return totalGroupItems + props.rest.length;
28
+ });
29
+
30
+ const scrollIntoActive = () => {
31
+ overlay.value?.scrollIntoActive();
32
+ };
33
+
34
+ defineExpose({
35
+ scrollIntoActive,
36
+ listRef,
37
+ });
38
+ </script>
39
+
40
+ <template>
41
+ <DropdownOverlay ref="overlay" :root="rootRef" class="pl-dropdown__options" tabindex="-1" :gap="3">
42
+ <div v-for="[group, items] in groups.entries()" :key="group" :class="{ 'group-container': hasGroups }">
43
+ <TextLabel>{{ group }}</TextLabel>
44
+ <div>
45
+ <DropdownListItem
46
+ v-for="(item, index) in items"
47
+ :key="index"
48
+ :option="item"
49
+ :is-selected="item.isSelected"
50
+ :is-hovered="item.isActive"
51
+ :size="optionSize"
52
+ @click.stop="selectOption(item.value)"
53
+ />
54
+ </div>
55
+ </div>
56
+ <div v-if="rest.length" :class="{ 'group-container': hasGroups }">
57
+ <TextLabel />
58
+ <div>
59
+ <DropdownListItem
60
+ v-for="(item, index) in rest"
61
+ :key="index" :option="item"
62
+ :is-selected="item.isSelected"
63
+ :is-hovered="item.isActive"
64
+ :size="optionSize"
65
+ @click.stop="selectOption(item.value)"
66
+ />
67
+ </div>
68
+ </div>
69
+ <div v-if="!optionsLength" class="nothing-found">Nothing found</div>
70
+ </DropdownOverlay>
71
+ </template>
@@ -9,21 +9,22 @@ export default {
9
9
 
10
10
  <script lang="ts" setup generic="M = unknown">
11
11
  import './pl-dropdown.scss';
12
- import { computed, reactive, ref, unref, useSlots, useTemplateRef, watch, watchPostEffect } from 'vue';
12
+ import { computed, reactive, ref, unref, useTemplateRef, watch, watchPostEffect } from 'vue';
13
13
  import { tap } from '../../helpers/functions';
14
14
  import { PlTooltip } from '../PlTooltip';
15
15
  import DoubleContour from '../../utils/DoubleContour.vue';
16
16
  import { useLabelNotch } from '../../utils/useLabelNotch';
17
17
  import type { ListOption, ListOptionNormalized } from '../../types';
18
18
  import { deepEqual } from '../../helpers/objects';
19
- import DropdownListItem from '../DropdownListItem.vue';
20
19
  import LongText from '../LongText.vue';
21
20
  import { normalizeListOptions } from '../../helpers/utils';
22
21
  import { PlIcon16 } from '../PlIcon16';
23
22
  import { PlMaskIcon24 } from '../PlMaskIcon24';
24
- import { DropdownOverlay } from '../../utils/DropdownOverlay';
25
23
  import SvgRequired from '../../generated/components/svg/images/SvgRequired.vue';
26
24
  import { getErrorMessage } from '../../helpers/error.ts';
25
+ import OptionList from './OptionList.vue';
26
+ import { useGroupBy } from './useGroupBy';
27
+ import type { LOption } from './types';
27
28
 
28
29
  const emit = defineEmits<{
29
30
  /**
@@ -110,7 +111,7 @@ const slots = defineSlots<{
110
111
  const rootRef = ref<HTMLElement | undefined>();
111
112
  const input = ref<HTMLInputElement | undefined>();
112
113
 
113
- const overlayRef = useTemplateRef('overlay');
114
+ const optionListRef = useTemplateRef<InstanceType<typeof OptionList>>('optionListRef');
114
115
 
115
116
  const data = reactive({
116
117
  search: '',
@@ -121,7 +122,7 @@ const data = reactive({
121
122
 
122
123
  const findActiveIndex = () =>
123
124
  tap(
124
- filteredRef.value.findIndex((o) => deepEqual(o.value, props.modelValue)),
125
+ orderedRef.value.findIndex((o) => deepEqual(o.value, props.modelValue)),
125
126
  (v) => (v < 0 ? 0 : v),
126
127
  );
127
128
 
@@ -159,7 +160,7 @@ const computedError = computed(() => {
159
160
  return undefined;
160
161
  });
161
162
 
162
- const optionsRef = computed(() =>
163
+ const optionsRef = computed<LOption<M>[]>(() =>
163
164
  normalizeListOptions(props.options ?? []).map((opt, index) => ({
164
165
  ...opt,
165
166
  index,
@@ -214,6 +215,8 @@ const filteredRef = computed(() => {
214
215
  return options;
215
216
  });
216
217
 
218
+ const { orderedRef, groupsRef, restRef } = useGroupBy(filteredRef, 'group');
219
+
217
220
  const tabindex = computed(() => (isDisabled.value ? undefined : '0'));
218
221
 
219
222
  const selectOption = (v: M | undefined) => {
@@ -223,6 +226,10 @@ const selectOption = (v: M | undefined) => {
223
226
  rootRef?.value?.focus();
224
227
  };
225
228
 
229
+ const selectOptionWrapper = (v: unknown) => {
230
+ selectOption(v as M | undefined);
231
+ };
232
+
226
233
  const clear = () => emit('update:modelValue', undefined);
227
234
 
228
235
  const setFocusOnInput = () => input.value?.focus();
@@ -239,7 +246,7 @@ const onInputFocus = () => (data.open = true);
239
246
  const onFocusOut = (event: FocusEvent) => {
240
247
  const relatedTarget = event.relatedTarget as Node | null;
241
248
 
242
- if (!rootRef.value?.contains(relatedTarget) && !overlayRef.value?.listRef?.contains(relatedTarget)) {
249
+ if (!rootRef.value?.contains(relatedTarget) && !optionListRef.value?.listRef?.contains(relatedTarget)) {
243
250
  data.search = '';
244
251
  data.open = false;
245
252
  }
@@ -266,25 +273,25 @@ const handleKeydown = (e: { code: string; preventDefault(): void }) => {
266
273
  rootRef.value?.focus();
267
274
  }
268
275
 
269
- const filtered = unref(filteredRef);
276
+ const ordered = orderedRef.value;
270
277
 
271
- const { length } = filtered;
278
+ const { length } = ordered;
272
279
 
273
280
  if (!length) {
274
281
  return;
275
282
  }
276
283
 
277
284
  if (e.code === 'Enter') {
278
- selectOption(filtered.find((it) => it.index === activeIndex)?.value);
285
+ selectOption(ordered.find((it) => it.index === activeIndex)?.value);
279
286
  }
280
287
 
281
- const localIndex = filtered.findIndex((it) => it.index === activeIndex) ?? -1;
288
+ const localIndex = ordered.findIndex((it) => it.index === activeIndex) ?? -1;
282
289
 
283
290
  const delta = e.code === 'ArrowDown' ? 1 : e.code === 'ArrowUp' ? -1 : 0;
284
291
 
285
292
  const newIndex = Math.abs(localIndex + delta + length) % length;
286
293
 
287
- data.activeIndex = filteredRef.value[newIndex].index ?? -1;
294
+ data.activeIndex = ordered[newIndex].index ?? -1;
288
295
  };
289
296
 
290
297
  useLabelNotch(rootRef);
@@ -301,7 +308,7 @@ watchPostEffect(() => {
301
308
  data.search; // to watch
302
309
 
303
310
  if (data.activeIndex >= 0 && data.open) {
304
- overlayRef.value?.scrollIntoActive();
311
+ optionListRef.value?.scrollIntoActive();
305
312
  }
306
313
  });
307
314
  </script>
@@ -354,18 +361,15 @@ watchPostEffect(() => {
354
361
  </template>
355
362
  </PlTooltip>
356
363
  </label>
357
- <DropdownOverlay v-if="data.open" ref="overlay" :root="rootRef" class="pl-dropdown__options" tabindex="-1" :gap="3">
358
- <DropdownListItem
359
- v-for="(item, index) in filteredRef"
360
- :key="index"
361
- :option="item"
362
- :is-selected="item.isSelected"
363
- :is-hovered="item.isActive"
364
- :size="optionSize"
365
- @click.stop="selectOption(item.value)"
366
- />
367
- <div v-if="!filteredRef.length" class="nothing-found">Nothing found</div>
368
- </DropdownOverlay>
364
+ <OptionList
365
+ v-if="data.open"
366
+ ref="optionListRef"
367
+ :root-ref="rootRef!"
368
+ :groups="groupsRef"
369
+ :rest="restRef"
370
+ :option-size="optionSize"
371
+ :select-option="selectOptionWrapper"
372
+ />
369
373
  <DoubleContour class="pl-dropdown__contour" />
370
374
  </div>
371
375
  </div>
@@ -22,6 +22,10 @@
22
22
  font-style: italic;
23
23
  }
24
24
 
25
+ .group-container {
26
+ padding: 4px 0;
27
+ }
28
+
25
29
  .option {
26
30
  position: relative;
27
31
  padding: 0 30px 0 10px;
@@ -0,0 +1,3 @@
1
+ import type { ListOptionNormalized } from '../../types';
2
+
3
+ export type LOption<M = unknown> = ListOptionNormalized<M> & { isSelected: boolean; isActive: boolean; index: number };
@@ -0,0 +1,63 @@
1
+ import type { Ref } from 'vue';
2
+ import { computed } from 'vue';
3
+
4
+ function groupBy<T, K extends keyof T>(
5
+ list: T[],
6
+ groupBy: K,
7
+ ): {
8
+ grouped: Map<NonNullable<T[K]>, T[]>;
9
+ rest: T[];
10
+ ordered: T[];
11
+ } {
12
+ const grouped: Map<NonNullable<T[K]>, T[]> = new Map();
13
+
14
+ if (!list) {
15
+ return {
16
+ grouped,
17
+ rest: [],
18
+ ordered: [],
19
+ };
20
+ }
21
+
22
+ // Group items by the specified key
23
+ for (const item of list) {
24
+ const key = item[groupBy];
25
+ if (key === undefined) continue;
26
+ if (key === null) continue;
27
+ if (!grouped.has(key)) grouped.set(key, []);
28
+ grouped.get(key)?.push(item);
29
+ }
30
+
31
+ // Items without a group key
32
+ const rest = list.filter((item: T) => {
33
+ const key = item[groupBy];
34
+ return key === undefined || key === null;
35
+ });
36
+
37
+ const ordered = [...Array.from(grouped.values()).flat(), ...rest];
38
+
39
+ return {
40
+ grouped,
41
+ rest,
42
+ ordered,
43
+ };
44
+ }
45
+
46
+ export function useGroupBy<T, K extends keyof T>(
47
+ list: Ref<T[]>,
48
+ byKey: K,
49
+ ) {
50
+ const result = computed(() => groupBy(list.value, byKey));
51
+
52
+ const orderedRef = computed(() => result.value.ordered);
53
+
54
+ const groupsRef = computed(() => result.value.grouped);
55
+
56
+ const restRef = computed(() => result.value.rest);
57
+
58
+ return {
59
+ orderedRef,
60
+ groupsRef,
61
+ restRef,
62
+ };
63
+ }
@@ -88,6 +88,7 @@ const options = computed(() =>
88
88
  props.options?.map((opt) => ({
89
89
  label: opt.label,
90
90
  value: opt.ref,
91
+ group: opt.group,
91
92
  })),
92
93
  );
93
94
 
@@ -131,5 +131,6 @@ export function normalizeListOptions<V = unknown>(options: Readonly<ListOption<V
131
131
  label: 'label' in it ? it.label : it.text,
132
132
  value: it.value,
133
133
  description: it.description,
134
+ group: it.group,
134
135
  }));
135
136
  }
package/src/types.ts CHANGED
@@ -32,31 +32,21 @@ export type SimpleOption<T = unknown> =
32
32
  value: T;
33
33
  };
34
34
 
35
- export type SimpleOptionNormalized<T = unknown> = {
36
- label: string;
37
- description?: string;
38
- value: T;
39
- };
35
+ export type ListOptionNormalized<T = unknown> = ListOptionBase<T>;
40
36
 
37
+ // @TODO: remove `text` support
41
38
  export type ListOption<T = unknown> =
42
- | {
39
+ | Omit<ListOptionBase<T>, 'label'> & {
43
40
  text: string;
44
- description?: string;
45
- value: T;
46
41
  }
47
- | {
48
- label: string;
49
- description?: string;
50
- value: T;
51
- };
52
-
53
- export type ListOptionNormalized<T = unknown> = ListOptionBase<T>;
42
+ | ListOptionBase<T>;
54
43
 
55
44
  export type { ModelRef };
56
45
 
57
46
  export type RefOption = {
58
47
  readonly label: string;
59
48
  readonly ref: ModelRef;
49
+ readonly group?: string;
60
50
  };
61
51
 
62
52
  export type ListOptionType<Type> = Type extends ListOption<infer X>[] ? X : never;
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <div :class="$style.textLabel">
3
+ <div :class="$style.label">
4
+ <div :class="$style.labelText">
5
+ <slot />
6
+ </div>
7
+ </div>
8
+ </div>
9
+ </template>
10
+
11
+ <style lang="scss" module>
12
+ .textLabel {
13
+ padding: 0px 12px;
14
+ }
15
+
16
+ .label {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 4px;
20
+ color: var(--txt-03);
21
+ font-size: 12px;
22
+ font-weight: 500;
23
+ line-height: 16px;
24
+ }
25
+
26
+ .labelText {
27
+ display: inline-block;
28
+ max-width: 100%;
29
+ min-width: 0;
30
+ overflow: hidden;
31
+ text-overflow: ellipsis;
32
+ white-space: nowrap;
33
+ }
34
+
35
+ .label::after {
36
+ display: block;
37
+ flex: 1;
38
+ content: '';
39
+ height: 1px;
40
+ background-color: var(--border-color-div-grey);
41
+ width: 100%;
42
+ }
43
+ </style>