Enhance FormNodeConfiguration with User and Role Management Features

- Refactored user and role assignment sections to improve user experience in the FormNodeConfiguration component.
- Introduced new reactive states for selected user and role, allowing for dynamic selection and assignment.
- Implemented filtering logic to prevent already assigned users and roles from being selectable, enhancing usability.
- Added functionality to remove assigned users and roles with intuitive UI elements for better interaction.
- Updated the handling of assignment type changes to reset selections appropriately, ensuring a smooth user experience.
- Enhanced documentation to reflect the new features and their usage within the form configuration context.
This commit is contained in:
Afiq 2025-07-03 12:27:36 +08:00
parent 6887a2b9bc
commit e10a3745c0
3 changed files with 537 additions and 38 deletions

View File

@ -458,7 +458,6 @@
{ label: 'Dynamic (from Variable)', value: 'variable' }
]"
placeholder="Select assignment type"
@input="handleAssignmentTypeChange"
:classes="{ outer: 'mb-0' }"
/>
<p class="mt-1 text-xs text-gray-500">How should this task be assigned</p>
@ -472,15 +471,32 @@
</div>
<div class="space-y-3">
<FormKit
type="select"
v-model="localNodeData.assignedUsers"
:options="availableUsers"
placeholder="Select users..."
multiple
:classes="{ outer: 'mb-0' }"
/>
<p class="text-xs text-blue-700">Selected users will be able to complete this form task</p>
<!-- User Dropdown -->
<div class="relative">
<FormKit
type="select"
v-model="selectedUserId"
:options="filteredAvailableUsers"
placeholder="Select a user to add..."
:classes="{ outer: 'mb-0' }"
@input="handleUserSelection"
/>
<p class="mt-1 text-xs text-blue-700">Select users who will be able to complete this form task</p>
</div>
<!-- Selected Users Pills -->
<div v-if="localNodeData.assignedUsers && localNodeData.assignedUsers.length > 0" class="mt-3">
<label class="block text-sm font-medium text-blue-700 mb-2">Selected Users</label>
<div class="flex flex-wrap gap-2 p-2 bg-white border border-blue-100 rounded-md min-h-[40px]">
<div v-for="(user, index) in localNodeData.assignedUsers" :key="'user-' + user.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">{{ user.label }}</span>
<button @click="removeAssignedUser(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>
@ -492,15 +508,32 @@
</div>
<div class="space-y-3">
<FormKit
type="select"
v-model="localNodeData.assignedRoles"
:options="availableRoles"
placeholder="Select roles..."
multiple
:classes="{ outer: 'mb-0' }"
/>
<p class="text-xs text-purple-700">Any user with the selected roles will be able to complete this form task</p>
<!-- Role Dropdown -->
<div class="relative">
<FormKit
type="select"
v-model="selectedRoleId"
:options="filteredAvailableRoles"
placeholder="Select a role to add..."
:classes="{ outer: 'mb-0' }"
@input="handleRoleSelection"
/>
<p class="mt-1 text-xs text-purple-700">Select roles that will be able to complete this form task</p>
</div>
<!-- Selected Roles Pills -->
<div v-if="localNodeData.assignedRoles && localNodeData.assignedRoles.length > 0" class="mt-3">
<label class="block text-sm font-medium text-purple-700 mb-2">Selected Roles</label>
<div class="flex flex-wrap gap-2 p-2 bg-white border border-purple-100 rounded-md min-h-[40px]">
<div v-for="(role, index) in localNodeData.assignedRoles" :key="'role-' + role.value"
class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-purple-100 text-purple-800 border border-purple-200">
<span class="mr-1">{{ role.label }}</span>
<button @click="removeAssignedRole(index)" class="text-purple-600 hover:text-purple-800">
<Icon name="material-symbols:close" class="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</div>
@ -623,6 +656,10 @@ const localNodeData = ref({
assignmentVariableType: 'user_id'
});
// New reactive state for selected user and role
const selectedUserId = ref('');
const selectedRoleId = ref('');
// Watch for changes from parent props
watch(() => props.nodeData, async (newNodeData) => {
if (newNodeData) {
@ -859,23 +896,33 @@ const processVariableOptions = computed(() => {
const users = ref([]);
const roles = ref([]);
// Computed property for available users (for FormKit select)
const availableUsers = computed(() => {
return users.value.map(user => ({
label: user.userFullName ? `${user.userFullName} (${user.userUsername})` : user.userUsername,
value: user.userID,
username: user.userUsername,
email: user.userEmail
}));
// Computed property for available users with filtering out already selected users
const filteredAvailableUsers = computed(() => {
// Convert all IDs to strings for consistent comparison
const selectedUserIds = (localNodeData.value.assignedUsers || []).map(user => String(user.value));
return users.value
.filter(user => !selectedUserIds.includes(String(user.userID)))
.map(user => ({
label: user.userFullName ? `${user.userFullName} (${user.userUsername})` : user.userUsername,
value: String(user.userID), // Ensure value is a string
username: user.userUsername,
email: user.userEmail
}));
});
// Computed property for available roles (for FormKit select)
const availableRoles = computed(() => {
return roles.value.map(role => ({
label: role.roleName,
value: role.roleID,
description: role.roleDescription
}));
// Computed property for available roles with filtering out already selected roles
const filteredAvailableRoles = computed(() => {
// Convert all IDs to strings for consistent comparison
const selectedRoleIds = (localNodeData.value.assignedRoles || []).map(role => String(role.value));
return roles.value
.filter(role => !selectedRoleIds.includes(String(role.roleID)))
.map(role => ({
label: role.roleName,
value: String(role.roleID), // Ensure value is a string
description: role.roleDescription
}));
});
// Fetch users and roles data
@ -974,11 +1021,161 @@ function getStringValue(value) {
}
}
// Handle assignment type change
function handleAssignmentTypeChange() {
// Implementation of handleAssignmentTypeChange function
saveChanges();
// Handle user selection from dropdown
function handleUserSelection(userId) {
if (userId) {
console.log('User selected:', userId, typeof userId);
// Convert userId to string to ensure consistent comparison
const userIdStr = String(userId);
// Find the selected user from available users
const selectedUser = filteredAvailableUsers.value.find(user => String(user.value) === userIdStr);
if (selectedUser) {
console.log('Found user:', selectedUser);
// Initialize the array if needed
if (!localNodeData.value.assignedUsers) {
localNodeData.value.assignedUsers = [];
}
// Add the user to the assigned users
localNodeData.value.assignedUsers.push({...selectedUser});
// Reset the selection
selectedUserId.value = '';
// Save changes
saveChanges();
} else {
console.warn('Selected user not found in filtered available users', userIdStr);
// Fallback: If we can't find the user in filtered list, try to find it in the original users list
const userFromOriginal = users.value.find(user => String(user.userID) === userIdStr);
if (userFromOriginal) {
console.log('Found user in original users list:', userFromOriginal);
// Add the user to the assigned users
const userToAdd = {
label: userFromOriginal.userFullName ? `${userFromOriginal.userFullName} (${userFromOriginal.userUsername})` : userFromOriginal.userUsername,
value: String(userFromOriginal.userID),
username: userFromOriginal.userUsername,
email: userFromOriginal.userEmail
};
if (!localNodeData.value.assignedUsers) {
localNodeData.value.assignedUsers = [];
}
localNodeData.value.assignedUsers.push(userToAdd);
selectedUserId.value = '';
saveChanges();
} else {
console.error('User not found in any list. Available users:', users.value);
}
}
}
}
// Handle role selection from dropdown
function handleRoleSelection(roleId) {
if (roleId) {
console.log('Role selected:', roleId, typeof roleId);
// Convert roleId to string to ensure consistent comparison
const roleIdStr = String(roleId);
// Find the selected role from available roles
const selectedRole = filteredAvailableRoles.value.find(role => String(role.value) === roleIdStr);
if (selectedRole) {
console.log('Found role:', selectedRole);
// Initialize the array if needed
if (!localNodeData.value.assignedRoles) {
localNodeData.value.assignedRoles = [];
}
// Add the role to the assigned roles
localNodeData.value.assignedRoles.push({...selectedRole});
// Reset the selection
selectedRoleId.value = '';
// Save changes
saveChanges();
} else {
console.warn('Selected role not found in filtered available roles', roleIdStr);
// Fallback: If we can't find the role in filtered list, try to find it in the original roles list
const roleFromOriginal = roles.value.find(role => String(role.roleID) === roleIdStr);
if (roleFromOriginal) {
console.log('Found role in original roles list:', roleFromOriginal);
// Add the role to the assigned roles
const roleToAdd = {
label: roleFromOriginal.roleName,
value: String(roleFromOriginal.roleID),
description: roleFromOriginal.roleDescription
};
if (!localNodeData.value.assignedRoles) {
localNodeData.value.assignedRoles = [];
}
localNodeData.value.assignedRoles.push(roleToAdd);
selectedRoleId.value = '';
saveChanges();
} else {
console.error('Role not found in any list. Available roles:', roles.value);
}
}
}
}
// Function to remove an assigned user
function removeAssignedUser(index) {
if (localNodeData.value.assignedUsers) {
localNodeData.value.assignedUsers.splice(index, 1);
saveChanges();
}
}
// Function to remove an assigned role
function removeAssignedRole(index) {
if (localNodeData.value.assignedRoles) {
localNodeData.value.assignedRoles.splice(index, 1);
saveChanges();
}
}
// Watch for changes to assignment type
watch(() => localNodeData.value.assignmentType, (newType, oldType) => {
if (newType !== oldType) {
// Reset selections when assignment type changes
selectedUserId.value = '';
selectedRoleId.value = '';
// If changing from users to another type, clear assigned users
if (oldType === 'users' && newType !== 'users') {
localNodeData.value.assignedUsers = [];
}
// If changing from roles to another type, clear assigned roles
if (oldType === 'roles' && newType !== 'roles') {
localNodeData.value.assignedRoles = [];
}
// If changing from variable to another type, clear assignment variable
if (oldType === 'variable' && newType !== 'variable') {
localNodeData.value.assignmentVariable = '';
localNodeData.value.assignmentVariableType = 'user_id';
}
saveChanges();
}
});
</script>
<style scoped>

View File

@ -63,6 +63,18 @@
],
"format": "date-time"
},
"caseInstance": {
"type": "array",
"items": {
"$ref": "#/definitions/caseInstance"
}
},
"caseTimeline": {
"type": "array",
"items": {
"$ref": "#/definitions/caseTimeline"
}
},
"forms": {
"type": "array",
"items": {
@ -87,6 +99,12 @@
"$ref": "#/definitions/processHistory"
}
},
"task": {
"type": "array",
"items": {
"$ref": "#/definitions/task"
}
},
"userrole": {
"type": "array",
"items": {
@ -246,6 +264,12 @@
"items": {
"$ref": "#/definitions/formHistory"
}
},
"task": {
"type": "array",
"items": {
"$ref": "#/definitions/task"
}
}
}
},
@ -449,6 +473,12 @@
],
"format": "date-time"
},
"caseInstance": {
"type": "array",
"items": {
"$ref": "#/definitions/caseInstance"
}
},
"creator": {
"anyOf": [
{
@ -582,6 +612,203 @@
]
}
}
},
"caseInstance": {
"type": "object",
"properties": {
"caseID": {
"type": "integer"
},
"caseUUID": {
"type": "string"
},
"caseName": {
"type": "string"
},
"caseStatus": {
"type": "string",
"default": "active"
},
"caseVariables": {
"type": [
"number",
"string",
"boolean",
"object",
"array",
"null"
]
},
"caseSettings": {
"type": [
"number",
"string",
"boolean",
"object",
"array",
"null"
]
},
"caseDefinition": {
"type": [
"number",
"string",
"boolean",
"object",
"array",
"null"
]
},
"caseCreatedDate": {
"type": "string",
"format": "date-time"
},
"caseModifiedDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"caseCompletedDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"user": {
"anyOf": [
{
"$ref": "#/definitions/user"
},
{
"type": "null"
}
]
},
"process": {
"$ref": "#/definitions/process"
},
"caseTimeline": {
"type": "array",
"items": {
"$ref": "#/definitions/caseTimeline"
}
},
"task": {
"type": "array",
"items": {
"$ref": "#/definitions/task"
}
}
}
},
"caseTimeline": {
"type": "object",
"properties": {
"timelineID": {
"type": "integer"
},
"timelineType": {
"type": "string"
},
"timelineDescription": {
"type": [
"string",
"null"
]
},
"timelineDate": {
"type": "string",
"format": "date-time"
},
"caseInstance": {
"$ref": "#/definitions/caseInstance"
},
"user": {
"anyOf": [
{
"$ref": "#/definitions/user"
},
{
"type": "null"
}
]
}
}
},
"task": {
"type": "object",
"properties": {
"taskID": {
"type": "integer"
},
"taskUUID": {
"type": "string"
},
"taskName": {
"type": "string"
},
"taskType": {
"type": "string"
},
"taskStatus": {
"type": "string",
"default": "pending"
},
"taskData": {
"type": [
"number",
"string",
"boolean",
"object",
"array",
"null"
]
},
"taskCreatedDate": {
"type": "string",
"format": "date-time"
},
"taskModifiedDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"taskCompletedDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"caseInstance": {
"$ref": "#/definitions/caseInstance"
},
"user": {
"anyOf": [
{
"$ref": "#/definitions/user"
},
{
"type": "null"
}
]
},
"form": {
"anyOf": [
{
"$ref": "#/definitions/form"
},
{
"type": "null"
}
]
}
}
}
},
"type": "object",
@ -606,6 +833,15 @@
},
"processHistory": {
"$ref": "#/definitions/processHistory"
},
"caseInstance": {
"$ref": "#/definitions/caseInstance"
},
"caseTimeline": {
"$ref": "#/definitions/caseTimeline"
},
"task": {
"$ref": "#/definitions/task"
}
}
}

