Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1ecf738
Add outline processor for discovery
underscope Mar 27, 2026
972f7cb
Add filtering type
underscope Mar 27, 2026
fe07d4c
Emit search input
underscope Mar 27, 2026
12347d8
Init Topic picker
underscope Mar 27, 2026
3b02082
Add outline based discovery
underscope Mar 27, 2026
eaf411b
Add outline processor for discovery
underscope Mar 27, 2026
9d53106
Add filtering type
underscope Mar 27, 2026
979b8e0
Emit search input
underscope Mar 27, 2026
df17615
Init Topic picker
underscope Mar 27, 2026
f3bc112
Add outline based discovery
underscope Mar 27, 2026
16392b8
Merge branch 'feature/contextual-discovery' of https://github.com/tai…
underscope Apr 7, 2026
44ca48e
🧹
underscope Apr 7, 2026
43b6364
💄
underscope Apr 7, 2026
678abb9
💄
underscope Apr 7, 2026
07a47c7
Merge branch 'feature/asset-lib' into feature/contextual-discovery
underscope Apr 7, 2026
b60e807
Merge branch 'feature/asset-lib' into feature/contextual-discovery
underscope Apr 7, 2026
56f55cd
Merge branch 'feature/asset-lib' into feature/contextual-discovery
underscope Apr 8, 2026
8274295
Merge branch 'feature/asset-lib' into feature/contextual-discovery
underscope Apr 9, 2026
f0b9ead
Merge branch 'feature/asset-lib' into feature/contextual-discovery
underscope Apr 9, 2026
7b47366
Merge branch 'feature/asset-lib' into feature/contextual-discovery
underscope Apr 13, 2026
aba427a
💄
underscope Apr 13, 2026
107dc84
💄
underscope Apr 15, 2026
dad7320
Merge branch 'main' into feature/contextual-discovery
underscope Apr 15, 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
hide-details
@click:clear="query = ''"
@keyup.enter="emit('search')"
@update:model-value="emit('search:input')"
/>
<VBtn
:loading="isSearching"
Expand Down Expand Up @@ -50,7 +51,10 @@ defineProps<{
isSearching: boolean;
}>();

const emit = defineEmits<{ search: [] }>();
const emit = defineEmits<{
'search': [];
'search:input': [];
}>();

