diff --git a/apps/backend/activity/activity.controller.js b/apps/backend/activity/activity.controller.js index efb593fc9..45f99d37f 100644 --- a/apps/backend/activity/activity.controller.js +++ b/apps/backend/activity/activity.controller.js @@ -18,7 +18,7 @@ const { getOutlineLevels, isOutlineActivity } = schema; const logger = createLogger('activity:controller'); const log = (msg) => logger.info(msg.replace(/\n/g, ' ')); -function list({ repository, query, opts }, res) { +async function list({ repository, query, opts }, res) { if (!query.detached) opts.where.detached = false; if (query.outlineOnly) { // Include deleted if published and deletion is not published yet @@ -34,7 +34,9 @@ function list({ repository, query, opts }, res) { }, ]; } - return repository.getActivities(opts).then((data) => res.json({ data })); + const activities = await repository.getActivities(opts); + await Promise.all(activities.map((it) => it.processEmbeddedElements())); + return res.json({ data: activities }); } function create({ user, repository, body }, res) { diff --git a/apps/backend/activity/activity.model.js b/apps/backend/activity/activity.model.js index 9cf08cc99..cbb6f1b37 100644 --- a/apps/backend/activity/activity.model.js +++ b/apps/backend/activity/activity.model.js @@ -1,12 +1,15 @@ import { Model, Op } from 'sequelize'; import { schema, workflow } from '@tailor-cms/config'; import { Activity as Events } from '@tailor-cms/common/src/sse.js'; +import { ContentContainerType } from '@tailor-cms/content-container-collection/types.js'; +import calculatePosition from '#shared/util/calculatePosition.js'; +import contentElementHooks from '../content-element/hooks.js'; +import hooks from './hooks.js'; import isEmpty from 'lodash/isEmpty.js'; import map from 'lodash/map.js'; import pick from 'lodash/pick.js'; import Promise from 'bluebird'; -import hooks from './hooks.js'; -import calculatePosition from '#shared/util/calculatePosition.js'; + import { detectMissingReferences, removeReference, @@ -228,7 +231,10 @@ class Activity extends Model { static detectMissingReferences(activities, transaction) { return detectMissingReferences( - Activity, activities, this.sequelize, transaction, + Activity, + activities, + this.sequelize, + transaction, ); } @@ -348,6 +354,16 @@ class Activity extends Model { }); } + async processEmbeddedElements() { + if (this.type !== ContentContainerType.CollectionItemContent) + return Promise.resolve(this); + for (const key of Object.keys(this.data)) { + if (!this.data?.[key]?.embedded) return; + contentElementHooks.applyFetchHooks(this.data[key]); + } + return this; + } + touch(transaction) { return this.update({ modifiedAt: new Date() }, { transaction }); } diff --git a/apps/backend/package.json b/apps/backend/package.json index a0282645f..aac04c4c1 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -13,9 +13,9 @@ "#storage": "./repository/storage.js" }, "scripts": { - "dev": "node --watch --import ./script/preflight.js --experimental-strip-types ./index.ts || exit 0", - "start": "node --import ./script/preflight.js --experimental-strip-types ./index.ts", - "start:docker": "node --experimental-strip-types ./index.ts", + "dev": "node --watch --import ./script/preflight.js --experimental-transform-types ./index.ts || exit 0", + "start": "node --import ./script/preflight.js --experimental-transform-types ./index.ts", + "start:docker": "node --experimental-transform-types ./index.ts", "db": "node --import ./script/preflight.js ./script/sequelize.js", "db:reset": "pnpm db drop && pnpm db create && pnpm db migrate", "db:seed": "pnpm db seed:all", diff --git a/apps/backend/shared/storage/helpers.js b/apps/backend/shared/storage/helpers.js index 1c5553e76..1ac58f38a 100644 --- a/apps/backend/shared/storage/helpers.js +++ b/apps/backend/shared/storage/helpers.js @@ -12,6 +12,7 @@ const isLegacyQuestion = (element) => !isComposite(element) && !!get(element, 'data.question'); const isStorageAsset = (v) => isString(v) && v.startsWith(config.protocol); const extractStorageKey = (v) => v.substr(config.protocol.length, v.length); +const isContentElement = (v) => !!get(v, 'data'); async function resolveAssetsMap(element) { if (!get(element, 'data.assets')) return element; @@ -25,24 +26,28 @@ async function resolveAssetsMap(element) { return element; } +/** + * Resolves a single meta value with storage URL - mutates in place and returns value + */ +async function resolveMeta(element) { + const url = get(element, 'url'); + if (url && isStorageAsset(url)) { + element.publicUrl = await storage.getFileUrl(extractStorageKey(url)); + } + return element; +} + async function resolveMetaMap(element) { const meta = Object.values(element.meta || {}); - await Promise.all( - meta.map(async (value) => { - const url = get(value, 'url'); - if (!url || !isStorageAsset(url)) return Promise.resolve(); - value.publicUrl = await storage.getFileUrl(extractStorageKey(url)); - }), - ); + await Promise.all(meta.map(resolveMeta)); return element; } -function resolveStatics(element) { - return isComposite(element) - ? resolveComposite(element) - : isLegacyQuestion(element) - ? resolveLegacyQuestion(element) - : resolvePrimitive(element); +async function resolveStatics(element) { + if (!isContentElement(element)) return resolveMeta(element); + if (isComposite(element)) return resolveComposite(element); + if (isLegacyQuestion(element)) return resolveLegacyQuestion(element); + return resolvePrimitive(element); } async function resolvePrimitive(primitive) { diff --git a/apps/frontend/app/components/common/AppBar.vue b/apps/frontend/app/components/common/AppBar.vue index d11a16bca..d841a7425 100644 --- a/apps/frontend/app/components/common/AppBar.vue +++ b/apps/frontend/app/components/common/AppBar.vue @@ -154,7 +154,7 @@ const repositoryStore = useRepositoryStore(); const currentRepositoryStore = useCurrentRepository(); const route = useRoute(); -const { repository } = storeToRefs(currentRepositoryStore); +const { repository, isCollection } = storeToRefs(currentRepositoryStore); const routes = computed(() => { const items = [ @@ -170,7 +170,7 @@ const routes = computed(() => { if (!authStore.hasAdminAccess) items.pop(); if (repository.value) { items.unshift({ - name: `${repository.value.name} structure`, + name: `${repository.value.name} ${isCollection.value ? 'items' : 'structure'}`, to: `/repository/${repository.value?.id}/root/structure`, icon: 'mdi-file-tree-outline', }); diff --git a/apps/frontend/app/components/editor/ActivityContent/ContainerList.vue b/apps/frontend/app/components/editor/ActivityContent/ContainerList.vue index a025c8428..ad13f96d5 100644 --- a/apps/frontend/app/components/editor/ActivityContent/ContainerList.vue +++ b/apps/frontend/app/components/editor/ActivityContent/ContainerList.vue @@ -39,6 +39,7 @@ @delete:subcontainer="requestContainerDeletion" @reorder:element="reorderContentElements" @save:element="saveContentElements" + @update:container="updateContainer" @update:element="(val: any) => saveContentElements([val])" @update:subcontainer="activityStore.update" /> @@ -150,6 +151,15 @@ const addContainer = async (data: Record = {}) => { emit('createdContainer', payload); }; +const updateContainer = async (container: any) => { + try { + await activityStore.update(container); + showNotification(`${capitalize(name.value)} saved`); + } catch { + showNotification(`Failed to save ${name.value}`); + } +}; + const saveContentElements = (elements: ContentElement[]) => { const contentElements = castArray(elements); return BBPromise.map(contentElements, (element) => diff --git a/apps/frontend/app/components/editor/Sidebar/index.vue b/apps/frontend/app/components/editor/Sidebar/index.vue index bb3d0871d..b870dbe67 100644 --- a/apps/frontend/app/components/editor/Sidebar/index.vue +++ b/apps/frontend/app/components/editor/Sidebar/index.vue @@ -69,6 +69,7 @@ import { useDisplay } from 'vuetify'; import ActivityNavigation from './ActivityNavigation.vue'; import ElementSidebar from './ElementSidebar/index.vue'; import ActivityDiscussion from '@/components/repository/Discussion/index.vue'; +import { useCurrentRepository } from '@/stores/current-repository'; const props = defineProps<{ repository: Repository; @@ -83,14 +84,14 @@ const ELEMENT_TAB = 'ELEMENT_TAB'; const { $ceRegistry, $schemaService } = useNuxtApp() as any; const { lgAndUp } = useDisplay(); +const { isCollection } = storeToRefs(useCurrentRepository()); -const selectedTab = ref(BROWSER_TAB); +const defaultTab = isCollection.value ? COMMENTS_TAB : BROWSER_TAB; +const selectedTab = ref(defaultTab); const tabs: any = computed(() => [ - { - name: BROWSER_TAB, - label: 'Browse', - icon: 'file-tree', - }, + ...(!isCollection.value + ? [{ name: BROWSER_TAB, label: 'Browse', icon: 'file-tree' }] + : []), { name: COMMENTS_TAB, label: 'Comments', @@ -130,7 +131,7 @@ watch( return; } if (selectedTab.value !== ELEMENT_TAB) return; - selectedTab.value = BROWSER_TAB; + selectedTab.value = defaultTab; }, ); diff --git a/apps/frontend/app/components/repository/Outline/CollectionTable.vue b/apps/frontend/app/components/repository/Outline/CollectionTable.vue new file mode 100644 index 000000000..218a8ec8e --- /dev/null +++ b/apps/frontend/app/components/repository/Outline/CollectionTable.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/apps/frontend/app/components/repository/Outline/CreateDialog/index.vue b/apps/frontend/app/components/repository/Outline/CreateDialog/index.vue index ed389f08d..471ee6700 100644 --- a/apps/frontend/app/components/repository/Outline/CreateDialog/index.vue +++ b/apps/frontend/app/components/repository/Outline/CreateDialog/index.vue @@ -7,11 +7,12 @@ >