stash
This commit is contained in:
parent
6fcdd1eefa
commit
c0f70004eb
21 changed files with 1855 additions and 260 deletions
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.2.2 on 2026-08-09 13:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0003_accountpreference'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountpreference',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -73,8 +73,8 @@ admin.site.register(StorageLocation, StorageLocationAdmin)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowInstanceAdmin(admin.ModelAdmin):
|
class WorkflowInstanceAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'state', 'owner', 'created_at', 'updated_at')
|
list_display = ('slug', 'state', 'owner', 'created_at', 'updated_at')
|
||||||
search_fields = ('name', 'owner__username')
|
search_fields = ('slug', 'owner__username')
|
||||||
list_filter = ('state', 'created_at', 'owner')
|
list_filter = ('state', 'created_at', 'owner')
|
||||||
readonly_fields = ('created_at', 'updated_at')
|
readonly_fields = ('created_at', 'updated_at')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.2.2 on 2026-08-09 13:08
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('toolshed', '0009_alter_workflowinstance_payload'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='workflowinstance',
|
||||||
|
old_name='name',
|
||||||
|
new_name='slug',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -135,7 +135,7 @@ class StorageLocation(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class WorkflowInstance(models.Model):
|
class WorkflowInstance(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
slug = models.CharField(max_length=255)
|
||||||
state = models.CharField(max_length=255)
|
state = models.CharField(max_length=255)
|
||||||
payload = models.TextField(default='', blank=True) # an opaque, frontend-serialized JSON string on the backend.
|
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')
|
owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='workflows')
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,6 @@ class WorkflowInstanceSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkflowInstance
|
model = WorkflowInstance
|
||||||
fields = ['id', 'name', 'state', 'payload', 'owner', 'created_at', 'updated_at']
|
fields = ['id', 'slug', 'state', 'payload', 'owner', 'created_at', 'updated_at']
|
||||||
read_only_fields = ['owner', 'created_at', 'updated_at']
|
read_only_fields = ['owner', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
name: deploy
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend-a:
|
backend-a:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<Sidebar/>
|
<Sidebar/>
|
||||||
<div class="main">
|
<div class="main" id="page">
|
||||||
<nav class="navbar navbar-expand navbar-light navbar-bg">
|
<nav class="navbar navbar-expand navbar-light navbar-bg">
|
||||||
<a class="sidebar-toggle d-flex" @click="toggleSidebar">
|
<a class="sidebar-toggle d-flex" @click="toggleSidebar">
|
||||||
<i class="hamburger align-self-center"></i>
|
<i class="hamburger align-self-center"></i>
|
||||||
|
|
@ -55,6 +55,7 @@ export default {
|
||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
closeAllDropdowns();
|
closeAllDropdowns();
|
||||||
document.getElementById("sidebar").classList.toggle("collapsed");
|
document.getElementById("sidebar").classList.toggle("collapsed");
|
||||||
|
document.getElementById("page").classList.toggle("expanded");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ import * as BIcons from 'bootstrap-icons-vue';
|
||||||
export default {
|
export default {
|
||||||
name: 'BackupRestoreWorkflow',
|
name: 'BackupRestoreWorkflow',
|
||||||
meta: {
|
meta: {
|
||||||
id: 'backup-restore',
|
slug: 'backup-restore',
|
||||||
name: 'Data Backup',
|
title: 'Data Backup',
|
||||||
category: 'System Maintenance',
|
category: 'System Maintenance',
|
||||||
description: 'Create a comprehensive backup of your inventory and settings data.',
|
description: 'Create a comprehensive backup of your inventory and settings data.',
|
||||||
icons: ['b-icon-gear', 'b-icon-download'],
|
icons: ['b-icon-gear', 'b-icon-download'],
|
||||||
|
|
|
||||||
|
|
@ -279,8 +279,8 @@ export default {
|
||||||
// `@/workflows.js` (via `Component.meta`) to assemble the catalog used
|
// `@/workflows.js` (via `Component.meta`) to assemble the catalog used
|
||||||
// by the Workflows and WorkflowDetail views.
|
// by the Workflows and WorkflowDetail views.
|
||||||
meta: {
|
meta: {
|
||||||
id: 'import-items',
|
slug: 'import-items',
|
||||||
name: 'Bulk Item Import',
|
title: 'Bulk Item Import',
|
||||||
category: 'Data Management',
|
category: 'Data Management',
|
||||||
description: 'Import multiple inventory items from CSV or Excel files with validation.',
|
description: 'Import multiple inventory items from CSV or Excel files with validation.',
|
||||||
icons: ['b-icon-upload', 'b-icon-file-earmark-spreadsheet', 'b-icon-list-check'],
|
icons: ['b-icon-upload', 'b-icon-file-earmark-spreadsheet', 'b-icon-list-check'],
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ import * as BIcons from 'bootstrap-icons-vue';
|
||||||
export default {
|
export default {
|
||||||
name: 'ExpiryCheckWorkflow',
|
name: 'ExpiryCheckWorkflow',
|
||||||
meta: {
|
meta: {
|
||||||
id: 'expiry-check',
|
slug: 'expiry-check',
|
||||||
name: 'Expiry Date Check',
|
title: 'Expiry Date Check',
|
||||||
category: 'Quality Control',
|
category: 'Quality Control',
|
||||||
description: 'Identify and handle items approaching or past their expiry dates.',
|
description: 'Identify and handle items approaching or past their expiry dates.',
|
||||||
icons: ['b-icon-clock-history', 'b-icon-exclamation-triangle'],
|
icons: ['b-icon-clock-history', 'b-icon-exclamation-triangle'],
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
<div class="foto-first-workflow">
|
<div class="foto-first-workflow">
|
||||||
<!-- Step 1: Photo Capture -->
|
<!-- Step 1: Photo Capture -->
|
||||||
<div v-if="step === '1'" class="foto-first-step-1">
|
<div v-if="step === '1'" class="foto-first-step-1">
|
||||||
<div class="step-header mb-4">
|
|
||||||
<h4 class="mb-2">Photo Capture</h4>
|
|
||||||
<p class="text-muted">Capture or upload item photos to begin the import process.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="upload-area mb-4">
|
<div class="upload-area mb-4">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -39,13 +35,67 @@
|
||||||
@change="handleFileUpload"
|
@change="handleFileUpload"
|
||||||
class="d-none"
|
class="d-none"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-success" @click="$refs.fileInput.click()" :disabled="loadingCamera">
|
<button class="btn btn-success" @click="$refs.fileInput.click()"
|
||||||
|
:disabled="loadingCamera">
|
||||||
<b-icon-upload class="me-1"></b-icon-upload>
|
<b-icon-upload class="me-1"></b-icon-upload>
|
||||||
Upload Photos
|
Upload Photos
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<drag-drop-file-source @input="addFiles">
|
||||||
|
<ul>
|
||||||
|
<li v-for="file in without_images(staged_files)" :key="file.id">
|
||||||
|
{{ file.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<div style="position: relative;">
|
||||||
|
<div class="image-list">
|
||||||
|
<deletable-wrapper v-for="file in only_images(staged_files).filter(file => file.owner)"
|
||||||
|
:key="file.id"
|
||||||
|
@delete="deleteFile(file)">
|
||||||
|
<authenticated-image :src="file.name" :owner="file.owner" class="img-thumbnail"/>
|
||||||
|
</deletable-wrapper>
|
||||||
|
<deletable-wrapper v-for="file in only_images(staged_files).filter(file => file.data)"
|
||||||
|
:key="file.id"
|
||||||
|
@delete="deleteTempFile(file)">
|
||||||
|
<img :alt="file.name" :src="'data:' + file.mime_type + ';base64,' + file.data"
|
||||||
|
class="img-thumbnail border-info">
|
||||||
|
</deletable-wrapper>
|
||||||
|
<fs-file-source @input="addFiles">
|
||||||
|
<div class="img-thumbnail btn btn-outline-primary">
|
||||||
|
<b-icon-upload></b-icon-upload>
|
||||||
|
</div>
|
||||||
|
</fs-file-source>
|
||||||
|
<camera-file-source @input="addFiles">
|
||||||
|
<div class="img-thumbnail btn btn-outline-primary">
|
||||||
|
<b-icon-camera></b-icon-camera>
|
||||||
|
</div>
|
||||||
|
</camera-file-source>
|
||||||
|
<webcam-file-source @input="addFiles">
|
||||||
|
<div class="img-thumbnail btn btn-outline-primary">
|
||||||
|
<b-icon-camera-video></b-icon-camera-video>
|
||||||
|
</div>
|
||||||
|
</webcam-file-source>
|
||||||
|
<label class="img-thumbnail btn btn-outline-primary" for="file-dropdown">
|
||||||
|
<b-icon-plus></b-icon-plus>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" id="file-dropdown" class="invisible-input">
|
||||||
|
<div class="dropdown-menu" v-if="only_images([]).length > 0">
|
||||||
|
<div class="image-list">
|
||||||
|
<span v-for="file in only_images([])" :key="file.id" @click="addExistingFiles([file])"
|
||||||
|
style="cursor: pointer;">
|
||||||
|
<authenticated-image :src="file.name" :owner="file.owner" class="img-thumbnail"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</drag-drop-file-source>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -223,7 +273,8 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div v-for="(image, index) in processedImages" :key="index" class="col-sm-6 col-md-4 col-lg-3 mb-3">
|
<div v-for="(image, index) in processedImages" :key="index" class="col-sm-6 col-md-4 col-lg-3 mb-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<img :src="image.processedUrl" class="card-img-top processed-thumbnail" :alt="`Processed ${index + 1}`">
|
<img :src="image.processedUrl" class="card-img-top processed-thumbnail"
|
||||||
|
:alt="`Processed ${index + 1}`">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
<small class="text-muted">{{ image.name }}</small>
|
<small class="text-muted">{{ image.name }}</small>
|
||||||
|
|
@ -233,10 +284,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="processing-info">
|
<div class="processing-info">
|
||||||
<small class="text-muted d-block">
|
<small class="text-muted d-block">
|
||||||
{{ formatFileSize(image.originalSize) }} → {{ formatFileSize(image.processedSize) }}
|
{{ formatFileSize(image.originalSize) }} →
|
||||||
|
{{ formatFileSize(image.processedSize) }}
|
||||||
</small>
|
</small>
|
||||||
<small class="text-success">
|
<small class="text-success">
|
||||||
{{ Math.round(((image.originalSize - image.processedSize) / image.originalSize) * 100) }}% reduced
|
{{
|
||||||
|
Math.round(((image.originalSize - image.processedSize) / image.originalSize) * 100)
|
||||||
|
}}% reduced
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -299,7 +353,8 @@
|
||||||
<!-- Image Preview -->
|
<!-- Image Preview -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<img :src="currentItem.processedUrl || currentItem.preview" class="card-img-top item-image" alt="Current item">
|
<img :src="currentItem.processedUrl || currentItem.preview" class="card-img-top item-image"
|
||||||
|
alt="Current item">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<small class="text-muted">{{ currentItem.name }}</small>
|
<small class="text-muted">{{ currentItem.name }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -476,9 +531,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showCompleted" class="card-body">
|
<div v-if="showCompleted" class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div v-for="(item, index) in completedItems" :key="index" class="col-sm-6 col-md-4 col-lg-3 mb-2">
|
<div v-for="(item, index) in completedItems" :key="index"
|
||||||
|
class="col-sm-6 col-md-4 col-lg-3 mb-2">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<img :src="item.image.processedUrl || item.image.preview" class="completed-item-thumb me-2" alt="Item">
|
<img :src="item.image.processedUrl || item.image.preview"
|
||||||
|
class="completed-item-thumb me-2" alt="Item">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<div class="fw-bold small">{{ item.details.name }}</div>
|
<div class="fw-bold small">{{ item.details.name }}</div>
|
||||||
<div class="text-muted small">{{ item.details.category || 'No category' }}</div>
|
<div class="text-muted small">{{ item.details.category || 'No category' }}</div>
|
||||||
|
|
@ -563,7 +620,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div v-if="Object.keys(categoryBreakdown).length > 0" class="row">
|
<div v-if="Object.keys(categoryBreakdown).length > 0" class="row">
|
||||||
<div v-for="(count, category) in categoryBreakdown" :key="category" class="col-sm-6 col-md-4 col-lg-3 mb-2">
|
<div v-for="(count, category) in categoryBreakdown" :key="category"
|
||||||
|
class="col-sm-6 col-md-4 col-lg-3 mb-2">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span class="text-capitalize">{{ category || 'Uncategorized' }}</span>
|
<span class="text-capitalize">{{ category || 'Uncategorized' }}</span>
|
||||||
<span class="badge bg-secondary">{{ count }}</span>
|
<span class="badge bg-secondary">{{ count }}</span>
|
||||||
|
|
@ -663,15 +721,19 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- Grid View -->
|
<!-- Grid View -->
|
||||||
<div v-if="viewMode === 'grid'" class="row">
|
<div v-if="viewMode === 'grid'" class="row">
|
||||||
<div v-for="(item, index) in completedItems" :key="index" class="col-sm-6 col-md-4 col-lg-3 mb-3">
|
<div v-for="(item, index) in completedItems" :key="index"
|
||||||
|
class="col-sm-6 col-md-4 col-lg-3 mb-3">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<img :src="item.image.processedUrl || item.image.preview" class="card-img-top item-thumb" :alt="item.details.name">
|
<img :src="item.image.processedUrl || item.image.preview"
|
||||||
|
class="card-img-top item-thumb" :alt="item.details.name">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<h6 class="card-title mb-1">{{ item.details.name }}</h6>
|
<h6 class="card-title mb-1">{{ item.details.name }}</h6>
|
||||||
<p class="card-text small text-muted mb-1">{{ item.details.category || 'No category' }}</p>
|
<p class="card-text small text-muted mb-1">
|
||||||
|
{{ item.details.category || 'No category' }}</p>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<small class="text-muted">Qty: {{ item.details.quantity }}</small>
|
<small class="text-muted">Qty: {{ item.details.quantity }}</small>
|
||||||
<small v-if="item.details.estimated_value" class="text-success">${{ item.details.estimated_value }}</small>
|
<small v-if="item.details.estimated_value"
|
||||||
|
class="text-success">${{ item.details.estimated_value }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -682,32 +744,35 @@
|
||||||
<div v-else class="table-responsive">
|
<div v-else class="table-responsive">
|
||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Image</th>
|
<th>Image</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Quantity</th>
|
<th>Quantity</th>
|
||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
<th>Value</th>
|
<th>Value</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(item, index) in completedItems" :key="index">
|
<tr v-for="(item, index) in completedItems" :key="index">
|
||||||
<td>
|
<td>
|
||||||
<img :src="item.image.processedUrl || item.image.preview" class="list-item-thumb" :alt="item.details.name">
|
<img :src="item.image.processedUrl || item.image.preview"
|
||||||
</td>
|
class="list-item-thumb" :alt="item.details.name">
|
||||||
<td class="fw-bold">{{ item.details.name }}</td>
|
</td>
|
||||||
<td>
|
<td class="fw-bold">{{ item.details.name }}</td>
|
||||||
<span v-if="item.details.category" class="badge bg-light text-dark">{{ item.details.category }}</span>
|
<td>
|
||||||
<span v-else class="text-muted">-</span>
|
<span v-if="item.details.category"
|
||||||
</td>
|
class="badge bg-light text-dark">{{ item.details.category }}</span>
|
||||||
<td>{{ item.details.quantity }} {{ item.details.unit }}</td>
|
<span v-else class="text-muted">-</span>
|
||||||
<td>{{ item.details.location || '-' }}</td>
|
</td>
|
||||||
<td>
|
<td>{{ item.details.quantity }} {{ item.details.unit }}</td>
|
||||||
<span v-if="item.details.estimated_value" class="text-success">${{ item.details.estimated_value }}</span>
|
<td>{{ item.details.location || '-' }}</td>
|
||||||
<span v-else class="text-muted">-</span>
|
<td>
|
||||||
</td>
|
<span v-if="item.details.estimated_value"
|
||||||
</tr>
|
class="text-success">${{ item.details.estimated_value }}</span>
|
||||||
|
<span v-else class="text-muted">-</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -722,7 +787,8 @@
|
||||||
<b-icon-check-circle class="text-success mb-3" style="font-size: 3rem;"></b-icon-check-circle>
|
<b-icon-check-circle class="text-success mb-3" style="font-size: 3rem;"></b-icon-check-circle>
|
||||||
<h5 class="mb-3">Ready to Complete Import</h5>
|
<h5 class="mb-3">Ready to Complete Import</h5>
|
||||||
<p class="text-muted mb-4">
|
<p class="text-muted mb-4">
|
||||||
All {{ finalTotalItems }} items have been processed and are ready to be added to your inventory.
|
All {{ finalTotalItems }} items have been processed and are ready to be added to your
|
||||||
|
inventory.
|
||||||
This action cannot be undone.
|
This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex justify-content-center gap-3">
|
<div class="d-flex justify-content-center gap-3">
|
||||||
|
|
@ -755,34 +821,28 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import {mapActions, mapState} from "vuex";
|
||||||
|
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
|
||||||
|
import DeletableWrapper from "@/components/DeletableWrapper.vue";
|
||||||
|
import DragDropFileSource from "@/components/inputs/DragDropFileSource.vue";
|
||||||
|
import CameraFileSource from "@/components/inputs/CameraFileSource.vue";
|
||||||
|
import FsFileSource from "@/components/inputs/FsFileSource.vue";
|
||||||
|
import WebcamFileSource from "@/components/inputs/WebcamFileSource.vue";
|
||||||
|
|
||||||
/**
|
|
||||||
* Foto First Bulk Import Workflow
|
|
||||||
*
|
|
||||||
* This single component implements every step of the 'foto-first-bulk-import'
|
|
||||||
* workflow (photo capture, image processing, item detail entry and import
|
|
||||||
* completion). Keeping the whole workflow in one file avoids splitting
|
|
||||||
* closely related state (photos, processed images, completed items) across
|
|
||||||
* many small step components and their prop/emit boundaries.
|
|
||||||
*/
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FotoFirstBulkImportWorkflow',
|
name: 'FotoFirstBulkImportWorkflow',
|
||||||
// Metadata describing this workflow, co-located with its implementation
|
|
||||||
// so there is a single source of truth per workflow type. Consumed by
|
|
||||||
// `@/workflows.js` (via `Component.meta`) to assemble the catalog used
|
|
||||||
// by the Workflows and WorkflowDetail views.
|
|
||||||
meta: {
|
meta: {
|
||||||
id: 'foto-first-bulk-import',
|
slug: 'foto-first-bulk-import',
|
||||||
name: 'Foto First Bulk Import',
|
title: 'Foto First Bulk Import',
|
||||||
category: 'Data Management',
|
category: 'Data Management',
|
||||||
description: 'Capture unlimited photos via mobile camera or upload images, then sequentially enter details for each item.',
|
description: 'Capture unlimited photos via mobile camera or upload images, then sequentially enter details for each item.',
|
||||||
icons: ['b-icon-camera', 'b-icon-pencil-square'],
|
icons: ['b-icon-camera', 'b-icon-pencil-square'],
|
||||||
estimatedDuration: '10-60 minutes',
|
estimatedDuration: '10-60 minutes',
|
||||||
stepDefinitions: [
|
stepDefinitions: [
|
||||||
{ step: '1', name: 'Photo Capture', description: 'Capture or upload item photos' },
|
{step: '1', name: 'Photo Capture', description: 'Capture or upload item photos'},
|
||||||
{ step: '2', name: 'Image Processing', description: 'Process and optimize images' },
|
{step: '2', name: 'Image Processing', description: 'Process and optimize images'},
|
||||||
{ step: '3', name: 'Item Details Entry', description: 'Enter details for each photographed item' },
|
{step: '3', name: 'Item Details Entry', description: 'Enter details for each photographed item'},
|
||||||
{ step: '4', name: 'Import Completion', description: 'Finalize and save imported items' }
|
{step: '4', name: 'Import Completion', description: 'Finalize and save imported items'}
|
||||||
],
|
],
|
||||||
getInitialPayload() {
|
getInitialPayload() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -797,6 +857,12 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
WebcamFileSource,
|
||||||
|
AuthenticatedImage,
|
||||||
|
DeletableWrapper,
|
||||||
|
DragDropFileSource,
|
||||||
|
CameraFileSource,
|
||||||
|
FsFileSource,
|
||||||
...BIcons
|
...BIcons
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -815,6 +881,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
staged_files: [],
|
||||||
// Step 1: photo capture
|
// Step 1: photo capture
|
||||||
loadingCamera: false,
|
loadingCamera: false,
|
||||||
showCamera: false,
|
showCamera: false,
|
||||||
|
|
@ -913,19 +980,20 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(['stageFile']),
|
||||||
loadFromPayload() {
|
loadFromPayload() {
|
||||||
if (this.payload.photos) this.photos = [...this.payload.photos];
|
if (this.payload.photos) this.photos = [...this.payload.photos];
|
||||||
if (this.payload.processing_options) {
|
if (this.payload.processing_options) {
|
||||||
this.processingOptions = { ...this.processingOptions, ...this.payload.processing_options };
|
this.processingOptions = {...this.processingOptions, ...this.payload.processing_options};
|
||||||
}
|
}
|
||||||
if (this.payload.processed_images) {
|
if (this.payload.processed_images) {
|
||||||
this.processedImages = [...this.payload.processed_images];
|
this.processedImages = [...this.payload.processed_images];
|
||||||
this.processedCount = this.processedImages.length;
|
this.processedCount = this.processedImages.length;
|
||||||
}
|
}
|
||||||
if (this.payload.completed_items) this.completedItems = [...this.payload.completed_items];
|
if (this.payload.completed_items) this.completedItems = [...this.payload.completed_items];
|
||||||
if (this.payload.current_item_details) this.currentItemDetails = { ...this.payload.current_item_details };
|
if (this.payload.current_item_details) this.currentItemDetails = {...this.payload.current_item_details};
|
||||||
if (this.payload.current_item_index !== undefined) this.currentItemIndex = this.payload.current_item_index;
|
if (this.payload.current_item_index !== undefined) this.currentItemIndex = this.payload.current_item_index;
|
||||||
if (this.payload.import_options) this.importOptions = { ...this.importOptions, ...this.payload.import_options };
|
if (this.payload.import_options) this.importOptions = {...this.importOptions, ...this.payload.import_options};
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- Step 1: Photo capture ---
|
// --- Step 1: Photo capture ---
|
||||||
|
|
@ -933,7 +1001,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
this.loadingCamera = true;
|
this.loadingCamera = true;
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
video: { facingMode: 'environment' }
|
video: {facingMode: 'environment'}
|
||||||
});
|
});
|
||||||
this.$refs.video.srcObject = this.stream;
|
this.$refs.video.srcObject = this.stream;
|
||||||
this.showCamera = true;
|
this.showCamera = true;
|
||||||
|
|
@ -1010,7 +1078,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
updatePhotosPayload() {
|
updatePhotosPayload() {
|
||||||
this.$emit('update', { photos: this.photos });
|
this.$emit('update', {photos: this.photos});
|
||||||
},
|
},
|
||||||
|
|
||||||
proceedFromStep1() {
|
proceedFromStep1() {
|
||||||
|
|
@ -1060,7 +1128,7 @@ export default {
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
// Calculate new dimensions
|
// Calculate new dimensions
|
||||||
let { width, height } = this.calculateDimensions(
|
let {width, height} = this.calculateDimensions(
|
||||||
img.width,
|
img.width,
|
||||||
img.height,
|
img.height,
|
||||||
this.processingOptions.max_width,
|
this.processingOptions.max_width,
|
||||||
|
|
@ -1103,7 +1171,7 @@ export default {
|
||||||
height = maxHeight;
|
height = maxHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { width: Math.round(width), height: Math.round(height) };
|
return {width: Math.round(width), height: Math.round(height)};
|
||||||
},
|
},
|
||||||
|
|
||||||
formatFileSize(bytes) {
|
formatFileSize(bytes) {
|
||||||
|
|
@ -1149,7 +1217,7 @@ export default {
|
||||||
|
|
||||||
const itemData = {
|
const itemData = {
|
||||||
image: this.currentItem,
|
image: this.currentItem,
|
||||||
details: { ...this.currentItemDetails },
|
details: {...this.currentItemDetails},
|
||||||
saved_at: new Date().toISOString()
|
saved_at: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1187,7 +1255,7 @@ export default {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
this.currentItemDetails = { ...existingItem.details };
|
this.currentItemDetails = {...existingItem.details};
|
||||||
} else {
|
} else {
|
||||||
this.currentItemDetails = this.getDefaultItemDetails();
|
this.currentItemDetails = this.getDefaultItemDetails();
|
||||||
}
|
}
|
||||||
|
|
@ -1199,7 +1267,7 @@ export default {
|
||||||
const itemIndex = this.availableItems.findIndex(img => img === item.image);
|
const itemIndex = this.availableItems.findIndex(img => img === item.image);
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
this.currentItemIndex = itemIndex;
|
this.currentItemIndex = itemIndex;
|
||||||
this.currentItemDetails = { ...item.details };
|
this.currentItemDetails = {...item.details};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1267,6 +1335,54 @@ export default {
|
||||||
category_breakdown: this.categoryBreakdown
|
category_breakdown: this.categoryBreakdown
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
async uploadFiles(files) {
|
||||||
|
const jobs = files.map(async file => {
|
||||||
|
return await this.stageFile({
|
||||||
|
file: file,
|
||||||
|
item_id: this.item_id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return await Promise.all(jobs);
|
||||||
|
},
|
||||||
|
addFiles(files) {
|
||||||
|
console.log("add files", files);
|
||||||
|
const new_files = files.filter(file => !this.staged_files.find(f => f.hash === file.hash));
|
||||||
|
if (new_files.length === 0) {
|
||||||
|
console.log("no new files");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.create) {
|
||||||
|
this.uploadFiles(new_files).then((uploaded) => {
|
||||||
|
this.$emit("change", [...this.staged_files, ...uploaded]);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$emit("change", [...this.staged_files, ...new_files]);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
addExistingFiles(files) {
|
||||||
|
console.log("add existing files", files);
|
||||||
|
const new_files = files.filter(file => !this.staged_files.find(f => f.id === file.id));
|
||||||
|
if (new_files.length === 0) {
|
||||||
|
console.log("no new files");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$emit("change", [...this.staged_files, ...new_files]);
|
||||||
|
},
|
||||||
|
deleteFile(file) {
|
||||||
|
this.deleteItemFile({item_id: this.item_id, file_id: file.id}).then(() => {
|
||||||
|
this.$emit("change", this.staged_files.filter(f => f.id !== file.id));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteTempFile(file) {
|
||||||
|
this.$emit("change", this.staged_files.filter(f => f.hash !== file.hash));
|
||||||
|
},
|
||||||
|
only_images(files) {
|
||||||
|
return files.filter(file => file.mime_type.startsWith("image/"));
|
||||||
|
},
|
||||||
|
without_images(files) {
|
||||||
|
return files.filter(file => !file.mime_type.startsWith("image/"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1348,5 +1464,45 @@ export default {
|
||||||
border: 2px solid #28a745;
|
border: 2px solid #28a745;
|
||||||
background: linear-gradient(135deg, #f8fff8 0%, #e8f5e8 100%);
|
background: linear-gradient(135deg, #f8fff8 0%, #e8f5e8 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.img-thumbnail {
|
||||||
|
width: 95px;
|
||||||
|
height: 54px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-thumbnail svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-dropdown:checked ~ .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu:hover {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-dropdown:checked ~ label {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--bs-primary);
|
||||||
|
border-color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-dropdown:checked ~ label:hover {
|
||||||
|
color: var(--bs-primary);
|
||||||
|
background-color: initial;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -60,8 +60,8 @@ import * as BIcons from 'bootstrap-icons-vue';
|
||||||
export default {
|
export default {
|
||||||
name: 'InventoryAuditWorkflow',
|
name: 'InventoryAuditWorkflow',
|
||||||
meta: {
|
meta: {
|
||||||
id: 'inventory-audit',
|
slug: 'inventory-audit',
|
||||||
name: 'Inventory Audit',
|
title: 'Inventory Audit',
|
||||||
category: 'Inventory Management',
|
category: 'Inventory Management',
|
||||||
description: 'Perform a complete audit of your inventory items, checking quantities, locations, and conditions.',
|
description: 'Perform a complete audit of your inventory items, checking quantities, locations, and conditions.',
|
||||||
icons: ['b-icon-list-ul'],
|
icons: ['b-icon-list-ul'],
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ import * as BIcons from 'bootstrap-icons-vue';
|
||||||
export default {
|
export default {
|
||||||
name: 'MaintenanceScheduleWorkflow',
|
name: 'MaintenanceScheduleWorkflow',
|
||||||
meta: {
|
meta: {
|
||||||
id: 'maintenance-schedule',
|
slug: 'maintenance-schedule',
|
||||||
name: 'Maintenance Schedule',
|
title: 'Maintenance Schedule',
|
||||||
category: 'Tool Maintenance',
|
category: 'Tool Maintenance',
|
||||||
description: 'Create and execute maintenance schedules for tools and equipment.',
|
description: 'Create and execute maintenance schedules for tools and equipment.',
|
||||||
icons: ['b-icon-tools', 'b-icon-calendar'],
|
icons: ['b-icon-tools', 'b-icon-calendar'],
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,8 @@ import * as BIcons from 'bootstrap-icons-vue';
|
||||||
export default {
|
export default {
|
||||||
name: 'StorageOptimizationWorkflow',
|
name: 'StorageOptimizationWorkflow',
|
||||||
meta: {
|
meta: {
|
||||||
id: 'storage-optimization',
|
slug: 'storage-optimization',
|
||||||
name: 'Storage Optimization',
|
title: 'Storage Optimization',
|
||||||
category: 'Storage Management',
|
category: 'Storage Management',
|
||||||
description: 'Analyze and reorganize storage locations for maximum efficiency and accessibility.',
|
description: 'Analyze and reorganize storage locations for maximum efficiency and accessibility.',
|
||||||
icons: ['b-icon-boxes', 'b-icon-diagram-3', 'b-icon-archive'],
|
icons: ['b-icon-boxes', 'b-icon-diagram-3', 'b-icon-archive'],
|
||||||
|
|
|
||||||
|
|
@ -137,4 +137,41 @@ body {
|
||||||
background-color: var(--bs-table-bg);
|
background-color: var(--bs-table-bg);
|
||||||
background-image: linear-gradient(var(--bs-table-accent-bg), var(--bs-table-accent-bg));
|
background-image: linear-gradient(var(--bs-table-accent-bg), var(--bs-table-accent-bg));
|
||||||
border-bottom-width: 1px !important;
|
border-bottom-width: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: map-get($grid-breakpoints, xl)) {
|
||||||
|
.main.expanded {
|
||||||
|
.col-xl-12-ex {
|
||||||
|
flex: 0 0 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-xl-9-ex {
|
||||||
|
flex: 0 0 75% !important;
|
||||||
|
max-width: 75% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-xl-block-ex {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: map-get($grid-breakpoints, lg)) {
|
||||||
|
.main.expanded {
|
||||||
|
.col-lg-12-ex {
|
||||||
|
flex: 0 0 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-lg-9-ex {
|
||||||
|
flex: 0 0 75% !important;
|
||||||
|
max-width: 75% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-lg-block-ex {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -488,6 +488,15 @@ export default createStore({
|
||||||
await servers.delete(getters.signAuth, '/api/item_files/' + item_id + '/' + file_id + '/')
|
await servers.delete(getters.signAuth, '/api/item_files/' + item_id + '/' + file_id + '/')
|
||||||
state.files = state.files.filter(file => file.id !== file_id)
|
state.files = state.files.filter(file => file.id !== file_id)
|
||||||
},
|
},
|
||||||
|
async stageFile({state, dispatch, getters}, {lifetime_id, file}) {
|
||||||
|
const servers = await dispatch('getHomeServers')
|
||||||
|
const data = await servers.post(getters.signAuth, '/api/staged_files/' + lifetime_id + '/', file)
|
||||||
|
if (data.name) {
|
||||||
|
data.owner = state.user
|
||||||
|
//state.files.push(data)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
},
|
||||||
async fetchTags({state, commit, dispatch, getters}) {
|
async fetchTags({state, commit, dispatch, getters}) {
|
||||||
if (state.last_load.tags > Date.now() - 1000 * 60 * 60 * 24) {
|
if (state.last_load.tags > Date.now() - 1000 * 60 * 60 * 24) {
|
||||||
return state.tags
|
return state.tags
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,11 @@
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
<router-link to="/workflows" class="text-decoration-none">Workflows</router-link>
|
<router-link to="/workflows" class="text-decoration-none">Workflows</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">{{ workflowDefinition?.name || workflowInstance?.name || 'Loading...' }}</li>
|
<li class="breadcrumb-item active" aria-current="page">{{ workflowDefinition?.title || workflowInstance?.title || 'Loading...' }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
<h1 class="h3 mb-0">{{ workflowDefinition?.name || workflowInstance?.name || 'Workflow Detail' }}</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group breadcrumb" role="group">
|
||||||
<button class="btn btn-outline-secondary" @click="$router.go(-1)">
|
<button class="btn btn-outline-secondary" @click="$router.go(-1)">
|
||||||
<b-icon-arrow-left class="me-1"></b-icon-arrow-left>
|
<b-icon-arrow-left class="me-1"></b-icon-arrow-left>
|
||||||
Back
|
Back
|
||||||
|
|
@ -43,8 +42,86 @@
|
||||||
|
|
||||||
<!-- Workflow Content -->
|
<!-- Workflow Content -->
|
||||||
<div v-else-if="workflowInstance" class="row">
|
<div v-else-if="workflowInstance" class="row">
|
||||||
|
<!-- Main Content Area -->
|
||||||
|
<div class="col-xl-9 col-lg-12 col-lg-9-ex">
|
||||||
|
<!-- Current Step Content -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
{{ currentStepDefinition?.name || `Step ${currentStep}` }}
|
||||||
|
</h5>
|
||||||
|
<small class="text-muted">{{ currentStepDefinition?.description }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="step-navigation">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary me-1"
|
||||||
|
@click="handlePrevStep"
|
||||||
|
:disabled="!canNavigatePrev">
|
||||||
|
<b-icon-chevron-left></b-icon-chevron-left>
|
||||||
|
</button>
|
||||||
|
<span class="mx-2 small">{{ currentStepIndex + 1 }} / {{ totalSteps }}</span>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary"
|
||||||
|
@click="handleNextStep"
|
||||||
|
:disabled="!canNavigateNext">
|
||||||
|
<b-icon-chevron-right></b-icon-chevron-right>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Step-specific content based on workflow type and current step -->
|
||||||
|
<component
|
||||||
|
v-if="workflowComponent"
|
||||||
|
:is="workflowComponent"
|
||||||
|
:workflow-instance="workflowInstance"
|
||||||
|
:step="currentStep"
|
||||||
|
:payload="workflowInstance.payload"
|
||||||
|
@update="handleStepUpdate"
|
||||||
|
@next="handleNextStep"
|
||||||
|
@prev="handlePrevStep"
|
||||||
|
@complete="completeWorkflow"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Default step content if no specific component -->
|
||||||
|
<div v-else class="text-center py-5">
|
||||||
|
<b-icon-gear class="text-muted mb-3" style="font-size: 3rem;"></b-icon-gear>
|
||||||
|
<h5 class="text-muted">{{ currentStepDefinition?.name || 'Step Content' }}</h5>
|
||||||
|
<p class="text-muted">{{ currentStepDefinition?.description || 'This step is in progress.' }}</p>
|
||||||
|
|
||||||
|
<!-- Step Navigation Buttons -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<button v-if="canNavigatePrev"
|
||||||
|
class="btn btn-outline-secondary me-2"
|
||||||
|
@click="handlePrevStep">
|
||||||
|
<b-icon-chevron-left class="me-1"></b-icon-chevron-left>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button v-if="canNavigateNext"
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="handleNextStep">
|
||||||
|
Next
|
||||||
|
<b-icon-chevron-right class="ms-1"></b-icon-chevron-right>
|
||||||
|
</button>
|
||||||
|
<button v-else-if="currentStepIndex === stepDefinitions.length - 1"
|
||||||
|
class="btn btn-success"
|
||||||
|
@click="completeWorkflow">
|
||||||
|
<b-icon-check-circle class="me-1"></b-icon-check-circle>
|
||||||
|
Complete Workflow
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Debug Info (only in development) -->
|
||||||
|
<div v-if="true" class="mt-4 border-top pt-3">
|
||||||
|
<details>
|
||||||
|
<summary class="text-muted small">Debug Info</summary>
|
||||||
|
<pre class="small mt-2">{{ JSON.stringify(workflowInstance, null, 2) }}</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Workflow Progress Sidebar -->
|
<!-- Workflow Progress Sidebar -->
|
||||||
<div class="col-lg-3 mb-4">
|
<div class="col-lg-3 mb-4 d-none d-xl-block d-lg-block-ex">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h6 class="card-title mb-0">Progress Overview</h6>
|
<h6 class="card-title mb-0">Progress Overview</h6>
|
||||||
|
|
@ -113,85 +190,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
|
||||||
<div class="col-lg-9">
|
|
||||||
<!-- Current Step Content -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h5 class="card-title mb-0">
|
|
||||||
{{ currentStepDefinition?.name || `Step ${currentStep}` }}
|
|
||||||
</h5>
|
|
||||||
<small class="text-muted">{{ currentStepDefinition?.description }}</small>
|
|
||||||
</div>
|
|
||||||
<div class="step-navigation">
|
|
||||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
|
||||||
@click="handlePrevStep"
|
|
||||||
:disabled="!canNavigatePrev">
|
|
||||||
<b-icon-chevron-left></b-icon-chevron-left>
|
|
||||||
</button>
|
|
||||||
<span class="mx-2 small">{{ currentStepIndex + 1 }} / {{ totalSteps }}</span>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary"
|
|
||||||
@click="handleNextStep"
|
|
||||||
:disabled="!canNavigateNext">
|
|
||||||
<b-icon-chevron-right></b-icon-chevron-right>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Step-specific content based on workflow type and current step -->
|
|
||||||
<component
|
|
||||||
v-if="workflowComponent"
|
|
||||||
:is="workflowComponent"
|
|
||||||
:workflow-instance="workflowInstance"
|
|
||||||
:step="currentStep"
|
|
||||||
:payload="workflowInstance.payload"
|
|
||||||
@update="handleStepUpdate"
|
|
||||||
@next="handleNextStep"
|
|
||||||
@prev="handlePrevStep"
|
|
||||||
@complete="completeWorkflow"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Default step content if no specific component -->
|
|
||||||
<div v-else class="text-center py-5">
|
|
||||||
<b-icon-gear class="text-muted mb-3" style="font-size: 3rem;"></b-icon-gear>
|
|
||||||
<h5 class="text-muted">{{ currentStepDefinition?.name || 'Step Content' }}</h5>
|
|
||||||
<p class="text-muted">{{ currentStepDefinition?.description || 'This step is in progress.' }}</p>
|
|
||||||
|
|
||||||
<!-- Step Navigation Buttons -->
|
|
||||||
<div class="mt-4">
|
|
||||||
<button v-if="canNavigatePrev"
|
|
||||||
class="btn btn-outline-secondary me-2"
|
|
||||||
@click="handlePrevStep">
|
|
||||||
<b-icon-chevron-left class="me-1"></b-icon-chevron-left>
|
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
<button v-if="canNavigateNext"
|
|
||||||
class="btn btn-primary"
|
|
||||||
@click="handleNextStep">
|
|
||||||
Next
|
|
||||||
<b-icon-chevron-right class="ms-1"></b-icon-chevron-right>
|
|
||||||
</button>
|
|
||||||
<button v-else-if="currentStepIndex === stepDefinitions.length - 1"
|
|
||||||
class="btn btn-success"
|
|
||||||
@click="completeWorkflow">
|
|
||||||
<b-icon-check-circle class="me-1"></b-icon-check-circle>
|
|
||||||
Complete Workflow
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Debug Info (only in development) -->
|
|
||||||
<div v-if="$isDevelopment" class="mt-4 border-top pt-3">
|
|
||||||
<details>
|
|
||||||
<summary class="text-muted small">Debug Info</summary>
|
|
||||||
<pre class="small mt-2">{{ JSON.stringify(workflowInstance, null, 2) }}</pre>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -217,7 +215,7 @@ export default {
|
||||||
},
|
},
|
||||||
step: {
|
step: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "initial"
|
default: "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -232,13 +230,17 @@ export default {
|
||||||
...mapState(['active_workflows']),
|
...mapState(['active_workflows']),
|
||||||
|
|
||||||
currentStep() {
|
currentStep() {
|
||||||
return this.step || "initial";
|
return this.step || "1";
|
||||||
},
|
},
|
||||||
|
|
||||||
workflowDefinition() {
|
workflowDefinition() {
|
||||||
// `name` doubles as the workflow type identifier (e.g. 'import-items').
|
// `name` doubles as the workflow type identifier (e.g. 'import-items').
|
||||||
if (!this.workflowInstance?.name) return null;
|
if (!this.workflowInstance?.slug) return null;
|
||||||
return getWorkflow(this.workflowInstance.name);
|
return getWorkflow(this.workflowInstance.slug);
|
||||||
|
},
|
||||||
|
|
||||||
|
getWorkflowDisplayName() {
|
||||||
|
return getWorkflow(this.workflowInstance.slug)?.title;
|
||||||
},
|
},
|
||||||
|
|
||||||
stepDefinitions() {
|
stepDefinitions() {
|
||||||
|
|
@ -261,7 +263,7 @@ export default {
|
||||||
// Return the single component implementing the whole workflow, if any,
|
// Return the single component implementing the whole workflow, if any,
|
||||||
// using the workflow component registry. The component itself decides
|
// using the workflow component registry. The component itself decides
|
||||||
// what to render based on the `step` prop it receives.
|
// what to render based on the `step` prop it receives.
|
||||||
const workflowType = this.workflowInstance?.name;
|
const workflowType = this.workflowInstance?.slug;
|
||||||
return workflowType ? getWorkflowComponent(workflowType) : null;
|
return workflowType ? getWorkflowComponent(workflowType) : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -506,4 +508,8 @@ export default {
|
||||||
.step-navigation {
|
.step-navigation {
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-group.breadcrumb{
|
||||||
|
padding: calc(0.5rem - 1px) 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -16,49 +16,51 @@
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Workflow</th>
|
<th>Workflow</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Progress</th>
|
<th>Progress</th>
|
||||||
<th>Started</th>
|
<th>Started</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="workflow in activeWorkflows" :key="workflow.id">
|
<tr v-for="workflow in activeWorkflows" :key="workflow.id">
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ getWorkflowDisplayName(workflow) }}</strong>
|
<strong>{{ getWorkflowDisplayName(workflow) }}</strong>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">{{ workflow.payload?.workflow_config?.category || 'System' }}</small>
|
<small class="text-muted">{{
|
||||||
</td>
|
workflow.payload?.workflow_config?.category || 'System'
|
||||||
<td>
|
}}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
<span :class="getStatusBadgeClass(workflow.state)">
|
<span :class="getStatusBadgeClass(workflow.state)">
|
||||||
{{ workflow.status_display || workflow.state }}
|
{{ workflow.status_display || workflow.state }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="progress" style="height: 8px;">
|
<div class="progress" style="height: 8px;">
|
||||||
<div class="progress-bar"
|
<div class="progress-bar"
|
||||||
:style="{ width: workflow.progress_percentage + '%' }"
|
:style="{ width: workflow.progress_percentage + '%' }"
|
||||||
:class="getProgressBarClass(workflow.state)">
|
:class="getProgressBarClass(workflow.state)">
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">{{ workflow.progress_percentage }}%</small>
|
</div>
|
||||||
</td>
|
<small class="text-muted">{{ workflow.progress_percentage }}%</small>
|
||||||
<td>{{ formatDate(workflow.started_at) }}</td>
|
</td>
|
||||||
<td>
|
<td>{{ formatDate(workflow.started_at) }}</td>
|
||||||
<button class="btn btn-sm btn-outline-primary me-1"
|
<td>
|
||||||
@click="viewWorkflowDetails(workflow)"
|
<button class="btn btn-sm btn-outline-primary me-1"
|
||||||
:disabled="loading">
|
@click="viewWorkflowDetails(workflow)"
|
||||||
<b-icon-eye></b-icon-eye>
|
:disabled="loading">
|
||||||
</button>
|
<b-icon-eye></b-icon-eye>
|
||||||
<button class="btn btn-sm btn-outline-danger"
|
</button>
|
||||||
@click="abortWorkflowInstance(workflow)"
|
<button class="btn btn-sm btn-outline-danger"
|
||||||
:disabled="loading">
|
@click="abortWorkflowInstance(workflow)"
|
||||||
<b-icon-x-circle></b-icon-x-circle>
|
:disabled="loading">
|
||||||
</button>
|
<b-icon-x-circle></b-icon-x-circle>
|
||||||
</td>
|
</button>
|
||||||
</tr>
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,21 +79,25 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div v-for="workflow in availableWorkflows" :key="workflow.id" class="col-lg-4 col-md-6 mb-3">
|
<div v-for="workflow in availableWorkflows" :key="workflow.id"
|
||||||
|
class="col-lg-4 col-md-6 mb-3">
|
||||||
<div class="card h-100 workflow-card">
|
<div class="card h-100 workflow-card">
|
||||||
<div class="card-body d-flex flex-column">
|
<div class="card-body d-flex flex-column">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<template v-for="(icon, index) in workflow.icons" :key="icon">
|
<template v-for="(icon, index) in workflow.icons" :key="icon">
|
||||||
<div class="workflow-icon me-3">
|
<div class="workflow-icon me-3">
|
||||||
<component :is="icon" class="text-primary" style="font-size: 1.5rem;"></component>
|
<component :is="icon" class="text-primary"
|
||||||
|
style="font-size: 1.5rem;"></component>
|
||||||
</div>
|
</div>
|
||||||
<!-- Add arrow between icons, but not after the last one -->
|
<!-- Add arrow between icons, but not after the last one -->
|
||||||
<div v-if="index < workflow.icons.length - 1" class="workflow-arrow me-3">
|
<div v-if="index < workflow.icons.length - 1"
|
||||||
<b-icon-arrow-right class="text-muted" style="font-size: 1rem;"></b-icon-arrow-right>
|
class="workflow-arrow me-3">
|
||||||
|
<b-icon-arrow-right class="text-muted"
|
||||||
|
style="font-size: 1rem;"></b-icon-arrow-right>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="workflow-header">
|
<div class="workflow-header">
|
||||||
<h6 class="card-title mb-1">{{ workflow.name }}</h6>
|
<h6 class="card-title mb-1">{{ workflow.title }}</h6>
|
||||||
<small class="text-muted">{{ workflow.category }}</small>
|
<small class="text-muted">{{ workflow.category }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,10 +111,9 @@
|
||||||
<span class="badge bg-light text-dark">{{ workflow.steps }} steps</span>
|
<span class="badge bg-light text-dark">{{ workflow.steps }} steps</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary w-100"
|
<button class="btn btn-primary w-100"
|
||||||
@click="startWorkflow(workflow)"
|
@click="startWorkflow(workflow)">
|
||||||
:disabled="isWorkflowRunning(workflow.id)">
|
|
||||||
<b-icon-play-fill class="me-1"></b-icon-play-fill>
|
<b-icon-play-fill class="me-1"></b-icon-play-fill>
|
||||||
{{ isWorkflowRunning(workflow.id) ? 'Running...' : 'Start Workflow' }}
|
Start Workflow
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,8 +132,8 @@
|
||||||
<script>
|
<script>
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
import BaseLayout from "@/components/BaseLayout.vue";
|
import BaseLayout from "@/components/BaseLayout.vue";
|
||||||
import { mapState, mapActions } from 'vuex';
|
import {mapState, mapActions} from 'vuex';
|
||||||
import { getAllWorkflows, getWorkflow, buildWorkflowApiPayload } from '@/workflows.js';
|
import {getAllWorkflows, getWorkflow, buildWorkflowApiPayload} from '@/workflows.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Workflows',
|
name: 'Workflows',
|
||||||
|
|
@ -213,15 +218,8 @@ export default {
|
||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
}).format(date);
|
}).format(date);
|
||||||
},
|
},
|
||||||
isWorkflowRunning(workflowId) {
|
|
||||||
return this.activeWorkflows.some(active =>
|
|
||||||
active.name === workflowId &&
|
|
||||||
active.state === 'running'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
getWorkflowDisplayName(workflow) {
|
getWorkflowDisplayName(workflow) {
|
||||||
// `name` doubles as the workflow type identifier (e.g. 'import-items').
|
return getWorkflow(workflow.slug)?.title;
|
||||||
return getWorkflow(workflow.name)?.name || workflow.name;
|
|
||||||
},
|
},
|
||||||
async startWorkflow(workflow) {
|
async startWorkflow(workflow) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -232,11 +230,10 @@ export default {
|
||||||
const workflowData = buildWorkflowApiPayload(workflow);
|
const workflowData = buildWorkflowApiPayload(workflow);
|
||||||
|
|
||||||
const newWorkflow = await this.createWorkflow(workflowData);
|
const newWorkflow = await this.createWorkflow(workflowData);
|
||||||
console.log('Workflow started successfully:', newWorkflow);
|
|
||||||
|
|
||||||
// Immediately navigate to the workflow detail view
|
// Immediately navigate to the workflow detail view
|
||||||
// Get the first step from the workflow definition
|
// Get the first step from the workflow definition
|
||||||
const firstStep = workflow.stepDefinitions?.[0]?.step || "initial";
|
const firstStep = workflow.stepDefinitions?.[0]?.step || 1;
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'workflow-detail',
|
name: 'workflow-detail',
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -257,9 +254,9 @@ export default {
|
||||||
// Navigate to the workflow detail view
|
// Navigate to the workflow detail view
|
||||||
// Use the workflow's current step if available, otherwise use the first step
|
// Use the workflow's current step if available, otherwise use the first step
|
||||||
const currentStep = workflow.current_step ||
|
const currentStep = workflow.current_step ||
|
||||||
workflow.payload?.current_step ||
|
workflow.payload?.current_step ||
|
||||||
getWorkflow(workflow.name)?.stepDefinitions?.[0]?.step ||
|
getWorkflow(workflow.name)?.stepDefinitions?.[0]?.step ||
|
||||||
"initial";
|
"initial";
|
||||||
|
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'workflow-detail',
|
name: 'workflow-detail',
|
||||||
|
|
|
||||||
|
|
@ -24,23 +24,20 @@ import StorageOptimizationWorkflow from '@/components/workflow/workflows/Storage
|
||||||
import MaintenanceScheduleWorkflow from '@/components/workflow/workflows/MaintenanceScheduleWorkflow.vue';
|
import MaintenanceScheduleWorkflow from '@/components/workflow/workflows/MaintenanceScheduleWorkflow.vue';
|
||||||
import ExpiryCheckWorkflow from '@/components/workflow/workflows/ExpiryCheckWorkflow.vue';
|
import ExpiryCheckWorkflow from '@/components/workflow/workflows/ExpiryCheckWorkflow.vue';
|
||||||
import BackupRestoreWorkflow from '@/components/workflow/workflows/BackupRestoreWorkflow.vue';
|
import BackupRestoreWorkflow from '@/components/workflow/workflows/BackupRestoreWorkflow.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workflows with a fully co-located component + metadata.
|
* Workflows with a fully co-located component + metadata.
|
||||||
*/
|
*/
|
||||||
const implementedWorkflows = [
|
const workflows = [
|
||||||
{ ...FotoFirstBulkImportWorkflow.meta, component: FotoFirstBulkImportWorkflow },
|
{...FotoFirstBulkImportWorkflow.meta, component: FotoFirstBulkImportWorkflow},
|
||||||
{ ...BulkItemImportWorkflow.meta, component: BulkItemImportWorkflow },
|
{...BulkItemImportWorkflow.meta, component: BulkItemImportWorkflow},
|
||||||
{ ...InventoryAuditWorkflow.meta, component: InventoryAuditWorkflow },
|
{...InventoryAuditWorkflow.meta, component: InventoryAuditWorkflow},
|
||||||
{ ...StorageOptimizationWorkflow.meta, component: StorageOptimizationWorkflow },
|
{...StorageOptimizationWorkflow.meta, component: StorageOptimizationWorkflow},
|
||||||
{ ...MaintenanceScheduleWorkflow.meta, component: MaintenanceScheduleWorkflow },
|
{...MaintenanceScheduleWorkflow.meta, component: MaintenanceScheduleWorkflow},
|
||||||
{ ...ExpiryCheckWorkflow.meta, component: ExpiryCheckWorkflow },
|
{...ExpiryCheckWorkflow.meta, component: ExpiryCheckWorkflow},
|
||||||
{ ...BackupRestoreWorkflow.meta, component: BackupRestoreWorkflow },
|
{...BackupRestoreWorkflow.meta, component: BackupRestoreWorkflow},
|
||||||
];
|
];
|
||||||
/**
|
|
||||||
* The full workflow catalog: every workflow type known to the frontend,
|
|
||||||
* whether it has a custom UI or not.
|
|
||||||
*/
|
|
||||||
const workflows = implementedWorkflows;
|
|
||||||
/**
|
/**
|
||||||
* Get every workflow in the catalog.
|
* Get every workflow in the catalog.
|
||||||
* @returns {Array<Object>}
|
* @returns {Array<Object>}
|
||||||
|
|
@ -48,14 +45,16 @@ const workflows = implementedWorkflows;
|
||||||
export function getAllWorkflows() {
|
export function getAllWorkflows() {
|
||||||
return workflows;
|
return workflows;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single workflow definition by id.
|
* Get a single workflow definition by id.
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @returns {Object|undefined}
|
* @returns {Object|undefined}
|
||||||
*/
|
*/
|
||||||
export function getWorkflow(id) {
|
export function getWorkflow(slug) {
|
||||||
return workflows.find(workflow => workflow.id === id);
|
return workflows.find(workflow => workflow.slug === slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Vue component implementing a workflow's UI, if any.
|
* Get the Vue component implementing a workflow's UI, if any.
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
|
|
@ -64,6 +63,7 @@ export function getWorkflow(id) {
|
||||||
export function getWorkflowComponent(id) {
|
export function getWorkflowComponent(id) {
|
||||||
return getWorkflow(id)?.component || null;
|
return getWorkflow(id)?.component || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all workflows belonging to a category.
|
* Get all workflows belonging to a category.
|
||||||
* @param {string} category
|
* @param {string} category
|
||||||
|
|
@ -72,6 +72,7 @@ export function getWorkflowComponent(id) {
|
||||||
export function getWorkflowsByCategory(category) {
|
export function getWorkflowsByCategory(category) {
|
||||||
return workflows.filter(workflow => workflow.category === category);
|
return workflows.filter(workflow => workflow.category === category);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all unique categories present in the catalog.
|
* Get all unique categories present in the catalog.
|
||||||
* @returns {Array<string>}
|
* @returns {Array<string>}
|
||||||
|
|
@ -79,6 +80,7 @@ export function getWorkflowsByCategory(category) {
|
||||||
export function getWorkflowCategories() {
|
export function getWorkflowCategories() {
|
||||||
return [...new Set(workflows.map(workflow => workflow.category))];
|
return [...new Set(workflows.map(workflow => workflow.category))];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the payload sent to the backend to start a new instance of a
|
* Build the payload sent to the backend to start a new instance of a
|
||||||
* workflow, merging the common `workflow_config` metadata block with the
|
* workflow, merging the common `workflow_config` metadata block with the
|
||||||
|
|
@ -89,31 +91,28 @@ export function getWorkflowCategories() {
|
||||||
export function buildWorkflowApiPayload(workflow) {
|
export function buildWorkflowApiPayload(workflow) {
|
||||||
const ownPayload = workflow.getInitialPayload ? workflow.getInitialPayload() : {};
|
const ownPayload = workflow.getInitialPayload ? workflow.getInitialPayload() : {};
|
||||||
return {
|
return {
|
||||||
name: workflow.name,
|
title: workflow.title,
|
||||||
workflow_type: workflow.id,
|
slug: workflow.slug,
|
||||||
state: 'running',
|
state: 'running',
|
||||||
current_step: 1,
|
current_step: 1,
|
||||||
total_steps: workflow.stepDefinitions.length,
|
total_steps: workflow.stepDefinitions.length,
|
||||||
payload: {
|
payload: {
|
||||||
workflow_config: {
|
|
||||||
name: workflow.name,
|
|
||||||
description: workflow.description,
|
|
||||||
category: workflow.category,
|
|
||||||
estimated_duration: workflow.estimatedDuration
|
|
||||||
},
|
|
||||||
...ownPayload
|
...ownPayload
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The backend stores WorkflowInstance.payload as an opaque string - it never
|
* The backend stores WorkflowInstance.payload as an opaque string - it never
|
||||||
* parses or understands it as JSON. The frontend is fully responsible for
|
* parses or understands it as JSON. The frontend is fully responsible for
|
||||||
* serializing it before sending and deserializing it after receiving.
|
* serializing it before sending and deserializing it after receiving.
|
||||||
*/
|
*/
|
||||||
export function serializeWorkflowPayload(workflow) {
|
export function serializeWorkflowPayload(workflow) {
|
||||||
|
console.log(workflow);
|
||||||
if (!workflow || !('payload' in workflow)) return workflow;
|
if (!workflow || !('payload' in workflow)) return workflow;
|
||||||
return {...workflow, payload: JSON.stringify(workflow.payload ?? {})};
|
return {...workflow, payload: JSON.stringify(workflow.payload ?? {})};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deserializeWorkflowPayload(workflow) {
|
export function deserializeWorkflowPayload(workflow) {
|
||||||
if (!workflow) return workflow;
|
if (!workflow) return workflow;
|
||||||
let payload = {};
|
let payload = {};
|
||||||
|
|
@ -124,6 +123,7 @@ export function deserializeWorkflowPayload(workflow) {
|
||||||
}
|
}
|
||||||
return {...workflow, payload};
|
return {...workflow, payload};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getAllWorkflows,
|
getAllWorkflows,
|
||||||
getWorkflow,
|
getWorkflow,
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ export default defineConfig({
|
||||||
'Access-Control-Max-Age': '86400',
|
'Access-Control-Max-Age': '86400',
|
||||||
//'Upgrade-Insecure-Requests': '1',
|
//'Upgrade-Insecure-Requests': '1',
|
||||||
'Content-Security-Policy': 'default-src \'self\';'
|
'Content-Security-Policy': 'default-src \'self\';'
|
||||||
+ ' script-src \'self\' \'wasm-unsafe-eval\' \'unsafe-eval\';'
|
+ ' script-src \'self\' \'wasm-unsafe-eval\' \'unsafe-eval\' \'unsafe-inline\';'
|
||||||
+ ' style-src \'self\' \'unsafe-inline\';'
|
+ ' style-src \'self\' \'unsafe-inline\';'
|
||||||
+ ' img-src \'self\' * data:;'
|
+ ' img-src \'self\' * data: blob:;'
|
||||||
+ ' connect-src * data:',
|
+ ' connect-src * data:',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue