Compare commits
2 Commits
fb2533fa4c
...
integrate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baf3997fb6 | ||
|
|
a2d48f6a03 |
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Add 'alias' and virtual 'name' column to users table.
|
||||||
|
*
|
||||||
|
* - alias: a user-editable display name / nickname
|
||||||
|
* - name: a generated column that returns COALESCE(alias, CONCAT(firstName, ' ', lastName), email)
|
||||||
|
* so that lookup fields referencing User.name always resolve.
|
||||||
|
*/
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.alterTable('users', (table) => {
|
||||||
|
table.string('alias', 255).nullable().after('lastName');
|
||||||
|
table.string('name', 512).nullable().after('alias');
|
||||||
|
}).then(() => {
|
||||||
|
// Backfill existing rows: name = alias, or firstName + lastName, or email
|
||||||
|
return knex.raw(`
|
||||||
|
UPDATE users
|
||||||
|
SET name = COALESCE(
|
||||||
|
NULLIF(alias, ''),
|
||||||
|
NULLIF(TRIM(CONCAT(COALESCE(firstName, ''), ' ', COALESCE(lastName, ''))), ''),
|
||||||
|
email
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.alterTable('users', (table) => {
|
||||||
|
table.dropColumn('name');
|
||||||
|
table.dropColumn('alias');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -20,6 +20,8 @@ model User {
|
|||||||
password String
|
password String
|
||||||
firstName String?
|
firstName String?
|
||||||
lastName String?
|
lastName String?
|
||||||
|
alias String?
|
||||||
|
name String?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BaseModel } from './base.model';
|
import { BaseModel } from './base.model';
|
||||||
|
import { ModelOptions, QueryContext } from 'objection';
|
||||||
|
|
||||||
export class User extends BaseModel {
|
export class User extends BaseModel {
|
||||||
static tableName = 'users';
|
static tableName = 'users';
|
||||||
@@ -8,6 +9,8 @@ export class User extends BaseModel {
|
|||||||
password: string;
|
password: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
alias?: string;
|
||||||
|
name?: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -22,11 +25,37 @@ export class User extends BaseModel {
|
|||||||
password: { type: 'string' },
|
password: { type: 'string' },
|
||||||
firstName: { type: 'string' },
|
firstName: { type: 'string' },
|
||||||
lastName: { type: 'string' },
|
lastName: { type: 'string' },
|
||||||
|
alias: { type: 'string' },
|
||||||
|
name: { type: 'string' },
|
||||||
isActive: { type: 'boolean' },
|
isActive: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the `name` column before insert/update so lookup fields
|
||||||
|
* referencing User.name always have a value.
|
||||||
|
*/
|
||||||
|
private computeName() {
|
||||||
|
if (this.alias) {
|
||||||
|
this.name = this.alias;
|
||||||
|
} else if (this.firstName || this.lastName) {
|
||||||
|
this.name = [this.firstName, this.lastName].filter(Boolean).join(' ');
|
||||||
|
} else if (this.email) {
|
||||||
|
this.name = this.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$beforeInsert(queryContext: QueryContext) {
|
||||||
|
super.$beforeInsert(queryContext);
|
||||||
|
this.computeName();
|
||||||
|
}
|
||||||
|
|
||||||
|
$beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {
|
||||||
|
super.$beforeUpdate(opt, queryContext);
|
||||||
|
this.computeName();
|
||||||
|
}
|
||||||
|
|
||||||
static get relationMappings() {
|
static get relationMappings() {
|
||||||
const { UserRole } = require('./user-role.model');
|
const { UserRole } = require('./user-role.model');
|
||||||
const { Role } = require('./role.model');
|
const { Role } = require('./role.model');
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export class FieldMapperService {
|
|||||||
'phone': 'text',
|
'phone': 'text',
|
||||||
'picklist': 'select',
|
'picklist': 'select',
|
||||||
'multipicklist': 'multiSelect',
|
'multipicklist': 'multiSelect',
|
||||||
|
'multi_picklist': 'multiSelect',
|
||||||
'lookup': 'belongsTo',
|
'lookup': 'belongsTo',
|
||||||
'master-detail': 'belongsTo',
|
'master-detail': 'belongsTo',
|
||||||
'currency': 'currency',
|
'currency': 'currency',
|
||||||
|
|||||||
@@ -336,13 +336,27 @@ export class ObjectService {
|
|||||||
updated_at: knex.fn.now(),
|
updated_at: knex.fn.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store relationDisplayField in UI metadata if provided
|
// Build UI metadata from all sources
|
||||||
if (data.relationDisplayField || data.relationObjects || data.relationTypeField) {
|
const uiMetadataObj: any = {};
|
||||||
fieldData.ui_metadata = JSON.stringify({
|
|
||||||
relationDisplayField: data.relationDisplayField,
|
// Merge general uiMetadata (options, placeholder, helpText, etc.)
|
||||||
relationObjects: data.relationObjects,
|
if (data.uiMetadata && typeof data.uiMetadata === 'object') {
|
||||||
relationTypeField: data.relationTypeField,
|
Object.assign(uiMetadataObj, data.uiMetadata);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Store relation-specific fields in UI metadata if provided
|
||||||
|
if (data.relationDisplayField) {
|
||||||
|
uiMetadataObj.relationDisplayField = data.relationDisplayField;
|
||||||
|
}
|
||||||
|
if (data.relationObjects) {
|
||||||
|
uiMetadataObj.relationObjects = data.relationObjects;
|
||||||
|
}
|
||||||
|
if (data.relationTypeField) {
|
||||||
|
uiMetadataObj.relationTypeField = data.relationTypeField;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(uiMetadataObj).length > 0) {
|
||||||
|
fieldData.ui_metadata = JSON.stringify(uiMetadataObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
await knex('field_definitions').insert(fieldData);
|
await knex('field_definitions').insert(fieldData);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class SetupUsersController {
|
|||||||
@Post()
|
@Post()
|
||||||
async createUser(
|
async createUser(
|
||||||
@TenantId() tenantId: string,
|
@TenantId() tenantId: string,
|
||||||
@Body() data: { email: string; password: string; firstName?: string; lastName?: string },
|
@Body() data: { email: string; password: string; firstName?: string; lastName?: string; alias?: string },
|
||||||
) {
|
) {
|
||||||
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
@@ -52,6 +52,7 @@ export class SetupUsersController {
|
|||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
firstName: data.firstName,
|
firstName: data.firstName,
|
||||||
lastName: data.lastName,
|
lastName: data.lastName,
|
||||||
|
alias: data.alias,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ export class SetupUsersController {
|
|||||||
async updateUser(
|
async updateUser(
|
||||||
@TenantId() tenantId: string,
|
@TenantId() tenantId: string,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() data: { email?: string; password?: string; firstName?: string; lastName?: string },
|
@Body() data: { email?: string; password?: string; firstName?: string; lastName?: string; alias?: string },
|
||||||
) {
|
) {
|
||||||
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
@@ -72,6 +73,7 @@ export class SetupUsersController {
|
|||||||
if (data.email) updateData.email = data.email;
|
if (data.email) updateData.email = data.email;
|
||||||
if (data.firstName !== undefined) updateData.firstName = data.firstName;
|
if (data.firstName !== undefined) updateData.firstName = data.firstName;
|
||||||
if (data.lastName !== undefined) updateData.lastName = data.lastName;
|
if (data.lastName !== undefined) updateData.lastName = data.lastName;
|
||||||
|
if (data.alias !== undefined) updateData.alias = data.alias;
|
||||||
|
|
||||||
// Hash password if provided
|
// Hash password if provided
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
|
|||||||
@@ -249,6 +249,51 @@ const handleRelationTypeUpdate = (value: string | null) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<!-- Multi-Select -->
|
||||||
|
<div v-else-if="field.type === FieldType.MULTI_SELECT" class="space-y-2">
|
||||||
|
<div class="flex flex-wrap gap-1 min-h-[36px] rounded-md border border-input bg-background px-3 py-2">
|
||||||
|
<Badge
|
||||||
|
v-for="selectedVal in (Array.isArray(value) ? value : [])"
|
||||||
|
:key="String(selectedVal)"
|
||||||
|
variant="secondary"
|
||||||
|
class="gap-1 cursor-pointer"
|
||||||
|
@click="value = (value || []).filter((v: any) => v !== selectedVal)"
|
||||||
|
>
|
||||||
|
{{ field.options?.find(o => o.value === selectedVal)?.label || selectedVal }}
|
||||||
|
<span class="text-xs ml-1">×</span>
|
||||||
|
</Badge>
|
||||||
|
<span v-if="!value || (Array.isArray(value) && value.length === 0)" class="text-sm text-muted-foreground">
|
||||||
|
{{ field.placeholder || 'Select options...' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="option in field.options"
|
||||||
|
:key="String(option.value)"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
:id="`${field.id}-${option.value}`"
|
||||||
|
:checked="Array.isArray(value) && value.includes(option.value)"
|
||||||
|
@update:checked="(checked: boolean) => {
|
||||||
|
const current = Array.isArray(value) ? [...value] : []
|
||||||
|
if (checked) {
|
||||||
|
current.push(option.value)
|
||||||
|
} else {
|
||||||
|
const idx = current.indexOf(option.value)
|
||||||
|
if (idx > -1) current.splice(idx, 1)
|
||||||
|
}
|
||||||
|
value = current
|
||||||
|
}"
|
||||||
|
:disabled="field.isReadOnly"
|
||||||
|
/>
|
||||||
|
<Label :for="`${field.id}-${option.value}`" class="text-sm font-normal cursor-pointer">
|
||||||
|
{{ option.label }}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Boolean - Checkbox -->
|
<!-- Boolean - Checkbox -->
|
||||||
<div v-else-if="field.type === FieldType.BOOLEAN" class="flex items-center gap-2">
|
<div v-else-if="field.type === FieldType.BOOLEAN" class="flex items-center gap-2">
|
||||||
<Checkbox :id="field.id" v-model:checked="value" :disabled="field.isReadOnly" />
|
<Checkbox :id="field.id" v-model:checked="value" :disabled="field.isReadOnly" />
|
||||||
|
|||||||
@@ -33,6 +33,10 @@
|
|||||||
<Label class="text-muted-foreground">Email</Label>
|
<Label class="text-muted-foreground">Email</Label>
|
||||||
<p class="font-medium">{{ user?.email }}</p>
|
<p class="font-medium">{{ user?.email }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label class="text-muted-foreground">Alias</Label>
|
||||||
|
<p class="font-medium">{{ user?.alias || 'N/A' }}</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label class="text-muted-foreground">First Name</Label>
|
<Label class="text-muted-foreground">First Name</Label>
|
||||||
<p class="font-medium">{{ user?.firstName || 'N/A' }}</p>
|
<p class="font-medium">{{ user?.firstName || 'N/A' }}</p>
|
||||||
@@ -210,6 +214,9 @@ const removeRole = async (roleId: string) => {
|
|||||||
|
|
||||||
const getUserName = (user: any) => {
|
const getUserName = (user: any) => {
|
||||||
if (!user) return 'User';
|
if (!user) return 'User';
|
||||||
|
if (user.alias) {
|
||||||
|
return user.alias;
|
||||||
|
}
|
||||||
if (user.firstName || user.lastName) {
|
if (user.firstName || user.lastName) {
|
||||||
return [user.firstName, user.lastName].filter(Boolean).join(' ');
|
return [user.firstName, user.lastName].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,10 @@
|
|||||||
<Label for="lastName">Last Name (Optional)</Label>
|
<Label for="lastName">Last Name (Optional)</Label>
|
||||||
<Input id="lastName" v-model="newUser.lastName" placeholder="Doe" />
|
<Input id="lastName" v-model="newUser.lastName" placeholder="Doe" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="alias">Alias (Optional)</Label>
|
||||||
|
<Input id="alias" v-model="newUser.alias" placeholder="Display name" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" @click="showCreateDialog = false">Cancel</Button>
|
<Button variant="outline" @click="showCreateDialog = false">Cancel</Button>
|
||||||
@@ -131,6 +135,10 @@
|
|||||||
<Label for="edit-lastName">Last Name</Label>
|
<Label for="edit-lastName">Last Name</Label>
|
||||||
<Input id="edit-lastName" v-model="editUser.lastName" placeholder="Doe" />
|
<Input id="edit-lastName" v-model="editUser.lastName" placeholder="Doe" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-alias">Alias</Label>
|
||||||
|
<Input id="edit-alias" v-model="editUser.alias" placeholder="Display name" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" @click="showEditDialog = false">Cancel</Button>
|
<Button variant="outline" @click="showEditDialog = false">Cancel</Button>
|
||||||
@@ -187,6 +195,7 @@ const newUser = ref({
|
|||||||
password: '',
|
password: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
|
alias: '',
|
||||||
});
|
});
|
||||||
const editUser = ref({
|
const editUser = ref({
|
||||||
id: '',
|
id: '',
|
||||||
@@ -194,6 +203,7 @@ const editUser = ref({
|
|||||||
password: '',
|
password: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
|
alias: '',
|
||||||
});
|
});
|
||||||
const userToDelete = ref<any>(null);
|
const userToDelete = ref<any>(null);
|
||||||
|
|
||||||
@@ -215,7 +225,7 @@ const createUser = async () => {
|
|||||||
await api.post('/setup/users', newUser.value);
|
await api.post('/setup/users', newUser.value);
|
||||||
toast.success('User created successfully');
|
toast.success('User created successfully');
|
||||||
showCreateDialog.value = false;
|
showCreateDialog.value = false;
|
||||||
newUser.value = { email: '', password: '', firstName: '', lastName: '' };
|
newUser.value = { email: '', password: '', firstName: '', lastName: '', alias: '' };
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to create user:', error);
|
console.error('Failed to create user:', error);
|
||||||
@@ -230,6 +240,7 @@ const openEditDialog = (user: any) => {
|
|||||||
password: '',
|
password: '',
|
||||||
firstName: user.firstName || '',
|
firstName: user.firstName || '',
|
||||||
lastName: user.lastName || '',
|
lastName: user.lastName || '',
|
||||||
|
alias: user.alias || '',
|
||||||
};
|
};
|
||||||
showEditDialog.value = true;
|
showEditDialog.value = true;
|
||||||
};
|
};
|
||||||
@@ -240,6 +251,7 @@ const updateUser = async () => {
|
|||||||
email: editUser.value.email,
|
email: editUser.value.email,
|
||||||
firstName: editUser.value.firstName,
|
firstName: editUser.value.firstName,
|
||||||
lastName: editUser.value.lastName,
|
lastName: editUser.value.lastName,
|
||||||
|
alias: editUser.value.alias,
|
||||||
};
|
};
|
||||||
if (editUser.value.password) {
|
if (editUser.value.password) {
|
||||||
payload.password = editUser.value.password;
|
payload.password = editUser.value.password;
|
||||||
@@ -273,6 +285,9 @@ const deleteUser = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getUserName = (user: any) => {
|
const getUserName = (user: any) => {
|
||||||
|
if (user.alias) {
|
||||||
|
return user.alias;
|
||||||
|
}
|
||||||
if (user.firstName || user.lastName) {
|
if (user.firstName || user.lastName) {
|
||||||
return [user.firstName, user.lastName].filter(Boolean).join(' ');
|
return [user.firstName, user.lastName].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user