const query = defineModel<string>('query', { required: true });
const contentFilter = defineModel<ContentFilter>('contentFilter', { required: true });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div v-if="isSearching" class="d-flex justify-center pa-12">
<div v-if="isSearching" class="d-flex flex-column align-center pa-12 ga-4">
<span class="text-body-1 text-primary-lighten-3">
Searching the web for relevant resources...
</span>
Expand All @@ -13,27 +13,30 @@
variant="tonal"
>
{{ suggestions.length }} results
<template v-if="selectedUrls.size">
<VIcon size="x-small">mdi-circle-small</VIcon>
{{ selectedUrls.size }} selected
</template>
</VChip>
<VSpacer />
<VBtn
v-if="selectedUrls.size"
color="primary-lighten-3"
prepend-icon="mdi-close-circle-outline"
prepend-icon="mdi-checkbox-multiple-outline"
size="small"
variant="text"
@click="emit('toggle-all', false)"
@click="emit('select:all')"
>
Deselect all
Select all
</VBtn>
<VBtn
v-else
v-if="selectedUrls.size"
color="primary-lighten-3"
prepend-icon="mdi-checkbox-multiple-outline"
prepend-icon="mdi-close-circle-outline"
size="small"
variant="text"
@click="emit('toggle-all', true)"
@click="emit('select:clear')"
>
Select all
Clear
</VBtn>
</div>
<div class="d-flex flex-column ga-2">
Expand All @@ -42,7 +45,7 @@
:key="it.url"
:is-selected="selectedUrls.has(it.url)"
:suggestion="it"
@toggle="emit('toggle', it.url)"
@toggle="emit('result:toggle', it.url)"
/>
</div>
<div v-if="totalPages > 1" class="d-flex justify-center mt-4">
Expand Down Expand Up @@ -85,8 +88,10 @@ const props = defineProps<{
const page = defineModel<number>('page', { required: true });

const emit = defineEmits<{
'toggle': [url: string];
'toggle-all': [selected: boolean];
'result:toggle': [url: string];
'select:all': [];
'select:clear': [];
'search:cancel': [];
}>();

const totalPages = computed(() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
Comment thread
rkusan00 marked this conversation as resolved.
<VList class="topic-list pa-2" density="compact">
<template v-for="item in items" :key="item.id">
<VListSubheader
v-if="item.isGroup"
class="text-uppercase font-weight-bold"
>
{{ item.name }}
</VListSubheader>
<VListItem
v-else
:prepend-icon="item.isLeaf
? 'mdi-file-document-outline'
: 'mdi-folder-outline'"
:value="item.id"
class="topic-list-item"
@click="emit('topic:select', item)"
>
<VListItemTitle class="text-body-2">
{{ item.name }}
</VListItemTitle>
<VListItemSubtitle v-if="item.parentName" class="text-caption">
{{ item.parentName }}
</VListItemSubtitle>
</VListItem>
</template>
</VList>
</template>

<script lang="ts" setup>
import type { TopicItem } from './types';

defineProps<{
items: TopicItem[];
}>();

const emit = defineEmits<{
'topic:select': [topic: TopicItem];
}>();
</script>

<style lang="scss" scoped>
.topic-list {
overflow-y: auto;
}

.topic-list-item {
cursor: pointer;

&:hover {
background: rgba(0, 0, 0, 0.04);
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<template>
<VAutocomplete
:items="items"
:model-value="modelValue?.id ?? null"
color="primary-lighten-3"
density="default"
item-title="name"
item-value="id"
label="Outline topic"
min-width="550"
max-width="550"
placeholder="Browse topics..."
prepend-inner-icon="mdi-file-tree-outline"
variant="outlined"
clearable
hide-details
@update:model-value="onSelect"
>
<template #item="{ item, props: itemProps }">
<VListItem v-bind="itemProps" color="primary-darken-4">
<template #prepend>
<VIcon
:icon="item.raw.isLeaf
? 'mdi-file-document-outline'
: 'mdi-folder-outline'"
size="small"
/>
</template>
<VListItemSubtitle v-if="item.raw.breadcrumb" class="text-caption">
{{ item.raw.breadcrumb }}
</VListItemSubtitle>
</VListItem>
</template>
<template v-if="modelValue?.breadcrumb" #selection>
<div>
<div class="text-body-2">{{ modelValue.name }}</div>
<div class="text-caption text-primary-lighten-3">
{{ modelValue.breadcrumb }}
</div>
</div>
</template>
</VAutocomplete>
</template>

<script lang="ts" setup>
import type { TopicItem } from './types';
import { useOutlineTree } from './useOutlineTree';

defineProps<{
modelValue: TopicItem | null;
}>();

const emit = defineEmits<{
'update:modelValue': [topic: TopicItem | null];
'topic:clear': [];
}>();

const { items } = useOutlineTree();

function onSelect(id: number | null) {
if (!id) {
emit('topic:clear');
return;
}
const topic = items.value.find((it) => it.id === id);
if (topic) emit('update:modelValue', topic);
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface TopicItem {
id: number;
name: string;
parentName?: string;
// Full ancestor breadcrumb (e.g. "Module 1 / Sub-module")
breadcrumb: string;
isGroup: boolean;
isLeaf: boolean;
depth: number;
// Full context path for building search queries.
context: string[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { activity as activityUtils } from '@tailor-cms/utils';
import { schema as schemaConfig } from '@tailor-cms/config';

import { useCurrentRepository } from '@/stores/current-repository';
import type { TopicItem } from './types';

const buildTopicMeta = (
activity: any,
activityCollection: any[],
repositoryName: string,
): TopicItem => {
const name = activity.data?.name || '';
const isLeaf = schemaConfig.isEditable(activity.type);
const ancestors = activityUtils.getAncestors(activityCollection, activity);
const ancestorNames = ancestors.map((a: any) => a.data?.name).filter(Boolean);
return {
id: activity.id,
name,
isGroup: !isLeaf,
parentName: ancestorNames.at(-1),
breadcrumb: ancestorNames.toReversed().join(' / '),
isLeaf,
depth: ancestors.length,
context: [name, ...ancestorNames, repositoryName].filter(Boolean),
};
};

function flattenTree(nodes: any[]): TopicItem[] {
const result: TopicItem[] = [];
for (const node of nodes) {
if (node.topicMeta) result.push(node.topicMeta);
if (node.children?.length) {
result.push(...flattenTree(node.children));
}
}
return result;
}

/**
* Derives a flat topic list from the repository outline.
* Uses shared toTreeFormat for hierarchy, schemaConfig.isEditable
* for leaf detection, and getAncestors for parent context.
*/
export function useOutlineTree() {
const store = useCurrentRepository();
const hasOutline = computed(() => store.outlineActivities.length > 0);

const items = computed<TopicItem[]>(() => {
const activities = store.outlineActivities as any[];
if (!activities.length) return [];
const repo = store.repository;
const tree = activityUtils.toTreeFormat(activities, {
processNodeFn: (activity: any) => ({
topicMeta: buildTopicMeta(activity, activities, repo?.name || ''),
}),
});
return flattenTree(tree);
});

return { hasOutline, items };
}
Loading
Loading