@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 41.12.0 (En cours)
|
|
4
|
+
|
|
5
|
+
**Features**
|
|
6
|
+
|
|
7
|
+
- Export de `colorFromString` pour usage hors du sdk
|
|
8
|
+
- Ajout d'un composable `useSticky` pour déterminer quand un élément sticky se
|
|
9
|
+
déclenche
|
|
10
|
+
- Ajout de `@stylistic/eslint-plugin` et mise à jour des règles eslint
|
|
11
|
+
- Ajout d'un prop dans `ClassicPopover` pour modifier le z-index
|
|
12
|
+
- Ajout d'un slot `option` pour `ClassicSelect`
|
|
13
|
+
- Composants `Octopus`
|
|
14
|
+
- Ajout de `OctopusSelect`, une version moderne de `ClassicSelect`
|
|
15
|
+
- Mise en commun de code entre `OctopusSelect` & `OctopusMultiselect`
|
|
16
|
+
- Ajustement de `OctopusMultiselect` pour fonctionner correctement avec des
|
|
17
|
+
objets identiques avec références différentes
|
|
18
|
+
- Ajout de nouvelles classes utilitaires dans le css
|
|
19
|
+
- Ajout de nouvelles options d'apparence pour `ClassicButton`
|
|
20
|
+
- Changement propriété principale de `ClassicButtonGroup` pour plus de cohérence
|
|
21
|
+
|
|
22
|
+
**Fixes**
|
|
23
|
+
|
|
24
|
+
- Correction d'une exception dans `deepEquals` si la comparaison est faite entre
|
|
25
|
+
une variable primitive et un objet
|
|
26
|
+
|
|
27
|
+
**Misc**
|
|
28
|
+
|
|
29
|
+
- Mise à jour des règles eslint
|
|
30
|
+
|
|
31
|
+
## 41.11.1 (17/06/2026)
|
|
32
|
+
|
|
33
|
+
**Fixes**
|
|
34
|
+
|
|
35
|
+
- **14257** - Correction lecture radio depuis orga sécurisée
|
|
36
|
+
- **14587** - Correction détection de langue pour les sous-titres
|
|
37
|
+
|
|
3
38
|
## 41.11.0 (05/06/2026)
|
|
4
39
|
|
|
5
40
|
**Features**
|
package/eslint-config.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import eslint from '@eslint/js';
|
|
|
2
2
|
import eslintPluginVue from 'eslint-plugin-vue';
|
|
3
3
|
import globals from 'globals';
|
|
4
4
|
import typescriptEslint from 'typescript-eslint';
|
|
5
|
+
import stylistic from '@stylistic/eslint-plugin';
|
|
5
6
|
|
|
6
7
|
export default typescriptEslint.config(
|
|
7
8
|
{ ignores: ['*.d.ts', '**/coverage', '**/dist'] },
|
|
@@ -11,6 +12,9 @@ export default typescriptEslint.config(
|
|
|
11
12
|
...typescriptEslint.configs.recommended,
|
|
12
13
|
...eslintPluginVue.configs['flat/recommended'],
|
|
13
14
|
],
|
|
15
|
+
plugins: {
|
|
16
|
+
'@stylistic': stylistic,
|
|
17
|
+
},
|
|
14
18
|
files: ['**/*.{ts,vue}'],
|
|
15
19
|
languageOptions: {
|
|
16
20
|
ecmaVersion: 'latest',
|
|
@@ -33,11 +37,15 @@ export default typescriptEslint.config(
|
|
|
33
37
|
"vue/no-ref-as-operand": ['error'],
|
|
34
38
|
|
|
35
39
|
// Indentation
|
|
40
|
+
"@stylistic/indent": ['warn', 4, { "SwitchCase": 0 }],
|
|
36
41
|
"vue/html-indent": ['warn', 4],
|
|
37
42
|
"vue/script-indent": ['warn', 4],
|
|
43
|
+
"@stylistic/no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
|
|
38
44
|
|
|
39
45
|
// Number of attributes per line (increase because sometimes two is not a lot)
|
|
40
|
-
"vue/max-attributes-per-line": ['warn', { singleline: 2 } ]
|
|
41
|
-
|
|
46
|
+
"vue/max-attributes-per-line": ['warn', { singleline: 2 } ],
|
|
47
|
+
|
|
48
|
+
"@typescript-eslint/no-unused-vars": "warn"
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
);
|
package/index.ts
CHANGED
|
@@ -59,7 +59,8 @@ export * from "./src/components/buttons/";
|
|
|
59
59
|
// Form
|
|
60
60
|
import ClassicButtonGroup from "./src/components/form/ClassicButtonGroup.vue";
|
|
61
61
|
import OctopusMultiselect from "./src/components/form/OctopusMultiselect.vue";
|
|
62
|
-
|
|
62
|
+
import OctopusSelect from "./src/components/form/OctopusSelect.vue";
|
|
63
|
+
export { ClassicButtonGroup, OctopusMultiselect, OctopusSelect };
|
|
63
64
|
export type { ButtonGroupOption } from "./src/components/form/ClassicButtonGroup.vue";
|
|
64
65
|
|
|
65
66
|
//Display
|
|
@@ -144,6 +145,7 @@ export { useSeoTitleUrl } from "./src/components/composable/route/useSeoTitleUrl
|
|
|
144
145
|
export { useSeasonsManagement } from "./src/components/composable/useSeasonsManagement.ts";
|
|
145
146
|
export { useTranslation } from "./src/components/composable/useTranslation.ts";
|
|
146
147
|
export { useDayjs } from "./src/components/composable/useDayjs.ts";
|
|
148
|
+
export { useSticky } from "./src/components/composable/useSticky.ts";
|
|
147
149
|
|
|
148
150
|
//helper
|
|
149
151
|
import domHelper from "./src/helper/domHelper.ts";
|
|
@@ -155,6 +157,7 @@ import downloadHelper from "./src/helper/downloadHelper.ts";
|
|
|
155
157
|
import displayHelper from "./src/helper/displayHelper.ts";
|
|
156
158
|
import debounce from "./src/helper/debounceHelper.ts";
|
|
157
159
|
import { deepEqual } from "./src/helper/equals.ts";
|
|
160
|
+
export { colorFromString } from './src/helper/colorFromString';
|
|
158
161
|
|
|
159
162
|
//stores
|
|
160
163
|
import {useVastStore} from "./src/stores/VastStore.ts";
|
|
@@ -175,7 +178,7 @@ export * from "./src/api";
|
|
|
175
178
|
// Types
|
|
176
179
|
export { type Emission, SeasonMode, emptyEmissionData } from "./src/stores/class/general/emission.ts";
|
|
177
180
|
export { type Organisation, type OrganisationAttributes, emptyOrganisationData, emptyOrgaData } from "./src/stores/class/general/organisation.ts";
|
|
178
|
-
export { type Podcast, type PodcastAvailability, PodcastType } from "./src/stores/class/general/podcast.ts";
|
|
181
|
+
export { type Podcast, type SimplifiedPodcast, type PodcastAvailability, PodcastType, podcastToSimplified } from "./src/stores/class/general/podcast.ts";
|
|
179
182
|
export { type Playlist, type PlaylistRule } from "./src/stores/class/general/playlist.ts";
|
|
180
183
|
export { type Annotations } from "./src/stores/class/general";
|
|
181
184
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saooti/octopus-sdk",
|
|
3
|
-
"version": "41.
|
|
3
|
+
"version": "41.12.0-beta",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Javascript SDK for using octopus",
|
|
6
6
|
"author": "Saooti",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@pinia/testing": "^1.0.3",
|
|
64
|
+
"@stylistic/eslint-plugin": "*",
|
|
64
65
|
"@types/sockjs-client": "^1.5.4",
|
|
65
66
|
"@types/webpack-env": "^1.18.8",
|
|
66
67
|
"@vitejs/plugin-vue": "^5.2.4",
|
|
@@ -90,9 +91,11 @@
|
|
|
90
91
|
"url": "git+https://github.com/saooti/octopus-sdk.git"
|
|
91
92
|
},
|
|
92
93
|
"peerDependencies": {
|
|
94
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
93
95
|
"dayjs": "^1.11.0",
|
|
94
96
|
"eslint-plugin-vue": "^10.9.1",
|
|
95
97
|
"pinia": ">=2.3.0",
|
|
98
|
+
"typescript-eslint": "^8.47.0",
|
|
96
99
|
"vue": "^3.5.0",
|
|
97
100
|
"vue-i18n": "^11.1.0",
|
|
98
101
|
"vue-router": "^4.6.0"
|
package/src/api/radioApi.ts
CHANGED
|
@@ -14,6 +14,19 @@ async function get(radioId: number): Promise<Canal> {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Retrieve all radio canal by organisation
|
|
19
|
+
* @param organisationId Id of the organisation
|
|
20
|
+
* @returns The list of canal data
|
|
21
|
+
*/
|
|
22
|
+
async function getAll(organisationId: string): Promise<Array<Canal>> {
|
|
23
|
+
return await classicApi.fetchData<Array<Canal>>({
|
|
24
|
+
api: ModuleApi.RADIO,
|
|
25
|
+
path: "canal/orga/" + organisationId + "/"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
export const radioApi = {
|
|
18
|
-
get
|
|
30
|
+
get,
|
|
31
|
+
getAll
|
|
19
32
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- A simple button with accessibility in mind -->
|
|
2
2
|
<template>
|
|
3
3
|
<button
|
|
4
|
-
class="
|
|
4
|
+
:class="classes"
|
|
5
5
|
:aria-disabled="disabled"
|
|
6
6
|
:title="title"
|
|
7
7
|
:aria-label="title"
|
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
</template>
|
|
13
13
|
|
|
14
14
|
<script setup lang="ts">
|
|
15
|
+
import { computed } from 'vue';
|
|
16
|
+
|
|
15
17
|
const props = defineProps<{
|
|
16
18
|
/** Disable the button */
|
|
17
19
|
disabled?: boolean;
|
|
18
20
|
/** Title of the button */
|
|
19
21
|
title?: string;
|
|
22
|
+
/** Primary style for button */
|
|
23
|
+
primary?: boolean;
|
|
24
|
+
/** Icon style for button */
|
|
25
|
+
icon?: boolean;
|
|
20
26
|
}>();
|
|
21
27
|
|
|
22
28
|
const emit = defineEmits<{
|
|
@@ -24,6 +30,17 @@ const emit = defineEmits<{
|
|
|
24
30
|
(e: 'click'): void;
|
|
25
31
|
}>();
|
|
26
32
|
|
|
33
|
+
const classes = computed((): Array<string> => {
|
|
34
|
+
const ary: Array<string> = ['btn', 'd-flex'];
|
|
35
|
+
if (props.primary) {
|
|
36
|
+
ary.push('btn-primary');
|
|
37
|
+
}
|
|
38
|
+
if (props.icon) {
|
|
39
|
+
ary.push('btn-icon');
|
|
40
|
+
}
|
|
41
|
+
return ary;
|
|
42
|
+
});
|
|
43
|
+
|
|
27
44
|
function onClick(): void {
|
|
28
45
|
if (!props.disabled) {
|
|
29
46
|
emit('click');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { computed, getCurrentInstance, nextTick, ref } from 'vue';
|
|
2
|
+
import { onClickOutside } from '@vueuse/core';
|
|
3
|
+
import { useI18n } from 'vue-i18n';
|
|
4
|
+
|
|
5
|
+
export interface OctopusDropdownProps<T> {
|
|
6
|
+
options: T[];
|
|
7
|
+
optionLabel: keyof T & string;
|
|
8
|
+
optionKey?: keyof T;
|
|
9
|
+
isDisabled?: boolean;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useOctopusDropdown<T>(
|
|
14
|
+
props: OctopusDropdownProps<T>,
|
|
15
|
+
emitSearch: (query: string) => void,
|
|
16
|
+
idPrefix: string = 'octopus-dropdown'
|
|
17
|
+
) {
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
const instance = getCurrentInstance();
|
|
20
|
+
|
|
21
|
+
const searchQuery = ref('');
|
|
22
|
+
const isOpen = ref(false);
|
|
23
|
+
const isHovered = ref(false);
|
|
24
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
25
|
+
const inputRef = ref<HTMLInputElement | null>(null);
|
|
26
|
+
|
|
27
|
+
const computedId = computed(() => `${idPrefix}-${instance?.uid}`);
|
|
28
|
+
|
|
29
|
+
// vnode.props contains the raw parent-provided props before defineEmits strips them
|
|
30
|
+
// from attrs, so onSearch is present whenever the parent writes @search="...".
|
|
31
|
+
const hasSearchListener = computed(() => !!instance?.vnode.props?.onSearch);
|
|
32
|
+
|
|
33
|
+
const displayedOptions = computed((): T[] => {
|
|
34
|
+
if (hasSearchListener.value) {
|
|
35
|
+
return props.options;
|
|
36
|
+
}
|
|
37
|
+
const query = searchQuery.value.toLowerCase();
|
|
38
|
+
if (!query) {
|
|
39
|
+
return props.options;
|
|
40
|
+
}
|
|
41
|
+
return props.options.filter((option) =>
|
|
42
|
+
getLabel(option).toLowerCase().includes(query)
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const inputPlaceholder = computed(() => props.placeholder ?? t('Search'));
|
|
47
|
+
|
|
48
|
+
function getLabel(option: T): string {
|
|
49
|
+
return option[props.optionLabel] as string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function openDropdown(): void {
|
|
53
|
+
if (props.isDisabled) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
isOpen.value = true;
|
|
57
|
+
nextTick(() => {
|
|
58
|
+
inputRef.value?.focus();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function closeDropdown(): void {
|
|
63
|
+
isOpen.value = false;
|
|
64
|
+
searchQuery.value = '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toggleDropdown(): void {
|
|
68
|
+
if (isOpen.value) {
|
|
69
|
+
closeDropdown();
|
|
70
|
+
} else {
|
|
71
|
+
openDropdown();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handleInput(): void {
|
|
76
|
+
if (hasSearchListener.value) {
|
|
77
|
+
emitSearch(searchQuery.value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onClickOutside(containerRef, closeDropdown);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
searchQuery,
|
|
85
|
+
isOpen,
|
|
86
|
+
isHovered,
|
|
87
|
+
containerRef,
|
|
88
|
+
inputRef,
|
|
89
|
+
computedId,
|
|
90
|
+
hasSearchListener,
|
|
91
|
+
displayedOptions,
|
|
92
|
+
inputPlaceholder,
|
|
93
|
+
getLabel,
|
|
94
|
+
openDropdown,
|
|
95
|
+
closeDropdown,
|
|
96
|
+
toggleDropdown,
|
|
97
|
+
handleInput,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -24,12 +24,17 @@ export const usePlayerLive = (hlsReady: Ref<boolean>)=>{
|
|
|
24
24
|
const apiStore = useApiStore();
|
|
25
25
|
const authStore = useAuthStore();
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
function needToAddToken(url: string): boolean {
|
|
28
|
+
if (authStore.authParam.accessToken && ("SECURED" === playerStore.playerLive?.organisation?.privacy || playerStore.playerRadio?.secured)) {
|
|
29
|
+
const baseDomain = apiStore.frontendUrl.replace(/^https?:\/\//, '');
|
|
30
|
+
// Add token only if request is on same domain
|
|
31
|
+
return url.includes(baseDomain);
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
30
35
|
|
|
31
36
|
function onPlay(): void {
|
|
32
|
-
playerStore.playerChangeStatus(PlayerStatus.PAUSED ===playerStore.playerStatus);
|
|
37
|
+
playerStore.playerChangeStatus(PlayerStatus.PAUSED === playerStore.playerStatus);
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function playRadio() {
|
|
@@ -77,7 +82,7 @@ export const usePlayerLive = (hlsReady: Ref<boolean>)=>{
|
|
|
77
82
|
!isAndroid
|
|
78
83
|
) {
|
|
79
84
|
let url = playerStore.playerHlsUrl;
|
|
80
|
-
if(needToAddToken
|
|
85
|
+
if(needToAddToken(url)) {
|
|
81
86
|
if (url.includes('?')) {
|
|
82
87
|
url += "&access_token="+authStore.authParam.accessToken;
|
|
83
88
|
} else {
|
|
@@ -123,8 +128,8 @@ export const usePlayerLive = (hlsReady: Ref<boolean>)=>{
|
|
|
123
128
|
backBufferLength:10,
|
|
124
129
|
maxBufferLength:60,
|
|
125
130
|
|
|
126
|
-
xhrSetup: (xhr: XMLHttpRequest) => {
|
|
127
|
-
if (needToAddToken
|
|
131
|
+
xhrSetup: (xhr: XMLHttpRequest, url: string) => {
|
|
132
|
+
if (needToAddToken(url)) {
|
|
128
133
|
xhr.setRequestHeader("Authorization", "Bearer " +authStore.authParam.accessToken);
|
|
129
134
|
}
|
|
130
135
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ref, toValue, onMounted, onBeforeUnmount, type Ref, type MaybeRef } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detects whether a sticky element is currently stuck (fixed at its `top` offset)
|
|
5
|
+
* or in its natural DOM position.
|
|
6
|
+
*
|
|
7
|
+
* A 1px invisible sentinel element is inserted immediately before the sticky element.
|
|
8
|
+
* When that sentinel scrolls out of the IntersectionObserver's root viewport (adjusted
|
|
9
|
+
* by `topOffset`), the element is considered stuck.
|
|
10
|
+
*
|
|
11
|
+
* `topOffset` must match the element's CSS `top` value in pixels. It is read once at
|
|
12
|
+
* mount time. Pass a `Ref<number>` if the value is computed asynchronously (e.g. from
|
|
13
|
+
* a CSS variable set by JavaScript after first render).
|
|
14
|
+
*/
|
|
15
|
+
export function useSticky(elementRef: Ref<HTMLElement | null>, topOffset: MaybeRef<number> = 0) {
|
|
16
|
+
const isStuck = ref(false);
|
|
17
|
+
let sentinel: HTMLElement | null = null;
|
|
18
|
+
let observer: IntersectionObserver | null = null;
|
|
19
|
+
|
|
20
|
+
onMounted(() => {
|
|
21
|
+
const el = elementRef.value;
|
|
22
|
+
if (!el) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const offset = toValue(topOffset);
|
|
27
|
+
|
|
28
|
+
sentinel = document.createElement('div');
|
|
29
|
+
sentinel.style.cssText = 'height:1px;pointer-events:none;';
|
|
30
|
+
el.parentNode!.insertBefore(sentinel, el);
|
|
31
|
+
|
|
32
|
+
observer = new IntersectionObserver(
|
|
33
|
+
([entry]) => { isStuck.value = !entry.isIntersecting; },
|
|
34
|
+
{ rootMargin: `-${offset}px 0px 0px 0px` }
|
|
35
|
+
);
|
|
36
|
+
observer.observe(sentinel);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
onBeforeUnmount(() => {
|
|
40
|
+
observer?.disconnect();
|
|
41
|
+
sentinel?.remove();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return { isStuck };
|
|
45
|
+
}
|
|
@@ -122,17 +122,15 @@ export const useTranslation = () => {
|
|
|
122
122
|
// languages defined on browser, then default, and finally native language
|
|
123
123
|
const languages: string[] = [];
|
|
124
124
|
[userLanguage, ...navigator.languages, DEFAULT_LANGUAGE, translationData.nativeLanguage].forEach(l => {
|
|
125
|
+
// For each language, if it is a variant (for example fr-CH), use
|
|
126
|
+
// only the base language
|
|
127
|
+
if (l.includes('-')) {
|
|
128
|
+
l = l.split('-')[0];
|
|
129
|
+
}
|
|
130
|
+
|
|
125
131
|
if (!languages.includes(l)) {
|
|
126
132
|
languages.push(l);
|
|
127
133
|
}
|
|
128
|
-
// For each language, if it is a variant (for example fr-CH), also
|
|
129
|
-
// add the base language
|
|
130
|
-
if (l.includes('-')) {
|
|
131
|
-
const base = l.split('-')[0];
|
|
132
|
-
if (!languages.includes(base)) {
|
|
133
|
-
languages.push(base);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
134
|
});
|
|
137
135
|
|
|
138
136
|
const podcast = await podcastApi.get(translationData.podcastId);
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<OctopusMultiselect
|
|
3
|
+
:placeholder="placeholder ?? $t('Search - Emission groups placeholder')"
|
|
4
|
+
:selected="groups"
|
|
5
|
+
:options="allGroups"
|
|
6
|
+
:no-border="noBorder"
|
|
5
7
|
option-label="name"
|
|
6
|
-
|
|
7
|
-
:
|
|
8
|
-
width="400px"
|
|
9
|
-
in-modal
|
|
10
|
-
:option-chosen="groups"
|
|
11
|
-
multiple
|
|
12
|
-
@on-search="onSearch"
|
|
13
|
-
@selected="emitSelected"
|
|
8
|
+
option-key="groupId"
|
|
9
|
+
@update:selected="emitSelected"
|
|
14
10
|
/>
|
|
15
11
|
</template>
|
|
16
12
|
|
|
17
13
|
<script setup lang="ts">
|
|
18
|
-
import {
|
|
14
|
+
import { onMounted, ref } from "vue";
|
|
19
15
|
|
|
20
|
-
import
|
|
16
|
+
import OctopusMultiselect from "../../form/OctopusMultiselect.vue";
|
|
21
17
|
import { groupsApi, EmissionGroup } from "../../../api/groupsApi";
|
|
22
18
|
|
|
23
19
|
//Props
|
|
@@ -26,29 +22,31 @@ const props = defineProps<{
|
|
|
26
22
|
organisationId?: string|Array<string>;
|
|
27
23
|
/** Currently selected groups */
|
|
28
24
|
groups: Array<EmissionGroup>;
|
|
25
|
+
/** Change default placeholder */
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
/** Disable borders */
|
|
28
|
+
noBorder?: boolean;
|
|
29
29
|
}>();
|
|
30
30
|
|
|
31
31
|
//Emits
|
|
32
|
-
const emit = defineEmits
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
(e: "update:groups", groups: Array<EmissionGroup>): void;
|
|
34
|
+
}>();
|
|
33
35
|
|
|
34
36
|
//Data
|
|
35
|
-
const maxElement =
|
|
36
|
-
const
|
|
37
|
+
const maxElement = 200;
|
|
38
|
+
const allGroups = ref<Array<EmissionGroup>>([]);
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
async function onSearch(query?: string): Promise<void> {
|
|
40
|
+
onMounted(async() => {
|
|
40
41
|
const response = await groupsApi.search({
|
|
41
42
|
first: 0,
|
|
42
43
|
size: maxElement,
|
|
43
|
-
search: query,
|
|
44
44
|
organisationIds: [props.organisationId].flat(),
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
51
|
-
}
|
|
47
|
+
// Only groups with emissions are available
|
|
48
|
+
allGroups.value = response.result.filter(g => g.emissionIds?.length ?? 0 > 0);
|
|
49
|
+
});
|
|
52
50
|
|
|
53
51
|
function emitSelected(option: Array<EmissionGroup>) {
|
|
54
52
|
emit("update:groups", option);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:key="option.value"
|
|
6
6
|
type="button"
|
|
7
7
|
class="btn"
|
|
8
|
-
:class="{ active:
|
|
8
|
+
:class="{ active: value.includes(option.value) }"
|
|
9
9
|
@click="toggle(option.value)"
|
|
10
10
|
>
|
|
11
11
|
{{ option.label }}
|
|
@@ -13,28 +13,30 @@
|
|
|
13
13
|
</div>
|
|
14
14
|
</template>
|
|
15
15
|
|
|
16
|
-
<script setup lang="ts">
|
|
17
|
-
export interface ButtonGroupOption {
|
|
16
|
+
<script setup lang="ts" generic="T">
|
|
17
|
+
export interface ButtonGroupOption<T> {
|
|
18
18
|
label: string;
|
|
19
|
-
value:
|
|
19
|
+
value: T;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const props = defineProps<{
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/** Currently selected values */
|
|
24
|
+
value: T[];
|
|
25
|
+
/** Possible values */
|
|
26
|
+
options: ButtonGroupOption<T>[];
|
|
25
27
|
}>();
|
|
26
28
|
|
|
27
29
|
const emit = defineEmits<{
|
|
28
|
-
'update:
|
|
30
|
+
(e: 'update:value', values: T[]): void;
|
|
29
31
|
}>();
|
|
30
32
|
|
|
31
|
-
function toggle(value:
|
|
32
|
-
const isSelected = props.
|
|
33
|
-
if (isSelected && props.
|
|
33
|
+
function toggle(value: T): void {
|
|
34
|
+
const isSelected = props.value.includes(value);
|
|
35
|
+
if (isSelected && props.value.length <= 1) { return; }
|
|
34
36
|
const newValues = isSelected
|
|
35
|
-
? props.
|
|
36
|
-
: [...props.
|
|
37
|
-
emit('update:
|
|
37
|
+
? props.value.filter(v => v !== value)
|
|
38
|
+
: [...props.value, value];
|
|
39
|
+
emit('update:value', newValues);
|
|
38
40
|
}
|
|
39
41
|
</script>
|
|
40
42
|
|
|
@@ -17,7 +17,14 @@
|
|
|
17
17
|
:required="displayRequired"
|
|
18
18
|
@change="onChange($event.target.value)"
|
|
19
19
|
>
|
|
20
|
-
<option
|
|
20
|
+
<option
|
|
21
|
+
v-if="placeholder"
|
|
22
|
+
value=""
|
|
23
|
+
disabled
|
|
24
|
+
selected
|
|
25
|
+
>
|
|
26
|
+
{{ placeholder }}
|
|
27
|
+
</option>
|
|
21
28
|
<option
|
|
22
29
|
v-for="option in optionsOrder"
|
|
23
30
|
:key="option.title"
|
|
@@ -25,7 +32,9 @@
|
|
|
25
32
|
:data-selenium="'select-option-' + option.value"
|
|
26
33
|
:style="option.fontFamily ? 'font-family:' + option.fontFamily : ''"
|
|
27
34
|
>
|
|
28
|
-
|
|
35
|
+
<slot name="option" :option="option">
|
|
36
|
+
{{ option.title }}
|
|
37
|
+
</slot>
|
|
29
38
|
</option>
|
|
30
39
|
</select>
|
|
31
40
|
</div>
|