diff --git a/.gitignore b/.gitignore index d9f8ef98..41b308ce 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules __pycache__ studio/docs/current studio/public/frontend -studio/www/studio.html \ No newline at end of file +studio/www/studio.html +studio/templates/generators/renderer.html \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index b518babd..4dbc149f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,12 +9,13 @@
- -
- + diff --git a/frontend/package.json b/frontend/package.json index 65fdd768..b7da0aef 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,8 +4,9 @@ "version": "0.0.0", "scripts": { "dev": "vite", - "build": "vite build --base=/assets/studio/frontend/ && yarn copy-html-entry", + "build": "vite build --base=/assets/studio/frontend/ && yarn copy-html-entry && yarn copy-app-renderer", "copy-html-entry": "cp ../studio/public/frontend/index.html ../studio/www/studio.html", + "copy-app-renderer": "cp ../studio/public/frontend/renderer.html ../studio/templates/generators/renderer.html", "serve": "vite preview", "extract-frappeui-types": "tsx src/scripts/tsToJSONGenerator.ts frappeui", "extract-studio-types": "tsx src/scripts/tsToJSONGenerator.ts studio" diff --git a/frontend/renderer.html b/frontend/renderer.html new file mode 100644 index 00000000..41c505d7 --- /dev/null +++ b/frontend/renderer.html @@ -0,0 +1,29 @@ + + + + + + + + {{ app_title }} + + +
+
+
+ + {% if is_developer_mode %} + + + + {% else %} + + {% endif %} + + \ No newline at end of file diff --git a/frontend/src/assets/Inter/Inter-Black.woff b/frontend/src/assets/Inter/Inter-Black.woff deleted file mode 100644 index c7737ed3..00000000 Binary files a/frontend/src/assets/Inter/Inter-Black.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Black.woff2 b/frontend/src/assets/Inter/Inter-Black.woff2 deleted file mode 100644 index b16b995b..00000000 Binary files a/frontend/src/assets/Inter/Inter-Black.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BlackItalic.woff b/frontend/src/assets/Inter/Inter-BlackItalic.woff deleted file mode 100644 index b5f14476..00000000 Binary files a/frontend/src/assets/Inter/Inter-BlackItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BlackItalic.woff2 b/frontend/src/assets/Inter/Inter-BlackItalic.woff2 deleted file mode 100644 index a3f1b70c..00000000 Binary files a/frontend/src/assets/Inter/Inter-BlackItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Bold.woff b/frontend/src/assets/Inter/Inter-Bold.woff deleted file mode 100644 index e3845558..00000000 Binary files a/frontend/src/assets/Inter/Inter-Bold.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Bold.woff2 b/frontend/src/assets/Inter/Inter-Bold.woff2 deleted file mode 100644 index 835dd497..00000000 Binary files a/frontend/src/assets/Inter/Inter-Bold.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BoldItalic.woff b/frontend/src/assets/Inter/Inter-BoldItalic.woff deleted file mode 100644 index ffac3f59..00000000 Binary files a/frontend/src/assets/Inter/Inter-BoldItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-BoldItalic.woff2 b/frontend/src/assets/Inter/Inter-BoldItalic.woff2 deleted file mode 100644 index 1a41a14f..00000000 Binary files a/frontend/src/assets/Inter/Inter-BoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBold.woff b/frontend/src/assets/Inter/Inter-ExtraBold.woff deleted file mode 100644 index 885ac94f..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBold.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBold.woff2 b/frontend/src/assets/Inter/Inter-ExtraBold.woff2 deleted file mode 100644 index ae956b15..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBold.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff b/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff deleted file mode 100644 index d6cf8623..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2 b/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2 deleted file mode 100644 index 86578995..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraBoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLight.woff b/frontend/src/assets/Inter/Inter-ExtraLight.woff deleted file mode 100644 index ff769193..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLight.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLight.woff2 b/frontend/src/assets/Inter/Inter-ExtraLight.woff2 deleted file mode 100644 index 694b2df9..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLight.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff b/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff deleted file mode 100644 index c6ed13a4..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2 b/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2 deleted file mode 100644 index 9a7bd110..00000000 Binary files a/frontend/src/assets/Inter/Inter-ExtraLightItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Italic.woff b/frontend/src/assets/Inter/Inter-Italic.woff deleted file mode 100644 index 4fdb59dc..00000000 Binary files a/frontend/src/assets/Inter/Inter-Italic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Italic.woff2 b/frontend/src/assets/Inter/Inter-Italic.woff2 deleted file mode 100644 index deca637d..00000000 Binary files a/frontend/src/assets/Inter/Inter-Italic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Light.woff b/frontend/src/assets/Inter/Inter-Light.woff deleted file mode 100644 index 42850acc..00000000 Binary files a/frontend/src/assets/Inter/Inter-Light.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Light.woff2 b/frontend/src/assets/Inter/Inter-Light.woff2 deleted file mode 100644 index 65a7dadd..00000000 Binary files a/frontend/src/assets/Inter/Inter-Light.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-LightItalic.woff b/frontend/src/assets/Inter/Inter-LightItalic.woff deleted file mode 100644 index c4ed9a94..00000000 Binary files a/frontend/src/assets/Inter/Inter-LightItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-LightItalic.woff2 b/frontend/src/assets/Inter/Inter-LightItalic.woff2 deleted file mode 100644 index 555fc559..00000000 Binary files a/frontend/src/assets/Inter/Inter-LightItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Medium.woff b/frontend/src/assets/Inter/Inter-Medium.woff deleted file mode 100644 index 495faef7..00000000 Binary files a/frontend/src/assets/Inter/Inter-Medium.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Medium.woff2 b/frontend/src/assets/Inter/Inter-Medium.woff2 deleted file mode 100644 index 871ce4ce..00000000 Binary files a/frontend/src/assets/Inter/Inter-Medium.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-MediumItalic.woff b/frontend/src/assets/Inter/Inter-MediumItalic.woff deleted file mode 100644 index 389c7a2b..00000000 Binary files a/frontend/src/assets/Inter/Inter-MediumItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-MediumItalic.woff2 b/frontend/src/assets/Inter/Inter-MediumItalic.woff2 deleted file mode 100644 index aa805799..00000000 Binary files a/frontend/src/assets/Inter/Inter-MediumItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Regular.woff b/frontend/src/assets/Inter/Inter-Regular.woff deleted file mode 100644 index fa7715d1..00000000 Binary files a/frontend/src/assets/Inter/Inter-Regular.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Regular.woff2 b/frontend/src/assets/Inter/Inter-Regular.woff2 deleted file mode 100644 index b52dd0a0..00000000 Binary files a/frontend/src/assets/Inter/Inter-Regular.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBold.woff b/frontend/src/assets/Inter/Inter-SemiBold.woff deleted file mode 100644 index 18d7749f..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBold.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBold.woff2 b/frontend/src/assets/Inter/Inter-SemiBold.woff2 deleted file mode 100644 index ece5204a..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBold.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff b/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff deleted file mode 100644 index 8ee64396..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2 b/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2 deleted file mode 100644 index b32c0ba3..00000000 Binary files a/frontend/src/assets/Inter/Inter-SemiBoldItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Thin.woff b/frontend/src/assets/Inter/Inter-Thin.woff deleted file mode 100644 index 1a22286f..00000000 Binary files a/frontend/src/assets/Inter/Inter-Thin.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-Thin.woff2 b/frontend/src/assets/Inter/Inter-Thin.woff2 deleted file mode 100644 index c56bc7ca..00000000 Binary files a/frontend/src/assets/Inter/Inter-Thin.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ThinItalic.woff b/frontend/src/assets/Inter/Inter-ThinItalic.woff deleted file mode 100644 index d8ec8373..00000000 Binary files a/frontend/src/assets/Inter/Inter-ThinItalic.woff and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-ThinItalic.woff2 b/frontend/src/assets/Inter/Inter-ThinItalic.woff2 deleted file mode 100644 index eca5608c..00000000 Binary files a/frontend/src/assets/Inter/Inter-ThinItalic.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-italic.var.woff2 b/frontend/src/assets/Inter/Inter-italic.var.woff2 deleted file mode 100644 index 1f5d9261..00000000 Binary files a/frontend/src/assets/Inter/Inter-italic.var.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter-roman.var.woff2 b/frontend/src/assets/Inter/Inter-roman.var.woff2 deleted file mode 100644 index 05621d8d..00000000 Binary files a/frontend/src/assets/Inter/Inter-roman.var.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/Inter.var.woff2 b/frontend/src/assets/Inter/Inter.var.woff2 deleted file mode 100644 index 46bb5153..00000000 Binary files a/frontend/src/assets/Inter/Inter.var.woff2 and /dev/null differ diff --git a/frontend/src/assets/Inter/inter.css b/frontend/src/assets/Inter/inter.css deleted file mode 100644 index 3ca1bbf6..00000000 --- a/frontend/src/assets/Inter/inter.css +++ /dev/null @@ -1,152 +0,0 @@ -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 100; - font-display: swap; - src: url("Inter-Thin.woff2?v=3.12") format("woff2"), - url("Inter-Thin.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 100; - font-display: swap; - src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"), - url("Inter-ThinItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 200; - font-display: swap; - src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"), - url("Inter-ExtraLight.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 200; - font-display: swap; - src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"), - url("Inter-ExtraLightItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 300; - font-display: swap; - src: url("Inter-Light.woff2?v=3.12") format("woff2"), - url("Inter-Light.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 300; - font-display: swap; - src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"), - url("Inter-LightItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url("Inter-Regular.woff2?v=3.12") format("woff2"), - url("Inter-Regular.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 400; - font-display: swap; - src: url("Inter-Italic.woff2?v=3.12") format("woff2"), - url("Inter-Italic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url("Inter-Medium.woff2?v=3.12") format("woff2"), - url("Inter-Medium.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 500; - font-display: swap; - src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"), - url("Inter-MediumItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"), - url("Inter-SemiBold.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 600; - font-display: swap; - src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"), - url("Inter-SemiBoldItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url("Inter-Bold.woff2?v=3.12") format("woff2"), - url("Inter-Bold.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 700; - font-display: swap; - src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"), - url("Inter-BoldItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 800; - font-display: swap; - src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"), - url("Inter-ExtraBold.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 800; - font-display: swap; - src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"), - url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff"); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 900; - font-display: swap; - src: url("Inter-Black.woff2?v=3.12") format("woff2"), - url("Inter-Black.woff?v=3.12") format("woff"); -} -@font-face { - font-family: 'Inter'; - font-style: italic; - font-weight: 900; - font-display: swap; - src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"), - url("Inter-BlackItalic.woff?v=3.12") format("woff"); -} diff --git a/frontend/src/components/PageOptions.vue b/frontend/src/components/PageOptions.vue index a9f053b8..2efcbe0a 100644 --- a/frontend/src/components/PageOptions.vue +++ b/frontend/src/components/PageOptions.vue @@ -18,10 +18,13 @@ type="text" variant="outline" class="w-full" - @input="(val: string) => (page.route = val)" :modelValue="pageRoute" :hideClearButton="true" - @update:modelValue="(val: string) => store.updateActivePage('route', `${app?.route}/${val}`)" + @update:modelValue=" + (val: string) => { + store.updateActivePage('route', val.startsWith('/') ? val : `/${val}`) + } + " /> @@ -53,10 +56,11 @@ const props = defineProps<{ }>() const inputRef = ref | null>(null) - -const pageRoute = computed(() => { - return props.page.route.replace(`${props.app?.route}/`, "") -}) +const pageRoute = ref(props.page.route) +const setPageRoute = () => { + // remove leading slash from route because app route prefix will be / so that user doesn't have to type the leading slash + pageRoute.value = props.page.route.replace(/^\//, "") +} const dynamicPadding = computed(() => { const prefixWidth = props.app?.route?.length * 8 + 15 // Assuming 8px per character plus 4px for padding @@ -78,6 +82,7 @@ watch( // apply dynamic padding to input element when the popover is opened // to avoid overlapping with the prefix content applyDynamicPadding() + setPageRoute() }, { immediate: true }, ) diff --git a/frontend/src/components/StudioToolbar.vue b/frontend/src/components/StudioToolbar.vue index 1af6b28f..73ef904a 100644 --- a/frontend/src/components/StudioToolbar.vue +++ b/frontend/src/components/StudioToolbar.vue @@ -27,7 +27,7 @@ name="external-link" v-if="store.activePage && store.activePage.published" class="h-[14px] w-[14px] !text-gray-700 dark:!text-gray-200" - @click="store.openPageInBrowser(store.activePage)" + @click="store.openPageInBrowser(store.activeApp, store.activePage)" > diff --git a/frontend/src/index.css b/frontend/src/index.css index 166ac595..12e80884 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,2 +1,2 @@ -@import './assets/Inter/inter.css'; +@import 'frappe-ui/src/fonts/Inter/inter.css'; @import 'frappe-ui/src/style.css'; diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 82cab143..6958821c 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -2,12 +2,11 @@ import "./index.css" import { createApp } from "vue" import { createPinia } from "pinia" -import studio_router from "@/router/studio_router" import "./setupFrappeUIResource" -import app_router from "@/router/app_router" +import studio_router from "@/router/studio_router" import App from "./App.vue" -import { resourcesPlugin, FeatherIcon } from "frappe-ui" +import { resourcesPlugin, frappeRequest } from "frappe-ui" import { registerGlobalComponents } from "./globals" const studio = createApp(App) @@ -18,11 +17,23 @@ studio.use(studio_router) studio.use(resourcesPlugin) studio.use(pinia) registerGlobalComponents(studio) -studio.mount("#studio") -// For rendering apps built by studio -const app = createApp(App) -app.use(app_router) -app.use(pinia) -registerGlobalComponents(app) -app.mount("#app") +declare global { + interface Window { + site_url: string + [key: string]: string + } +} + +studio_router.isReady().then(async () => { + if (import.meta.env.DEV) { + await frappeRequest({ + url: "/api/method/studio.www.studio.get_context_for_dev", + }).then(async (values: Record) => { + for (let key in values) { + window[key] = values[key] + } + }) + } + studio.mount("#studio") +}) \ No newline at end of file diff --git a/frontend/src/pages/AppContainer.vue b/frontend/src/pages/AppContainer.vue index a9728112..9ea7eba7 100644 --- a/frontend/src/pages/AppContainer.vue +++ b/frontend/src/pages/AppContainer.vue @@ -24,11 +24,10 @@ const rootBlock = ref(null) watch( () => route.path, async () => { - let { appRoute, pageRoute } = route.params as { appRoute: string; pageRoute: string[] } + let { pageRoute } = route.params as { pageRoute: string[] } const isDynamic = route.meta?.isDynamic - if (appRoute === "studio") return - let currentPath = "" + let currentPath = "/" if (isDynamic) { currentPath = route.matched?.[0]?.path } else if (pageRoute) { @@ -36,7 +35,7 @@ watch( } if (currentPath) { - page.value = await findPageWithRoute(appRoute, currentPath) + page.value = await findPageWithRoute(window.app_name, currentPath) if (!page.value) return const blocks = jsonToJs(page.value?.blocks) if (blocks) { diff --git a/frontend/src/pages/Home.vue b/frontend/src/pages/Home.vue index 8ee538b5..a3536493 100644 --- a/frontend/src/pages/Home.vue +++ b/frontend/src/pages/Home.vue @@ -45,29 +45,8 @@ > @@ -95,7 +74,7 @@ const createStudioApp = (app: NewStudioApp) => { studioApps.insert .submit({ app_title: app.app_title, - route: `studio-app/${app.route}`, + route: app.route, }) .then((res: StudioApp) => { showDialog.value = false diff --git a/frontend/src/pages/StudioPage.vue b/frontend/src/pages/StudioPage.vue index 7527de19..0da12b6d 100644 --- a/frontend/src/pages/StudioPage.vue +++ b/frontend/src/pages/StudioPage.vue @@ -87,9 +87,9 @@ async function setPage() { }) .then(async (data: StudioPage) => { const appID = route.params.appID as string - router.push({ name: "StudioPage", params: { pageID: data.name }, force: true }) + router.push({ name: "StudioPage", params: { appID: appID, pageID: data.name }, force: true }) store.setApp(appID) - store.setPage(data.name) + await store.setPage(data.name) }) } else { store.setApp(route.params.appID as string) @@ -97,7 +97,13 @@ async function setPage() { } } -onActivated(() => setPage()) +onActivated(() => { + const pageID = route.params.pageID + if (pageID && pageID !== store.selectedPage && pageID !== "new") { + store.setApp(route.params.appID as string) + store.setPage(pageID as string) + } +}) onDeactivated(() => { store.selectedPage = null @@ -106,8 +112,8 @@ onDeactivated(() => { watch( () => route.params.pageID, - () => { - setPage() + async () => { + await setPage() }, { immediate: true }, ) diff --git a/frontend/src/renderer.ts b/frontend/src/renderer.ts new file mode 100644 index 00000000..d28d9fb9 --- /dev/null +++ b/frontend/src/renderer.ts @@ -0,0 +1,20 @@ +import "./index.css" + +import { createApp } from "vue" +import { createPinia } from "pinia" +import "./setupFrappeUIResource" +import app_router from "@/router/app_router" +import App from "./App.vue" +import { resourcesPlugin } from "frappe-ui" +import { registerGlobalComponents } from "./globals" + +// For rendering apps built by studio +const app = createApp(App) +const pinia = createPinia() + +app.use(app_router) +app.use(pinia) +app.use(resourcesPlugin) +registerGlobalComponents(app) + +app.mount("#app") \ No newline at end of file diff --git a/frontend/src/router/app_router.ts b/frontend/src/router/app_router.ts index ca29e30c..bf4fe1f9 100644 --- a/frontend/src/router/app_router.ts +++ b/frontend/src/router/app_router.ts @@ -1,26 +1,36 @@ import { createRouter, createWebHistory } from "vue-router" -import { fetchAppPages } from "@/utils/helpers" const routes = [ { - path: "/:appRoute/:pageRoute(.*)*", + path: "/:pageRoute(.*)*", name: "AppContainer", component: () => import("@/pages/AppContainer.vue"), props: true, }, ] +interface Page { + name: string + route: string + page_title: string +} +declare global { + interface Window { + app_name: string + app_route: string + app_pages: Page[] + } +} + let router = createRouter({ - history: createWebHistory("/studio-app"), + history: createWebHistory(`/${window.app_route}`), routes, }) -const addDynamicRoutes = async (appRoute: string) => { - const pages = await fetchAppPages(appRoute) - +const addDynamicRoutes = (appRoute: string, pages: Page[]) => { pages.forEach((page) => { router.addRoute({ - path: page.route.replace("studio-app", ""), + path: page.route, name: page.page_title, component: () => import("@/pages/AppContainer.vue"), props: true, @@ -32,20 +42,18 @@ const addDynamicRoutes = async (appRoute: string) => { }) } -router.beforeEach(async (to, _, next) => { - // TODO: find a performant way to handle adding dynamic routes - if (to.params.appRoute && to.params.appRoute !== "studio") { +router.beforeEach((to, _, next) => { + if (to.params.pageRoute && to.params.pageRoute !== "studio") { + // if pageRoute is still a param, dynamic routes have not been added yet try { - await addDynamicRoutes(to.params.appRoute as string) - + addDynamicRoutes(to.params.appRoute as string, window.app_pages) // Redirect to the same route to trigger re-evaluation with new routes return next(to.fullPath) } catch (error) { - console.error("Error fetching dynamic routes:", error) + console.error("Error adding dynamic routes:", error) return next() } } - next() }) diff --git a/frontend/src/router/studio_router.ts b/frontend/src/router/studio_router.ts index aa8d6d1a..a8ec4e0d 100644 --- a/frontend/src/router/studio_router.ts +++ b/frontend/src/router/studio_router.ts @@ -6,6 +6,10 @@ const routes = [ name: "Home", component: () => import("@/pages/Home.vue"), }, + { + path: "/", + redirect: "home", + }, { path: "/app/:appID", name: "StudioApp", diff --git a/frontend/src/stores/studioStore.ts b/frontend/src/stores/studioStore.ts index 2b08ef4f..4691f6d6 100644 --- a/frontend/src/stores/studioStore.ts +++ b/frontend/src/stores/studioStore.ts @@ -295,15 +295,15 @@ const useStudioStore = defineStore("store", () => { }) .then(async () => { activePage.value = await fetchPage(selectedPage.value!) - if (activePage.value) { - openPageInBrowser(activePage.value) + if (activeApp.value && activePage.value) { + openPageInBrowser(activeApp.value, activePage.value) } }) } - function openPageInBrowser(page: StudioPage) { - let route = page.route - window.open(`/${route}`, "studio-preview") + function openPageInBrowser(app: StudioApp, page: StudioPage) { + let route = `${window.site_url}/${app.route}${page.route}` + window.open(route, "studio-preview") } // styles diff --git a/frontend/src/utils/helpers.ts b/frontend/src/utils/helpers.ts index b8777319..0b1cc019 100644 --- a/frontend/src/utils/helpers.ts +++ b/frontend/src/utils/helpers.ts @@ -7,7 +7,6 @@ import { toast } from "vue-sonner" import { ObjectLiteral, BlockOptions, StyleValue, ExpressionEvaluationContext, SelectOption, HashString, RGBString } from "@/types" import { DataResult, DocumentResource, DocumentResult, Filters, Resource } from "@/types/Studio/StudioResource" -import { StudioPage } from "@/types/Studio/StudioPage" import { Variable } from "@/types/Studio/StudioPageVariable" function getBlockString(block: BlockOptions | Block): string { @@ -265,33 +264,17 @@ async function fetchPage(pageName: string) { return pageResource?.doc } -async function findPageWithRoute(appRoute: string, pageRoute: string) { - let route = `studio-app` - if (appRoute) { - route += `/${appRoute}/` - } - route += pageRoute - +async function findPageWithRoute(appName: string, pageRoute: string) { let pageName = createResource({ url: "studio.studio.doctype.studio_page.studio_page.find_page_with_route", method: "GET", - params: { route: route }, + params: { app_name: appName, page_route: pageRoute }, }) await pageName.fetch() pageName = pageName.data return fetchPage(pageName) } -async function fetchAppPages(appRoute: string): Promise { - let appRoutes = createResource({ - url: "studio.studio.doctype.studio_app.studio_app.get_app_pages", - method: "GET", - params: { app_route: appRoute }, - }) - await appRoutes.fetch() - return appRoutes.data -} - // data function getAutocompleteValues(data: SelectOption[]) { if (!data.length || typeof data[0] === "string") return data @@ -675,7 +658,6 @@ export { isHTML, // app fetchApp, - fetchAppPages, // page fetchPage, findPageWithRoute, diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 2c54e86c..4ca24b5c 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -8,6 +8,12 @@ export default defineConfig({ define: { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, }, + server: { + // explicitly set origin of generated assets (images, fonts, etc) during development. + // Required for the app renderer running on webserver port + // https://vite.dev/guide/backend-integration + origin: "http://127.0.0.1:8080", + }, plugins: [frappeui({ source: "^/(app|login|api|assets|files|pages)" }), vue()], resolve: { alias: { @@ -16,6 +22,12 @@ export default defineConfig({ }, }, build: { + rollupOptions: { + input: { + studio: path.resolve(__dirname, "index.html"), + renderer: path.resolve(__dirname, "renderer.html"), + }, + }, outDir: `../studio/public/frontend`, emptyOutDir: true, target: "es2015", diff --git a/studio/hooks.py b/studio/hooks.py index c71ba92c..a3181ce0 100644 --- a/studio/hooks.py +++ b/studio/hooks.py @@ -53,7 +53,7 @@ # ---------- # automatically create page for each record of this doctype -# website_generators = ["Web Page"] +website_generators = ["Studio App"] # Jinja # ---------- @@ -222,8 +222,8 @@ website_route_rules = [ {"from_route": "/studio/", "to_route": "studio"}, - {"from_route": "/studio-app/", "to_route": "studio-app"}, ] +page_renderer = "studio.studio.doctype.studio_app.studio_app.StudioAppRenderer" # Automatically update python controller files with type annotations for this app. export_python_type_annotations = True diff --git a/studio/studio/doctype/studio_app/studio_app.json b/studio/studio/doctype/studio_app/studio_app.json index 6787041d..e572f1d0 100644 --- a/studio/studio/doctype/studio_app/studio_app.json +++ b/studio/studio/doctype/studio_app/studio_app.json @@ -10,7 +10,8 @@ "app_title", "route", "column_break_aroi", - "app_home" + "app_home", + "published" ], "fields": [ { @@ -33,8 +34,6 @@ { "fieldname": "app_name", "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, "label": "App Name", "unique": 1 }, @@ -42,6 +41,14 @@ "fieldname": "app_title", "fieldtype": "Data", "label": "App Title" + }, + { + "default": "0", + "fieldname": "published", + "fieldtype": "Check", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Published" } ], "index_web_pages_for_search": 1, @@ -51,7 +58,7 @@ "link_fieldname": "studio_app" } ], - "modified": "2025-02-18 15:18:52.558045", + "modified": "2025-03-05 16:54:27.699139", "modified_by": "Administrator", "module": "Studio", "name": "Studio App", @@ -74,5 +81,6 @@ "sort_field": "creation", "sort_order": "DESC", "states": [], - "title_field": "app_title" + "title_field": "app_title", + "track_changes": 1 } \ No newline at end of file diff --git a/studio/studio/doctype/studio_app/studio_app.py b/studio/studio/doctype/studio_app/studio_app.py index d38b34f7..df5dae06 100644 --- a/studio/studio/doctype/studio_app/studio_app.py +++ b/studio/studio/doctype/studio_app/studio_app.py @@ -3,11 +3,27 @@ import frappe from frappe.model.document import Document +from frappe.website.page_renderers.document_page import DocumentPage +from frappe.website.website_generator import WebsiteGenerator from studio.utils import camel_case_to_kebab_case -class StudioApp(Document): +class StudioAppRenderer(DocumentPage): + def can_render(self): + if app := self.find_app_for_path(): + self.doctype = "Studio App" + self.docname = app + return True + + return False + + def find_app_for_path(self): + app_route = self.path.split("/")[0] + return frappe.db.get_value("Studio App", dict(route=app_route), "name") + + +class StudioApp(WebsiteGenerator): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -19,9 +35,27 @@ class StudioApp(Document): app_home: DF.Link | None app_name: DF.Data | None app_title: DF.Data | None + published: DF.Check route: DF.Data | None # end: auto-generated types + website = frappe._dict( + template="templates/generators/renderer.html", + page_title_field="app_title", + condition_field="published", + ) + + def get_context(self, context): + context.no_cache = 1 + + context.app_name = self.name + context.app_route = self.route + context.app_title = self.app_title + context.base_url = frappe.utils.get_url(self.route) + context.app_pages = self.get_studio_pages() + context.is_developer_mode = frappe.conf.developer_mode + context.site_name = frappe.local.site + def autoname(self): if not self.name: self.name = f"app-{frappe.generate_hash(length=8)}" @@ -30,20 +64,7 @@ def before_insert(self): if not self.app_title: self.app_title = "My App" if not self.route: - self.route = f"studio-app/{camel_case_to_kebab_case(self.app_title, True)}-{frappe.generate_hash(length=4)}" - - def validate(self): - self.set_app_home() - - def set_app_home(self): - if self.app_home: - return - - if self.pages: - self.app_home = self.pages[0].studio_page - + self.route = f"{camel_case_to_kebab_case(self.app_title, True)}-{frappe.generate_hash(length=4)}" -@frappe.whitelist() -def get_app_pages(app_route: str) -> list[dict]: - app_name = frappe.db.get_value("Studio App", dict(route=f"studio-app/{app_route}"), "name") - return frappe.get_all("Studio Page", {"studio_app": app_name}, ["name", "page_title", "route"]) + def get_studio_pages(self): + return frappe.get_all("Studio Page", dict(studio_app=self.name), ["name", "page_title", "route"]) diff --git a/studio/studio/doctype/studio_page/studio_page.json b/studio/studio/doctype/studio_page/studio_page.json index 842f3c0b..d274a37e 100644 --- a/studio/studio/doctype/studio_page/studio_page.json +++ b/studio/studio/doctype/studio_page/studio_page.json @@ -29,6 +29,8 @@ { "fieldname": "page_title", "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Title" }, { @@ -38,6 +40,8 @@ { "fieldname": "route", "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Route" }, { @@ -53,6 +57,8 @@ "default": "0", "fieldname": "published", "fieldtype": "Check", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Published" }, { @@ -80,13 +86,15 @@ { "fieldname": "studio_app", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Studio App", "options": "Studio App" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-02-18 15:17:48.133502", + "modified": "2025-03-05 16:53:24.306451", "modified_by": "Administrator", "module": "Studio", "name": "Studio Page", @@ -109,5 +117,6 @@ "sort_field": "creation", "sort_order": "DESC", "states": [], - "title_field": "page_title" + "title_field": "page_title", + "track_changes": 1 } \ No newline at end of file diff --git a/studio/studio/doctype/studio_page/studio_page.py b/studio/studio/doctype/studio_page/studio_page.py index 8217bd95..1f94e8b6 100644 --- a/studio/studio/doctype/studio_page/studio_page.py +++ b/studio/studio/doctype/studio_page/studio_page.py @@ -45,6 +45,11 @@ def before_insert(self): if not self.route: self.route = f"{camel_case_to_kebab_case(self.page_title, True)}-{frappe.generate_hash(length=4)}" + def validate(self): + # vue router needs a leading slash + if not self.route.startswith("/"): + self.route = f"/{self.route}" + @frappe.whitelist() def publish(self, **kwargs): frappe.form_dict.update(kwargs) @@ -56,9 +61,13 @@ def publish(self, **kwargs): @frappe.whitelist() -def find_page_with_route(route: str) -> str | None: +def find_page_with_route(app_name: str, page_route: str) -> str | None: + if not page_route.startswith("/"): + page_route = f"/{page_route}" try: - return frappe.db.get_value("Studio Page", dict(route=route, published=1), "name", cache=True) + return frappe.db.get_value( + "Studio Page", dict(studio_app=app_name, route=page_route, published=1), "name", cache=True + ) except frappe.DoesNotExistError: pass diff --git a/studio/templates/generators/__init__.py b/studio/templates/generators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/studio/www/studio.py b/studio/www/studio.py index 5941148f..6cb305f0 100644 --- a/studio/www/studio.py +++ b/studio/www/studio.py @@ -7,3 +7,15 @@ def get_context(context): csrf_token = frappe.sessions.get_csrf_token() frappe.db.commit() context.csrf_token = csrf_token + context.site_url = get_site_url() + + +@frappe.whitelist(methods=["POST"], allow_guest=True) +def get_context_for_dev(): + if not frappe.conf.developer_mode: + frappe.throw(frappe._("This method is only meant for developer mode")) + return frappe._dict({"site_url": get_site_url()}) + + +def get_site_url() -> str: + return frappe.utils.get_site_url(frappe.local.site) diff --git a/studio/www/studio_app.py b/studio/www/studio_app.py deleted file mode 100644 index 5941148f..00000000 --- a/studio/www/studio_app.py +++ /dev/null @@ -1,9 +0,0 @@ -import frappe - -no_cache = 1 - - -def get_context(context): - csrf_token = frappe.sessions.get_csrf_token() - frappe.db.commit() - context.csrf_token = csrf_token