Add Form and Process History Modals with Versioning Support
- Introduced FormHistoryModal and ProcessHistoryModal components for viewing and restoring previous versions of forms and processes. - Implemented version tracking in the backend with new API endpoints for fetching and restoring historical data. - Added database migrations for form and process history tables to support versioning functionality. - Enhanced form and process update logic to save previous versions before modifications. - Updated documentation to include details on the new history system and its benefits for data management. - Improved user experience with intuitive modals for accessing historical data and restoring previous versions.
This commit is contained in:
parent
8805484de2
commit
33dc901107
377
components/FormHistoryModal.vue
Normal file
377
components/FormHistoryModal.vue
Normal file
@ -0,0 +1,377 @@
|
||||
<template>
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-6xl w-full mx-4 max-h-[90vh] overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">Form History</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">{{ formInfo.formName }}</p>
|
||||
</div>
|
||||
<button @click="$emit('close')" class="text-gray-400 hover:text-gray-600">
|
||||
<Icon name="heroicons:x-mark" class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex h-[calc(90vh-120px)]">
|
||||
<!-- Version List (Left Side) -->
|
||||
<div class="w-1/3 border-r border-gray-200 bg-gray-50">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h3 class="font-medium text-gray-900">Versions</h3>
|
||||
<p class="text-sm text-gray-600">{{ totalVersions }} total versions</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto h-full pb-20">
|
||||
<!-- Current Version -->
|
||||
<div class="p-4 bg-blue-50 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
Current
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900">Latest Version</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
{{ formatDate(currentVersion?.formModifiedDate || currentVersion?.formCreatedDate) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
by {{ currentVersion?.creator?.userFullName || 'Unknown' }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="previewCurrent"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm"
|
||||
:class="{ 'font-semibold': selectedVersion === 'current' }"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historical Versions -->
|
||||
<div
|
||||
v-for="version in history"
|
||||
:key="version.historyID"
|
||||
class="p-4 border-b border-gray-200 hover:bg-gray-100 cursor-pointer"
|
||||
:class="{ 'bg-blue-50': selectedVersion?.historyID === version.historyID }"
|
||||
@click="selectVersion(version)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm font-medium text-gray-900">
|
||||
Version {{ version.versionNumber }}
|
||||
</span>
|
||||
<span v-if="version.changeDescription" class="text-xs text-gray-500">
|
||||
- {{ version.changeDescription }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
{{ formatDate(version.savedDate) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
by {{ version.savedByUser?.userFullName || 'Unknown' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click.stop="previewVersion(version)"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
<button
|
||||
@click.stop="restoreVersion(version)"
|
||||
class="text-green-600 hover:text-green-800 text-sm"
|
||||
:disabled="isRestoring"
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Area (Right Side) -->
|
||||
<div class="flex-1 bg-white">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium text-gray-900">
|
||||
{{ previewData ? 'Preview' : 'Select a version to preview' }}
|
||||
</h3>
|
||||
<div v-if="previewData && selectedVersion !== 'current'" class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="restoreVersion(selectedVersion)"
|
||||
:disabled="isRestoring"
|
||||
class="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{{ isRestoring ? 'Restoring...' : 'Restore This Version' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto h-full pb-20">
|
||||
<!-- Preview Content -->
|
||||
<div v-if="previewData" class="p-6">
|
||||
<!-- Form Info -->
|
||||
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-medium text-gray-900 mb-2">Form Information</h4>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-600">Name:</span>
|
||||
<span class="ml-2 font-medium">{{ previewData.formName }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600">Components:</span>
|
||||
<span class="ml-2 font-medium">{{ previewData.formComponents?.length || 0 }}</span>
|
||||
</div>
|
||||
<div class="col-span-2" v-if="previewData.formDescription">
|
||||
<span class="text-gray-600">Description:</span>
|
||||
<span class="ml-2">{{ previewData.formDescription }}</span>
|
||||
</div>
|
||||
<div v-if="previewData.versionInfo" class="col-span-2">
|
||||
<span class="text-gray-600">Version:</span>
|
||||
<span class="ml-2 font-medium">{{ previewData.versionInfo.versionNumber }}</span>
|
||||
<span class="ml-2 text-gray-500">
|
||||
({{ formatDate(previewData.versionInfo.savedDate) }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Components Preview -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-medium text-gray-900">Form Components</h4>
|
||||
<div class="border rounded-lg p-4 bg-white">
|
||||
<div v-if="previewData.formComponents?.length" class="space-y-3">
|
||||
<div
|
||||
v-for="(component, index) in previewData.formComponents"
|
||||
:key="index"
|
||||
class="flex items-center justify-between p-3 border rounded-lg bg-gray-50"
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Icon :name="getComponentIcon(component.type)" class="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">
|
||||
{{ component.props?.label || component.type }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ component.props?.name || `${component.type}_${index + 1}` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ getComponentTypeName(component.type) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-8 text-gray-500">
|
||||
No components in this version
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Scripts Preview (if any) -->
|
||||
<div v-if="previewData.customScript" class="mt-6">
|
||||
<h4 class="font-medium text-gray-900 mb-2">Custom Scripts</h4>
|
||||
<div class="bg-gray-900 text-gray-100 p-4 rounded-lg text-sm font-mono">
|
||||
<pre class="whitespace-pre-wrap">{{ previewData.customScript }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="flex items-center justify-center h-full text-gray-500">
|
||||
<div class="text-center">
|
||||
<Icon name="heroicons:document-text" class="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
||||
<p>Select a version to preview its contents</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer with Close Button -->
|
||||
<div class="flex justify-end p-4 border-t border-gray-200 bg-gray-50">
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useFormBuilderStore } from '~/stores/formBuilder'
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
formId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'restored'])
|
||||
|
||||
const formStore = useFormBuilderStore()
|
||||
|
||||
// Try to use toast composable if available, with fallback
|
||||
let toast
|
||||
try {
|
||||
toast = useToast()
|
||||
} catch (error) {
|
||||
toast = {
|
||||
success: (msg) => console.log('Success:', msg),
|
||||
error: (msg) => console.error('Error:', msg),
|
||||
info: (msg) => console.info('Info:', msg),
|
||||
warning: (msg) => console.warn('Warning:', msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Reactive data
|
||||
const history = ref([])
|
||||
const currentVersion = ref(null)
|
||||
const formInfo = ref({})
|
||||
const totalVersions = ref(0)
|
||||
const selectedVersion = ref(null)
|
||||
const previewData = ref(null)
|
||||
const isRestoring = ref(false)
|
||||
|
||||
// Computed
|
||||
const isLoading = computed(() => history.value.length === 0 && props.isOpen)
|
||||
|
||||
// Methods
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
const result = await formStore.getFormHistory(props.formId)
|
||||
history.value = result.history
|
||||
currentVersion.value = result.currentVersion
|
||||
formInfo.value = result.form
|
||||
totalVersions.value = result.totalVersions
|
||||
} catch (error) {
|
||||
console.error('Error loading form history:', error)
|
||||
toast.error('Failed to load form history')
|
||||
}
|
||||
}
|
||||
|
||||
const selectVersion = (version) => {
|
||||
selectedVersion.value = version
|
||||
previewVersion(version)
|
||||
}
|
||||
|
||||
const previewCurrent = () => {
|
||||
selectedVersion.value = 'current'
|
||||
previewData.value = {
|
||||
formName: currentVersion.value.formName,
|
||||
formDescription: currentVersion.value.formDescription,
|
||||
formComponents: currentVersion.value.formComponents,
|
||||
customScript: currentVersion.value.customScript,
|
||||
customCSS: currentVersion.value.customCSS,
|
||||
formEvents: currentVersion.value.formEvents,
|
||||
scriptMode: currentVersion.value.scriptMode
|
||||
}
|
||||
}
|
||||
|
||||
const previewVersion = async (version) => {
|
||||
try {
|
||||
if (version === 'current') {
|
||||
previewCurrent()
|
||||
return
|
||||
}
|
||||
|
||||
const result = await formStore.loadFormVersionPreview(props.formId, version.historyID)
|
||||
previewData.value = result
|
||||
} catch (error) {
|
||||
console.error('Error loading version preview:', error)
|
||||
toast.error('Failed to load version preview')
|
||||
}
|
||||
}
|
||||
|
||||
const restoreVersion = async (version) => {
|
||||
if (isRestoring.value) return
|
||||
|
||||
const confirmed = confirm(`Are you sure you want to restore to version ${version.versionNumber}? This will create a new version with the restored content.`)
|
||||
if (!confirmed) return
|
||||
|
||||
isRestoring.value = true
|
||||
try {
|
||||
const result = await formStore.restoreFormVersion(props.formId, version)
|
||||
toast.success(result.message || 'Form restored successfully')
|
||||
emit('restored', result)
|
||||
emit('close')
|
||||
} catch (error) {
|
||||
console.error('Error restoring version:', error)
|
||||
toast.error('Failed to restore form version')
|
||||
} finally {
|
||||
isRestoring.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return 'Unknown'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const getComponentIcon = (type) => {
|
||||
const icons = {
|
||||
'text': 'heroicons:document-text',
|
||||
'textarea': 'heroicons:document-text',
|
||||
'number': 'heroicons:hashtag',
|
||||
'email': 'heroicons:at-symbol',
|
||||
'password': 'heroicons:lock-closed',
|
||||
'select': 'heroicons:chevron-down',
|
||||
'checkbox': 'heroicons:check-circle',
|
||||
'radio': 'heroicons:radio',
|
||||
'date': 'heroicons:calendar',
|
||||
'file': 'heroicons:document-arrow-up',
|
||||
'button': 'heroicons:cursor-arrow-rays',
|
||||
'heading': 'heroicons:h1',
|
||||
'paragraph': 'heroicons:document-text'
|
||||
}
|
||||
return icons[type] || 'heroicons:square-3-stack-3d'
|
||||
}
|
||||
|
||||
const getComponentTypeName = (type) => {
|
||||
const names = {
|
||||
'text': 'Text Field',
|
||||
'textarea': 'Text Area',
|
||||
'number': 'Number Field',
|
||||
'email': 'Email Field',
|
||||
'password': 'Password Field',
|
||||
'select': 'Select Dropdown',
|
||||
'checkbox': 'Checkbox Group',
|
||||
'radio': 'Radio Group',
|
||||
'date': 'Date Picker',
|
||||
'file': 'File Upload',
|
||||
'button': 'Button',
|
||||
'heading': 'Heading',
|
||||
'paragraph': 'Paragraph'
|
||||
}
|
||||
return names[type] || type.charAt(0).toUpperCase() + type.slice(1)
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(() => props.isOpen, (newValue) => {
|
||||
if (newValue) {
|
||||
loadHistory()
|
||||
} else {
|
||||
// Reset state when modal closes
|
||||
selectedVersion.value = null
|
||||
previewData.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
393
components/ProcessHistoryModal.vue
Normal file
393
components/ProcessHistoryModal.vue
Normal file
@ -0,0 +1,393 @@
|
||||
<template>
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-6xl w-full mx-4 max-h-[90vh] overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">Process History</h2>
|
||||
<p class="text-sm text-gray-600 mt-1">{{ processInfo.processName }}</p>
|
||||
</div>
|
||||
<button @click="$emit('close')" class="text-gray-400 hover:text-gray-600">
|
||||
<Icon name="heroicons:x-mark" class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex h-[calc(90vh-120px)]">
|
||||
<!-- Version List (Left Side) -->
|
||||
<div class="w-1/3 border-r border-gray-200 bg-gray-50">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h3 class="font-medium text-gray-900">Versions</h3>
|
||||
<p class="text-sm text-gray-600">{{ totalVersions }} total versions</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto h-full pb-20">
|
||||
<!-- Current Version -->
|
||||
<div class="p-4 bg-blue-50 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
Current
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900">Latest Version</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
{{ formatDate(currentVersion?.processModifiedDate || currentVersion?.processCreatedDate) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
by {{ currentVersion?.creator?.userFullName || 'Unknown' }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@click="previewCurrent"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm"
|
||||
:class="{ 'font-semibold': selectedVersion === 'current' }"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historical Versions -->
|
||||
<div
|
||||
v-for="version in history"
|
||||
:key="version.historyID"
|
||||
class="p-4 border-b border-gray-200 hover:bg-gray-100 cursor-pointer"
|
||||
:class="{ 'bg-blue-50': selectedVersion?.historyID === version.historyID }"
|
||||
@click="selectVersion(version)"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm font-medium text-gray-900">
|
||||
Version {{ version.versionNumber }}
|
||||
</span>
|
||||
<span v-if="version.changeDescription" class="text-xs text-gray-500">
|
||||
- {{ version.changeDescription }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-1">
|
||||
{{ formatDate(version.savedDate) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
by {{ version.savedByUser?.userFullName || 'Unknown' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click.stop="previewVersion(version)"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
<button
|
||||
@click.stop="restoreVersion(version)"
|
||||
class="text-green-600 hover:text-green-800 text-sm"
|
||||
:disabled="isRestoring"
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Area (Right Side) -->
|
||||
<div class="flex-1 bg-white">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-medium text-gray-900">
|
||||
{{ previewData ? 'Preview' : 'Select a version to preview' }}
|
||||
</h3>
|
||||
<div v-if="previewData && selectedVersion !== 'current'" class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="restoreVersion(selectedVersion)"
|
||||
:disabled="isRestoring"
|
||||
class="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{{ isRestoring ? 'Restoring...' : 'Restore This Version' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto h-full pb-20">
|
||||
<!-- Preview Content -->
|
||||
<div v-if="previewData" class="p-6">
|
||||
<!-- Process Info -->
|
||||
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||
<h4 class="font-medium text-gray-900 mb-2">Process Information</h4>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-600">Name:</span>
|
||||
<span class="ml-2 font-medium">{{ previewData.processName }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600">Status:</span>
|
||||
<span class="ml-2 font-medium capitalize">{{ previewData.processStatus }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600">Nodes:</span>
|
||||
<span class="ml-2 font-medium">{{ previewData.processDefinition?.nodes?.length || 0 }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-600">Connections:</span>
|
||||
<span class="ml-2 font-medium">{{ previewData.processDefinition?.edges?.length || 0 }}</span>
|
||||
</div>
|
||||
<div v-if="previewData.processCategory" class="col-span-2">
|
||||
<span class="text-gray-600">Category:</span>
|
||||
<span class="ml-2">{{ previewData.processCategory }}</span>
|
||||
</div>
|
||||
<div class="col-span-2" v-if="previewData.processDescription">
|
||||
<span class="text-gray-600">Description:</span>
|
||||
<span class="ml-2">{{ previewData.processDescription }}</span>
|
||||
</div>
|
||||
<div v-if="previewData.versionInfo" class="col-span-2">
|
||||
<span class="text-gray-600">Version:</span>
|
||||
<span class="ml-2 font-medium">{{ previewData.versionInfo.versionNumber }}</span>
|
||||
<span class="ml-2 text-gray-500">
|
||||
({{ formatDate(previewData.versionInfo.savedDate) }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Nodes Preview -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-medium text-gray-900">Process Nodes</h4>
|
||||
<div class="border rounded-lg p-4 bg-white">
|
||||
<div v-if="previewData.processDefinition?.nodes?.length" class="space-y-3">
|
||||
<div
|
||||
v-for="(node, index) in previewData.processDefinition.nodes"
|
||||
:key="index"
|
||||
class="flex items-center justify-between p-3 border rounded-lg bg-gray-50"
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Icon :name="getNodeIcon(node.type)" class="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">
|
||||
{{ node.data?.label || node.label || node.type }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ node.id }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ getNodeTypeName(node.type) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-8 text-gray-500">
|
||||
No nodes in this version
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Variables Preview (if any) -->
|
||||
<div v-if="previewData.processVariables && Object.keys(previewData.processVariables).length > 0" class="mt-6">
|
||||
<h4 class="font-medium text-gray-900 mb-2">Process Variables</h4>
|
||||
<div class="bg-gray-900 text-gray-100 p-4 rounded-lg text-sm font-mono">
|
||||
<pre class="whitespace-pre-wrap">{{ JSON.stringify(previewData.processVariables, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Settings Preview (if any) -->
|
||||
<div v-if="previewData.processSettings && Object.keys(previewData.processSettings).length > 0" class="mt-6">
|
||||
<h4 class="font-medium text-gray-900 mb-2">Process Settings</h4>
|
||||
<div class="bg-gray-900 text-gray-100 p-4 rounded-lg text-sm font-mono">
|
||||
<pre class="whitespace-pre-wrap">{{ JSON.stringify(previewData.processSettings, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="flex items-center justify-center h-full text-gray-500">
|
||||
<div class="text-center">
|
||||
<Icon name="heroicons:document-text" class="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
||||
<p>Select a version to preview its contents</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer with Close Button -->
|
||||
<div class="flex justify-end p-4 border-t border-gray-200 bg-gray-50">
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="px-4 py-2 text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useProcessBuilderStore } from '~/stores/processBuilder'
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
processId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'restored'])
|
||||
|
||||
const processStore = useProcessBuilderStore()
|
||||
|
||||
// Try to use toast composable if available, with fallback
|
||||
let toast
|
||||
try {
|
||||
toast = useToast()
|
||||
} catch (error) {
|
||||
toast = {
|
||||
success: (msg) => console.log('Success:', msg),
|
||||
error: (msg) => console.error('Error:', msg),
|
||||
info: (msg) => console.info('Info:', msg),
|
||||
warning: (msg) => console.warn('Warning:', msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Reactive data
|
||||
const history = ref([])
|
||||
const currentVersion = ref(null)
|
||||
const processInfo = ref({})
|
||||
const totalVersions = ref(0)
|
||||
const selectedVersion = ref(null)
|
||||
const previewData = ref(null)
|
||||
const isRestoring = ref(false)
|
||||
|
||||
// Computed
|
||||
const isLoading = computed(() => history.value.length === 0 && props.isOpen)
|
||||
|
||||
// Methods
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
const result = await processStore.getProcessHistory(props.processId)
|
||||
history.value = result.history
|
||||
currentVersion.value = result.currentVersion
|
||||
processInfo.value = result.form
|
||||
totalVersions.value = result.totalVersions
|
||||
} catch (error) {
|
||||
console.error('Error loading process history:', error)
|
||||
toast.error('Failed to load process history')
|
||||
}
|
||||
}
|
||||
|
||||
const selectVersion = (version) => {
|
||||
selectedVersion.value = version
|
||||
previewVersion(version)
|
||||
}
|
||||
|
||||
const previewCurrent = () => {
|
||||
selectedVersion.value = 'current'
|
||||
previewData.value = {
|
||||
processName: currentVersion.value.processName,
|
||||
processDescription: currentVersion.value.processDescription,
|
||||
processDefinition: currentVersion.value.processDefinition,
|
||||
processStatus: currentVersion.value.processStatus,
|
||||
processCategory: currentVersion.value.processCategory,
|
||||
processOwner: currentVersion.value.processOwner,
|
||||
processPermissions: currentVersion.value.processPermissions,
|
||||
processPriority: currentVersion.value.processPriority,
|
||||
processSettings: currentVersion.value.processSettings,
|
||||
processVariables: currentVersion.value.processVariables,
|
||||
templateCategory: currentVersion.value.templateCategory
|
||||
}
|
||||
}
|
||||
|
||||
const previewVersion = async (version) => {
|
||||
try {
|
||||
if (version === 'current') {
|
||||
previewCurrent()
|
||||
return
|
||||
}
|
||||
|
||||
const result = await processStore.loadProcessVersionPreview(props.processId, version.historyID)
|
||||
previewData.value = result
|
||||
} catch (error) {
|
||||
console.error('Error loading version preview:', error)
|
||||
toast.error('Failed to load version preview')
|
||||
}
|
||||
}
|
||||
|
||||
const restoreVersion = async (version) => {
|
||||
if (isRestoring.value) return
|
||||
|
||||
const confirmed = confirm(`Are you sure you want to restore to version ${version.versionNumber}? This will create a new version with the restored content.`)
|
||||
if (!confirmed) return
|
||||
|
||||
isRestoring.value = true
|
||||
try {
|
||||
const result = await processStore.restoreProcessVersion(props.processId, version)
|
||||
toast.success(result.message || 'Process restored successfully')
|
||||
emit('restored', result)
|
||||
emit('close')
|
||||
} catch (error) {
|
||||
console.error('Error restoring version:', error)
|
||||
toast.error('Failed to restore process version')
|
||||
} finally {
|
||||
isRestoring.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return 'Unknown'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const getNodeIcon = (type) => {
|
||||
const icons = {
|
||||
'start': 'heroicons:play-circle',
|
||||
'end': 'heroicons:stop-circle',
|
||||
'task': 'heroicons:rectangle-stack',
|
||||
'form': 'heroicons:document-text',
|
||||
'gateway': 'heroicons:arrows-pointing-out',
|
||||
'script': 'heroicons:code-bracket',
|
||||
'api': 'heroicons:cloud-arrow-down',
|
||||
'notification': 'heroicons:bell',
|
||||
'business-rule': 'heroicons:document-check'
|
||||
}
|
||||
return icons[type] || 'heroicons:rectangle-stack'
|
||||
}
|
||||
|
||||
const getNodeTypeName = (type) => {
|
||||
const names = {
|
||||
'start': 'Start Event',
|
||||
'end': 'End Event',
|
||||
'task': 'Task',
|
||||
'form': 'Form Task',
|
||||
'gateway': 'Gateway',
|
||||
'script': 'Script Task',
|
||||
'api': 'API Call',
|
||||
'notification': 'Notification',
|
||||
'business-rule': 'Business Rule'
|
||||
}
|
||||
return names[type] || type.charAt(0).toUpperCase() + type.slice(1)
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(() => props.isOpen, (newValue) => {
|
||||
if (newValue) {
|
||||
loadHistory()
|
||||
} else {
|
||||
// Reset state when modal closes
|
||||
selectedVersion.value = null
|
||||
previewData.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
134
docs/form-builder/FORM_HISTORY_SYSTEM.md
Normal file
134
docs/form-builder/FORM_HISTORY_SYSTEM.md
Normal file
@ -0,0 +1,134 @@
|
||||
# Form History & Versioning System
|
||||
|
||||
## Overview
|
||||
|
||||
The form history system provides automatic versioning for all forms, allowing users to view previous versions and restore to any point in time. Every time a user saves a form, the previous version is automatically archived in the `formHistory` table.
|
||||
|
||||
## Database Schema
|
||||
|
||||
### formHistory Table
|
||||
- `historyID` - Primary key for the history entry
|
||||
- `formID` - Foreign key linking to the main form
|
||||
- `formUUID` - UUID of the form for easy reference
|
||||
- `formName` - Name of the form at the time of save
|
||||
- `formDescription` - Description at the time of save
|
||||
- `formComponents` - JSON of all form components at the time of save
|
||||
- `formStatus` - Status (active/inactive) at the time of save
|
||||
- `customCSS` - Custom CSS code at the time of save
|
||||
- `customScript` - Custom JavaScript code at the time of save
|
||||
- `formEvents` - Form events configuration at the time of save
|
||||
- `scriptMode` - Script execution mode (safe/advanced) at the time of save
|
||||
- `versionNumber` - Sequential version number (1, 2, 3...)
|
||||
- `changeDescription` - Optional description of changes made
|
||||
- `savedBy` - User ID who saved this version
|
||||
- `savedDate` - Timestamp when this version was saved
|
||||
|
||||
## How It Works
|
||||
|
||||
### Automatic Versioning
|
||||
1. When a user clicks "Save" on a form
|
||||
2. The system first retrieves the current form data
|
||||
3. The current form data is saved to `formHistory` with the next sequential version number
|
||||
4. The form is then updated with the new data
|
||||
5. Each save creates a complete snapshot of the form state
|
||||
|
||||
### Version Management
|
||||
- **Version Numbers**: Sequential integers starting from 1
|
||||
- **Change Descriptions**: Optional descriptions that can be added when saving
|
||||
- **User Tracking**: Each version records who made the changes
|
||||
- **Complete Snapshots**: Each version contains all form data, not just changes
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Get Form History
|
||||
```
|
||||
GET /api/forms/{formId}/history
|
||||
```
|
||||
Returns all versions of a form with metadata.
|
||||
|
||||
### Get Specific Version
|
||||
```
|
||||
GET /api/forms/{formId}/version/{versionId}
|
||||
```
|
||||
Returns details of a specific version. `versionId` can be either the `historyID` or `versionNumber`.
|
||||
|
||||
### Restore Version
|
||||
```
|
||||
POST /api/forms/{formId}/restore
|
||||
```
|
||||
Body:
|
||||
```json
|
||||
{
|
||||
"versionNumber": 5,
|
||||
"restoredBy": 1,
|
||||
"changeDescription": "Restored to working version"
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Features
|
||||
|
||||
### History Modal
|
||||
- **Version List**: Shows all versions with timestamps and change descriptions
|
||||
- **Preview**: Users can preview any version before restoring
|
||||
- **Restore**: One-click restore to any previous version
|
||||
- **User Information**: Shows who made each change
|
||||
|
||||
### History Button
|
||||
- Appears in the form builder header next to "Templates"
|
||||
- Only visible for saved forms (not new forms)
|
||||
- Opens the history modal for the current form
|
||||
|
||||
## Store Functions
|
||||
|
||||
### Form Builder Store Methods
|
||||
- `getFormHistory(formId)` - Get all versions of a form
|
||||
- `getFormVersion(formId, versionId)` - Get specific version details
|
||||
- `restoreFormVersion(formId, versionData)` - Restore to a specific version
|
||||
- `loadFormVersionPreview(formId, versionId)` - Load version for preview only
|
||||
- `setChangeDescription(description)` - Set description for next save
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Save with Description
|
||||
```javascript
|
||||
// Set optional change description
|
||||
formStore.setChangeDescription("Added validation to email field");
|
||||
|
||||
// Save form (automatically creates history entry)
|
||||
await formStore.saveForm();
|
||||
```
|
||||
|
||||
### View Form History
|
||||
```javascript
|
||||
const history = await formStore.getFormHistory(formId);
|
||||
console.log(`Form has ${history.totalVersions} versions`);
|
||||
```
|
||||
|
||||
### Restore to Previous Version
|
||||
```javascript
|
||||
const versionToRestore = history.history[2]; // Version 3
|
||||
await formStore.restoreFormVersion(formId, versionToRestore, "Reverted problematic changes");
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Never Lose Data**: Every form state is preserved
|
||||
2. **Easy Rollback**: One-click restore to any previous version
|
||||
3. **Change Tracking**: See who made changes and when
|
||||
4. **Preview Before Restore**: View any version before making it current
|
||||
5. **Audit Trail**: Complete history of all form modifications
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- **Storage Efficiency**: Each version stores complete form data for simplicity and reliability
|
||||
- **Cascade Deletion**: When a form is deleted, all its history is automatically removed
|
||||
- **Foreign Key Constraints**: Ensures data integrity between forms and history
|
||||
- **Indexing**: Optimized for fast lookups by form, date, and UUID
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Diff View**: Show differences between versions
|
||||
- **Branch/Merge**: Allow creating branches of forms
|
||||
- **Bulk Operations**: Restore multiple forms to specific dates
|
||||
- **Export History**: Download form history as JSON/CSV
|
||||
- **Retention Policy**: Automatic cleanup of old versions
|
@ -2,27 +2,27 @@
|
||||
<div class="flex flex-col h-screen bg-gray-50">
|
||||
<!-- Header Bar -->
|
||||
<header
|
||||
class="bg-gray-800 px-4 py-4 flex items-center justify-between text-white shadow-md"
|
||||
class="bg-gray-800 px-6 py-3 flex items-center justify-between text-white shadow-lg border-b border-gray-700"
|
||||
>
|
||||
<!-- Left section - Logo and navigation -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-4">
|
||||
<Icon
|
||||
@click="navigateTo('/', { external: true })"
|
||||
name="ph:arrow-circle-left-duotone"
|
||||
class="cursor-pointer w-6 h-6"
|
||||
class="cursor-pointer w-6 h-6 hover:text-gray-300 transition-colors"
|
||||
/>
|
||||
<img
|
||||
src="@/assets/img/logo/logo-word-white.svg"
|
||||
alt="Corrad Logo"
|
||||
class="h-7"
|
||||
/>
|
||||
<div v-if="isPreview" class="bg-blue-500 text-white text-xs px-2 py-1 rounded-full ml-2">
|
||||
<div v-if="isPreview" class="bg-blue-500 text-white text-xs px-3 py-1 rounded-full ml-2 font-medium">
|
||||
Preview Mode
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle section - Form name -->
|
||||
<div class="flex-1 flex justify-center items-center mx-4">
|
||||
<div class="flex-1 flex justify-center items-center mx-8">
|
||||
<FormKit
|
||||
v-if="!isPreview"
|
||||
type="text"
|
||||
@ -41,48 +41,57 @@
|
||||
</div>
|
||||
|
||||
<!-- Right section - Actions -->
|
||||
<div class="flex items-center">
|
||||
<!-- Primary actions -->
|
||||
<div class="flex items-center mr-2 border-r border-gray-600 pr-2">
|
||||
<RsButton v-if="!isPreview" @click="handleSave" variant="primary" size="sm" class="mr-2">
|
||||
<Icon name="material-symbols:save" class="mr-1" />
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Primary Action Group -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<RsButton v-if="!isPreview" @click="handleSave" variant="primary" size="sm">
|
||||
<Icon name="material-symbols:save" class="mr-1.5" />
|
||||
Save
|
||||
</RsButton>
|
||||
|
||||
<RsButton @click="togglePreview" :variant="isPreview ? 'primary' : 'secondary'" size="sm">
|
||||
<Icon :name="isPreview ? 'material-symbols:edit' : 'material-symbols:preview'" class="mr-1" />
|
||||
<Icon :name="isPreview ? 'material-symbols:edit' : 'material-symbols:preview'" class="mr-1.5" />
|
||||
{{ isPreview ? 'Edit' : 'Preview' }}
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Templates button -->
|
||||
<div v-if="!isPreview" class="mr-2 border-r border-gray-600 pr-2">
|
||||
<!-- Secondary Action Group -->
|
||||
<div v-if="!isPreview" class="flex items-center space-x-2">
|
||||
<RsButton @click="showTemplatesModal = true" variant="secondary" size="sm">
|
||||
<Icon name="material-symbols:description-outline" class="mr-1" />
|
||||
<Icon name="material-symbols:description-outline" class="mr-1.5" />
|
||||
Templates
|
||||
</RsButton>
|
||||
|
||||
<!-- Form History button - only show if form is saved -->
|
||||
<RsButton
|
||||
v-if="formStore.currentFormId"
|
||||
@click="showFormHistoryModal = true"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
<Icon name="material-symbols:history" class="mr-1.5" />
|
||||
History
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Secondary actions - only in edit mode -->
|
||||
<div v-if="!isPreview" class="flex items-center">
|
||||
<div class="dropdown relative">
|
||||
<!-- More Actions Dropdown -->
|
||||
<div v-if="!isPreview" class="relative">
|
||||
<RsButton @click="showDropdown = !showDropdown" variant="tertiary" size="sm" class="flex items-center">
|
||||
<Icon name="material-symbols:more-vert" class="w-5 h-5" />
|
||||
</RsButton>
|
||||
|
||||
<div v-if="showDropdown" class="dropdown-menu absolute right-0 mt-2 bg-white rounded shadow-lg py-1 z-10 w-48 text-gray-800">
|
||||
<button @click="showFormSettings = true; showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
||||
<Icon name="material-symbols:code" class="mr-2 w-4 h-4" />
|
||||
<div v-if="showDropdown" class="dropdown-menu absolute right-0 mt-2 bg-white rounded-lg shadow-lg py-2 z-10 w-48 text-gray-800 border border-gray-200">
|
||||
<button @click="showFormSettings = true; showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-50 flex items-center transition-colors">
|
||||
<Icon name="material-symbols:code" class="mr-3 w-4 h-4 text-gray-500" />
|
||||
<span>Form Settings</span>
|
||||
</button>
|
||||
<button @click="navigateToManage(); showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
||||
<Icon name="material-symbols:settings" class="mr-2 w-4 h-4" />
|
||||
<button @click="navigateToManage(); showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-50 flex items-center transition-colors">
|
||||
<Icon name="material-symbols:settings" class="mr-3 w-4 h-4 text-gray-500" />
|
||||
<span>Manage Forms</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
@ -861,6 +870,14 @@
|
||||
@close="showFieldSettings = false"
|
||||
/>
|
||||
|
||||
<!-- Form History Modal -->
|
||||
<FormHistoryModal
|
||||
:is-open="showFormHistoryModal"
|
||||
:form-id="formStore.currentFormId"
|
||||
@close="showFormHistoryModal = false"
|
||||
@restored="handleFormRestored"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -872,6 +889,7 @@ import FormTemplatesModal from '~/components/FormTemplatesModal.vue';
|
||||
import FormBuilderFieldSettingsModal from '~/components/FormBuilderFieldSettingsModal.vue';
|
||||
import FormScriptEngine from '~/components/FormScriptEngine.vue';
|
||||
import ConditionalLogicEngine from '~/components/ConditionalLogicEngine.vue';
|
||||
import FormHistoryModal from '~/components/FormHistoryModal.vue';
|
||||
|
||||
definePageMeta({
|
||||
title: "Form Builder",
|
||||
@ -911,6 +929,7 @@ const showDropdown = ref(false);
|
||||
const showTemplatesModal = ref(false);
|
||||
const showFieldSettings = ref(false);
|
||||
const showFieldSettingsPanel = ref(false);
|
||||
const showFormHistoryModal = ref(false);
|
||||
const previewForm = ref(null);
|
||||
const formScriptEngine = ref(null);
|
||||
const conditionalLogicEngine = ref(null);
|
||||
@ -2443,6 +2462,12 @@ const handleConditionalLogicGenerated = (script) => {
|
||||
formStore.formCustomScript += `\n// Conditional Logic Script\n${script}`;
|
||||
toast.success('Conditional logic script added successfully');
|
||||
};
|
||||
|
||||
const handleFormRestored = (restoredForm) => {
|
||||
// Handle form restoration logic
|
||||
console.log('Form restored:', restoredForm);
|
||||
// You might want to update the form state or show a success message
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -20,6 +20,7 @@ import BusinessRuleNodeConfigurationModal from '~/components/process-flow/Busine
|
||||
import NotificationNodeConfigurationModal from '~/components/process-flow/NotificationNodeConfigurationModal.vue';
|
||||
import ProcessTemplatesModal from '~/components/ProcessTemplatesModal.vue';
|
||||
import ProcessSettingsModal from '~/components/process-flow/ProcessSettingsModal.vue';
|
||||
import ProcessHistoryModal from '~/components/ProcessHistoryModal.vue';
|
||||
|
||||
// Define page meta
|
||||
definePageMeta({
|
||||
@ -88,6 +89,7 @@ const showNotificationConfigModal = ref(false);
|
||||
const showTemplatesModal = ref(false);
|
||||
const showProcessSettings = ref(false);
|
||||
const showDropdown = ref(false);
|
||||
const showProcessHistoryModal = ref(false);
|
||||
|
||||
// Component definitions
|
||||
const components = [
|
||||
@ -997,6 +999,14 @@ const handleNotificationNodeUpdate = (updatedData) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle process restoration from history
|
||||
const handleProcessRestored = (restoredProcess) => {
|
||||
// The process has been restored in the backend, so we need to reload it
|
||||
console.log('Process restored:', restoredProcess);
|
||||
// The current process will be automatically updated by the store
|
||||
toast.success('Process has been restored successfully');
|
||||
};
|
||||
|
||||
// Navigate to variables page
|
||||
const navigateToVariables = () => {
|
||||
confirmNavigation('/variables');
|
||||
@ -1087,6 +1097,14 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Process History button - only show if process is saved -->
|
||||
<div v-if="processStore.currentProcess && processStore.currentProcess.id" class="mr-2 border-r border-gray-600 pr-2">
|
||||
<RsButton @click="showProcessHistoryModal = true" variant="secondary" size="sm">
|
||||
<Icon name="material-symbols:history" class="mr-1" />
|
||||
History
|
||||
</RsButton>
|
||||
</div>
|
||||
|
||||
<!-- Secondary actions -->
|
||||
<div class="flex items-center">
|
||||
<div class="dropdown relative">
|
||||
@ -1099,10 +1117,6 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
<Icon name="material-symbols:tune" class="mr-2 w-4 h-4" />
|
||||
<span>Process Settings</span>
|
||||
</button>
|
||||
<button @click="navigateToVariables(); showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
||||
<Icon name="material-symbols:data-object" class="mr-2 w-4 h-4" />
|
||||
<span>Variables</span>
|
||||
</button>
|
||||
<button @click="confirmNavigation('/process-builder/manage'); showDropdown = false" class="w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
||||
<Icon name="material-symbols:settings" class="mr-2 w-4 h-4" />
|
||||
<span>Manage Processes</span>
|
||||
@ -1332,6 +1346,14 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
||||
<ProcessSettingsModal
|
||||
v-model="showProcessSettings"
|
||||
/>
|
||||
|
||||
<!-- Process History Modal -->
|
||||
<ProcessHistoryModal
|
||||
:is-open="showProcessHistoryModal"
|
||||
:process-id="processStore.currentProcess?.id"
|
||||
@close="showProcessHistoryModal = false"
|
||||
@restored="handleProcessRestored"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -69,12 +69,24 @@
|
||||
"$ref": "#/definitions/form"
|
||||
}
|
||||
},
|
||||
"formHistoryEntries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/formHistory"
|
||||
}
|
||||
},
|
||||
"processes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/process"
|
||||
}
|
||||
},
|
||||
"processHistoryEntries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/processHistory"
|
||||
}
|
||||
},
|
||||
"userrole": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@ -228,6 +240,99 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"history": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/formHistory"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"formHistory": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"historyID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"formUUID": {
|
||||
"type": "string"
|
||||
},
|
||||
"formName": {
|
||||
"type": "string"
|
||||
},
|
||||
"formDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"formComponents": {
|
||||
"type": [
|
||||
"number",
|
||||
"string",
|
||||
"boolean",
|
||||
"object",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"formStatus": {
|
||||
"type": "string"
|
||||
},
|
||||
"customCSS": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"customScript": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"formEvents": {
|
||||
"type": [
|
||||
"number",
|
||||
"string",
|
||||
"boolean",
|
||||
"object",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"scriptMode": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"versionNumber": {
|
||||
"type": "integer"
|
||||
},
|
||||
"changeDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedDate": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"form": {
|
||||
"$ref": "#/definitions/form"
|
||||
},
|
||||
"savedByUser": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/user"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -353,6 +458,128 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"history": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/processHistory"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"processHistory": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"historyID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"processUUID": {
|
||||
"type": "string"
|
||||
},
|
||||
"processName": {
|
||||
"type": "string"
|
||||
},
|
||||
"processDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"processDefinition": {
|
||||
"type": [
|
||||
"number",
|
||||
"string",
|
||||
"boolean",
|
||||
"object",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"processVersion": {
|
||||
"type": "integer"
|
||||
},
|
||||
"processStatus": {
|
||||
"type": "string"
|
||||
},
|
||||
"processCategory": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"processOwner": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"processPermissions": {
|
||||
"type": [
|
||||
"number",
|
||||
"string",
|
||||
"boolean",
|
||||
"object",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"processPriority": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"processSettings": {
|
||||
"type": [
|
||||
"number",
|
||||
"string",
|
||||
"boolean",
|
||||
"object",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"processVariables": {
|
||||
"type": [
|
||||
"number",
|
||||
"string",
|
||||
"boolean",
|
||||
"object",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"templateCategory": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"versionNumber": {
|
||||
"type": "integer"
|
||||
},
|
||||
"changeDescription": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"savedDate": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"process": {
|
||||
"$ref": "#/definitions/process"
|
||||
},
|
||||
"savedByUser": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/user"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -371,8 +598,14 @@
|
||||
"form": {
|
||||
"$ref": "#/definitions/form"
|
||||
},
|
||||
"formHistory": {
|
||||
"$ref": "#/definitions/formHistory"
|
||||
},
|
||||
"process": {
|
||||
"$ref": "#/definitions/process"
|
||||
},
|
||||
"processHistory": {
|
||||
"$ref": "#/definitions/processHistory"
|
||||
}
|
||||
}
|
||||
}
|
25
prisma/migrations/20241201_add_form_history.sql
Normal file
25
prisma/migrations/20241201_add_form_history.sql
Normal file
@ -0,0 +1,25 @@
|
||||
-- Create formHistory table for form versioning
|
||||
CREATE TABLE `formHistory` (
|
||||
`historyID` int NOT NULL AUTO_INCREMENT,
|
||||
`formID` int NOT NULL,
|
||||
`formUUID` varchar(36) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`formName` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`formDescription` text COLLATE utf8mb4_unicode_ci,
|
||||
`formComponents` json NOT NULL,
|
||||
`formStatus` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`customCSS` text COLLATE utf8mb4_unicode_ci,
|
||||
`customScript` longtext COLLATE utf8mb4_unicode_ci,
|
||||
`formEvents` json DEFAULT NULL,
|
||||
`scriptMode` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`versionNumber` int NOT NULL,
|
||||
`changeDescription` text COLLATE utf8mb4_unicode_ci,
|
||||
`savedBy` int DEFAULT NULL,
|
||||
`savedDate` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`historyID`),
|
||||
KEY `FK_formHistory_form` (`formID`),
|
||||
KEY `FK_formHistory_savedBy` (`savedBy`),
|
||||
KEY `IDX_formHistory_uuid` (`formUUID`),
|
||||
KEY `IDX_formHistory_date` (`savedDate`),
|
||||
CONSTRAINT `FK_formHistory_form` FOREIGN KEY (`formID`) REFERENCES `form` (`formID`) ON DELETE CASCADE,
|
||||
CONSTRAINT `FK_formHistory_savedBy` FOREIGN KEY (`savedBy`) REFERENCES `user` (`userID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
33
prisma/migrations/20241201_add_process_history.sql
Normal file
33
prisma/migrations/20241201_add_process_history.sql
Normal file
@ -0,0 +1,33 @@
|
||||
-- Add process history table for version tracking
|
||||
CREATE TABLE `processHistory` (
|
||||
`historyID` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`processID` INTEGER NOT NULL,
|
||||
`processUUID` VARCHAR(36) NOT NULL,
|
||||
`processName` VARCHAR(255) NOT NULL,
|
||||
`processDescription` TEXT NULL,
|
||||
`processDefinition` JSON NOT NULL,
|
||||
`processVersion` INTEGER NOT NULL,
|
||||
`processStatus` VARCHAR(50) NOT NULL,
|
||||
`processCategory` VARCHAR(100) NULL,
|
||||
`processOwner` VARCHAR(255) NULL,
|
||||
`processPermissions` JSON NULL,
|
||||
`processPriority` VARCHAR(50) NULL,
|
||||
`processSettings` JSON NULL,
|
||||
`processVariables` JSON NULL,
|
||||
`templateCategory` VARCHAR(100) NULL,
|
||||
`versionNumber` INTEGER NOT NULL,
|
||||
`changeDescription` TEXT NULL,
|
||||
`savedBy` INTEGER NULL,
|
||||
`savedDate` DATETIME(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
|
||||
|
||||
INDEX `FK_processHistory_process`(`processID`),
|
||||
INDEX `FK_processHistory_savedBy`(`savedBy`),
|
||||
INDEX `IDX_processHistory_uuid`(`processUUID`),
|
||||
INDEX `IDX_processHistory_date`(`savedDate`),
|
||||
PRIMARY KEY (`historyID`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- Add foreign key constraints
|
||||
ALTER TABLE `processHistory` ADD CONSTRAINT `processHistory_processID_fkey` FOREIGN KEY (`processID`) REFERENCES `process`(`processID`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE `processHistory` ADD CONSTRAINT `processHistory_savedBy_fkey` FOREIGN KEY (`savedBy`) REFERENCES `user`(`userID`) ON DELETE SET NULL ON UPDATE CASCADE;
|
@ -24,7 +24,9 @@ model user {
|
||||
userCreatedDate DateTime? @db.DateTime(0)
|
||||
userModifiedDate DateTime? @db.DateTime(0)
|
||||
forms form[] @relation("FormCreator")
|
||||
formHistoryEntries formHistory[]
|
||||
processes process[] @relation("ProcessCreator")
|
||||
processHistoryEntries processHistory[]
|
||||
userrole userrole[]
|
||||
}
|
||||
|
||||
@ -65,10 +67,36 @@ model form {
|
||||
formEvents Json?
|
||||
scriptMode String? @default("safe") @db.VarChar(20)
|
||||
creator user? @relation("FormCreator", fields: [formCreatedBy], references: [userID])
|
||||
history formHistory[] @relation("FormHistoryEntries")
|
||||
|
||||
@@index([formCreatedBy], map: "FK_form_creator")
|
||||
}
|
||||
|
||||
model formHistory {
|
||||
historyID Int @id @default(autoincrement())
|
||||
formID Int
|
||||
formUUID String @db.VarChar(36)
|
||||
formName String @db.VarChar(255)
|
||||
formDescription String? @db.Text
|
||||
formComponents Json
|
||||
formStatus String @db.VarChar(50)
|
||||
customCSS String? @db.Text
|
||||
customScript String? @db.LongText
|
||||
formEvents Json?
|
||||
scriptMode String? @db.VarChar(20)
|
||||
versionNumber Int
|
||||
changeDescription String? @db.Text
|
||||
savedBy Int?
|
||||
savedDate DateTime @default(now()) @db.DateTime(0)
|
||||
form form @relation("FormHistoryEntries", fields: [formID], references: [formID], onDelete: Cascade)
|
||||
savedByUser user? @relation(fields: [savedBy], references: [userID])
|
||||
|
||||
@@index([formID], map: "FK_formHistory_form")
|
||||
@@index([savedBy], map: "FK_formHistory_savedBy")
|
||||
@@index([formUUID], map: "IDX_formHistory_uuid")
|
||||
@@index([savedDate], map: "IDX_formHistory_date")
|
||||
}
|
||||
|
||||
model process {
|
||||
processID Int @id @default(autoincrement())
|
||||
processUUID String @unique @db.VarChar(36)
|
||||
@ -90,9 +118,39 @@ model process {
|
||||
templateCategory String? @db.VarChar(100)
|
||||
processDeletedDate DateTime? @db.DateTime(0)
|
||||
creator user? @relation("ProcessCreator", fields: [processCreatedBy], references: [userID])
|
||||
history processHistory[] @relation("ProcessHistoryEntries")
|
||||
|
||||
@@index([processCreatedBy], map: "FK_process_creator")
|
||||
@@index([processStatus], map: "IDX_process_status")
|
||||
@@index([processCategory], map: "IDX_process_category")
|
||||
@@index([isTemplate], map: "IDX_process_template")
|
||||
}
|
||||
|
||||
model processHistory {
|
||||
historyID Int @id @default(autoincrement())
|
||||
processID Int
|
||||
processUUID String @db.VarChar(36)
|
||||
processName String @db.VarChar(255)
|
||||
processDescription String? @db.Text
|
||||
processDefinition Json
|
||||
processVersion Int
|
||||
processStatus String @db.VarChar(50)
|
||||
processCategory String? @db.VarChar(100)
|
||||
processOwner String? @db.VarChar(255)
|
||||
processPermissions Json?
|
||||
processPriority String? @db.VarChar(50)
|
||||
processSettings Json?
|
||||
processVariables Json?
|
||||
templateCategory String? @db.VarChar(100)
|
||||
versionNumber Int
|
||||
changeDescription String? @db.Text
|
||||
savedBy Int?
|
||||
savedDate DateTime @default(now()) @db.DateTime(0)
|
||||
process process @relation("ProcessHistoryEntries", fields: [processID], references: [processID], onDelete: Cascade)
|
||||
savedByUser user? @relation(fields: [savedBy], references: [userID])
|
||||
|
||||
@@index([processID], map: "FK_processHistory_process")
|
||||
@@index([savedBy], map: "FK_processHistory_savedBy")
|
||||
@@index([processUUID], map: "IDX_processHistory_uuid")
|
||||
@@index([savedDate], map: "IDX_processHistory_date")
|
||||
}
|
||||
|
@ -19,6 +19,59 @@ export default defineEventHandler(async (event) => {
|
||||
};
|
||||
}
|
||||
|
||||
// First, get the current form data to save to history
|
||||
let currentForm;
|
||||
try {
|
||||
currentForm = await prisma.form.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ formUUID: id },
|
||||
{ formID: !isNaN(parseInt(id)) ? parseInt(id) : -1 }
|
||||
]
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error fetching current form for history:', e);
|
||||
}
|
||||
|
||||
// If we found the current form, save it to history before updating
|
||||
if (currentForm) {
|
||||
try {
|
||||
// Get the next version number
|
||||
const lastHistory = await prisma.formHistory.findFirst({
|
||||
where: { formID: currentForm.formID },
|
||||
orderBy: { versionNumber: 'desc' }
|
||||
});
|
||||
|
||||
const nextVersionNumber = lastHistory ? lastHistory.versionNumber + 1 : 1;
|
||||
|
||||
// Save current form data to history
|
||||
await prisma.formHistory.create({
|
||||
data: {
|
||||
formID: currentForm.formID,
|
||||
formUUID: currentForm.formUUID,
|
||||
formName: currentForm.formName,
|
||||
formDescription: currentForm.formDescription,
|
||||
formComponents: currentForm.formComponents,
|
||||
formStatus: currentForm.formStatus,
|
||||
customCSS: currentForm.customCSS,
|
||||
customScript: currentForm.customScript,
|
||||
formEvents: currentForm.formEvents,
|
||||
scriptMode: currentForm.scriptMode,
|
||||
versionNumber: nextVersionNumber,
|
||||
changeDescription: body.changeDescription || null,
|
||||
savedBy: body.savedBy || currentForm.formCreatedBy,
|
||||
savedDate: currentForm.formModifiedDate || currentForm.formCreatedDate
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Saved form ${currentForm.formUUID} version ${nextVersionNumber} to history`);
|
||||
} catch (historyError) {
|
||||
console.error('Error saving form to history:', historyError);
|
||||
// Continue with update even if history save fails
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const updateData = {
|
||||
formName: body.formName,
|
||||
@ -73,7 +126,8 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
form
|
||||
form,
|
||||
versionSaved: currentForm ? true : false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error updating form ${id}:`, error);
|
||||
|
83
server/api/forms/[id]/history.get.js
Normal file
83
server/api/forms/[id]/history.get.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
// Initialize Prisma client
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Get the form ID from the route params
|
||||
const id = event.context.params.id;
|
||||
|
||||
try {
|
||||
// First, find the form to get its internal ID
|
||||
let form;
|
||||
try {
|
||||
form = await prisma.form.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ formUUID: id },
|
||||
{ formID: !isNaN(parseInt(id)) ? parseInt(id) : -1 }
|
||||
]
|
||||
},
|
||||
select: { formID: true, formUUID: true, formName: true }
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error('Form not found');
|
||||
}
|
||||
|
||||
if (!form) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Form not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Get all history entries for this form
|
||||
const history = await prisma.formHistory.findMany({
|
||||
where: { formID: form.formID },
|
||||
include: {
|
||||
savedByUser: {
|
||||
select: {
|
||||
userID: true,
|
||||
userFullName: true,
|
||||
userUsername: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { versionNumber: 'desc' }
|
||||
});
|
||||
|
||||
// Get current form info for comparison
|
||||
const currentForm = await prisma.form.findUnique({
|
||||
where: { formID: form.formID },
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
userID: true,
|
||||
userFullName: true,
|
||||
userUsername: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
form: {
|
||||
formID: form.formID,
|
||||
formUUID: form.formUUID,
|
||||
formName: form.formName
|
||||
},
|
||||
currentVersion: currentForm,
|
||||
history,
|
||||
totalVersions: history.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching form history for ${id}:`, error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to fetch form history',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
};
|
||||
}
|
||||
});
|
166
server/api/forms/[id]/restore.post.js
Normal file
166
server/api/forms/[id]/restore.post.js
Normal file
@ -0,0 +1,166 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
// Initialize Prisma client
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Get the form ID from the route params
|
||||
const id = event.context.params.id;
|
||||
|
||||
try {
|
||||
// Parse the request body
|
||||
const body = await readBody(event);
|
||||
|
||||
// Validate required fields
|
||||
if (!body.versionNumber && !body.historyID) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Either versionNumber or historyID is required'
|
||||
};
|
||||
}
|
||||
|
||||
// First, find the form to get its internal ID
|
||||
let form;
|
||||
try {
|
||||
form = await prisma.form.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ formUUID: id },
|
||||
{ formID: !isNaN(parseInt(id)) ? parseInt(id) : -1 }
|
||||
]
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error('Form not found');
|
||||
}
|
||||
|
||||
if (!form) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Form not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific version to restore
|
||||
let historyEntry;
|
||||
if (body.historyID) {
|
||||
historyEntry = await prisma.formHistory.findFirst({
|
||||
where: {
|
||||
historyID: body.historyID,
|
||||
formID: form.formID
|
||||
}
|
||||
});
|
||||
} else {
|
||||
historyEntry = await prisma.formHistory.findFirst({
|
||||
where: {
|
||||
formID: form.formID,
|
||||
versionNumber: body.versionNumber
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!historyEntry) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Version not found in history'
|
||||
};
|
||||
}
|
||||
|
||||
// Save current form to history before restoring (creating a new version)
|
||||
try {
|
||||
// Get the next version number
|
||||
const lastHistory = await prisma.formHistory.findFirst({
|
||||
where: { formID: form.formID },
|
||||
orderBy: { versionNumber: 'desc' }
|
||||
});
|
||||
|
||||
const nextVersionNumber = lastHistory ? lastHistory.versionNumber + 1 : 1;
|
||||
|
||||
// Save current state to history
|
||||
await prisma.formHistory.create({
|
||||
data: {
|
||||
formID: form.formID,
|
||||
formUUID: form.formUUID,
|
||||
formName: form.formName,
|
||||
formDescription: form.formDescription,
|
||||
formComponents: form.formComponents,
|
||||
formStatus: form.formStatus,
|
||||
customCSS: form.customCSS,
|
||||
customScript: form.customScript,
|
||||
formEvents: form.formEvents,
|
||||
scriptMode: form.scriptMode,
|
||||
versionNumber: nextVersionNumber,
|
||||
changeDescription: `Pre-restore backup before restoring to version ${historyEntry.versionNumber}`,
|
||||
savedBy: body.restoredBy || form.formCreatedBy,
|
||||
savedDate: form.formModifiedDate || form.formCreatedDate
|
||||
}
|
||||
});
|
||||
} catch (historyError) {
|
||||
console.error('Error creating pre-restore backup:', historyError);
|
||||
// Continue with restore even if backup fails
|
||||
}
|
||||
|
||||
// Restore the form to the selected version
|
||||
const restoredForm = await prisma.form.update({
|
||||
where: { formID: form.formID },
|
||||
data: {
|
||||
formName: historyEntry.formName,
|
||||
formDescription: historyEntry.formDescription,
|
||||
formComponents: historyEntry.formComponents,
|
||||
formStatus: historyEntry.formStatus,
|
||||
customCSS: historyEntry.customCSS,
|
||||
customScript: historyEntry.customScript,
|
||||
formEvents: historyEntry.formEvents,
|
||||
scriptMode: historyEntry.scriptMode,
|
||||
formModifiedDate: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Create a history entry for the restore action
|
||||
try {
|
||||
const lastHistoryAfterRestore = await prisma.formHistory.findFirst({
|
||||
where: { formID: form.formID },
|
||||
orderBy: { versionNumber: 'desc' }
|
||||
});
|
||||
|
||||
const nextVersionAfterRestore = lastHistoryAfterRestore ? lastHistoryAfterRestore.versionNumber + 1 : 1;
|
||||
|
||||
await prisma.formHistory.create({
|
||||
data: {
|
||||
formID: form.formID,
|
||||
formUUID: form.formUUID,
|
||||
formName: historyEntry.formName,
|
||||
formDescription: historyEntry.formDescription,
|
||||
formComponents: historyEntry.formComponents,
|
||||
formStatus: historyEntry.formStatus,
|
||||
customCSS: historyEntry.customCSS,
|
||||
customScript: historyEntry.customScript,
|
||||
formEvents: historyEntry.formEvents,
|
||||
scriptMode: historyEntry.scriptMode,
|
||||
versionNumber: nextVersionAfterRestore,
|
||||
changeDescription: `Restored from version ${historyEntry.versionNumber}`,
|
||||
savedBy: body.restoredBy || form.formCreatedBy,
|
||||
savedDate: new Date()
|
||||
}
|
||||
});
|
||||
} catch (restoreHistoryError) {
|
||||
console.error('Error creating restore history entry:', restoreHistoryError);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
form: restoredForm,
|
||||
restoredFromVersion: historyEntry.versionNumber,
|
||||
restoredFromDate: historyEntry.savedDate,
|
||||
message: `Form successfully restored to version ${historyEntry.versionNumber}`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error restoring form ${id}:`, error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to restore form',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
};
|
||||
}
|
||||
});
|
130
server/api/forms/[id]/version/[versionId].get.js
Normal file
130
server/api/forms/[id]/version/[versionId].get.js
Normal file
@ -0,0 +1,130 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
// Initialize Prisma client
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Get the form ID and version ID from the route params
|
||||
const { id: formId, versionId } = event.context.params;
|
||||
|
||||
try {
|
||||
// First, find the form to get its internal ID
|
||||
let form;
|
||||
try {
|
||||
form = await prisma.form.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ formUUID: formId },
|
||||
{ formID: !isNaN(parseInt(formId)) ? parseInt(formId) : -1 }
|
||||
]
|
||||
},
|
||||
select: { formID: true, formUUID: true, formName: true }
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error('Form not found');
|
||||
}
|
||||
|
||||
if (!form) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Form not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific version - can be either historyID or versionNumber
|
||||
let historyEntry;
|
||||
|
||||
if (!isNaN(parseInt(versionId))) {
|
||||
// Try as historyID first
|
||||
historyEntry = await prisma.formHistory.findFirst({
|
||||
where: {
|
||||
historyID: parseInt(versionId),
|
||||
formID: form.formID
|
||||
},
|
||||
include: {
|
||||
savedByUser: {
|
||||
select: {
|
||||
userID: true,
|
||||
userFullName: true,
|
||||
userUsername: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If not found, try as versionNumber
|
||||
if (!historyEntry) {
|
||||
historyEntry = await prisma.formHistory.findFirst({
|
||||
where: {
|
||||
formID: form.formID,
|
||||
versionNumber: parseInt(versionId)
|
||||
},
|
||||
include: {
|
||||
savedByUser: {
|
||||
select: {
|
||||
userID: true,
|
||||
userFullName: true,
|
||||
userUsername: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!historyEntry) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Version not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Get adjacent versions for navigation
|
||||
const previousVersion = await prisma.formHistory.findFirst({
|
||||
where: {
|
||||
formID: form.formID,
|
||||
versionNumber: { lt: historyEntry.versionNumber }
|
||||
},
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
select: { historyID: true, versionNumber: true, savedDate: true }
|
||||
});
|
||||
|
||||
const nextVersion = await prisma.formHistory.findFirst({
|
||||
where: {
|
||||
formID: form.formID,
|
||||
versionNumber: { gt: historyEntry.versionNumber }
|
||||
},
|
||||
orderBy: { versionNumber: 'asc' },
|
||||
select: { historyID: true, versionNumber: true, savedDate: true }
|
||||
});
|
||||
|
||||
// Get total version count
|
||||
const totalVersions = await prisma.formHistory.count({
|
||||
where: { formID: form.formID }
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
version: historyEntry,
|
||||
navigation: {
|
||||
previous: previousVersion,
|
||||
next: nextVersion,
|
||||
totalVersions,
|
||||
currentPosition: historyEntry.versionNumber
|
||||
},
|
||||
formInfo: {
|
||||
formID: form.formID,
|
||||
formUUID: form.formUUID,
|
||||
formName: form.formName
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching form version ${formId}/${versionId}:`, error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to fetch form version',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
};
|
||||
}
|
||||
});
|
@ -19,94 +19,106 @@ export default defineEventHandler(async (event) => {
|
||||
// Parse the request body
|
||||
const body = await readBody(event);
|
||||
|
||||
// Validate required fields
|
||||
if (!body.processName) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Process name is required'
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the ID is a UUID or numeric ID
|
||||
const isUUID = processId.length === 36 && processId.includes('-');
|
||||
|
||||
// Build update data
|
||||
const updateData = {};
|
||||
|
||||
// Basic fields
|
||||
if (body.processName !== undefined) updateData.processName = body.processName;
|
||||
if (body.processDescription !== undefined) updateData.processDescription = body.processDescription;
|
||||
if (body.processCategory !== undefined) updateData.processCategory = body.processCategory;
|
||||
if (body.processPriority !== undefined) updateData.processPriority = body.processPriority;
|
||||
if (body.processOwner !== undefined) updateData.processOwner = body.processOwner;
|
||||
if (body.isTemplate !== undefined) updateData.isTemplate = body.isTemplate;
|
||||
if (body.templateCategory !== undefined) updateData.templateCategory = body.templateCategory;
|
||||
|
||||
// Get current process to check status
|
||||
const currentProcess = await prisma.process.findFirst({
|
||||
// Find the existing process first to save its current state to history
|
||||
const existingProcess = await prisma.process.findFirst({
|
||||
where: isUUID
|
||||
? { processUUID: processId }
|
||||
: { processID: parseInt(processId) },
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
processStatus: true,
|
||||
processVersion: true
|
||||
userID: true,
|
||||
userFullName: true,
|
||||
userUsername: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentProcess) {
|
||||
if (!existingProcess) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Process not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Handle status changes with validation
|
||||
if (body.processStatus !== undefined) {
|
||||
const currentStatus = currentProcess.processStatus;
|
||||
const newStatus = body.processStatus;
|
||||
// Get the next version number for history
|
||||
const lastHistory = await prisma.processHistory.findFirst({
|
||||
where: { processID: existingProcess.processID },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
select: { versionNumber: true }
|
||||
});
|
||||
|
||||
// Validate status transitions
|
||||
if (currentStatus === 'published' && newStatus === 'draft') {
|
||||
// Allow unpublishing only if explicitly requested
|
||||
if (body.allowUnpublish !== true) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cannot change published process to draft without explicit confirmation. Use allowUnpublish: true.'
|
||||
};
|
||||
}
|
||||
}
|
||||
const nextVersionNumber = (lastHistory?.versionNumber || 0) + 1;
|
||||
|
||||
updateData.processStatus = newStatus;
|
||||
// Save current state to history before updating
|
||||
await prisma.processHistory.create({
|
||||
data: {
|
||||
processID: existingProcess.processID,
|
||||
processUUID: existingProcess.processUUID,
|
||||
processName: existingProcess.processName,
|
||||
processDescription: existingProcess.processDescription,
|
||||
processDefinition: existingProcess.processDefinition,
|
||||
processVersion: existingProcess.processVersion,
|
||||
processStatus: existingProcess.processStatus,
|
||||
processCategory: existingProcess.processCategory,
|
||||
processOwner: existingProcess.processOwner,
|
||||
processPermissions: existingProcess.processPermissions,
|
||||
processPriority: existingProcess.processPriority,
|
||||
processSettings: existingProcess.processSettings,
|
||||
processVariables: existingProcess.processVariables,
|
||||
templateCategory: existingProcess.templateCategory,
|
||||
versionNumber: nextVersionNumber,
|
||||
changeDescription: body.changeDescription || null,
|
||||
savedBy: body.savedBy || existingProcess.processCreatedBy
|
||||
}
|
||||
// If no status provided, preserve current status (don't change it)
|
||||
});
|
||||
|
||||
// Process definition (nodes, edges, viewport)
|
||||
if (body.nodes !== undefined || body.edges !== undefined || body.viewport !== undefined) {
|
||||
updateData.processDefinition = {
|
||||
// Prepare process definition
|
||||
const processDefinition = {
|
||||
nodes: body.nodes || [],
|
||||
edges: body.edges || [],
|
||||
viewport: body.viewport || { x: 0, y: 0, zoom: 1 }
|
||||
};
|
||||
}
|
||||
|
||||
// Process variables
|
||||
if (body.variables !== undefined) {
|
||||
updateData.processVariables = Object.keys(body.variables).length > 0 ? body.variables : null;
|
||||
}
|
||||
// Prepare process variables (if any)
|
||||
const processVariables = body.variables || {};
|
||||
|
||||
// Process settings
|
||||
if (body.settings !== undefined) {
|
||||
updateData.processSettings = Object.keys(body.settings).length > 0 ? body.settings : null;
|
||||
}
|
||||
// Prepare process settings (if any)
|
||||
const processSettings = body.settings || {};
|
||||
|
||||
// Process permissions
|
||||
if (body.permissions !== undefined) {
|
||||
updateData.processPermissions = Object.keys(body.permissions).length > 0 ? body.permissions : null;
|
||||
}
|
||||
|
||||
// Version increment if major changes
|
||||
if (body.incrementVersion === true) {
|
||||
updateData.processVersion = currentProcess.processVersion + 1;
|
||||
}
|
||||
// Prepare process permissions (if any)
|
||||
const processPermissions = body.permissions || {};
|
||||
|
||||
// Update the process
|
||||
const updatedProcess = await prisma.process.update({
|
||||
where: isUUID
|
||||
? { processUUID: processId }
|
||||
: { processID: parseInt(processId) },
|
||||
data: updateData,
|
||||
data: {
|
||||
processName: body.processName,
|
||||
processDescription: body.processDescription || null,
|
||||
processCategory: body.processCategory || null,
|
||||
processPriority: body.processPriority || 'normal',
|
||||
processOwner: body.processOwner || null,
|
||||
processDefinition: processDefinition,
|
||||
processVariables: Object.keys(processVariables).length > 0 ? processVariables : null,
|
||||
processSettings: Object.keys(processSettings).length > 0 ? processSettings : null,
|
||||
processPermissions: Object.keys(processPermissions).length > 0 ? processPermissions : null,
|
||||
processStatus: body.processStatus || existingProcess.processStatus,
|
||||
processVersion: existingProcess.processVersion + 1
|
||||
},
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
@ -120,19 +132,12 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
process: updatedProcess
|
||||
process: updatedProcess,
|
||||
message: 'Process updated successfully and previous version saved to history'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error updating process:', error);
|
||||
|
||||
// Handle specific Prisma errors
|
||||
if (error.code === 'P2025') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Process not found'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to update process',
|
||||
|
83
server/api/process/[id]/history.get.js
Normal file
83
server/api/process/[id]/history.get.js
Normal file
@ -0,0 +1,83 @@
|
||||
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('-');
|
||||
|
||||
// Find the current process
|
||||
const currentProcess = await prisma.process.findFirst({
|
||||
where: isUUID
|
||||
? { processUUID: processId }
|
||||
: { processID: parseInt(processId) },
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
userID: true,
|
||||
userFullName: true,
|
||||
userUsername: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentProcess) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Process not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Get process history ordered by version number (newest first)
|
||||
const history = await prisma.processHistory.findMany({
|
||||
where: { processID: currentProcess.processID },
|
||||
include: {
|
||||
savedByUser: {
|
||||
select: {
|
||||
userID: true,
|
||||
userFullName: true,
|
||||
userUsername: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { versionNumber: 'desc' }
|
||||
});
|
||||
|
||||
// Get total count of versions
|
||||
const totalVersions = history.length + 1; // +1 for current version
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
currentVersion: currentProcess,
|
||||
history: history,
|
||||
form: {
|
||||
processName: currentProcess.processName,
|
||||
processDescription: currentProcess.processDescription
|
||||
},
|
||||
totalVersions: totalVersions
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting process history:', error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to get process history',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
};
|
||||
}
|
||||
});
|
@ -15,40 +15,98 @@ export default defineEventHandler(async (event) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the ID is a UUID or numeric ID
|
||||
const isUUID = processId.length === 36 && processId.includes('-');
|
||||
// Parse the request body
|
||||
const body = await readBody(event);
|
||||
const { historyId, versionNumber, restoredBy } = body;
|
||||
|
||||
// First, check if the process exists and is deleted
|
||||
const existingProcess = await prisma.process.findFirst({
|
||||
where: {
|
||||
...(isUUID
|
||||
? { processUUID: processId }
|
||||
: { processID: parseInt(processId) }),
|
||||
processStatus: 'deleted'
|
||||
},
|
||||
select: {
|
||||
processID: true,
|
||||
processName: true,
|
||||
processStatus: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingProcess) {
|
||||
if (!historyId && !versionNumber) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Deleted process not found'
|
||||
error: 'History ID or version number is required'
|
||||
};
|
||||
}
|
||||
|
||||
// Restore the process by setting status back to draft
|
||||
const restoredProcess = await prisma.process.update({
|
||||
// Check if the ID is a UUID or numeric ID
|
||||
const isUUID = processId.length === 36 && processId.includes('-');
|
||||
|
||||
// Find the current process
|
||||
const currentProcess = await prisma.process.findFirst({
|
||||
where: isUUID
|
||||
? { processUUID: processId }
|
||||
: { processID: parseInt(processId) },
|
||||
: { processID: parseInt(processId) }
|
||||
});
|
||||
|
||||
if (!currentProcess) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Process not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Find the history record to restore
|
||||
const historyRecord = await prisma.processHistory.findFirst({
|
||||
where: historyId
|
||||
? { historyID: parseInt(historyId) }
|
||||
: {
|
||||
processID: currentProcess.processID,
|
||||
versionNumber: parseInt(versionNumber)
|
||||
}
|
||||
});
|
||||
|
||||
if (!historyRecord) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'History record not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Save current state to history before restoring
|
||||
const lastHistory = await prisma.processHistory.findFirst({
|
||||
where: { processID: currentProcess.processID },
|
||||
orderBy: { versionNumber: 'desc' },
|
||||
select: { versionNumber: true }
|
||||
});
|
||||
|
||||
const nextVersionNumber = (lastHistory?.versionNumber || 0) + 1;
|
||||
|
||||
await prisma.processHistory.create({
|
||||
data: {
|
||||
processStatus: 'draft', // Restore as draft for safety
|
||||
processDeletedDate: null, // Clear deletion date
|
||||
processModifiedDate: new Date()
|
||||
processID: currentProcess.processID,
|
||||
processUUID: currentProcess.processUUID,
|
||||
processName: currentProcess.processName,
|
||||
processDescription: currentProcess.processDescription,
|
||||
processDefinition: currentProcess.processDefinition,
|
||||
processVersion: currentProcess.processVersion,
|
||||
processStatus: currentProcess.processStatus,
|
||||
processCategory: currentProcess.processCategory,
|
||||
processOwner: currentProcess.processOwner,
|
||||
processPermissions: currentProcess.processPermissions,
|
||||
processPriority: currentProcess.processPriority,
|
||||
processSettings: currentProcess.processSettings,
|
||||
processVariables: currentProcess.processVariables,
|
||||
templateCategory: currentProcess.templateCategory,
|
||||
versionNumber: nextVersionNumber,
|
||||
changeDescription: `Restored to version ${historyRecord.versionNumber}`,
|
||||
savedBy: restoredBy || currentProcess.processCreatedBy
|
||||
}
|
||||
});
|
||||
|
||||
// Restore the process from history
|
||||
const restoredProcess = await prisma.process.update({
|
||||
where: { processID: currentProcess.processID },
|
||||
data: {
|
||||
processName: historyRecord.processName,
|
||||
processDescription: historyRecord.processDescription,
|
||||
processDefinition: historyRecord.processDefinition,
|
||||
processStatus: historyRecord.processStatus,
|
||||
processCategory: historyRecord.processCategory,
|
||||
processOwner: historyRecord.processOwner,
|
||||
processPermissions: historyRecord.processPermissions,
|
||||
processPriority: historyRecord.processPriority,
|
||||
processSettings: historyRecord.processSettings,
|
||||
processVariables: historyRecord.processVariables,
|
||||
templateCategory: historyRecord.templateCategory,
|
||||
processVersion: currentProcess.processVersion + 1
|
||||
},
|
||||
include: {
|
||||
creator: {
|
||||
@ -63,20 +121,12 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Process "${existingProcess.processName}" has been restored`,
|
||||
process: restoredProcess
|
||||
process: restoredProcess,
|
||||
message: `Process restored to version ${historyRecord.versionNumber} successfully`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error restoring process:', error);
|
||||
|
||||
// Handle specific Prisma errors
|
||||
if (error.code === 'P2025') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Process not found'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to restore process',
|
||||
|
94
server/api/process/[id]/version/[versionId].get.js
Normal file
94
server/api/process/[id]/version/[versionId].get.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
// Initialize Prisma client
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Get the process ID and version ID from the route parameters
|
||||
const processId = getRouterParam(event, 'id');
|
||||
const versionId = getRouterParam(event, 'versionId');
|
||||
|
||||
if (!processId || !versionId) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Process ID and version ID are required'
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the ID is a UUID or numeric ID
|
||||
const isUUID = processId.length === 36 && processId.includes('-');
|
||||
|
||||
// Find the current process
|
||||
const currentProcess = await prisma.process.findFirst({
|
||||
where: isUUID
|
||||
? { processUUID: processId }
|
||||
: { processID: parseInt(processId) }
|
||||
});
|
||||
|
||||
if (!currentProcess) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Process not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Find the specific version
|
||||
const versionRecord = await prisma.processHistory.findFirst({
|
||||
where: {
|
||||
processID: currentProcess.processID,
|
||||
historyID: parseInt(versionId)
|
||||
},
|
||||
include: {
|
||||
savedByUser: {
|
||||
select: {
|
||||
userID: true,
|
||||
userFullName: true,
|
||||
userUsername: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!versionRecord) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Version not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Format the version data for preview
|
||||
const versionData = {
|
||||
processName: versionRecord.processName,
|
||||
processDescription: versionRecord.processDescription,
|
||||
processDefinition: versionRecord.processDefinition,
|
||||
processStatus: versionRecord.processStatus,
|
||||
processCategory: versionRecord.processCategory,
|
||||
processOwner: versionRecord.processOwner,
|
||||
processPermissions: versionRecord.processPermissions,
|
||||
processPriority: versionRecord.processPriority,
|
||||
processSettings: versionRecord.processSettings,
|
||||
processVariables: versionRecord.processVariables,
|
||||
templateCategory: versionRecord.templateCategory,
|
||||
versionInfo: {
|
||||
versionNumber: versionRecord.versionNumber,
|
||||
savedDate: versionRecord.savedDate,
|
||||
savedBy: versionRecord.savedByUser,
|
||||
changeDescription: versionRecord.changeDescription
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: versionData
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting process version:', error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to get process version',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
};
|
||||
}
|
||||
});
|
@ -28,6 +28,9 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
|
||||
// Form preview data
|
||||
previewFormData: {},
|
||||
|
||||
// Form history tracking
|
||||
lastChangeDescription: null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
@ -651,7 +654,10 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
customScript: this.formCustomScript,
|
||||
customCSS: this.formCustomCSS,
|
||||
formEvents: this.formEvents,
|
||||
scriptMode: this.scriptMode
|
||||
scriptMode: this.scriptMode,
|
||||
// Add user info and change description for history tracking
|
||||
savedBy: 1, // TODO: Get from authenticated user
|
||||
changeDescription: this.lastChangeDescription || null
|
||||
};
|
||||
|
||||
// Determine if this is a new form or an update
|
||||
@ -684,12 +690,14 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
// Update store state with the saved form
|
||||
this.currentFormId = result.form.formUUID;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.lastChangeDescription = null; // Reset after save
|
||||
|
||||
// Record in history
|
||||
this.recordHistory('save_form', {
|
||||
formName: this.formName,
|
||||
formDescription: this.formDescription,
|
||||
componentCount: this.formComponents.length
|
||||
componentCount: this.formComponents.length,
|
||||
versionSaved: result.versionSaved
|
||||
});
|
||||
|
||||
return result.form;
|
||||
@ -931,6 +939,115 @@ export const useFormBuilderStore = defineStore('formBuilder', {
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Get form history/versions
|
||||
async getFormHistory(formId = null) {
|
||||
try {
|
||||
const id = formId || this.currentFormId;
|
||||
if (!id) {
|
||||
throw new Error('No form ID provided');
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/forms/${id}/history`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
return result;
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to fetch form history');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching form history:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Get specific form version details
|
||||
async getFormVersion(formId, versionId) {
|
||||
try {
|
||||
const response = await fetch(`/api/forms/${formId}/version/${versionId}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
return result;
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to fetch form version');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching form version:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Restore form to a specific version
|
||||
async restoreFormVersion(formId, versionData, changeDescription = null) {
|
||||
try {
|
||||
const response = await fetch(`/api/forms/${formId}/restore`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
versionNumber: versionData.versionNumber,
|
||||
historyID: versionData.historyID,
|
||||
restoredBy: 1, // TODO: Get from authenticated user
|
||||
changeDescription: changeDescription || `Restored to version ${versionData.versionNumber}`
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Reload the form to reflect the restored state
|
||||
await this.loadForm(formId);
|
||||
|
||||
return result;
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to restore form version');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error restoring form version:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Load form version for preview (without changing current form)
|
||||
async loadFormVersionPreview(formId, versionId) {
|
||||
try {
|
||||
const versionResult = await this.getFormVersion(formId, versionId);
|
||||
|
||||
if (versionResult.success) {
|
||||
const version = versionResult.version;
|
||||
|
||||
// Return the version data in a format compatible with form preview
|
||||
return {
|
||||
formName: version.formName,
|
||||
formDescription: version.formDescription,
|
||||
formComponents: version.formComponents,
|
||||
customScript: version.customScript,
|
||||
customCSS: version.customCSS,
|
||||
formEvents: version.formEvents,
|
||||
scriptMode: version.scriptMode,
|
||||
versionInfo: {
|
||||
versionNumber: version.versionNumber,
|
||||
savedDate: version.savedDate,
|
||||
savedBy: version.savedByUser,
|
||||
changeDescription: version.changeDescription
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw new Error('Failed to load version preview');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading form version preview:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Set change description for next save
|
||||
setChangeDescription(description) {
|
||||
this.lastChangeDescription = description;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -10,7 +10,8 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
selectedEdgeId: null,
|
||||
history: [],
|
||||
historyIndex: -1,
|
||||
unsavedChanges: false
|
||||
unsavedChanges: false,
|
||||
lastChangeDescription: ''
|
||||
}),
|
||||
|
||||
getters: {
|
||||
@ -737,6 +738,148 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
||||
});
|
||||
}
|
||||
this.historyIndex = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get process version history
|
||||
*/
|
||||
async getProcessHistory(processId) {
|
||||
try {
|
||||
const response = await $fetch(`/api/process/${processId}/history`);
|
||||
|
||||
if (response.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to get process history');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting process history:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get specific process version details
|
||||
*/
|
||||
async getProcessVersion(processId, versionId) {
|
||||
try {
|
||||
const response = await $fetch(`/api/process/${processId}/version/${versionId}`);
|
||||
|
||||
if (response.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to get process version');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting process version:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore process to a previous version
|
||||
*/
|
||||
async restoreProcessVersion(processId, version) {
|
||||
try {
|
||||
const requestData = {
|
||||
historyId: version.historyID,
|
||||
versionNumber: version.versionNumber,
|
||||
restoredBy: 1 // TODO: Get from auth store
|
||||
};
|
||||
|
||||
const response = await $fetch(`/api/process/${processId}/restore`, {
|
||||
method: 'POST',
|
||||
body: requestData
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Update local state with restored process
|
||||
if (this.currentProcess && this.currentProcess.id === processId) {
|
||||
await this.loadProcess(processId);
|
||||
}
|
||||
|
||||
return response;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to restore process version');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error restoring process version:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load process version for preview (without changing current process)
|
||||
*/
|
||||
async loadProcessVersionPreview(processId, versionId) {
|
||||
try {
|
||||
const response = await $fetch(`/api/process/${processId}/version/${versionId}`);
|
||||
|
||||
if (response.success) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to load process version preview');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading process version preview:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set change description for next save
|
||||
*/
|
||||
setChangeDescription(description) {
|
||||
this.lastChangeDescription = description;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enhanced save process with change tracking
|
||||
*/
|
||||
async saveProcessWithDescription(changeDescription = '') {
|
||||
if (!this.currentProcess) return false;
|
||||
|
||||
try {
|
||||
const processData = {
|
||||
processName: this.currentProcess.name,
|
||||
processDescription: this.currentProcess.description,
|
||||
nodes: this.currentProcess.nodes,
|
||||
edges: this.currentProcess.edges,
|
||||
viewport: this.currentProcess.viewport || { x: 0, y: 0, zoom: 1 },
|
||||
variables: useVariableStore().getAllVariables.process || {},
|
||||
settings: this.currentProcess.settings || {},
|
||||
permissions: this.currentProcess.permissions || {},
|
||||
changeDescription: changeDescription || this.lastChangeDescription,
|
||||
savedBy: 1 // TODO: Get from auth store
|
||||
};
|
||||
|
||||
const response = await $fetch(`/api/process/${this.currentProcess.id}`, {
|
||||
method: 'PUT',
|
||||
body: processData
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Update local state with server response
|
||||
const apiProcess = response.process;
|
||||
this.currentProcess.updatedAt = apiProcess.processModifiedDate;
|
||||
this.currentProcess.version = apiProcess.processVersion;
|
||||
|
||||
// Update in processes array if it exists there
|
||||
const index = this.processes.findIndex(p => p.id === this.currentProcess.id);
|
||||
if (index !== -1) {
|
||||
this.processes[index] = { ...this.currentProcess };
|
||||
}
|
||||
|
||||
this.unsavedChanges = false;
|
||||
this.lastChangeDescription = '';
|
||||
return response;
|
||||
} else {
|
||||
throw new Error(response.error || 'Failed to save process');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving process:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user