Move template management into Entries side panel
This commit is contained in:
parent
64c06081f0
commit
a72ddf6aec
@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { listEntryTemplates, type EntryTemplateItemDto } from "$lib/backend/templates";
|
||||||
import CalendarWidget from "$lib/components/CalendarWidget.svelte";
|
import CalendarWidget from "$lib/components/CalendarWidget.svelte";
|
||||||
import { entriesStore } from "$lib/stores/entries";
|
import { entriesBusyStore, entriesStore } from "$lib/stores/entries";
|
||||||
import { createFragmentDraft, fragmentsStore } from "$lib/stores/fragments";
|
import { createFragmentDraft, fragmentsStore } from "$lib/stores/fragments";
|
||||||
import { createListDraft, createListFromLabel, listsStore } from "$lib/stores/lists";
|
import { createListDraft, createListFromLabel, listsStore } from "$lib/stores/lists";
|
||||||
import { createTodoListDraft, createTodoListFromLabel, serializeTodoList, todoListsStore, todosStore } from "$lib/stores/todos";
|
import { createTodoListDraft, createTodoListFromLabel, serializeTodoList, todoListsStore, todosStore } from "$lib/stores/todos";
|
||||||
|
|
||||||
export let activeSection = "entries";
|
export let activeSection = "entries";
|
||||||
export let activeDocumentId = "";
|
export let activeDocumentId = "";
|
||||||
|
export let templateRefreshToken = 0;
|
||||||
export let onOpenDocument: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
export let onOpenDocument: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
||||||
export let onEditItem: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
export let onEditItem: (doc: { id: string; label: string; initialContent: string }) => void = () => {};
|
||||||
export let onDeleteItem: (doc: { id: string; label: string }) => void = () => {};
|
export let onDeleteItem: (doc: { id: string; label: string }) => void = () => {};
|
||||||
@ -14,6 +16,7 @@
|
|||||||
let showNewItemInput = false;
|
let showNewItemInput = false;
|
||||||
let newItemName = "";
|
let newItemName = "";
|
||||||
let newItemInput: HTMLInputElement | null = null;
|
let newItemInput: HTMLInputElement | null = null;
|
||||||
|
let createTemplateMode = false;
|
||||||
|
|
||||||
type SidePanelItem = {
|
type SidePanelItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -22,6 +25,11 @@
|
|||||||
};
|
};
|
||||||
let todoDocuments: SidePanelItem[] = [];
|
let todoDocuments: SidePanelItem[] = [];
|
||||||
let customCalendarEntries: SidePanelItem[] = [];
|
let customCalendarEntries: SidePanelItem[] = [];
|
||||||
|
let templateItems: SidePanelItem[] = [];
|
||||||
|
let templatesBusy = false;
|
||||||
|
let templateError = "";
|
||||||
|
let wasEntriesSection = false;
|
||||||
|
let lastTemplateRefreshToken = -1;
|
||||||
|
|
||||||
const sectionTitles: Record<string, string> = {
|
const sectionTitles: Record<string, string> = {
|
||||||
entries: "Entries",
|
entries: "Entries",
|
||||||
@ -62,6 +70,37 @@
|
|||||||
selectedCalendarDate = payload;
|
selectedCalendarDate = payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toTemplateStoreId(filePath: string): string {
|
||||||
|
return `entries/template-file/${encodeURIComponent(filePath)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTemplateLabel(fileName: string): string {
|
||||||
|
return fileName.replace(/\.template\.md$/i, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapTemplateDto(item: EntryTemplateItemDto): SidePanelItem {
|
||||||
|
return {
|
||||||
|
id: toTemplateStoreId(item.filePath),
|
||||||
|
label: toTemplateLabel(item.fileName),
|
||||||
|
initialContent: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTemplates() {
|
||||||
|
if (activeSection !== "entries") return;
|
||||||
|
templatesBusy = true;
|
||||||
|
templateError = "";
|
||||||
|
try {
|
||||||
|
const templates = await listEntryTemplates();
|
||||||
|
templateItems = templates.map(mapTemplateDto);
|
||||||
|
} catch (error) {
|
||||||
|
templateError = String(error);
|
||||||
|
templateItems = [];
|
||||||
|
} finally {
|
||||||
|
templatesBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function daysInMonth(year: number, month: number): number {
|
function daysInMonth(year: number, month: number): number {
|
||||||
return new Date(year, month + 1, 0).getDate();
|
return new Date(year, month + 1, 0).getDate();
|
||||||
}
|
}
|
||||||
@ -126,6 +165,9 @@
|
|||||||
|
|
||||||
function handleAddItem() {
|
function handleAddItem() {
|
||||||
if (activeSection === "entries" || activeSection === "todos" || activeSection === "lists") {
|
if (activeSection === "entries" || activeSection === "todos" || activeSection === "lists") {
|
||||||
|
if (activeSection === "entries") {
|
||||||
|
createTemplateMode = false;
|
||||||
|
}
|
||||||
showNewItemInput = true;
|
showNewItemInput = true;
|
||||||
newItemName = "";
|
newItemName = "";
|
||||||
queueMicrotask(() => newItemInput?.focus());
|
queueMicrotask(() => newItemInput?.focus());
|
||||||
@ -158,6 +200,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAddTemplate() {
|
||||||
|
if (activeSection !== "entries") return;
|
||||||
|
createTemplateMode = true;
|
||||||
|
showNewItemInput = true;
|
||||||
|
newItemName = "";
|
||||||
|
queueMicrotask(() => newItemInput?.focus());
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmNewItem() {
|
async function confirmNewItem() {
|
||||||
const label = newItemName.trim();
|
const label = newItemName.trim();
|
||||||
if (!label) {
|
if (!label) {
|
||||||
@ -168,10 +218,17 @@
|
|||||||
newItemName = "";
|
newItemName = "";
|
||||||
|
|
||||||
if (activeSection === "entries") {
|
if (activeSection === "entries") {
|
||||||
const id = `entries/draft-${Date.now()}`;
|
const isTemplate = createTemplateMode;
|
||||||
const item = { id, label, initialContent: `# ${label}\n\n` };
|
const displayLabel = isTemplate
|
||||||
|
? label.endsWith("_template")
|
||||||
|
? label
|
||||||
|
: `${label}_template`
|
||||||
|
: label;
|
||||||
|
const id = isTemplate ? `entries/template-draft-${Date.now()}` : `entries/draft-${Date.now()}`;
|
||||||
|
const item = { id, label: displayLabel, initialContent: `# ${displayLabel}\n\n` };
|
||||||
entriesStore.update((items) => [item, ...items]);
|
entriesStore.update((items) => [item, ...items]);
|
||||||
onEditItem(item);
|
onEditItem(item);
|
||||||
|
createTemplateMode = false;
|
||||||
} else if (activeSection === "todos") {
|
} else if (activeSection === "todos") {
|
||||||
try {
|
try {
|
||||||
const { meta, items: todoItems } = await createTodoListFromLabel(label);
|
const { meta, items: todoItems } = await createTodoListFromLabel(label);
|
||||||
@ -208,6 +265,7 @@
|
|||||||
function cancelNewItem() {
|
function cancelNewItem() {
|
||||||
showNewItemInput = false;
|
showNewItemInput = false;
|
||||||
newItemName = "";
|
newItemName = "";
|
||||||
|
createTemplateMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewItemKeydown(event: KeyboardEvent) {
|
function handleNewItemKeydown(event: KeyboardEvent) {
|
||||||
@ -237,14 +295,38 @@
|
|||||||
$: isCalendarSection = activeSection === "calendar";
|
$: isCalendarSection = activeSection === "calendar";
|
||||||
$: calendarEntries = [...customCalendarEntries, ...getCalendarEntries(calendarYear, calendarMonth)];
|
$: calendarEntries = [...customCalendarEntries, ...getCalendarEntries(calendarYear, calendarMonth)];
|
||||||
$: showItemActions = activeSection === "entries" || activeSection === "todos" || activeSection === "lists" || activeSection === "fragments";
|
$: showItemActions = activeSection === "entries" || activeSection === "todos" || activeSection === "lists" || activeSection === "fragments";
|
||||||
|
$: entryItems = activeSection === "entries"
|
||||||
|
? $entriesStore.filter((item) => !item.id.startsWith("entries/template-draft-"))
|
||||||
|
: [];
|
||||||
|
$: templateDraftItems = activeSection === "entries"
|
||||||
|
? $entriesStore.filter((item) => item.id.startsWith("entries/template-draft-"))
|
||||||
|
: [];
|
||||||
|
$: allTemplateItems = [...templateDraftItems, ...templateItems];
|
||||||
|
|
||||||
|
$: if (activeSection === "entries" && (!wasEntriesSection || templateRefreshToken !== lastTemplateRefreshToken)) {
|
||||||
|
wasEntriesSection = true;
|
||||||
|
lastTemplateRefreshToken = templateRefreshToken;
|
||||||
|
void refreshTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (activeSection !== "entries") {
|
||||||
|
wasEntriesSection = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="side-panel" aria-label="Section panel">
|
<section class="side-panel" aria-label="Section panel">
|
||||||
<header class="panel-header">
|
<header class="panel-header">
|
||||||
<h2>{panelTitle}</h2>
|
<h2>{panelTitle}</h2>
|
||||||
<button type="button" class="panel-action" aria-label="Add item" on:click={handleAddItem}>
|
<div class="panel-header-actions">
|
||||||
<span class="material-symbols-outlined">add</span>
|
{#if activeSection === "entries"}
|
||||||
</button>
|
<button type="button" class="panel-action" aria-label="Add template" title="Add template" on:click={handleAddTemplate}>
|
||||||
|
<span class="material-symbols-outlined">palette</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button type="button" class="panel-action" aria-label="Add item" title="Add item" on:click={handleAddItem}>
|
||||||
|
<span class="material-symbols-outlined">add</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if isCalendarSection}
|
{#if isCalendarSection}
|
||||||
@ -281,36 +363,111 @@
|
|||||||
type="text"
|
type="text"
|
||||||
bind:this={newItemInput}
|
bind:this={newItemInput}
|
||||||
bind:value={newItemName}
|
bind:value={newItemName}
|
||||||
placeholder={activeSection === "entries" ? "Entry name..." : activeSection === "todos" ? "Todo list name..." : "List name..."}
|
placeholder={activeSection === "entries"
|
||||||
|
? createTemplateMode
|
||||||
|
? "Template name..."
|
||||||
|
: "Entry name..."
|
||||||
|
: activeSection === "todos"
|
||||||
|
? "Todo list name..."
|
||||||
|
: "List name..."}
|
||||||
on:keydown={handleNewItemKeydown}
|
on:keydown={handleNewItemKeydown}
|
||||||
on:blur={confirmNewItem}
|
on:blur={confirmNewItem}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ul class="panel-list">
|
{#if activeSection === "entries"}
|
||||||
{#each items as item}
|
<div class="panel-subsection">
|
||||||
<li class:is-active={item.id === activeDocumentId}>
|
<h3>Entries</h3>
|
||||||
<button
|
{#if $entriesBusyStore}
|
||||||
type="button"
|
<p class="section-copy">Loading entries...</p>
|
||||||
class="item-label"
|
{:else}
|
||||||
on:click={() => onOpenDocument(item)}
|
<ul class="panel-list">
|
||||||
>
|
{#each entryItems as item}
|
||||||
{item.label}
|
<li class:is-active={item.id === activeDocumentId}>
|
||||||
</button>
|
<button
|
||||||
{#if showItemActions}
|
type="button"
|
||||||
<div class="item-actions">
|
class="item-label"
|
||||||
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
on:click={() => onOpenDocument(item)}
|
||||||
<span class="material-symbols-outlined">edit</span>
|
>
|
||||||
</button>
|
{item.label}
|
||||||
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
</button>
|
||||||
<span class="material-symbols-outlined">delete</span>
|
<div class="item-actions">
|
||||||
</button>
|
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
||||||
</div>
|
<span class="material-symbols-outlined">edit</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
||||||
|
<span class="material-symbols-outlined">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{#if !entryItems.length}
|
||||||
|
<p class="section-copy">No entries found.</p>
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
{/if}
|
||||||
{/each}
|
</div>
|
||||||
</ul>
|
|
||||||
|
<div class="panel-subsection">
|
||||||
|
<h3>Templates</h3>
|
||||||
|
{#if templatesBusy}
|
||||||
|
<p class="section-copy">Loading templates...</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="panel-list">
|
||||||
|
{#each allTemplateItems as item}
|
||||||
|
<li class:is-active={item.id === activeDocumentId}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="item-label"
|
||||||
|
on:click={() => onOpenDocument(item)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
<div class="item-actions">
|
||||||
|
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
||||||
|
<span class="material-symbols-outlined">edit</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
||||||
|
<span class="material-symbols-outlined">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{#if !allTemplateItems.length}
|
||||||
|
<p class="section-copy">No templates found.</p>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if templateError}
|
||||||
|
<p class="template-error">{templateError}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<ul class="panel-list">
|
||||||
|
{#each items as item}
|
||||||
|
<li class:is-active={item.id === activeDocumentId}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="item-label"
|
||||||
|
on:click={() => onOpenDocument(item)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
{#if showItemActions}
|
||||||
|
<div class="item-actions">
|
||||||
|
<button type="button" class="item-action" on:click|stopPropagation={() => onEditItem(item)} aria-label="Edit">
|
||||||
|
<span class="material-symbols-outlined">edit</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="item-action item-action-danger" on:click|stopPropagation={() => onDeleteItem(item)} aria-label="Delete">
|
||||||
|
<span class="material-symbols-outlined">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -332,6 +489,12 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-header h2 {
|
.panel-header h2 {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -481,6 +644,30 @@
|
|||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-subsection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-subsection h3 {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-copy {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-error {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
.new-item-input {
|
.new-item-input {
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { unlockVaultWorkspace } from "$lib/backend/auth";
|
import { unlockVaultWorkspace } from "$lib/backend/auth";
|
||||||
import AppModal from "$lib/components/AppModal.svelte";
|
import AppModal from "$lib/components/AppModal.svelte";
|
||||||
|
import { deleteEntryTemplate, loadEntryTemplate, saveEntryTemplate } from "$lib/backend/templates";
|
||||||
import { deleteEntryByStoreId, entriesStore, getDefaultEntry, hydrateEntries, loadEntryByStoreId, saveEntryFromStore } from "$lib/stores/entries";
|
import { deleteEntryByStoreId, entriesStore, getDefaultEntry, hydrateEntries, loadEntryByStoreId, saveEntryFromStore } from "$lib/stores/entries";
|
||||||
import { deleteFragmentByStoreId, hydrateFragments } from "$lib/stores/fragments";
|
import { deleteFragmentByStoreId, hydrateFragments } from "$lib/stores/fragments";
|
||||||
import { deleteListByStoreId, hydrateLists, updateListByStoreId } from "$lib/stores/lists";
|
import { deleteListByStoreId, hydrateLists, updateListByStoreId } from "$lib/stores/lists";
|
||||||
@ -45,6 +46,25 @@
|
|||||||
let unlockResolver: ((password: string | null) => void) | null = null;
|
let unlockResolver: ((password: string | null) => void) | null = null;
|
||||||
let fragmentBootstrapInFlight = false;
|
let fragmentBootstrapInFlight = false;
|
||||||
let pendingDeleteItemId = "";
|
let pendingDeleteItemId = "";
|
||||||
|
let templateRefreshToken = 0;
|
||||||
|
|
||||||
|
function toTemplatePath(id: string): string | null {
|
||||||
|
const prefix = "entries/template-file/";
|
||||||
|
if (!id.startsWith(prefix)) return null;
|
||||||
|
const encoded = id.slice(prefix.length).trim();
|
||||||
|
if (!encoded) return null;
|
||||||
|
try {
|
||||||
|
const decoded = decodeURIComponent(encoded);
|
||||||
|
return decoded || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function templateNameFromPath(path: string): string {
|
||||||
|
const fileName = path.split(/[\\/]/).pop() ?? path;
|
||||||
|
return fileName.replace(/\.template\.md$/i, "");
|
||||||
|
}
|
||||||
|
|
||||||
function showModal(options: {
|
function showModal(options: {
|
||||||
action: "logout-confirm" | "logout-info" | "unlock-vault" | "delete-confirm";
|
action: "logout-confirm" | "logout-info" | "unlock-vault" | "delete-confirm";
|
||||||
@ -160,6 +180,7 @@
|
|||||||
if (isVaultReady()) {
|
if (isVaultReady()) {
|
||||||
try {
|
try {
|
||||||
await hydrateEntries();
|
await hydrateEntries();
|
||||||
|
templateRefreshToken += 1;
|
||||||
const firstEntry = getDefaultEntry(get(entriesStore));
|
const firstEntry = getDefaultEntry(get(entriesStore));
|
||||||
if (firstEntry && activeDocumentId === "entries/daily-notes") {
|
if (firstEntry && activeDocumentId === "entries/daily-notes") {
|
||||||
await handleOpenDocument(firstEntry);
|
await handleOpenDocument(firstEntry);
|
||||||
@ -185,6 +206,7 @@
|
|||||||
setVaultSession(password);
|
setVaultSession(password);
|
||||||
|
|
||||||
await hydrateEntries();
|
await hydrateEntries();
|
||||||
|
templateRefreshToken += 1;
|
||||||
const firstEntry = getDefaultEntry(get(entriesStore));
|
const firstEntry = getDefaultEntry(get(entriesStore));
|
||||||
if (firstEntry && activeDocumentId === "entries/daily-notes") {
|
if (firstEntry && activeDocumentId === "entries/daily-notes") {
|
||||||
await handleOpenDocument(firstEntry);
|
await handleOpenDocument(firstEntry);
|
||||||
@ -211,7 +233,31 @@
|
|||||||
if (!content?.trim()) return;
|
if (!content?.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (selectedSection === "entries" && (activeDocumentId.startsWith("entries/file/") || activeDocumentId.startsWith("entries/draft-"))) {
|
if (selectedSection === "entries" && activeDocumentId.startsWith("entries/template-draft-")) {
|
||||||
|
const draft = get(entriesStore).find((item) => item.id === activeDocumentId);
|
||||||
|
const draftLabel = (draft?.label ?? activeDocumentLabel ?? "").trim();
|
||||||
|
const templateName = draftLabel.replace(/_template$/i, "").trim() || draftLabel;
|
||||||
|
await saveEntryTemplate({
|
||||||
|
name: templateName,
|
||||||
|
content
|
||||||
|
});
|
||||||
|
templateRefreshToken += 1;
|
||||||
|
entriesStore.update((items) => items.filter((item) => item.id !== activeDocumentId));
|
||||||
|
const { [activeDocumentId]: _, ...rest } = openDocuments;
|
||||||
|
openDocuments = rest;
|
||||||
|
activeDocumentId = "";
|
||||||
|
activeDocumentLabel = "";
|
||||||
|
editMode = false;
|
||||||
|
} else if (selectedSection === "entries" && activeDocumentId.startsWith("entries/template-file/")) {
|
||||||
|
const filePath = toTemplatePath(activeDocumentId);
|
||||||
|
if (!filePath) return;
|
||||||
|
await saveEntryTemplate({
|
||||||
|
name: templateNameFromPath(filePath),
|
||||||
|
content,
|
||||||
|
filePath
|
||||||
|
});
|
||||||
|
templateRefreshToken += 1;
|
||||||
|
} else if (selectedSection === "entries" && (activeDocumentId.startsWith("entries/file/") || activeDocumentId.startsWith("entries/draft-"))) {
|
||||||
const saved = await saveEntryFromStore(activeDocumentId, content, "Overwrite");
|
const saved = await saveEntryFromStore(activeDocumentId, content, "Overwrite");
|
||||||
if (saved && saved.id !== activeDocumentId) {
|
if (saved && saved.id !== activeDocumentId) {
|
||||||
const { [activeDocumentId]: _, ...rest } = openDocuments;
|
const { [activeDocumentId]: _, ...rest } = openDocuments;
|
||||||
@ -281,6 +327,20 @@
|
|||||||
} catch {
|
} catch {
|
||||||
// entry content will use initialContent fallback
|
// entry content will use initialContent fallback
|
||||||
}
|
}
|
||||||
|
} else if (selectedSection === "entries" && doc.id.startsWith("entries/template-file/") && !doc.initialContent) {
|
||||||
|
try {
|
||||||
|
const filePath = toTemplatePath(doc.id);
|
||||||
|
if (filePath) {
|
||||||
|
const loaded = await loadEntryTemplate(filePath);
|
||||||
|
resolvedDoc = {
|
||||||
|
id: doc.id,
|
||||||
|
label: loaded.fileName.replace(/\.template\.md$/i, ""),
|
||||||
|
initialContent: loaded.content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// template content will use initialContent fallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(resolvedDoc.id in openDocuments)) {
|
if (!(resolvedDoc.id in openDocuments)) {
|
||||||
@ -303,7 +363,16 @@
|
|||||||
try {
|
try {
|
||||||
let ok = false;
|
let ok = false;
|
||||||
if (selectedSection === "entries") {
|
if (selectedSection === "entries") {
|
||||||
ok = await deleteEntryByStoreId(id);
|
const templatePath = toTemplatePath(id);
|
||||||
|
if (templatePath) {
|
||||||
|
ok = await deleteEntryTemplate(templatePath);
|
||||||
|
if (ok) templateRefreshToken += 1;
|
||||||
|
} else if (id.startsWith("entries/template-draft-")) {
|
||||||
|
entriesStore.update((items) => items.filter((item) => item.id !== id));
|
||||||
|
ok = true;
|
||||||
|
} else {
|
||||||
|
ok = await deleteEntryByStoreId(id);
|
||||||
|
}
|
||||||
} else if (selectedSection === "todos") {
|
} else if (selectedSection === "todos") {
|
||||||
ok = await deleteTodoListByStoreId(id);
|
ok = await deleteTodoListByStoreId(id);
|
||||||
} else if (selectedSection === "lists") {
|
} else if (selectedSection === "lists") {
|
||||||
@ -356,6 +425,7 @@
|
|||||||
<SidePanel
|
<SidePanel
|
||||||
activeSection={selectedSection}
|
activeSection={selectedSection}
|
||||||
{activeDocumentId}
|
{activeDocumentId}
|
||||||
|
{templateRefreshToken}
|
||||||
onOpenDocument={handleOpenDocument}
|
onOpenDocument={handleOpenDocument}
|
||||||
onEditItem={handleEditItem}
|
onEditItem={handleEditItem}
|
||||||
onDeleteItem={handleDeleteItem}
|
onDeleteItem={handleDeleteItem}
|
||||||
@ -402,4 +472,3 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,6 @@
|
|||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import AppModal from "$lib/components/AppModal.svelte";
|
import AppModal from "$lib/components/AppModal.svelte";
|
||||||
import Navbar from "$lib/components/Navbar.svelte";
|
import Navbar from "$lib/components/Navbar.svelte";
|
||||||
import {
|
|
||||||
deleteEntryTemplate,
|
|
||||||
listEntryTemplates,
|
|
||||||
loadEntryTemplate,
|
|
||||||
saveEntryTemplate,
|
|
||||||
type EntryTemplateItemDto
|
|
||||||
} from "$lib/backend/templates";
|
|
||||||
import {
|
import {
|
||||||
addFragmentType,
|
addFragmentType,
|
||||||
addSettingsTag,
|
addSettingsTag,
|
||||||
@ -41,12 +34,6 @@
|
|||||||
let sidecarRoot = "";
|
let sidecarRoot = "";
|
||||||
let sidecarRootIsCustom = false;
|
let sidecarRootIsCustom = false;
|
||||||
let sidecarRootError = "";
|
let sidecarRootError = "";
|
||||||
let templates: EntryTemplateItemDto[] = [];
|
|
||||||
let templateName = "";
|
|
||||||
let templateContent = "";
|
|
||||||
let templateFilePath: string | null = null;
|
|
||||||
let templateError = "";
|
|
||||||
let templatesBusy = false;
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
@ -56,8 +43,6 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
sidecarRootError = String(e);
|
sidecarRootError = String(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
await refreshTemplates();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function saveSidecarRoot() {
|
async function saveSidecarRoot() {
|
||||||
@ -208,83 +193,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTemplates() {
|
|
||||||
templatesBusy = true;
|
|
||||||
templateError = "";
|
|
||||||
try {
|
|
||||||
templates = await listEntryTemplates();
|
|
||||||
} catch (error) {
|
|
||||||
templateError = String(error);
|
|
||||||
} finally {
|
|
||||||
templatesBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetTemplateEditor() {
|
|
||||||
templateName = "";
|
|
||||||
templateContent = "";
|
|
||||||
templateFilePath = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function editTemplate(item: EntryTemplateItemDto) {
|
|
||||||
templateError = "";
|
|
||||||
try {
|
|
||||||
const loaded = await loadEntryTemplate(item.filePath);
|
|
||||||
templateFilePath = loaded.filePath;
|
|
||||||
templateName = loaded.fileName.replace(/\.template\.md$/i, "");
|
|
||||||
templateContent = loaded.content;
|
|
||||||
} catch (error) {
|
|
||||||
templateError = String(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveTemplate() {
|
|
||||||
templateError = "";
|
|
||||||
if (!templateName.trim()) {
|
|
||||||
templateError = "Template name is required.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!templateContent.trim()) {
|
|
||||||
templateError = "Template content is required.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const wasCreate = !templateFilePath;
|
|
||||||
const saved = await saveEntryTemplate({
|
|
||||||
name: templateName,
|
|
||||||
content: templateContent,
|
|
||||||
filePath: templateFilePath ?? undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
if (wasCreate) {
|
|
||||||
resetTemplateEditor();
|
|
||||||
} else {
|
|
||||||
templateFilePath = saved.filePath;
|
|
||||||
}
|
|
||||||
await refreshTemplates();
|
|
||||||
} catch (error) {
|
|
||||||
templateError = String(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeTemplate(item: EntryTemplateItemDto) {
|
|
||||||
templateError = "";
|
|
||||||
try {
|
|
||||||
const ok = await deleteEntryTemplate(item.filePath);
|
|
||||||
if (!ok) {
|
|
||||||
templateError = "Failed to delete template.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (templateFilePath === item.filePath) {
|
|
||||||
resetTemplateEditor();
|
|
||||||
}
|
|
||||||
await refreshTemplates();
|
|
||||||
} catch (error) {
|
|
||||||
templateError = String(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-shell panel-closed">
|
<div class="app-shell panel-closed">
|
||||||
@ -392,57 +300,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="route-card">
|
|
||||||
<h2>Entry Templates</h2>
|
|
||||||
<p class="section-copy">Create reusable markdown templates stored as <code>.template.md</code> files in your data directory.</p>
|
|
||||||
|
|
||||||
<div class="template-grid">
|
|
||||||
<div class="template-editor">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Template name (example: Weekly Review)"
|
|
||||||
bind:value={templateName}
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
rows="8"
|
|
||||||
placeholder="Template markdown content"
|
|
||||||
bind:value={templateContent}
|
|
||||||
></textarea>
|
|
||||||
<div class="row-actions">
|
|
||||||
<button type="button" class="secondary-btn" on:click={saveTemplate}>
|
|
||||||
{templateFilePath ? "Update Template" : "Create Template"}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="ghost-btn" on:click={resetTemplateEditor}>New</button>
|
|
||||||
<button type="button" class="ghost-btn" on:click={refreshTemplates}>Refresh</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="template-list">
|
|
||||||
{#if templatesBusy}
|
|
||||||
<p class="section-copy">Loading templates...</p>
|
|
||||||
{:else if templates.length === 0}
|
|
||||||
<p class="section-copy">No templates found.</p>
|
|
||||||
{:else}
|
|
||||||
<ul class="item-list">
|
|
||||||
{#each templates as item}
|
|
||||||
<li class="item-row">
|
|
||||||
<span>{item.fileName.replace(/\.template\.md$/i, "")}</span>
|
|
||||||
<div class="row-actions">
|
|
||||||
<button type="button" class="ghost-btn" on:click={() => editTemplate(item)}>Edit</button>
|
|
||||||
<button type="button" class="danger-btn" on:click={() => removeTemplate(item)}>Delete</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if templateError}
|
|
||||||
<p class="error-text">{templateError}</p>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="route-card">
|
<section class="route-card">
|
||||||
<h2>Sidecar</h2>
|
<h2>Sidecar</h2>
|
||||||
<p class="section-copy">Root directory containing the Journal.Sidecar project.</p>
|
<p class="section-copy">Root directory containing the Journal.Sidecar project.</p>
|
||||||
@ -625,31 +482,4 @@
|
|||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-editor {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-editor textarea {
|
|
||||||
border: 1px solid var(--border-soft);
|
|
||||||
border-radius: 7px;
|
|
||||||
background: var(--bg-app);
|
|
||||||
color: var(--text-primary);
|
|
||||||
padding: 8px 9px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 980px) {
|
|
||||||
.template-grid {
|
|
||||||
grid-template-columns: 1.2fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user