@saooti/octopus-sdk 41.0.13 → 41.0.14

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/eslint.config.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  import eslint from '@eslint/js';
2
- import eslintConfigPrettier from 'eslint-config-prettier';
3
2
  import eslintPluginVue from 'eslint-plugin-vue';
4
3
  import globals from 'globals';
5
4
  import typescriptEslint from 'typescript-eslint';
@@ -23,7 +22,10 @@ export default typescriptEslint.config(
23
22
  },
24
23
  rules: {
25
24
  // your rules
25
+ "curly": ['error'],
26
+ "no-console": ['warn', { allow: ['warn', 'error'] }],
27
+ "no-warning-comments": ['warn'],
28
+ "no-duplicate-imports": ['warn']
26
29
  },
27
- },
28
- eslintConfigPrettier
30
+ }
29
31
  );
package/index.ts CHANGED
@@ -120,6 +120,7 @@ import cookiesHelper from "./src/helper/cookiesHelper.ts";
120
120
  import downloadHelper from "./src/helper/downloadHelper.ts";
121
121
  import displayHelper from "./src/helper/displayHelper.ts";
122
122
  import debounce from "./src/helper/debounceHelper.ts";
123
+ import { deepEqual } from "./src/helper/equals.ts";
123
124
 
124
125
  //stores
125
126
  import {useVastStore} from "./src/stores/VastStore.ts";
