snapshot
This commit is contained in:
+28
-23
@@ -80,35 +80,40 @@ export function NotesApp() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6 p-6">
|
<div className="mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 py-4 sm:px-5">
|
||||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
<header className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end sm:gap-3">
|
||||||
<div className="grid w-full gap-2 sm:max-w-md">
|
<div className="min-w-0 flex-1 space-y-1.5">
|
||||||
<Label htmlFor="search">Suche</Label>
|
<Label htmlFor="search" className="text-xs text-muted-foreground">
|
||||||
|
Suche
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="search"
|
id="search"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
placeholder="Titel, Tags oder Notiztext …"
|
placeholder="Ohne Prefix: Titel & Tags · tag:x · title:y · content:z"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
className="h-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col gap-2 sm:w-auto sm:items-end">
|
<div className="w-full min-w-[min(100%,220px)] space-y-1.5 sm:w-72">
|
||||||
<div className="grid w-full gap-2 sm:w-[280px]">
|
<Label htmlFor="sort" className="text-xs text-muted-foreground">
|
||||||
<Label>Sortierung</Label>
|
Sortierung
|
||||||
<Select value={sort} onValueChange={v => setSort(v as SortValue)}>
|
</Label>
|
||||||
<SelectTrigger>
|
<Select value={sort} onValueChange={v => setSort(v as SortValue)}>
|
||||||
<SelectValue placeholder="Sortierung" />
|
<SelectTrigger id="sort" className="h-9 w-full">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Sortierung" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{SORT_OPTIONS.map(o => (
|
<SelectContent>
|
||||||
<SelectItem key={o.value} value={o.value}>
|
{SORT_OPTIONS.map(o => (
|
||||||
{o.label}
|
<SelectItem key={o.value} value={o.value}>
|
||||||
</SelectItem>
|
{o.label}
|
||||||
))}
|
</SelectItem>
|
||||||
</SelectContent>
|
))}
|
||||||
</Select>
|
</SelectContent>
|
||||||
</div>
|
</Select>
|
||||||
<Button type="button" className="w-full sm:w-auto" onClick={() => void createNew()}>
|
</div>
|
||||||
|
<div className="flex sm:shrink-0">
|
||||||
|
<Button type="button" className="h-9 w-full sm:w-auto" onClick={() => void createNew()}>
|
||||||
Neu
|
Neu
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +122,7 @@ export function NotesApp() {
|
|||||||
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||||
{loading ? <p className="text-sm text-muted-foreground">Lade Snippets …</p> : null}
|
{loading ? <p className="text-sm text-muted-foreground">Lade Snippets …</p> : null}
|
||||||
|
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-4">
|
||||||
{!loading && snippets.length === 0 ? (
|
{!loading && snippets.length === 0 ? (
|
||||||
<p className="text-center text-sm text-muted-foreground">Keine Snippets. Mit „Neu“ starten.</p>
|
<p className="text-center text-sm text-muted-foreground">Keine Snippets. Mit „Neu“ starten.</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import type { Snippet } from "@/types/snippet";
|
import type { Snippet } from "@/types/snippet";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||||
@@ -8,6 +8,12 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
|
|
||||||
marked.setOptions({ gfm: true });
|
marked.setOptions({ gfm: true });
|
||||||
|
|
||||||
|
/** API `null` / fehlender Wert → leerer String, damit kontrollierte Inputs nie `null` anzeigen. */
|
||||||
|
function stringFieldForForm(v: string | null | undefined): string {
|
||||||
|
if (v == null) return "";
|
||||||
|
return typeof v === "string" ? v : "";
|
||||||
|
}
|
||||||
|
|
||||||
function tagsFromString(s: string): string[] {
|
function tagsFromString(s: string): string[] {
|
||||||
return s
|
return s
|
||||||
.split(",")
|
.split(",")
|
||||||
@@ -30,23 +36,36 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function SnippetCard({ snippet, onReplace, onRemove }: Props) {
|
export function SnippetCard({ snippet, onReplace, onRemove }: Props) {
|
||||||
const pristineEmpty = !snippet.body && !snippet.title && snippet.tags.length === 0;
|
const pristineEmpty =
|
||||||
|
!stringFieldForForm(snippet.body) && (snippet.title == null || snippet.title.trim() === "") && (snippet.tags?.length ?? 0) === 0;
|
||||||
const [editing, setEditing] = useState(pristineEmpty);
|
const [editing, setEditing] = useState(pristineEmpty);
|
||||||
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [body, setBody] = useState(snippet.body);
|
const [body, setBody] = useState(() => stringFieldForForm(snippet.body));
|
||||||
const [title, setTitle] = useState(snippet.title ?? "");
|
const [title, setTitle] = useState(() => stringFieldForForm(snippet.title));
|
||||||
const [tagsStr, setTagsStr] = useState(snippet.tags.join(", "));
|
const [tagsStr, setTagsStr] = useState(() => (snippet.tags ?? []).join(", "));
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const html = useMemo(() => marked.parse(editing ? body : snippet.body) as string, [editing, body, snippet.body]);
|
const html = useMemo(
|
||||||
|
() => marked.parse(editing ? body : stringFieldForForm(snippet.body)) as string,
|
||||||
|
[editing, body, snippet.body],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editing) return;
|
||||||
|
setBody(stringFieldForForm(snippet.body));
|
||||||
|
setTitle(stringFieldForForm(snippet.title));
|
||||||
|
setTagsStr((snippet.tags ?? []).join(", "));
|
||||||
|
}, [editing, snippet.id, snippet.updated_at, snippet.body, snippet.title, snippet.tags]);
|
||||||
|
|
||||||
const startEdit = () => {
|
const startEdit = () => {
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
|
setShowPreview(false);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
setBody(snippet.body);
|
setBody(stringFieldForForm(snippet.body));
|
||||||
setTitle(snippet.title ?? "");
|
setTitle(stringFieldForForm(snippet.title));
|
||||||
setTagsStr(snippet.tags.join(", "));
|
setTagsStr((snippet.tags ?? []).join(", "));
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,45 +122,76 @@ export function SnippetCard({ snippet, onReplace, onRemove }: Props) {
|
|||||||
}
|
}
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
setBody(snippet.body);
|
setBody(stringFieldForForm(snippet.body));
|
||||||
setTitle(snippet.title ?? "");
|
setTitle(stringFieldForForm(snippet.title));
|
||||||
setTagsStr(snippet.tags.join(", "));
|
setTagsStr((snippet.tags ?? []).join(", "));
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const markDirty = () => setDirty(true);
|
const markDirty = () => setDirty(true);
|
||||||
|
|
||||||
|
const mdBodyClass =
|
||||||
|
"snippet-md max-h-[min(55vh,420px)] overflow-auto text-sm leading-relaxed [&_a]:text-primary [&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground/40 [&_blockquote]:pl-3 [&_blockquote]:italic [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_h1]:mb-2 [&_h1]:text-xl [&_h1]:font-semibold [&_h2]:mb-2 [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:mb-1 [&_h3]:text-base [&_h3]:font-semibold [&_li]:ml-4 [&_ol]:list-decimal [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_ul]:list-disc";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="text-left">
|
<Card className="gap-3 py-4 text-left shadow-sm">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="px-4 pb-0 pt-0">
|
||||||
<div className="min-h-[180px] rounded-lg border bg-muted/30 p-4">
|
<div className="min-h-[140px] rounded-md border bg-muted/30 p-2">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<div className="space-y-2">
|
||||||
<Textarea
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
value={body}
|
<Button
|
||||||
onChange={e => {
|
type="button"
|
||||||
markDirty();
|
variant="outline"
|
||||||
setBody(e.target.value);
|
size="sm"
|
||||||
}}
|
className="h-8"
|
||||||
className="min-h-[220px] resize-y font-mono text-sm"
|
onClick={() => setShowPreview(v => !v)}
|
||||||
placeholder="Markdown …"
|
>
|
||||||
/>
|
{showPreview ? "Vorschau ausblenden" : "Vorschau"}
|
||||||
<div
|
</Button>
|
||||||
className="snippet-md max-h-[420px] min-h-[120px] overflow-auto rounded-md border bg-background p-3 text-sm leading-relaxed [&_a]:text-primary [&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground/40 [&_blockquote]:pl-3 [&_blockquote]:italic [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_h1]:mb-2 [&_h1]:text-xl [&_h1]:font-semibold [&_h2]:mb-2 [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:mb-1 [&_h3]:text-base [&_h3]:font-semibold [&_li]:ml-4 [&_ol]:list-decimal [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_ul]:list-disc"
|
</div>
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
<div className={showPreview ? "grid gap-3 lg:grid-cols-2" : "grid gap-3"}>
|
||||||
/>
|
<Textarea
|
||||||
|
value={body}
|
||||||
|
onChange={e => {
|
||||||
|
markDirty();
|
||||||
|
setBody(e.target.value);
|
||||||
|
}}
|
||||||
|
className="min-h-[200px] resize-y font-mono text-sm"
|
||||||
|
placeholder="Markdown …"
|
||||||
|
/>
|
||||||
|
{showPreview ? (
|
||||||
|
<div
|
||||||
|
className={`${mdBodyClass} min-h-[120px] rounded-md border bg-background p-2`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="snippet-md max-h-[420px] overflow-auto text-sm leading-relaxed [&_a]:text-primary [&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground/40 [&_blockquote]:pl-3 [&_blockquote]:italic [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_h1]:mb-2 [&_h1]:text-xl [&_h1]:font-semibold [&_h2]:mb-2 [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:mb-1 [&_h3]:text-base [&_h3]:font-semibold [&_li]:ml-4 [&_ol]:list-decimal [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_ul]:list-disc"
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
title="Zum Bearbeiten klicken"
|
||||||
|
className={`${mdBodyClass} cursor-pointer rounded-md p-2 outline-none ring-offset-background hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-ring`}
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
onClick={e => {
|
||||||
|
if ((e.target as HTMLElement).closest("a")) return;
|
||||||
|
startEdit();
|
||||||
|
}}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
startEdit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{error ? <p className="mt-2 text-sm text-destructive">{error}</p> : null}
|
{error ? <p className="mt-2 text-sm text-destructive">{error}</p> : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col gap-4 border-t pt-6 sm:flex-row sm:items-end sm:justify-between">
|
<CardFooter className="flex flex-col gap-3 border-t px-4 pb-3 pt-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<div className="grid w-full gap-3 sm:max-w-xl">
|
<div className="grid w-full gap-2 sm:max-w-xl">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
@@ -163,7 +213,13 @@ export function SnippetCard({ snippet, onReplace, onRemove }: Props) {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1 text-sm">
|
<div className="space-y-1 text-sm">
|
||||||
<div className="font-medium">{snippet.title?.trim() ? snippet.title : <span className="text-muted-foreground">Ohne Titel</span>}</div>
|
<div>
|
||||||
|
{snippet.title?.trim() ? (
|
||||||
|
<span className="font-medium text-foreground">{snippet.title}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm font-normal italic tracking-wide text-muted-foreground">(Kein Titel)</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="text-muted-foreground">{snippet.tags.length ? snippet.tags.join(", ") : "Keine Tags"}</div>
|
<div className="text-muted-foreground">{snippet.tags.length ? snippet.tags.join(", ") : "Keine Tags"}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+96
-18
@@ -15,6 +15,38 @@ function escapeLike(raw: string): string {
|
|||||||
return raw.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
return raw.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `tag:x`, `title:y`, `content:z` (case-insensitive); remainder = default (title OR tag names, LIKE). All parts AND. */
|
||||||
|
function parseSearchQuery(raw: string): {
|
||||||
|
defaultText: string | null;
|
||||||
|
tagTerms: string[];
|
||||||
|
titleTerms: string[];
|
||||||
|
contentTerms: string[];
|
||||||
|
} {
|
||||||
|
const tagTerms: string[] = [];
|
||||||
|
const titleTerms: string[] = [];
|
||||||
|
const contentTerms: string[] = [];
|
||||||
|
const re = /(?:^|\s)(tag|title|content):(\S+)/gi;
|
||||||
|
const spans: [number, number][] = [];
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = re.exec(raw)) !== null) {
|
||||||
|
const kind = m[1].toLowerCase();
|
||||||
|
const val = m[2];
|
||||||
|
if (!val) continue;
|
||||||
|
const escaped = escapeLike(val);
|
||||||
|
if (kind === "tag") tagTerms.push(escaped);
|
||||||
|
else if (kind === "title") titleTerms.push(escaped);
|
||||||
|
else if (kind === "content") contentTerms.push(escaped);
|
||||||
|
spans.push([m.index, m.index + m[0].length]);
|
||||||
|
}
|
||||||
|
let rest = raw;
|
||||||
|
for (const [start, end] of [...spans].sort((a, b) => b[0] - a[0])) {
|
||||||
|
rest = `${rest.slice(0, start)} ${rest.slice(end)}`;
|
||||||
|
}
|
||||||
|
const defaultTrim = rest.replace(/\s+/g, " ").trim();
|
||||||
|
const defaultText = defaultTrim === "" ? null : escapeLike(defaultTrim);
|
||||||
|
return { defaultText, tagTerms, titleTerms, contentTerms };
|
||||||
|
}
|
||||||
|
|
||||||
function nowIso(): string {
|
function nowIso(): string {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
@@ -101,22 +133,59 @@ export function listSnippets(q: string | null, sort: string | null): SnippetRow[
|
|||||||
)
|
)
|
||||||
.all() as typeof rows;
|
.all() as typeof rows;
|
||||||
} else {
|
} else {
|
||||||
const term = escapeLike(trimmed);
|
const parsed = parseSearchQuery(trimmed);
|
||||||
rows = database
|
const conditions: string[] = [];
|
||||||
.query(
|
const params: string[] = [];
|
||||||
`SELECT s.id, s.title, s.body, s.sort_order, s.created_at, s.updated_at
|
|
||||||
FROM snippets s
|
if (parsed.defaultText !== null) {
|
||||||
WHERE s.title LIKE '%' || ?1 || '%' ESCAPE '\\'
|
conditions.push(`(
|
||||||
OR s.body LIKE '%' || ?1 || '%' ESCAPE '\\'
|
(s.title IS NOT NULL AND s.title LIKE '%' || ? || '%' ESCAPE '\\')
|
||||||
OR EXISTS (
|
OR EXISTS (
|
||||||
SELECT 1 FROM snippet_tags st
|
SELECT 1 FROM snippet_tags st
|
||||||
JOIN tags t ON t.id = st.tag_id
|
JOIN tags t ON t.id = st.tag_id
|
||||||
WHERE st.snippet_id = s.id
|
WHERE st.snippet_id = s.id
|
||||||
AND t.name LIKE '%' || ?1 || '%' ESCAPE '\\'
|
AND t.name LIKE '%' || ? || '%' ESCAPE '\\'
|
||||||
)
|
)
|
||||||
${order}`,
|
)`);
|
||||||
)
|
params.push(parsed.defaultText, parsed.defaultText);
|
||||||
.all(term) as typeof rows;
|
}
|
||||||
|
for (const term of parsed.tagTerms) {
|
||||||
|
conditions.push(`EXISTS (
|
||||||
|
SELECT 1 FROM snippet_tags st
|
||||||
|
JOIN tags t ON t.id = st.tag_id
|
||||||
|
WHERE st.snippet_id = s.id
|
||||||
|
AND t.name LIKE '%' || ? || '%' ESCAPE '\\'
|
||||||
|
)`);
|
||||||
|
params.push(term);
|
||||||
|
}
|
||||||
|
for (const term of parsed.titleTerms) {
|
||||||
|
conditions.push(`(s.title IS NOT NULL AND s.title LIKE '%' || ? || '%' ESCAPE '\\')`);
|
||||||
|
params.push(term);
|
||||||
|
}
|
||||||
|
for (const term of parsed.contentTerms) {
|
||||||
|
conditions.push(`(s.body LIKE '%' || ? || '%' ESCAPE '\\')`);
|
||||||
|
params.push(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.length === 0) {
|
||||||
|
rows = database
|
||||||
|
.query(
|
||||||
|
`SELECT s.id, s.title, s.body, s.sort_order, s.created_at, s.updated_at
|
||||||
|
FROM snippets s
|
||||||
|
${order}`,
|
||||||
|
)
|
||||||
|
.all() as typeof rows;
|
||||||
|
} else {
|
||||||
|
const where = conditions.join(" AND ");
|
||||||
|
rows = database
|
||||||
|
.query(
|
||||||
|
`SELECT s.id, s.title, s.body, s.sort_order, s.created_at, s.updated_at
|
||||||
|
FROM snippets s
|
||||||
|
WHERE ${where}
|
||||||
|
${order}`,
|
||||||
|
)
|
||||||
|
.all(...params) as typeof rows;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = rows.map(r => r.id);
|
const ids = rows.map(r => r.id);
|
||||||
@@ -174,7 +243,12 @@ export function createSnippet(input: {
|
|||||||
}): SnippetRow {
|
}): SnippetRow {
|
||||||
const database = getDb();
|
const database = getDb();
|
||||||
const t = nowIso();
|
const t = nowIso();
|
||||||
const title = input.title === undefined || input.title === "" ? null : String(input.title);
|
const title =
|
||||||
|
input.title === undefined || input.title === null
|
||||||
|
? null
|
||||||
|
: String(input.title).trim() === ""
|
||||||
|
? null
|
||||||
|
: String(input.title).trim();
|
||||||
const body = input.body ?? "";
|
const body = input.body ?? "";
|
||||||
const sortOrder = typeof input.sort_order === "number" ? input.sort_order : Date.now();
|
const sortOrder = typeof input.sort_order === "number" ? input.sort_order : Date.now();
|
||||||
|
|
||||||
@@ -210,7 +284,11 @@ export function updateSnippet(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const title =
|
const title =
|
||||||
input.title === undefined ? current.title : input.title === "" || input.title === null ? null : String(input.title);
|
input.title === undefined
|
||||||
|
? current.title
|
||||||
|
: input.title === null || String(input.title).trim() === ""
|
||||||
|
? null
|
||||||
|
: String(input.title).trim();
|
||||||
const body = input.body === undefined ? current.body : String(input.body);
|
const body = input.body === undefined ? current.body : String(input.body);
|
||||||
const sort_order = input.sort_order === undefined ? current.sort_order : input.sort_order;
|
const sort_order = input.sort_order === undefined ? current.sort_order : input.sort_order;
|
||||||
const t = nowIso();
|
const t = nowIso();
|
||||||
|
|||||||
Reference in New Issue
Block a user