@saooti/octopus-sdk 41.11.0 → 41.12.0-beta
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 +35 -0
- package/eslint-config.mjs +10 -2
- package/index.ts +5 -2
- package/package.json +4 -1
- package/src/api/radioApi.ts +14 -1
- package/src/components/buttons/ClassicButton.vue +18 -1
- package/src/components/composable/form/useOctopusDropdown.ts +99 -0
- package/src/components/composable/player/usePlayerLive.ts +12 -7
- package/src/components/composable/useSticky.ts +45 -0
- package/src/components/composable/useTranslation.ts +6 -8
- package/src/components/display/emission/EmissionGroupChooser.vue +22 -24
- package/src/components/form/ClassicButtonGroup.vue +15 -13
- package/src/components/form/ClassicSelect.vue +11 -2
- package/src/components/form/OctopusMultiselect.vue +169 -73
- package/src/components/form/OctopusSelect.vue +237 -0
- package/src/components/misc/ClassicPopover.vue +9 -2
- package/src/components/misc/modal/ClassicModal.vue +1 -0
- package/src/components/pages/PlaylistsPage.vue +1 -0
- package/src/helper/colorFromString.ts +12 -5
- package/src/helper/equals.ts +21 -9
- package/src/stores/class/general/podcast.ts +8 -0
- package/src/style/_utilities.scss +1 -0
- package/src/style/bootstrap.scss +7 -0
- package/tests/components/form/ClassicButtonGroup.spec.ts +10 -10
- package/tests/components/form/OctopusMultiselect.spec.ts +27 -6
- package/tests/components/form/OctopusSelect.spec.ts +143 -0
package/src/helper/equals.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
export function deepEqual(obj1: any, obj2: any) {
|
|
2
|
+
// it's just the same object. No need to compare.
|
|
3
|
+
if(obj1 === obj2) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
// compare primitives
|
|
8
|
+
if(isPrimitive(obj1) && isPrimitive(obj2)) {
|
|
9
|
+
return obj1 === obj2;
|
|
10
|
+
}
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
// one is a primitive but not the other (for example undefined vs array)
|
|
13
|
+
if (isPrimitive(obj1) || isPrimitive(obj2)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
if(Object.keys(obj1).length !== Object.keys(obj2).length) {
|
|
10
18
|
return false;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
// compare objects with same number of keys
|
|
14
|
-
for(const key in obj1)
|
|
15
|
-
|
|
16
|
-
if(!(key in obj2)) {
|
|
17
|
-
|
|
22
|
+
for(const key in obj1) {
|
|
23
|
+
//other object doesn't have this prop
|
|
24
|
+
if(!(key in obj2)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if(!deepEqual(obj1[key], obj2[key])) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
return true;
|
|
@@ -23,4 +35,4 @@ export function deepEqual(obj1: any, obj2: any) {
|
|
|
23
35
|
//check if value is primitive
|
|
24
36
|
function isPrimitive(obj: unknown) {
|
|
25
37
|
return (obj !== Object(obj));
|
|
26
|
-
}
|
|
38
|
+
}
|
|
@@ -129,6 +129,14 @@ export function simplifiedToFull(simplified: SimplifiedPodcast, organisation: Or
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
export function podcastToSimplified(full: Podcast): SimplifiedPodcast {
|
|
133
|
+
return {
|
|
134
|
+
...full,
|
|
135
|
+
emissionId: full.emission.emissionId,
|
|
136
|
+
organisationId: full.organisation.id
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
132
140
|
export function emptyPodcastData(): Podcast {
|
|
133
141
|
return {
|
|
134
142
|
podcastId: 0,
|
package/src/style/bootstrap.scss
CHANGED
|
@@ -219,6 +219,13 @@ input:not([class^="vs__"]), button:not([class^="vs__"]), select:not([class^="vs_
|
|
|
219
219
|
border: 0;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
.btn-icon {
|
|
223
|
+
background: transparent;
|
|
224
|
+
border: 0;
|
|
225
|
+
border-radius: 100%;
|
|
226
|
+
height: fit-content;
|
|
227
|
+
}
|
|
228
|
+
|
|
222
229
|
.share-btn {
|
|
223
230
|
display: inline-flex;
|
|
224
231
|
align-items: center;
|
|
@@ -11,14 +11,14 @@ const options = [
|
|
|
11
11
|
describe('ClassicButtonGroup', () => {
|
|
12
12
|
it('renders one button per option', async () => {
|
|
13
13
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
14
|
-
props: { options,
|
|
14
|
+
props: { options, value: ['PODCAST'] },
|
|
15
15
|
});
|
|
16
16
|
expect(wrapper.findAll('button')).toHaveLength(3);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
it('applies active class to selected options only', async () => {
|
|
20
20
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
21
|
-
props: { options,
|
|
21
|
+
props: { options, value: ['PODCAST', 'LIVE'] },
|
|
22
22
|
});
|
|
23
23
|
const buttons = wrapper.findAll('button');
|
|
24
24
|
expect(buttons[0].classes()).toContain('active');
|
|
@@ -26,27 +26,27 @@ describe('ClassicButtonGroup', () => {
|
|
|
26
26
|
expect(buttons[2].classes()).toContain('active');
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it('emits update:
|
|
29
|
+
it('emits update:value with added value when clicking inactive button', async () => {
|
|
30
30
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
31
|
-
props: { options,
|
|
31
|
+
props: { options, value: ['PODCAST'] },
|
|
32
32
|
});
|
|
33
33
|
await wrapper.findAll('button')[1].trigger('click');
|
|
34
|
-
expect(wrapper.emitted('update:
|
|
34
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([['PODCAST', 'VIDEO']]);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it('emits update:
|
|
37
|
+
it('emits update:value without removed value when clicking active button (not last)', async () => {
|
|
38
38
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
39
|
-
props: { options,
|
|
39
|
+
props: { options, value: ['PODCAST', 'VIDEO'] },
|
|
40
40
|
});
|
|
41
41
|
await wrapper.findAll('button')[0].trigger('click');
|
|
42
|
-
expect(wrapper.emitted('update:
|
|
42
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([['VIDEO']]);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it('does not emit when clicking the only active button', async () => {
|
|
46
46
|
const wrapper = await mount(ClassicButtonGroup, {
|
|
47
|
-
props: { options,
|
|
47
|
+
props: { options, value: ['PODCAST'] },
|
|
48
48
|
});
|
|
49
49
|
await wrapper.findAll('button')[0].trigger('click');
|
|
50
|
-
expect(wrapper.emitted('update:
|
|
50
|
+
expect(wrapper.emitted('update:value')).toBeUndefined();
|
|
51
51
|
});
|
|
52
52
|
});
|
|
@@ -120,20 +120,19 @@ describe('OctopusMultiselect', () => {
|
|
|
120
120
|
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[]]);
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
it('shows selected items
|
|
123
|
+
it('shows selected items in the selection display', async () => {
|
|
124
124
|
const wrapper = await mount(OctopusMultiselect, {
|
|
125
125
|
props: { options, optionLabel: 'name', selected: [options[0], options[1]] },
|
|
126
126
|
});
|
|
127
|
-
|
|
128
|
-
expect(
|
|
127
|
+
expect(wrapper.find('.octopus-multiselect-selection-text').text()).toBe('Alpha, Beta');
|
|
128
|
+
expect(wrapper.find('.octopus-multiselect-selection-count').exists()).toBe(false);
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
it('shows count
|
|
131
|
+
it('shows overflow count badge when more than visible items are selected', async () => {
|
|
132
132
|
const wrapper = await mount(OctopusMultiselect, {
|
|
133
133
|
props: { options, optionLabel: 'name', selected: [...options] },
|
|
134
134
|
});
|
|
135
|
-
|
|
136
|
-
expect(input.attributes('placeholder')).toBe('Alpha, Beta (+1)');
|
|
135
|
+
expect(wrapper.find('.octopus-multiselect-selection-count').exists()).toBe(true);
|
|
137
136
|
});
|
|
138
137
|
|
|
139
138
|
it('disables input when isDisabled is true', async () => {
|
|
@@ -159,4 +158,26 @@ describe('OctopusMultiselect', () => {
|
|
|
159
158
|
const allCheckboxLabel = wrapper.find('.octopus-multiselect-dropdown > .octopus-form-item label');
|
|
160
159
|
expect(allCheckboxLabel.text()).toBe('Tout');
|
|
161
160
|
});
|
|
161
|
+
|
|
162
|
+
it('deselects by optionKey even when selected objects are different references', async () => {
|
|
163
|
+
const selected = [{ id: 1, name: 'Alpha' }];
|
|
164
|
+
const wrapper = await mount(OctopusMultiselect, {
|
|
165
|
+
props: { options, optionLabel: 'name', optionKey: 'id', selected },
|
|
166
|
+
});
|
|
167
|
+
await wrapper.find('input').trigger('focus');
|
|
168
|
+
const optionCheckboxes = wrapper.findAll('.octopus-multiselect-options input[type="checkbox"]');
|
|
169
|
+
await optionCheckboxes[0].trigger('input');
|
|
170
|
+
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[]]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('toggleAll deselects by optionKey even when selected objects are different references', async () => {
|
|
174
|
+
const selected = options.map((o) => ({ ...o }));
|
|
175
|
+
const wrapper = await mount(OctopusMultiselect, {
|
|
176
|
+
props: { options, optionLabel: 'name', optionKey: 'id', selected },
|
|
177
|
+
});
|
|
178
|
+
await wrapper.find('input').trigger('focus');
|
|
179
|
+
const allCheckbox = wrapper.find('.octopus-multiselect-dropdown > .octopus-form-item input[type="checkbox"]');
|
|
180
|
+
await allCheckbox.trigger('input');
|
|
181
|
+
expect(wrapper.emitted('update:selected')?.[0]).toEqual([[]]);
|
|
182
|
+
});
|
|
162
183
|
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import '@tests/mocks/i18n';
|
|
2
|
+
|
|
3
|
+
import OctopusSelect from '@/components/form/OctopusSelect.vue';
|
|
4
|
+
import { mount } from '@tests/utils';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
const options = [
|
|
8
|
+
{ id: 1, name: 'Alpha' },
|
|
9
|
+
{ id: 2, name: 'Beta' },
|
|
10
|
+
{ id: 3, name: 'Gamma' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe('OctopusSelect', () => {
|
|
14
|
+
it('renders label when provided', async () => {
|
|
15
|
+
const wrapper = await mount(OctopusSelect, {
|
|
16
|
+
props: { options, optionLabel: 'name', label: 'My label' },
|
|
17
|
+
});
|
|
18
|
+
expect(wrapper.find('label.form-label').text()).toBe('My label');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('does not render label when not provided', async () => {
|
|
22
|
+
const wrapper = await mount(OctopusSelect, {
|
|
23
|
+
props: { options, optionLabel: 'name' },
|
|
24
|
+
});
|
|
25
|
+
expect(wrapper.find('label.form-label').exists()).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('opens dropdown on input focus', async () => {
|
|
29
|
+
const wrapper = await mount(OctopusSelect, {
|
|
30
|
+
props: { options, optionLabel: 'name' },
|
|
31
|
+
});
|
|
32
|
+
expect(wrapper.find('.octopus-select-dropdown').exists()).toBe(false);
|
|
33
|
+
await wrapper.find('input').trigger('focus');
|
|
34
|
+
expect(wrapper.find('.octopus-select-dropdown').exists()).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('shows one button per option when open', async () => {
|
|
38
|
+
const wrapper = await mount(OctopusSelect, {
|
|
39
|
+
props: { options, optionLabel: 'name' },
|
|
40
|
+
});
|
|
41
|
+
await wrapper.find('input').trigger('focus');
|
|
42
|
+
const optionButtons = wrapper.findAll('.octopus-select-option');
|
|
43
|
+
expect(optionButtons).toHaveLength(3);
|
|
44
|
+
expect(optionButtons[0].text()).toBe('Alpha');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('filters options locally by search query', async () => {
|
|
48
|
+
const wrapper = await mount(OctopusSelect, {
|
|
49
|
+
props: { options, optionLabel: 'name' },
|
|
50
|
+
});
|
|
51
|
+
await wrapper.find('input').trigger('focus');
|
|
52
|
+
await wrapper.find('input').setValue('al');
|
|
53
|
+
await wrapper.find('input').trigger('input');
|
|
54
|
+
expect(wrapper.findAll('.octopus-select-option')).toHaveLength(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('shows no-results message when filter matches nothing', async () => {
|
|
58
|
+
const wrapper = await mount(OctopusSelect, {
|
|
59
|
+
props: { options, optionLabel: 'name' },
|
|
60
|
+
});
|
|
61
|
+
await wrapper.find('input').trigger('focus');
|
|
62
|
+
await wrapper.find('input').setValue('zzz');
|
|
63
|
+
await wrapper.find('input').trigger('input');
|
|
64
|
+
expect(wrapper.find('.text-indic').exists()).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('emits update:value with the clicked option', async () => {
|
|
68
|
+
const wrapper = await mount(OctopusSelect, {
|
|
69
|
+
props: { options, optionLabel: 'name' },
|
|
70
|
+
});
|
|
71
|
+
await wrapper.find('input').trigger('focus');
|
|
72
|
+
await wrapper.findAll('.octopus-select-option')[1].trigger('click');
|
|
73
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([options[1]]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('closes dropdown after selecting an option', async () => {
|
|
77
|
+
const wrapper = await mount(OctopusSelect, {
|
|
78
|
+
props: { options, optionLabel: 'name' },
|
|
79
|
+
});
|
|
80
|
+
await wrapper.find('input').trigger('focus');
|
|
81
|
+
await wrapper.findAll('.octopus-select-option')[0].trigger('click');
|
|
82
|
+
expect(wrapper.find('.octopus-select-dropdown').exists()).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('shows selected item label in the field when closed', async () => {
|
|
86
|
+
const wrapper = await mount(OctopusSelect, {
|
|
87
|
+
props: { options, optionLabel: 'name', value: options[0] },
|
|
88
|
+
});
|
|
89
|
+
expect(wrapper.find('.octopus-select-value').text()).toBe('Alpha');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('marks the selected option with the selected class', async () => {
|
|
93
|
+
const wrapper = await mount(OctopusSelect, {
|
|
94
|
+
props: { options, optionLabel: 'name', value: options[0] },
|
|
95
|
+
});
|
|
96
|
+
await wrapper.find('.octopus-select-field').trigger('click');
|
|
97
|
+
const optionButtons = wrapper.findAll('.octopus-select-option');
|
|
98
|
+
expect(optionButtons[0].classes()).toContain('selected');
|
|
99
|
+
expect(optionButtons[1].classes()).not.toContain('selected');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('deselects the current option by default (allowDeselect defaults to true)', async () => {
|
|
103
|
+
const wrapper = await mount(OctopusSelect, {
|
|
104
|
+
props: { options, optionLabel: 'name', optionKey: 'id', value: options[0] },
|
|
105
|
+
});
|
|
106
|
+
await wrapper.find('.octopus-select-field').trigger('click');
|
|
107
|
+
await wrapper.findAll('.octopus-select-option')[0].trigger('click');
|
|
108
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([undefined]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('does not deselect when allowDeselect is false', async () => {
|
|
112
|
+
const wrapper = await mount(OctopusSelect, {
|
|
113
|
+
props: { options, optionLabel: 'name', optionKey: 'id', value: options[0], allowDeselect: false as boolean },
|
|
114
|
+
});
|
|
115
|
+
await wrapper.find('.octopus-select-field').trigger('click');
|
|
116
|
+
await wrapper.findAll('.octopus-select-option')[0].trigger('click');
|
|
117
|
+
expect(wrapper.emitted('update:value')?.[0]).toEqual([options[0]]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('does not open dropdown when disabled', async () => {
|
|
121
|
+
const wrapper = await mount(OctopusSelect, {
|
|
122
|
+
props: { options, optionLabel: 'name', isDisabled: true },
|
|
123
|
+
});
|
|
124
|
+
await wrapper.find('input').trigger('focus');
|
|
125
|
+
expect(wrapper.find('.octopus-select-dropdown').exists()).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('disables input when isDisabled is true', async () => {
|
|
129
|
+
const wrapper = await mount(OctopusSelect, {
|
|
130
|
+
props: { options, optionLabel: 'name', isDisabled: true },
|
|
131
|
+
});
|
|
132
|
+
expect(wrapper.find('input').attributes('disabled')).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('matches by optionKey when checking selection', async () => {
|
|
136
|
+
const value = { id: 1, name: 'Alpha' };
|
|
137
|
+
const wrapper = await mount(OctopusSelect, {
|
|
138
|
+
props: { options, optionLabel: 'name', optionKey: 'id', value },
|
|
139
|
+
});
|
|
140
|
+
await wrapper.find('.octopus-select-field').trigger('click');
|
|
141
|
+
expect(wrapper.findAll('.octopus-select-option')[0].classes()).toContain('selected');
|
|
142
|
+
});
|
|
143
|
+
});
|