- Updated the modal to use a smaller size for improved usability. - Removed unnecessary fields related to priority, owner, and execution settings to streamline the process configuration. - Introduced a new loading state for permissions and error handling for better user feedback. - Simplified the settings tabs to focus on essential configurations, enhancing the user experience. - Added functionality to dynamically load available roles and permissions from the database, improving flexibility in process management.
579 lines
20 KiB
Vue
579 lines
20 KiB
Vue
<template>
|
|
<RsModal v-model="showModal" title="Process Settings" size="lg" position="center">
|
|
<div>
|
|
<RsTab :tabs="settingsTabs" v-model="activeTab">
|
|
<!-- Process Info Tab -->
|
|
<template #info>
|
|
<div class="p-4 space-y-4">
|
|
<FormKit
|
|
type="text"
|
|
label="Process Name"
|
|
v-model="localProcess.name"
|
|
help="Name of your process"
|
|
validation="required"
|
|
/>
|
|
|
|
<FormKit
|
|
type="textarea"
|
|
label="Process Description"
|
|
v-model="localProcess.description"
|
|
help="Brief description of what this process does"
|
|
rows="3"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="Process Category"
|
|
v-model="localProcess.category"
|
|
help="Category or department this process belongs to"
|
|
placeholder="e.g., HR, Finance, Operations"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Basic Settings Tab -->
|
|
<template #basic>
|
|
<div class="p-4 space-y-4">
|
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
|
<div class="flex items-center mb-2">
|
|
<Icon name="material-symbols:info" class="w-5 h-5 text-blue-600 mr-2" />
|
|
<h4 class="font-medium text-blue-900">Basic Settings</h4>
|
|
</div>
|
|
<p class="text-sm text-blue-700">
|
|
Configure the essential behavior of your process.
|
|
</p>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Process Type"
|
|
v-model="localProcess.processType"
|
|
:options="[
|
|
{ label: 'Standard Process', value: 'standard' },
|
|
{ label: 'Approval Workflow', value: 'approval' },
|
|
{ label: 'Data Collection', value: 'data_collection' },
|
|
{ label: 'Automated Task', value: 'automation' }
|
|
]"
|
|
help="Type of process workflow"
|
|
/>
|
|
|
|
<FormKit
|
|
type="checkbox"
|
|
label="Allow Parallel Execution"
|
|
v-model="localProcess.allowParallel"
|
|
help="Allow multiple instances of this process to run simultaneously"
|
|
/>
|
|
|
|
<FormKit
|
|
type="checkbox"
|
|
label="Send Completion Notifications"
|
|
v-model="localProcess.sendNotifications"
|
|
help="Send notifications when process completes"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Permissions Tab -->
|
|
<template #permissions>
|
|
<div class="p-4 space-y-4">
|
|
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
|
|
<div class="flex items-center mb-2">
|
|
<Icon name="material-symbols:security" class="w-5 h-5 text-green-600 mr-2" />
|
|
<h4 class="font-medium text-green-900">Access Control</h4>
|
|
</div>
|
|
<p class="text-sm text-green-700">
|
|
Define who can execute and modify this process.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="loadingPermissions" class="flex justify-center items-center py-8">
|
|
<div class="text-center">
|
|
<Icon name="material-symbols:progress-activity" class="w-6 h-6 animate-spin text-green-500 mx-auto mb-2" />
|
|
<p class="text-gray-500">Loading permissions...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="permissionsError" class="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<Icon name="material-symbols:error-outline" class="w-5 h-5 text-red-500 mr-2" />
|
|
<p class="text-sm text-red-700">{{ permissionsError }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Permissions Form -->
|
|
<div v-else class="space-y-4">
|
|
<FormKit
|
|
type="select"
|
|
label="Execution Permission"
|
|
v-model="localProcess.executionPermission"
|
|
:options="executionPermissionOptions"
|
|
help="Who can start and execute this process"
|
|
/>
|
|
|
|
<!-- Role-based Assignment -->
|
|
<div v-if="localProcess.executionPermission === 'roles'" class="bg-blue-50 p-4 rounded-md border border-blue-200">
|
|
<div class="flex items-center mb-3">
|
|
<Icon name="material-symbols:group" class="text-blue-600 mr-2" />
|
|
<h5 class="text-sm font-medium text-blue-900">Select Allowed Roles</h5>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<!-- Role Dropdown -->
|
|
<div class="relative">
|
|
<FormKit
|
|
type="select"
|
|
v-model="selectedRoleId"
|
|
:options="filteredAvailableRoles"
|
|
placeholder="Select a role to add..."
|
|
:classes="{ outer: 'mb-0' }"
|
|
/>
|
|
<p class="mt-1 text-xs text-blue-700">Select roles that will be able to execute this process</p>
|
|
</div>
|
|
|
|
<!-- Selected Roles Pills -->
|
|
<div v-if="localProcess.allowedRoles && localProcess.allowedRoles.length > 0" class="mt-3">
|
|
<label class="block text-sm font-medium text-blue-700 mb-2">Selected Roles</label>
|
|
<div class="flex flex-wrap gap-2 p-2 bg-white border border-blue-100 rounded-md min-h-[40px]">
|
|
<div v-for="(role, index) in localProcess.allowedRoles" :key="'role-' + role.value"
|
|
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800 border border-blue-200">
|
|
<span class="mr-1">{{ role.label }}</span>
|
|
<button @click="removeAllowedRole(index)" class="text-blue-600 hover:text-blue-800">
|
|
<Icon name="material-symbols:close" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Modification Permission"
|
|
v-model="localProcess.modificationPermission"
|
|
:options="modificationPermissionOptions"
|
|
help="Who can modify this process"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- JSON Export Tab -->
|
|
<template #json>
|
|
<div class="p-4">
|
|
<div class="mb-4">
|
|
<h3 class="text-lg font-medium mb-2">Process Configuration</h3>
|
|
<p class="text-sm text-gray-600 mb-4">
|
|
This section displays the complete process configuration as JSON for developers and system integration.
|
|
</p>
|
|
|
|
<!-- Process metadata -->
|
|
<div class="bg-gray-50 p-3 rounded border mb-4 text-sm">
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<span class="font-medium">Node Count:</span> {{ nodeCount }}
|
|
</div>
|
|
<div>
|
|
<span class="font-medium">Edge Count:</span> {{ edgeCount }}
|
|
</div>
|
|
<div>
|
|
<span class="font-medium">Process ID:</span> {{ localProcess.id || 'Not saved yet' }}
|
|
</div>
|
|
<div>
|
|
<span class="font-medium">Variable Count:</span> {{ variableCount }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action buttons -->
|
|
<div class="flex gap-2 mb-4">
|
|
<RsButton @click="copyToClipboard" variant="secondary" size="sm">
|
|
<Icon name="material-symbols:content-copy" class="mr-1" />
|
|
Copy JSON
|
|
</RsButton>
|
|
<RsButton @click="downloadJson" variant="secondary" size="sm">
|
|
<Icon name="material-symbols:download" class="mr-1" />
|
|
Download
|
|
</RsButton>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JSON Display -->
|
|
<div class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-auto" style="max-height: 400px;">
|
|
<pre class="text-sm">{{ formattedJson }}</pre>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</RsTab>
|
|
</div>
|
|
|
|
<!-- Footer Actions -->
|
|
<template #footer>
|
|
<div class="flex justify-end gap-2">
|
|
<RsButton @click="closeModal" variant="tertiary">
|
|
Cancel
|
|
</RsButton>
|
|
<RsButton @click="saveSettings" variant="primary">
|
|
Save Settings
|
|
</RsButton>
|
|
</div>
|
|
</template>
|
|
</RsModal>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
|
import { useProcessBuilderStore } from '~/stores/processBuilder'
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const processStore = useProcessBuilderStore()
|
|
|
|
// Modal visibility
|
|
const showModal = computed({
|
|
get: () => props.modelValue,
|
|
set: (value) => emit('update:modelValue', value)
|
|
})
|
|
|
|
// Settings tabs configuration - simplified to only essential tabs
|
|
const settingsTabs = [
|
|
{ key: 'info', label: 'Process Info', icon: 'material-symbols:info-outline' },
|
|
{ key: 'basic', label: 'Basic Settings', icon: 'material-symbols:settings' },
|
|
{ key: 'permissions', label: 'Permissions', icon: 'material-symbols:security' },
|
|
{ key: 'json', label: 'Source', icon: 'material-symbols:code' }
|
|
]
|
|
|
|
const activeTab = ref('info')
|
|
|
|
// Available roles and permissions from database
|
|
const availableRoles = ref([])
|
|
const availablePermissions = ref([])
|
|
const loadingPermissions = ref(false)
|
|
const permissionsError = ref(null)
|
|
|
|
// Role selection for pill design
|
|
const selectedRoleId = ref('')
|
|
const filteredAvailableRoles = ref([])
|
|
|
|
// Local process data - simplified to only essential settings
|
|
const localProcess = ref({
|
|
name: '',
|
|
description: '',
|
|
category: '',
|
|
processType: 'standard',
|
|
allowParallel: false,
|
|
sendNotifications: true,
|
|
executionPermission: 'authenticated',
|
|
allowedRoles: [],
|
|
modificationPermission: 'managers'
|
|
})
|
|
|
|
// Load permissions and roles from database
|
|
const loadPermissionsFromDatabase = async () => {
|
|
try {
|
|
loadingPermissions.value = true
|
|
permissionsError.value = null
|
|
|
|
// Load available roles
|
|
const rolesResponse = await $fetch('/api/roles')
|
|
if (rolesResponse.success) {
|
|
availableRoles.value = rolesResponse.roles || []
|
|
// Update filtered roles (exclude already selected ones)
|
|
updateFilteredRoles()
|
|
} else {
|
|
console.error('Failed to load roles:', rolesResponse.message)
|
|
permissionsError.value = 'Failed to load roles from database'
|
|
}
|
|
|
|
// Load available permissions
|
|
const permissionsResponse = await $fetch('/api/permissions')
|
|
if (permissionsResponse.success) {
|
|
availablePermissions.value = permissionsResponse.data?.permissions || permissionsResponse.permissions || []
|
|
} else {
|
|
console.error('Failed to load permissions:', permissionsResponse.message)
|
|
permissionsError.value = 'Failed to load permissions from database'
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading permissions:', error)
|
|
permissionsError.value = 'Failed to load permissions and roles'
|
|
} finally {
|
|
loadingPermissions.value = false
|
|
}
|
|
}
|
|
|
|
// Update filtered roles (exclude already selected ones)
|
|
const updateFilteredRoles = () => {
|
|
const selectedRoleIds = localProcess.value.allowedRoles?.map(role => role.value) || []
|
|
filteredAvailableRoles.value = availableRoles.value
|
|
.filter(role => !selectedRoleIds.includes(role.roleID))
|
|
.map(role => ({
|
|
label: role.roleName,
|
|
value: role.roleID
|
|
}))
|
|
}
|
|
|
|
// Watch for changes to selectedRoleId to handle role selection
|
|
watch(selectedRoleId, (newRoleId) => {
|
|
if (newRoleId) {
|
|
console.log('Role selected:', newRoleId, typeof newRoleId);
|
|
|
|
// Convert roleId to string to ensure consistent comparison
|
|
const roleIdStr = String(newRoleId);
|
|
|
|
// Find the selected role from available roles
|
|
const selectedRole = availableRoles.value.find(role => String(role.roleID) === roleIdStr);
|
|
|
|
if (selectedRole) {
|
|
console.log('Found role:', selectedRole);
|
|
|
|
// Initialize the array if needed
|
|
if (!localProcess.value.allowedRoles) {
|
|
localProcess.value.allowedRoles = [];
|
|
}
|
|
|
|
// Check if role already exists
|
|
const roleExists = localProcess.value.allowedRoles.some(role => String(role.value) === roleIdStr);
|
|
|
|
if (!roleExists) {
|
|
// Add the role to the allowed roles
|
|
localProcess.value.allowedRoles.push({
|
|
label: selectedRole.roleName,
|
|
value: String(selectedRole.roleID)
|
|
});
|
|
|
|
// Update filtered roles
|
|
updateFilteredRoles();
|
|
}
|
|
} else {
|
|
console.warn('Selected role not found in available roles', roleIdStr);
|
|
}
|
|
|
|
// Reset the selection
|
|
selectedRoleId.value = '';
|
|
}
|
|
})
|
|
|
|
// Remove allowed role
|
|
const removeAllowedRole = (index) => {
|
|
if (localProcess.value.allowedRoles && localProcess.value.allowedRoles[index]) {
|
|
localProcess.value.allowedRoles.splice(index, 1)
|
|
// Update filtered roles
|
|
updateFilteredRoles()
|
|
}
|
|
}
|
|
|
|
// Computed properties for form options
|
|
const roleOptions = computed(() => {
|
|
return availableRoles.value.map(role => ({
|
|
label: role.roleName,
|
|
value: role.roleID
|
|
}))
|
|
})
|
|
|
|
// Computed properties for permission options
|
|
const executionPermissionOptions = computed(() => {
|
|
return availablePermissions.value
|
|
.filter(permission => permission.category === 'execution')
|
|
.map(permission => ({
|
|
label: permission.name,
|
|
value: permission.id,
|
|
help: permission.description
|
|
}))
|
|
})
|
|
|
|
const modificationPermissionOptions = computed(() => {
|
|
return availablePermissions.value
|
|
.filter(permission => permission.category === 'modification')
|
|
.map(permission => ({
|
|
label: permission.name,
|
|
value: permission.id,
|
|
help: permission.description
|
|
}))
|
|
})
|
|
|
|
// Computed properties for metadata
|
|
const nodeCount = computed(() => {
|
|
return processStore.currentProcess?.nodes?.length || 0
|
|
})
|
|
|
|
const edgeCount = computed(() => {
|
|
return processStore.currentProcess?.edges?.length || 0
|
|
})
|
|
|
|
const variableCount = computed(() => {
|
|
const processVariables = processStore.getProcessVariables()
|
|
if (!processVariables || typeof processVariables !== 'object') {
|
|
return 0
|
|
}
|
|
return Object.keys(processVariables).length
|
|
})
|
|
|
|
// JSON export functionality - simplified
|
|
const formattedJson = computed(() => {
|
|
const exportData = {
|
|
processInfo: {
|
|
id: localProcess.value.id,
|
|
name: localProcess.value.name,
|
|
description: localProcess.value.description,
|
|
category: localProcess.value.category
|
|
},
|
|
settings: {
|
|
processType: localProcess.value.processType,
|
|
allowParallel: localProcess.value.allowParallel,
|
|
sendNotifications: localProcess.value.sendNotifications
|
|
},
|
|
permissions: {
|
|
executionPermission: localProcess.value.executionPermission,
|
|
allowedRoles: localProcess.value.allowedRoles,
|
|
modificationPermission: localProcess.value.modificationPermission
|
|
},
|
|
workflow: {
|
|
nodes: processStore.currentProcess?.nodes || [],
|
|
edges: processStore.currentProcess?.edges || []
|
|
},
|
|
variables: processStore.getProcessVariables(),
|
|
metadata: {
|
|
nodeCount: nodeCount.value,
|
|
edgeCount: edgeCount.value,
|
|
variableCount: variableCount.value,
|
|
exportedAt: new Date().toISOString()
|
|
}
|
|
}
|
|
|
|
return JSON.stringify(exportData, null, 2)
|
|
})
|
|
|
|
// Watch for changes to current process and sync with local data
|
|
watch(() => processStore.currentProcess, (newProcess) => {
|
|
if (newProcess) {
|
|
// Handle roles conversion - convert string to array if needed
|
|
let allowedRoles = newProcess.settings?.allowedRoles || []
|
|
if (typeof allowedRoles === 'string' && allowedRoles.trim()) {
|
|
// Convert comma-separated string to array
|
|
allowedRoles = allowedRoles.split(',').map(role => role.trim()).filter(role => role)
|
|
} else if (!Array.isArray(allowedRoles)) {
|
|
allowedRoles = []
|
|
}
|
|
|
|
localProcess.value = {
|
|
...localProcess.value,
|
|
id: newProcess.id,
|
|
name: newProcess.name || '',
|
|
description: newProcess.description || '',
|
|
category: newProcess.category || '',
|
|
processType: newProcess.settings?.processType || 'standard',
|
|
allowParallel: newProcess.settings?.allowParallel || false,
|
|
sendNotifications: newProcess.settings?.sendNotifications !== false,
|
|
executionPermission: newProcess.settings?.executionPermission || 'authenticated',
|
|
allowedRoles: allowedRoles,
|
|
modificationPermission: newProcess.settings?.modificationPermission || 'managers'
|
|
}
|
|
|
|
// Update filtered roles after setting the data
|
|
updateFilteredRoles()
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Load permissions when modal opens
|
|
watch(() => showModal.value, (isOpen) => {
|
|
if (isOpen && availableRoles.value.length === 0) {
|
|
loadPermissionsFromDatabase()
|
|
}
|
|
})
|
|
|
|
// Methods
|
|
const closeModal = () => {
|
|
showModal.value = false
|
|
}
|
|
|
|
const saveSettings = () => {
|
|
// Update the process in the store with simplified settings
|
|
if (processStore.currentProcess) {
|
|
// Convert roles array to string for storage (for backward compatibility)
|
|
const allowedRolesString = Array.isArray(localProcess.value.allowedRoles)
|
|
? localProcess.value.allowedRoles.join(', ')
|
|
: localProcess.value.allowedRoles
|
|
|
|
const updatedProcess = {
|
|
...processStore.currentProcess,
|
|
name: localProcess.value.name,
|
|
description: localProcess.value.description,
|
|
category: localProcess.value.category,
|
|
settings: {
|
|
processType: localProcess.value.processType,
|
|
allowParallel: localProcess.value.allowParallel,
|
|
sendNotifications: localProcess.value.sendNotifications,
|
|
executionPermission: localProcess.value.executionPermission,
|
|
allowedRoles: allowedRolesString, // Store as string for compatibility
|
|
modificationPermission: localProcess.value.modificationPermission
|
|
}
|
|
}
|
|
|
|
// Update the store
|
|
processStore.updateCurrentProcess(updatedProcess)
|
|
}
|
|
|
|
closeModal()
|
|
}
|
|
|
|
const copyToClipboard = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(formattedJson.value)
|
|
console.log('JSON copied to clipboard')
|
|
} catch (err) {
|
|
console.error('Failed to copy JSON:', err)
|
|
}
|
|
}
|
|
|
|
const downloadJson = () => {
|
|
const blob = new Blob([formattedJson.value], { type: 'application/json' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = `${localProcess.value.name || 'process'}_settings.json`
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
document.body.removeChild(a)
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Custom styling for the settings modal */
|
|
:deep(.formkit-outer) {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
:deep(.formkit-label) {
|
|
font-weight: 500;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.875rem;
|
|
color: #374151;
|
|
}
|
|
|
|
:deep(.formkit-help) {
|
|
font-size: 0.75rem;
|
|
color: #6b7280;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
:deep(.formkit-messages) {
|
|
font-size: 0.75rem;
|
|
color: #ef4444;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
pre {
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 0.875rem;
|
|
line-height: 1.5;
|
|
}
|
|
</style> |