@innertia-solutions/innertia-nuxt 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/app.config.ts +10 -10
- package/components/Attention.vue +269 -0
- package/components/Auth/LoginForm.vue +1 -1
- package/components/Chart/index.vue +193 -0
- package/components/Container.vue +116 -0
- package/components/CopyButton.vue +98 -0
- package/components/Files/Card.vue +215 -0
- package/components/Files/Preview.vue +675 -0
- package/components/Files/Upload.vue +408 -0
- package/components/Files/UploadModal.vue +83 -0
- package/components/Forms/InputPin.vue +162 -0
- package/components/InfoToolbar.vue +54 -0
- package/components/Kanban/Standard.vue +588 -0
- package/components/Loader.vue +358 -0
- package/components/MobileBlocker.vue +5 -5
- package/components/MobileLoginPicker.vue +13 -13
- package/components/Modal/Base.vue +6 -0
- package/components/Modal/Confirm.vue +89 -0
- package/components/Modal/DeleteConfirm.vue +14 -31
- package/components/Modal/Form.vue +76 -0
- package/components/Modal/Host.vue +58 -0
- package/components/Modal/Prompt.vue +74 -0
- package/components/Modal.vue +119 -34
- package/components/Organization/Picker.vue +3 -3
- package/components/Table/Standard.vue +198 -25
- package/components/Table/index.vue +169 -369
- package/components/Table/views/ViewCards.vue +74 -0
- package/components/Table/views/ViewDatabase.vue +236 -0
- package/components/Table/views/ViewTable.vue +299 -0
- package/components/Toast/Alert.vue +16 -111
- package/components/Toast/Container.vue +4 -29
- package/components/Toast/Notification.vue +8 -43
- package/components/Toast/Process.vue +8 -86
- package/components/Toast/Stack.vue +59 -0
- package/components/Toast/Standard.vue +115 -0
- package/components/Tree/Standard.vue +49 -4
- package/components/Workflow/Board.vue +356 -0
- package/components/Workflow/Builder.vue +454 -0
- package/components/Workflow/Form.vue +526 -0
- package/components/Workflow/Viewer.vue +225 -0
- package/components/Workflow/edges/TransitionEdge.vue +84 -0
- package/components/Workflow/nodes/StepNode.vue +88 -0
- package/composables/useAuth.js +1 -1
- package/composables/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useContext.js +53 -5
- package/composables/useFile.js +189 -0
- package/composables/useMobileGuard.ts +21 -21
- package/composables/useModal.js +71 -0
- package/composables/useOrganization.ts +3 -3
- package/composables/useToast.js +38 -65
- package/composables/useWorkflow/form.js +162 -0
- package/composables/useWorkflow/index.js +18 -0
- package/composables/useWorkflow/view.js +220 -0
- package/composables/useWorkflowLayout.js +162 -0
- package/css/tokens.css +10 -10
- package/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
- package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
- package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
- package/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- package/stores/toast.js +156 -117
- package/composables/useApp.ts +0 -46
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { ref, computed, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useWorkflowForm — gestiona el estado de edición de una WorkflowDefinition.
|
|
5
|
+
*
|
|
6
|
+
* Responsabilidad: CRUD de steps y transitions sobre el JSON de configuración.
|
|
7
|
+
* No sabe nada de instancias, vistas ni API calls de runtime.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object|null} initial — definición inicial { steps:[], transitions:[] }
|
|
10
|
+
*/
|
|
11
|
+
export function useWorkflowForm(initial = null) {
|
|
12
|
+
const definition = ref(
|
|
13
|
+
initial
|
|
14
|
+
? JSON.parse(JSON.stringify(initial))
|
|
15
|
+
: { steps: [], transitions: [] }
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const isDirty = ref(false)
|
|
19
|
+
const saving = ref(false)
|
|
20
|
+
|
|
21
|
+
// Track changes — se activa después del primer tick para no marcar dirty en la carga
|
|
22
|
+
let _trackChanges = false
|
|
23
|
+
watch(definition, () => { if (_trackChanges) isDirty.value = true }, { deep: true })
|
|
24
|
+
setTimeout(() => { _trackChanges = true }, 0)
|
|
25
|
+
|
|
26
|
+
// ── Steps ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const addStep = (step) => {
|
|
29
|
+
if (!step.key || !step.label || !step.type) throw new Error('Step requiere key, label y type')
|
|
30
|
+
if (definition.value.steps.some(s => s.key === step.key)) throw new Error(`Step '${step.key}' ya existe`)
|
|
31
|
+
definition.value.steps.push({ ...step })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const updateStep = (key, patch) => {
|
|
35
|
+
const idx = definition.value.steps.findIndex(s => s.key === key)
|
|
36
|
+
if (idx === -1) return
|
|
37
|
+
const newKey = patch.key ?? key
|
|
38
|
+
definition.value.steps[idx] = { ...definition.value.steps[idx], ...patch }
|
|
39
|
+
// Si cambió el key, actualizar todas las transiciones que lo referencian
|
|
40
|
+
if (newKey !== key) {
|
|
41
|
+
definition.value.transitions = definition.value.transitions.map(t => ({
|
|
42
|
+
...t,
|
|
43
|
+
from: t.from === key ? newKey : t.from,
|
|
44
|
+
to: t.to === key ? newKey : t.to,
|
|
45
|
+
}))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const deleteStep = (key) => {
|
|
50
|
+
definition.value.steps = definition.value.steps.filter(s => s.key !== key)
|
|
51
|
+
definition.value.transitions = definition.value.transitions.filter(t => t.from !== key && t.to !== key)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const reorderSteps = (fromIdx, toIdx) => {
|
|
55
|
+
const steps = [...definition.value.steps]
|
|
56
|
+
const [moved] = steps.splice(fromIdx, 1)
|
|
57
|
+
steps.splice(toIdx, 0, moved)
|
|
58
|
+
definition.value.steps = steps
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Transitions ────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
const addTransition = (transition) => {
|
|
64
|
+
if (!transition.from || !transition.to) throw new Error('Transition requiere from y to')
|
|
65
|
+
const exists = definition.value.transitions.some(
|
|
66
|
+
t => t.from === transition.from && t.to === transition.to
|
|
67
|
+
)
|
|
68
|
+
if (exists) throw new Error(`Transición ${transition.from} → ${transition.to} ya existe`)
|
|
69
|
+
definition.value.transitions.push({
|
|
70
|
+
from: transition.from,
|
|
71
|
+
to: transition.to,
|
|
72
|
+
label: transition.label ?? '',
|
|
73
|
+
description: transition.description ?? '',
|
|
74
|
+
restrictions: transition.restrictions ?? [],
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const updateTransition = (from, to, patch) => {
|
|
79
|
+
const idx = definition.value.transitions.findIndex(t => t.from === from && t.to === to)
|
|
80
|
+
if (idx === -1) return
|
|
81
|
+
definition.value.transitions[idx] = { ...definition.value.transitions[idx], ...patch }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const deleteTransition = (from, to) => {
|
|
85
|
+
definition.value.transitions = definition.value.transitions.filter(
|
|
86
|
+
t => !(t.from === from && t.to === to)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Restrictions helpers ───────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const addRestriction = (from, to, restriction) => {
|
|
93
|
+
const t = definition.value.transitions.find(t => t.from === from && t.to === to)
|
|
94
|
+
if (!t) return
|
|
95
|
+
t.restrictions = [...(t.restrictions ?? []), { type: 'role', message: '', ...restriction }]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const removeRestriction = (from, to, idx) => {
|
|
99
|
+
const t = definition.value.transitions.find(t => t.from === from && t.to === to)
|
|
100
|
+
if (!t) return
|
|
101
|
+
t.restrictions = t.restrictions.filter((_, i) => i !== idx)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Computed helpers ───────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const stepByKey = computed(() =>
|
|
107
|
+
Object.fromEntries((definition.value.steps ?? []).map(s => [s.key, s]))
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const transitionsFrom = (key) =>
|
|
111
|
+
(definition.value.transitions ?? []).filter(t => t.from === key)
|
|
112
|
+
|
|
113
|
+
const transitionsTo = (key) =>
|
|
114
|
+
(definition.value.transitions ?? []).filter(t => t.to === key)
|
|
115
|
+
|
|
116
|
+
const isValid = computed(() => {
|
|
117
|
+
const steps = definition.value.steps ?? []
|
|
118
|
+
return (
|
|
119
|
+
steps.length >= 2 &&
|
|
120
|
+
steps.some(s => s.type === 'start') &&
|
|
121
|
+
steps.some(s => ['finished', 'cancelled'].includes(s.type))
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// ── Reset / load ───────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
const reset = (newDefinition = null) => {
|
|
128
|
+
definition.value = newDefinition
|
|
129
|
+
? JSON.parse(JSON.stringify(newDefinition))
|
|
130
|
+
: { steps: [], transitions: [] }
|
|
131
|
+
isDirty.value = false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const markSaved = () => { isDirty.value = false }
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
// State
|
|
138
|
+
definition,
|
|
139
|
+
isDirty,
|
|
140
|
+
saving,
|
|
141
|
+
isValid,
|
|
142
|
+
// Step CRUD
|
|
143
|
+
addStep,
|
|
144
|
+
updateStep,
|
|
145
|
+
deleteStep,
|
|
146
|
+
reorderSteps,
|
|
147
|
+
// Transition CRUD
|
|
148
|
+
addTransition,
|
|
149
|
+
updateTransition,
|
|
150
|
+
deleteTransition,
|
|
151
|
+
// Restriction helpers
|
|
152
|
+
addRestriction,
|
|
153
|
+
removeRestriction,
|
|
154
|
+
// Computed
|
|
155
|
+
stepByKey,
|
|
156
|
+
transitionsFrom,
|
|
157
|
+
transitionsTo,
|
|
158
|
+
// Utils
|
|
159
|
+
reset,
|
|
160
|
+
markSaved,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWorkflow — punto de entrada único del sistema de workflows.
|
|
3
|
+
*
|
|
4
|
+
* Estructura:
|
|
5
|
+
* useWorkflowForm → edición de definiciones (steps, transitions, restrictions)
|
|
6
|
+
* useWorkflowView → estado operacional (items, columnas, onMove, viewMode)
|
|
7
|
+
*
|
|
8
|
+
* Uso independiente:
|
|
9
|
+
* import { useWorkflowForm } from '~/composables/useWorkflow'
|
|
10
|
+
* import { useWorkflowView } from '~/composables/useWorkflow'
|
|
11
|
+
*
|
|
12
|
+
* Con auto-import de Nuxt también funcionan directamente:
|
|
13
|
+
* const form = useWorkflowForm(initialDefinition)
|
|
14
|
+
* const view = useWorkflowView(form.definition, { endpoint: 'tasks' })
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { useWorkflowForm } from './form.js'
|
|
18
|
+
export { useWorkflowView } from './view.js'
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { ref, computed, watch } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useWorkflowView — estado operacional de un workflow en runtime.
|
|
5
|
+
*
|
|
6
|
+
* Responsabilidad: cargar items, derivar columnas desde la definición,
|
|
7
|
+
* validar y ejecutar transiciones, manejar el view mode del board.
|
|
8
|
+
*
|
|
9
|
+
* NO edita la definición — eso es trabajo de useWorkflowForm.
|
|
10
|
+
*
|
|
11
|
+
* @param {Ref<Object>} definitionRef — ref reactivo a { steps, transitions }
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {string} options.endpoint — endpoint para cargar items via useApi()
|
|
14
|
+
* @param {Object} options.params — params extra para el fetch
|
|
15
|
+
* @param {string} options.stateKey — campo del item que indica el step actual (default: 'current_step')
|
|
16
|
+
* @param {Function} options.onTransition — async (item, toStep, fromStep) => void — consumer llama su API
|
|
17
|
+
* @param {string} options.defaultView — 'kanban' | 'table' | 'flow' | 'funnel' (default: 'kanban')
|
|
18
|
+
* @param {boolean} options.validateTransitions — si false, permite cualquier movimiento (default: true)
|
|
19
|
+
*/
|
|
20
|
+
export function useWorkflowView(definitionRef, options = {}) {
|
|
21
|
+
const {
|
|
22
|
+
endpoint = null,
|
|
23
|
+
params = {},
|
|
24
|
+
stateKey = 'current_step',
|
|
25
|
+
onTransition = null,
|
|
26
|
+
defaultView = 'kanban',
|
|
27
|
+
validateTransitions = true,
|
|
28
|
+
} = options
|
|
29
|
+
|
|
30
|
+
// ── Items ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const items = ref([])
|
|
33
|
+
const loading = ref(false)
|
|
34
|
+
const error = ref(null)
|
|
35
|
+
const total = computed(() => items.value.length)
|
|
36
|
+
|
|
37
|
+
const api = useApi?.() ?? null // auto-import de Nuxt
|
|
38
|
+
|
|
39
|
+
const load = async () => {
|
|
40
|
+
if (!endpoint) return
|
|
41
|
+
loading.value = true
|
|
42
|
+
error.value = null
|
|
43
|
+
try {
|
|
44
|
+
const res = await api.get(endpoint, params)
|
|
45
|
+
items.value = Array.isArray(res) ? res : (res.data ?? [])
|
|
46
|
+
} catch (e) {
|
|
47
|
+
error.value = e?.message ?? 'Error al cargar'
|
|
48
|
+
} finally {
|
|
49
|
+
loading.value = false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const reload = () => load()
|
|
54
|
+
|
|
55
|
+
// Cargar al montar si hay endpoint
|
|
56
|
+
if (endpoint) {
|
|
57
|
+
// onMounted no está disponible aquí — el consumer debe llamar load() o
|
|
58
|
+
// lo hacemos con watch en el primer tick
|
|
59
|
+
watch(() => endpoint, load, { immediate: true })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Columnas derivadas de la definición ────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const STEP_TYPE_COLOR = {
|
|
65
|
+
start: 'slate',
|
|
66
|
+
in_progress: 'blue',
|
|
67
|
+
pause_internal: 'amber',
|
|
68
|
+
pause_external: 'orange',
|
|
69
|
+
finished: 'green',
|
|
70
|
+
cancelled: 'red',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const columns = computed(() =>
|
|
74
|
+
(definitionRef.value?.steps ?? []).map(step => ({
|
|
75
|
+
key: step.key,
|
|
76
|
+
label: step.label,
|
|
77
|
+
color: STEP_TYPE_COLOR[step.type] ?? 'slate',
|
|
78
|
+
type: step.type,
|
|
79
|
+
}))
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// ── Validación de transiciones ─────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Verifica si existe una transición definida entre dos steps.
|
|
86
|
+
*/
|
|
87
|
+
const canTransition = (fromStep, toStep) => {
|
|
88
|
+
if (!validateTransitions) return true
|
|
89
|
+
return (definitionRef.value?.transitions ?? []).some(
|
|
90
|
+
t => t.from === fromStep && t.to === toStep
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Devuelve los steps destino disponibles desde un step dado.
|
|
96
|
+
*/
|
|
97
|
+
const availableTransitions = (fromStep) =>
|
|
98
|
+
(definitionRef.value?.transitions ?? [])
|
|
99
|
+
.filter(t => t.from === fromStep)
|
|
100
|
+
.map(t => ({
|
|
101
|
+
key: t.to,
|
|
102
|
+
label: definitionRef.value?.steps?.find(s => s.key === t.to)?.label ?? t.to,
|
|
103
|
+
transitionLabel: t.label ?? '',
|
|
104
|
+
restrictions: t.restrictions ?? [],
|
|
105
|
+
}))
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Devuelve la config de una transición específica.
|
|
109
|
+
*/
|
|
110
|
+
const getTransition = (fromStep, toStep) =>
|
|
111
|
+
(definitionRef.value?.transitions ?? []).find(
|
|
112
|
+
t => t.from === fromStep && t.to === toStep
|
|
113
|
+
) ?? null
|
|
114
|
+
|
|
115
|
+
// ── onMove — para KanbanStandard ──────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Función compatible con KanbanStandard :move-mutation.
|
|
119
|
+
* Valida la transición y llama al onTransition del consumer.
|
|
120
|
+
*
|
|
121
|
+
* @param {string|Object} itemOrId — id del item o el item completo
|
|
122
|
+
* @param {string} toStep — step destino
|
|
123
|
+
* @param {string} fromStep — step origen (opcional, se busca en items)
|
|
124
|
+
*/
|
|
125
|
+
const onMove = async (itemOrId, toStep, fromStep) => {
|
|
126
|
+
const id = typeof itemOrId === 'object' ? itemOrId.id : itemOrId
|
|
127
|
+
const item = items.value.find(i => i.id === id) ?? itemOrId
|
|
128
|
+
|
|
129
|
+
const from = fromStep ?? item?.[stateKey]
|
|
130
|
+
|
|
131
|
+
if (validateTransitions && !canTransition(from, toStep)) {
|
|
132
|
+
console.warn(`[useWorkflowView] Transición bloqueada: ${from} → ${toStep}`)
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Optimistic update
|
|
137
|
+
const idx = items.value.findIndex(i => i.id === id)
|
|
138
|
+
if (idx >= 0) items.value[idx] = { ...items.value[idx], [stateKey]: toStep }
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (onTransition) await onTransition(item, toStep, from)
|
|
142
|
+
return true
|
|
143
|
+
} catch (e) {
|
|
144
|
+
// Rollback
|
|
145
|
+
if (idx >= 0) items.value[idx] = { ...items.value[idx], [stateKey]: from }
|
|
146
|
+
error.value = e?.message ?? 'Error en transición'
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Funnel data — para WorkflowFunnel / Chart ──────────────────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Cuenta de items por step — útil para la vista funnel.
|
|
155
|
+
*/
|
|
156
|
+
const funnelData = computed(() => {
|
|
157
|
+
const counts = Object.fromEntries(
|
|
158
|
+
(definitionRef.value?.steps ?? []).map(s => [s.key, 0])
|
|
159
|
+
)
|
|
160
|
+
for (const item of items.value) {
|
|
161
|
+
const step = item[stateKey]
|
|
162
|
+
if (step in counts) counts[step]++
|
|
163
|
+
}
|
|
164
|
+
return (definitionRef.value?.steps ?? []).map(step => ({
|
|
165
|
+
key: step.key,
|
|
166
|
+
label: step.label,
|
|
167
|
+
type: step.type,
|
|
168
|
+
count: counts[step.key] ?? 0,
|
|
169
|
+
}))
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// ── View mode ──────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
const VALID_VIEWS = ['kanban', 'table', 'flow', 'funnel']
|
|
175
|
+
|
|
176
|
+
const viewMode = ref(VALID_VIEWS.includes(defaultView) ? defaultView : 'kanban')
|
|
177
|
+
const setViewMode = (mode) => {
|
|
178
|
+
if (VALID_VIEWS.includes(mode)) viewMode.value = mode
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Item helpers ───────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
const itemsByStep = computed(() => {
|
|
184
|
+
const map = Object.fromEntries(
|
|
185
|
+
(definitionRef.value?.steps ?? []).map(s => [s.key, []])
|
|
186
|
+
)
|
|
187
|
+
for (const item of items.value) {
|
|
188
|
+
const step = item[stateKey]
|
|
189
|
+
if (step in map) map[step].push(item)
|
|
190
|
+
}
|
|
191
|
+
return map
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const setItems = (newItems) => { items.value = newItems }
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
// Items
|
|
198
|
+
items,
|
|
199
|
+
loading,
|
|
200
|
+
error,
|
|
201
|
+
total,
|
|
202
|
+
load,
|
|
203
|
+
reload,
|
|
204
|
+
setItems,
|
|
205
|
+
itemsByStep,
|
|
206
|
+
// Columnas derivadas
|
|
207
|
+
columns,
|
|
208
|
+
stateKey,
|
|
209
|
+
// Transición
|
|
210
|
+
canTransition,
|
|
211
|
+
availableTransitions,
|
|
212
|
+
getTransition,
|
|
213
|
+
onMove,
|
|
214
|
+
// Funnel
|
|
215
|
+
funnelData,
|
|
216
|
+
// View mode
|
|
217
|
+
viewMode,
|
|
218
|
+
setViewMode,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-layout para workflow definitions.
|
|
3
|
+
*
|
|
4
|
+
* Usa BFS desde el nodo start (en vez de Kahn) para manejar ciclos
|
|
5
|
+
* correctamente. Back-edges (transiciones hacia nodos ya visitados con
|
|
6
|
+
* capa ≤ origen) se marcan como isBackEdge:true y usan handles top para
|
|
7
|
+
* que su path se rutee por encima del row de nodos.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const NODE_W = 208 // w-52 en Tailwind
|
|
11
|
+
const NODE_H = 96 // alto aproximado de StepNode con descripción
|
|
12
|
+
const H_GAP = 100 // espacio horizontal entre columnas (para labels de edge)
|
|
13
|
+
const V_GAP = 40 // espacio vertical entre nodos en misma columna
|
|
14
|
+
|
|
15
|
+
// ── Helpers privados ──────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* BFS desde el nodo start para asignar capa (layer) a cada step.
|
|
19
|
+
* Nodos no alcanzables se colocan en max_layer + 1.
|
|
20
|
+
*/
|
|
21
|
+
function buildLayerMap(steps, transitions) {
|
|
22
|
+
if (!steps.length) return {}
|
|
23
|
+
const keys = steps.map(s => s.key)
|
|
24
|
+
const startKey = steps.find(s => s.type === 'start')?.key ?? keys[0]
|
|
25
|
+
|
|
26
|
+
// Adjacency list (solo forward)
|
|
27
|
+
const adj = Object.fromEntries(keys.map(k => [k, []]))
|
|
28
|
+
for (const t of transitions) {
|
|
29
|
+
if (adj[t.from] !== undefined) adj[t.from].push(t.to)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// BFS
|
|
33
|
+
const layer = {}
|
|
34
|
+
const queue = [startKey]
|
|
35
|
+
layer[startKey] = 0
|
|
36
|
+
let head = 0
|
|
37
|
+
|
|
38
|
+
while (head < queue.length) {
|
|
39
|
+
const node = queue[head++]
|
|
40
|
+
for (const nb of adj[node] ?? []) {
|
|
41
|
+
if (!(nb in layer)) {
|
|
42
|
+
layer[nb] = layer[node] + 1
|
|
43
|
+
queue.push(nb)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Nodos desconectados → capa extra al final
|
|
49
|
+
const maxL = Math.max(-1, ...Object.values(layer))
|
|
50
|
+
for (const k of keys) {
|
|
51
|
+
if (!(k in layer)) layer[k] = maxL + 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return layer
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Convierte un layerMap en posiciones x/y.
|
|
59
|
+
* Cada capa es una columna; los nodos de una misma capa se centran verticalmente.
|
|
60
|
+
*/
|
|
61
|
+
function computePositions(steps, layerMap) {
|
|
62
|
+
const groups = {}
|
|
63
|
+
for (const s of steps) {
|
|
64
|
+
const l = layerMap[s.key] ?? 0
|
|
65
|
+
if (!groups[l]) groups[l] = []
|
|
66
|
+
groups[l].push(s.key)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const positions = {}
|
|
70
|
+
for (const [l, group] of Object.entries(groups)) {
|
|
71
|
+
const col = parseInt(l)
|
|
72
|
+
const totalH = group.length * NODE_H + (group.length - 1) * V_GAP
|
|
73
|
+
const startY = -totalH / 2
|
|
74
|
+
for (let row = 0; row < group.length; row++) {
|
|
75
|
+
positions[group[row]] = {
|
|
76
|
+
x: col * (NODE_W + H_GAP),
|
|
77
|
+
y: startY + row * (NODE_H + V_GAP),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return positions
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── API pública ───────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Calcula posiciones { x, y } para cada step.
|
|
89
|
+
* Exportado para uso directo en Builder (relayout).
|
|
90
|
+
*
|
|
91
|
+
* @param {Array} steps
|
|
92
|
+
* @param {Array} transitions
|
|
93
|
+
* @returns {Record<string, { x: number, y: number }>}
|
|
94
|
+
*/
|
|
95
|
+
export function autoLayoutWorkflow(steps, transitions) {
|
|
96
|
+
const layer = buildLayerMap(steps, transitions)
|
|
97
|
+
return computePositions(steps, layer)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Convierte una workflow definition al formato nodes/edges de Vue Flow.
|
|
102
|
+
*
|
|
103
|
+
* Back-edges (transiciones que van hacia una capa ≤ origen) reciben:
|
|
104
|
+
* - data.isBackEdge = true
|
|
105
|
+
* - sourceHandle: 'top-source' / targetHandle: 'top-target'
|
|
106
|
+
* (StepNode expone esos handles invisibles en Position.Top)
|
|
107
|
+
*/
|
|
108
|
+
export function definitionToFlow(config) {
|
|
109
|
+
const layers = buildLayerMap(config.steps ?? [], config.transitions ?? [])
|
|
110
|
+
const positions = computePositions(config.steps ?? [], layers)
|
|
111
|
+
|
|
112
|
+
const nodes = (config.steps ?? []).map(step => ({
|
|
113
|
+
id: step.key,
|
|
114
|
+
type: 'step',
|
|
115
|
+
position: step.position ?? positions[step.key] ?? { x: 0, y: 0 },
|
|
116
|
+
data: { ...step },
|
|
117
|
+
}))
|
|
118
|
+
|
|
119
|
+
const edges = (config.transitions ?? []).map(t => {
|
|
120
|
+
const fromL = layers[t.from] ?? 0
|
|
121
|
+
const toL = layers[t.to] ?? 0
|
|
122
|
+
const isBack = fromL >= toL
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
id: `${t.from}__${t.to}`,
|
|
126
|
+
source: t.from,
|
|
127
|
+
target: t.to,
|
|
128
|
+
type: 'transition',
|
|
129
|
+
label: t.label ?? '',
|
|
130
|
+
data: { ...t, isBackEdge: isBack },
|
|
131
|
+
markerEnd: { type: 'arrowclosed' },
|
|
132
|
+
// Back-edges: rutear por los handles top para que el path arque por encima
|
|
133
|
+
...(isBack ? { sourceHandle: 'top-source', targetHandle: 'top-target' } : {}),
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return { nodes, edges }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Convierte nodes/edges de Vue Flow de vuelta al config JSON de Laravel.
|
|
142
|
+
* Filtra isBackEdge del data (es metadata de layout, no del dominio).
|
|
143
|
+
*/
|
|
144
|
+
export function flowToDefinition(nodes, edges) {
|
|
145
|
+
const steps = nodes.map(n => ({
|
|
146
|
+
...n.data,
|
|
147
|
+
position: { x: Math.round(n.position.x), y: Math.round(n.position.y) },
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
const transitions = edges.map(e => {
|
|
151
|
+
// eslint-disable-next-line no-unused-vars
|
|
152
|
+
const { isBackEdge, ...rest } = e.data ?? {}
|
|
153
|
+
return {
|
|
154
|
+
...rest,
|
|
155
|
+
from: e.source,
|
|
156
|
+
to: e.target,
|
|
157
|
+
label: e.label ?? rest.label ?? '',
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return { steps, transitions }
|
|
162
|
+
}
|
package/css/tokens.css
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* y solo redefinen estos tokens semánticos. NO redefinen primitivos.
|
|
15
15
|
* ============================================================================ */
|
|
16
16
|
|
|
17
|
-
@theme inline
|
|
17
|
+
/* @theme (sin `inline`) → Tailwind genera `rounded-card`, `rounded-control`, etc.
|
|
18
|
+
* referenciando `var(--radius-card)` etc. en lugar de inline-ar el valor en
|
|
19
|
+
* build-time. Esto permite OVERRIDES en runtime (templates, playground toggles). */
|
|
20
|
+
@theme {
|
|
18
21
|
/* ─── Radius ───────────────────────────────────────────────────────────
|
|
19
22
|
* Categorización por *función*, no por tamaño. */
|
|
20
23
|
--radius-control: var(--radius-md); /* botones, inputs, selects */
|
|
@@ -25,16 +28,13 @@
|
|
|
25
28
|
--radius-badge: var(--radius-full); /* chips, tags, status pills */
|
|
26
29
|
--radius-avatar: var(--radius-full); /* avatares circulares */
|
|
27
30
|
|
|
28
|
-
/* ─── Fields (controles de formulario) ─────────────────────────────────
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
--field-height: 2.5rem; /* 40px */
|
|
33
|
-
--field-height-sm: 2rem; /* 32px */
|
|
34
|
-
--field-padding-x: 0.875rem; /* 14px */
|
|
31
|
+
/* ─── Fields (controles de formulario) ───────────────────────────────── */
|
|
32
|
+
--field-height: 2.5rem;
|
|
33
|
+
--field-height-sm: 2rem;
|
|
34
|
+
--field-padding-x: 0.875rem;
|
|
35
35
|
--field-border-width: 1px;
|
|
36
|
-
--field-font-size: 0.875rem;
|
|
37
|
-
--field-font-size-sm: 0.8125rem;
|
|
36
|
+
--field-font-size: 0.875rem;
|
|
37
|
+
--field-font-size-sm: 0.8125rem;
|
|
38
38
|
--field-bg: var(--color-card);
|
|
39
39
|
--field-dropdown-bg: var(--field-bg);
|
|
40
40
|
--field-border: var(--color-card-line);
|