diff --git a/src/lib/server/pcd/queries/customFormats/index.ts b/src/lib/server/pcd/queries/customFormats/index.ts index 80e3e2e..f64406f 100644 --- a/src/lib/server/pcd/queries/customFormats/index.ts +++ b/src/lib/server/pcd/queries/customFormats/index.ts @@ -10,6 +10,7 @@ export type { DeleteTestOptions } from './testDelete.ts'; export type { ConditionData } from './conditions.ts'; export type { ConditionListItem } from './listConditions.ts'; export type { ConditionResult, EvaluationResult, ParsedInfo } from './evaluator.ts'; +export type { UpdateGeneralInput, UpdateGeneralOptions } from './updateGeneral.ts'; // Export query functions (reads) export { list } from './list.ts'; @@ -23,3 +24,4 @@ export { evaluateCustomFormat, getParsedInfo } from './evaluator.ts'; export { createTest } from './testCreate.ts'; export { updateTest } from './testUpdate.ts'; export { deleteTest } from './testDelete.ts'; +export { updateGeneral } from './updateGeneral.ts'; diff --git a/src/lib/server/pcd/queries/customFormats/updateGeneral.ts b/src/lib/server/pcd/queries/customFormats/updateGeneral.ts new file mode 100644 index 0000000..87ec40e --- /dev/null +++ b/src/lib/server/pcd/queries/customFormats/updateGeneral.ts @@ -0,0 +1,138 @@ +/** + * Update custom format general information + */ + +import type { PCDCache } from '../../cache.ts'; +import { writeOperation, type OperationLayer } from '../../writer.ts'; +import type { CustomFormatGeneral } from './types.ts'; +import { logger } from '$logger/logger.ts'; + +export interface UpdateGeneralInput { + name: string; + description: string; + includeInRename: boolean; + tags: string[]; +} + +export interface UpdateGeneralOptions { + databaseId: number; + cache: PCDCache; + layer: OperationLayer; + /** The current custom format data (for value guards) */ + current: CustomFormatGeneral; + /** The new values */ + input: UpdateGeneralInput; +} + +/** + * Escape a string for SQL + */ +function esc(value: string): string { + return value.replace(/'/g, "''"); +} + +/** + * Update custom format general information + */ +export async function updateGeneral(options: UpdateGeneralOptions) { + const { databaseId, cache, layer, current, input } = options; + const db = cache.kb; + + const queries = []; + + // 1. Update the custom format with value guards + const updateFormat = db + .updateTable('custom_formats') + .set({ + name: input.name, + description: input.description || null, + include_in_rename: input.includeInRename ? 1 : 0 + }) + .where('id', '=', current.id) + // Value guards - ensure current values match what we expect + .where('name', '=', current.name) + .compile(); + + queries.push(updateFormat); + + // 2. Handle tag changes + const currentTagNames = current.tags.map(t => t.name); + const newTagNames = input.tags; + + // Tags to remove + const tagsToRemove = currentTagNames.filter(t => !newTagNames.includes(t)); + for (const tagName of tagsToRemove) { + const removeTag = { + sql: `DELETE FROM custom_format_tags WHERE custom_format_id = (SELECT id FROM custom_formats WHERE name = '${esc(current.name)}') AND tag_id = tag('${esc(tagName)}')`, + parameters: [], + query: {} as never + }; + queries.push(removeTag); + } + + // Tags to add + const tagsToAdd = newTagNames.filter(t => !currentTagNames.includes(t)); + for (const tagName of tagsToAdd) { + // Insert tag if not exists + const insertTag = db + .insertInto('tags') + .values({ name: tagName }) + .onConflict((oc) => oc.column('name').doNothing()) + .compile(); + + queries.push(insertTag); + + // Link tag to custom format + // Use input.name for lookup since the format might have been renamed + const formatName = input.name !== current.name ? input.name : current.name; + const linkTag = { + sql: `INSERT INTO custom_format_tags (custom_format_id, tag_id) VALUES ((SELECT id FROM custom_formats WHERE name = '${esc(formatName)}'), tag('${esc(tagName)}'))`, + parameters: [], + query: {} as never + }; + + queries.push(linkTag); + } + + // Log what's being changed + const changes: Record = {}; + + if (current.name !== input.name) { + changes.name = { from: current.name, to: input.name }; + } + if (current.description !== input.description) { + changes.description = { from: current.description, to: input.description }; + } + if (current.include_in_rename !== input.includeInRename) { + changes.includeInRename = { from: current.include_in_rename, to: input.includeInRename }; + } + if (tagsToAdd.length > 0 || tagsToRemove.length > 0) { + changes.tags = { from: currentTagNames, to: input.tags }; + } + + await logger.info(`Save custom format "${input.name}"`, { + source: 'CustomFormat', + meta: { + id: current.id, + changes + } + }); + + // Write the operation with metadata + const isRename = input.name !== current.name; + + const result = await writeOperation({ + databaseId, + layer, + description: `update-custom-format-${input.name}`, + queries, + metadata: { + operation: 'update', + entity: 'custom_format', + name: input.name, + ...(isRename && { previousName: current.name }) + } + }); + + return result; +} diff --git a/src/routes/custom-formats/[databaseId]/[id]/+layout.svelte b/src/routes/custom-formats/[databaseId]/[id]/+layout.svelte index 7f9231f..7261ce8 100644 --- a/src/routes/custom-formats/[databaseId]/[id]/+layout.svelte +++ b/src/routes/custom-formats/[databaseId]/[id]/+layout.svelte @@ -1,5 +1,6 @@ @@ -31,47 +71,118 @@ {data.format.name} - General - Profilarr - +
{ + saving = true; + return async ({ result, update: formUpdate }) => { + if (result.type === 'failure' && result.data) { + alertStore.add('error', (result.data as { error?: string }).error || 'Operation failed'); + } else if (result.type === 'redirect') { + alertStore.add('success', 'Custom format updated!'); + // Mark as clean so navigation guard doesn't trigger + initEdit($current as GeneralFormData); + } + await formUpdate(); + saving = false; + }; + }} +> + + + + -
- - - - -
-
Tags
-

- Add tags to organize and categorize this custom format. -

- - 0 ? JSON.stringify(tags) : ''} /> -
- -
-
- Include In Rename -
-

- When enabled, this custom format's name will be included in the renamed filename. -

-
- (includeInRename = !includeInRename)} +
+ +
+ +

+ The name of this custom format +

+ update('name', e.currentTarget.value)} + placeholder="Enter custom format name" + class="mt-2 block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 placeholder-neutral-400 focus:border-accent-500 focus:outline-none focus:ring-1 focus:ring-accent-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder-neutral-500" /> - - {includeInRename ? 'Enabled' : 'Disabled'} - +
+ + + update('description', v)} + /> + + +
+
Tags
+

+ Add tags to organize and categorize this custom format. +

+ update('tags', newTags)} + /> +
+ + +
+
+ Include In Rename +
+

+ When enabled, this custom format's name will be included in the renamed filename. +

+
+ update('includeInRename', !($current as GeneralFormData).includeInRename)} + /> + + {($current as GeneralFormData).includeInRename ? 'Enabled' : 'Disabled'} + +
+
+ + +
+
-
+ + + +{#if data.canWriteToBase} + (showSaveTargetModal = false)} + /> +{/if}