stash
This commit is contained in:
parent
796fef81be
commit
7df585d774
11 changed files with 246 additions and 72 deletions
|
|
@ -125,12 +125,12 @@ class StorageLocation(models.Model):
|
|||
class WorkflowInstance(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
state = models.CharField(max_length=255)
|
||||
payload = models.JSONField(default=dict, blank=True)
|
||||
payload = models.TextField(default='', blank=True) # an opaque, frontend-serialized JSON string on the backend.
|
||||
owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='workflows')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.status})"
|
||||
return f"{self.name} ({self.state})"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from toolshed.models import Category, Tag, Property, InventoryItem, ItemProperty, StorageLocation, WorkflowInstance
|
||||
import json
|
||||
|
||||
|
||||
class CategoryTestMixin:
|
||||
|
|
@ -58,28 +59,29 @@ class LocationTestMixin:
|
|||
|
||||
class WorkflowTestMixin:
|
||||
def prepare_workflows(self):
|
||||
# `payload` is an opaque, frontend-serialized JSON string on the backend.
|
||||
self.f['workflow1'] = WorkflowInstance.objects.create(
|
||||
name='workflow1',
|
||||
state='initial',
|
||||
payload={},
|
||||
payload=json.dumps({}),
|
||||
owner=self.f['local_user1']
|
||||
)
|
||||
self.f['workflow2'] = WorkflowInstance.objects.create(
|
||||
name='workflow1',
|
||||
state='upload',
|
||||
payload={'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2']},
|
||||
payload=json.dumps({'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2']}),
|
||||
owner=self.f['local_user1']
|
||||
)
|
||||
self.f['workflow3'] = WorkflowInstance.objects.create(
|
||||
name='workflow1',
|
||||
state='describe',
|
||||
payload={'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2', 'a1b2c3d4e5f60718293a4b5c6d7e8f90', 'b1c2d3e4f5a60718293b4c5d6e7f8090'],
|
||||
'descriptions': ['file 1 description']},
|
||||
payload=json.dumps({'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2', 'a1b2c3d4e5f60718293a4b5c6d7e8f90', 'b1c2d3e4f5a60718293b4c5d6e7f8090'],
|
||||
'descriptions': ['file 1 description']}),
|
||||
owner=self.f['local_user1']
|
||||
)
|
||||
self.f['workflow_user2'] = WorkflowInstance.objects.create(
|
||||
name='workflow2',
|
||||
state='initial',
|
||||
payload={},
|
||||
payload=json.dumps({}),
|
||||
owner=self.f['local_user2']
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ class WorkflowInstanceApiTestCase(UserTestMixin, WorkflowTestMixin, ToolshedTest
|
|||
self.assertEqual(reply.data[0]['state'], 'initial')
|
||||
self.assertEqual(reply.data[1]['state'], 'upload')
|
||||
self.assertEqual(reply.data[2]['state'], 'describe')
|
||||
self.assertEqual(reply.data[0]['payload'], {})
|
||||
self.assertEqual(reply.data[1]['payload'], {'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2']})
|
||||
self.assertEqual(reply.data[2]['payload'], {'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2',
|
||||
self.assertEqual(json.loads(reply.data[0]['payload']), {})
|
||||
self.assertEqual(json.loads(reply.data[1]['payload']), {'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2']})
|
||||
self.assertEqual(json.loads(reply.data[2]['payload']), {'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2',
|
||||
'a1b2c3d4e5f60718293a4b5c6d7e8f90',
|
||||
'b1c2d3e4f5a60718293b4c5d6e7f8090'],
|
||||
'descriptions': ['file 1 description']})
|
||||
|
|
@ -44,5 +44,5 @@ class WorkflowInstanceApiTestCase(UserTestMixin, WorkflowTestMixin, ToolshedTest
|
|||
self.assertEqual(len(reply.data), 1)
|
||||
self.assertEqual(reply.data[0]['name'], 'workflow2')
|
||||
self.assertEqual(reply.data[0]['state'], 'initial')
|
||||
self.assertEqual(reply.data[0]['payload'], {})
|
||||
self.assertEqual(json.loads(reply.data[0]['payload']), {})
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import FotoFirstStep2 from './steps/FotoFirstStep2.vue';
|
|||
import FotoFirstStep3 from './steps/FotoFirstStep3.vue';
|
||||
import FotoFirstStep4 from './steps/FotoFirstStep4.vue';
|
||||
import BulkImportStep1 from './steps/BulkImportStep1.vue';
|
||||
import BulkImportStep6 from './steps/BulkImportStep6.vue';
|
||||
|
||||
/**
|
||||
* Component registry mapping workflow types and steps to components
|
||||
|
|
@ -25,6 +26,7 @@ const componentRegistry = {
|
|||
|
||||
// Bulk Item Import Workflow components
|
||||
'import-items-1': BulkImportStep1,
|
||||
'import-items-6': BulkImportStep6,
|
||||
// Additional steps can be added as needed
|
||||
// 'import-items-2': BulkImportStep2,
|
||||
// 'import-items-3': BulkImportStep3,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ The `stepComponent` computed property now uses the registry:
|
|||
|
||||
```javascript
|
||||
stepComponent() {
|
||||
const workflowType = this.workflowInstance?.workflow_type;
|
||||
// `name` on the WorkflowInstance doubles as the workflow type identifier
|
||||
const workflowType = this.workflowInstance?.name;
|
||||
if (workflowType && this.currentStep) {
|
||||
return getStepComponent(workflowType, this.currentStep);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ export default {
|
|||
isDragOver: false,
|
||||
fileAnalysis: null,
|
||||
detectedColumns: [],
|
||||
parsedRows: [],
|
||||
previewData: [],
|
||||
columnMapping: {},
|
||||
requiredFields: [
|
||||
|
|
@ -239,11 +240,11 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
// Load existing data if resuming
|
||||
if (this.payload.uploaded_file) {
|
||||
this.uploadedFile = this.payload.uploaded_file;
|
||||
if (this.payload.file_name) {
|
||||
this.uploadTime = this.payload.upload_time;
|
||||
this.fileAnalysis = this.payload.file_analysis;
|
||||
this.detectedColumns = this.payload.detected_columns || [];
|
||||
this.parsedRows = this.payload.parsed_rows || [];
|
||||
this.previewData = this.payload.preview_data || [];
|
||||
this.columnMapping = this.payload.column_mapping || {};
|
||||
}
|
||||
|
|
@ -296,55 +297,66 @@ export default {
|
|||
},
|
||||
|
||||
async analyzeFile(file) {
|
||||
// This is a simplified version - in reality, you'd use a library like Papa Parse for CSV
|
||||
// or SheetJS for Excel files
|
||||
|
||||
// Parsing happens entirely client-side. The backend never sees the
|
||||
// raw file - only the structured `items` list produced here, sent
|
||||
// later as generic payload/request body.
|
||||
if (file.name.endsWith('.csv')) {
|
||||
const text = await file.text();
|
||||
const lines = text.split('\n').filter(line => line.trim());
|
||||
|
||||
if (lines.length > 0) {
|
||||
// Parse CSV header
|
||||
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
|
||||
this.detectedColumns = headers;
|
||||
|
||||
// Parse preview data
|
||||
this.previewData = lines.slice(1, 6).map(line => {
|
||||
const values = line.split(',').map(v => v.trim().replace(/"/g, ''));
|
||||
const row = {};
|
||||
headers.forEach((header, index) => {
|
||||
row[header] = values[index] || '';
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
this.fileAnalysis = {
|
||||
totalRows: lines.length - 1, // Exclude header
|
||||
totalColumns: headers.length,
|
||||
validRows: lines.length - 1, // Simplified - assume all valid for demo
|
||||
errorRows: 0
|
||||
};
|
||||
}
|
||||
this.parsedRows = this.parseCsv(text);
|
||||
} else {
|
||||
// For Excel files, you'd use a library like SheetJS
|
||||
// This is a mock implementation
|
||||
// For Excel files, you'd use a library like SheetJS to parse
|
||||
// client-side. This is a mock implementation.
|
||||
this.detectedColumns = ['Name', 'Category', 'Quantity', 'Unit', 'Description'];
|
||||
this.previewData = [
|
||||
this.parsedRows = [
|
||||
{ Name: 'Sample Item 1', Category: 'Tools', Quantity: '1', Unit: 'piece', Description: 'Sample description' },
|
||||
{ Name: 'Sample Item 2', Category: 'Hardware', Quantity: '5', Unit: 'box', Description: 'Another sample' }
|
||||
];
|
||||
this.fileAnalysis = {
|
||||
totalRows: 100,
|
||||
totalColumns: 5,
|
||||
validRows: 98,
|
||||
errorRows: 2
|
||||
};
|
||||
}
|
||||
|
||||
this.previewData = this.parsedRows.slice(0, 5);
|
||||
this.fileAnalysis = {
|
||||
totalRows: this.parsedRows.length,
|
||||
totalColumns: this.detectedColumns.length,
|
||||
validRows: this.parsedRows.length,
|
||||
errorRows: 0
|
||||
};
|
||||
|
||||
// Auto-map columns based on common names
|
||||
this.autoMapColumns();
|
||||
},
|
||||
|
||||
// Simple CSV parser: no external dependency, entirely client-side.
|
||||
// Returns an array of row objects keyed by the detected header columns.
|
||||
parseCsv(text) {
|
||||
const lines = text.split('\n').filter(line => line.trim());
|
||||
if (lines.length === 0) {
|
||||
this.detectedColumns = [];
|
||||
return [];
|
||||
}
|
||||
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
|
||||
this.detectedColumns = headers;
|
||||
return lines.slice(1).map(line => {
|
||||
const values = line.split(',').map(v => v.trim().replace(/"/g, ''));
|
||||
const row = {};
|
||||
headers.forEach((header, index) => {
|
||||
row[header] = values[index] || '';
|
||||
});
|
||||
return row;
|
||||
});
|
||||
},
|
||||
|
||||
// Convert the fully parsed rows + column mapping into the generic item
|
||||
// dicts consumed by the "Import Items" step, which creates each one via
|
||||
// the standard generic InventoryItem create endpoint.
|
||||
buildItemsFromMapping() {
|
||||
return this.parsedRows.map(row => ({
|
||||
name: this.columnMapping.name ? row[this.columnMapping.name] : '',
|
||||
description: this.columnMapping.description ? row[this.columnMapping.description] : '',
|
||||
category: this.columnMapping.category ? row[this.columnMapping.category] : '',
|
||||
quantity: this.columnMapping.quantity ? row[this.columnMapping.quantity] : undefined,
|
||||
})).filter(item => item.name);
|
||||
},
|
||||
|
||||
autoMapColumns() {
|
||||
const mapping = {};
|
||||
|
||||
|
|
@ -401,6 +413,7 @@ export default {
|
|||
this.uploadTime = null;
|
||||
this.fileAnalysis = null;
|
||||
this.detectedColumns = [];
|
||||
this.parsedRows = [];
|
||||
this.previewData = [];
|
||||
this.columnMapping = {};
|
||||
this.updatePayload();
|
||||
|
|
@ -429,12 +442,14 @@ export default {
|
|||
|
||||
updatePayload() {
|
||||
this.$emit('update', {
|
||||
uploaded_file: this.uploadedFile,
|
||||
file_name: this.uploadedFile?.name || null,
|
||||
upload_time: this.uploadTime,
|
||||
file_analysis: this.fileAnalysis,
|
||||
detected_columns: this.detectedColumns,
|
||||
parsed_rows: this.parsedRows,
|
||||
preview_data: this.previewData,
|
||||
column_mapping: this.columnMapping
|
||||
column_mapping: this.columnMapping,
|
||||
items: this.buildItemsFromMapping()
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
124
frontend/src/components/workflow/steps/BulkImportStep6.vue
Normal file
124
frontend/src/components/workflow/steps/BulkImportStep6.vue
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<div class="bulk-import-step-6">
|
||||
<div class="step-header mb-4">
|
||||
<h4 class="mb-2">Import Items</h4>
|
||||
<p class="text-muted">
|
||||
Create inventory items from the rows parsed in the File Upload step.
|
||||
The file itself was never sent to the server - only these structured
|
||||
item rows are.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!results" class="card mb-4">
|
||||
<div class="card-body text-center py-4">
|
||||
<p class="mb-3">
|
||||
<strong>{{ items.length }}</strong> item(s) ready to import.
|
||||
</p>
|
||||
<button class="btn btn-primary" :disabled="importing || items.length === 0" @click="runImport">
|
||||
<span v-if="importing" class="spinner-border spinner-border-sm me-2"></span>
|
||||
Import {{ items.length }} item(s)
|
||||
</button>
|
||||
<div v-if="error" class="alert alert-danger mt-3 mb-0">{{ error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="mb-2">Import complete</h6>
|
||||
<p class="mb-2">
|
||||
<span class="badge bg-success me-2">{{ results.created_count }} created</span>
|
||||
<span v-if="results.errors && results.errors.length" class="badge bg-warning text-dark">
|
||||
{{ results.errors.length }} issue(s)
|
||||
</span>
|
||||
</p>
|
||||
<ul v-if="results.errors && results.errors.length" class="small text-muted mb-0">
|
||||
<li v-for="(err, index) in results.errors" :key="index">{{ err }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-navigation d-flex justify-content-between">
|
||||
<button class="btn btn-outline-secondary" @click="$emit('prev')">
|
||||
<b-icon-chevron-left class="me-1"></b-icon-chevron-left>
|
||||
Previous
|
||||
</button>
|
||||
<button class="btn btn-primary" :disabled="!results" @click="$emit('next')">
|
||||
Next: Generate Report
|
||||
<b-icon-chevron-right class="ms-1"></b-icon-chevron-right>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as BIcons from "bootstrap-icons-vue";
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'BulkImportStep6',
|
||||
components: {
|
||||
...BIcons
|
||||
},
|
||||
props: {
|
||||
workflowInstance: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
step: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
payload: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
importing: false,
|
||||
error: null,
|
||||
results: this.payload.import_results || null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
items() {
|
||||
return this.payload.items || [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['createInventoryItem']),
|
||||
async runImport() {
|
||||
this.importing = true;
|
||||
this.error = null;
|
||||
const created_item_ids = [];
|
||||
const errors = [];
|
||||
// Items were fully parsed client-side (File Upload step). The backend
|
||||
// never sees the source file or any bulk-import-specific endpoint -
|
||||
// each row is created through the same generic
|
||||
// POST /api/inventory_items/ endpoint any other client would use.
|
||||
for (const [index, row] of this.items.entries()) {
|
||||
if (!row.name) {
|
||||
errors.push(`Row ${index + 1}: missing required "name" field, skipped.`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const created = await this.createInventoryItem({
|
||||
name: row.name,
|
||||
description: row.description || '',
|
||||
owned_quantity: parseInt(row.quantity) || 1,
|
||||
...(row.category ? {category: row.category} : {})
|
||||
});
|
||||
created_item_ids.push(created.id);
|
||||
} catch (err) {
|
||||
console.error('Failed to create item:', row, err);
|
||||
errors.push(`Row ${index + 1} ("${row.name}"): failed to create.`);
|
||||
}
|
||||
}
|
||||
this.results = {created_item_ids, created_count: created_item_ids.length, errors};
|
||||
this.$emit('update', {import_results: this.results});
|
||||
this.importing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -4,6 +4,7 @@ import FallBackResolver from "@/dns";
|
|||
import NeighborsCache from "@/neigbors";
|
||||
import {createNullAuth, createSignAuth, createTokenAuth, ServerSet, ServerSetUnion} from "@/federation";
|
||||
import {parseIdentityRecord, serializeIdentityRecord} from "@/identity";
|
||||
import {serializeWorkflowPayload, deserializeWorkflowPayload} from "@/workflows.js";
|
||||
//import sharedStatePlugin from "@/../extras/shared-state-plugin";
|
||||
//import persistentStatePlugin from "@/../extras/persistent-state-plugin";
|
||||
|
||||
|
|
@ -549,30 +550,33 @@ export default createStore({
|
|||
}
|
||||
const servers = await dispatch('getHomeServers')
|
||||
const data = await servers.get(getters.signAuth, '/api/workflows/')
|
||||
commit('setActiveWorkflows', data)
|
||||
const workflows = data.map(deserializeWorkflowPayload)
|
||||
commit('setActiveWorkflows', workflows)
|
||||
state.last_load.active_workflows = Date.now()
|
||||
return data
|
||||
return workflows
|
||||
},
|
||||
async createWorkflow({state, commit, dispatch, getters}, workflowData) {
|
||||
const servers = await dispatch('getHomeServers')
|
||||
const data = await servers.post(getters.signAuth, '/api/workflows/', workflowData)
|
||||
// The backend stores `payload` as an opaque string - the frontend is
|
||||
// responsible for serializing/deserializing the JSON itself.
|
||||
const data = await servers.post(getters.signAuth, '/api/workflows/', serializeWorkflowPayload(workflowData))
|
||||
state.last_load.active_workflows = 0 // Invalidate cache
|
||||
return data
|
||||
return deserializeWorkflowPayload(data)
|
||||
},
|
||||
async updateWorkflow({state, commit, dispatch, getters}, workflow) {
|
||||
const servers = await dispatch('getHomeServers')
|
||||
const data = await servers.patch(getters.signAuth, '/api/workflows/' + workflow.id + '/', workflow)
|
||||
const data = await servers.patch(getters.signAuth, '/api/workflows/' + workflow.id + '/', serializeWorkflowPayload(workflow))
|
||||
state.last_load.active_workflows = 0 // Invalidate cache
|
||||
return data
|
||||
return deserializeWorkflowPayload(data)
|
||||
},
|
||||
async updateWorkflowStep({state, commit, dispatch, getters}, {workflowId, currentStep, payload}) {
|
||||
const servers = await dispatch('getHomeServers')
|
||||
const data = await servers.post(getters.signAuth, '/api/workflows/' + workflowId + '/update_step/', {
|
||||
current_step: currentStep,
|
||||
payload: payload
|
||||
payload: JSON.stringify(payload)
|
||||
})
|
||||
state.last_load.active_workflows = 0 // Invalidate cache
|
||||
return data
|
||||
return deserializeWorkflowPayload(data)
|
||||
},
|
||||
async deleteWorkflow({state, commit, dispatch, getters}, workflowId) {
|
||||
const servers = await dispatch('getHomeServers')
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@
|
|||
<li class="breadcrumb-item">
|
||||
<router-link to="/workflows" class="text-decoration-none">Workflows</router-link>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ workflowInstance?.workflow_type || 'Loading...' }}</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ workflowDefinition?.name || workflowInstance?.name || 'Loading...' }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="h3 mb-0">{{ workflowInstance?.workflow_type || 'Workflow Detail' }}</h1>
|
||||
<h1 class="h3 mb-0">{{ workflowDefinition?.name || workflowInstance?.name || 'Workflow Detail' }}</h1>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-outline-secondary" @click="$router.go(-1)">
|
||||
|
|
@ -236,8 +236,9 @@ export default {
|
|||
},
|
||||
|
||||
workflowDefinition() {
|
||||
if (!this.workflowInstance?.workflow_type) return null;
|
||||
return workflowRegistry.get(this.workflowInstance.workflow_type);
|
||||
// `name` doubles as the workflow type identifier (e.g. 'import-items').
|
||||
if (!this.workflowInstance?.name) return null;
|
||||
return workflowRegistry.get(this.workflowInstance.name);
|
||||
},
|
||||
|
||||
stepDefinitions() {
|
||||
|
|
@ -258,8 +259,7 @@ export default {
|
|||
|
||||
stepComponent() {
|
||||
// Return step-specific component if it exists using the component registry
|
||||
const workflowType = this.workflowInstance?.workflow_type;
|
||||
console.log('Determining step component for workflow type:', this.workflowInstance, 'and step:', this.currentStep);
|
||||
const workflowType = this.workflowInstance?.name;
|
||||
if (workflowType && this.currentStep) {
|
||||
return getStepComponent(workflowType, this.currentStep);
|
||||
}
|
||||
|
|
@ -467,7 +467,7 @@ export default {
|
|||
|
||||
hasStepComponent(stepNumber) {
|
||||
// Check if a specific step has a custom component available
|
||||
const workflowType = this.workflowInstance?.workflow_type;
|
||||
const workflowType = this.workflowInstance?.name;
|
||||
return workflowType ? hasStepComponent(workflowType, stepNumber) : false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
<tbody>
|
||||
<tr v-for="workflow in activeWorkflows" :key="workflow.id">
|
||||
<td>
|
||||
<strong>{{ workflow.workflow_type }}</strong>
|
||||
<strong>{{ getWorkflowDisplayName(workflow) }}</strong>
|
||||
<br>
|
||||
<small class="text-muted">{{ workflow.payload?.workflow_config?.category || 'System' }}</small>
|
||||
</td>
|
||||
|
|
@ -215,10 +215,14 @@ export default {
|
|||
},
|
||||
isWorkflowRunning(workflowId) {
|
||||
return this.activeWorkflows.some(active =>
|
||||
active.workflow_type === workflowId &&
|
||||
active.name === workflowId &&
|
||||
active.state === 'running'
|
||||
);
|
||||
},
|
||||
getWorkflowDisplayName(workflow) {
|
||||
// `name` doubles as the workflow type identifier (e.g. 'import-items').
|
||||
return workflowRegistry.get(workflow.name)?.name || workflow.name;
|
||||
},
|
||||
async startWorkflow(workflow) {
|
||||
try {
|
||||
this.loading = true;
|
||||
|
|
@ -266,16 +270,17 @@ export default {
|
|||
});
|
||||
},
|
||||
async abortWorkflowInstance(workflow) {
|
||||
if (confirm(`Are you sure you want to abort "${workflow.workflow_type}"?`)) {
|
||||
const displayName = this.getWorkflowDisplayName(workflow);
|
||||
if (confirm(`Are you sure you want to abort "${displayName}"?`)) {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
await this.deleteWorkflow(workflow.id);
|
||||
await this.loadActiveWorkflows();
|
||||
console.log('Workflow aborted successfully:', workflow.workflow_type);
|
||||
console.log('Workflow aborted successfully:', displayName);
|
||||
} catch (error) {
|
||||
console.error('Error aborting workflow:', error);
|
||||
this.error = `Failed to abort ${workflow.workflow_type}`;
|
||||
this.error = `Failed to abort ${displayName}`;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -404,6 +404,27 @@ class WorkflowRegistry {
|
|||
// Create and export the default registry instance
|
||||
export const workflowRegistry = new WorkflowRegistry();
|
||||
|
||||
/**
|
||||
* The backend stores WorkflowInstance.payload as an opaque string - it never
|
||||
* parses or understands it as JSON. The frontend is fully responsible for
|
||||
* serializing it before sending and deserializing it after receiving.
|
||||
*/
|
||||
export function serializeWorkflowPayload(workflow) {
|
||||
if (!workflow || !('payload' in workflow)) return workflow;
|
||||
return {...workflow, payload: JSON.stringify(workflow.payload ?? {})};
|
||||
}
|
||||
|
||||
export function deserializeWorkflowPayload(workflow) {
|
||||
if (!workflow) return workflow;
|
||||
let payload = {};
|
||||
try {
|
||||
payload = workflow.payload ? JSON.parse(workflow.payload) : {};
|
||||
} catch (e) {
|
||||
console.error('Failed to parse workflow payload JSON:', e);
|
||||
}
|
||||
return {...workflow, payload};
|
||||
}
|
||||
|
||||
// Export individual classes for direct use
|
||||
export {
|
||||
BaseWorkflow,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue