From 7df585d774e7568fb7b5008ba13191bf48bc3584 Mon Sep 17 00:00:00 2001 From: jedi Date: Thu, 23 Jul 2026 05:17:44 +0200 Subject: [PATCH] stash --- backend/toolshed/models.py | 4 +- backend/toolshed/tests/fixtures.py | 12 +- backend/toolshed/tests/test_workflow_api.py | 8 +- .../components/workflow/ComponentRegistry.js | 2 + frontend/src/components/workflow/README.md | 3 +- .../workflow/steps/BulkImportStep1.vue | 95 ++++++++------ .../workflow/steps/BulkImportStep6.vue | 124 ++++++++++++++++++ frontend/src/store.js | 20 +-- frontend/src/views/WorkflowDetail.vue | 14 +- frontend/src/views/Workflows.vue | 15 ++- frontend/src/workflows.js | 21 +++ 11 files changed, 246 insertions(+), 72 deletions(-) create mode 100644 frontend/src/components/workflow/steps/BulkImportStep6.vue diff --git a/backend/toolshed/models.py b/backend/toolshed/models.py index a2a8d7d..ea038a7 100644 --- a/backend/toolshed/models.py +++ b/backend/toolshed/models.py @@ -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})" diff --git a/backend/toolshed/tests/fixtures.py b/backend/toolshed/tests/fixtures.py index 7e0cb49..79b573d 100644 --- a/backend/toolshed/tests/fixtures.py +++ b/backend/toolshed/tests/fixtures.py @@ -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'] ) diff --git a/backend/toolshed/tests/test_workflow_api.py b/backend/toolshed/tests/test_workflow_api.py index 6921aa7..98489df 100644 --- a/backend/toolshed/tests/test_workflow_api.py +++ b/backend/toolshed/tests/test_workflow_api.py @@ -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']), {}) diff --git a/frontend/src/components/workflow/ComponentRegistry.js b/frontend/src/components/workflow/ComponentRegistry.js index f938458..48a83ca 100644 --- a/frontend/src/components/workflow/ComponentRegistry.js +++ b/frontend/src/components/workflow/ComponentRegistry.js @@ -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, diff --git a/frontend/src/components/workflow/README.md b/frontend/src/components/workflow/README.md index b91a72d..50969e1 100644 --- a/frontend/src/components/workflow/README.md +++ b/frontend/src/components/workflow/README.md @@ -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); } diff --git a/frontend/src/components/workflow/steps/BulkImportStep1.vue b/frontend/src/components/workflow/steps/BulkImportStep1.vue index 75a9722..54ede66 100644 --- a/frontend/src/components/workflow/steps/BulkImportStep1.vue +++ b/frontend/src/components/workflow/steps/BulkImportStep1.vue @@ -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() }); }, diff --git a/frontend/src/components/workflow/steps/BulkImportStep6.vue b/frontend/src/components/workflow/steps/BulkImportStep6.vue new file mode 100644 index 0000000..ff67fa6 --- /dev/null +++ b/frontend/src/components/workflow/steps/BulkImportStep6.vue @@ -0,0 +1,124 @@ + + + + diff --git a/frontend/src/store.js b/frontend/src/store.js index 09c1fed..1dee5a6 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -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') diff --git a/frontend/src/views/WorkflowDetail.vue b/frontend/src/views/WorkflowDetail.vue index dbec73a..ee47508 100644 --- a/frontend/src/views/WorkflowDetail.vue +++ b/frontend/src/views/WorkflowDetail.vue @@ -10,10 +10,10 @@ - + -

{{ workflowInstance?.workflow_type || 'Workflow Detail' }}

+

{{ workflowDefinition?.name || workflowInstance?.name || 'Workflow Detail' }}