/** * useFlowEditor.js — Global reactive store for the Flow Editor dialog. * * Odoo-style "inline create from anywhere": any page can call openFlow() * to pop the editor without having to route to Settings. * * The shared state (singleton module-level refs) is consumed by * mounted in MainLayout. Pages simply call * `const fe = useFlowEditor(); fe.openTemplate('FT-00001')`. * * API (all methods optimised to avoid redundant fetches): * openTemplate(name, opts?) — load an existing FT by name and open editor * openNew(opts?) — open editor with a blank draft * close() — close + clear state * isOpen Ref * template Ref — current FT doc being edited * loading Ref * error Ref * dirty Ref — unsaved changes flag * * Options accepted by openTemplate / openNew: * { category, applies_to, kind } — preset fields for new templates * { onSaved(tpl) } — callback after a successful save * { context } — opaque host context (for telemetry) */ import { ref, reactive, readonly } from 'vue' import { getFlowTemplate, createFlowTemplate, updateFlowTemplate, duplicateFlowTemplate, deleteFlowTemplate, } from 'src/api/flow-templates' // ── Module-level shared state (singleton) ─────────────────────────────────── const isOpen = ref(false) const template = ref(null) // the doc currently being edited const templateName = ref(null) // non-null once saved once const loading = ref(false) const saving = ref(false) const error = ref(null) const dirty = ref(false) const onSavedCbs = ref([]) // callbacks from the caller const mode = ref('edit') // 'new' | 'edit' | 'view' /** Build a blank FT draft with sensible defaults. */ function blankTemplate (opts = {}) { return { template_name: '', category: opts.category || 'other', applies_to: opts.applies_to || null, icon: opts.icon || 'account_tree', is_active: 1, is_system: 0, version: 1, description: '', trigger_event: opts.trigger_event || null, trigger_condition: '', flow_definition: { version: 1, trigger: { type: opts.kind === 'agent' ? 'manual' : 'on_flow_start' }, variables: {}, steps: [], }, tags: '', notes: '', } } // ── Actions ───────────────────────────────────────────────────────────────── /** * Open the editor on an existing template. * Fetches full body (incl. flow_definition) from the Hub. */ async function openTemplate (name, opts = {}) { if (!name) return openNew(opts) error.value = null loading.value = true isOpen.value = true mode.value = 'edit' templateName.value = name onSavedCbs.value = opts.onSaved ? [opts.onSaved] : [] try { const tpl = await getFlowTemplate(name) // Ensure flow_definition is a live object (Hub returns parsed JSON) if (!tpl.flow_definition || typeof tpl.flow_definition === 'string') { try { tpl.flow_definition = JSON.parse(tpl.flow_definition || '{}') } catch { tpl.flow_definition = { steps: [] } } } if (!tpl.flow_definition.steps) tpl.flow_definition.steps = [] template.value = tpl dirty.value = false } catch (e) { error.value = e.message isOpen.value = false } finally { loading.value = false } } /** Open the editor on a fresh draft. Save-on-Enregistrer creates it. */ function openNew (opts = {}) { error.value = null isOpen.value = true mode.value = 'new' templateName.value = null template.value = blankTemplate(opts) dirty.value = false onSavedCbs.value = opts.onSaved ? [opts.onSaved] : [] } /** Close the editor. Caller is responsible for confirming unsaved changes. */ function close (force = false) { if (dirty.value && !force) { if (!window.confirm('Modifications non sauvegardées. Fermer quand même ?')) return } isOpen.value = false template.value = null templateName.value = null dirty.value = false error.value = null } /** Mark the editor state as dirty (called by child on any mutation). */ function markDirty () { dirty.value = true } /** * Save the current draft. Creates if new, else patches. * Returns the saved template on success; throws on error. */ async function save () { if (!template.value) return saving.value = true error.value = null try { let saved const body = { ...template.value } // flow_definition will be stringified server-side if (mode.value === 'new') { saved = await createFlowTemplate(body) templateName.value = saved.name mode.value = 'edit' } else { saved = await updateFlowTemplate(templateName.value, body) } template.value = { ...template.value, ...saved, flow_definition: template.value.flow_definition } dirty.value = false for (const cb of onSavedCbs.value) { try { cb(saved) } catch { /* noop */ } } return saved } catch (e) { error.value = e.message throw e } finally { saving.value = false } } /** Duplicate the current template (only valid in edit mode). */ async function duplicate (newName) { if (mode.value !== 'edit' || !templateName.value) return const dup = await duplicateFlowTemplate(templateName.value, newName) await openTemplate(dup.name) return dup } /** Delete the current template (only valid in edit mode + non-system). */ async function remove () { if (mode.value !== 'edit' || !templateName.value) return if (template.value?.is_system) { throw new Error('Impossible de supprimer un template système (utilisez "Dupliquer")') } await deleteFlowTemplate(templateName.value) close(true) } // ── Public API ────────────────────────────────────────────────────────────── export function useFlowEditor () { return { // state (read-only refs so consumers can't accidentally mutate) isOpen: readonly(isOpen), loading: readonly(loading), saving: readonly(saving), error: readonly(error), dirty: readonly(dirty), mode: readonly(mode), templateName: readonly(templateName), // mutable state (only the draft content, by design) template, // actions openTemplate, openNew, close, save, duplicate, remove, markDirty, } } // Named default for convenience export default useFlowEditor