Valider fase 19–20: arbeidsflaten + universell overføring bestått
Fikser funnet under validering: - Gjør collection-prop valgfri i alle trait-komponenter slik at de fungerer i personlig arbeidsflate uten collection-kontekst - Legger til null-guards for collection.id i alle derived-blokker og funksjoner som oppretter edges - Fjerner microsSinceUnixEpoch-rester fra STDB-migrasjonen — createdAt er nå et tall (Unix µs), ikke et objekt - Retter saveTimeout-lekkasje i collection-sida: timer ryddes nå ved navigasjon mellom samlinger - Fikser TypeScript-feil i editorial (number vs string, uoppnåelig 'scheduled'-sammenligning), studio (bigint vs number), RecordingTrait ($state-generics) - Typefeil redusert fra 55 → 4 (gjenværende er pre-eksisterende i mixer.ts/livekit.ts, ikke fase 19-20) Validert: Canvas pan/zoom, BlockShell, layout-persistering, snarveier, transfer service, alle panelreworks. Frontend bygger OK.
This commit is contained in:
parent
7b601ead1f
commit
15dd23b873
17 changed files with 65 additions and 56 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
|
|
||||||
const chatNodes = $derived.by(() => {
|
const chatNodes = $derived.by(() => {
|
||||||
const nodes: Node[] = [];
|
const nodes: Node[] = [];
|
||||||
|
if (!collection) return nodes;
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const edge of edgeStore.byTarget(collection.id)) {
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
||||||
if (edge.edgeType !== 'belongs_to') continue;
|
if (edge.edgeType !== 'belongs_to') continue;
|
||||||
|
|
@ -121,8 +122,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.sort((a, b) => {
|
nodes.sort((a, b) => {
|
||||||
const ta = a.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const ta = a.createdAt ?? 0;
|
||||||
const tb = b.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const tb = b.createdAt ?? 0;
|
||||||
return ta > tb ? 1 : ta < tb ? -1 : 0;
|
return ta > tb ? 1 : ta < tb ? -1 : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -189,8 +190,8 @@
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
function formatTime(node: Node): string {
|
function formatTime(node: Node): string {
|
||||||
if (!node.createdAt?.microsSinceUnixEpoch) return '';
|
if (!node.createdAt) return '';
|
||||||
const ms = Number(node.createdAt.microsSinceUnixEpoch / 1000n);
|
const ms = Math.floor(node.createdAt / 1000);
|
||||||
const date = new Date(ms);
|
const date = new Date(ms);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const isToday = date.toDateString() === now.toDateString();
|
const isToday = date.toDateString() === now.toDateString();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentItems = $derived.by((): ContentItem[] => {
|
const contentItems = $derived.by((): ContentItem[] => {
|
||||||
|
if (!collection) return [];
|
||||||
const items: ContentItem[] = [];
|
const items: ContentItem[] = [];
|
||||||
for (const edge of edgeStore.byTarget(collection.id)) {
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
||||||
if (edge.edgeType !== 'belongs_to') continue;
|
if (edge.edgeType !== 'belongs_to') continue;
|
||||||
|
|
@ -78,8 +79,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items.sort((a, b) => {
|
items.sort((a, b) => {
|
||||||
const ta = a.node.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const ta = a.node.createdAt ?? 0;
|
||||||
const tb = b.node.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const tb = b.node.createdAt ?? 0;
|
||||||
return tb > ta ? 1 : tb < ta ? -1 : 0;
|
return tb > ta ? 1 : tb < ta ? -1 : 0;
|
||||||
});
|
});
|
||||||
return items;
|
return items;
|
||||||
|
|
@ -131,8 +132,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodes.sort((a, b) => {
|
nodes.sort((a, b) => {
|
||||||
const ta = a.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const ta = a.createdAt ?? 0;
|
||||||
const tb = b.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const tb = b.createdAt ?? 0;
|
||||||
return tb > ta ? 1 : tb < ta ? -1 : 0;
|
return tb > ta ? 1 : tb < ta ? -1 : 0;
|
||||||
});
|
});
|
||||||
return nodes;
|
return nodes;
|
||||||
|
|
@ -217,7 +218,7 @@
|
||||||
let showCreateForm = $state(false);
|
let showCreateForm = $state(false);
|
||||||
|
|
||||||
async function handleCreateArticle() {
|
async function handleCreateArticle() {
|
||||||
if (!accessToken || !newTitle.trim() || isCreating) return;
|
if (!accessToken || !collection || !newTitle.trim() || isCreating) return;
|
||||||
isCreating = true;
|
isCreating = true;
|
||||||
try {
|
try {
|
||||||
const { node_id } = await createNode(accessToken, {
|
const { node_id } = await createNode(accessToken, {
|
||||||
|
|
@ -360,8 +361,8 @@
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
function formatTime(node: Node): string {
|
function formatTime(node: Node): string {
|
||||||
if (!node.createdAt?.microsSinceUnixEpoch) return '';
|
if (!node.createdAt) return '';
|
||||||
const ms = Number(node.createdAt.microsSinceUnixEpoch / 1000n);
|
const ms = Math.floor(node.createdAt / 1000);
|
||||||
const date = new Date(ms);
|
const date = new Date(ms);
|
||||||
return date.toLocaleDateString('nb-NO', { day: 'numeric', month: 'short' });
|
return date.toLocaleDateString('nb-NO', { day: 'numeric', month: 'short' });
|
||||||
}
|
}
|
||||||
|
|
@ -664,7 +665,7 @@
|
||||||
{#if publishTarget && accessToken && pubConfig}
|
{#if publishTarget && accessToken && pubConfig}
|
||||||
<PublishDialog
|
<PublishDialog
|
||||||
node={publishTarget}
|
node={publishTarget}
|
||||||
{collection}
|
collection={collection!}
|
||||||
pubConfig={pubConfig}
|
pubConfig={pubConfig}
|
||||||
{accessToken}
|
{accessToken}
|
||||||
onclose={() => { publishTarget = null; }}
|
onclose={() => { publishTarget = null; }}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|
@ -43,6 +43,7 @@
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
const columns = $derived.by(() => {
|
const columns = $derived.by(() => {
|
||||||
|
if (!collection) return ['todo', 'in_progress', 'done'];
|
||||||
try {
|
try {
|
||||||
const meta = JSON.parse(collection.metadata ?? '{}');
|
const meta = JSON.parse(collection.metadata ?? '{}');
|
||||||
const traitConf = meta.traits?.kanban;
|
const traitConf = meta.traits?.kanban;
|
||||||
|
|
@ -163,7 +164,7 @@
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragOverColumn = null;
|
dragOverColumn = null;
|
||||||
|
|
||||||
if (!draggedCard || !accessToken) return;
|
if (!draggedCard || !accessToken || !collection) return;
|
||||||
if (draggedCard.status === targetColumn) {
|
if (draggedCard.status === targetColumn) {
|
||||||
draggedCard = null;
|
draggedCard = null;
|
||||||
return;
|
return;
|
||||||
|
|
@ -205,7 +206,7 @@
|
||||||
let isCreating = $state(false);
|
let isCreating = $state(false);
|
||||||
|
|
||||||
async function handleCreateCard(column: string) {
|
async function handleCreateCard(column: string) {
|
||||||
if (!accessToken || !newCardTitle.trim() || isCreating) return;
|
if (!accessToken || !collection || !newCardTitle.trim() || isCreating) return;
|
||||||
|
|
||||||
isCreating = true;
|
isCreating = true;
|
||||||
try {
|
try {
|
||||||
|
|
@ -259,8 +260,8 @@
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
function formatTime(node: Node): string {
|
function formatTime(node: Node): string {
|
||||||
if (!node.createdAt?.microsSinceUnixEpoch) return '';
|
if (!node.createdAt) return '';
|
||||||
const ms = Number(node.createdAt.microsSinceUnixEpoch / 1000n);
|
const ms = Math.floor(node.createdAt / 1000);
|
||||||
const date = new Date(ms);
|
const date = new Date(ms);
|
||||||
return date.toLocaleDateString('nb-NO', { day: 'numeric', month: 'short' });
|
return date.toLocaleDateString('nb-NO', { day: 'numeric', month: 'short' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
} from '$lib/mixer';
|
} from '$lib/mixer';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +90,7 @@
|
||||||
// Derive room_id from the collection's communication node
|
// Derive room_id from the collection's communication node
|
||||||
// Pattern: "communication_{communication_node_id}"
|
// Pattern: "communication_{communication_node_id}"
|
||||||
const roomId = $derived.by(() => {
|
const roomId = $derived.by(() => {
|
||||||
|
if (!collection) return null;
|
||||||
for (const edge of edgeStore.byTarget(collection.id)) {
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
||||||
if (edge.edgeType !== 'belongs_to') continue;
|
if (edge.edgeType !== 'belongs_to') continue;
|
||||||
const node = nodeStore.get(edge.sourceId);
|
const node = nodeStore.get(edge.sourceId);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import TraitPanel from './TraitPanel.svelte';
|
import TraitPanel from './TraitPanel.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
/** Media nodes (episodes) belonging to this collection */
|
/** Media nodes (episodes) belonging to this collection */
|
||||||
const episodes = $derived.by(() => {
|
const episodes = $derived.by(() => {
|
||||||
const nodes: Node[] = [];
|
const nodes: Node[] = [];
|
||||||
|
if (!collection) return nodes;
|
||||||
for (const edge of edgeStore.byTarget(collection.id)) {
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
||||||
if (edge.edgeType !== 'belongs_to') continue;
|
if (edge.edgeType !== 'belongs_to') continue;
|
||||||
const node = nodeStore.get(edge.sourceId);
|
const node = nodeStore.get(edge.sourceId);
|
||||||
|
|
@ -31,8 +32,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodes.sort((a, b) => {
|
nodes.sort((a, b) => {
|
||||||
const ta = a.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const ta = a.createdAt ?? 0;
|
||||||
const tb = b.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const tb = b.createdAt ?? 0;
|
||||||
return tb > ta ? 1 : tb < ta ? -1 : 0;
|
return tb > ta ? 1 : tb < ta ? -1 : 0;
|
||||||
});
|
});
|
||||||
return nodes;
|
return nodes;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import TraitPanel from './TraitPanel.svelte';
|
import TraitPanel from './TraitPanel.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,14 +45,14 @@
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
{#if requireApproval}
|
{#if requireApproval}
|
||||||
<a
|
<a
|
||||||
href="/editorial/{collection.id}"
|
href="/editorial/{collection?.id ?? ''}"
|
||||||
class="inline-flex items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700"
|
class="inline-flex items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-700"
|
||||||
>
|
>
|
||||||
Redaksjonell arbeidsflate
|
Redaksjonell arbeidsflate
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
<a
|
<a
|
||||||
href="/collection/{collection.id}/forside"
|
href="/collection/{collection?.id ?? ''}/forside"
|
||||||
class="inline-flex items-center gap-1 rounded-lg bg-emerald-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-emerald-700"
|
class="inline-flex items-center gap-1 rounded-lg bg-emerald-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-emerald-700"
|
||||||
>
|
>
|
||||||
Rediger forside
|
Rediger forside
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,14 @@
|
||||||
} from '$lib/livekit';
|
} from '$lib/livekit';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { collection, config, accessToken }: Props = $props();
|
let { collection, config, accessToken }: Props = $props();
|
||||||
|
|
||||||
let status: RoomStatus = $state('disconnected');
|
let status = $state<RoomStatus>('disconnected');
|
||||||
let participants: LiveKitParticipant[] = $state([]);
|
let participants: LiveKitParticipant[] = $state([]);
|
||||||
let localIdentity: string = $state('');
|
let localIdentity: string = $state('');
|
||||||
let error: string | null = $state(null);
|
let error: string | null = $state(null);
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
/** Find communication nodes linked to this collection */
|
/** Find communication nodes linked to this collection */
|
||||||
const communicationNodes = $derived.by(() => {
|
const communicationNodes = $derived.by(() => {
|
||||||
const nodes: Node[] = [];
|
const nodes: Node[] = [];
|
||||||
|
if (!collection) return nodes;
|
||||||
for (const edge of edgeStore.byTarget(collection.id)) {
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
||||||
if (edge.edgeType !== 'belongs_to') continue;
|
if (edge.edgeType !== 'belongs_to') continue;
|
||||||
const node = nodeStore.get(edge.sourceId);
|
const node = nodeStore.get(edge.sourceId);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import TraitPanel from './TraitPanel.svelte';
|
import TraitPanel from './TraitPanel.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
/** Build the feed URL from publishing slug if available */
|
/** Build the feed URL from publishing slug if available */
|
||||||
const feedUrl = $derived.by(() => {
|
const feedUrl = $derived.by(() => {
|
||||||
|
if (!collection) return '';
|
||||||
try {
|
try {
|
||||||
const meta = JSON.parse(collection.metadata ?? '{}');
|
const meta = JSON.parse(collection.metadata ?? '{}');
|
||||||
const slug = meta.traits?.publishing?.slug;
|
const slug = meta.traits?.publishing?.slug;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
isViewer?: boolean;
|
isViewer?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
|
|
||||||
// Parse pad config from collection metadata
|
// Parse pad config from collection metadata
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const meta = collection.metadata ? JSON.parse(collection.metadata) : {};
|
const meta = collection?.metadata ? JSON.parse(collection.metadata) : {};
|
||||||
const mixerMeta = meta?.mixer ?? {};
|
const mixerMeta = meta?.mixer ?? {};
|
||||||
const rawPads: PadConfig[] = mixerMeta?.pads ?? [];
|
const rawPads: PadConfig[] = mixerMeta?.pads ?? [];
|
||||||
|
|
||||||
|
|
@ -212,10 +212,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function savePadConfigs(configs: PadConfig[]) {
|
async function savePadConfigs(configs: PadConfig[]) {
|
||||||
if (!accessToken) return;
|
if (!accessToken || !collection) return;
|
||||||
// Filter out empty pads for clean storage
|
// Filter out empty pads for clean storage
|
||||||
const padsToSave = configs.filter(p => p.cas_hash);
|
const padsToSave = configs.filter(p => p.cas_hash);
|
||||||
const meta = collection.metadata ? JSON.parse(collection.metadata) : {};
|
const meta = collection?.metadata ? JSON.parse(collection.metadata) : {};
|
||||||
const mixer = meta.mixer ?? {};
|
const mixer = meta.mixer ?? {};
|
||||||
mixer.pads = padsToSave.length > 0 ? configs : undefined;
|
mixer.pads = padsToSave.length > 0 ? configs : undefined;
|
||||||
meta.mixer = mixer;
|
meta.mixer = mixer;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
|
|
||||||
const audioNodes = $derived.by(() => {
|
const audioNodes = $derived.by(() => {
|
||||||
const nodes: Node[] = [];
|
const nodes: Node[] = [];
|
||||||
|
if (!collection) return nodes;
|
||||||
for (const edge of edgeStore.byTarget(collection.id)) {
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
||||||
if (edge.edgeType !== 'belongs_to') continue;
|
if (edge.edgeType !== 'belongs_to') continue;
|
||||||
const node = nodeStore.get(edge.sourceId);
|
const node = nodeStore.get(edge.sourceId);
|
||||||
|
|
@ -55,8 +56,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nodes.sort((a, b) => {
|
nodes.sort((a, b) => {
|
||||||
const ta = a.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const ta = a.createdAt ?? 0;
|
||||||
const tb = b.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const tb = b.createdAt ?? 0;
|
||||||
return tb > ta ? 1 : tb < ta ? -1 : 0;
|
return tb > ta ? 1 : tb < ta ? -1 : 0;
|
||||||
});
|
});
|
||||||
return nodes;
|
return nodes;
|
||||||
|
|
@ -121,8 +122,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(node: Node): string {
|
function formatTime(node: Node): string {
|
||||||
if (!node.createdAt?.microsSinceUnixEpoch) return '';
|
if (!node.createdAt) return '';
|
||||||
const ms = Number(node.createdAt.microsSinceUnixEpoch / 1000n);
|
const ms = Math.floor(node.createdAt / 1000);
|
||||||
const date = new Date(ms);
|
const date = new Date(ms);
|
||||||
return date.toLocaleDateString('nb-NO', { day: 'numeric', month: 'short' });
|
return date.toLocaleDateString('nb-NO', { day: 'numeric', month: 'short' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import TraitPanel from './TraitPanel.svelte';
|
import TraitPanel from './TraitPanel.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection?: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,8 @@
|
||||||
|
|
||||||
// Sort by created_at ascending (oldest first, like a chat)
|
// Sort by created_at ascending (oldest first, like a chat)
|
||||||
nodes.sort((a, b) => {
|
nodes.sort((a, b) => {
|
||||||
const ta = a.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const ta = a.createdAt ?? 0;
|
||||||
const tb = b.createdAt?.microsSinceUnixEpoch ?? 0n;
|
const tb = b.createdAt ?? 0;
|
||||||
return ta > tb ? 1 : ta < tb ? -1 : 0;
|
return ta > tb ? 1 : ta < tb ? -1 : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -113,8 +113,8 @@
|
||||||
|
|
||||||
/** Format timestamp for display */
|
/** Format timestamp for display */
|
||||||
function formatTime(node: Node): string {
|
function formatTime(node: Node): string {
|
||||||
if (!node.createdAt?.microsSinceUnixEpoch) return '';
|
if (!node.createdAt) return '';
|
||||||
const ms = Number(node.createdAt.microsSinceUnixEpoch / 1000n);
|
const ms = Math.floor(node.createdAt / 1000);
|
||||||
const date = new Date(ms);
|
const date = new Date(ms);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const isToday = date.toDateString() === now.toDateString();
|
const isToday = date.toDateString() === now.toDateString();
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,13 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let saveTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
// Reset when collection changes
|
// Reset when collection changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const _id = collectionId;
|
const _id = collectionId;
|
||||||
layoutInitialized = false;
|
layoutInitialized = false;
|
||||||
|
clearTimeout(saveTimeout);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Convert layout panels to CanvasObjects for the Canvas component */
|
/** Convert layout panels to CanvasObjects for the Canvas component */
|
||||||
|
|
@ -148,8 +151,6 @@
|
||||||
// Layout persistence
|
// Layout persistence
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
let saveTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
||||||
|
|
||||||
/** Persist layout to user's edge metadata (debounced) */
|
/** Persist layout to user's edge metadata (debounced) */
|
||||||
function persistLayout() {
|
function persistLayout() {
|
||||||
if (!accessToken || !userEdge) return;
|
if (!accessToken || !userEdge) return;
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
feedback: string | null;
|
feedback: string | null;
|
||||||
edgeId: string;
|
edgeId: string;
|
||||||
edgeMeta: Record<string, unknown>;
|
edgeMeta: Record<string, unknown>;
|
||||||
createdAt: string;
|
createdAt: number;
|
||||||
discussionIds: string[];
|
discussionIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
feedback: (meta.feedback as string) ?? null,
|
feedback: (meta.feedback as string) ?? null,
|
||||||
edgeId: edge.id,
|
edgeId: edge.id,
|
||||||
edgeMeta: meta,
|
edgeMeta: meta,
|
||||||
createdAt: node.createdAt ?? '',
|
createdAt: node.createdAt ?? 0,
|
||||||
discussionIds
|
discussionIds
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -143,9 +143,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort each column: newest first for pending, oldest first for others
|
// Sort each column: newest first for pending, oldest first for others
|
||||||
grouped.pending.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
grouped.pending.sort((a, b) => b.createdAt - a.createdAt);
|
||||||
for (const col of ['in_review', 'approved', 'scheduled'] as Column[]) {
|
for (const col of ['in_review', 'approved', 'scheduled'] as Column[]) {
|
||||||
grouped[col].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
grouped[col].sort((a, b) => a.createdAt - b.createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
return grouped;
|
return grouped;
|
||||||
|
|
@ -202,12 +202,13 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Map column back to actual status for the edge
|
// Map column back to actual status for the edge
|
||||||
const newStatus = targetColumn === 'scheduled' ? 'approved' : targetColumn;
|
// Note: 'scheduled' case handled above with early return + dialog
|
||||||
|
const newStatus = targetColumn;
|
||||||
|
|
||||||
const newMeta: Record<string, unknown> = { ...card.edgeMeta, status: newStatus };
|
const newMeta: Record<string, unknown> = { ...card.edgeMeta, status: newStatus };
|
||||||
|
|
||||||
// If moving away from scheduled, remove publish_at
|
// If moving away from scheduled, remove publish_at
|
||||||
if (card.status === 'scheduled' && targetColumn !== 'scheduled') {
|
if (card.status === 'scheduled') {
|
||||||
delete newMeta.publish_at;
|
delete newMeta.publish_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
// Version history: processed nodes derived from this media node
|
// Version history: processed nodes derived from this media node
|
||||||
const versions = $derived.by(() => {
|
const versions = $derived.by(() => {
|
||||||
if (!connected || !mediaNodeId) return [];
|
if (!connected || !mediaNodeId) return [];
|
||||||
const nodes: { id: string; title: string; createdAt: bigint }[] = [];
|
const nodes: { id: string; title: string; createdAt: number }[] = [];
|
||||||
for (const edge of edgeStore.byTarget(mediaNodeId)) {
|
for (const edge of edgeStore.byTarget(mediaNodeId)) {
|
||||||
if (edge.edgeType !== 'derived_from') continue;
|
if (edge.edgeType !== 'derived_from') continue;
|
||||||
const node = nodeStore.get(edge.sourceId);
|
const node = nodeStore.get(edge.sourceId);
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
title: node.title ?? 'Prosessert',
|
title: node.title ?? 'Prosessert',
|
||||||
createdAt: node.createdAt?.microsSinceUnixEpoch ?? 0n,
|
createdAt: node.createdAt ?? 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -303,8 +303,7 @@ med spesifikasjon for det som trenger en dedikert sesjon.
|
||||||
- [x] 23.6 Valider fase 13–14 (traits + publisering): trait-validering, pakkevelger, Tera-templates, HTML-rendering, forside, slot-håndtering, redaksjonell flyt, planlagt publisering, A/B-testing.
|
- [x] 23.6 Valider fase 13–14 (traits + publisering): trait-validering, pakkevelger, Tera-templates, HTML-rendering, forside, slot-håndtering, redaksjonell flyt, planlagt publisering, A/B-testing.
|
||||||
- [x] 23.7 Valider fase 15–16 (admin + lydmixer): systemvarsler, graceful shutdown, jobbkø-oversikt, ressursstyring, serverhelse, Web Audio mixer, delt kontroll, sound pads, EQ, stemmeeffekter.
|
- [x] 23.7 Valider fase 15–16 (admin + lydmixer): systemvarsler, graceful shutdown, jobbkø-oversikt, ressursstyring, serverhelse, Web Audio mixer, delt kontroll, sound pads, EQ, stemmeeffekter.
|
||||||
- [x] 23.8 Valider fase 17–18 (lydstudio-utbedring + AI-verktøy): responsivt layout, FFmpeg-validering, fade/silence, AI-presets, direction-logikk, drag-and-drop integrasjon.
|
- [x] 23.8 Valider fase 17–18 (lydstudio-utbedring + AI-verktøy): responsivt layout, FFmpeg-validering, fade/silence, AI-presets, direction-logikk, drag-and-drop integrasjon.
|
||||||
- [~] 23.9 Valider fase 19–20 (arbeidsflaten + universell overføring): canvas pan/zoom, BlockShell, layout-persistering, snarveier, transfer service, alle panelreworks (chat, kanban, kalender, editor, studio).
|
- [x] 23.9 Valider fase 19–20 (arbeidsflaten + universell overføring): canvas pan/zoom, BlockShell, layout-persistering, snarveier, transfer service, alle panelreworks (chat, kanban, kalender, editor, studio).
|
||||||
> Påbegynt: 2026-03-18T15:50
|
|
||||||
- [ ] 23.10 Valider fase 21 (CLI-verktøy): kjør hvert synops-*-verktøy, verifiser --help, --payload-json, output-format, feilhåndtering, synops-common integrasjon.
|
- [ ] 23.10 Valider fase 21 (CLI-verktøy): kjør hvert synops-*-verktøy, verifiser --help, --payload-json, output-format, feilhåndtering, synops-common integrasjon.
|
||||||
- [ ] 23.11 Valider fase 22 (STDB-migrering): WebSocket-sanntid fungerer, PG LISTEN/NOTIFY-triggere, ingen STDB-rester i aktiv kode/konfig.
|
- [ ] 23.11 Valider fase 22 (STDB-migrering): WebSocket-sanntid fungerer, PG LISTEN/NOTIFY-triggere, ingen STDB-rester i aktiv kode/konfig.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue