Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 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
37908b0
Add support for element call action
rkusan00 Feb 23, 2026
872b0a7
Merge branch 'main' into task/support-flat-structure
rkusan00 Mar 3, 2026
adb718f
Cleanup
rkusan00 Mar 4, 2026
4b371e3
Fix typo
rkusan00 Mar 9, 2026
03ba2cc
Merge branch 'main' into task/support-flat-structure
rkusan00 Mar 9, 2026
27f6aee
Merge branch 'main' into task/support-flat-structure
rkusan00 Apr 13, 2026
d243e7b
Merge branch 'main' into task/support-flat-structure
rkusan00 Apr 15, 2026
5e68df6
Rename call actions to procedures
rkusan00 Apr 15, 2026
3d6a923
Add max width
rkusan00 Apr 16, 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 @@ -19,7 +19,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 @@ -35,7 +35,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
17 changes: 15 additions & 2 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 @@ -469,6 +472,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
17 changes: 17 additions & 0 deletions apps/backend/content-element/content-element.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import pick from 'lodash/pick.js';

import { createError } from '#shared/error/helpers.js';
import db from '#shared/database/index.js';
import PluginRegistry from '#shared/content-plugins/index.js';

const { elementRegistry } = PluginRegistry;

const { NOT_FOUND } = StatusCodes;
const { Activity, ContentElement } = db;
Expand Down Expand Up @@ -62,6 +65,19 @@ async function reorder({ body, contentElement }, res) {
return res.json({ data: contentElement });
}

async function rpc({ contentElement, user, repository, body, params }, res) {
const { procedure } = params;
const { type } = contentElement;
const handler = elementRegistry.getProcedure(type, procedure);
if (!handler) {
const error = `Procedure "${procedure}" not found for element type "${type}"`;
return res.status(StatusCodes.NOT_FOUND).json({ error });
}
const context = { userId: user.id, repository };
const result = await handler(contentElement, body, { context });
return res.json({ data: result });
}

/**
* Link element from another repository into this repository.
* Creates a linked copy that receives auto-sync updates from source.
Expand Down Expand Up @@ -131,6 +147,7 @@ export default {
patch,
remove,
reorder,
rpc,
link,
unlink,
getSource,
Expand Down
1 change: 1 addition & 0 deletions apps/backend/content-element/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ router.post('/:elementId/reorder', ctrl.reorder);
router.post('/:elementId/unlink', ctrl.unlink);
router.get('/:elementId/source', ctrl.getSource);
router.get('/:elementId/copies', ctrl.getCopies);
router.post('/:elementId/rpc/:procedure', ctrl.rpc);

function getContentElement(req, _res, next, elementId) {
if (!Number.isInteger(Number(elementId))) {
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
12 changes: 12 additions & 0 deletions apps/backend/shared/content-plugins/elementRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class ElementsRegistry {
constructor() {
this._registry = elements;
this._hooks = {};
this._procedures = {};
this._aiSchemas = {};
}

Expand All @@ -23,6 +24,7 @@ class ElementsRegistry {
Object.assign(this._aiSchemas, {
[it.type]: it.ai,
});
if (it.procedures) this._procedures[it.type] = it.procedures;
});
}

Expand All @@ -43,6 +45,16 @@ class ElementsRegistry {
);
};
}

getProcedure(elementType, procedure) {
const handlers = this._procedures[elementType];
if (!handlers || !handlers[procedure]) return;
const services = { config: pick(config, ['tce']), storage };
return (element, payload, options) => {
const context = options?.context || {};
return handlers[procedure](element, { ...services, context }, payload);
};
}
}

export default new ElementsRegistry();
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
9 changes: 9 additions & 0 deletions apps/frontend/app/api/contentElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const urls = {
root: (repositoryId) => `${urls.repository(repositoryId)}/content-elements`,
resource: (repositoryId, id) => `${urls.root(repositoryId)}/${id}`,
reorder: (repositoryId, id) => `${urls.resource(repositoryId, id)}/reorder`,
rpc: (repositoryId, id, procedure) =>
`${urls.resource(repositoryId, id)}/rpc/${procedure}`,
link: (repositoryId) => `${urls.root(repositoryId)}/link`,
unlink: (repositoryId, id) => `${urls.resource(repositoryId, id)}/unlink`,
source: (repositoryId, id) => `${urls.resource(repositoryId, id)}/source`,
Expand Down Expand Up @@ -34,6 +36,12 @@ function remove(repositoryId, id) {
return request.delete(urls.resource(repositoryId, id));
}

function rpc(repositoryId, id, procedure, payload = {}) {
return request
.post(urls.rpc(repositoryId, id, procedure), payload)
.then(extractData);
}

function link(repositoryId, payload) {
return request.post(urls.link(repositoryId), payload).then(extractData);
}
Expand All @@ -56,6 +64,7 @@ export default {
patch,
reorder,
remove,
rpc,
link,
unlink,
getSource,
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 @@ -38,6 +38,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 @@ -158,6 +159,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
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ const createActivity = async (payload: any) => {
provide('$editorBus', editorChannel);
provide('$eventBus', $eventBus);
provide('$storageService', storageService);
provide('$rpc', contentElementStore.rpc);
if (config.props.aiUiEnabled) {
provide('$doTheMagic', doTheMagic);
provide('$createActivity', createActivity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,31 @@ import { schema } from '@tailor-cms/config';

import ElementMeta from './ElementMeta/index.vue';
import { exposedApi } from '@/api';
import { useContentElementStore } from '@/stores/content-elements';

const eventBus = inject('$eventBus') as any;
const authStore = useAuthStore();
const { rpc } = useContentElementStore();
const storageService = useStorageService();

interface Props {
element: ContentElement;
metadata?: any;
}

const props = withDefaults(defineProps<Props>(), {
metadata: () => ({}),
});
const { repositoryId, id: elementId } = props.element;

const elementBus = eventBus.channel(`element:${getElementId(props.element)}`);
const editorChannel = eventBus.channel('editor');
provide('$elementBus', elementBus);
provide('$editorBus', editorChannel);
provide('$storageService', storageService);
provide('$rpc', (procedure: string, payload?: any) =>
rpc(repositoryId, elementId, procedure, payload),
);
provide('$api', exposedApi);
provide('$schemaService', schema);
provide('$getCurrentUser', () => authStore.user);
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 @@ -79,6 +79,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 modelValue = defineModel<boolean>({ required: true });

Expand All @@ -95,14 +96,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 @@ -142,7 +143,7 @@ watch(
return;
}
if (selectedTab.value !== ELEMENT_TAB) return;
selectedTab.value = BROWSER_TAB;
selectedTab.value = defaultTab;
},
);

Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/app/components/editor/Toolbar/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ const usersWithActivity = computed(() => {
position: absolute;
padding: 0 !important;

.v-messages {
.v-messages__message {
margin-top: 0.5rem;
border-radius: 4px;
padding: 0.5rem 0.75rem;
Expand Down
Loading
Loading