Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1434-workflow-rename-delete.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Settings → Workflows lists user-owned Workflows with rename and delete controls; `workflow/rename` and `workflow/delete` RPC persist changes and clean up runs ([#1434](https://github.com/ThinkFlowLab/FlyBot/issues/1434)). Factory presets remain read-only.
171 changes: 151 additions & 20 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ var ZH = {
"The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow.": "工作流插件没有安装,或没有启用。默认安装包含 @flybot/workflow。",
"Could not copy this workflow": "这个工作流复制不了。",
"Could not save this workflow": "这个工作流保存不了。",
"Could not rename this workflow": "这个工作流改不了名。",
"Could not delete this workflow": "这个工作流删不掉。",
"Use this": "用这个",
"Rename workflow": "重命名工作流",
"Delete workflow": "删除工作流",
'Are you sure you want to delete "{name}"? This cannot be undone.': "确定删除「{name}」吗?删除后无法恢复。",
"Finish editing": "完成",
"Drag a Bot here": "把 Bot 拖到图上",
"System prompt": "系统提示词",
Expand Down Expand Up @@ -14603,7 +14608,7 @@ function GraphCanvas({ graph, editing, roster, onCommit }) {
)
);
}
function Row2({ item, source, onOpen }) {
function Row2({ item, source, onOpen, onRename, onDelete }) {
return (0, import_react34.createElement)(
"button",
{
Expand All @@ -14619,14 +14624,38 @@ function Row2({ item, source, onOpen }) {
(0, import_react34.createElement)("span", null, item.name),
(0, import_react34.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "")
),
source === "own" ? (0, import_react34.createElement)(
"span",
{ className: "bt-set-actions" },
(0, import_react34.createElement)("button", {
type: "button",
className: "bt-set-action",
"aria-label": t("Rename"),
title: t("Rename"),
onClick: (event) => {
event.stopPropagation();
onRename?.(item);
}
}, "✎"),
(0, import_react34.createElement)("button", {
type: "button",
className: "bt-set-action",
"aria-label": t("Delete"),
title: t("Delete"),
onClick: (event) => {
event.stopPropagation();
onDelete?.(item);
}
}, "×")
) : null,
(0, import_react34.createElement)("span", { className: "bt-chevron", "aria-hidden": true }, "›")
);
}
function alertCopy(error) {
if (error === WORKFLOW_PLUGIN_HINT) return t("The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow.");
return error || t("Could not read workflows");
}
function WorkflowsView({ phase = "loading", pack = null, error = "", opened = null, editing = false, roster = null, onOpen, onBack, onEditing, onRetry, onUse, onCommit }) {
function WorkflowsView({ phase = "loading", pack = null, error = "", opened = null, editing = false, roster = null, onOpen, onBack, onEditing, onRetry, onUse, onCommit, onRename, onDelete }) {
const graph = phase === "ready" ? openedGraph(pack, opened) : null;
const templates = pack?.templates ?? [];
const templateIds = new Set(templates.map((item) => item.id));
Expand Down Expand Up @@ -14670,7 +14699,7 @@ function WorkflowsView({ phase = "loading", pack = null, error = "", opened = nu
(0, import_react34.createElement)(
"div",
{ className: "bt-set-card" },
yours.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-set-row" }, (0, import_react34.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react34.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own") }))
yours.length === 0 ? (0, import_react34.createElement)("div", { className: "bt-set-row" }, (0, import_react34.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react34.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete }))
)
)
) : null,
Expand All @@ -14695,12 +14724,60 @@ function WorkflowsView({ phase = "loading", pack = null, error = "", opened = nu
) : null
);
}
function RenameModal({ workflow, onClose, onConfirm }) {
const [name, setName] = (0, import_react34.useState)(workflow?.name ?? "");
return (0, import_react34.createElement)(
"div",
{ className: "bt-modal-mask", onClick: (event) => {
if (event.target === event.currentTarget) onClose?.();
} },
(0, import_react34.createElement)(
"div",
{ className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") },
(0, import_react34.createElement)("h3", null, t("Rename workflow")),
(0, import_react34.createElement)(
"label",
{ className: "bt-field" },
t("Name"),
(0, import_react34.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true })
),
(0, import_react34.createElement)(
"div",
{ className: "bt-modal-actions" },
(0, import_react34.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
(0, import_react34.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename"))
)
)
);
}
function DeleteModal({ workflow, onClose, onConfirm }) {
return (0, import_react34.createElement)(
"div",
{ className: "bt-modal-mask", onClick: (event) => {
if (event.target === event.currentTarget) onClose?.();
} },
(0, import_react34.createElement)(
"div",
{ className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") },
(0, import_react34.createElement)("h3", null, t("Delete workflow")),
(0, import_react34.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })),
(0, import_react34.createElement)(
"div",
{ className: "bt-modal-actions" },
(0, import_react34.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")),
(0, import_react34.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete"))
)
)
);
}
function WorkflowsPage({ call: call4, roster }) {
const [phase, setPhase] = (0, import_react34.useState)("loading");
const [pack, setPack] = (0, import_react34.useState)(null);
const [error, setError] = (0, import_react34.useState)("");
const [opened, setOpened] = (0, import_react34.useState)(null);
const [editing, setEditing] = (0, import_react34.useState)(false);
const [renaming, setRenaming] = (0, import_react34.useState)(null);
const [deleting, setDeleting] = (0, import_react34.useState)(null);
const invoke = typeof call4 === "function" ? call4 : () => Promise.reject(new Error("Could not read workflows"));
const apply2 = (settled) => {
setPhase(settled.phase);
Expand Down Expand Up @@ -14754,6 +14831,44 @@ function WorkflowsPage({ call: call4, roster }) {
setError(workflowCallError(failure, t("Could not copy this workflow")));
});
};
const rename = (item) => {
setRenaming(item);
};
const confirmRename = (workflowId, name) => {
if (!name.trim()) return;
Promise.resolve(invoke("workflow/rename", { workflowId, name: name.trim() })).then((value) => {
const settled = settleWorkflowRead(value);
if (settled.phase === "error") {
setError(settled.error);
return;
}
setPack(settled.pack);
setPhase("ready");
setError("");
setRenaming(null);
}).catch((failure) => {
setError(workflowCallError(failure, t("Could not rename this workflow")));
});
};
const deleteWorkflow = (item) => {
setDeleting(item);
};
const confirmDelete = (workflowId) => {
Promise.resolve(invoke("workflow/delete", { workflowId })).then((value) => {
const settled = settleWorkflowRead(value);
if (settled.phase === "error") {
setError(settled.error);
return;
}
setPack(settled.pack);
setPhase(settled.phase);
setError("");
setDeleting(null);
if (opened?.id === workflowId) setOpened(null);
}).catch((failure) => {
setError(workflowCallError(failure, t("Could not delete this workflow")));
});
};
const commit = (graph) => {
if (!opened || opened.source !== "own") return;
Promise.resolve(invoke("workflow/graph", { workflowId: opened.id, nodes: graph.nodes, edges: graph.edges })).then((value) => {
Expand All @@ -14769,23 +14884,39 @@ function WorkflowsPage({ call: call4, roster }) {
setError(workflowCallError(failure, t("Could not save this workflow")));
});
};
return (0, import_react34.createElement)(WorkflowsView, {
phase,
pack,
error,
opened,
editing,
roster,
onOpen: open,
onBack: () => {
setOpened(null);
setEditing(false);
},
onEditing: setEditing,
onRetry: retry,
onUse: useTemplate,
onCommit: commit
});
return (0, import_react34.createElement)(
"div",
null,
(0, import_react34.createElement)(WorkflowsView, {
phase,
pack,
error,
opened,
editing,
roster,
onOpen: open,
onBack: () => {
setOpened(null);
setEditing(false);
},
onEditing: setEditing,
onRetry: retry,
onUse: useTemplate,
onCommit: commit,
onRename: rename,
onDelete: deleteWorkflow
}),
renaming ? (0, import_react34.createElement)(RenameModal, {
workflow: renaming,
onClose: () => setRenaming(null),
onConfirm: (name) => confirmRename(renaming.id, name)
}) : null,
deleting ? (0, import_react34.createElement)(DeleteModal, {
workflow: deleting,
onClose: () => setDeleting(null),
onConfirm: () => confirmDelete(deleting.id)
}) : null
);
}

// src/client/usage-data.js
Expand Down
5 changes: 5 additions & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ const ZH = {
'工作流插件没有安装,或没有启用。默认安装包含 @flybot/workflow。',
'Could not copy this workflow': '这个工作流复制不了。',
'Could not save this workflow': '这个工作流保存不了。',
'Could not rename this workflow': '这个工作流改不了名。',
'Could not delete this workflow': '这个工作流删不掉。',
'Use this': '用这个',
'Rename workflow': '重命名工作流',
'Delete workflow': '删除工作流',
'Are you sure you want to delete "{name}"? This cannot be undone.': '确定删除「{name}」吗?删除后无法恢复。',
'Finish editing': '完成',
'Drag a Bot here': '把 Bot 拖到图上',
'System prompt': '系统提示词',
Expand Down
96 changes: 89 additions & 7 deletions core/src/client/workflows.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,23 @@ function GraphCanvas({ graph, editing, roster, onCommit }) {
})))
}

