corrad-bp/components/process-flow/ProcessSettingsModal.vue
Md Afiq Iskandar 5261bf601e Refactor ProcessSettingsModal to Simplify Settings and Enhance Permissions Management
- 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.
2025-07-23 06:50:48 +08:00

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>