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
134 changes: 126 additions & 8 deletions apps/desktop/src/session/components/note-input/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ function CreateOtherFormatButton({
created_at: string;
title: string;
description: string;
category?: string;
targets?: string[];
sections: Array<{ title: string; description: string }>;
}) => p.id,
(p: {
Expand All @@ -442,11 +444,15 @@ function CreateOtherFormatButton({
created_at: string;
title: string;
description: string;
category?: string;
targets?: string[];
sections: Array<{ title: string; description: string }>;
}) => ({
user_id: p.user_id,
title: p.title,
description: p.description,
category: p.category,
targets: p.targets ? JSON.stringify(p.targets) : undefined,
sections: JSON.stringify(p.sections),
}),
[],
Expand Down Expand Up @@ -511,6 +517,8 @@ function CreateOtherFormatButton({
created_at: now,
title: template.title,
description: template.description,
category: template.category,
targets: template.targets,
sections: template.sections ?? [],
});

Expand Down Expand Up @@ -578,21 +586,34 @@ function CreateOtherFormatButton({
[meetingContent, suggestedTemplates],
);

const favoriteTemplates = useMemo(
() => sortFavoriteTemplates(userTemplates),
[userTemplates],
);
const otherTemplates = useMemo(
() => sortOtherTemplates(userTemplates),
[userTemplates],
);

const filteredFavoriteTemplates = useMemo(() => {
const sortedTemplates = [...userTemplates].sort((a, b) =>
(a.title || "").localeCompare(b.title || ""),
if (!searchQuery) {
return favoriteTemplates;
}

return favoriteTemplates.filter((template) =>
matchesTemplateSearch(template, searchQuery),
);
}, [favoriteTemplates, searchQuery]);

const filteredOtherTemplates = useMemo(() => {
if (!searchQuery) {
return sortedTemplates;
return otherTemplates;
}

return sortedTemplates.filter(
(template) =>
template.title?.toLowerCase().includes(searchQuery) ||
template.description?.toLowerCase().includes(searchQuery),
return otherTemplates.filter((template) =>
matchesTemplateSearch(template, searchQuery),
);
}, [searchQuery, userTemplates]);
}, [otherTemplates, searchQuery]);

const filteredSuggestedTemplates = useMemo(() => {
if (!searchQuery) {
Expand Down Expand Up @@ -622,6 +643,7 @@ function CreateOtherFormatButton({
key: string;
title: string;
description?: string;
tags?: string[];
onClick: () => void;
}>;
}>
Expand All @@ -635,6 +657,7 @@ function CreateOtherFormatButton({
key: template.slug || `suggested-${index}`,
title: template.title || "Untitled",
description: template.description,
tags: getTemplateTags(template),
onClick: () => handleSuggestedTemplateClick(template),
})),
emptyMessage: isSuggestedTemplatesLoading
Expand All @@ -648,10 +671,23 @@ function CreateOtherFormatButton({
key: template.id,
title: template.title || "Untitled",
description: template.description,
tags: getTemplateTags(template),
onClick: () => handleUseTemplate(template.id),
})),
emptyMessage: "No favorite templates yet",
},
{
key: "mine",
title: "My templates",
items: filteredOtherTemplates.map((template) => ({
key: template.id,
title: template.title || "Untitled",
description: template.description,
tags: getTemplateTags(template),
onClick: () => handleUseTemplate(template.id),
})),
emptyMessage: "No other templates yet",
},
];
}

Expand All @@ -678,6 +714,7 @@ function CreateOtherFormatButton({
key: template.slug || `suggested-${index}`,
title: template.title || "Untitled",
description: template.description,
tags: getTemplateTags(template),
onClick: () => handleSuggestedTemplateClick(template),
})),
},
Expand All @@ -692,6 +729,22 @@ function CreateOtherFormatButton({
key: template.id,
title: template.title || "Untitled",
description: template.description,
tags: getTemplateTags(template),
onClick: () => handleUseTemplate(template.id),
})),
},
]
: []),
...(filteredOtherTemplates.length > 0
? [
{
key: "mine",
title: "My templates",
items: filteredOtherTemplates.map((template) => ({
key: template.id,
title: template.title || "Untitled",
description: template.description,
tags: getTemplateTags(template),
onClick: () => handleUseTemplate(template.id),
})),
},
Expand All @@ -700,6 +753,7 @@ function CreateOtherFormatButton({
];
}, [
filteredFavoriteTemplates,
filteredOtherTemplates,
filteredSuggestedTemplates,
handleCreateTemplate,
handleSuggestedTemplateClick,
Expand Down Expand Up @@ -825,6 +879,7 @@ function CreateOtherFormatButton({
}}
title={item.title}
description={item.description}
tags={item.tags}
onClick={item.onClick}
onKeyDown={(e) => handleResultKeyDown(e, itemIndex)}
/>
Expand Down Expand Up @@ -1111,6 +1166,55 @@ type WebTemplate = {
sections: Array<{ title: string; description: string }>;
};

function getTemplateTags(template: { category?: string; targets?: string[] }) {
return [
...new Set([
...(template.category ? [template.category] : []),
...(template.targets ?? []),
]),
];
}

function matchesTemplateSearch(
template: {
title?: string;
description?: string;
category?: string;
targets?: string[];
},
query: string,
) {
return (
template.title?.toLowerCase().includes(query) ||
template.description?.toLowerCase().includes(query) ||
template.category?.toLowerCase().includes(query) ||
template.targets?.some((target) => target.toLowerCase().includes(query))
);
}

function sortFavoriteTemplates<
T extends { pinned?: boolean; pin_order?: number; title?: string },
>(templates: T[]) {
return [...templates]
.filter((template) => template.pinned)
.sort((a, b) => {
const orderA = a.pin_order ?? Infinity;
const orderB = b.pin_order ?? Infinity;
if (orderA !== orderB) {
return orderA - orderB;
}
return (a.title || "").localeCompare(b.title || "");
});
}

function sortOtherTemplates<T extends { pinned?: boolean; title?: string }>(
templates: T[],
) {
return [...templates]
.filter((template) => !template.pinned)
.sort((a, b) => (a.title || "").localeCompare(b.title || ""));
}

const TEMPLATE_SUGGESTION_STOP_WORDS = new Set([
"about",
"after",
Expand Down Expand Up @@ -1297,12 +1401,14 @@ function TemplateResultButton({
buttonRef,
title,
description,
tags,
onClick,
onKeyDown,
}: {
buttonRef?: React.Ref<HTMLButtonElement>;
title: string;
description?: string;
tags?: string[];
onClick: () => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLButtonElement>) => void;
}) {
Expand All @@ -1324,6 +1430,18 @@ function TemplateResultButton({
{description}
</span>
) : null}
{tags && tags.length > 0 ? (
<span className="mt-1 flex flex-wrap gap-1">
{tags.map((tag, index) => (
<span
key={`${tag}-${index}`}
className="rounded-xs bg-neutral-100 px-1.5 py-0.5 text-[11px] text-neutral-500"
>
{tag}
</span>
))}
</span>
) : null}
</button>
);
}
54 changes: 27 additions & 27 deletions apps/desktop/src/shared/ui/resource-list/preview-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,38 @@ export function ResourcePreviewHeader({
children?: ReactNode;
}) {
return (
<div className="border-b border-neutral-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<h2 className="truncate text-lg font-semibold">
{title || "Untitled"}
</h2>
{description && (
<p className="mt-1 text-sm text-neutral-500">{description}</p>
)}
<div className="pt-1 pr-1 pb-4 pl-3">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
{category ? (
<span className="font-mono text-xs text-stone-400">{category}</span>
) : null}
</div>
<Button onClick={onClone} size="sm" className="ml-4 shrink-0">
<Button onClick={onClone} size="sm" className="shrink-0">
{actionIcon ?? <Copy className="mr-2 h-4 w-4" />}
{actionLabel}
</Button>
</div>
{category && (
<div className="mt-2">
<span className="font-mono text-xs text-stone-400">({category})</span>
</div>
)}
{targets && targets.length > 0 && (
<div className="mt-2 flex flex-wrap items-center gap-2">
{targets.map((target, index) => (
<span
key={index}
className="rounded-xs bg-neutral-100 px-2 py-0.5 text-xs text-neutral-600"
>
{target}
</span>
))}
</div>
)}
<div className="mt-3 min-w-0 px-3">
<h2 className="truncate text-lg font-semibold">
{title || "Untitled"}
</h2>
{description && (
<p className="mt-1 text-sm text-neutral-500">{description}</p>
)}
{targets && targets.length > 0 && (
<div className="mt-2 flex flex-wrap items-center gap-2">
{targets.map((target, index) => (
<span
key={index}
className="rounded-xs bg-neutral-100 px-2 py-0.5 text-xs text-neutral-600"
>
{target}
</span>
))}
</div>
)}
</div>
{children}
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/store/tinybase/store/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ export const StoreComponent = () => {
({ select }) => {
select("title");
select("description");
select("pinned");
select("pin_order");
select("category");
select("targets");
select("sections");
},
)
Expand Down Expand Up @@ -194,6 +198,10 @@ export const StoreComponent = () => {
({ select, where, param }) => {
select("title");
select("description");
select("pinned");
select("pin_order");
select("category");
select("targets");
select("sections");
select("user_id");
where("user_id", (param("user_id") as string) ?? "");
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/templates/components/details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,19 @@ export function TemplateDetailsColumn({
selectedMineId,
selectedWebTemplate,
handleDeleteTemplate,
handleDuplicateTemplate,
handleCloneTemplate,
}: {
isWebMode: boolean;
selectedMineId: string | null;
selectedWebTemplate: WebTemplate | null;
handleDeleteTemplate: (id: string) => void;
handleDuplicateTemplate: (id: string) => void;
handleCloneTemplate: (template: {
title: string;
description: string;
category?: string;
targets?: string[];
sections: TemplateSection[];
}) => void;
}) {
Expand All @@ -57,6 +61,7 @@ export function TemplateDetailsColumn({
key={selectedMineId}
id={selectedMineId}
handleDeleteTemplate={handleDeleteTemplate}
handleDuplicateTemplate={handleDuplicateTemplate}
/>
);
}
Expand All @@ -69,6 +74,8 @@ function WebTemplatePreview({
onClone: (template: {
title: string;
description: string;
category?: string;
targets?: string[];
sections: TemplateSection[];
}) => void;
}) {
Expand All @@ -85,6 +92,8 @@ function WebTemplatePreview({
onClone({
title: template.title ?? "",
description: template.description ?? "",
category: template.category,
targets: template.targets,
sections: template.sections ?? [],
})
}
Expand Down
Loading
Loading