Enhance Form Management and Process Handling
- Updated the form management interface to improve the search and filter functionality, including new filters for category and group, enhancing user experience and organization. - Redesigned the forms display to utilize a grid layout, providing a more visually appealing and user-friendly interface for form management. - Introduced loading states and improved error handling for process publishing and unpublishing, ensuring better feedback for users during these actions. - Added functionality to prevent deletion of published processes, guiding users to unpublish first, thereby improving data integrity and user guidance. - Enhanced the process store with a new unpublish method, allowing for better management of process states and updates to the UI accordingly.
This commit is contained in:
parent
c43d9b6849
commit
0c93e93460
@ -47,65 +47,71 @@
|
|||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<div class="flex-1 p-6 overflow-auto">
|
<div class="flex-1 p-6 overflow-auto">
|
||||||
<!-- Filters and Search -->
|
<!-- Filters and Search -->
|
||||||
<div class="mb-6 flex flex-col sm:flex-row gap-4">
|
<div class="mb-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
||||||
<div class="flex-1 max-w-md">
|
<div class="flex flex-col lg:flex-row gap-4">
|
||||||
<FormKit
|
<div class="flex-1 max-w-md">
|
||||||
v-model="searchQuery"
|
<FormKit
|
||||||
type="text"
|
v-model="searchQuery"
|
||||||
placeholder="Search forms by name or description..."
|
type="text"
|
||||||
:classes="{
|
placeholder="Search forms by name or description..."
|
||||||
outer: 'mb-0',
|
:classes="{
|
||||||
wrapper: 'relative',
|
outer: 'mb-0',
|
||||||
inner: 'relative',
|
wrapper: 'relative',
|
||||||
input: 'w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
inner: 'relative',
|
||||||
prefixIcon: 'absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none'
|
input: 'w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
||||||
}"
|
prefixIcon: 'absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none'
|
||||||
>
|
}"
|
||||||
<template #prefixIcon>
|
>
|
||||||
<Icon name="material-symbols:search" class="text-lg" />
|
<template #prefixIcon>
|
||||||
</template>
|
<Icon name="material-symbols:search" class="text-lg" />
|
||||||
</FormKit>
|
</template>
|
||||||
|
</FormKit>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-48">
|
||||||
|
<FormKit
|
||||||
|
v-model="selectedCategory"
|
||||||
|
type="select"
|
||||||
|
:options="categoryOptions"
|
||||||
|
placeholder="Filter by category"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mb-0',
|
||||||
|
input: 'w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-48">
|
||||||
|
<FormKit
|
||||||
|
v-model="selectedGroup"
|
||||||
|
type="select"
|
||||||
|
:options="groupOptions"
|
||||||
|
placeholder="Filter by group"
|
||||||
|
:classes="{
|
||||||
|
outer: 'mb-0',
|
||||||
|
input: 'w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<RsButton
|
||||||
|
@click="clearFilters"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
:disabled="loading"
|
||||||
|
v-if="hasActiveFilters"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
||||||
|
Clear
|
||||||
|
</RsButton>
|
||||||
|
|
||||||
|
<RsButton @click="loadForms" variant="tertiary" size="sm" :disabled="loading">
|
||||||
|
<Icon name="material-symbols:refresh" class="mr-1" />
|
||||||
|
Refresh
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<!-- Category Filter -->
|
|
||||||
<div class="relative">
|
|
||||||
<FormKit
|
|
||||||
v-model="selectedCategory"
|
|
||||||
type="select"
|
|
||||||
placeholder="All Categories"
|
|
||||||
:options="categoryOptions"
|
|
||||||
:classes="{
|
|
||||||
outer: 'mb-0',
|
|
||||||
input: 'min-w-[140px] text-sm'
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Group Filter -->
|
|
||||||
<div class="relative">
|
|
||||||
<FormKit
|
|
||||||
v-model="selectedGroup"
|
|
||||||
type="select"
|
|
||||||
placeholder="All Groups"
|
|
||||||
:options="groupOptions"
|
|
||||||
:classes="{
|
|
||||||
outer: 'mb-0',
|
|
||||||
input: 'min-w-[140px] text-sm'
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RsButton @click="clearFilters" variant="secondary" size="sm" :disabled="loading" v-if="hasActiveFilters">
|
|
||||||
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
|
||||||
Clear Filters
|
|
||||||
</RsButton>
|
|
||||||
|
|
||||||
<RsButton @click="loadForms" variant="tertiary" size="sm" :disabled="loading">
|
|
||||||
<Icon name="material-symbols:refresh" class="mr-1" />
|
|
||||||
Refresh
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@ -116,133 +122,115 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Forms Table -->
|
<!-- Forms Grid -->
|
||||||
<div v-else class="bg-white shadow-md rounded-lg overflow-hidden">
|
<div v-else-if="filteredForms.length > 0" class="grid gap-4">
|
||||||
<table class="w-full table-auto">
|
<div
|
||||||
<thead class="bg-gray-50 border-b">
|
v-for="form in filteredForms"
|
||||||
<tr>
|
:key="form.id"
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
|
>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
|
<div class="flex items-start justify-between">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Group</th>
|
<div class="flex-1">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Updated</th>
|
<h3 class="text-lg font-semibold text-gray-900">{{ form.name || 'Untitled Form' }}</h3>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
<div class="flex gap-2">
|
||||||
</tr>
|
<RsBadge v-if="form.category" :variant="getCategoryColor(form.category)" size="sm">
|
||||||
</thead>
|
{{ form.category }}
|
||||||
<tbody class="divide-y divide-gray-200">
|
</RsBadge>
|
||||||
<tr v-if="filteredForms.length === 0">
|
<RsBadge v-if="form.group" variant="secondary" size="sm">
|
||||||
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
|
{{ form.group }}
|
||||||
<div class="flex flex-col items-center">
|
</RsBadge>
|
||||||
<Icon name="material-symbols:description-outline" class="w-12 h-12 text-gray-300 mb-2" />
|
|
||||||
<p class="text-lg font-medium mb-1">
|
|
||||||
{{ searchQuery ? 'No forms match your search' : 'No forms found' }}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm mb-2">
|
|
||||||
{{ searchQuery ? 'Try adjusting your search terms' : 'Create your first form to get started' }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Show current search if any -->
|
|
||||||
<div v-if="searchQuery" class="text-xs text-gray-400 mb-4">
|
|
||||||
<div>Search: "{{ searchQuery }}"</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<RsButton v-if="hasActiveFilters" @click="clearFilters" variant="secondary" size="sm">
|
|
||||||
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
|
||||||
Clear Filters
|
|
||||||
</RsButton>
|
|
||||||
<RsButton v-if="!hasActiveFilters" @click="createNewForm" variant="primary" size="sm">
|
|
||||||
<Icon name="material-symbols:add" class="mr-1" />
|
|
||||||
Create New Form
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
|
||||||
<tr v-for="form in filteredForms" :key="form.id" class="hover:bg-gray-50">
|
<p class="text-gray-600 mb-3">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
{{ form.description || 'No description available' }}
|
||||||
<div class="font-medium text-gray-900">{{ form.name || 'Untitled Form' }}</div>
|
</p>
|
||||||
<div class="text-sm text-gray-500">ID: {{ form.id }}</div>
|
|
||||||
</td>
|
<div class="flex items-center gap-6 text-sm text-gray-500">
|
||||||
<td class="px-6 py-4">
|
<div class="flex items-center gap-1">
|
||||||
<div class="text-sm text-gray-700 max-w-xs">
|
<Icon name="material-symbols:fingerprint" class="w-4 h-4" />
|
||||||
<div
|
ID: {{ form.id }}
|
||||||
v-if="form.description && form.description.trim()"
|
|
||||||
class="line-clamp-2 cursor-help"
|
|
||||||
:title="form.description.length > 100 ? form.description : ''"
|
|
||||||
>
|
|
||||||
{{ form.description }}
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-gray-400 italic">
|
|
||||||
No description
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div v-if="form.category" class="text-sm">
|
|
||||||
<RsBadge :variant="getCategoryColor(form.category)" size="sm">
|
|
||||||
{{ form.category }}
|
|
||||||
</RsBadge>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-sm text-gray-400 italic">No category</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div v-if="form.group" class="text-sm">
|
|
||||||
<RsBadge variant="secondary" size="sm">
|
|
||||||
{{ form.group }}
|
|
||||||
</RsBadge>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-sm text-gray-400 italic">No group</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="text-sm text-gray-500">{{ formatDate(form.createdAt) }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="text-sm text-gray-500">{{ formatDate(form.updatedAt) }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<div class="flex space-x-2 justify-end">
|
|
||||||
<button
|
|
||||||
@click="editForm(form.id)"
|
|
||||||
class="p-1 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded"
|
|
||||||
title="Edit Form"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:edit" class="text-lg" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="duplicateForm(form)"
|
|
||||||
class="p-1 text-indigo-600 hover:text-indigo-900 hover:bg-indigo-50 rounded"
|
|
||||||
title="Duplicate Form"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:content-copy" class="text-lg" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="exportForm(form)"
|
|
||||||
class="p-1 text-green-600 hover:text-green-900 hover:bg-green-50 rounded"
|
|
||||||
title="Export Form as JSON"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:download" class="text-lg" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="confirmDelete(form.id)"
|
|
||||||
class="p-1 text-red-600 hover:text-red-900 hover:bg-red-50 rounded"
|
|
||||||
title="Delete Form"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:delete" class="text-lg" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
<div class="flex items-center gap-1">
|
||||||
</tr>
|
<Icon name="material-symbols:calendar-add-on-outline" class="w-4 h-4" />
|
||||||
</tbody>
|
Created {{ formatDate(form.createdAt) }}
|
||||||
</table>
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Icon name="material-symbols:schedule" class="w-4 h-4" />
|
||||||
|
Updated {{ formatDate(form.updatedAt) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 ml-4">
|
||||||
|
<button
|
||||||
|
@click="editForm(form.id)"
|
||||||
|
class="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
|
title="Edit Form"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:edit" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="duplicateForm(form)"
|
||||||
|
class="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded-lg transition-colors"
|
||||||
|
title="Duplicate Form"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:content-copy" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="exportForm(form)"
|
||||||
|
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
|
title="Export Form as JSON"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:download" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="confirmDelete(form.id)"
|
||||||
|
class="p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
title="Delete Form"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:delete" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-else class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||||
|
<Icon name="material-symbols:description-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
{{ hasActiveFilters ? 'No forms match your filters' : 'No forms found' }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
{{ hasActiveFilters ? 'Try adjusting your search or filter criteria' : 'Create your first form to get started' }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Show current filters if any -->
|
||||||
|
<div v-if="hasActiveFilters" class="text-sm text-gray-400 mb-6 space-y-1">
|
||||||
|
<div v-if="searchQuery">Search: "{{ searchQuery }}"</div>
|
||||||
|
<div v-if="selectedCategory">Category: {{ selectedCategory }}</div>
|
||||||
|
<div v-if="selectedGroup">Group: {{ selectedGroup }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-3">
|
||||||
|
<RsButton v-if="hasActiveFilters" @click="clearFilters" variant="secondary">
|
||||||
|
<Icon name="material-symbols:filter-alt-off" class="mr-2" />
|
||||||
|
Clear Filters
|
||||||
|
</RsButton>
|
||||||
|
<RsButton v-if="!hasActiveFilters" @click="createNewForm" variant="primary">
|
||||||
|
<Icon name="material-symbols:add" class="mr-2" />
|
||||||
|
Create New Form
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Global Loading Overlay -->
|
<!-- Global Loading Overlay -->
|
||||||
@ -541,18 +529,17 @@ const duplicateForm = async (form) => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const newName = `${form.name || 'Form'} (Copy)`;
|
const newName = `${form.name || 'Form'} (Copy)`;
|
||||||
|
|
||||||
// Create a copy of the form
|
// Load the original form first
|
||||||
const duplicatedForm = {
|
|
||||||
...form,
|
|
||||||
id: null, // Will get new ID when saved
|
|
||||||
name: newName,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load the original form, duplicate it, and save as new
|
|
||||||
await formStore.loadForm(form.id);
|
await formStore.loadForm(form.id);
|
||||||
|
|
||||||
|
// Clear the current form ID to ensure a new form is created
|
||||||
|
formStore.currentFormId = null;
|
||||||
|
|
||||||
|
// Update the form name for the duplicate
|
||||||
formStore.setFormName(newName);
|
formStore.setFormName(newName);
|
||||||
|
|
||||||
|
// Mark as having unsaved changes and save as new form
|
||||||
|
formStore.hasUnsavedChanges = true;
|
||||||
await formStore.saveForm();
|
await formStore.saveForm();
|
||||||
|
|
||||||
// Refresh the form list
|
// Refresh the form list
|
||||||
@ -923,9 +910,17 @@ const closeImportModal = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Custom styles for better UX */
|
/* Custom transitions and animations */
|
||||||
.hover\:bg-gray-50:hover {
|
.transition-colors {
|
||||||
transition: background-color 0.15s ease-in-out;
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-shadow {
|
||||||
|
transition-property: box-shadow;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
@ -933,6 +928,11 @@ button:disabled {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism effect for cards */
|
||||||
|
.bg-white {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Line clamp utility for description text */
|
/* Line clamp utility for description text */
|
||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
@ -283,8 +283,24 @@ const publishProcess = async (processId) => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
await processStore.publishProcess(processId);
|
await processStore.publishProcess(processId);
|
||||||
await loadProcesses();
|
await loadProcesses();
|
||||||
|
toast.success('Process published successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error publishing process:', error);
|
console.error('Error publishing process:', error);
|
||||||
|
toast.error('Failed to publish process: ' + (error.message || 'Unknown error'));
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unpublishProcess = async (processId) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
await processStore.unpublishProcess(processId);
|
||||||
|
await loadProcesses();
|
||||||
|
toast.success('Process unpublished successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unpublishing process:', error);
|
||||||
|
toast.error('Failed to unpublish process: ' + (error.message || 'Unknown error'));
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@ -303,6 +319,14 @@ const restoreProcess = async (processId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = (processId) => {
|
const confirmDelete = (processId) => {
|
||||||
|
const process = processStore.processes.find(p => p.id === processId);
|
||||||
|
|
||||||
|
// If process is published, show a message instead of allowing deletion
|
||||||
|
if (process && process.status === 'published') {
|
||||||
|
toast.warning('Please unpublish the process first before deleting it.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
processToDelete.value = processId;
|
processToDelete.value = processId;
|
||||||
showDeleteConfirm.value = true;
|
showDeleteConfirm.value = true;
|
||||||
};
|
};
|
||||||
@ -710,13 +734,23 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
<button
|
<button
|
||||||
v-if="process.status === 'draft'"
|
v-if="process.status === 'draft'"
|
||||||
@click="publishProcess(process.id)"
|
@click="publishProcess(process.id)"
|
||||||
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
class="p-2 text-green-600 hover:text-green-800 hover:bg-green-50 rounded-lg transition-colors"
|
||||||
title="Publish Process"
|
title="Publish Process"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:publish" class="text-lg" />
|
<Icon name="material-symbols:publish" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="process.status === 'published'"
|
||||||
|
@click="unpublishProcess(process.id)"
|
||||||
|
class="p-2 text-orange-600 hover:text-orange-800 hover:bg-orange-50 rounded-lg transition-colors"
|
||||||
|
title="Unpublish Process (Move to Draft)"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:unpublished" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="duplicateProcess(process)"
|
@click="duplicateProcess(process)"
|
||||||
class="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded-lg transition-colors"
|
class="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded-lg transition-colors"
|
||||||
@ -728,8 +762,8 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
@click="confirmDelete(process.id)"
|
@click="confirmDelete(process.id)"
|
||||||
class="p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors"
|
class="p-2 text-red-600 hover:text-red-800 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
title="Delete Process"
|
:title="process.status === 'published' ? 'Unpublish first to delete this process' : 'Delete Process'"
|
||||||
:disabled="loading || process.status === 'published'"
|
:disabled="loading || process.status === 'published'"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:delete" class="text-lg" />
|
<Icon name="material-symbols:delete" class="text-lg" />
|
||||||
@ -790,7 +824,7 @@ const copyWorkflowLink = async (processId) => {
|
|||||||
This process will be moved to trash but not permanently deleted. You can restore it later from the "Deleted" filter if needed.
|
This process will be moved to trash but not permanently deleted. You can restore it later from the "Deleted" filter if needed.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
Note: Published processes cannot be deleted and must be unpublished first.
|
Note: To delete a published process, first unpublish it using the orange unpublish button.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
84
server/api/process/[id]/unpublish.post.js
Normal file
84
server/api/process/[id]/unpublish.post.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
// Initialize Prisma client
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
// Get the process ID from the route parameter
|
||||||
|
const processId = getRouterParam(event, 'id');
|
||||||
|
|
||||||
|
if (!processId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process ID is required'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the ID is a UUID or numeric ID
|
||||||
|
const isUUID = processId.length === 36 && processId.includes('-');
|
||||||
|
|
||||||
|
// First, get the current process to validate it can be unpublished
|
||||||
|
const currentProcess = await prisma.process.findFirst({
|
||||||
|
where: isUUID
|
||||||
|
? { processUUID: processId }
|
||||||
|
: { processID: parseInt(processId) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentProcess) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the process is currently published
|
||||||
|
if (currentProcess.processStatus !== 'published') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process is not currently published'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the process status to draft
|
||||||
|
const unpublishedProcess = await prisma.process.update({
|
||||||
|
where: isUUID
|
||||||
|
? { processUUID: processId }
|
||||||
|
: { processID: parseInt(processId) },
|
||||||
|
data: {
|
||||||
|
processStatus: 'draft'
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
creator: {
|
||||||
|
select: {
|
||||||
|
userID: true,
|
||||||
|
userFullName: true,
|
||||||
|
userUsername: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Process unpublished successfully',
|
||||||
|
process: unpublishedProcess
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unpublishing process:', error);
|
||||||
|
|
||||||
|
// Handle specific Prisma errors
|
||||||
|
if (error.code === 'P2025') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to unpublish process',
|
||||||
|
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
@ -633,6 +633,39 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpublish a process (change status from published to draft)
|
||||||
|
*/
|
||||||
|
async unpublishProcess(processId) {
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`/api/process/${processId}/unpublish`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Update local state if process exists in the array
|
||||||
|
const process = this.processes.find(p => p.id === processId);
|
||||||
|
if (process) {
|
||||||
|
process.status = 'draft';
|
||||||
|
process.updatedAt = response.process.processModifiedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current process if it's the same one
|
||||||
|
if (this.currentProcess && this.currentProcess.id === processId) {
|
||||||
|
this.currentProcess.status = 'draft';
|
||||||
|
this.currentProcess.updatedAt = response.process.processModifiedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Failed to unpublish process');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unpublishing process:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duplicate a process
|
* Duplicate a process
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user