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
This commit is contained in:
511
admin-ui/js/core/workflows.js
Normal file
511
admin-ui/js/core/workflows.js
Normal file
@@ -0,0 +1,511 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
Reference in New Issue
Block a user