Fix virtual sample routing and refactor for clarity
Virtual sample variations now display correctly when loading from URL hash. Old URLs with underscores are normalized to hyphens on load. URL hash updates automatically when navigating between variations. Refactored both DynamicView and Selector components with explicit function names, removed unnecessary comments, and improved code organization. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
dfb8d1038b
commit
6b80e242b8
2 changed files with 224 additions and 168 deletions
|
|
@ -76,112 +76,149 @@ const { items, label, isCompareModeEnabled, index } = defineProps({
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const currentValue = ref(null);
|
const currentValue = ref(null);
|
||||||
const syncing = ref(false); // empêche les réactions pendant les mises à jour programmatiques
|
const syncing = ref(false);
|
||||||
|
|
||||||
// Store
|
|
||||||
const { activeTracks } = storeToRefs(useDialogStore());
|
const { activeTracks } = storeToRefs(useDialogStore());
|
||||||
|
|
||||||
// Utils
|
function normalizeSlug(slug) {
|
||||||
function isSame(a, b) {
|
return slug.replace(/_/g, '-');
|
||||||
if (!a || !b) return false;
|
|
||||||
if (a.slug && b.slug) return a.slug === b.slug;
|
|
||||||
return a.title === b.title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toVariation(v) {
|
function areVariationsEqual(variationA, variationB) {
|
||||||
if (!v) return null;
|
if (!variationA || !variationB) return false;
|
||||||
return Array.isArray(v) ? v[v.length - 1] || null : v;
|
|
||||||
|
if (variationA.slug && variationB.slug) {
|
||||||
|
return normalizeSlug(variationA.slug) === normalizeSlug(variationB.slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variationA.title === variationB.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialisation : remplir le 1er select localement ET initialiser le store
|
function extractVariation(value) {
|
||||||
onBeforeMount(() => {
|
if (!value) return null;
|
||||||
syncing.value = true;
|
return Array.isArray(value) ? value[value.length - 1] || null : value;
|
||||||
|
}
|
||||||
|
|
||||||
if (index === 0) {
|
function convertValueForCompareMode(value, shouldBeArray) {
|
||||||
currentValue.value = items[0] || null;
|
if (shouldBeArray) {
|
||||||
// si le store est vide, initialiser avec la variation du premier sélecteur
|
return value && !Array.isArray(value) ? [value] : value;
|
||||||
if (!activeTracks.value || activeTracks.value.length === 0) {
|
|
||||||
const v = toVariation(items[0]);
|
|
||||||
if (v) activeTracks.value = [v];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// les autres ne forcent pas le store ; leur currentValue restera à null
|
return Array.isArray(value) ? value[0] || null : value;
|
||||||
currentValue.value = null;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nextTick(() => (syncing.value = false));
|
function findMatchingVariationsInStore(storeVariations) {
|
||||||
});
|
return storeVariations.filter((storeVar) =>
|
||||||
|
items.some((item) => areVariationsEqual(item, storeVar))
|
||||||
// Quand on bascule compare mode (objet <-> tableau)
|
|
||||||
watch(
|
|
||||||
() => isCompareModeEnabled,
|
|
||||||
(flag) => {
|
|
||||||
syncing.value = true;
|
|
||||||
if (flag) {
|
|
||||||
if (currentValue.value && !Array.isArray(currentValue.value)) {
|
|
||||||
currentValue.value = [currentValue.value];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (Array.isArray(currentValue.value)) {
|
|
||||||
currentValue.value = currentValue.value[0] || null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nextTick(() => (syncing.value = false));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Détection ajout / suppression dans le MultiSelect (côté composant)
|
|
||||||
// On n'agit que si l'ajout/suppression concerne une variation appartenant à `items`
|
|
||||||
watch(
|
|
||||||
currentValue,
|
|
||||||
(newVal, oldVal) => {
|
|
||||||
if (syncing.value) return;
|
|
||||||
|
|
||||||
const newItems = Array.isArray(newVal) ? newVal : newVal ? [newVal] : [];
|
|
||||||
const oldItems = Array.isArray(oldVal) ? oldVal : oldVal ? [oldVal] : [];
|
|
||||||
|
|
||||||
const added = newItems.find((n) => !oldItems.some((o) => isSame(o, n)));
|
|
||||||
const removed = oldItems.find((o) => !newItems.some((n) => isSame(n, o)));
|
|
||||||
|
|
||||||
if (added && items.some((it) => isSame(it, added))) {
|
|
||||||
selectTrack(added, 'add');
|
|
||||||
} else if (removed && items.some((it) => isSame(it, removed))) {
|
|
||||||
selectTrack(removed, 'remove');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Quand activeTracks change elsewhere -> synchroniser l'affichage local
|
|
||||||
// Mais n'adopter que les variations qui appartiennent à ce Selector (`items`)
|
|
||||||
watch(
|
|
||||||
activeTracks,
|
|
||||||
(newVal) => {
|
|
||||||
syncing.value = true;
|
|
||||||
|
|
||||||
const storeList = Array.isArray(newVal) ? newVal : [];
|
|
||||||
// ne garder que les variations du store qui sont dans `items`
|
|
||||||
const matched = storeList.filter((av) =>
|
|
||||||
items.some((it) => isSame(it, av))
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncCurrentValueFromStore(storeVariations) {
|
||||||
|
syncing.value = true;
|
||||||
|
|
||||||
|
const matchedVariations = findMatchingVariationsInStore(storeVariations);
|
||||||
|
|
||||||
if (isCompareModeEnabled) {
|
if (isCompareModeEnabled) {
|
||||||
currentValue.value = matched.length ? [...matched] : [];
|
currentValue.value = matchedVariations.length ? [...matchedVariations] : [];
|
||||||
} else {
|
} else {
|
||||||
currentValue.value = matched[0] || null;
|
currentValue.value = matchedVariations[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => (syncing.value = false));
|
nextTick(() => (syncing.value = false));
|
||||||
},
|
}
|
||||||
{ deep: true }
|
|
||||||
|
function detectVariationChanges(newValues, oldValues) {
|
||||||
|
const newList = Array.isArray(newValues)
|
||||||
|
? newValues
|
||||||
|
: newValues
|
||||||
|
? [newValues]
|
||||||
|
: [];
|
||||||
|
const oldList = Array.isArray(oldValues)
|
||||||
|
? oldValues
|
||||||
|
: oldValues
|
||||||
|
? [oldValues]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const addedVariation = newList.find(
|
||||||
|
(n) => !oldList.some((o) => areVariationsEqual(o, n))
|
||||||
|
);
|
||||||
|
const removedVariation = oldList.find(
|
||||||
|
(o) => !newList.some((n) => areVariationsEqual(n, o))
|
||||||
|
);
|
||||||
|
|
||||||
|
return { addedVariation, removedVariation };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVariationChange(newValue, oldValue) {
|
||||||
|
if (syncing.value) return;
|
||||||
|
|
||||||
|
const { addedVariation, removedVariation } = detectVariationChanges(
|
||||||
|
newValue,
|
||||||
|
oldValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
addedVariation &&
|
||||||
|
items.some((item) => areVariationsEqual(item, addedVariation))
|
||||||
|
) {
|
||||||
|
updateActiveTracks(addedVariation, 'add');
|
||||||
|
} else if (
|
||||||
|
removedVariation &&
|
||||||
|
items.some((item) => areVariationsEqual(item, removedVariation))
|
||||||
|
) {
|
||||||
|
updateActiveTracks(removedVariation, 'remove');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isCompareModeEnabled,
|
||||||
|
(shouldBeArray) => {
|
||||||
|
syncing.value = true;
|
||||||
|
currentValue.value = convertValueForCompareMode(
|
||||||
|
currentValue.value,
|
||||||
|
shouldBeArray
|
||||||
|
);
|
||||||
|
nextTick(() => (syncing.value = false));
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Logique centrale de sélection (ajout / suppression)
|
watch(currentValue, handleVariationChange, { deep: true });
|
||||||
// Règles :
|
|
||||||
// - mode normal -> activeTracks = [variation]
|
watch(
|
||||||
// - mode comparaison -> conserver activeTracks[0] si possible; second élément ajouté/remplacé; suppression gère le cas de la suppression de la première
|
activeTracks,
|
||||||
function selectTrack(track, action = 'add') {
|
(storeVariations) => {
|
||||||
const variation = toVariation(track);
|
const variationsList = Array.isArray(storeVariations)
|
||||||
|
? storeVariations
|
||||||
|
: [];
|
||||||
|
syncCurrentValueFromStore(variationsList);
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
function removeVariationFromActiveTracks(variation) {
|
||||||
|
activeTracks.value = activeTracks.value.filter(
|
||||||
|
(track) => !areVariationsEqual(track, variation)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addVariationToActiveTracks(variation) {
|
||||||
|
const isAlreadyPresent = activeTracks.value.some((track) =>
|
||||||
|
areVariationsEqual(track, variation)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAlreadyPresent) return;
|
||||||
|
|
||||||
|
if (activeTracks.value.length === 0) {
|
||||||
|
activeTracks.value = [variation];
|
||||||
|
} else if (activeTracks.value.length === 1) {
|
||||||
|
activeTracks.value = [activeTracks.value[0], variation];
|
||||||
|
} else {
|
||||||
|
activeTracks.value = [activeTracks.value[0], variation];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateActiveTracks(track, action = 'add') {
|
||||||
|
const variation = extractVariation(track);
|
||||||
if (!variation) return;
|
if (!variation) return;
|
||||||
|
|
||||||
if (!isCompareModeEnabled) {
|
if (!isCompareModeEnabled) {
|
||||||
|
|
@ -190,34 +227,12 @@ function selectTrack(track, action = 'add') {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'remove') {
|
if (action === 'remove') {
|
||||||
const wasFirst =
|
removeVariationFromActiveTracks(variation);
|
||||||
activeTracks.value.length && isSame(activeTracks.value[0], variation);
|
|
||||||
activeTracks.value = activeTracks.value.filter(
|
|
||||||
(t) => !isSame(t, variation)
|
|
||||||
);
|
|
||||||
|
|
||||||
// si on a retiré la première et qu'il reste une piste, elle devient naturellement index 0
|
|
||||||
// pas d'action supplémentaire nécessaire ici (déjà assuré par le filter)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// action === 'add'
|
|
||||||
if (activeTracks.value.some((t) => isSame(t, variation))) {
|
|
||||||
// déjà présent -> ignore
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTracks.value.length === 0) {
|
|
||||||
activeTracks.value = [variation];
|
|
||||||
} else if (activeTracks.value.length === 1) {
|
|
||||||
activeTracks.value = [activeTracks.value[0], variation];
|
|
||||||
} else {
|
} else {
|
||||||
// remplacer le 2e
|
addVariationToActiveTracks(variation);
|
||||||
activeTracks.value = [activeTracks.value[0], variation];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers pour affichage (inchangés)
|
|
||||||
function getFrontViewUrl(item) {
|
function getFrontViewUrl(item) {
|
||||||
if (!item) return '';
|
if (!item) return '';
|
||||||
if (Array.isArray(item)) {
|
if (Array.isArray(item)) {
|
||||||
|
|
@ -232,7 +247,7 @@ function getFrontViewUrl(item) {
|
||||||
|
|
||||||
function setImage() {
|
function setImage() {
|
||||||
return getFrontViewUrl(currentValue.value)
|
return getFrontViewUrl(currentValue.value)
|
||||||
? '--image: url(\'' + getFrontViewUrl(currentValue.value) + '\')'
|
? "--image: url('" + getFrontViewUrl(currentValue.value) + "')"
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -250,7 +265,8 @@ function setImage() {
|
||||||
padding: var(--space-8) var(--space-48) var(--space-8) var(--space-16);
|
padding: var(--space-8) var(--space-48) var(--space-8) var(--space-16);
|
||||||
}
|
}
|
||||||
.selector-dropdown.has-image,
|
.selector-dropdown.has-image,
|
||||||
.selector-dropdown.has-image :is(#selector-select, #selector-multiselect, [role='combobox']) {
|
.selector-dropdown.has-image
|
||||||
|
:is(#selector-select, #selector-multiselect, [role='combobox']) {
|
||||||
padding-left: var(--space-64);
|
padding-left: var(--space-64);
|
||||||
}
|
}
|
||||||
.selector-dropdown.has-image:before {
|
.selector-dropdown.has-image:before {
|
||||||
|
|
@ -290,7 +306,9 @@ function setImage() {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
[role='combobox'] p,
|
[role='combobox'] p,
|
||||||
.selector-dropdown [data-pc-section="labelcontainer"] > [data-pc-section='label'] {
|
.selector-dropdown
|
||||||
|
[data-pc-section='labelcontainer']
|
||||||
|
> [data-pc-section='label'] {
|
||||||
max-height: 1lh;
|
max-height: 1lh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,14 @@ import { storeToRefs } from 'pinia';
|
||||||
import { usePageStore } from '../../../stores/page';
|
import { usePageStore } from '../../../stores/page';
|
||||||
import { useDialogStore } from '../../../stores/dialog';
|
import { useDialogStore } from '../../../stores/dialog';
|
||||||
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import Interactive360 from './Interactive360.vue';
|
import Interactive360 from './Interactive360.vue';
|
||||||
import SingleImage from './SingleImage.vue';
|
import SingleImage from './SingleImage.vue';
|
||||||
import Selector from '../../Selector.vue';
|
import Selector from '../../Selector.vue';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { page } = storeToRefs(usePageStore());
|
const { page } = storeToRefs(usePageStore());
|
||||||
const { isCommentsOpen, isCommentPanelEnabled, activeTracks, openedFile } =
|
const { isCommentsOpen, isCommentPanelEnabled, activeTracks, openedFile } =
|
||||||
|
|
@ -92,51 +93,74 @@ const tracks = computed(() => {
|
||||||
return list;
|
return list;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- INITIALISATION ----------
|
function normalizeSlug(slug) {
|
||||||
// onBeforeMount : on initialise toujours activeTracks avec une VARIATION (jamais un track)
|
return slug.replace(/_/g, '-');
|
||||||
onBeforeMount(() => {
|
}
|
||||||
// essayer la hash en priorité
|
|
||||||
let initialVariation = null;
|
|
||||||
|
|
||||||
|
function getVariationSlug(variation) {
|
||||||
|
return variation.slug || (variation.title ? slugify(variation.title) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findVariationByHash(hashValue) {
|
||||||
|
const allVariations = tracks.value.flatMap((track) => track.variations || []);
|
||||||
|
const normalizedHash = normalizeSlug(hashValue);
|
||||||
|
|
||||||
|
return allVariations.find((variation) => {
|
||||||
|
const variationSlug = getVariationSlug(variation);
|
||||||
|
if (!variationSlug) return false;
|
||||||
|
|
||||||
|
const normalizedVariationSlug = normalizeSlug(variationSlug);
|
||||||
|
return normalizedVariationSlug === normalizedHash;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialVariation() {
|
||||||
if (route?.hash && route.hash.length > 0) {
|
if (route?.hash && route.hash.length > 0) {
|
||||||
const variations = tracks.value.flatMap((t) => t.variations || []);
|
|
||||||
const hashValue = route.hash.substring(1);
|
const hashValue = route.hash.substring(1);
|
||||||
|
const variationFromHash = findVariationByHash(hashValue);
|
||||||
// Essayer de trouver la variation soit par slug direct, soit en normalisant le hash
|
if (variationFromHash) return variationFromHash;
|
||||||
initialVariation = variations.find((v) => {
|
|
||||||
// Comparaison directe
|
|
||||||
if (v.slug === hashValue) return true;
|
|
||||||
// Comparaison en convertissant underscores en tirets (slugify par défaut)
|
|
||||||
if (v.slug === hashValue.replace(/_/g, '-')) return true;
|
|
||||||
// Comparaison inverse : le slug du backend pourrait avoir des underscores
|
|
||||||
if (v.slug.replace(/-/g, '_') === hashValue) return true;
|
|
||||||
return false;
|
|
||||||
}) || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback : première variation du premier track
|
return tracks.value[0]?.variations?.[0] || null;
|
||||||
if (!initialVariation) {
|
}
|
||||||
initialVariation = tracks.value[0]?.variations?.[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialVariation) {
|
function initializeActiveTracks() {
|
||||||
activeTracks.value = [initialVariation];
|
const initialVariation = getInitialVariation();
|
||||||
} else {
|
activeTracks.value = initialVariation ? [initialVariation] : [];
|
||||||
activeTracks.value = []; // aucun contenu disponible
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// scroll si hash présent
|
function normalizeUrlHash() {
|
||||||
onMounted(() => {
|
if (route?.hash && route.hash.includes('_')) {
|
||||||
if (route.query?.comments) isCommentsOpen.value = true;
|
const normalizedHash = normalizeSlug(route.hash);
|
||||||
|
router.replace({ ...route, hash: normalizedHash });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCommentsIfRequested() {
|
||||||
|
if (route.query?.comments) {
|
||||||
|
isCommentsOpen.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToHashTarget() {
|
||||||
if (!route?.hash || route.hash.length === 0) return;
|
if (!route?.hash || route.hash.length === 0) return;
|
||||||
|
|
||||||
const selector = route.hash.replace('#', '#track--');
|
const selectorId = route.hash.replace('#', '#track--');
|
||||||
const targetBtn = document.querySelector(selector);
|
const targetButton = document.querySelector(selectorId);
|
||||||
if (targetBtn) targetBtn.scrollIntoView();
|
if (targetButton) {
|
||||||
|
targetButton.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
initializeActiveTracks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- COMPUTED / WATCH ----------
|
onMounted(() => {
|
||||||
|
openCommentsIfRequested();
|
||||||
|
normalizeUrlHash();
|
||||||
|
scrollToHashTarget();
|
||||||
|
});
|
||||||
|
|
||||||
const isSingleImage = computed(() => {
|
const isSingleImage = computed(() => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -149,38 +173,52 @@ const singleFile = computed(() => {
|
||||||
return isSingleImage.value ? activeTracks.value[0].files[0] : null;
|
return isSingleImage.value ? activeTracks.value[0].files[0] : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
function updateOpenedFile(file) {
|
||||||
singleFile,
|
if (file) {
|
||||||
(newValue) => {
|
openedFile.value = file;
|
||||||
if (newValue) openedFile.value = newValue;
|
}
|
||||||
},
|
}
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// gestion du mode comparaison : fermer les commentaires, etc.
|
function enableCompareModeUI() {
|
||||||
watch(isCompareModeEnabled, (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
isCommentsOpen.value = false;
|
isCommentsOpen.value = false;
|
||||||
isCommentPanelEnabled.value = false;
|
isCommentPanelEnabled.value = false;
|
||||||
} else {
|
}
|
||||||
isCommentPanelEnabled.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// quand on quitte le mode comparaison on retire l'élément secondaire si nécessaire
|
function disableCompareModeUI() {
|
||||||
if (!newValue && activeTracks.value.length === 2) {
|
isCommentPanelEnabled.value = true;
|
||||||
|
|
||||||
|
if (activeTracks.value.length === 2) {
|
||||||
activeTracks.value.pop();
|
activeTracks.value.pop();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUrlHash(firstTrack) {
|
||||||
|
const trackSlug = getVariationSlug(firstTrack);
|
||||||
|
if (!trackSlug) return;
|
||||||
|
|
||||||
|
const currentHash = route.hash ? route.hash.substring(1) : '';
|
||||||
|
const normalizedTrackSlug = normalizeSlug(trackSlug);
|
||||||
|
|
||||||
|
if (currentHash !== normalizedTrackSlug) {
|
||||||
|
router.replace({ ...route, hash: '#' + normalizedTrackSlug });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(singleFile, updateOpenedFile, { immediate: true });
|
||||||
|
|
||||||
|
watch(isCompareModeEnabled, (isEnabled) => {
|
||||||
|
isEnabled ? enableCompareModeUI() : disableCompareModeUI();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- UTIL / helper ----------
|
watch(
|
||||||
function getCommentsCount(track) {
|
activeTracks,
|
||||||
if (!track || !Array.isArray(track.files)) return undefined;
|
(tracks) => {
|
||||||
let count = 0;
|
if (tracks && tracks.length > 0) {
|
||||||
for (const file of track.files) {
|
updateUrlHash(tracks[0]);
|
||||||
count += file?.comments?.length || 0;
|
|
||||||
}
|
}
|
||||||
return count > 0 ? count : undefined;
|
},
|
||||||
}
|
{ deep: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue