Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cms/src/components/content/ContentDisplayCard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe("ContentDisplayCard", () => {
parentType: mockData.mockEnglishContentDto.parentType as DocType.Post | DocType.Tag,
languageId: engNonDefault._id,
languages: [engNonDefault, mockData.mockLanguageDtoFra, swaDefault],
translations: [mockData.mockEnglishContentDto],
},
});

Expand Down
35 changes: 14 additions & 21 deletions cms/src/components/content/ContentDisplayCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
AclPermission,
verifyAccess,
type GroupDto,
useHybridQueryWithState,
useHasLocalChanges,
} from "luminary-shared";
import { computed, ref, watch } from "vue";
import LBadge from "../common/LBadge.vue";
Expand All @@ -36,6 +36,12 @@ type Props = {
* suppress it. (Related/fuzzy search leaves it on.)
*/
hideBodySnippet?: boolean;
/**
* Every translation of this card's parent, supplied by the list. Sourced once for
* the whole list rather than per card: an unbounded per-parent Content query always
* hits the API supplement, so a per-card one costs one request per rendered row.
*/
translations: ContentDto[];
};

const props = defineProps<Props>();
Expand All @@ -46,19 +52,7 @@ const highlight = computed(() =>
: undefined,
);

// All translations of this card's parent (no language filter), Dexie-first via HybridQuery. The
// top-level `type` is required — without it HybridQuery.readType returns undefined and routes
// API-only. `parentId` alone scopes to the parent (parentType is redundant given the unique
// parentId), and `{ type, parentId }` matches the `[type+parentId]` index — no full-table-scan warning.
const { output: contentDocs, hasLocalChanges } = useHybridQueryWithState<ContentDto>(
() => ({
selector: {
type: DocType.Content,
parentId: props.contentDoc.parentId,
},
}),
{ live: true },
);
const hasLocalChanges = useHasLocalChanges();
const isLocalChange = computed(() => hasLocalChanges.value(props.contentDoc._id));

const tagsContent = ref<ContentDto[]>([]);
Expand All @@ -70,11 +64,10 @@ const accessibleLanguages = computed(() =>
);

watch(
contentDocs,
() => [props.contentDoc.parentTags, props.languageId] as const,
async () => {
if (!contentDocs.value || contentDocs.value.length === 0) return;
tagsContent.value = await db.whereParent(
contentDocs.value[0].parentTags,
props.contentDoc.parentTags,
DocType.Tag,
props.languageId,
);
Expand Down Expand Up @@ -152,10 +145,10 @@ const navigateTo = computed(() => {
<LBadge
type="language"
withIcon
:variant="translationStatus(contentDocs, language)"
:variant="translationStatus(translations, language)"
:class="{
'z-20 cursor-pointer hover:opacity-65':
translationStatus(contentDocs, language) !== 'default',
translationStatus(translations, language) !== 'default',
}"
>
{{ language.languageCode }}
Expand All @@ -173,10 +166,10 @@ const navigateTo = computed(() => {
<LBadge
type="language"
withIcon
:variant="translationStatus(contentDocs, language)"
:variant="translationStatus(translations, language)"
:class="{
'cursor-pointer hover:opacity-65':
translationStatus(contentDocs, language) !== 'default',
translationStatus(translations, language) !== 'default',
}"
>
{{ language.languageCode }}
Expand Down
28 changes: 28 additions & 0 deletions cms/src/components/content/ContentOverview/ContentOverview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,33 @@ const search = useContentSearchQuery(
const searchIsStale = search.isStale;

const contentDocs = computed(() => (searchActive.value ? search.docs.value : browse.docs.value));

// Every rendered row needs its parent's other translations for the language badges. An
// unbounded per-parent Content query always hits the API supplement, so asking per card
// costs one request per row; one `$in` query for the whole page collapses that (and the
// per-row Dexie subscriptions) into a single source.
const rowTranslations = useHybridQuery<ContentDto>(
() => ({
selector: {
type: DocType.Content,
parentId: { $in: [...new Set(contentDocs.value.map((d) => d.parentId))] },
},
}),
{
live: true,
keepPreviousResult: true,
stripFields: ["fts", "ftsTokenCount", "text", "_rev"],
},
);
const translationsByParent = computed(() => {
const byParent = new Map<string, ContentDto[]>();
for (const doc of rowTranslations.value) {
const list = byParent.get(doc.parentId);
if (list) list.push(doc);
else byParent.set(doc.parentId, [doc]);
}
return byParent;
});
const isLoading = computed(() =>
searchActive.value ? search.isLoading.value : browse.isLoading.value,
);
Expand Down Expand Up @@ -284,6 +311,7 @@ const createNew = () => {
:key="contentDoc._id"
:groups="groups.filter((group) => contentDoc.memberOf?.includes(group._id))"
:content-doc="contentDoc as ContentDto"
:translations="translationsByParent.get(contentDoc.parentId) ?? []"
:parent-type="queryOptions.parentType"
:language-id="queryOptions.languageId"
:languages="languages"
Expand Down
Loading
Loading