Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2c8820b
Introduce flat outline view
rkusan00 Nov 11, 2024
9b045b8
Add type filter
rkusan00 Nov 11, 2024
ab5f2c5
Fix filter
rkusan00 Nov 11, 2024
60dc485
Fix lint error
rkusan00 Nov 11, 2024
1a20465
Refactor layout
rkusan00 Nov 11, 2024
1c8889f
Cleanup
rkusan00 Nov 11, 2024
832581a
Hide selector if only one is available
rkusan00 Nov 11, 2024
c86ef76
Reduce height
rkusan00 Nov 11, 2024
b1ff1c0
Update outline style naming
rkusan00 Nov 12, 2024
61a5e50
Merge branch 'main' into task/support-flat-structure
underscope Jun 23, 2025
c7bc98f
🐐
underscope Jun 23, 2025
1e17dc1
Init Collection creation lib
underscope Jul 4, 2025
9f6f321
Improve TS support
underscope Jul 4, 2025
3566965
Create example collection
underscope Jul 4, 2025
5dd5bcb
Init collection item container
underscope Jul 8, 2025
33cb70c
Install collection item container
underscope Jul 8, 2025
8a96329
Add enum generation for installed extension types
underscope Jul 8, 2025
92620c7
Update schema
underscope Jul 8, 2025
0646359
Expose update container event
underscope Jul 8, 2025
38e9ec7
Merge branch 'main' into task/support-flat-structure
underscope Jul 10, 2025
533390d
Target only first level nodes
underscope Jul 11, 2025
0056d39
Generate container type enum
underscope Jul 11, 2025
c334c6e
💄
underscope Jul 11, 2025
7031d49
🔧
underscope Jul 11, 2025
cef8b56
Resolve nested elements
underscope Jul 15, 2025
dd4d14d
Merge branch 'main' into task/support-flat-structure
rkusan00 Dec 8, 2025
6fb654f
Fix lockfile
rkusan00 Dec 8, 2025
f85b66a
Fix config
rkusan00 Dec 10, 2025
b63477d
Remove grid for course
rkusan00 Dec 10, 2025
4fc8a0c
Update html type and description
rkusan00 Dec 10, 2025
1b0b66c
Update styles to match
rkusan00 Dec 10, 2025
c8123e9
Cleanup
rkusan00 Dec 10, 2025
747cba5
Add collection item validation
rkusan00 Dec 15, 2025
c69d183
Cleanup
rkusan00 Jan 7, 2026
53b0bbe
Tweak validation trigger
rkusan00 Jan 8, 2026
465e0a6
Merge branch 'main' into task/support-flat-structure
rkusan00 Jan 20, 2026
9136b21
Merge branch 'main' into task/support-flat-structure
rkusan00 Jan 22, 2026
58bc304
Cleanup
rkusan00 Jan 22, 2026
35dfe64
Merge branch 'main' into task/support-flat-structure
rkusan00 Feb 6, 2026
cbfdc63
Cleanup config and validation
rkusan00 Feb 6, 2026
ed9a1db
Cleanup
rkusan00 Feb 6, 2026
8aa98a0
Add flat collection server
rkusan00 Feb 10, 2026
e712ca1
Cleanup
rkusan00 Feb 10, 2026
d6844ef
Replace outlineStyle with isCollection
rkusan00 Feb 11, 2026
043e2bd
Refactor collection display
rkusan00 Feb 11, 2026
6954dfb
Cleanup
rkusan00 Feb 11, 2026
d91cedf
Cleanup
rkusan00 Feb 12, 2026
0404e1c
Update file upload meta
rkusan00 Feb 12, 2026
ee4b609
Cleanup
rkusan00 Feb 12, 2026
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
6 changes: 4 additions & 2 deletions apps/backend/activity/activity.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
22 changes: 19 additions & 3 deletions apps/backend/activity/activity.model.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -228,7 +231,10 @@ class Activity extends Model {

static detectMissingReferences(activities, transaction) {
return detectMissingReferences(
Activity, activities, this.sequelize, transaction,
Activity,
activities,
this.sequelize,
transaction,
);
}

Expand Down Expand Up @@ -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 });
}
Expand Down
6 changes: 3 additions & 3 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 18 additions & 13 deletions apps/backend/shared/storage/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/app/components/common/AppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
Expand Down Expand Up @@ -150,6 +151,15 @@ const addContainer = async (data: Record<string, any> = {}) => {
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) =>
Expand Down
15 changes: 8 additions & 7 deletions apps/frontend/app/components/editor/Sidebar/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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',
Expand Down Expand Up @@ -130,7 +131,7 @@ watch(
return;
}
if (selectedTab.value !== ELEMENT_TAB) return;
selectedTab.value = BROWSER_TAB;
selectedTab.value = defaultTab;
},
);

