@paris-ias/list 1.3.4 → 1.3.5

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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@paris-ias/list",
3
3
  "configKey": "list",
4
- "version": "1.3.4",
4
+ "version": "1.3.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
@@ -155,7 +155,6 @@
155
155
  <script setup>
156
156
  import { useNuxtApp, ref, computed } from "#imports"
157
157
  import { useDisplay } from "vuetify"
158
- import { useRootStore } from "../../../stores/root"
159
158
 
160
159
  const { $stores } = useNuxtApp()
161
160
  const props = defineProps({
@@ -166,34 +165,16 @@ const props = defineProps({
166
165
  })
167
166
  const feminine = ref(["news", "publications", "people"].includes(props.type))
168
167
  const { smAndDown: isSmAndDown } = useDisplay()
169
- const rootStore = useRootStore()
170
168
 
171
- const isLoading = computed(() =>
169
+ // The skeleton must track the loading state ONLY. A previous extra clause
170
+ // (`isZeroTotal && (isAnySearchActive || itemsCount > 0)`) was permanently true
171
+ // after any search that returned zero results — so a completed empty search
172
+ // (e.g. "posten") stayed pinned in skeleton mode instead of showing the
173
+ // "0 items found searching for X" message, which is exactly the v-else branch
174
+ // below and the correct thing to render in that case.
175
+ const showSkeleton = computed(() =>
172
176
  Boolean($stores[props.type] && $stores[props.type].loading),
173
177
  )
174
- const isTypeSearchActive = computed(() => {
175
- const s = $stores[props.type]?.search
176
- return Boolean(s && s.length)
177
- })
178
- const isGlobalSearchActive = computed(
179
- () => typeof rootStore.search === "string" && rootStore.search.length > 0,
180
- )
181
- const isAnySearchActive = computed(
182
- () => isTypeSearchActive.value || isGlobalSearchActive.value,
183
- )
184
- const isZeroTotal = computed(
185
- () => Number($stores[props.type]?.total || 0) === 0,
186
- )
187
- const itemsCount = computed(() =>
188
- Array.isArray($stores[props.type]?.items)
189
- ? $stores[props.type]?.items?.length || 0
190
- : 0,
191
- )
192
- const showSkeleton = computed(
193
- () =>
194
- isLoading.value ||
195
- (isZeroTotal.value && (isAnySearchActive.value || itemsCount.value > 0)),
196
- )
197
178
  </script>
198
179
 
199
180
  <style lang="scss" scoped></style>
@@ -54,8 +54,6 @@ import {
54
54
  useNuxtApp,
55
55
  resolveComponent,
56
56
  computed,
57
- onUpdated,
58
- onMounted,
59
57
  watch,
60
58
  onBeforeUnmount,
61
59
  nextTick,
@@ -168,6 +166,22 @@ if (data.value) {
168
166
  rootStore.applyListResult(props.type, data.value)
169
167
  }
170
168
 
169
+ // Keep the store `loading` flag in lockstep with the query's real `pending`
170
+ // state. This is the single source of truth for every skeleton (the items view
171
+ // and SearchString both read `$stores[type].loading`). Deriving it from
172
+ // `pending` avoids the SSR/hydration race that previously left a search-on-load
173
+ // (e.g. ?search=posten) pinned in skeleton mode: on a hard load the manual
174
+ // `onMounted` reset and the `variables` watcher could fire in an order that
175
+ // re-set `loading: true` and never cleared it. `pending` flips to false exactly
176
+ // once, when the query resolves, regardless of that ordering.
177
+ watch(
178
+ pending,
179
+ (isPending) => {
180
+ rootStore.setLoading(Boolean(isPending), props.type)
181
+ },
182
+ { immediate: true },
183
+ )
184
+
171
185
  // Watch for route query changes to update filters
172
186
  watch(
173
187
  () => route.query,
@@ -186,13 +200,17 @@ watch(
186
200
  async (newVars, oldVars) => {
187
201
  if (newVars && JSON.stringify(newVars) !== JSON.stringify(oldVars)) {
188
202
  /* console.log("Variables changed, refreshing query, newVars: ", newVars) */
189
- rootStore.setLoading(true, props.type)
190
- await refresh()
191
- if (data.value) {
192
- /* console.log("Applying refreshed data to store") */
193
- rootStore.applyListResult(props.type, data.value)
203
+ // `loading` is driven by the `pending` watch above — `refresh()` toggles
204
+ // `pending` for us, so we only own the refetch + applying the result here.
205
+ try {
206
+ await refresh()
207
+ if (data.value) {
208
+ /* console.log("Applying refreshed data to store") */
209
+ rootStore.applyListResult(props.type, data.value)
210
+ }
211
+ } catch (e) {
212
+ console.error("List refresh failed: ", e)
194
213
  }
195
- rootStore.setLoading(false, props.type)
196
214
  }
197
215
  },
198
216
  { deep: true },
@@ -201,21 +219,12 @@ watch(
201
219
  // Reactive items computed from the store (single source of truth)
202
220
  const items = computed(() => $stores[props.type]?.items || [])
203
221
 
204
- onMounted(() => {
205
- // On initial mount: clear loading state
206
- /* console.log("STOP local loading from mounted") */
207
- rootStore.setLoading(false, props.type)
208
- })
209
-
210
222
  onBeforeUnmount(() => {
211
223
  rootStore.resetState(props.type, locale.value)
212
224
  })
213
225
  async function onPageChange(newPage) {
214
226
  /* console.log("onPageChange: ", newPage) */
215
227
 
216
- // Set loading state first
217
- rootStore.setLoading(true, props.type)
218
-
219
228
  // Update the page in the store
220
229
  rootStore.updatePage({
221
230
  page: newPage,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "license": "AGPL-3.0-only",
3
3
  "main": "./dist/module.mjs",
4
- "version": "1.3.4",
4
+ "version": "1.3.5",
5
5
  "name": "@paris-ias/list",
6
6
  "repository": {
7
7
  "url": "git+https://github.com/IEA-Paris/list.git",