@@ -147,6 +148,9 @@ export const getRadiolineIcon = () => import("./src/components/icons/RadiolineIc
147
148
  export const getTuninIcon = () => import("./src/components/icons/TuninIcon.vue");
148
149
  export const getXIcon = () => import("./src/components/icons/XIcon.vue");
149
150
 
151
+ // Routing
152
+ import { setupRouter } from './src/router/utils';
153
+
150
154
  export {
151
155
  useResizePhone,
152
156
  useTagOf,
@@ -175,6 +179,8 @@ export {
175
179
  ModuleApi,
176
180
  classicApi,
177
181
  cookiesHelper,
182
+ deepEqual,
178
183
  downloadHelper,
179
- displayHelper
184
+ displayHelper,
185
+ setupRouter
180
186
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saooti/octopus-sdk",
3
- "version": "41.0.13",
3
+ "version": "41.0.14",
4
4
  "private": false,
5
5
  "description": "Javascript SDK for using octopus",
6
6
  "author": "Saooti",
@@ -32,7 +32,6 @@
32
32
  "axios": "^1.9.0",
33
33
  "dayjs": "^1.11.13",
34
34
  "emoji-mart-vue-fast": "^15.0.4",
35
- "eslint-config-prettier": "^10.1.5",
36
35
  "express": "^5.1.0",
37
36
  "globals": "^16.2.0",
38
37
  "hls.js": "^1.6.5",
@@ -11,7 +11,8 @@ import fetchHelper from "../../../helper/fetchHelper";
11
11
  import classicApi from "../../../api/classicApi";
12
12
  import dayjs from "dayjs";
13
13
  import { FetchParam } from "@/stores/class/general/fetchParam";
14
- export const usePlayerLogic = (forceHide: Ref<boolean, boolean>)=>{
14
+
15
+ export const usePlayerLogic = (forceHide: Ref<boolean, boolean>) => {
15
16
  const hlsReady= ref(false);
16
17
 
17
18
  const { listenTime, onPlay, setDownloadId, onTimeUpdateProgress, playLive, endingLive, playRadio} = usePlayerLive(hlsReady);
@@ -29,11 +29,16 @@ export const useSimplePageParam = (props: {readonly [key:string]: string|number}
29
29
  }
30
30
  });
31
31
 
32
+ // When changing global organisation, update organisation here
33
+ watch(() => filterStore.filterOrgaId, () => {
34
+ organisationId.value = filterStore.filterOrgaId;
35
+ });
36
+
32
37
  onMounted(() => {
33
38
  initOrga();
34
39
  initSearchPattern();
35
40
  isInit.value = true;
36
- })
41
+ });
37
42
 
38
43
  function getMinSize(param:string){
39
44
  return param.length>3 ?param : ""
@@ -122,8 +122,9 @@ const displayArray = computed(() => {
122
122
  });
123
123
  const displayRubriquage = computed(() => state.emissionsPage.rubriquage);
124
124
  const changePaginate = computed(() => `${props.first}|${props.size}`);
125
+ /** Computed property to track for configuration changes */
125
126
  const changed = computed(() => {
126
- return `${props.organisationId}|${props.query}|${props.monetisable}|${props.includeHidden}
127
+ return `${props.organisationId}|${props.query}|${props.monetisable}|${props.includeHidden}|\
127
128
  ${props.iabId}|${props.rubriqueId}|${props.rubriquageId}|${props.before}|${props.after}|${props.sort}|${props.noRubriquageId}`;
128
129
  });
129
130
  const sortText = computed(() => {
@@ -173,7 +173,7 @@ const isSelectValidity = computed(() => {
173
173
 
174
174
  //Watch
175
175
  watch(organisation, async () => {
176
- const hidden =undefined !== organisation.value && organisationRight.value &&!props.isEmission;
176
+ const hidden = undefined !== organisation.value && organisationRight.value && !props.isEmission;
177
177
  if (hidden !== props.includeHidden) {
178
178
  updateIncludeHidden(hidden);
179
179
  }
@@ -190,7 +190,6 @@ watch(()=>props.searchPattern, (value: string) => {
190
190
  });
191
191
  });
192
192
 
193
-
194
193
  //Methods
195
194
  function updateMonetisable(value: string): void {
196
195
  emit("update:monetisable", value);
@@ -253,6 +252,7 @@ function clickShowFilters(): void {
253
252
  showFilters.value = !showFilters.value;
254
253
  }
255
254
  </script>
255
+
256
256
  <style lang="scss">
257
257
  .octopus-app {
258
258
  .advanced-search-container {
@@ -1,66 +1,64 @@
1
1
  <template>
2
- <ClassicSelect
3
- v-if="(!value || init) && organisation"
4
- v-model:text-init="actual"
5
- :display-label="false"
6
- id-select="organisation-chooser-footer"
7
- :label="t('select productor')"
8
- :transparent="true"
9
- :options="[
10
- { title: organisation.name, value: organisation.id },
11
- { title: t('No organisation filter'), value: 'NONE' },
12
- ]"
13
- class="my-1"
14
- />
2
+ <ClassicSelect
3
+ v-if="init && organisation"
4
+ :text-init="actual"
5
+ :display-label="false"
6
+ id-select="organisation-chooser-footer"
7
+ :label="t('select productor')"
8
+ :transparent="true"
9
+ :options="[
10
+ { title: organisation.name, value: organisation.id },
11
+ { title: t('No organisation filter'), value: 'NONE' },
12
+ ]"
13
+ class="my-1"
14
+ @update:text-init="updateOrganisation"
15
+ />
15
16
  </template>
16
17
 
17
18
  <script setup lang="ts">
18
19
  import ClassicSelect from "../../form/ClassicSelect.vue";
19
20
  import { Organisation } from "@/stores/class/general/organisation";
20
21
  import { useSaveFetchStore } from "../../../stores/SaveFetchStore";
21
- import { Ref, ref, watch } from "vue";
22
+ import { computed, ref, watch } from "vue";
22
23
  import { useI18n } from "vue-i18n";
23
-
24
-
25
- //Props
26
- const props = defineProps({
27
- value: { default: undefined, type: String },
28
- reset: { default: false, type: Boolean },
29
- })
24
+ import { useFilterStore } from "../../../stores/FilterStore";
30
25
 
31
26
  //Emits
32
27
  const emit = defineEmits(["selected"]);
33
28
 
34
29
  //Data
35
- const actual = ref("NONE");
36
- const organisation: Ref<Organisation | undefined> = ref(undefined);
30
+ const organisation = ref<Organisation|undefined>(undefined);
37
31
  const init = ref(false);
38
32
 
39
33
  //Composables
40
34
  const { t } = useI18n();
41
35
  const SaveFetchStore = useSaveFetchStore();
36
+ const filterStore = useFilterStore();
42
37
 
38
+ // Computed
39
+ const actual = computed(() => {
40
+ if (filterStore.filterOrgaId) {
41
+ return filterStore.filterOrgaId;
42
+ } else {
43
+ return 'NONE';
44
+ }
45
+ });
43
46
 
44
47
  //Watch
45
- watch(()=>props.value, async () => {
46
- if (!init.value || props.value) {
47
- fetchOrganisation();
48
- }
48
+ watch(()=>filterStore.realOrgaId, async () => {
49
+ fetchOrganisation();
49
50
  }, {deep: true, immediate: true});
50
- watch(()=>props.reset, async () => {
51
- actual.value = "NONE";
52
- });
53
- watch(actual, async () => {
54
- emit("selected","NONE" === actual.value ? undefined : organisation.value);
55
- });
56
51
 
57
52
  //Methods
58
53
  async function fetchOrganisation(): Promise<void> {
59
- if (!props.value) {
54
+ if (!filterStore.realOrgaId) {
60
55
  return;
61
56
  }
62
- organisation.value = await SaveFetchStore.getOrgaData(props.value);
63
- actual.value = organisation.value.id;
57
+ organisation.value = await SaveFetchStore.getOrgaData(filterStore.realOrgaId);
64
58
  init.value = true;
65
59
  }
60
+
61
+ function updateOrganisation(value: string): void {
62
+ emit("selected", "NONE" === value ? undefined : organisation.value);
63
+ }
66
64
  </script>
@@ -228,6 +228,8 @@ function play(isVideo: boolean): void {
228
228
  position: absolute;
229
229
  inset: 0;
230
230
  background-color:var(--octopus-background-transparent);
231
+ // Allow pointer events to go through (allow click on image beneath blur)
232
+ pointer-events: none;
231
233
  }
232
234
 
233
235
  .live-image-status {
@@ -2,7 +2,7 @@
2
2
  <div class="d-flex flex-nowrap align-items-center octopus-form-item">
3
3
  <div :class="isSwitch ? 'octopus-form-switch me-2' : ''">
4
4
  <input
5
- :id="idCheckbox"
5
+ :id="computedIdCheckbox"
6
6
  :checked="textInit"
7
7
  type="checkbox"
8
8
  :disabled="isDisabled"
@@ -16,26 +16,35 @@
16
16
  v-if="isSwitch"
17
17
  class="slider btn-transparent"
18
18
  :title="label"
19
+ :disabled="isDisabled"
19
20
  @click="clickSlider"
20
21
  @keydown.space.prevent="clickSlider"
21
22
  />
22
23
  </div>
23
24
  <label
24
25
  class="c-hand"
25
- :class="[classLabel, displayLabel ? '' : 'd-none']"
26
- :for="idCheckbox"
27
- >{{ label }}</label
26
+ :class="[classLabel, displayLabel ? '' : 'd-none', isDisabled ? 'disabled' : '']"
27
+ :for="computedIdCheckbox"
28
28
  >
29
+ {{ label }}
30
+ </label>
29
31
  </div>
30
32
  </template>
31
33
 
32
34
  <script setup lang="ts">
35
+ import { computed, getCurrentInstance } from 'vue';
36
+
33
37
  //Props
34
38
  const props = defineProps({
39
+ /** The ID for the checkbox input */
35
40
  idCheckbox: { default: "", type: String },
41
+ /** The label to display with the checkbox */
36
42
  label: { default: "", type: String },
43
+ /** Disables input */
37
44
  isDisabled: { default: false, type: Boolean },
45
+ /** The value of the checkbox */
38
46
  textInit: { default: false, type: Boolean },
47
+ /** If true, displays a switch instead of a checkbox */
39
48
  isSwitch: { default: false, type: Boolean },
40
49
  displayLabel: { default: true, type: Boolean },
41
50
  classLabel: { default: "", type: String },
@@ -45,6 +54,11 @@ const props = defineProps({
45
54
  //Emits
46
55
  const emit = defineEmits(["update:textInit", "clickAction"]);
47
56
 
57
+ // Computed
58
+ const computedIdCheckbox = computed(() => {
59
+ return props.idCheckbox || 'checkbox-' + getCurrentInstance()?.uid;
60
+ });
61
+
48
62
  //Methods
49
63
  function emitClickAction(): void {
50
64
  emit("clickAction");
@@ -59,6 +73,12 @@ function clickSlider() {
59
73
 
60
74
  <style lang="scss">
61
75
  .octopus-app {
76
+
77
+ label.disabled {
78
+ color: var(--octopus-text-disabled);
79
+ cursor: default;
80
+ }
81
+
62
82
  .octopus-form-switch {
63
83
  position: relative;
64
84
  display: inline-block;
@@ -92,6 +112,11 @@ function clickSlider() {
92
112
  border-radius: 50%;
93
113
  }
94
114
 
115
+ .slider:disabled::before {
116
+ background-color: var(--octopus-text-disabled);
117
+ opacity: 0.6;
118
+ }
119
+
95
120
  input:checked + .slider {
96
121
  background-color: var(--octopus-primary);
97
122
  }
@@ -8,14 +8,14 @@
8
8
  <component
9
9
  :is="isWysiwyg? 'div': 'label'"
10
10
  :class="[classLabel, displayLabel ? '' : 'd-none']"
11
- :for="isWysiwyg ? '': inputId"
11
+ :for="isWysiwyg ? '': computedInputId"
12
12
  >{{ label }}
13
13
  <AsteriskIcon v-if="displayRequired" :size="10" class="ms-1 mb-2" :title="t('Mandatory input')"/>
14
14
  </component>
15
15
  <slot name="afterTitle"/>
16
16
  <template v-if="popover">
17
17
  <button
18
- :id="'popover' + inputId"
18
+ :id="'popover' + computedInputId"
19
19
  :title="t('Help')"
20
20
  class="btn-transparent"
21
21
  >
@@ -23,7 +23,7 @@
23
23
  </button>
24
24
 
25
25
  <ClassicPopover
26
- :target="'popover' + inputId"
26
+ :target="'popover' + computedInputId"
27
27
  popover-class="popover-z-index"
28
28
  :relative-class="popoverRelativeClass"
29
29
  >
@@ -38,7 +38,7 @@
38
38
  <input
39
39
  v-if="!isWysiwyg && !isTextarea"
40
40
  v-show="showField"
41
- :id="inputId"
41
+ :id="computedInputId"
42
42
  ref="focusElement"
43
43
  v-model="textValue"
44
44
  :type="typeInput"
@@ -59,7 +59,7 @@
59
59
  <textarea
60
60
  v-else-if="isTextarea"
61
61
  v-show="showField"
62
- :id="inputId"
62
+ :id="computedInputId"
63
63
  ref="focusElement"
64
64
  v-model="textValue"
65
65
  :data-selenium="dataSelenium"
@@ -113,10 +113,11 @@
113
113
  </div>
114
114
  </div>
115
115
  </template>
116
+
116
117
  <script setup lang="ts">
117
118
  import AsteriskIcon from "vue-material-design-icons/Asterisk.vue";
118
119
  import HelpCircleIcon from "vue-material-design-icons/HelpCircle.vue";
119
- import { computed, defineAsyncComponent, onMounted, Ref, ref, useTemplateRef, watch } from "vue";
120
+ import { computed, defineAsyncComponent, onMounted, Ref, ref, useTemplateRef, watch, getCurrentInstance } from "vue";
120
121
  import { useI18n } from "vue-i18n";
121
122
  const ClassicPopover = defineAsyncComponent(
122
123
  () => import("../misc/ClassicPopover.vue"),
@@ -132,6 +133,7 @@ const ClassicEmojiPicker = defineAsyncComponent(
132
133
  const props = defineProps({
133
134
  inputId: { default: "", type: String },
134
135
  label: { default: "", type: String },
136
+ /** The input's value */
135
137
  textInit: { default: undefined, type: String },
136
138
  maxLength: { default: 0, type: Number },
137
139
  errorText: { default: "", type: String },
@@ -141,6 +143,7 @@ const props = defineProps({
141
143
  canBeNull: { default: false, type: Boolean },
142
144
  inputMaxLengthField: { default: undefined, type: Number },
143
145
  errorVariable: { default: true, type: Boolean },
146
+ /** Disable the text input */
144
147
  isDisable: { default: false, type: Boolean },
145
148
  indicText: { default: "", type: String },
146
149
  dataSelenium: { default: "", type: String },
@@ -171,6 +174,7 @@ const focusElementRef = useTemplateRef('focusElement');
171
174
  const { t } = useI18n();
172
175
 
173
176
  //Computed
177
+ const computedInputId = computed(() => props.inputId || 'input-' + getCurrentInstance()?.uid);
174
178
  const isError = computed(() => !valueTrimValid.value || !valueLengthValid.value || !valueRegexValid.value);
175
179
  const countValue = computed(() => {
176
180
  if (textValue.value) {
@@ -239,6 +243,7 @@ function addEmojiSelected(emoji: string) {
239
243
  textValue.value = (textValue.value ?? "") + emoji;
240
244
  }
241
245
  </script>
246
+
242
247
  <style lang="scss">
243
248
  .octopus-app .classic-input-text {
244
249
  .text-indic {
@@ -4,9 +4,10 @@
4
4
  <slot v-else name="preview" />
5
5
  </div>
6
6
  </template>
7
+
7
8
  <script setup lang="ts">
8
9
  import { useIntersectionObserver } from "@vueuse/core";
9
- import { ref, nextTick, watch } from "vue";
10
+ import { ref, nextTick, watch, onMounted } from "vue";
10
11
 
11
12
  //Props
12
13
  const props = defineProps({
@@ -37,8 +38,8 @@ const { pause, resume } = useIntersectionObserver(
37
38
  if (isIntersecting) {
38
39
  // perhaps the user re-scrolled to a component that was set to unrender. In that case stop the unrendering timer
39
40
  clearTimeout(unrenderTimer);
40
- // if we're dealing underndering lets add a waiting period of 200ms before rendering. If a component enters the viewport and also leaves it within 200ms it will not render at all. This saves work and improves performance when user scrolls very fast
41
41
 
42
+ // if we're dealing underndering lets add a waiting period of 200ms before rendering. If a component enters the viewport and also leaves it within 200ms it will not render at all. This saves work and improves performance when user scrolls very fast
42
43
  renderTimer = setTimeout(
43
44
  () => {
44
45
  shouldRender.value = true;
@@ -46,6 +47,7 @@ const { pause, resume } = useIntersectionObserver(
46
47
  },
47
48
  props.unrender ? 200 : 0,
48
49
  );
50
+
49
51
  if (!props.unrender) {
50
52
  pause();
51
53
  }
@@ -64,18 +66,27 @@ const { pause, resume } = useIntersectionObserver(
64
66
  );
65
67
 
66
68
  //Logic
67
- setTimeout(() => {
68
- waitBeforeInit.value = false;
69
- }, props.initRenderDelay);
70
- if (props.renderOnIdle) {
71
- onIdle(() => {
72
- shouldRender.value = true;
73
- emit("isRender", true);
74
- if (!props.unrender) {
75
- pause();
76
- }
77
- });
78
- }
69
+ onMounted(() => {
70
+ if (props.initRenderDelay <= 0) {
71
+ // If there's no render delay, do not delay initialization
72
+ waitBeforeInit.value = false;
73
+ } else {
74
+ // Otherwise delay initialization
75
+ setTimeout(() => {
76
+ waitBeforeInit.value = false;
77
+ }, props.initRenderDelay);
78
+ }
79
+
80
+ if (props.renderOnIdle) {
81
+ onIdle(() => {
82
+ shouldRender.value = true;
83
+ emit("isRender", true);
84
+ if (!props.unrender) {
85
+ pause();
86
+ }
87
+ });
88
+ }
89
+ });
79
90
 
80
91
  //Watch
81
92
  watch(
@@ -1,3 +1,6 @@
1
+ <!--
2
+ Component to make a tab-based navigation
3
+ -->
1
4
  <template>
2
5
  <ul class="octopus-nav" :class="light ? 'light' : ''">
3
6
  <li
@@ -5,11 +5,17 @@
5
5
  role="contentinfo"
6
6
  class="d-flex align-items-center justify-content-between border-top mt-auto"
7
7
  >
8
- <div v-if="!state.generalParameters.podcastmaker" class="d-flex flex-column px-1">
8
+ <div
9
+ v-if="!state.generalParameters.podcastmaker"
10
+ class="d-flex flex-column px-1"
11
+ >
9
12
  <div class="text-dark my-1 special-select-align-magic-trick">
10
13
  &copy; Saooti 2025
11
14
  </div>
12
- <FooterGarSection v-if="authStore.isGarRole" :auth-orga-id="authStore.authOrgaId" />
15
+ <FooterGarSection
16
+ v-if="authStore.isGarRole"
17
+ :auth-orga-id="authStore.authOrgaId"
18
+ />
13
19
  <nav :aria-label="t('Site menu')">
14
20
  <ul class="p-0 m-0">
15
21
  <li
@@ -43,13 +49,11 @@
43
49
  class="my-1"
44
50
  />
45
51
  <OrganisationChooserLight
46
- v-if="!state.generalParameters.podcastmaker && organisationId && authenticated"
52
+ v-if="!state.generalParameters.podcastmaker && authenticated"
47
53
  page="footer"
48
54
  width="auto"
49
55
  class="my-1"
50
56
  :defaultanswer="t('No organisation filter')"
51
- :value="organisationId"
52
- :reset="reset"
53
57
  @selected="onOrganisationSelected"
54
58
  />
55
59
  </div>
@@ -83,7 +87,7 @@ import { useFilterStore } from "../../stores/FilterStore";
83
87
  import { useGeneralStore } from "../../stores/GeneralStore";
84
88
  import { useAuthStore } from "../../stores/AuthStore";
85
89
  import { Category } from "@/stores/class/general/category";
86
- import { computed, defineAsyncComponent, Ref, ref, watch } from "vue";
90
+ import { computed, defineAsyncComponent, ref, watch } from "vue";
87
91
  import { Organisation } from "@/stores/class/general/organisation";
88
92
  import { useI18n } from "vue-i18n";
89
93
  import { useRoute, useRouter } from "vue-router";
@@ -98,8 +102,6 @@ const { t, locale } = useI18n();
98
102
 
99
103
  //Data
100
104
  const language = ref(locale);
101
- const reset = ref(false);
102
- const organisationId: Ref<string | undefined> = ref(undefined);
103
105
 
104
106
  //Composables
105
107
  const generalStore = useGeneralStore();
@@ -127,15 +129,6 @@ const routerLinkSecondArray = computed(() => {
127
129
 
128
130
  //Watch
129
131
  watch(language, () => changeLanguage());
130
- watch(()=>filterStore.filterOrgaId, () => {
131
- if (filterStore.filterOrgaId) {
132
- organisationId.value = filterStore.filterOrgaId;
133
- } else {
134
- reset.value = !reset.value;
135
- }
136
- }, {immediate: true});
137
-
138
-
139
132
 
140
133
  //Methods
141
134
  function changeLanguage(): void {
@@ -164,15 +157,20 @@ function changeLanguage(): void {
164
157
  }
165
158
  });
166
159
  }
160
+
161
+ /**
162
+ * Select another organisation
163
+ * @param organisation The new organisation to focus on, or undefined to remove focus
164
+ */
167
165
  async function onOrganisationSelected( organisation: Organisation | undefined): Promise<void> {
166
+ // TODO use router utils
168
167
  if (organisation?.id) {
169
168
  router.push({
170
- query: { ...route.query, ...{ productor: organisation.id, o:undefined } },
169
+ query: { ...route.query, ...{ productor: organisation.id, o:undefined, displayAll: "false" } },
171
170
  });
172
171
  }else{
173
- organisationId.value = undefined;
174
172
  router.push({
175
- query: { ...route.query, ...{ productor: undefined } },
173
+ query: { ...route.query, ...{ productor: undefined, displayAll: "true" } },
176
174
  });
177
175
  }
178
176
  }
@@ -13,22 +13,20 @@
13
13
  :src="logoUrl"
14
14
  aria-hidden="true"
15
15
  alt=""
16
-
17
16
  width="140"
18
17
  height="50"
19
18
  title="Logo"
20
19
  :class="generalStore.platformEducation ? 'education-logo' : 'octopus-logo'"
21
- />
20
+ >
22
21
  <img
23
22
  v-else
24
23
  :src="useProxyImageUrl(imgUrl, '', '80')"
25
24
  aria-hidden="true"
26
25
  alt=""
27
-
28
26
  class="client-logo"
29
27
  title="Logo"
30
28
  :class="generalStore.platformEducation ? 'education-logo' : ''"
31
- />
29
+ >
32
30
  </router-link>
33
31
  <h1 v-if="titleIsDisplayed" class="text-truncate m-0 align-self-center">
34
32
  {{ titleDisplay }}
@@ -46,14 +44,13 @@
46
44
  v-if="authStore.isGarRole"
47
45
  :src="logoUrl"
48
46
  aria-hidden="true"
49
- alt=""
50
-
47
+ alt=""
51
48
  width="100"
52
49
  height="29"
53
50
  class="ms-2"
54
51
  title="Logo"
55
52
  :class="generalStore.platformEducation ? 'education-logo' : 'octopus-logo'"
56
- />
53
+ >
57
54
  <a
58
55
  v-else
59
56
  href="https://www.saooti.com/"
@@ -64,14 +61,14 @@
64
61
  <img
65
62
  :src="logoUrl"
66
63
  aria-hidden="true"
67
- alt=""
64
+ alt=""
68
65
 
69
66
  title="Saooti"
70
67
  width="100"
71
68
  height="29"
72
69
  class="ms-2"
73
70
  :class="generalStore.platformEducation ? 'education-logo' : 'octopus-logo'"
74
- />
71
+ >
75
72
  </a>
76
73
  </template>
77
74
  <div role="navigation" class="d-flex align-items-center justify-content-end flex-grow-1">
@@ -128,10 +125,11 @@
128
125
  {{ link.title }}
129
126
  </router-link>
130
127
  </li>
131
- </template>
128
+ </template>
132
129
  </ul>
133
130
  </nav>
134
131
  </ClassicPopover>
132
+
135
133
  <MobileMenu
136
134
  :is-education="generalStore.platformEducation"
137
135
  :show="mobileMenuDisplay"
@@ -48,8 +48,11 @@ import { useI18n } from "vue-i18n";
48
48
  //Props
49
49
  defineProps({
50
50
  idModal: { default: undefined, type: String },
51
+ /** The title of the modal */
51
52
  titleModal: { default: undefined, type: String },
53
+ /** If false, the modal won't display a close button (default: true) */
52
54
  closable: { default: true, type: Boolean },
55
+ /** If true, the modal can be reduced to show only the header (default: false) */
53
56
  canBeReduced: { default: false, type: Boolean },
54
57
  })
55
58
 
@@ -79,6 +82,7 @@ function closePopup(): void {
79
82
  emit("close");
80
83
  }
81
84
  </script>
85
+
82
86
  <style lang="scss">
83
87
 
84
88
  .octopus-app .octopus-modal.octopus-modal-top-layer{
@@ -39,6 +39,7 @@
39
39
  </button>
40
40
  </div>
41
41
  </template>
42
+
42
43
  <script setup lang="ts">
43
44
  import ChevronUpIcon from "vue-material-design-icons/ChevronUp.vue";
44
45
  import WindowCloseIcon from "vue-material-design-icons/WindowClose.vue";
@@ -52,6 +52,7 @@
52
52
  </template>
53
53
  </section>
54
54
  </template>
55
+
55
56
  <script setup lang="ts">
56
57
  import {usePlayerLogic} from "../../composable/player/usePlayerLogic";
57
58
  import { usePlayerStore } from "../../../stores/PlayerStore";
@@ -14,7 +14,7 @@
14
14
  :button-text="t('All podcast button', { name: c.name })"
15
15
  />
16
16
  <template #preview>
17
- <div style="min-height: 650px"></div>
17
+ <div style="min-height: 650px" />
18
18
  </template>
19
19
  </ClassicLazy>
20
20
  </template>
@@ -33,7 +33,7 @@
33
33
  :button-text="t('All podcast button', { name: r.name })"
34
34
  />
35
35
  <template #preview>
36
- <div style="min-height: 650px"></div>
36
+ <div style="min-height: 650px" />
37
37
  </template>
38
38
  </ClassicLazy>
39
39
  <template v-if="rubriqueDisplay && rubriqueDisplay.length > 0">
@@ -129,8 +129,9 @@ const categories = computed(() => {
129
129
  });
130
130
  } else {
131
131
  arrayCategories = generalStore.storedCategories.filter((c: Category) => {
132
- if (state.generalParameters.podcastmaker)
132
+ if (state.generalParameters.podcastmaker) {
133
133
  return c.podcastOrganisationCount;
134
+ }
134
135
  return c.podcastCount;
135
136
  });
136
137
  }
@@ -38,6 +38,7 @@
38
38
  />
39
39
  </section>
40
40
  </template>
41
+
41
42
  <script setup lang="ts">
42
43
  import PodcastList from "../display/podcasts/PodcastList.vue";
43
44
  import ProductorSearch from "../display/filter/ProductorSearch.vue";
@@ -85,7 +86,6 @@ const {
85
86
  isInit
86
87
  } = useAdvancedParamInit(props, false);
87
88
 
88
-
89
89
  //Computed
90
90
  const orgaArray = computed(() => organisationId.value ? [organisationId.value] : []);
91
91
  const withVideo = computed(() => false === onlyVideo.value ? undefined : true);
@@ -0,0 +1,26 @@
1
+ export function deepEqual(obj1: any, obj2: any) {
2
+
3
+ if(obj1 === obj2) // it's just the same object. No need to compare.
4
+ {return true;}
5
+
6
+ if(isPrimitive(obj1) && isPrimitive(obj2)) // compare primitives
7
+ {return obj1 === obj2;}
8
+
9
+ if(Object.keys(obj1).length !== Object.keys(obj2).length) {
10
+ return false;
11
+ }
12
+
13
+ // compare objects with same number of keys
14
+ for(const key in obj1)
15
+ {
16
+ if(!(key in obj2)) {return false;} //other object doesn't have this prop
17
+ if(!deepEqual(obj1[key], obj2[key])) {return false;}
18
+ }
19
+
20
+ return true;
21
+ }
22
+
23
+ //check if value is primitive
24
+ function isPrimitive(obj: unknown) {
25
+ return (obj !== Object(obj));
26
+ }
@@ -4,12 +4,10 @@ import {
4
4
  RouteLocationNormalized,
5
5
  RouteRecordRaw,
6
6
  } from "vue-router";
7
- import { useFilterStore } from "../stores/FilterStore";
8
- import { useSaveFetchStore } from "@/stores/SaveFetchStore";
9
- import { Rubriquage } from "@/stores/class/rubrique/rubriquage";
10
7
  import classicApi from "@/api/classicApi";
11
- import { useAuthStore } from "../stores/AuthStore";
8
+ import { AuthStore } from "../stores/AuthStore";
12
9
  import fetchHelper from "@/helper/fetchHelper";
10
+ import { setupRouter } from "./utils";
13
11
 
14
12
  /*--------------------------------------------------------------------------
15
13
  Composants publics
@@ -334,13 +332,16 @@ const router = createRouter({
334
332
  history: createWebHistory(),
335
333
  routes: routes,
336
334
  scrollBehavior(to, from) {
337
- if (to.name === from.name && to.meta.noScroll) return false;
338
- return { left: 0, top: 0 };
335
+ if (to.name === from.name && to.meta.noScroll) {
336
+ return false;
337
+ } else {
338
+ return { left: 0, top: 0 };
339
+ }
339
340
  },
340
341
  });
341
342
 
342
343
  //Do in frontoffice but not podcastmakers
343
- async function getMyOrgaActive(authStore: any): Promise<string>{
344
+ async function getMyOrgaActive(authStore: AuthStore): Promise<string>{
344
345
  const orgaActive = await classicApi.fetchData<string>({
345
346
  api: 3,
346
347
  path: "user/active"
@@ -352,72 +353,7 @@ async function getMyOrgaActive(authStore: any): Promise<string>{
352
353
  }
353
354
  return orgaActive;
354
355
  }
355
- async function changeOrgaFilter(orgaFilter: string, filterStore: any){
356
- const saveStore = useSaveFetchStore();
357
- const response = await saveStore.getOrgaData(orgaFilter);
358
- const data = await classicApi.fetchData<Array<Rubriquage>>({
359
- api: 0,
360
- path: "rubriquage/find/" + orgaFilter,
361
- parameters: {
362
- sort: "HOMEPAGEORDER",
363
- homePageOrder: true,
364
- },
365
- specialTreatement: true,
366
- });
367
- const isLive = await saveStore.getOrgaLiveEnabled(orgaFilter);
368
- filterStore.filterUpdateOrga({
369
- orgaId: orgaFilter,
370
- imgUrl: response.imageUrl,
371
- name: response.name,
372
- rubriquageArray: data.filter((element: Rubriquage) => {
373
- return element.rubriques.length;
374
- }),
375
- isLive: isLive,
376
- });
377
- }
378
- let fetchMyOrgaActive = false;
379
- router.beforeResolve(async () =>{
380
- fetchMyOrgaActive = false;
381
- });
382
- router.beforeEach(async (to, from) => {
383
- if ("/logout" === to.path && "/logout" !== from.path) {
384
- setTimeout(() => {
385
- window.location.reload(true);
386
- }, 500);
387
- }
388
- const authStore = useAuthStore();
389
- const filterStore = useFilterStore();
390
-
391
- const isSamePath = to.matched[0]?.path === from.matched[0]?.path && to.path.includes(from.path);
392
- let orgaToFocus = isSamePath ? (to.query.productor?.toString() ?? undefined) : undefined;
393
356
 
394
- if(authStore.authProfile){
395
- if(!isSamePath && !fetchMyOrgaActive){
396
- await getMyOrgaActive(authStore);
397
- fetchMyOrgaActive = true;
398
- }
399
- if(undefined!==orgaToFocus){
400
- orgaToFocus = authStore.authOrgaId;
401
- }
402
- }
403
- if (isSamePath && orgaToFocus !== from.query.productor) {
404
- if (undefined === orgaToFocus) {
405
- filterStore.filterUpdateOrga({ orgaId: undefined });
406
- } else if (filterStore.filterOrgaId !== orgaToFocus) {
407
- await changeOrgaFilter(orgaToFocus, filterStore);
408
- }
409
- }
410
- if (
411
- "/logout" !== to.path &&
412
- filterStore.filterOrgaId !== to.query.productor &&
413
- undefined !== filterStore.filterOrgaId
414
- ) {
415
- return {
416
- path: to.path,
417
- query: { ...to.query, ...{ productor: filterStore.filterOrgaId } },
418
- params: to.params,
419
- name: to.name,
420
- };
421
- }
422
- });
357
+ setupRouter(router, getMyOrgaActive);
358
+
423
359
  export default router;
@@ -0,0 +1,112 @@
1
+ import { Router } from "vue-router";
2
+ import { useFilterStore, FilterStore } from "../stores/FilterStore";
3
+ import { useSaveFetchStore } from "../stores/SaveFetchStore";
4
+ import { Rubriquage } from "../stores/class/rubrique/rubriquage";
5
+ import classicApi from "../api/classicApi";
6
+ import { useAuthStore, AuthStore } from "../stores/AuthStore";
7
+ import { deepEqual } from "../helper/equals";
8
+
9
+ async function changeOrgaFilter(orgaFilter: string, filterStore: FilterStore){
10
+ const saveStore = useSaveFetchStore();
11
+ const response = await saveStore.getOrgaData(orgaFilter);
12
+ const data = await classicApi.fetchData<Array<Rubriquage>>({
13
+ api: 0,
14
+ path: "rubriquage/find/" + orgaFilter,
15
+ parameters: {
16
+ sort: "HOMEPAGEORDER",
17
+ homePageOrder: true,
18
+ },
19
+ specialTreatement: true,
20
+ });
21
+ const isLive = await saveStore.getOrgaLiveEnabled(orgaFilter);
22
+ filterStore.filterUpdateOrga({
23
+ orgaId: orgaFilter,
24
+ imgUrl: response.imageUrl,
25
+ name: response.name,
26
+ rubriquageArray: data.filter((element: Rubriquage) => {
27
+ return element.rubriques.length;
28
+ }),
29
+ isLive: isLive,
30
+ });
31
+ }
32
+
33
+ let fetchMyOrgaActive = false;
34
+ /** Variable used to apply beforeEach redirect only once */
35
+ let resolved = false;
36
+
37
+ /**
38
+ * Utility function seting up the router with a custom beforeEach
39
+ */
40
+ export function setupRouter(router: Router, getMyOrgaActive: (authStore: AuthStore) => Promise<string>): void {
41
+ router.beforeResolve(async () =>{
42
+ fetchMyOrgaActive = false;
43
+ // Reinit variable to allow one redirect
44
+ resolved = false;
45
+ });
46
+
47
+ // Navigation guard that updates current organisation & may make redirects
48
+ router.beforeEach(async (to, from) => {
49
+
50
+ if ("/logout" === to.path && "/logout" !== from.path) {
51
+ setTimeout(() => {
52
+ window.location.reload(true);
53
+ }, 500);
54
+ }
55
+ const authStore = useAuthStore();
56
+ const filterStore = useFilterStore();
57
+
58
+ const isSamePath = to.matched[0]?.path === from.matched[0]?.path && to.path.includes(from.path);
59
+ let orgaToFocus = isSamePath ? (to.query.productor?.toString() ?? undefined) : undefined;
60
+
61
+ if(authStore.authProfile){
62
+ if(!isSamePath && !fetchMyOrgaActive){
63
+ await getMyOrgaActive(authStore);
64
+ fetchMyOrgaActive = true;
65
+ }
66
+ if(undefined!==orgaToFocus){
67
+ orgaToFocus = authStore.authOrgaId;
68
+ }
69
+ }
70
+
71
+ // Update organisation
72
+ if (isSamePath && orgaToFocus !== from.query.productor) {
73
+ if (filterStore.filterOrgaId !== orgaToFocus && orgaToFocus !== undefined) {
74
+ await changeOrgaFilter(orgaToFocus, filterStore);
75
+ }
76
+ }
77
+
78
+ // Only change target if not going to logout and not already resolved
79
+ if ("/logout" !== to.path && resolved !== true) {
80
+ resolved = true;
81
+ const newQuery = {
82
+ ...to.query
83
+ };
84
+
85
+ // Set productor
86
+ if (to.query.productor) {
87
+ newQuery.productor = to.query.productor;
88
+ } else if (filterStore.filterOrgaId === undefined) {
89
+ delete newQuery.productor;
90
+ } else {
91
+ newQuery.productor = filterStore.filterOrgaId;
92
+ }
93
+
94
+ // Enable 'displayAll' mode if already active
95
+ if ((from.query.displayAll === "true" || to.query.displayAll === "true") && to.query.displayAll !== "false") {
96
+ newQuery.displayAll = "true";
97
+ } else {
98
+ delete newQuery.displayAll;
99
+ }
100
+
101
+ // If the queries are different, update path
102
+ if (!deepEqual(newQuery, to.query)) {
103
+ return {
104
+ path: to.path,
105
+ query: { ...newQuery },
106
+ params: to.params,
107
+ name: to.name,
108
+ };
109
+ }
110
+ }
111
+ });
112
+ }
@@ -5,12 +5,14 @@ import { defineStore } from "pinia";
5
5
  import { KeycloakInfo } from "@/stores/class/user/person";
6
6
  import { VideoConfig } from "@/stores/class/config/videoConfig";
7
7
  import classicApi from "../api/classicApi";
8
+
8
9
  interface AuthParam{
9
10
  accessToken?: string;
10
11
  refreshToken?: string;
11
12
  expiration?: Date|string;
12
13
  clientId?: string;
13
14
  }
15
+
14
16
  interface AuthState {
15
17
  authReload: number;
16
18
  authName: string;
@@ -231,3 +233,6 @@ export const useAuthStore = defineStore("AuthStore", {
231
233
  },
232
234
  },
233
235
  });
236
+
237
+ /** Type for the AuthStore */
238
+ export type AuthStore = ReturnType<typeof useAuthStore>;
@@ -1,73 +1,128 @@
1
- import { Category } from "@/stores/class/general/category";
2
- import { Rubriquage } from "@/stores/class/rubrique/rubriquage";
3
- import { RubriquageFilter } from "@/stores/class/rubrique/rubriquageFilter";
4
- import { Rubrique } from "@/stores/class/rubrique/rubrique";
5
- import { defineStore } from "pinia";
1
+ import { computed, ref } from 'vue';
6
2
 
7
- interface FilterState {
8
- filterOrgaId?: string;
9
- filterImgUrl?: string;
10
- filterName?: string;
11
- filterRubriquage: Array<Rubriquage>;
12
- filterRubrique: Array<RubriquageFilter>;
13
- filterRubriqueDisplay: Array<Rubrique>;
14
- filterTypeMedia?: string;
15
- filterSortOrder?: string;
16
- filterSortField?: string;
17
- filterLive?: boolean;
18
- filterIab?: Category;
19
- }
20
- export const useFilterStore = defineStore("FilterStore", {
21
- state: (): FilterState => ({
22
- filterRubriquage: [],
23
- filterRubrique: [],
24
- filterRubriqueDisplay: [],
25
- filterLive: false,
26
- }),
27
- actions: {
28
- filterUpdateOrga(filter: {
29
- orgaId?: string;
30
- imgUrl?: string;
31
- name?: string;
32
- rubriquageArray?: Array<Rubriquage>;
33
- isLive?: boolean;
34
- }) {
35
- if (filter.imgUrl || !filter.orgaId) {
36
- this.filterImgUrl = filter.imgUrl;
37
- }
38
- if (filter.name || !filter.orgaId) {
39
- this.filterName = filter.name;
40
- }
41
- if (filter.rubriquageArray) {
42
- this.filterRubriquage = filter.rubriquageArray;
43
- }
44
- this.filterLive = filter.isLive;
45
- this.filterIab = undefined;
46
- this.filterOrgaId = filter.orgaId;
47
- },
48
- filterUpdateIab(iab?: Category) {
49
- this.filterIab = iab;
50
- },
51
- filterUpdateRubrique(rubriqueFilter: Array<RubriquageFilter>) {
52
- this.filterRubrique = rubriqueFilter;
53
- },
54
- filterUpdateRubriqueDisplay(rubriques: Array<Rubrique>) {
55
- this.filterRubriqueDisplay = rubriques.filter(rubrique=> rubrique);
56
- },
57
- filterUpdateMedia(filter: {
58
- type?: string;
59
- order?: string;
60
- field?: string;
61
- }) {
62
- if (filter.type) {
63
- this.filterTypeMedia = filter.type;
64
- }
65
- if (filter.order) {
66
- this.filterSortOrder = filter.order;
67
- }
68
- if (filter.field) {
69
- this.filterSortField = filter.field;
70
- }
71
- },
72
- },
3
+ import { Category } from '@/stores/class/general/category';
4
+ import { Rubriquage } from '@/stores/class/rubrique/rubriquage';
5
+ import { RubriquageFilter } from '@/stores/class/rubrique/rubriquageFilter';
6
+ import { Rubrique } from '@/stores/class/rubrique/rubrique';
7
+ import { defineStore } from 'pinia';
8
+ import { useAuthStore } from './AuthStore';
9
+ import { useRoute } from 'vue-router';
10
+
11
+ /**
12
+ * Store managing data regarding the filters to apply to know which
13
+ * podcasts to show.
14
+ */
15
+ export const useFilterStore = defineStore("FilterStore", () => {
16
+ const _filterOrgaId = ref<string|null>(null);
17
+ const filterImgUrl = ref<string>();
18
+ const filterName = ref<string>();
19
+ const filterRubriquage = ref<Array<Rubriquage>>([]);
20
+ const filterRubrique = ref<Array<RubriquageFilter>>([]);
21
+ const filterRubriqueDisplay = ref<Array<Rubrique>>([]);
22
+ const filterTypeMedia = ref<string>();
23
+ const filterSortOrder = ref<string>();
24
+ const filterSortField = ref<string>();
25
+ const filterLive = ref<boolean>(false);
26
+ const filterIab = ref<Category>();
27
+
28
+ const route = useRoute();
29
+ const authStore = useAuthStore();
30
+
31
+ /**
32
+ * ID of the current organisation.
33
+ */
34
+ const filterOrgaId = computed((): string|undefined => {
35
+ if (route?.query.displayAll === "true") {
36
+ return undefined;
37
+ } else if (route?.query.productor) {
38
+ return route.query.productor as string;
39
+ } else if(_filterOrgaId.value === null) {
40
+ return authStore.authOrgaId;
41
+ } else {
42
+ return _filterOrgaId.value ?? undefined;
43
+ }
44
+ });
45
+
46
+ /**
47
+ * The ID of the current organisation, regardless of other options.
48
+ * Use this if you want to know the organisation of the user even in
49
+ * unfocused mode (ie displayAll = true)
50
+ */
51
+ const realOrgaId = computed(() => {
52
+ return _filterOrgaId.value ?? undefined;
53
+ });
54
+
55
+ function filterUpdateOrga(filter: {
56
+ orgaId?: string;
57
+ imgUrl?: string;
58
+ name?: string;
59
+ rubriquageArray?: Array<Rubriquage>;
60
+ isLive?: boolean;
61
+ }) {
62
+ if (filter.imgUrl || !filter.orgaId) {
63
+ filterImgUrl.value = filter.imgUrl;
64
+ }
65
+ if (filter.name || !filter.orgaId) {
66
+ filterName.value = filter.name;
67
+ }
68
+ if (filter.rubriquageArray) {
69
+ filterRubriquage.value = filter.rubriquageArray;
70
+ }
71
+ filterLive.value = filter.isLive ?? false;
72
+ filterIab.value = undefined;
73
+ _filterOrgaId.value = filter.orgaId ?? null;
74
+ }
75
+
76
+ function filterUpdateIab(iab?: Category) {
77
+ filterIab.value = iab;
78
+ }
79
+
80
+ function filterUpdateRubrique(rubriqueFilter: Array<RubriquageFilter>) {
81
+ filterRubrique.value = rubriqueFilter;
82
+ }
83
+
84
+ function filterUpdateRubriqueDisplay(rubriques: Array<Rubrique>) {
85
+ filterRubriqueDisplay.value = rubriques.filter(rubrique=> rubrique);
86
+ }
87
+
88
+ function filterUpdateMedia(filter: {
89
+ type?: string;
90
+ order?: string;
91
+ field?: string;
92
+ }) {
93
+ if (filter.type) {
94
+ filterTypeMedia.value = filter.type;
95
+ }
96
+ if (filter.order) {
97
+ filterSortOrder.value = filter.order;
98
+ }
99
+ if (filter.field) {
100
+ filterSortField.value = filter.field;
101
+ }
102
+ }
103
+
104
+ return {
105
+ filterOrgaId,
106
+ realOrgaId,
107
+
108
+ filterUpdateOrga,
109
+ filterUpdateIab,
110
+ filterUpdateRubrique,
111
+ filterUpdateRubriqueDisplay,
112
+ filterUpdateMedia,
113
+
114
+ filterImgUrl,
115
+ filterName,
116
+ filterRubriquage,
117
+ filterRubrique,
118
+ filterRubriqueDisplay,
119
+ filterTypeMedia,
120
+ filterSortOrder,
121
+ filterSortField,
122
+ filterLive,
123
+ filterIab
124
+ };
73
125
  });
126
+
127
+ /** Type for the FilterStore */
128
+ export type FilterStore = ReturnType<typeof useFilterStore>;
@@ -141,6 +141,11 @@ export const usePlayerStore = defineStore("PlayerStore", {
141
141
  },
142
142
  },
143
143
  actions: {
144
+ /**
145
+ * Start playing audio/video
146
+ * @param param The data
147
+ * @param isVideo If true, enable video mode
148
+ */
144
149
  async playerPlay(param?: any, isVideo = false) {
145
150
  if (!param) {
146
151
  this.playerCurrentChange = null;