function Row({ item, source, onOpen }) {
function Row({ item, source, onOpen, onRename, onDelete }) {
return h('button', {
type: 'button', className: 'bt-set-row', 'data-workflow': item.id, 'data-source': source,
onClick: () => onOpen?.(item),
},
h('span', { className: 'bt-set-copy' },
h('span', null, item.name),
h('span', { className: 'bt-set-hint' }, item.preview || chainOf(item) || item.description || '')),
source === 'own' ? h('span', { className: 'bt-set-actions' },
h('button', {
type: 'button', className: 'bt-set-action', 'aria-label': t('Rename'), title: t('Rename'),
onClick: (event) => { event.stopPropagation(); onRename?.(item) },
}, '✎'),
h('button', {
type: 'button', className: 'bt-set-action', 'aria-label': t('Delete'), title: t('Delete'),
onClick: (event) => { event.stopPropagation(); onDelete?.(item) },
}, '×')) : null,
h('span', { className: 'bt-chevron', 'aria-hidden': true }, '›'))
}

Expand All @@ -250,7 +259,7 @@ function alertCopy(error) {
return error || t('Could not read workflows')
}

export function WorkflowsView({ phase = 'loading', pack = null, error = '', opened = null, editing = false, roster = null, onOpen, onBack, onEditing, onRetry, onUse, onCommit }) {
export function WorkflowsView({ phase = 'loading', pack = null, error = '', opened = null, editing = false, roster = null, onOpen, onBack, onEditing, onRetry, onUse, onCommit, onRename, onDelete }) {
const graph = phase === 'ready' ? openedGraph(pack, opened) : null
const templates = pack?.templates ?? []
const templateIds = new Set(templates.map((item) => item.id))
Expand Down Expand Up @@ -280,7 +289,7 @@ export function WorkflowsView({ phase = 'loading', pack = null, error = '', open
h('div', { className: 'bt-set-card' },
yours.length === 0
? h('div', { className: 'bt-set-row' }, h('span', { className: 'bt-set-hint' }, t('No workflows of your own yet.')))
: yours.map((item) => h(Row, { key: item.id, item, source: 'own', onOpen: () => onOpen?.(item, 'own') }))))) : null,
: yours.map((item) => h(Row, { key: item.id, item, source: 'own', onOpen: () => onOpen?.(item, 'own'), onRename, onDelete }))))) : null,
graph ? h('div', { className: 'bt-wf-editor' },
h('div', { className: 'bt-wf-actions' },
h('button', { type: 'button', onClick: () => onBack?.() }, t('Back')),
Expand All @@ -296,12 +305,37 @@ export function WorkflowsView({ phase = 'loading', pack = null, error = '', open
})) : null)
}

