diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 4d0653208c..c5d953021f 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -19,7 +19,6 @@ import { makeComponentUnique, } from "../componentUtils" import { Helpers } from "@budibase/bbui" -import { DefaultAppTheme, LAYOUT_NAMES } from "../../constants" import { Utils } from "@budibase/frontend-core" const INITIAL_FRONTEND_STATE = { @@ -125,35 +124,6 @@ export const getFrontendStore = () => { await integrations.init() await queries.init() await tables.init() - - // Add navigation settings to old apps - if (!application.navigation) { - const layout = layouts.find(x => x._id === LAYOUT_NAMES.MASTER.PRIVATE) - const customTheme = application.customTheme - let navigationSettings = { - navigation: "Top", - title: application.name, - navWidth: "Large", - navBackground: - customTheme?.navBackground || DefaultAppTheme.navBackground, - navTextColor: - customTheme?.navTextColor || DefaultAppTheme.navTextColor, - } - if (layout) { - navigationSettings.hideLogo = layout.props.hideLogo - navigationSettings.hideTitle = layout.props.hideTitle - navigationSettings.title = layout.props.title || application.name - navigationSettings.logoUrl = layout.props.logoUrl - navigationSettings.links = layout.props.links - navigationSettings.navigation = layout.props.navigation || "Top" - navigationSettings.sticky = layout.props.sticky - navigationSettings.navWidth = layout.props.width || "Large" - if (navigationSettings.navigation === "None") { - navigationSettings.navigation = "Top" - } - } - await store.actions.navigation.save(navigationSettings) - } }, theme: { save: async theme => { diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index ae4a60d78d..926fe0ec52 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -47,7 +47,14 @@ import { checkAppMetadata } from "../../automations/logging" import { getUniqueRows } from "../../utilities/usageQuota/rows" import { quotas } from "@budibase/pro" import { errors, events, migrations } from "@budibase/backend-core" -import { App, MigrationType } from "@budibase/types" +import { + App, + Layout, + Screen, + MigrationType, + AppNavigation, +} from "@budibase/types" +import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" const URL_REGEX_SLASH = /\/|\\/g @@ -243,27 +250,19 @@ const performAppCreate = async (ctx: any) => { } const instance = await createInstance(instanceConfig) const appId = instance._id - const db = context.getAppDB() - let _rev - try { - // if template there will be an existing doc - const existing = await db.get(DocumentType.APP_METADATA) - _rev = existing._rev - } catch (err) { - // nothing to do - } - const newApplication: App = { + + let newApplication: App = { _id: DocumentType.APP_METADATA, - _rev, - appId: instance._id, + _rev: undefined, + appId, type: "app", version: packageJson.version, componentLibraries: ["@budibase/standard-components"], name: name, url: url, - template: ctx.request.body.template, - instance: instance, + template: templateKey, + instance, tenantId: getTenantId(), updatedAt: new Date().toISOString(), createdAt: new Date().toISOString(), @@ -285,6 +284,36 @@ const performAppCreate = async (ctx: any) => { buttonBorderRadius: "16px", }, } + + // If we used a template or imported an app there will be an existing doc. + // Fetch and migrate some metadata from the existing app. + try { + const existing: App = await db.get(DocumentType.APP_METADATA) + const keys: (keyof App)[] = [ + "_rev", + "navigation", + "theme", + "customTheme", + "icon", + ] + keys.forEach(key => { + if (existing[key]) { + // @ts-ignore + newApplication[key] = existing[key] + } + }) + + // Migrate navigation settings and screens if required + if (existing && !existing.navigation) { + const navigation = await migrateAppNavigation() + if (navigation) { + newApplication.navigation = navigation + } + } + } catch (err) { + // Nothing to do + } + const response = await db.put(newApplication, { force: true }) newApplication._rev = response.rev @@ -567,3 +596,55 @@ const updateAppPackage = async (appPackage: any, appId: any) => { return newAppPackage }) } + +const migrateAppNavigation = async () => { + const db = context.getAppDB() + const existing: App = await db.get(DocumentType.APP_METADATA) + const layouts: Layout[] = await getLayouts() + const screens: Screen[] = await getScreens() + + // Migrate all screens, removing custom layouts + for (let screen of screens) { + if (!screen.layoutId) { + return + } + const layout = layouts.find(layout => layout._id === screen.layoutId) + screen.layoutId = undefined + screen.showNavigation = layout?.props.navigation !== "None" + screen.width = layout?.props.width || "Large" + await db.put(screen) + } + + // Migrate layout navigation settings + const { name, customTheme } = existing + const layout = layouts?.find( + (layout: Layout) => layout._id === BASE_LAYOUT_PROP_IDS.PRIVATE + ) + if (layout) { + let navigationSettings: any = { + navigation: "Top", + title: name, + navWidth: "Large", + navBackground: + customTheme?.navBackground || "var(--spectrum-global-color-gray-50)", + navTextColor: + customTheme?.navTextColor || "var(--spectrum-global-color-gray-800)", + } + if (layout) { + navigationSettings.hideLogo = layout.props.hideLogo + navigationSettings.hideTitle = layout.props.hideTitle + navigationSettings.title = layout.props.title || name + navigationSettings.logoUrl = layout.props.logoUrl + navigationSettings.links = layout.props.links + navigationSettings.navigation = layout.props.navigation || "Top" + navigationSettings.sticky = layout.props.sticky + navigationSettings.navWidth = layout.props.width || "Large" + if (navigationSettings.navigation === "None") { + navigationSettings.navigation = "Top" + } + } + return navigationSettings + } else { + return null + } +} diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.js index 2620547298..dcfc2c6d9b 100644 --- a/packages/server/src/api/routes/tests/application.spec.js +++ b/packages/server/src/api/routes/tests/application.spec.js @@ -17,6 +17,7 @@ const { checkBuilderEndpoint, } = require("./utilities/TestFunctions") const setup = require("./utilities") +const { basicScreen, basicLayout } = setup.structures const { AppStatus } = require("../../../db/utils") const { events } = require("@budibase/backend-core") @@ -81,6 +82,31 @@ describe("/applications", () => { body: { name: "My App" }, }) }) + + it("migrates navigation settings from old apps", async () => { + const res = await request + .post("/api/applications") + .field("name", "Old App") + .field("useTemplate", "true") + .set(config.defaultHeaders()) + .attach("templateFile", "src/api/routes/tests/data/old-app.txt") + .expect("Content-Type", /json/) + .expect(200) + expect(res.body._id).toBeDefined() + expect(res.body.navigation).toBeDefined() + expect(res.body.navigation.hideLogo).toBe(true) + expect(res.body.navigation.title).toBe("Custom Title") + expect(res.body.navigation.hideLogo).toBe(true) + expect(res.body.navigation.navigation).toBe("Left") + expect(res.body.navigation.navBackground).toBe( + "var(--spectrum-global-color-blue-600)" + ) + expect(res.body.navigation.navTextColor).toBe( + "var(--spectrum-global-color-gray-50)" + ) + expect(events.app.created).toBeCalledTimes(1) + expect(events.app.fileImported).toBeCalledTimes(1) + }) }) describe("fetch", () => { diff --git a/packages/server/src/api/routes/tests/data/old-app.txt b/packages/server/src/api/routes/tests/data/old-app.txt new file mode 100644 index 0000000000..42732092e5 --- /dev/null +++ b/packages/server/src/api/routes/tests/data/old-app.txt @@ -0,0 +1,3 @@ +{"version":"1.2.9","db_type":"http","start_time":"2022-08-31T13:18:50.740Z","db_info":{"db_name":"app_dev_default_c45f987cfa274060840878557ff2f547","purge_seq":"0-g1AAAABXeJzLYWBgYMpgTmEQTM4vTc5ISXIwNDLXMwBCwxyQVB4LkGRoAFL_gSArkQGP2kSGpHqIoiwAtOgYRA","update_seq":"28-g1AAAABXeJzLYWBgYMpgTmEQTM4vTc5ISXIwNDLXMwBCwxyQVB4LkGRoAFL_gSArkQ2P2kSGpHqwIrEsALX-GGA","sizes":{"file":233890,"external":5856,"active":19326},"props":{},"doc_del_count":0,"doc_count":7,"disk_format_version":8,"compact_running":false,"cluster":{"q":2,"n":1,"w":1,"r":1},"instance_start_time":"0","host":"http://localhost:10000/db/app_dev_default_c45f987cfa274060840878557ff2f547/","auto_compaction":false,"adapter":"http"}} +{"docs":[{"_id":"_design/database","_rev":"4-afb0361e5ebb6b7e464c6c146ac3f559","views":{"by_link":{"map":"function (doc) {\n // everything in this must remain constant as its going to Pouch, no external variables\n if (doc.type === \"link\") {\n let doc1 = doc.doc1;\n let doc2 = doc.doc2;\n emit([doc1.tableId, doc1.rowId], {\n id: doc2.rowId,\n thisId: doc1.rowId,\n fieldName: doc1.fieldName,\n });\n // if linking to same table can't emit twice\n if (doc1.tableId !== doc2.tableId) {\n emit([doc2.tableId, doc2.rowId], {\n id: doc1.rowId,\n thisId: doc2.rowId,\n fieldName: doc2.fieldName,\n });\n }\n }\n }"},"screen_routes":{"map":"function(doc) {\n if (doc._id.startsWith(\"screen_\")) {\n emit(doc._id, {\n id: doc._id,\n routing: doc.routing,\n })\n }\n }"}},"indexes":{"rows":{"index":"function (doc) {\n function idx(input, prev) {\n for (let key of Object.keys(input)) {\n let idxKey = prev != null ? `${prev}.${key}` : key;\n idxKey = idxKey.replace(/ /g, \"_\");\n if (Array.isArray(input[key])) {\n for (let val of input[key]) {\n if (typeof val !== \"object\") {\n // eslint-disable-next-line no-undef\n index(idxKey, val, { store: true });\n }\n }\n }\n else if (key === \"_id\" || key === \"_rev\" || input[key] == null) {\n continue;\n }\n if (typeof input[key] === \"string\") {\n // eslint-disable-next-line no-undef\n index(idxKey, input[key].toLowerCase(), { store: true });\n }\n else if (typeof input[key] !== \"object\") {\n // eslint-disable-next-line no-undef\n index(idxKey, input[key], { store: true });\n }\n else {\n idx(input[key], idxKey);\n }\n }\n }\n if (doc._id.startsWith(\"ro_\")) {\n // eslint-disable-next-line no-undef\n index(\"default\", doc._id);\n idx(doc);\n }\n }","analyzer":"keyword"}},"_revisions":{"start":4,"ids":["afb0361e5ebb6b7e464c6c146ac3f559","d1eafe2832e6554b5d348e16d6a1b0d3","6e20795c5ba67179b478f0337a585b56","0b24e44a44af45e51e562fd124ce3007"]}},{"_id":"app_metadata","_rev":"8-9c8540f72e647e3ca7108978e862ff17","appId":"app_dev_default_c45f987cfa274060840878557ff2f547","type":"app","version":"1.0.79-alpha.7","componentLibraries":["@budibase/standard-components"],"name":"Old App","url":"/old%20app","instance":{"_id":"app_dev_default_c45f987cfa274060840878557ff2f547"},"tenantId":"default","updatedAt":"2022-08-31T13:18:05.273Z","createdAt":"2022-08-31T13:16:36.639Z","status":"development","customTheme":{"navBackground":"var(--spectrum-global-color-blue-600)","navTextColor":"var(--spectrum-global-color-gray-50)","buttonBorderRadius":"4px","primaryColor":"var(--spectrum-global-color-red-600)"},"_revisions":{"start":8,"ids":["9c8540f72e647e3ca7108978e862ff17","5c86ff8a96e1f3b4505e205f25a92656","75473d58cf92b61dbcdcd74157f9e2e1","ebc34a96a7ad678bae76d7d70880f4de","1c7397be4ef470d66bd0fd4b3fe4a002","89222c47e9d254b4885e1818cd0aac79","66bed1c391eabcea6ad3557d8ede5f27","0f1136445fd7c2cf73bb5cf41aaa19d2"]}},{"_id":"layout_private_master","_rev":"12-fa664c0538f1478b2a3695c8909a04e9","componentLibraries":["@budibase/standard-components"],"title":"Old App","favicon":"./_shared/favicon.png","stylesheets":[],"name":"Navigation Layout","props":{"_id":"4f569166-a4f3-47ea-a09e-6d218c75586f","_instanceName":"Navigation Layout","_component":"@budibase/standard-components/layout","_children":[{"_id":"7fcf11e4-6f5b-4085-8e0d-9f3d44c98967","_component":"@budibase/standard-components/screenslot","_instanceName":"Screen slot","_styles":{"normal":{"flex":"1 1 auto","display":"flex","flex-direction":"column","justify-content":"flex-start","align-items":"stretch"},"hover":{},"active":{},"selected":{}},"_children":[]}],"_styles":{"active":{},"hover":{},"normal":{},"selected":{}},"title":"Custom Title","navigation":"Left","width":"Small","links":[{"text":"Home","url":"/"}],"hideLogo":true,"hideTitle":false},"_revisions":{"start":12,"ids":["fa664c0538f1478b2a3695c8909a04e9","54b4084d5c10369ecd7a3b33d4cb7775","61e5d376e9f00d20d9905bf011680943","f222416fe5bcf0085ceb72bdc94327c1","b6d9abe1caa1a8ffec0b1e8cb7324c4b","85a347b8c8acb5ad4e31929384062e05","a7ce68d677f2fa7e9a0d54b2fb4275bb","83811e8befcdac752cdbe16cd8b33fa0","362cb9ad2c87ef9c92c58c0a41ed69d4","0a3bba6cdc98539abb3a4e98e13ef98a","d964020cc8907b37047bcd2bd1387735","156b45a34bbee17b6a4a97bfaaeb6f90"]}},{"_id":"layout_public_master","_rev":"1-c7d57c9c42e9e8f988d82f0e6cc7d32c","componentLibraries":["@budibase/standard-components"],"title":"Old App","favicon":"./_shared/favicon.png","stylesheets":[],"name":"Empty Layout","props":{"_id":"3723ffa1-f9e0-4c05-8013-98195c788ed6","_instanceName":"Empty Layout","_component":"@budibase/standard-components/layout","_children":[{"_id":"7fcf11e4-6f5b-4085-8e0d-9f3d44c98967","_component":"@budibase/standard-components/screenslot","_instanceName":"Screen slot","_styles":{"normal":{"flex":"1 1 auto","display":"flex","flex-direction":"column","justify-content":"flex-start","align-items":"stretch"},"hover":{},"active":{},"selected":{}},"_children":[]}],"_styles":{"active":{},"hover":{},"normal":{},"selected":{}},"navigation":"None","width":"Large","links":[{"text":"Home","url":"/"}]}},{"_id":"screen_c483ef802772479196a28c8a6eafe6a5","_rev":"1-db29ceb02c9c6c1ade847c8086d28a70","layoutId":"layout_private_master","props":{"_id":"cafd93b74e18b45a49a58c894d7ef29b4","_component":"@budibase/standard-components/container","_styles":{"normal":{},"hover":{},"active":{},"selected":{}},"_children":[],"_instanceName":"Home","direction":"column","hAlign":"stretch","vAlign":"top","size":"grow","gap":"M"},"routing":{"route":"/home","roleId":"BASIC"},"name":"cafd93b74e18b45a49a58c894d7ef29b4","template":"createFromScratch"},{"_id":"ta_users","_rev":"1-30a4344f056c24cf776d5736eb3c7ed5","type":"table","views":{},"name":"Users","schema":{"email":{"type":"string","constraints":{"type":"string","email":true,"length":{"maximum":""},"presence":true},"fieldName":"email","name":"email"},"firstName":{"name":"firstName","fieldName":"firstName","type":"string","constraints":{"type":"string","presence":false}},"lastName":{"name":"lastName","fieldName":"lastName","type":"string","constraints":{"type":"string","presence":false}},"roleId":{"fieldName":"roleId","name":"roleId","type":"options","constraints":{"type":"string","presence":false,"inclusion":["ADMIN","POWER","BASIC","PUBLIC"]}},"status":{"fieldName":"status","name":"status","type":"options","constraints":{"type":"string","presence":false,"inclusion":["active","inactive"]}}},"primaryDisplay":"email"}]} +{"seq":"28-g1AAAACbeJzLYWBgYMpgTmEQTM4vTc5ISXIwNDLXMwBCwxyQVCJDUv3___-zMpgTxXKBAuxJiZYGxgZG2DTgMSaPBUgyNACp_1DT2MCmGRmZWBglG2PTlwUAEaUn-w"} diff --git a/packages/types/src/documents/app/app.ts b/packages/types/src/documents/app/app.ts index 1def994697..a03875aa50 100644 --- a/packages/types/src/documents/app/app.ts +++ b/packages/types/src/documents/app/app.ts @@ -14,14 +14,11 @@ export interface App extends Document { tenantId: string status: string theme?: string - customTheme?: { - buttonBorderRadius?: string - primaryColor?: string - primaryColorHover?: string - } + customTheme?: AppCustomTheme revertableVersion?: string navigation?: AppNavigation automationErrors?: AppMetadataErrors + icon?: AppIcon } export interface AppInstance { @@ -47,3 +44,18 @@ export interface AppNavigationLink { id?: string roleId?: string } + +export interface AppCustomTheme { + buttonBorderRadius?: string + primaryColor?: string + primaryColorHover?: string + + // Used to exist before new design UI + navTextColor?: string + navBackground?: string +} + +export interface AppIcon { + name: string + color: string +} diff --git a/packages/types/src/documents/app/layout.ts b/packages/types/src/documents/app/layout.ts index 85ca4b7e94..db046e3d92 100644 --- a/packages/types/src/documents/app/layout.ts +++ b/packages/types/src/documents/app/layout.ts @@ -1,3 +1,5 @@ import { Document } from "../document" -export interface Layout extends Document {} +export interface Layout extends Document { + props: any +} diff --git a/packages/types/src/documents/app/screen.ts b/packages/types/src/documents/app/screen.ts index 1eead16aa0..98db658aa6 100644 --- a/packages/types/src/documents/app/screen.ts +++ b/packages/types/src/documents/app/screen.ts @@ -1,7 +1,7 @@ import { Document } from "../document" export interface Screen extends Document { - layoutId: string + layoutId?: string showNavigation?: boolean width?: string routing: { diff --git a/packages/types/src/sdk/events/screen.ts b/packages/types/src/sdk/events/screen.ts index 0d9f8cb32c..23c468b7ad 100644 --- a/packages/types/src/sdk/events/screen.ts +++ b/packages/types/src/sdk/events/screen.ts @@ -2,12 +2,12 @@ import { BaseEvent } from "./event" export interface ScreenCreatedEvent extends BaseEvent { screenId: string - layoutId: string + layoutId?: string roleId: string } export interface ScreenDeletedEvent extends BaseEvent { screenId: string - layoutId: string + layoutId?: string roleId: string }