- Opening a workflow on the canvas only hides the browser; the Workflows list stays, so opening the browser again no longer shows an empty page (the old close hid the workspace without restoring the page under it). - Card Open and more are two small round buttons in the cover's corner, so a press meant for the details does not open the canvas. - Workflow details: the models' fold says whether all are here; folds open as the viewer last left them; a tip when a workflow has no set (with Save the canvas) and one pointing at compare on a set until used. Narrow (docked) details turn the left column into a row of pills. - Save dialog rebuilt in ui_recipe_save_dialog.js: cover on the left from the canvas, the current cover or recent pictures as thumbnails; fields on the right; Cancel and Save always in view; the name starts as the canvas's saved workflow name; Esc and Enter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
6.0 KiB
JavaScript
124 lines
6.0 KiB
JavaScript
/** The edit dialog for Workflow Recipes (name, tags, notes, versions); saving is ui_recipe_save_dialog.js. */
|
|
|
|
import { translate } from "./locales.js";
|
|
import { anomalousAlert, anomalousConfirm } from "./ui_dialog.js";
|
|
import { appendText } from "./ui_recipe_detail_dom.js";
|
|
|
|
const t = (key, params) => translate(key, params);
|
|
|
|
export function formatRecipeText(key, values = {}) {
|
|
return Object.entries(values).reduce((text, [name, value]) => text.replaceAll(`{${name}}`, String(value)), t(key));
|
|
}
|
|
|
|
export function showRecipeEditDialog(owner, recipeData, filename, history) {
|
|
return new Promise((resolve) => {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'anomalous-recipe-dialog-overlay';
|
|
overlay.setAttribute('role', 'dialog');
|
|
overlay.setAttribute('aria-modal', 'true');
|
|
const dialog = document.createElement('div');
|
|
dialog.className = 'anomalous-recipe-dialog anomalous-recipe-edit-dialog';
|
|
appendText(dialog, 'h3', t('recipeEditTitle'));
|
|
|
|
const nameLabel = appendText(dialog, 'label', t('recipeName'));
|
|
const nameInput = document.createElement('input');
|
|
nameInput.className = 'anomalous-nb-select';
|
|
nameInput.type = 'text';
|
|
nameInput.maxLength = 120;
|
|
nameInput.value = recipeData.name || '';
|
|
nameLabel.appendChild(nameInput);
|
|
|
|
const tagsLabel = appendText(dialog, 'label', t('recipeTags'));
|
|
const tagsInput = document.createElement('input');
|
|
tagsInput.className = 'anomalous-nb-select';
|
|
tagsInput.type = 'text';
|
|
tagsInput.maxLength = 300;
|
|
tagsInput.value = Array.isArray(recipeData.tags) ? recipeData.tags.join(', ') : '';
|
|
tagsInput.placeholder = t('recipeTagsHint');
|
|
tagsLabel.appendChild(tagsInput);
|
|
|
|
const notesLabel = appendText(dialog, 'label', t('recipeNotes'));
|
|
const notesInput = document.createElement('textarea');
|
|
notesInput.className = 'anomalous-nb-textarea';
|
|
notesInput.maxLength = 3000;
|
|
notesInput.value = recipeData.notes || '';
|
|
notesInput.placeholder = t('recipeNotesHint');
|
|
notesLabel.appendChild(notesInput);
|
|
|
|
const historyDetails = document.createElement('details');
|
|
historyDetails.className = 'anomalous-recipe-node-details';
|
|
const historySummary = document.createElement('summary');
|
|
historySummary.textContent = `${t('recipeHistory')} (${history.length})`;
|
|
historyDetails.appendChild(historySummary);
|
|
const historyList = document.createElement('div');
|
|
historyList.className = 'anomalous-recipe-history-list';
|
|
if (!history.length) {
|
|
appendText(historyList, 'small', t('recipeHistoryEmpty'), 'anomalous-recipe-node-hint');
|
|
} else {
|
|
for (const version of history) {
|
|
const row = document.createElement('div');
|
|
row.className = 'anomalous-recipe-history-row';
|
|
const date = Number.isFinite(Number(version.timestamp))
|
|
? new Date(Number(version.timestamp)).toLocaleString()
|
|
: t('recipeUnknownVersion');
|
|
appendText(row, 'span', `${date} · ${version.name || t('recipeUntitled')}`);
|
|
const restore = appendText(row, 'button', t('recipeRestoreVersion'), 'anomalous-btn-danger');
|
|
restore.type = 'button';
|
|
restore.onclick = async () => {
|
|
if (!await anomalousConfirm(t('recipeRestoreVersionConfirm'))) return;
|
|
try {
|
|
const response = await fetch('/anomalous/restore_recipe_version', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ filename, version: version.version }),
|
|
});
|
|
if (!response.ok) throw new Error('recipe history restore failed');
|
|
overlay.remove();
|
|
await owner.refreshRecipes();
|
|
resolve({ mode: 'restored' });
|
|
} catch (error) {
|
|
console.error('Could not restore Workflow Recipe version:', error);
|
|
await anomalousAlert(t('recipeUpdateError'));
|
|
}
|
|
};
|
|
row.appendChild(restore);
|
|
historyList.appendChild(row);
|
|
}
|
|
}
|
|
historyDetails.appendChild(historyList);
|
|
dialog.appendChild(historyDetails);
|
|
|
|
const error = appendText(dialog, 'div', '', 'anomalous-recipe-dialog-error');
|
|
const actions = document.createElement('div');
|
|
actions.className = 'anomalous-recipe-actions';
|
|
const cancel = appendText(actions, 'button', t('recipeCancel'), 'anomalous-btn-danger');
|
|
const canvas = appendText(actions, 'button', t('recipeEditCanvas'), 'anomalous-btn-primary');
|
|
const save = appendText(actions, 'button', t('recipeUpdate'), 'anomalous-btn-success');
|
|
for (const button of [cancel, canvas, save]) button.type = 'button';
|
|
const close = (value) => { overlay.remove(); resolve(value); };
|
|
cancel.onclick = () => close(null);
|
|
overlay.onclick = (event) => { if (event.target === overlay) close(null); };
|
|
canvas.onclick = () => close({ mode: 'canvas' });
|
|
save.onclick = () => {
|
|
const name = nameInput.value.trim();
|
|
if (!name) {
|
|
error.textContent = t('recipeNameRequired');
|
|
nameInput.focus();
|
|
return;
|
|
}
|
|
close({
|
|
mode: 'save',
|
|
name,
|
|
tags: [...new Set(tagsInput.value.split(',').map((tag) => tag.trim()).filter(Boolean))].slice(0, 20),
|
|
notes: notesInput.value.trim(),
|
|
});
|
|
};
|
|
dialog.appendChild(actions);
|
|
overlay.appendChild(dialog);
|
|
(owner.nbPanel || document.body).appendChild(overlay);
|
|
nameInput.focus();
|
|
nameInput.select();
|
|
});
|
|
}
|
|
|