function RenameModal({ workflow, onClose, onConfirm }) {
const [name, setName] = useState(workflow?.name ?? '')
return h('div', { className: 'bt-modal-mask', onClick: (event) => { if (event.target === event.currentTarget) onClose?.() } },
h('div', { className: 'bt-modal', role: 'dialog', 'aria-modal': true, 'aria-label': t('Rename workflow') },
h('h3', null, t('Rename workflow')),
h('label', { className: 'bt-field' },
t('Name'),
h('input', { type: 'text', value: name, onChange: (event) => setName(event.target.value), autoFocus: true })),
h('div', { className: 'bt-modal-actions' },
h('button', { type: 'button', onClick: onClose }, t('Cancel')),
h('button', { type: 'button', onClick: () => onConfirm?.(name) }, t('Rename')))))
}

function DeleteModal({ workflow, onClose, onConfirm }) {
return h('div', { className: 'bt-modal-mask', onClick: (event) => { if (event.target === event.currentTarget) onClose?.() } },
h('div', { className: 'bt-modal', role: 'dialog', 'aria-modal': true, 'aria-label': t('Delete workflow') },
h('h3', null, t('Delete workflow')),
h('p', null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? '' })),
h('div', { className: 'bt-modal-actions' },
h('button', { type: 'button', onClick: onClose }, t('Cancel')),
h('button', { type: 'button', className: 'bt-danger', onClick: onConfirm }, t('Delete')))))
}

