fitle fixes
This commit is contained in:
parent
d510e96541
commit
63a862fd7d
|
|
@ -1 +1 @@
|
||||||
1.1.27
|
1.1.30
|
||||||
|
|
@ -10,7 +10,9 @@
|
||||||
import ConfirmDialog from './ConfirmDialog.svelte';
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
||||||
import WiedervorlageSection from './WiedervorlageSection.svelte';
|
import WiedervorlageSection from './WiedervorlageSection.svelte';
|
||||||
import LinkTitle from './LinkTitle.svelte';
|
import LinkTitle from './LinkTitle.svelte';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
import { currentScope, scopeSettings } from '$lib/stores/scopeContext';
|
import { currentScope, scopeSettings } from '$lib/stores/scopeContext';
|
||||||
|
import { normalizeTitleAndBody } from '$lib/utils/titleUtils';
|
||||||
import { useUnsavedGuard } from '$lib/utils/unsavedGuard.svelte';
|
import { useUnsavedGuard } from '$lib/utils/unsavedGuard.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -96,12 +98,14 @@
|
||||||
|
|
||||||
const isPrivate = journalScope === 'private';
|
const isPrivate = journalScope === 'private';
|
||||||
if (selectedLinkedContextId) {
|
if (selectedLinkedContextId) {
|
||||||
const topic = await createTopic(selectedLinkedContextId, title);
|
const topic = await createTopic(selectedLinkedContextId, title, isPrivate);
|
||||||
if (body) {
|
if (body) {
|
||||||
await createHistoryEntry(topic.id, selectedDate, body, null, false, isPrivate);
|
await createHistoryEntry(topic.id, selectedDate, body, null, false, isPrivate);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const text = body ? `${title}\n${body}` : title;
|
const { maxTitleLength } = get(scopeSettings);
|
||||||
|
const { title: shortTitle, body: fullBody } = normalizeTitleAndBody(title, body, maxTitleLength);
|
||||||
|
const text = fullBody ? `${shortTitle}\n\n${fullBody}` : shortTitle;
|
||||||
await getOrCreateJournalTopic();
|
await getOrCreateJournalTopic();
|
||||||
await createHistoryEntry(JOURNAL_TOPIC_ID, selectedDate, text, null, wiedervorlageChecked, isPrivate);
|
await createHistoryEntry(JOURNAL_TOPIC_ID, selectedDate, text, null, wiedervorlageChecked, isPrivate);
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +133,7 @@
|
||||||
function startEdit(entry: { id: string; text: string; isPrivate?: boolean }) {
|
function startEdit(entry: { id: string; text: string; isPrivate?: boolean }) {
|
||||||
const lines = entry.text.split('\n');
|
const lines = entry.text.split('\n');
|
||||||
editingId = entry.id;
|
editingId = entry.id;
|
||||||
editTitle = lines[0];
|
editTitle = lines[0].replace(/^#{1,6}\s+/, '');
|
||||||
editBody = lines.slice(1).join('\n').trim();
|
editBody = lines.slice(1).join('\n').trim();
|
||||||
editIsPrivate = !!entry.isPrivate;
|
editIsPrivate = !!entry.isPrivate;
|
||||||
}
|
}
|
||||||
|
|
@ -343,7 +347,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{@const lines = entry.text.split('\n')}
|
{@const lines = entry.text.split('\n')}
|
||||||
{@const title = lines[0]}
|
{@const title = lines[0].replace(/^#{1,6}\s+/, '')}
|
||||||
{@const body = lines.slice(1).join('\n').trim()}
|
{@const body = lines.slice(1).join('\n').trim()}
|
||||||
<div class="group mb-3 flex items-start gap-2 rounded bg-card-bg p-2.5">
|
<div class="group mb-3 flex items-start gap-2 rounded bg-card-bg p-2.5">
|
||||||
<span class="mt-0.5 text-xs text-muted whitespace-nowrap">{formatTime(entry.updatedAt)}</span>
|
<span class="mt-0.5 text-xs text-muted whitespace-nowrap">{formatTime(entry.updatedAt)}</span>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Topic, HistoryEntry } from "@ka-note/shared";
|
import type { Topic, HistoryEntry } from "@ka-note/shared";
|
||||||
|
import { get } from "svelte/store";
|
||||||
import { liveQuery } from "dexie";
|
import { liveQuery } from "dexie";
|
||||||
import { db } from "$lib/db/schema";
|
import { db } from "$lib/db/schema";
|
||||||
import {
|
import {
|
||||||
|
|
@ -11,6 +12,8 @@
|
||||||
} from "$lib/db/repositories";
|
} from "$lib/db/repositories";
|
||||||
import { today } from "$lib/db/helpers";
|
import { today } from "$lib/db/helpers";
|
||||||
import { processedTopicIds, collapsedTopicIds } from "$lib/stores/agenda";
|
import { processedTopicIds, collapsedTopicIds } from "$lib/stores/agenda";
|
||||||
|
import { scopeSettings } from "$lib/stores/scopeContext";
|
||||||
|
import { normalizeTitleAndBody } from "$lib/utils/titleUtils";
|
||||||
import HistoryItem from "./HistoryItem.svelte";
|
import HistoryItem from "./HistoryItem.svelte";
|
||||||
import LinkTitle from "./LinkTitle.svelte";
|
import LinkTitle from "./LinkTitle.svelte";
|
||||||
import MeetingControls from "./MeetingControls.svelte";
|
import MeetingControls from "./MeetingControls.svelte";
|
||||||
|
|
@ -109,9 +112,15 @@
|
||||||
editingTitle = true;
|
editingTitle = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTitleEdit() {
|
async function saveTitleEdit() {
|
||||||
if (titleInput.trim()) {
|
const raw = titleInput.trim();
|
||||||
updateTopic(topic.id, { title: titleInput.trim() });
|
if (raw) {
|
||||||
|
const { maxTitleLength } = get(scopeSettings);
|
||||||
|
const { title, body } = normalizeTitleAndBody(raw, '', maxTitleLength);
|
||||||
|
await updateTopic(topic.id, { title });
|
||||||
|
if (body) {
|
||||||
|
await createHistoryEntry(topic.id, today(), body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
editingTitle = false;
|
editingTitle = false;
|
||||||
}
|
}
|
||||||
|
|
@ -178,7 +187,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<LinkTitle text={topic.title} />
|
<LinkTitle text={topic.title} />
|
||||||
<button
|
<button
|
||||||
class="opacity-0 group-hover:opacity-100 ml-2.5 bg-transparent border-none text-[#aaa] cursor-pointer text-xs hover:text-white"
|
class="opacity-40 group-hover:opacity-100 ml-2.5 bg-transparent border-none text-[#aaa] cursor-pointer text-xs hover:text-white"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
startTitleEdit();
|
startTitleEdit();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
import { get } from 'svelte/store';
|
||||||
import { db } from './schema';
|
import { db } from './schema';
|
||||||
import { newId, now, today } from './helpers';
|
import { newId, now, today } from './helpers';
|
||||||
import { getAccessToken } from '$lib/auth/authStore';
|
import { getAccessToken } from '$lib/auth/authStore';
|
||||||
|
import { normalizeTitleAndBody } from '$lib/utils/titleUtils';
|
||||||
|
import { scopeSettings } from '$lib/stores/scopeContext';
|
||||||
import type { AgendaContext, Topic, HistoryEntry, Rating, ContextType, TopicStatus, ProjectMeta, PersonMeta, CompanyMeta, Page, Notebook, PageNotebook } from '@ka-note/shared';
|
import type { AgendaContext, Topic, HistoryEntry, Rating, ContextType, TopicStatus, ProjectMeta, PersonMeta, CompanyMeta, Page, Notebook, PageNotebook } from '@ka-note/shared';
|
||||||
|
|
||||||
// --- Contexts ---
|
// --- Contexts ---
|
||||||
|
|
@ -104,8 +107,10 @@ export async function getTopic(id: string): Promise<Topic | undefined> {
|
||||||
return db.topics.get(id);
|
return db.topics.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTopic(contextId: string, title: string): Promise<Topic> {
|
export async function createTopic(contextId: string, rawTitle: string, isPrivate = false): Promise<Topic> {
|
||||||
const existing = await getTopicsByContext(contextId);
|
const existing = await getTopicsByContext(contextId);
|
||||||
|
const { maxTitleLength } = get(scopeSettings);
|
||||||
|
const { title, body } = normalizeTitleAndBody(rawTitle, '', maxTitleLength);
|
||||||
const topic: Topic = {
|
const topic: Topic = {
|
||||||
id: newId(),
|
id: newId(),
|
||||||
contextId,
|
contextId,
|
||||||
|
|
@ -123,6 +128,9 @@ export async function createTopic(contextId: string, title: string): Promise<Top
|
||||||
await db.topics.update(t.id, { sortOrder: t.sortOrder + 1 });
|
await db.topics.update(t.id, { sortOrder: t.sortOrder + 1 });
|
||||||
}
|
}
|
||||||
await db.topics.put(topic);
|
await db.topics.put(topic);
|
||||||
|
if (body) {
|
||||||
|
await createHistoryEntry(topic.id, today(), body, null, false, isPrivate);
|
||||||
|
}
|
||||||
return topic;
|
return topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export interface ScopeSettings {
|
||||||
unknownColor: string;
|
unknownColor: string;
|
||||||
businessIcon: string;
|
businessIcon: string;
|
||||||
privateIcon: string;
|
privateIcon: string;
|
||||||
|
maxTitleLength: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS_KEY = 'ka-scope-settings';
|
const SETTINGS_KEY = 'ka-scope-settings';
|
||||||
|
|
@ -23,6 +24,7 @@ const defaults: ScopeSettings = {
|
||||||
unknownColor: '#555566',
|
unknownColor: '#555566',
|
||||||
businessIcon: '🏭',
|
businessIcon: '🏭',
|
||||||
privateIcon: '🏠',
|
privateIcon: '🏠',
|
||||||
|
maxTitleLength: 22,
|
||||||
};
|
};
|
||||||
|
|
||||||
function loadSettings(): ScopeSettings {
|
function loadSettings(): ScopeSettings {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
const DEFAULT_MAX_TITLE_LENGTH = 22;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If title exceeds maxLen, truncates it and prepends the full title to the body.
|
||||||
|
*/
|
||||||
|
export function normalizeTitleAndBody(
|
||||||
|
title: string,
|
||||||
|
body = '',
|
||||||
|
maxLen = DEFAULT_MAX_TITLE_LENGTH
|
||||||
|
): { title: string; body: string } {
|
||||||
|
if (title.length <= maxLen) return { title, body };
|
||||||
|
const short = title.slice(0, maxLen) + '…';
|
||||||
|
const newBody = body.trim() ? `${title}\n\n${body.trim()}` : title;
|
||||||
|
return { title: short, body: newBody };
|
||||||
|
}
|
||||||
|
|
@ -92,6 +92,7 @@
|
||||||
unknownColor: '#555566',
|
unknownColor: '#555566',
|
||||||
businessIcon: '🏭',
|
businessIcon: '🏭',
|
||||||
privateIcon: '🏠',
|
privateIcon: '🏠',
|
||||||
|
maxTitleLength: 22,
|
||||||
});
|
});
|
||||||
settings = { ...$scopeSettings };
|
settings = { ...$scopeSettings };
|
||||||
}
|
}
|
||||||
|
|
@ -168,6 +169,25 @@
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Title truncation -->
|
||||||
|
<section class="space-y-4">
|
||||||
|
<h2 class="text-sm font-semibold uppercase text-muted">Titel</h2>
|
||||||
|
<p class="text-xs text-muted">Titel, die diese Zeichenlänge überschreiten, werden automatisch gekürzt. Der vollständige Text wird als Notiz gespeichert.</p>
|
||||||
|
<div class="rounded border border-border bg-card-bg p-4 flex items-center gap-4">
|
||||||
|
<label class="text-xs text-muted w-40" for="maxTitleLength">Max. Titellänge</label>
|
||||||
|
<input
|
||||||
|
id="maxTitleLength"
|
||||||
|
type="number"
|
||||||
|
min="10"
|
||||||
|
max="100"
|
||||||
|
bind:value={settings.maxTitleLength}
|
||||||
|
oninput={save}
|
||||||
|
class="w-20 rounded border border-border bg-bg px-2 py-1 text-sm text-center focus:outline-none focus:ring-1 focus:ring-accent"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-muted">Zeichen</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- API Keys -->
|
<!-- API Keys -->
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<h2 class="text-sm font-semibold uppercase text-muted">API Keys</h2>
|
<h2 class="text-sm font-semibold uppercase text-muted">API Keys</h2>
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,22 @@ import type { AuthEnv } from '../middleware/auth.js';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
const JOURNAL_TOPIC_ID = 'daily-log-journal';
|
const JOURNAL_TOPIC_ID = 'daily-log-journal';
|
||||||
|
const MAX_TITLE_LENGTH = 22;
|
||||||
|
|
||||||
function now(): string {
|
function now(): string {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTitle(title: string, body: string): { title: string; text: string } {
|
||||||
|
if (title.length <= MAX_TITLE_LENGTH) {
|
||||||
|
const text = body.trim() ? `${title}\n\n${body}` : title;
|
||||||
|
return { title, text };
|
||||||
|
}
|
||||||
|
const short = title.slice(0, MAX_TITLE_LENGTH) + '…';
|
||||||
|
const fullBody = body.trim() ? `${title}\n\n${body}` : title;
|
||||||
|
return { title: short, text: `${short}\n\n${fullBody}` };
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureJournalTopic(userId: string): Promise<void> {
|
async function ensureJournalTopic(userId: string): Promise<void> {
|
||||||
const existing = await db.select({ id: topics.id }).from(topics)
|
const existing = await db.select({ id: topics.id }).from(topics)
|
||||||
.where(and(eq(topics.id, JOURNAL_TOPIC_ID), eq(topics.userId, userId)))
|
.where(and(eq(topics.id, JOURNAL_TOPIC_ID), eq(topics.userId, userId)))
|
||||||
|
|
@ -86,18 +97,18 @@ push.openapi(pushDailyLogRoute, async (c) => {
|
||||||
await ensureDailyLog(userId);
|
await ensureDailyLog(userId);
|
||||||
await ensureJournalTopic(userId);
|
await ensureJournalTopic(userId);
|
||||||
|
|
||||||
const { date, scope, title, body } = c.req.valid('json');
|
const { date, scope, title: rawTitle, body } = c.req.valid('json');
|
||||||
const isPrivate = scope === 'private';
|
const isPrivate = scope === 'private';
|
||||||
const titleMarker = `## ${title}`;
|
const { title, text } = normalizeTitle(rawTitle, body);
|
||||||
|
|
||||||
// Duplicate check: same date + title prefix in existing entries
|
// Duplicate check: same date + title as first line (plain or legacy ## prefix)
|
||||||
const existing = await db.select({ id: historyEntries.id })
|
const existing = await db.select({ id: historyEntries.id })
|
||||||
.from(historyEntries)
|
.from(historyEntries)
|
||||||
.where(and(
|
.where(and(
|
||||||
eq(historyEntries.userId, userId),
|
eq(historyEntries.userId, userId),
|
||||||
eq(historyEntries.topicId, JOURNAL_TOPIC_ID),
|
eq(historyEntries.topicId, JOURNAL_TOPIC_ID),
|
||||||
eq(historyEntries.date, date),
|
eq(historyEntries.date, date),
|
||||||
sql`${historyEntries.text} LIKE ${titleMarker + '%'}`,
|
sql`(${historyEntries.text} LIKE ${title + '%'} OR ${historyEntries.text} LIKE ${'## ' + title + '%'})`,
|
||||||
))
|
))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
|
|
@ -112,7 +123,6 @@ push.openapi(pushDailyLogRoute, async (c) => {
|
||||||
|
|
||||||
const sortOrder = (maxSortOrder?.val ?? 0) + 1;
|
const sortOrder = (maxSortOrder?.val ?? 0) + 1;
|
||||||
const id = randomUUID();
|
const id = randomUUID();
|
||||||
const text = body.trim() ? `${titleMarker}\n\n${body}` : titleMarker;
|
|
||||||
|
|
||||||
await db.insert(historyEntries).values({
|
await db.insert(historyEntries).values({
|
||||||
id,
|
id,
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Loading…
Reference in New Issue