View File

@ -23,10 +23,13 @@ model user {
userStatus String? @db.VarChar(255)
userCreatedDate DateTime? @db.DateTime(0)
userModifiedDate DateTime? @db.DateTime(0)
caseInstance caseInstance[]
caseTimeline caseTimeline[]
forms form[] @relation("FormCreator")
formHistoryEntries formHistory[]
processes process[] @relation("ProcessCreator")
processHistoryEntries processHistory[]
task task[]
userrole userrole[]
}
@ -68,6 +71,7 @@ model form {
scriptMode String? @default("safe") @db.VarChar(20)
creator user? @relation("FormCreator", fields: [formCreatedBy], references: [userID])
history formHistory[] @relation("FormHistoryEntries")
task task[]
@@index([formCreatedBy], map: "FK_form_creator")
}
@ -117,6 +121,7 @@ model process {
processVariables Json?
templateCategory String? @db.VarChar(100)
processDeletedDate DateTime? @db.DateTime(0)
caseInstance caseInstance[]
creator user? @relation("ProcessCreator", fields: [processCreatedBy], references: [userID])
history processHistory[] @relation("ProcessHistoryEntries")
@ -154,3 +159,64 @@ model processHistory {
@@index([processUUID], map: "IDX_processHistory_uuid")
@@index([savedDate], map: "IDX_processHistory_date")
}
model caseInstance {
caseID Int @id @default(autoincrement())
caseUUID String @unique @db.VarChar(36)
processID Int
caseName String @db.VarChar(255)
caseStatus String @default("active") @db.VarChar(50)
caseStartedBy Int?
caseVariables Json?
caseSettings Json?
caseDefinition Json?
caseCreatedDate DateTime @default(now()) @db.DateTime(0)
caseModifiedDate DateTime? @db.DateTime(0)
caseCompletedDate DateTime? @db.DateTime(0)
user user? @relation(fields: [caseStartedBy], references: [userID])
process process @relation(fields: [processID], references: [processID])
caseTimeline caseTimeline[]
task task[]
@@index([processID], map: "FK_case_process")
@@index([caseStartedBy], map: "FK_case_startedBy")
@@index([caseStatus], map: "IDX_case_status")
}
model caseTimeline {
timelineID Int @id @default(autoincrement())
caseID Int
timelineType String @db.VarChar(50)
timelineDescription String? @db.Text
timelineDate DateTime @default(now()) @db.DateTime(0)
timelineCreatedBy Int?
caseInstance caseInstance @relation(fields: [caseID], references: [caseID])
user user? @relation(fields: [timelineCreatedBy], references: [userID])
@@index([caseID], map: "FK_caseTimeline_case")
@@index([timelineCreatedBy], map: "FK_caseTimeline_createdBy")
@@index([timelineDate], map: "IDX_caseTimeline_date")
}
model task {
taskID Int @id @default(autoincrement())
taskUUID String @unique @db.VarChar(36)
caseID Int
taskName String @db.VarChar(255)
taskType String @db.VarChar(50)
taskStatus String @default("pending") @db.VarChar(50)
taskAssignedTo Int?
taskFormID Int?
taskData Json?
taskCreatedDate DateTime @default(now()) @db.DateTime(0)
taskModifiedDate DateTime? @db.DateTime(0)
taskCompletedDate DateTime? @db.DateTime(0)
caseInstance caseInstance @relation(fields: [caseID], references: [caseID])
user user? @relation(fields: [taskAssignedTo], references: [userID])
form form? @relation(fields: [taskFormID], references: [formID])
@@index([taskAssignedTo], map: "FK_task_assignedTo")
@@index([caseID], map: "FK_task_case")
@@index([taskFormID], map: "FK_task_form")
@@index([taskStatus], map: "IDX_task_status")
}