diff --git a/backend/migrations/tenant/20250126000006_add_name_field_to_objects.js b/backend/migrations/tenant/20250126000006_add_name_field_to_objects.js new file mode 100644 index 0000000..dfbd2da --- /dev/null +++ b/backend/migrations/tenant/20250126000006_add_name_field_to_objects.js @@ -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'); + }); +}; diff --git a/backend/migrations/tenant/20250126000007_add_app_id_to_objects.js b/backend/migrations/tenant/20250126000007_add_app_id_to_objects.js new file mode 100644 index 0000000..fb931b6 --- /dev/null +++ b/backend/migrations/tenant/20250126000007_add_app_id_to_objects.js @@ -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'); + }); +}; diff --git a/backend/scripts/update-name-field.ts b/backend/scripts/update-name-field.ts new file mode 100644 index 0000000..6d3dd3c --- /dev/null +++ b/backend/scripts/update-name-field.ts @@ -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(); diff --git a/backend/src/object/object.service.ts b/backend/src/object/object.service.ts index 530748b..344640c 100644 --- a/backend/src/object/object.service.ts +++ b/backend/src/object/object.service.ts @@ -8,9 +8,23 @@ export class ObjectService { // Setup endpoints - Object metadata management async getObjectDefinitions(tenantId: string) { const knex = await this.tenantDbService.getTenantKnex(tenantId); - return knex('object_definitions') - .select('*') + + const objects = await knex('object_definitions') + .select('object_definitions.*') .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) { @@ -29,9 +43,19 @@ export class ObjectService { .where({ objectDefinitionId: obj.id }) .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 { ...obj, fields, + app, }; } diff --git a/frontend/components/AppSidebar.vue b/frontend/components/AppSidebar.vue index 350bc14..21584f1 100644 --- a/frontend/components/AppSidebar.vue +++ b/frontend/components/AppSidebar.vue @@ -41,16 +41,17 @@ onMounted(async () => { const noAppObjects: any[] = [] allObjects.forEach((obj: any) => { - if (obj.appId) { - if (!appMap.has(obj.appId)) { - appMap.set(obj.appId, { - id: obj.appId, + const appId = obj.app_id || obj.appId + if (appId) { + if (!appMap.has(appId)) { + appMap.set(appId, { + id: appId, name: obj.app?.name || obj.app?.label || 'Unknown App', label: obj.app?.label || obj.app?.name || 'Unknown App', objects: [] }) } - appMap.get(obj.appId)!.objects.push(obj) + appMap.get(appId)!.objects.push(obj) } else { noAppObjects.push(obj) } diff --git a/frontend/composables/useBreadcrumbs.ts b/frontend/composables/useBreadcrumbs.ts new file mode 100644 index 0000000..aadbd2b --- /dev/null +++ b/frontend/composables/useBreadcrumbs.ts @@ -0,0 +1,20 @@ +import { ref } from 'vue' + +// Shared state for breadcrumbs +const customBreadcrumbs = ref>([]) + +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 + } +} diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 51c04fe..0f1a6d4 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -1,4 +1,5 @@