Added better display of bread crums and side bar menus for apps and objects
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('object_definitions', (table) => {
|
||||||
|
table.string('nameField', 255).comment('API name of the field to use as record display name');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.table('object_definitions', (table) => {
|
||||||
|
table.dropColumn('nameField');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('object_definitions', (table) => {
|
||||||
|
table.uuid('app_id').nullable()
|
||||||
|
.comment('Optional: App that this object belongs to');
|
||||||
|
|
||||||
|
table
|
||||||
|
.foreign('app_id')
|
||||||
|
.references('id')
|
||||||
|
.inTable('apps')
|
||||||
|
.onDelete('SET NULL');
|
||||||
|
|
||||||
|
table.index(['app_id']);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.table('object_definitions', (table) => {
|
||||||
|
table.dropForeign('app_id');
|
||||||
|
table.dropIndex('app_id');
|
||||||
|
table.dropColumn('app_id');
|
||||||
|
});
|
||||||
|
};
|
||||||
72
backend/scripts/update-name-field.ts
Normal file
72
backend/scripts/update-name-field.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { getCentralPrisma } from '../src/prisma/central-prisma.service';
|
||||||
|
import * as knex from 'knex';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
function decrypt(text: string): string {
|
||||||
|
const parts = text.split(':');
|
||||||
|
const iv = Buffer.from(parts.shift()!, 'hex');
|
||||||
|
const encryptedText = Buffer.from(parts.join(':'), 'hex');
|
||||||
|
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
|
||||||
|
const decipher = crypto.createDecipheriv(
|
||||||
|
'aes-256-cbc',
|
||||||
|
key,
|
||||||
|
iv,
|
||||||
|
);
|
||||||
|
let decrypted = decipher.update(encryptedText);
|
||||||
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||||
|
return decrypted.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateNameField() {
|
||||||
|
const centralPrisma = getCentralPrisma();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find tenant1
|
||||||
|
const tenant = await centralPrisma.tenant.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ id: 'tenant1' },
|
||||||
|
{ slug: 'tenant1' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tenant) {
|
||||||
|
console.error('❌ Tenant tenant1 not found');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 Tenant: ${tenant.name} (${tenant.slug})`);
|
||||||
|
console.log(`📊 Database: ${tenant.dbName}`);
|
||||||
|
|
||||||
|
// Decrypt password
|
||||||
|
const password = decrypt(tenant.dbPassword);
|
||||||
|
|
||||||
|
// Create connection
|
||||||
|
const tenantKnex = knex.default({
|
||||||
|
client: 'mysql2',
|
||||||
|
connection: {
|
||||||
|
host: tenant.dbHost,
|
||||||
|
port: tenant.dbPort,
|
||||||
|
user: tenant.dbUsername,
|
||||||
|
password: password,
|
||||||
|
database: tenant.dbName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update Account object
|
||||||
|
await tenantKnex('object_definitions')
|
||||||
|
.where({ apiName: 'Account' })
|
||||||
|
.update({ nameField: 'name' });
|
||||||
|
|
||||||
|
console.log('✅ Updated Account object nameField to "name"');
|
||||||
|
|
||||||
|
await tenantKnex.destroy();
|
||||||
|
await centralPrisma.$disconnect();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNameField();
|
||||||
@@ -8,9 +8,23 @@ export class ObjectService {
|
|||||||
// Setup endpoints - Object metadata management
|
// Setup endpoints - Object metadata management
|
||||||
async getObjectDefinitions(tenantId: string) {
|
async getObjectDefinitions(tenantId: string) {
|
||||||
const knex = await this.tenantDbService.getTenantKnex(tenantId);
|
const knex = await this.tenantDbService.getTenantKnex(tenantId);
|
||||||
return knex('object_definitions')
|
|
||||||
.select('*')
|
const objects = await knex('object_definitions')
|
||||||
|
.select('object_definitions.*')
|
||||||
.orderBy('label', 'asc');
|
.orderBy('label', 'asc');
|
||||||
|
|
||||||
|
// Fetch app information for objects that have app_id
|
||||||
|
for (const obj of objects) {
|
||||||
|
if (obj.app_id) {
|
||||||
|
const app = await knex('apps')
|
||||||
|
.where({ id: obj.app_id })
|
||||||
|
.select('id', 'slug', 'label', 'description')
|
||||||
|
.first();
|
||||||
|
obj.app = app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getObjectDefinition(tenantId: string, apiName: string) {
|
async getObjectDefinition(tenantId: string, apiName: string) {
|
||||||
@@ -29,9 +43,19 @@ export class ObjectService {
|
|||||||
.where({ objectDefinitionId: obj.id })
|
.where({ objectDefinitionId: obj.id })
|
||||||
.orderBy('label', 'asc');
|
.orderBy('label', 'asc');
|
||||||
|
|
||||||
|
// Get app information if object belongs to an app
|
||||||
|
let app = null;
|
||||||
|
if (obj.app_id) {
|
||||||
|
app = await knex('apps')
|
||||||
|
.where({ id: obj.app_id })
|
||||||
|
.select('id', 'slug', 'label', 'description')
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
fields,
|
fields,
|
||||||
|
app,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,16 +41,17 @@ onMounted(async () => {
|
|||||||
const noAppObjects: any[] = []
|
const noAppObjects: any[] = []
|
||||||
|
|
||||||
allObjects.forEach((obj: any) => {
|
allObjects.forEach((obj: any) => {
|
||||||
if (obj.appId) {
|
const appId = obj.app_id || obj.appId
|
||||||
if (!appMap.has(obj.appId)) {
|
if (appId) {
|
||||||
appMap.set(obj.appId, {
|
if (!appMap.has(appId)) {
|
||||||
id: obj.appId,
|
appMap.set(appId, {
|
||||||
|
id: appId,
|
||||||
name: obj.app?.name || obj.app?.label || 'Unknown App',
|
name: obj.app?.name || obj.app?.label || 'Unknown App',
|
||||||
label: obj.app?.label || obj.app?.name || 'Unknown App',
|
label: obj.app?.label || obj.app?.name || 'Unknown App',
|
||||||
objects: []
|
objects: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
appMap.get(obj.appId)!.objects.push(obj)
|
appMap.get(appId)!.objects.push(obj)
|
||||||
} else {
|
} else {
|
||||||
noAppObjects.push(obj)
|
noAppObjects.push(obj)
|
||||||
}
|
}
|
||||||
|
|||||||
20
frontend/composables/useBreadcrumbs.ts
Normal file
20
frontend/composables/useBreadcrumbs.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// Shared state for breadcrumbs
|
||||||
|
const customBreadcrumbs = ref<Array<{ name: string; path?: string; isLast?: boolean }>>([])
|
||||||
|
|
||||||
|
export function useBreadcrumbs() {
|
||||||
|
const setBreadcrumbs = (crumbs: Array<{ name: string; path?: string; isLast?: boolean }>) => {
|
||||||
|
customBreadcrumbs.value = crumbs
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearBreadcrumbs = () => {
|
||||||
|
customBreadcrumbs.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
breadcrumbs: customBreadcrumbs,
|
||||||
|
setBreadcrumbs,
|
||||||
|
clearBreadcrumbs
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
import AppSidebar from '@/components/AppSidebar.vue'
|
import AppSidebar from '@/components/AppSidebar.vue'
|
||||||
import AIChatBar from '@/components/AIChatBar.vue'
|
import AIChatBar from '@/components/AIChatBar.vue'
|
||||||
import {
|
import {
|
||||||
@@ -13,8 +14,15 @@ import { Separator } from '@/components/ui/separator'
|
|||||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
|
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { breadcrumbs: customBreadcrumbs } = useBreadcrumbs()
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
|
// If custom breadcrumbs are set by the page, use those
|
||||||
|
if (customBreadcrumbs.value.length > 0) {
|
||||||
|
return customBreadcrumbs.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, fall back to URL-based breadcrumbs
|
||||||
const paths = route.path.split('/').filter(Boolean)
|
const paths = route.path.split('/').filter(Boolean)
|
||||||
return paths.map((path, index) => ({
|
return paths.map((path, index) => ({
|
||||||
name: path.charAt(0).toUpperCase() + path.slice(1),
|
name: path.charAt(0).toUpperCase() + path.slice(1),
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ const router = useRouter()
|
|||||||
const { api } = useApi()
|
const { api } = useApi()
|
||||||
const { buildListViewConfig, buildDetailViewConfig, buildEditViewConfig } = useFields()
|
const { buildListViewConfig, buildDetailViewConfig, buildEditViewConfig } = useFields()
|
||||||
|
|
||||||
|
// Use breadcrumbs composable
|
||||||
|
const { setBreadcrumbs } = useBreadcrumbs()
|
||||||
|
|
||||||
// Get object API name from route (case-insensitive)
|
// Get object API name from route (case-insensitive)
|
||||||
const objectApiName = computed(() => {
|
const objectApiName = computed(() => {
|
||||||
const name = route.params.objectName as string
|
const name = route.params.objectName as string
|
||||||
@@ -45,6 +48,59 @@ const {
|
|||||||
handleSave,
|
handleSave,
|
||||||
} = useViewState(`/runtime/objects/${objectApiName.value}/records`)
|
} = useViewState(`/runtime/objects/${objectApiName.value}/records`)
|
||||||
|
|
||||||
|
// Compute breadcrumbs based on the current route and object data
|
||||||
|
const updateBreadcrumbs = () => {
|
||||||
|
if (!objectDefinition.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const crumbs: Array<{ name: string; path?: string; isLast?: boolean }> = []
|
||||||
|
|
||||||
|
// Add app breadcrumb if object belongs to an app
|
||||||
|
if (objectDefinition.value?.app) {
|
||||||
|
crumbs.push({
|
||||||
|
name: objectDefinition.value.app.label || objectDefinition.value.app.name,
|
||||||
|
path: undefined, // No path for app grouping
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add object breadcrumb - always use plural
|
||||||
|
const objectLabel = objectDefinition.value?.pluralLabel || objectDefinition.value?.label || objectApiName.value
|
||||||
|
|
||||||
|
crumbs.push({
|
||||||
|
name: objectLabel,
|
||||||
|
path: `/${objectApiName.value.toLowerCase()}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add record name if viewing/editing a specific record
|
||||||
|
if (recordId.value && recordId.value !== 'new' && currentRecord.value) {
|
||||||
|
const nameField = objectDefinition.value?.nameField
|
||||||
|
let recordName = recordId.value // fallback to ID
|
||||||
|
|
||||||
|
// Try to get the display name from the nameField
|
||||||
|
if (nameField && currentRecord.value[nameField]) {
|
||||||
|
recordName = currentRecord.value[nameField]
|
||||||
|
}
|
||||||
|
|
||||||
|
crumbs.push({
|
||||||
|
name: recordName,
|
||||||
|
isLast: true,
|
||||||
|
})
|
||||||
|
} else if (recordId.value === 'new') {
|
||||||
|
crumbs.push({
|
||||||
|
name: 'New',
|
||||||
|
isLast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setBreadcrumbs(crumbs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for changes that affect breadcrumbs
|
||||||
|
watch([objectDefinition, currentRecord, recordId], () => {
|
||||||
|
updateBreadcrumbs()
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
// View configs
|
// View configs
|
||||||
const listConfig = computed(() => {
|
const listConfig = computed(() => {
|
||||||
if (!objectDefinition.value) return null
|
if (!objectDefinition.value) return null
|
||||||
@@ -162,6 +218,9 @@ onMounted(async () => {
|
|||||||
} else if (recordId.value && recordId.value !== 'new') {
|
} else if (recordId.value && recordId.value !== 'new') {
|
||||||
await fetchRecord(recordId.value)
|
await fetchRecord(recordId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update breadcrumbs after data is loaded
|
||||||
|
updateBreadcrumbs()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user