This commit is contained in:
j3d1 2026-08-31 13:41:03 +02:00
parent 2218cc3543
commit 4671860fa4
6 changed files with 166 additions and 2 deletions

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.2
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('toolshed', '0023_remove_storagelocation_storagelocation_unique_owner_scoped_id_and_more'),
]
operations = [
migrations.AddField(
model_name='workflowinstance',
name='current_step',
field=models.PositiveIntegerField(default=1),
),
]

View file

@ -236,6 +236,7 @@ class StorageLocation(models.Model):
class WorkflowInstance(models.Model): class WorkflowInstance(models.Model):
slug = models.CharField(max_length=255) slug = models.CharField(max_length=255)
state = models.CharField(max_length=255) state = models.CharField(max_length=255)
current_step = models.PositiveIntegerField(default=1)
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')
staged_files = models.ManyToManyField(File, related_name='staged_by_workflows', blank=True) staged_files = models.ManyToManyField(File, related_name='staged_by_workflows', blank=True)

View file

@ -319,7 +319,7 @@ class WorkflowInstanceSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = WorkflowInstance model = WorkflowInstance
fields = ['id', 'slug', 'state', 'payload', 'owner', 'staged_files', 'created_at', 'updated_at'] fields = ['id', 'slug', 'state', 'current_step', 'payload', 'owner', 'staged_files', 'created_at', 'updated_at']
read_only_fields = ['owner', 'staged_files', 'created_at', 'updated_at'] read_only_fields = ['owner', 'staged_files', 'created_at', 'updated_at']
def get_staged_files(self, obj): def get_staged_files(self, obj):

View file

@ -0,0 +1,73 @@
<template>
<div class="d-flex align-items-center">
<div v-if="loading" class="spinner-border spinner-border-sm me-2" role="status"></div>
<template v-else-if="item">
<authenticated-image v-if="thumbnailFile" :src="thumbnailPathForHash(thumbnailFile.hash)"
:owner="thumbnailFile.owner" img-class="completed-item-thumb me-2"/>
<b-icon-image v-else class="me-2"></b-icon-image>
<div class="flex-grow-1">
<div class="fw-bold small">{{ item.name }}</div>
</div>
</template>
<div v-else class="text-muted small">
<b-icon-exclamation-triangle class="me-1"></b-icon-exclamation-triangle>
Item #{{ id }} (unavailable)
</div>
</div>
</template>
<script>
import * as BIcons from "bootstrap-icons-vue";
import {mapActions} from "vuex";
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
export default {
name: 'FotoFirstCompletedItemCard',
components: {AuthenticatedImage, ...BIcons},
props: {
id: {
type: [Number, String],
required: true
},
owner: {
type: String,
default: null
},
ownerGroup: {
type: String,
default: null
}
},
data() {
return {
loading: true,
item: null
};
},
computed: {
thumbnailFile() {
return this.item?.files?.find(file => file.mime_type?.startsWith('image/')) || null;
}
},
async mounted() {
// Always live: this card exists specifically so the summary reflects the item's current
// name/photo, not whatever it looked like at the moment this workflow created it.
try {
this.item = await this.fetchItemByHandle({handle: this.ownerGroup || this.owner, id: this.id});
} finally {
this.loading = false;
}
},
methods: {
...mapActions(['fetchItemByHandle']),
// Requests a small server-resized/cached thumbnail instead of the original upload - this
// renders in a fixed 40px box (see .completed-item-thumb), and the source photos here can
// be full camera resolution, so fetching the original would be a lot of wasted bandwidth
// and decode work for a summary list. Mirrors FotoFirstBulkImportWorkflow.vue's own
// thumbnailPathForHash(); see docs/implementation.md#thumbnail-lookup-by-hash.
thumbnailPathForHash(hash, size = 64) {
return `/media/${size}/${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash.slice(4, 6)}/${hash.slice(6)}/`;
}
}
}
</script>

View file

@ -0,0 +1,73 @@
<template>
<textarea v-if="type === 'textarea'" class="form-control" rows="2" v-model="localValue"></textarea>
<input v-else-if="type === 'number'" type="number" class="form-control" min="0" v-model.number="localValue">
<!-- Tags/properties mutate the passed-in array in place (same convention as
InventoryNew.vue's own `:value="item.tags"`), so no v-model wiring is needed here. -->
<tag-field v-else-if="type === 'tags'" :value="modelValue"></tag-field>
<property-field v-else-if="type === 'properties'" :value="modelValue"></property-field>
<select v-else-if="type === 'owner_group'" class="form-select" v-model="localValue">
<option :value="null">Myself</option>
<option v-for="group in ownerGroups" :key="group.handle" :value="group.handle">{{ group.handle }}</option>
</select>
<select v-else-if="type === 'availability_policy'" class="form-select" v-model="localValue">
<option v-for="policy in availabilityPolicies" :key="policy.slug" :value="policy.slug">
{{ policy.text }}
</option>
</select>
<select v-else-if="type === 'storage_location'" class="form-select" v-model="localValue">
<option :value="null">No storage location</option>
<option v-for="location in storageLocations" :key="location.id" :value="location.id">
{{ location.path }}
</option>
</select>
<input v-else type="text" class="form-control" v-model="localValue">
</template>
<script>
import TagField from "@/components/TagField.vue";
import PropertyField from "@/components/PropertyField.vue";
export default {
name: 'FotoFirstFieldInput',
components: {TagField, PropertyField},
props: {
type: {
type: String,
required: true
},
modelValue: {
default: null
},
ownerGroups: {
type: Array,
default: () => []
},
availabilityPolicies: {
type: Array,
default: () => []
},
storageLocations: {
type: Array,
default: () => []
}
},
emits: ['update:modelValue'],
computed: {
localValue: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
}
}
}
}
</script>

View file

@ -45,7 +45,6 @@ export function buildWorkflowApiPayload(workflow) {
slug: workflow.slug, slug: workflow.slug,
state: 'running', state: 'running',
current_step: 1, current_step: 1,
total_steps: workflow.stepDefinitions.length,
payload: { payload: {
...ownPayload ...ownPayload
} }