Merge pull request #16164 from Budibase/BUDI-9294/project-apps-to-workspace-apps

Rename project app to workspace app
This commit is contained in:
Adria Navarro 2025-05-19 09:53:18 +02:00 committed by GitHub
commit 0b6e78c006
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 250 additions and 246 deletions

View File

@ -214,9 +214,9 @@ export const getOAuth2ConfigParams = (
/**
* Gets parameters for retrieving project apps, this is a utility function for the getDocParams function.
*/
export const getProjectAppParams = (
projectAppId?: string | null,
export const getWorkspaceAppParams = (
workspaceAppId?: string | null,
otherProps: Partial<DatabaseQueryOpts> = {}
) => {
return getDocParams(DocumentType.PROJECT_APP, projectAppId, otherProps)
return getDocParams(DocumentType.WORKSPACE_APP, workspaceAppId, otherProps)
}

View File

@ -90,8 +90,8 @@ export class ScreenStore extends BudiStore<ScreenState> {
*/
syncAppScreens(pkg: FetchAppPackageResponse) {
let screens = [...pkg.screens]
if (featureFlag.isEnabled(FeatureFlag.PROJECT_APPS)) {
screens = [...pkg.projectApps.flatMap(p => p.screens)]
if (featureFlag.isEnabled(FeatureFlag.WORKSPACE_APPS)) {
screens = [...pkg.workspaceApps.flatMap(p => p.screens)]
}
this.update(state => ({
...state,

View File

@ -184,19 +184,19 @@ async function addSampleDataDocs() {
async function addSampleDataScreen() {
const db = context.getAppDB()
let projectAppId: string | undefined
if (await features.isEnabled(FeatureFlag.PROJECT_APPS)) {
let workspaceAppId: string | undefined
if (await features.isEnabled(FeatureFlag.WORKSPACE_APPS)) {
const appMetadata = await sdk.applications.metadata.get()
const projectApp = await sdk.projectApps.create({
const workspaceApp = await sdk.workspaceApps.create({
name: appMetadata.name,
urlPrefix: "/",
icon: "Monitoring",
})
projectAppId = projectApp._id!
workspaceAppId = workspaceApp._id!
}
let screen = await createSampleDataTableScreen(projectAppId)
let screen = await createSampleDataTableScreen(workspaceAppId)
screen._id = generateScreenID()
await db.put(screen)
}
@ -282,10 +282,10 @@ export async function fetchAppPackage(
screens = await accessController.checkScreensAccess(screens, userRoleId)
}
let projectApps: FetchAppPackageResponse["projectApps"] = []
let workspaceApps: FetchAppPackageResponse["workspaceApps"] = []
if (await features.flags.isEnabled(FeatureFlag.PROJECT_APPS)) {
projectApps = await extractScreensByProjectApp(screens)
if (await features.flags.isEnabled(FeatureFlag.WORKSPACE_APPS)) {
workspaceApps = await extractScreensByWorkspaceApp(screens)
screens = []
}
@ -297,7 +297,7 @@ export async function fetchAppPackage(
ctx.body = {
application: { ...application, upgradableVersion: envCore.VERSION },
licenseType: license?.plan.type || PlanType.FREE,
projectApps,
workspaceApps,
screens,
layouts,
clientLibPath,
@ -305,20 +305,20 @@ export async function fetchAppPackage(
}
}
async function extractScreensByProjectApp(
async function extractScreensByWorkspaceApp(
screens: Screen[]
): Promise<FetchAppPackageResponse["projectApps"]> {
const result: FetchAppPackageResponse["projectApps"] = []
): Promise<FetchAppPackageResponse["workspaceApps"]> {
const result: FetchAppPackageResponse["workspaceApps"] = []
const projectApps = await sdk.projectApps.fetch()
const workspaceApps = await sdk.workspaceApps.fetch()
const screensByProjectApp = groupBy(s => s.projectAppId, screens)
for (const projectAppId of Object.keys(screensByProjectApp)) {
const projectApp = projectApps.find(p => p._id === projectAppId)
const screensByWorkspaceApp = groupBy(s => s.workspaceAppId, screens)
for (const workspaceAppId of Object.keys(screensByWorkspaceApp)) {
const workspaceApp = workspaceApps.find(p => p._id === workspaceAppId)
result.push({
...projectApp!,
screens: screensByProjectApp[projectAppId],
...workspaceApp!,
screens: screensByWorkspaceApp[workspaceAppId],
})
}

View File

@ -1,70 +0,0 @@
import {
Ctx,
InsertProjectAppRequest,
InsertProjectAppResponse,
ProjectApp,
ProjectAppResponse,
UpdateProjectAppRequest,
UpdateProjectAppResponse,
} from "@budibase/types"
import sdk from "../../sdk"
function toProjectAppResponse(projectApp: ProjectApp): ProjectAppResponse {
return {
_id: projectApp._id!,
_rev: projectApp._rev!,
name: projectApp.name,
urlPrefix: projectApp.urlPrefix,
icon: projectApp.icon,
iconColor: projectApp.iconColor,
}
}
export async function create(
ctx: Ctx<InsertProjectAppRequest, InsertProjectAppResponse>
) {
const { body } = ctx.request
const newProjectApp = {
name: body.name,
urlPrefix: body.urlPrefix,
icon: body.icon,
iconColor: body.iconColor,
}
const projectApp = await sdk.projectApps.create(newProjectApp)
ctx.status = 201
ctx.body = {
projectApp: toProjectAppResponse(projectApp),
}
}
export async function edit(
ctx: Ctx<UpdateProjectAppRequest, UpdateProjectAppResponse>
) {
const { body } = ctx.request
if (ctx.params.id !== body._id) {
ctx.throw("Path and body ids do not match", 400)
}
const toUpdate = {
_id: body._id,
_rev: body._rev,
name: body.name,
urlPrefix: body.urlPrefix,
icon: body.icon,
iconColor: body.iconColor,
}
const projectApp = await sdk.projectApps.update(toUpdate)
ctx.body = {
projectApp: toProjectAppResponse(projectApp),
}
}
export async function remove(ctx: Ctx<void, void>) {
const { id, rev } = ctx.params
await sdk.projectApps.remove(id, rev)
ctx.status = 204
}

View File

@ -0,0 +1,72 @@
import {
Ctx,
InsertWorkspaceAppRequest,
InsertWorkspaceAppResponse,
WorkspaceApp,
WorkspaceAppResponse,
UpdateWorkspaceAppRequest,
UpdateWorkspaceAppResponse,
} from "@budibase/types"
import sdk from "../../sdk"
function toWorkspaceAppResponse(
workspaceApp: WorkspaceApp
): WorkspaceAppResponse {
return {
_id: workspaceApp._id!,
_rev: workspaceApp._rev!,
name: workspaceApp.name,
urlPrefix: workspaceApp.urlPrefix,
icon: workspaceApp.icon,
iconColor: workspaceApp.iconColor,
}
}
export async function create(
ctx: Ctx<InsertWorkspaceAppRequest, InsertWorkspaceAppResponse>
) {
const { body } = ctx.request
const newWorkspaceApp = {
name: body.name,
urlPrefix: body.urlPrefix,
icon: body.icon,
iconColor: body.iconColor,
}
const workspaceApp = await sdk.workspaceApps.create(newWorkspaceApp)
ctx.status = 201
ctx.body = {
workspaceApp: toWorkspaceAppResponse(workspaceApp),
}
}
export async function edit(
ctx: Ctx<UpdateWorkspaceAppRequest, UpdateWorkspaceAppResponse>
) {
const { body } = ctx.request
if (ctx.params.id !== body._id) {
ctx.throw("Path and body ids do not match", 400)
}
const toUpdate = {
_id: body._id,
_rev: body._rev,
name: body.name,
urlPrefix: body.urlPrefix,
icon: body.icon,
iconColor: body.iconColor,
}
const workspaceApp = await sdk.workspaceApps.update(toUpdate)
ctx.body = {
workspaceApp: toWorkspaceAppResponse(workspaceApp),
}
}
export async function remove(ctx: Ctx<void, void>) {
const { id, rev } = ctx.params
await sdk.workspaceApps.remove(id, rev)
ctx.status = 204
}

View File

@ -32,7 +32,7 @@ import rowActionRoutes from "./rowAction"
import oauth2Routes from "./oauth2"
import featuresRoutes from "./features"
import aiRoutes from "./ai"
import projectApps from "./projectApp"
import workspaceApps from "./workspaceApp"
export { default as staticRoutes } from "./static"
export { default as publicRoutes } from "./public"
@ -73,7 +73,7 @@ export const mainRoutes: Router[] = [
rowActionRoutes,
oauth2Routes,
featuresRoutes,
projectApps,
workspaceApps,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this
tableRoutes,

View File

@ -3,7 +3,7 @@ import { PermissionType } from "@budibase/types"
import { middleware } from "@budibase/backend-core"
import authorized from "../../middleware/authorized"
import * as controller from "../controllers/projectApp"
import * as controller from "../controllers/workspaceApp"
import Joi from "joi"
const baseSchema = {
@ -23,7 +23,7 @@ const updateSchema = Joi.object({
...baseSchema,
})
function projectAppValidator(
function workspaceAppValidator(
schema: typeof insertSchema | typeof updateSchema
) {
return middleware.joiValidator.body(schema, { allowUnknown: false })
@ -32,19 +32,19 @@ function projectAppValidator(
const router: Router = new Router()
router.post(
"/api/projectApp",
"/api/workspaceApp",
authorized(PermissionType.BUILDER),
projectAppValidator(insertSchema),
workspaceAppValidator(insertSchema),
controller.create
)
router.put(
"/api/projectApp/:id",
"/api/workspaceApp/:id",
authorized(PermissionType.BUILDER),
projectAppValidator(updateSchema),
workspaceAppValidator(updateSchema),
controller.edit
)
router.delete(
"/api/projectApp/:id/:rev",
"/api/workspaceApp/:id/:rev",
authorized(PermissionType.BUILDER),
controller.remove
)

View File

@ -4,7 +4,7 @@ import { features } from "@budibase/backend-core"
import { AppMigration } from "."
import m20240604153647_initial_sqs from "./migrations/20240604153647_initial_sqs"
import m20250514133719_project_apps from "./migrations/20250514133719_project_apps"
import m20250514133719_workspace_apps from "./migrations/20250514133719_workspace_apps"
import { FeatureFlag } from "@budibase/types"
export const MIGRATIONS: AppMigration[] = [
@ -14,12 +14,12 @@ export const MIGRATIONS: AppMigration[] = [
func: m20240604153647_initial_sqs,
},
{
id: "20250514133719_project_apps",
func: m20250514133719_project_apps,
id: "m20250514133719_workspace_apps",
func: m20250514133719_workspace_apps,
// Disabling it, enabling it via env variables to enable development.
// Using the existing flag system would require async checks and we could run to race conditions, so this keeps is simple
disabled: !features
.getEnvFlags()
.some(f => f.key === FeatureFlag.PROJECT_APPS && f.value === true),
.some(f => f.key === FeatureFlag.WORKSPACE_APPS && f.value === true),
},
]

View File

@ -6,24 +6,26 @@ const migration = async () => {
const screens = await sdk.screens.fetch()
const application = await sdk.applications.metadata.get()
const allProjectApps = await sdk.projectApps.fetch()
let projectAppId = allProjectApps.find(p => p.name === application.name)?._id
if (!projectAppId) {
const projectApp = await sdk.projectApps.create({
const allWorkspaceApps = await sdk.workspaceApps.fetch()
let workspaceAppId = allWorkspaceApps.find(
p => p.name === application.name
)?._id
if (!workspaceAppId) {
const workspaceApp = await sdk.workspaceApps.create({
name: application.name,
urlPrefix: "/",
icon: "Monitoring",
})
projectAppId = projectApp._id
workspaceAppId = workspaceApp._id
}
const db = context.getAppDB()
await db.bulkDocs(
screens
.filter(s => !s.projectAppId)
.filter(s => !s.workspaceAppId)
.map<Screen>(s => ({
...s,
projectAppId,
workspaceAppId,
}))
)
}

View File

@ -3,14 +3,14 @@ import { Screen } from "@budibase/types"
export const SAMPLE_DATA_SCREEN_NAME = "sample-data-inventory-screen"
export function createSampleDataTableScreen(
projectAppId: string | undefined
workspaceAppId: string | undefined
): Screen {
return {
showNavigation: true,
width: "Large",
routing: { route: "/inventory", roleId: "BASIC", homeScreen: false },
name: SAMPLE_DATA_SCREEN_NAME,
projectAppId,
workspaceAppId,
props: {
_id: "c38f2b9f250fb4c33965ce47e12c02a80",
_component: "@budibase/standard-components/container",

View File

@ -1,83 +0,0 @@
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
import {
DocumentType,
ProjectApp,
SEPARATOR,
WithoutDocMetadata,
} from "@budibase/types"
async function guardName(name: string, id?: string) {
const existingProjectApps = await fetch()
if (existingProjectApps.find(p => p.name === name && p._id !== id)) {
throw new HTTPError(`App with name '${name}' is already taken.`, 400)
}
}
export async function fetch(): Promise<ProjectApp[]> {
const db = context.getAppDB()
const docs = await db.allDocs<ProjectApp>(
docIds.getProjectAppParams(null, { include_docs: true })
)
const result = docs.rows.map(r => ({
...r.doc!,
_id: r.doc!._id!,
_rev: r.doc!._rev!,
}))
return result
}
export async function get(id: string): Promise<ProjectApp | undefined> {
const db = context.getAppDB()
const projectApp = await db.tryGet<ProjectApp>(id)
return projectApp
}
export async function create(projectApp: WithoutDocMetadata<ProjectApp>) {
const db = context.getAppDB()
await guardName(projectApp.name)
const response = await db.put({
_id: `${DocumentType.PROJECT_APP}${SEPARATOR}${utils.newid()}`,
...projectApp,
})
return {
_id: response.id!,
_rev: response.rev!,
...projectApp,
}
}
export async function update(
projectApp: Omit<ProjectApp, "createdAt" | "updatedAt">
) {
const db = context.getAppDB()
await guardName(projectApp.name, projectApp._id)
const response = await db.put(projectApp)
return {
_id: response.id!,
_rev: response.rev!,
...projectApp,
}
}
export async function remove(
projectAppId: string,
_rev: string
): Promise<void> {
const db = context.getAppDB()
try {
await db.remove(projectAppId, _rev)
} catch (e: any) {
if (e.status === 404) {
throw new HTTPError(
`Project app with id '${projectAppId}' not found.`,
404
)
}
throw e
}
}

View File

@ -0,0 +1,83 @@
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
import {
DocumentType,
WorkspaceApp,
SEPARATOR,
WithoutDocMetadata,
} from "@budibase/types"
async function guardName(name: string, id?: string) {
const existingWorkspaceApps = await fetch()
if (existingWorkspaceApps.find(p => p.name === name && p._id !== id)) {
throw new HTTPError(`App with name '${name}' is already taken.`, 400)
}
}
export async function fetch(): Promise<WorkspaceApp[]> {
const db = context.getAppDB()
const docs = await db.allDocs<WorkspaceApp>(
docIds.getWorkspaceAppParams(null, { include_docs: true })
)
const result = docs.rows.map(r => ({
...r.doc!,
_id: r.doc!._id!,
_rev: r.doc!._rev!,
}))
return result
}
export async function get(id: string): Promise<WorkspaceApp | undefined> {
const db = context.getAppDB()
const workspaceApp = await db.tryGet<WorkspaceApp>(id)
return workspaceApp
}
export async function create(workspaceApp: WithoutDocMetadata<WorkspaceApp>) {
const db = context.getAppDB()
await guardName(workspaceApp.name)
const response = await db.put({
_id: `${DocumentType.WORKSPACE_APP}${SEPARATOR}${utils.newid()}`,
...workspaceApp,
})
return {
_id: response.id!,
_rev: response.rev!,
...workspaceApp,
}
}
export async function update(
workspaceApp: Omit<WorkspaceApp, "createdAt" | "updatedAt">
) {
const db = context.getAppDB()
await guardName(workspaceApp.name, workspaceApp._id)
const response = await db.put(workspaceApp)
return {
_id: response.id!,
_rev: response.rev!,
...workspaceApp,
}
}
export async function remove(
workspaceAppId: string,
_rev: string
): Promise<void> {
const db = context.getAppDB()
try {
await db.remove(workspaceAppId, _rev)
} catch (e: any) {
if (e.status === 404) {
throw new HTTPError(
`Project app with id '${workspaceAppId}' not found.`,
404
)
}
throw e
}
}

View File

@ -15,7 +15,7 @@ import * as screens from "./app/screens"
import * as common from "./app/common"
import * as oauth2 from "./app/oauth2"
import * as ai from "./app/ai"
import * as projectApps from "./app/projectApps"
import * as workspaceApps from "./app/workspaceApps"
const sdk = {
backups,
@ -35,7 +35,7 @@ const sdk = {
common,
oauth2,
ai,
projectApps,
workspaceApps,
}
// default export for TS

View File

@ -587,7 +587,7 @@ function createHomeScreen(
roleId: config.roleId,
},
name: "home-screen",
projectAppId: "projectAppId",
workspaceAppId: "workspaceAppId",
}
}

View File

@ -65,7 +65,7 @@ export function createTableScreen(
homeScreen: false,
},
name: "screen-id",
projectAppId: "projectAppId",
workspaceAppId: "workspaceAppId",
}
}
@ -117,7 +117,7 @@ export function createViewScreen(view: ViewV2): Screen {
homeScreen: false,
},
name: "view-id",
projectAppId: "projectAppId",
workspaceAppId: "workspaceAppId",
}
}
@ -173,6 +173,6 @@ export function createQueryScreen(datasourceId: string, query: Query): Screen {
homeScreen: false,
},
name: "screen-id",
projectAppId: "projectAppId",
workspaceAppId: "workspaceAppId",
}
}

View File

@ -1,5 +1,5 @@
import type { PlanType } from "../../../sdk"
import type { Layout, App, Screen, ProjectApp } from "../../../documents"
import type { Layout, App, Screen, WorkspaceApp } from "../../../documents"
import { ReadStream } from "fs"
export interface SyncAppResponse {
@ -35,7 +35,7 @@ export interface FetchAppDefinitionResponse {
libraries: string[]
}
interface ProjectAppResponse extends ProjectApp {
interface WorkspaceAppResponse extends WorkspaceApp {
screens: Screen[]
}
@ -43,7 +43,7 @@ export interface FetchAppPackageResponse {
application: App
licenseType: PlanType
screens: Screen[]
projectApps: ProjectAppResponse[]
workspaceApps: WorkspaceAppResponse[]
layouts: Layout[]
clientLibPath: string
hasLock: boolean

View File

@ -11,7 +11,7 @@ export * from "./layout"
export * from "./metadata"
export * from "./oauth2"
export * from "./permission"
export * from "./projectApp"
export * from "./workspaceApp"
export * from "./query"
export * from "./role"
export * from "./rowAction"

View File

@ -1,32 +0,0 @@
export interface ProjectAppResponse {
_id: string
_rev: string
name: string
urlPrefix: string
icon: string
iconColor?: string
}
export interface InsertProjectAppRequest {
name: string
urlPrefix: string
icon: string
iconColor: string
}
export interface InsertProjectAppResponse {
projectApp: ProjectAppResponse
}
export interface UpdateProjectAppRequest {
_id: string
_rev: string
name: string
urlPrefix: string
icon: string
iconColor: string
}
export interface UpdateProjectAppResponse {
projectApp: ProjectAppResponse
}

View File

@ -0,0 +1,32 @@
export interface WorkspaceAppResponse {
_id: string
_rev: string
name: string
urlPrefix: string
icon: string
iconColor?: string
}
export interface InsertWorkspaceAppRequest {
name: string
urlPrefix: string
icon: string
iconColor: string
}
export interface InsertWorkspaceAppResponse {
workspaceApp: WorkspaceAppResponse
}
export interface UpdateWorkspaceAppRequest {
_id: string
_rev: string
name: string
urlPrefix: string
icon: string
iconColor: string
}
export interface UpdateWorkspaceAppResponse {
workspaceApp: WorkspaceAppResponse
}

View File

@ -9,7 +9,7 @@ export * from "./layout"
export * from "./links"
export * from "./metadata"
export * from "./oauth2"
export * from "./projectApp"
export * from "./workspaceApp"
export * from "./query"
export * from "./role"
export * from "./row"

View File

@ -29,7 +29,7 @@ export interface Screen extends Document {
pluginAdded?: boolean
onLoad?: EventHandler[]
variant?: ScreenVariant
projectAppId?: string
workspaceAppId?: string
}
export interface ScreenRoutesViewOutput extends Document {

View File

@ -1,6 +1,6 @@
import { Document } from "../document"
export interface ProjectApp extends Document {
export interface WorkspaceApp extends Document {
name: string
urlPrefix: string
icon: string

View File

@ -43,7 +43,7 @@ export enum DocumentType {
OAUTH2_CONFIG = "oauth2",
OAUTH2_CONFIG_LOG = "oauth2log",
AGENT_CHAT = "agentchat",
PROJECT_APP = "project_app",
WORKSPACE_APP = "workspace_app",
}
// Because DocumentTypes can overlap, we need to make sure that we search

View File

@ -4,7 +4,7 @@ export enum FeatureFlag {
AI_JS_GENERATION = "AI_JS_GENERATION",
AI_TABLE_GENERATION = "AI_TABLE_GENERATION",
AI_AGENTS = "AI_AGENTS",
PROJECT_APPS = "PROJECT_APPS",
WORKSPACE_APPS = "WORKSPACE_APPS",
// Account-portal
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
@ -15,7 +15,7 @@ export const FeatureFlagDefaults: Record<FeatureFlag, boolean> = {
[FeatureFlag.AI_JS_GENERATION]: false,
[FeatureFlag.AI_TABLE_GENERATION]: false,
[FeatureFlag.AI_AGENTS]: false,
[FeatureFlag.PROJECT_APPS]: false,
[FeatureFlag.WORKSPACE_APPS]: false,
// Account-portal
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,