Files
dss/admin-ui/js/core/workflows.js
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

512 lines
12 KiB
JavaScript

/**
* DSS Workflow State Machines
*
* State machine implementation for orchestrating multi-step workflows
* with transition guards, side effects, and progress tracking.
*
* @module workflows
*/
import { notifySuccess, notifyError, notifyInfo, ErrorCode } from './messaging.js';
import router from './router.js';
/**
* Base State Machine class
*/
class StateMachine {
constructor(config) {
this.config = config;
this.currentState = config.initial;
this.previousState = null;
this.context = {};
this.history = [];
this.listeners = new Map();
}
/**
* Get current state definition
*/
getCurrentStateDefinition() {
return this.config.states[this.currentState];
}
/**
* Check if transition is allowed
* @param {string} event - Event to transition on
* @returns {string|null} Next state or null if not allowed
*/
canTransition(event) {
const stateDefinition = this.getCurrentStateDefinition();
if (!stateDefinition || !stateDefinition.on) {
return null;
}
return stateDefinition.on[event] || null;
}
/**
* Send an event to trigger state transition
* @param {string} event - Event name
* @param {Object} [data] - Event data
* @returns {Promise<boolean>} Whether transition succeeded
*/
async send(event, data = {}) {
const nextState = this.canTransition(event);
if (!nextState) {
console.warn(`No transition for event "${event}" in state "${this.currentState}"`);
return false;
}
// Call exit actions for current state
await this.callActions(this.getCurrentStateDefinition().exit, { event, data });
// Store previous state
this.previousState = this.currentState;
// Transition to next state
this.currentState = nextState;
// Record history
this.history.push({
from: this.previousState,
to: this.currentState,
event,
timestamp: new Date().toISOString(),
data,
});
// Call entry actions for new state
await this.callActions(this.getCurrentStateDefinition().entry, { event, data });
// Emit state change
this.emit('stateChange', {
previous: this.previousState,
current: this.currentState,
event,
data,
});
return true;
}
/**
* Call state actions
* @param {string[]|undefined} actions - Action names to execute
* @param {Object} context - Action context
*/
async callActions(actions, context) {
if (!actions || !Array.isArray(actions)) {
return;
}
for (const actionName of actions) {
const action = this.config.actions?.[actionName];
if (action) {
try {
await action.call(this, { ...context, machine: this });
} catch (error) {
console.error(`Action "${actionName}" failed:`, error);
}
}
}
}
/**
* Check if machine is in a specific state
* @param {string} state - State name
* @returns {boolean} Whether machine is in that state
*/
isIn(state) {
return this.currentState === state;
}
/**
* Check if machine is in final state
* @returns {boolean} Whether machine is in final state
*/
isFinal() {
const stateDefinition = this.getCurrentStateDefinition();
return stateDefinition?.type === 'final' || false;
}
/**
* Subscribe to machine events
* @param {string} event - Event name
* @param {Function} handler - Event handler
* @returns {Function} Unsubscribe function
*/
on(event, handler) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(handler);
// Return unsubscribe function
return () => {
const handlers = this.listeners.get(event);
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
};
}
/**
* Emit event to listeners
* @param {string} event - Event name
* @param {*} data - Event data
*/
emit(event, data) {
const handlers = this.listeners.get(event) || [];
handlers.forEach(handler => {
try {
handler(data);
} catch (error) {
console.error(`Event handler error for "${event}":`, error);
}
});
}
/**
* Get workflow progress
* @returns {Object} Progress information
*/
getProgress() {
const states = Object.keys(this.config.states);
const currentIndex = states.indexOf(this.currentState);
const total = states.length;
return {
current: currentIndex + 1,
total,
percentage: Math.round(((currentIndex + 1) / total) * 100),
state: this.currentState,
isComplete: this.isFinal(),
};
}
/**
* Reset machine to initial state
*/
reset() {
this.currentState = this.config.initial;
this.previousState = null;
this.context = {};
this.history = [];
this.emit('reset', {});
}
/**
* Get state history
* @returns {Array} State transition history
*/
getHistory() {
return [...this.history];
}
}
/**
* Create Project Workflow
*
* Guides user through: Create Project → Configure Settings → Extract Tokens → Success
*/
export class CreateProjectWorkflow extends StateMachine {
constructor(options = {}) {
const config = {
initial: 'init',
states: {
init: {
on: {
CREATE_PROJECT: 'creating',
},
entry: ['showCreateForm'],
},
creating: {
on: {
PROJECT_CREATED: 'created',
CREATE_FAILED: 'init',
},
entry: ['createProject'],
},
created: {
on: {
CONFIGURE_SETTINGS: 'configuring',
SKIP_CONFIG: 'ready',
},
entry: ['showSuccessMessage', 'promptConfiguration'],
},
configuring: {
on: {
SETTINGS_SAVED: 'ready',
CONFIG_CANCELLED: 'created',
},
entry: ['navigateToSettings'],
},
ready: {
on: {
EXTRACT_TOKENS: 'extracting',
RECONFIGURE: 'configuring',
},
entry: ['showReadyMessage'],
},
extracting: {
on: {
EXTRACTION_SUCCESS: 'complete',
EXTRACTION_FAILED: 'ready',
},
entry: ['extractTokens'],
},
complete: {
type: 'final',
entry: ['showCompletionMessage', 'navigateToTokens'],
},
},
actions: {
showCreateForm: async ({ machine }) => {
notifyInfo('Create a new project to get started');
if (options.onShowCreateForm) {
await options.onShowCreateForm(machine);
}
},
createProject: async ({ data, machine }) => {
machine.context.projectName = data.projectName;
machine.context.projectId = data.projectId;
if (options.onCreateProject) {
await options.onCreateProject(machine.context);
}
},
showSuccessMessage: async ({ machine }) => {
notifySuccess(
`Project "${machine.context.projectName}" created successfully!`,
ErrorCode.SUCCESS_CREATED,
{ projectId: machine.context.projectId }
);
},
promptConfiguration: async ({ machine }) => {
notifyInfo(
'Next: Configure your project settings (Figma key, description)',
{ duration: 7000 }
);
if (options.onPromptConfiguration) {
await options.onPromptConfiguration(machine);
}
},
navigateToSettings: async ({ machine }) => {
router.navigate('figma');
if (options.onNavigateToSettings) {
await options.onNavigateToSettings(machine);
}
},
showReadyMessage: async ({ machine }) => {
notifyInfo('Project configured! Ready to extract tokens from Figma');
if (options.onReady) {
await options.onReady(machine);
}
},
extractTokens: async ({ machine }) => {
if (options.onExtractTokens) {
await options.onExtractTokens(machine);
}
},
showCompletionMessage: async ({ machine }) => {
notifySuccess(
'Tokens extracted successfully! Your design system is ready.',
ErrorCode.SUCCESS_OPERATION
);
if (options.onComplete) {
await options.onComplete(machine);
}
},
navigateToTokens: async ({ machine }) => {
router.navigate('tokens');
},
},
};
super(config);
// Store options
this.options = options;
// Subscribe to progress updates
this.on('stateChange', ({ current, previous }) => {
const progress = this.getProgress();
if (options.onProgress) {
options.onProgress(current, progress);
}
// Emit to global event bus
window.dispatchEvent(new CustomEvent('workflow-progress', {
detail: {
workflow: 'create-project',
current,
previous,
progress,
}
}));
});
}
/**
* Start the workflow
* @param {Object} data - Initial data
*/
async start(data = {}) {
this.reset();
this.context = { ...data };
await this.send('CREATE_PROJECT', data);
}
}
/**
* Token Extraction Workflow
*
* Guides through: Connect Figma → Select File → Extract → Sync
*/
export class TokenExtractionWorkflow extends StateMachine {
constructor(options = {}) {
const config = {
initial: 'disconnected',
states: {
disconnected: {
on: {
CONNECT_FIGMA: 'connecting',
},
entry: ['promptFigmaConnection'],
},
connecting: {
on: {
CONNECTION_SUCCESS: 'connected',
CONNECTION_FAILED: 'disconnected',
},
entry: ['testFigmaConnection'],
},
connected: {
on: {
SELECT_FILE: 'fileSelected',
DISCONNECT: 'disconnected',
},
entry: ['showFileSelector'],
},
fileSelected: {
on: {
EXTRACT: 'extracting',
CHANGE_FILE: 'connected',
},
entry: ['showExtractButton'],
},
extracting: {
on: {
EXTRACT_SUCCESS: 'extracted',
EXTRACT_FAILED: 'fileSelected',
},
entry: ['performExtraction'],
},
extracted: {
on: {
SYNC: 'syncing',
EXTRACT_AGAIN: 'extracting',
},
entry: ['showSyncOption'],
},
syncing: {
on: {
SYNC_SUCCESS: 'complete',
SYNC_FAILED: 'extracted',
},
entry: ['performSync'],
},
complete: {
type: 'final',
entry: ['showSuccess'],
},
},
actions: {
promptFigmaConnection: async () => {
notifyInfo('Connect to Figma to extract design tokens');
},
testFigmaConnection: async ({ data, machine }) => {
if (options.onTestConnection) {
await options.onTestConnection(data);
}
},
showFileSelector: async () => {
notifySuccess('Connected to Figma! Select a file to extract tokens.');
},
showExtractButton: async ({ data }) => {
notifyInfo(`File selected: ${data.fileName || 'Unknown'}. Ready to extract.`);
},
performExtraction: async ({ data, machine }) => {
if (options.onExtract) {
await options.onExtract(data);
}
},
showSyncOption: async () => {
notifySuccess('Tokens extracted! Sync to your codebase?');
},
performSync: async ({ data }) => {
if (options.onSync) {
await options.onSync(data);
}
},
showSuccess: async () => {
notifySuccess('Workflow complete! Tokens are synced and ready to use.');
},
},
};
super(config);
this.options = options;
}
async start(data = {}) {
this.reset();
await this.send('CONNECT_FIGMA', data);
}
}
/**
* Create workflow instances
*/
export function createProjectWorkflow(options) {
return new CreateProjectWorkflow(options);
}
export function tokenExtractionWorkflow(options) {
return new TokenExtractionWorkflow(options);
}
export { StateMachine };
export default {
StateMachine,
CreateProjectWorkflow,
TokenExtractionWorkflow,
createProjectWorkflow,
tokenExtractionWorkflow,
};