Expand Down
167 changes: 167 additions & 0 deletions apps/frontend/app/components/repository/Outline/CollectionTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<template>
<VDataTable
:headers="headers"
:items="activities"
:items-per-page="25"
:sort-by="[{ key: 'createdAt', order: 'desc' }]"
:row-props="getRowProps"
class="collection-table bg-primary-darken-2 rounded-lg text-left"
item-value="id"
fixed-header
@click:row="selectRow"
>
<template #[`item.data.name`]="{ item }">
{{ item.data.name }}
</template>
<template #[`item.createdAt`]="{ item }">
{{ formatDate(item.createdAt, 'MM/dd/yy HH:mm') }}
</template>
<template #[`item.updatedAt`]="{ item }">
{{ formatDate(item.updatedAt, 'MM/dd/yy HH:mm') }}
</template>
<template #[`item.actions`]="{ item }">
<VChip v-if="isSoftDeleted(item)" size="small">
<span class="mr-1 font-weight-bold">Deleted:</span>
Publish required
<VIcon
v-tooltip:bottom="'Will be removed upon publishing'"
class="ml-2"
icon="mdi-information-outline"
/>
</VChip>
<template v-else>
<VBtn
v-tooltip:bottom="'Open'"
class="mr-2"
color="teal-lighten-4"
prepend-icon="mdi-page-next-outline"
size="small"
variant="tonal"
text="Open"
@click.stop="openActivity(item)"
/>
<VBtn
v-tooltip:bottom="'Delete'"
color="secondary-lighten-3"
density="comfortable"
icon="mdi-trash-can-outline"
size="small"
variant="text"
@click.stop="deleteActivity(item)"
/>
</template>
</template>
<template #no-data>
<VAlert
class="my-4"
color="primary-lighten-3"
icon="mdi-magnify"
variant="tonal"
prominent
>
No matches found!
</VAlert>
</template>
</VDataTable>
</template>

<script lang="ts" setup>
import { activity as activityUtils } from '@tailor-cms/utils';
import { formatDate } from 'date-fns/format';
import { first, sortBy } from 'lodash-es';

import type { StoreActivity } from '@/stores/activity';
import { useConfirmationDialog } from '@/composables/useConfirmationDialog';
import { useCurrentRepository } from '@/stores/current-repository';

defineProps<{
activities: StoreActivity[];
}>();

const repositoryStore = useCurrentRepository();
const activityStore = useActivityStore();
const showConfirmationDialog = useConfirmationDialog();

const headers = [
{ title: 'Name', value: 'data.name', sortable: true },
{ title: 'Date Created', value: 'createdAt', sortable: true, width: '10rem' },
{ value: 'actions', sortable: false, align: 'end' as const, width: '10rem' },
];

const { doesRequirePublishing } = activityUtils;

const isSoftDeleted = (activity: StoreActivity) =>
doesRequirePublishing(activity);

const getRowProps = ({ item }: { item: StoreActivity }) => {
const classes = [];
if (isSoftDeleted(item)) classes.push('soft-deleted');
if (repositoryStore.selectedActivity?.id === item.id) classes.push('selected');
return { class: classes.join(' ') };
};

const selectRow = (_event: Event, { item }: any) => {
repositoryStore.selectActivity(item.id);
};

const openActivity = (activity: StoreActivity) => {
const { id: activityId, repositoryId } = activity;
navigateTo({ name: 'editor', params: { id: repositoryId, activityId } });
};

const deleteActivity = (activity: StoreActivity) =>
showConfirmationDialog({
title: 'Delete item?',
message: `Are you sure you want to delete ${activity.data.name}?`,
action: () => {
activityStore.remove(activity.id);
const focusNode = first(sortBy(repositoryStore.rootActivities, 'position'));
if (focusNode) repositoryStore.selectActivity(focusNode.id);
},
});
</script>

<style lang="scss" scoped>
.collection-table {
display: flex;
flex-direction: column;
overflow: hidden;

:deep(.v-table__wrapper) {
flex: 1 1 auto;
}

line-height: 1.25;

:deep(td) {
padding: 0.5rem 1rem !important;
}

:deep(tbody) tr {
transition: background-color 0.3s ease;

&.soft-deleted {
background: rgba(var(--v-theme-secondary-darken-1), 0.1) !important;

&.selected,
&:hover {
background: rgba(var(--v-theme-secondary-lighten-2), 0.2) !important;
}
}

&.selected,
&:hover {
background: rgba(var(--v-theme-primary-darken-1)) !important;
}
}

:deep(th) {
background: rgba(var(--v-theme-primary-darken-2)) !important;
color: white !important;

&:hover {
font-weight: bold;
}
}
}
</style>
Loading
Loading