Files
immich-folio/app/admin/components/journal/blockOps.ts
Ralksta ad8b09700b release: v0.19.0
Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of
[CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md)
for the full list.

Highlights: client proofing links (#683), custom content pages (#738),
asset picker search (#740), concurrent-edit detection (#742), env-locked
settings (#739), an accessibility baseline (#741), three security fixes
with advisories (#737, #755, #759), and a pre-release quality pass
(#780–#797).

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
2026-09-29 22:50:30 +02:00

87 lines
3.1 KiB
TypeScript

import type { JournalBlock } from '@/lib/journal';
/**
* Block-list operations of the journal editor, as plain functions so they can
* be tested without rendering the editor (#555).
*/
/**
* Photo blocks may carry a legacy positional reference ("1", "2") instead of an
* asset UUID. Those only ever resolved against a subpage's album; on a
* standalone journal page there is no album, so the photo silently disappears.
* Flag them in the editor so the author can re-pick before publishing.
*/
const ASSET_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export function isLegacyAssetRef(assetId: string): boolean {
return assetId.length > 0 && !ASSET_UUID.test(assetId);
}
/**
* Slots after a multi-pick (#602): picked photos fill the empty slots first,
* in pick order, and the rest are appended. With `capacity` (a pair holds two)
* the list never grows past it; overflow picks are dropped.
*/
export function fillAssetSlots(slots: string[], picked: string[], capacity?: number): string[] {
const next = [...slots];
const queue = [...picked];
for (let i = 0; i < next.length && queue.length > 0; i++) {
if (!next[i]) next[i] = queue.shift()!;
}
next.push(...queue);
return capacity === undefined ? next : next.slice(0, capacity);
}
/** One photo block per picked asset, for "+ Photos" (#602). */
export function createPhotoBlocks(assetIds: string[]): JournalBlock[] {
return assetIds.map((assetId) => ({ type: 'photo', assetId, caption: '', layout: 'contained' }));
}
/** The block the "Add Block" toolbar inserts for each type. */
export function createBlock(type: JournalBlock['type']): JournalBlock {
switch (type) {
case 'heading':
return { type: 'heading', level: 2, text: 'New Heading' };
case 'paragraph':
return { type: 'paragraph', html: 'Enter paragraph text here...' };
case 'quote':
return { type: 'quote', text: 'Enter quote text...', author: '' };
case 'photo':
return { type: 'photo', assetId: '', caption: '', layout: 'contained' };
case 'photo-pair':
return { type: 'photo-pair', assetIds: ['', ''], caption: '' };
case 'photo-grid':
return { type: 'photo-grid', assetIds: ['', '', ''], caption: '' };
case 'facts':
return {
type: 'facts',
items: [
{ label: '', value: '' },
{ label: '', value: '' },
],
};
case 'map':
return { type: 'map', caption: '', line: true, items: [] };
case 'album':
return { type: 'album', albumId: '', layout: 'grid' };
}
}
/**
* The blocks with the one at `index` moved a step up or down, or null when it
* is already at that end — the arrow buttons are disabled there, but a stale
* click must not reorder anything.
*/
export function moveBlock(
blocks: JournalBlock[],
index: number,
direction: 'up' | 'down',
): JournalBlock[] | null {
const target = direction === 'up' ? index - 1 : index + 1;
if (target < 0 || target >= blocks.length) return null;
const next = [...blocks];
const [moved] = next.splice(index, 1);
next.splice(target, 0, moved);
return next;
}