@drivenets/design-system 0.19.0 → 0.20.0
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/CHANGELOG.md +28 -0
- package/dist/index.cjs +12409 -10548
- package/dist/index.d.cts +678 -312
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +679 -313
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12404 -10550
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +29 -29
- package/src/components/ds-checkbox/ds-checkbox.figma.ts +3 -0
- package/src/components/ds-checkbox/ds-checkbox.module.scss +102 -20
- package/src/components/ds-checkbox/ds-checkbox.tsx +30 -2
- package/src/components/ds-checkbox/ds-checkbox.types.ts +10 -0
- package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.module.scss +169 -0
- package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.tsx +92 -0
- package/src/components/ds-code-input/collapsed-code-field/index.ts +1 -0
- package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.module.scss +110 -0
- package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.tsx +61 -0
- package/src/components/ds-code-input/ds-code-input-panel/index.ts +1 -0
- package/src/components/ds-code-input/ds-code-input.module.scss +25 -0
- package/src/components/ds-code-input/ds-code-input.tsx +401 -0
- package/src/components/ds-code-input/ds-code-input.types.ts +166 -0
- package/src/components/ds-code-input/ds-code-input.utils.ts +94 -0
- package/src/components/ds-code-input/ds-code-input.utils.unit.test.ts +166 -0
- package/src/components/ds-code-input/index.ts +2 -0
- package/src/components/ds-code-input/use-collapsed-viewport.ts +289 -0
- package/src/components/ds-code-input/use-editor-host.ts +87 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.types.ts +41 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-builder/index.ts +1 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-clear-all.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.types.ts +30 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/index.ts +1 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-pinned.ts +31 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.types.ts +22 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-query/index.ts +1 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-saved-filters.ts +19 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-search.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-summary.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-toolbar.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-view-switch.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-view.ts +11 -0
- package/src/components/ds-filters-bar/components/index.ts +11 -0
- package/src/components/ds-filters-bar/ds-filters-bar.context.ts +69 -0
- package/src/components/ds-filters-bar/ds-filters-bar.tsx +120 -0
- package/src/components/ds-filters-bar/ds-filters-bar.types.ts +440 -0
- package/src/components/ds-filters-bar/ds-filters-bar.unit.test.ts +216 -0
- package/src/components/ds-filters-bar/ds-filters-bar.utils.ts +141 -0
- package/src/components/ds-filters-bar/index.ts +1 -0
- package/src/components/ds-form-control/ds-form-control.module.scss +18 -0
- package/src/components/ds-form-control/ds-form-control.tsx +43 -9
- package/src/components/ds-form-control/ds-form-control.types.ts +2 -1
- package/src/components/ds-icon/custom-icons/index.ts +2 -0
- package/src/components/ds-icon/custom-icons/special-adv-filters.tsx +28 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.figma.ts +27 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.module.scss +41 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.tsx +43 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.types.ts +29 -0
- package/src/components/ds-pin-toggle/index.ts +2 -0
- package/src/components/ds-popover/components/popover-trigger/index.ts +1 -0
- package/src/components/ds-popover/components/popover-trigger/popover-trigger.tsx +15 -0
- package/src/components/ds-popover/ds-popover.context.ts +18 -0
- package/src/components/ds-popover/ds-popover.hover-intent.ts +65 -0
- package/src/components/ds-popover/ds-popover.tsx +163 -41
- package/src/components/ds-popover/ds-popover.types.ts +59 -1
- package/src/components/ds-popover/ds-popover.utils.ts +60 -0
- package/src/components/ds-popover/ds-popover.utils.unit.test.ts +25 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-delete-confirm.tsx +53 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-name-entry.tsx +64 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-name-modal.tsx +50 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.module.scss +68 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.tsx +83 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-row/index.ts +1 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-save.tsx +164 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.module.scss +21 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.tsx +237 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/index.ts +1 -0
- package/src/components/ds-saved-filters/components/index.ts +2 -0
- package/src/components/ds-saved-filters/ds-saved-filters.ts +6 -0
- package/src/components/ds-saved-filters/ds-saved-filters.types.ts +106 -0
- package/src/components/ds-saved-filters/ds-saved-filters.utils.ts +35 -0
- package/src/components/ds-saved-filters/hooks/index.ts +1 -0
- package/src/components/ds-saved-filters/hooks/use-picker-outside-dismiss.ts +42 -0
- package/src/components/ds-saved-filters/index.ts +2 -0
- package/src/components/ds-segment-group/ds-segment-group.module.scss +65 -37
- package/src/components/ds-segment-group/ds-segment-group.tsx +17 -5
- package/src/components/ds-segment-group/ds-segment-group.types.ts +20 -5
- package/src/components/ds-segment-group/figma/ds-segment-group-v2-item.figma.ts +109 -0
- package/src/components/ds-segment-group/figma/ds-segment-group-v2.figma.ts +31 -0
- package/src/components/ds-tag/ds-tag.figma.ts +23 -3
- package/src/components/ds-tag/ds-tag.module.scss +71 -7
- package/src/components/ds-tag/ds-tag.tsx +115 -14
- package/src/components/ds-tag/ds-tag.types.ts +70 -3
- package/src/components/ds-tag/ds-tag.unit.test.ts +23 -4
- package/src/components/ds-tag/index.ts +2 -2
- package/src/components/ds-text-input/ds-input-field-v02.figma.ts +2 -0
- package/src/components/ds-toggle/ds-toggle.tsx +19 -15
- package/src/components/ds-toggle/ds-toggle.types.ts +6 -0
- package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.module.scss +69 -0
- package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.tsx +55 -0
- package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.types.ts +43 -0
- package/src/components/ds-toggle-filter-data/index.ts +1 -0
- package/src/components/ds-tooltip/ds-tooltip.module.scss +8 -0
- package/src/components/ds-tooltip/ds-tooltip.tsx +1 -1
- package/src/components/ds-tree/ds-tree.tsx +10 -10
- package/src/components/ds-tree/ds-tree.types.ts +40 -7
- package/src/index.ts +2 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { DsFilterCondition, DsFilterField } from './ds-filters-bar.types';
|
|
3
|
+
import {
|
|
4
|
+
appendCondition,
|
|
5
|
+
createConditionId,
|
|
6
|
+
createSearchCondition,
|
|
7
|
+
describeCondition,
|
|
8
|
+
isFiltersBarView,
|
|
9
|
+
lockedViewsFor,
|
|
10
|
+
normalizeQuery,
|
|
11
|
+
removeConditionById,
|
|
12
|
+
replaceCondition,
|
|
13
|
+
} from './ds-filters-bar.utils';
|
|
14
|
+
|
|
15
|
+
const EQUALS = { value: '=', label: 'equals', symbol: '=' };
|
|
16
|
+
const NOT_EQUALS = { value: '!=', label: 'not equals', symbol: '≠' };
|
|
17
|
+
|
|
18
|
+
const FIELDS: DsFilterField[] = [
|
|
19
|
+
{
|
|
20
|
+
type: 'enum',
|
|
21
|
+
id: 'status',
|
|
22
|
+
label: 'Status',
|
|
23
|
+
operators: [EQUALS, NOT_EQUALS],
|
|
24
|
+
options: [
|
|
25
|
+
{ value: 'active', label: 'Active' },
|
|
26
|
+
{ value: 'deprecated', label: 'Deprecated' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{ type: 'number', id: 'parents', label: 'Parents', operators: [{ value: '>', label: 'greater than' }] },
|
|
30
|
+
{
|
|
31
|
+
type: 'date',
|
|
32
|
+
id: 'lastRun',
|
|
33
|
+
label: 'Last run',
|
|
34
|
+
operators: [EQUALS],
|
|
35
|
+
presets: [{ value: 'today', label: 'Today' }],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'compound',
|
|
39
|
+
id: 'input',
|
|
40
|
+
label: 'Input',
|
|
41
|
+
subfields: [{ type: 'text', id: 'name', label: 'Name', operators: [{ value: '~', label: 'contains' }] }],
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
describe('isFiltersBarView', () => {
|
|
46
|
+
it.each(['filters', 'builder', 'advanced'])('accepts %s', (value) => {
|
|
47
|
+
expect(isFiltersBarView(value)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it.each(['code', '', null])('rejects %s', (value) => {
|
|
51
|
+
expect(isFiltersBarView(value)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('normalizeQuery', () => {
|
|
56
|
+
it('keeps edited text as is', () => {
|
|
57
|
+
expect(normalizeQuery(' status = "Active" ')).toBe(' status = "Active" ');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it.each(['', ' ', null])('turns %j into null', (query) => {
|
|
61
|
+
expect(normalizeQuery(query)).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('lockedViewsFor', () => {
|
|
66
|
+
it('locks nothing while the conditions are the source', () => {
|
|
67
|
+
expect(lockedViewsFor(null)).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('locks the filters and builder views while an edited query is the source', () => {
|
|
71
|
+
expect(lockedViewsFor('status = "Active"')).toEqual(['filters', 'builder']);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('createConditionId', () => {
|
|
76
|
+
it('returns a different id every call', () => {
|
|
77
|
+
expect(createConditionId()).not.toBe(createConditionId());
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('createSearchCondition', () => {
|
|
82
|
+
it('creates a search condition with trimmed text', () => {
|
|
83
|
+
const condition = createSearchCondition(' AAA ');
|
|
84
|
+
|
|
85
|
+
expect(condition).toMatchObject({ kind: 'search', text: 'AAA' });
|
|
86
|
+
expect(condition?.id).toMatch(/^condition-/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns null for blank text', () => {
|
|
90
|
+
expect(createSearchCondition(' ')).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('condition list helpers', () => {
|
|
95
|
+
const first: DsFilterCondition = { kind: 'search', id: 'a', text: 'AAA' };
|
|
96
|
+
const second: DsFilterCondition = {
|
|
97
|
+
kind: 'field',
|
|
98
|
+
id: 'b',
|
|
99
|
+
field: 'status',
|
|
100
|
+
operator: '=',
|
|
101
|
+
value: 'active',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
it('appends a condition', () => {
|
|
105
|
+
expect(appendCondition([first], second)).toEqual([first, second]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('replaces the condition with the same id', () => {
|
|
109
|
+
const edited: DsFilterCondition = { ...second, operator: '!=' };
|
|
110
|
+
|
|
111
|
+
expect(replaceCondition([first, second], edited)).toEqual([first, edited]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('removes a condition by id', () => {
|
|
115
|
+
expect(removeConditionById([first, second], 'a')).toEqual([second]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('describeCondition', () => {
|
|
120
|
+
it('describes a search condition by its text only', () => {
|
|
121
|
+
expect(describeCondition({ kind: 'search', id: '1', text: 'WF456' }, FIELDS)).toEqual({
|
|
122
|
+
fieldPath: [],
|
|
123
|
+
value: 'WF456',
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('uses option labels for enum values', () => {
|
|
128
|
+
const condition: DsFilterCondition = {
|
|
129
|
+
kind: 'field',
|
|
130
|
+
id: '1',
|
|
131
|
+
field: 'status',
|
|
132
|
+
operator: '!=',
|
|
133
|
+
value: ['active', 'deprecated'],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
expect(describeCondition(condition, FIELDS)).toEqual({
|
|
137
|
+
fieldPath: ['Status'],
|
|
138
|
+
operator: 'not equals',
|
|
139
|
+
operatorSymbol: '≠',
|
|
140
|
+
value: 'Active, Deprecated',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('falls back to the operator label when there is no symbol', () => {
|
|
145
|
+
const condition: DsFilterCondition = {
|
|
146
|
+
kind: 'field',
|
|
147
|
+
id: '1',
|
|
148
|
+
field: 'parents',
|
|
149
|
+
operator: '>',
|
|
150
|
+
value: 2,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
expect(describeCondition(condition, FIELDS)).toMatchObject({
|
|
154
|
+
operatorSymbol: 'greater than',
|
|
155
|
+
value: '2',
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('uses the preset label for a date preset', () => {
|
|
160
|
+
const condition: DsFilterCondition = {
|
|
161
|
+
kind: 'field',
|
|
162
|
+
id: '1',
|
|
163
|
+
field: 'lastRun',
|
|
164
|
+
operator: '=',
|
|
165
|
+
value: 'today',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
expect(describeCondition(condition, FIELDS)).toMatchObject({ value: 'Today' });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('joins both ends of a range', () => {
|
|
172
|
+
const condition: DsFilterCondition = {
|
|
173
|
+
kind: 'field',
|
|
174
|
+
id: '1',
|
|
175
|
+
field: 'parents',
|
|
176
|
+
operator: '>',
|
|
177
|
+
value: { from: 2, to: 5 },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
expect(describeCondition(condition, FIELDS)).toMatchObject({ value: '2 – 5' });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('includes the subfield of a compound field in the path', () => {
|
|
184
|
+
const condition: DsFilterCondition = {
|
|
185
|
+
kind: 'field',
|
|
186
|
+
id: '1',
|
|
187
|
+
field: 'input',
|
|
188
|
+
subfield: 'name',
|
|
189
|
+
operator: '~',
|
|
190
|
+
value: 'WF456',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
expect(describeCondition(condition, FIELDS)).toMatchObject({
|
|
194
|
+
fieldPath: ['Input', 'Name'],
|
|
195
|
+
operator: 'contains',
|
|
196
|
+
value: 'WF456',
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('falls back to raw ids for a field missing from the schema', () => {
|
|
201
|
+
const condition: DsFilterCondition = {
|
|
202
|
+
kind: 'field',
|
|
203
|
+
id: '1',
|
|
204
|
+
field: 'removed',
|
|
205
|
+
operator: '=',
|
|
206
|
+
value: 'x',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
expect(describeCondition(condition, FIELDS)).toEqual({
|
|
210
|
+
fieldPath: ['removed'],
|
|
211
|
+
operator: '=',
|
|
212
|
+
operatorSymbol: '=',
|
|
213
|
+
value: 'x',
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
filtersBarViews,
|
|
3
|
+
type DsFilterCondition,
|
|
4
|
+
type DsFilterField,
|
|
5
|
+
type DsFilterOption,
|
|
6
|
+
type DsFilterRange,
|
|
7
|
+
type DsFilterScalarField,
|
|
8
|
+
type DsFilterSearchCondition,
|
|
9
|
+
type DsFilterValue,
|
|
10
|
+
type DsFiltersBarView,
|
|
11
|
+
} from './ds-filters-bar.types';
|
|
12
|
+
|
|
13
|
+
const ID_RANDOM_WORDS = 2;
|
|
14
|
+
const HEX_RADIX = 16;
|
|
15
|
+
|
|
16
|
+
const QUERY_LOCKED_VIEWS: ReadonlyArray<DsFiltersBarView> = Object.freeze(['filters', 'builder']);
|
|
17
|
+
const NO_LOCKED_VIEWS: ReadonlyArray<DsFiltersBarView> = Object.freeze([]);
|
|
18
|
+
|
|
19
|
+
export const isFiltersBarView = (value: string | null): value is DsFiltersBarView =>
|
|
20
|
+
filtersBarViews.some((view) => view === value);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Blank query text is not an edited query: it hands control back to the conditions.
|
|
24
|
+
*/
|
|
25
|
+
export const normalizeQuery = (query: string | null): string | null => (query?.trim() ? query : null);
|
|
26
|
+
|
|
27
|
+
export const lockedViewsFor = (query: string | null): ReadonlyArray<DsFiltersBarView> =>
|
|
28
|
+
query === null ? NO_LOCKED_VIEWS : QUERY_LOCKED_VIEWS;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Random, so ids stay unique across reloads and conditions restored from a **Saved filter**.
|
|
32
|
+
* `crypto.randomUUID` is avoided because it needs a secure context.
|
|
33
|
+
*/
|
|
34
|
+
export const createConditionId = (): string => {
|
|
35
|
+
const words = crypto.getRandomValues(new Uint32Array(ID_RANDOM_WORDS));
|
|
36
|
+
|
|
37
|
+
return `condition-${Array.from(words, (word) => word.toString(HEX_RADIX)).join('')}`;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const createSearchCondition = (text: string): DsFilterSearchCondition | null => {
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { kind: 'search', id: createConditionId(), text: trimmed };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const appendCondition = (
|
|
51
|
+
conditions: ReadonlyArray<DsFilterCondition>,
|
|
52
|
+
condition: DsFilterCondition,
|
|
53
|
+
): ReadonlyArray<DsFilterCondition> => [...conditions, condition];
|
|
54
|
+
|
|
55
|
+
export const replaceCondition = (
|
|
56
|
+
conditions: ReadonlyArray<DsFilterCondition>,
|
|
57
|
+
condition: DsFilterCondition,
|
|
58
|
+
): ReadonlyArray<DsFilterCondition> =>
|
|
59
|
+
conditions.map((item) => (item.id === condition.id ? condition : item));
|
|
60
|
+
|
|
61
|
+
export const removeConditionById = (
|
|
62
|
+
conditions: ReadonlyArray<DsFilterCondition>,
|
|
63
|
+
id: string,
|
|
64
|
+
): ReadonlyArray<DsFilterCondition> => conditions.filter((item) => item.id !== id);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Shared by the summary line and the chips, so every view words a condition the same way
|
|
68
|
+
*/
|
|
69
|
+
export interface DsFilterConditionDescription {
|
|
70
|
+
/**
|
|
71
|
+
* Field then subfield labels, for example `['Input', 'Name']`. Empty for search conditions.
|
|
72
|
+
*/
|
|
73
|
+
fieldPath: ReadonlyArray<string>;
|
|
74
|
+
/**
|
|
75
|
+
* Operator in words, for example `not equals`
|
|
76
|
+
*/
|
|
77
|
+
operator?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Operator in compact form for chips, for example `≠`
|
|
80
|
+
*/
|
|
81
|
+
operatorSymbol?: string;
|
|
82
|
+
value: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const RANGE_SEPARATOR = ' – ';
|
|
86
|
+
|
|
87
|
+
const labelFor = (options: ReadonlyArray<DsFilterOption>, value: string) =>
|
|
88
|
+
options.find((option) => option.value === value)?.label ?? value;
|
|
89
|
+
|
|
90
|
+
const isRange = (value: DsFilterValue): value is DsFilterRange<number> | DsFilterRange<string> =>
|
|
91
|
+
typeof value === 'object' && 'from' in value;
|
|
92
|
+
|
|
93
|
+
const formatValue = (value: DsFilterValue, field: DsFilterScalarField | undefined): string => {
|
|
94
|
+
if (isRange(value)) {
|
|
95
|
+
return [value.from ?? '', value.to ?? ''].map(String).join(RANGE_SEPARATOR).trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof value === 'object') {
|
|
99
|
+
const options = field?.type === 'enum' ? field.options : [];
|
|
100
|
+
|
|
101
|
+
return value.map((item) => labelFor(options, item)).join(', ');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof value === 'string' && field?.type === 'date') {
|
|
105
|
+
return labelFor(field.presets ?? [], value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return String(value);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Falls back to raw ids when a field, subfield, operator or option is missing from `fields`, so a
|
|
113
|
+
* **Saved filter** that references a removed field still shows instead of breaking the bar.
|
|
114
|
+
*/
|
|
115
|
+
export const describeCondition = (
|
|
116
|
+
condition: DsFilterCondition,
|
|
117
|
+
fields: ReadonlyArray<DsFilterField>,
|
|
118
|
+
): DsFilterConditionDescription => {
|
|
119
|
+
if (condition.kind === 'search') {
|
|
120
|
+
return { fieldPath: [], value: condition.text };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const field = fields.find((item) => item.id === condition.field);
|
|
124
|
+
const subfield =
|
|
125
|
+
field?.type === 'compound' ? field.subfields.find((item) => item.id === condition.subfield) : undefined;
|
|
126
|
+
const scalarField = field?.type === 'compound' ? subfield : field;
|
|
127
|
+
const operator = scalarField?.operators.find((item) => item.value === condition.operator);
|
|
128
|
+
|
|
129
|
+
const fieldPath = [field?.label ?? condition.field];
|
|
130
|
+
|
|
131
|
+
if (condition.subfield) {
|
|
132
|
+
fieldPath.push(subfield?.label ?? condition.subfield);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
fieldPath,
|
|
137
|
+
operator: operator?.label ?? condition.operator,
|
|
138
|
+
operatorSymbol: operator?.symbol ?? operator?.label ?? condition.operator,
|
|
139
|
+
value: formatValue(condition.value, scalarField),
|
|
140
|
+
};
|
|
141
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DsFiltersBar } from './ds-filters-bar';
|
|
@@ -125,6 +125,24 @@ $form-control-description-max-height: calc(
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Stays in the tree while empty, so a message that appears later is announced. Out of the flex
|
|
129
|
+
// flow while empty, so it adds no gap.
|
|
130
|
+
.messageRegion:empty {
|
|
131
|
+
position: absolute;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.visuallyHidden {
|
|
135
|
+
position: absolute;
|
|
136
|
+
width: 1px;
|
|
137
|
+
height: 1px;
|
|
138
|
+
margin: -1px;
|
|
139
|
+
padding: 0;
|
|
140
|
+
border: 0;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
clip-path: inset(50%);
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
.message {
|
|
129
147
|
@include typography.body-xs-reg;
|
|
130
148
|
color: var(--font-secondary);
|
|
@@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|
|
4
4
|
import { DsIcon } from '../ds-icon';
|
|
5
5
|
import { DsSelect } from '../ds-select';
|
|
6
6
|
import { DsTextInput } from '../ds-text-input';
|
|
7
|
+
import { DsCodeInput } from '../ds-code-input';
|
|
7
8
|
import { DsTextarea } from '../ds-textarea';
|
|
8
9
|
import { DsNumberInput } from '../ds-number-input';
|
|
9
10
|
import { DsPasswordInput } from '../ds-password-input';
|
|
@@ -13,7 +14,13 @@ import { DsDateInput } from '../ds-date-input';
|
|
|
13
14
|
import { DsDatePicker } from '../ds-date-picker';
|
|
14
15
|
import { DsTimePicker } from '../ds-time-picker';
|
|
15
16
|
|
|
16
|
-
const FormControlContext = createContext<{
|
|
17
|
+
const FormControlContext = createContext<{
|
|
18
|
+
controlId: string;
|
|
19
|
+
/**
|
|
20
|
+
* Id of the message, while there is one
|
|
21
|
+
*/
|
|
22
|
+
messageId?: string;
|
|
23
|
+
} | null>(null);
|
|
17
24
|
|
|
18
25
|
const useFormControlContext = () => {
|
|
19
26
|
const context = useContext(FormControlContext);
|
|
@@ -45,6 +52,25 @@ const controlify = <TProps extends { id?: string }>(
|
|
|
45
52
|
return WrappedFormControl;
|
|
46
53
|
};
|
|
47
54
|
|
|
55
|
+
/**
|
|
56
|
+
* HOC, same as `controlify`. Also sets `aria-describedby` to the message, for inputs that accept that prop.
|
|
57
|
+
*/
|
|
58
|
+
const controlifyDescribed = <TProps extends { id?: string; 'aria-describedby'?: string }>(
|
|
59
|
+
Component: ComponentType<TProps>,
|
|
60
|
+
displayName: string,
|
|
61
|
+
) => {
|
|
62
|
+
const WrappedFormControl = (props: TProps) => {
|
|
63
|
+
const { controlId, messageId } = useFormControlContext();
|
|
64
|
+
const describedBy = [messageId, props['aria-describedby']].filter(Boolean).join(' ') || undefined;
|
|
65
|
+
|
|
66
|
+
return <Component id={controlId} {...props} aria-describedby={describedBy} />;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
WrappedFormControl.displayName = displayName;
|
|
70
|
+
|
|
71
|
+
return WrappedFormControl;
|
|
72
|
+
};
|
|
73
|
+
|
|
48
74
|
const DsFormControlDescription: React.FC<DsFormControlDescriptionProps> = ({ children, className }) => {
|
|
49
75
|
return <div className={classNames(styles.description, className)}>{children}</div>;
|
|
50
76
|
};
|
|
@@ -63,10 +89,11 @@ const DsFormControlRoot = ({
|
|
|
63
89
|
children,
|
|
64
90
|
}: DsFormControlProps) => {
|
|
65
91
|
const generatedId = useId();
|
|
92
|
+
const messageId = useId();
|
|
66
93
|
const controlId = id || generatedId;
|
|
67
94
|
|
|
68
95
|
return (
|
|
69
|
-
<FormControlContext.Provider value={{ controlId }}>
|
|
96
|
+
<FormControlContext.Provider value={{ controlId, messageId: message ? messageId : undefined }}>
|
|
70
97
|
<div
|
|
71
98
|
className={classNames(
|
|
72
99
|
styles.container,
|
|
@@ -77,7 +104,11 @@ const DsFormControlRoot = ({
|
|
|
77
104
|
)}
|
|
78
105
|
style={style}
|
|
79
106
|
>
|
|
80
|
-
{
|
|
107
|
+
{hideLabel ? (
|
|
108
|
+
<label htmlFor={controlId} className={styles.visuallyHidden}>
|
|
109
|
+
{label}
|
|
110
|
+
</label>
|
|
111
|
+
) : (
|
|
81
112
|
<div className={styles.labelContainer}>
|
|
82
113
|
<label
|
|
83
114
|
htmlFor={controlId}
|
|
@@ -93,12 +124,14 @@ const DsFormControlRoot = ({
|
|
|
93
124
|
|
|
94
125
|
{children}
|
|
95
126
|
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
<div className={styles.messageRegion} aria-live="polite">
|
|
128
|
+
{message && (
|
|
129
|
+
<div className={styles.message}>
|
|
130
|
+
<DsIcon icon={messageIcon} size="tiny" filled />
|
|
131
|
+
<span id={messageId}>{message}</span>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
102
135
|
</div>
|
|
103
136
|
</FormControlContext.Provider>
|
|
104
137
|
);
|
|
@@ -109,6 +142,7 @@ DsFormControlDescription.displayName = 'DsFormControl.Description';
|
|
|
109
142
|
const DsFormControl = Object.assign(DsFormControlRoot, {
|
|
110
143
|
displayName: 'DsFormControl',
|
|
111
144
|
TextInput: controlify(DsTextInput, 'DsFormControl.TextInput'),
|
|
145
|
+
CodeInput: controlifyDescribed(DsCodeInput, 'DsFormControl.CodeInput'),
|
|
112
146
|
NumberInput: controlify(DsNumberInput, 'DsFormControl.NumberInput'),
|
|
113
147
|
PasswordInput: controlify(DsPasswordInput, 'DsFormControl.PasswordInput'),
|
|
114
148
|
/** @deprecated DsDateInput is deprecated. Use DsDatePicker or DsDateRangePicker instead. */
|
|
@@ -18,7 +18,8 @@ export interface DsFormControlProps extends React.PropsWithChildren {
|
|
|
18
18
|
*/
|
|
19
19
|
label: string;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Hides the label row (e.g. inline table cell editors). The label stays as the control's
|
|
22
|
+
* accessible name.
|
|
22
23
|
*/
|
|
23
24
|
hideLabel?: boolean;
|
|
24
25
|
/**
|
|
@@ -49,6 +49,7 @@ import { SpecialWebhook } from './special-webhook';
|
|
|
49
49
|
import { SpecialMapView } from './special-map-view';
|
|
50
50
|
|
|
51
51
|
// Misc icons
|
|
52
|
+
import { SpecialAdvFilters } from './special-adv-filters';
|
|
52
53
|
import { SpecialHome } from './special-home';
|
|
53
54
|
import { SpecialLeadingIcon } from './special-leading-icon';
|
|
54
55
|
import { SpecialLego } from './special-lego';
|
|
@@ -120,6 +121,7 @@ export const customIcons = {
|
|
|
120
121
|
'special-map-view': SpecialMapView,
|
|
121
122
|
|
|
122
123
|
// Misc
|
|
124
|
+
'special-adv-filters': SpecialAdvFilters,
|
|
123
125
|
'special-home': SpecialHome,
|
|
124
126
|
'special-leading-icon': SpecialLeadingIcon,
|
|
125
127
|
'special-lego': SpecialLego,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FC, SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Special Advanced Filters icon - magnifying glass with filter lines
|
|
5
|
+
* @figma https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=42571-31078
|
|
6
|
+
*/
|
|
7
|
+
export const SpecialAdvFilters: FC<SVGProps<SVGSVGElement>> = (props) => (
|
|
8
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
9
|
+
<path
|
|
10
|
+
d="M9.70117 12.1478C9.82249 12.1478 9.92473 12.1952 10.0068 12.2903C10.089 12.3856 10.1299 12.504 10.1299 12.6448C10.1299 12.7857 10.089 12.905 10.0068 13.0023C9.92473 13.0993 9.8225 13.1478 9.70117 13.1478H8.27344C8.15211 13.1478 8.04988 13.1003 7.96777 13.0052C7.88563 12.9099 7.84473 12.7915 7.84473 12.6507C7.84473 12.5099 7.88563 12.3905 7.96777 12.2933C8.04988 12.1962 8.15211 12.1478 8.27344 12.1478H9.70117Z"
|
|
11
|
+
fill="currentColor"
|
|
12
|
+
/>
|
|
13
|
+
<path
|
|
14
|
+
d="M11.416 9.64777C11.5374 9.6478 11.6396 9.6951 11.7217 9.79034C11.8038 9.88561 11.8447 10.004 11.8447 10.1448C11.8447 10.2857 11.8038 10.405 11.7217 10.5023C11.6396 10.5994 11.5374 10.6477 11.416 10.6478H6.55859C6.43723 10.6477 6.33504 10.6004 6.25293 10.5052C6.1708 10.4099 6.12989 10.2915 6.12988 10.1507C6.12988 10.0099 6.1708 9.89049 6.25293 9.79327C6.33504 9.69609 6.43723 9.6478 6.55859 9.64777H11.416Z"
|
|
15
|
+
fill="currentColor"
|
|
16
|
+
/>
|
|
17
|
+
<path
|
|
18
|
+
d="M12.5586 7.14777C12.68 7.14777 12.7821 7.19514 12.8643 7.29034C12.9464 7.38562 12.9873 7.50401 12.9873 7.64484C12.9873 7.78567 12.9464 7.90504 12.8643 8.00226C12.7821 8.09941 12.68 8.14777 12.5586 8.14777H5.41602C5.29463 8.14777 5.19248 8.10039 5.11035 8.00519C5.02821 7.90991 4.98731 7.79152 4.9873 7.6507C4.9873 7.50986 5.02821 7.3905 5.11035 7.29327C5.19248 7.19612 5.29463 7.14777 5.41602 7.14777H12.5586Z"
|
|
19
|
+
fill="currentColor"
|
|
20
|
+
/>
|
|
21
|
+
<path
|
|
22
|
+
fillRule="evenodd"
|
|
23
|
+
clipRule="evenodd"
|
|
24
|
+
d="M8.98535 2.14777C9.94489 2.14777 10.8519 2.3315 11.7061 2.69952C12.5602 3.06758 13.3036 3.56758 13.9355 4.19952C14.5675 4.83147 15.0675 5.57386 15.4355 6.42609C15.8036 7.27828 15.9873 8.18601 15.9873 9.14874C15.9873 9.98127 15.8491 10.7687 15.5723 11.511C15.2955 12.2535 14.9126 12.9311 14.4248 13.5433L17.4668 16.5853C17.6193 16.7329 17.6923 16.9052 17.6855 17.1019C17.6786 17.2988 17.5996 17.4745 17.4502 17.6273C17.3008 17.7797 17.1267 17.8557 16.9277 17.8558C16.7284 17.8558 16.5537 17.7798 16.4043 17.6273L13.3623 14.6058C12.7541 15.0933 12.0812 15.4724 11.3438 15.7425C10.6061 16.0126 9.82064 16.1478 8.9873 16.1478C8.01647 16.1478 7.10671 15.9641 6.25879 15.596C5.4111 15.228 4.66806 14.7277 4.0293 14.096C3.39041 13.4641 2.89041 12.7234 2.5293 11.8753C2.16829 11.0276 1.98736 10.1185 1.9873 9.14777C1.9873 8.18975 2.17104 7.28315 2.53906 6.42902C2.90712 5.57485 3.40627 4.83175 4.03516 4.19952C4.66407 3.56731 5.40284 3.06712 6.25195 2.69952C7.10137 2.33176 8.01274 2.14778 8.98535 2.14777ZM8.9873 3.64777C7.45523 3.64777 6.15525 4.18438 5.08789 5.25812C4.02067 6.33201 3.4873 7.62874 3.4873 9.14777C3.4873 10.6798 4.02066 11.9798 5.08691 13.0472C6.15288 14.1141 7.45106 14.6477 8.98145 14.6478C10.4989 14.6478 11.7967 14.1144 12.873 13.0482C13.9491 11.9822 14.4872 10.684 14.4873 9.15363C14.4873 7.63613 13.9507 6.33841 12.877 5.26202C11.8031 4.18565 10.5063 3.64777 8.9873 3.64777Z"
|
|
25
|
+
fill="currentColor"
|
|
26
|
+
/>
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=42863-16495
|
|
2
|
+
// source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-pin-toggle
|
|
3
|
+
// component=DsPinToggle
|
|
4
|
+
import figma from 'figma';
|
|
5
|
+
|
|
6
|
+
const instance = figma.selectedInstance;
|
|
7
|
+
|
|
8
|
+
// `Part_PinnedIcon` is a `Part_*` node, which we normally do not mirror. It is
|
|
9
|
+
// mapped here because, unlike `Part_CheckboxBtnV3`, it has a real exported
|
|
10
|
+
// counterpart in code — the rule exists to stop us inventing components for
|
|
11
|
+
// markup, not to hide ones that exist.
|
|
12
|
+
const active = instance.getEnum('active', {
|
|
13
|
+
true: 'true',
|
|
14
|
+
false: 'false',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// `itemLabel` has no Figma source — the pin carries no text, and the name it
|
|
18
|
+
// needs comes from the row it sits in. The snippet emits a placeholder so the
|
|
19
|
+
// required prop is visible rather than silently missing.
|
|
20
|
+
const attrs = ['itemLabel="Label"', active === 'true' ? 'pinned' : ''].filter(Boolean).join(' ');
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
example: figma.code`<DsPinToggle ${attrs} />`,
|
|
24
|
+
imports: ["import { DsPinToggle } from '@drivenets/design-system';"],
|
|
25
|
+
id: 'ds-pin-toggle',
|
|
26
|
+
metadata: { nestable: true },
|
|
27
|
+
} satisfies figma.Template;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
$pin-border-radius: 2px;
|
|
2
|
+
$pin-focus-ring-width: 2px;
|
|
3
|
+
$transition-duration: 0.2s;
|
|
4
|
+
|
|
5
|
+
.root {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
flex-shrink: 0;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: var(--4xs);
|
|
12
|
+
border: none;
|
|
13
|
+
border-radius: $pin-border-radius;
|
|
14
|
+
background-color: transparent;
|
|
15
|
+
line-height: 0;
|
|
16
|
+
color: var(--icon-tertiary);
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
transition: color $transition-duration ease-in-out;
|
|
19
|
+
|
|
20
|
+
&:not(:disabled):hover {
|
|
21
|
+
color: var(--icon-secondary);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&[aria-pressed='true'] {
|
|
25
|
+
color: var(--icon-action);
|
|
26
|
+
|
|
27
|
+
&:not(:disabled):hover {
|
|
28
|
+
color: var(--icon-action-hover);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&:focus-visible {
|
|
33
|
+
outline: $pin-focus-ring-width solid var(--border-action-primary);
|
|
34
|
+
outline-offset: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&:disabled {
|
|
38
|
+
color: var(--icon-disabled);
|
|
39
|
+
cursor: not-allowed;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import styles from './ds-pin-toggle.module.scss';
|
|
3
|
+
import type { DsPinToggleProps } from './ds-pin-toggle.types';
|
|
4
|
+
import { DsIcon } from '../ds-icon';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LOCALE = Object.freeze({ pin: 'Pin' });
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Bare pushpin for "keep this at the top" affordances.
|
|
10
|
+
*/
|
|
11
|
+
const DsPinToggle = ({
|
|
12
|
+
ref,
|
|
13
|
+
className,
|
|
14
|
+
style,
|
|
15
|
+
itemLabel,
|
|
16
|
+
pinned = false,
|
|
17
|
+
disabled = false,
|
|
18
|
+
locale,
|
|
19
|
+
onPinnedChange,
|
|
20
|
+
onClick,
|
|
21
|
+
...rest
|
|
22
|
+
}: DsPinToggleProps) => (
|
|
23
|
+
<button
|
|
24
|
+
{...rest}
|
|
25
|
+
ref={ref}
|
|
26
|
+
type="button"
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
aria-pressed={pinned}
|
|
29
|
+
aria-label={`${locale?.pin ?? DEFAULT_LOCALE.pin} ${itemLabel}`}
|
|
30
|
+
className={classNames(styles.root, className)}
|
|
31
|
+
style={style}
|
|
32
|
+
onClick={(event) => {
|
|
33
|
+
onClick?.(event);
|
|
34
|
+
onPinnedChange?.(!pinned);
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<DsIcon icon="keep" size="tiny" variant="rounded" filled aria-hidden />
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
DsPinToggle.displayName = 'DsPinToggle';
|
|
42
|
+
|
|
43
|
+
export default DsPinToggle;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, Ref } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface DsPinToggleProps extends Omit<
|
|
4
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
5
|
+
'aria-label' | 'aria-labelledby' | 'aria-pressed' | 'children' | 'type'
|
|
6
|
+
> {
|
|
7
|
+
ref?: Ref<HTMLButtonElement>;
|
|
8
|
+
|
|
9
|
+
itemLabel: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @default false
|
|
13
|
+
*/
|
|
14
|
+
pinned?: boolean;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
|
|
21
|
+
locale?: {
|
|
22
|
+
/**
|
|
23
|
+
* @default 'Pin'
|
|
24
|
+
*/
|
|
25
|
+
pin?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
onPinnedChange?: (pinned: boolean) => void;
|
|
29
|
+
}
|