export function WorkflowsPage({ call, roster }) {
const [phase, setPhase] = useState('loading')
const [pack, setPack] = useState(null)
const [error, setError] = useState('')
const [opened, setOpened] = useState(null)
const [editing, setEditing] = useState(false)
const [renaming, setRenaming] = useState(null)
const [deleting, setDeleting] = useState(null)
const invoke = typeof call === 'function' ? call : () => Promise.reject(new Error('Could not read workflows'))

const apply = (settled) => {
Expand Down Expand Up @@ -351,6 +385,42 @@ export function WorkflowsPage({ call, roster }) {
.catch((failure) => { setError(workflowCallError(failure, t('Could not copy this workflow'))) })
}

const rename = (item) => {
setRenaming(item)
}

const confirmRename = (workflowId, name) => {
if (!name.trim()) return
Promise.resolve(invoke('workflow/rename', { workflowId, name: name.trim() }))
.then((value) => {
const settled = settleWorkflowRead(value)
if (settled.phase === 'error') { setError(settled.error); return }
setPack(settled.pack)
setPhase('ready')
setError('')
setRenaming(null)
})
.catch((failure) => { setError(workflowCallError(failure, t('Could not rename this workflow'))) })
}

const deleteWorkflow = (item) => {
setDeleting(item)
}

const confirmDelete = (workflowId) => {
Promise.resolve(invoke('workflow/delete', { workflowId }))
.then((value) => {
const settled = settleWorkflowRead(value)
if (settled.phase === 'error') { setError(settled.error); return }
setPack(settled.pack)
setPhase(settled.phase)
setError('')
setDeleting(null)
if (opened?.id === workflowId) setOpened(null)
})
.catch((failure) => { setError(workflowCallError(failure, t('Could not delete this workflow'))) })
}

const commit = (graph) => {
if (!opened || opened.source !== 'own') return
Promise.resolve(invoke('workflow/graph', { workflowId: opened.id, nodes: graph.nodes, edges: graph.edges }))
Expand All @@ -364,8 +434,20 @@ export function WorkflowsPage({ call, roster }) {
.catch((failure) => { setError(workflowCallError(failure, t('Could not save this workflow'))) })
}

return h(WorkflowsView, {
phase, pack, error, opened, editing, roster,
onOpen: open, onBack: () => { setOpened(null); setEditing(false) }, onEditing: setEditing, onRetry: retry, onUse: useTemplate, onCommit: commit,
})
return h('div', null,
h(WorkflowsView, {
phase, pack, error, opened, editing, roster,
onOpen: open, onBack: () => { setOpened(null); setEditing(false) }, onEditing: setEditing, onRetry: retry, onUse: useTemplate, onCommit: commit,
onRename: rename, onDelete: deleteWorkflow,
}),
renaming ? h(RenameModal, {
workflow: renaming,
onClose: () => setRenaming(null),
onConfirm: (name) => confirmRename(renaming.id, name),
}) : null,
deleting ? h(DeleteModal, {
workflow: deleting,
onClose: () => setDeleting(null),
onConfirm: () => confirmDelete(deleting.id),
}) : null)
}
Loading
Loading