Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ declare module 'vue' {
FitScreenIcon: typeof import('./src/components/Icons/FitScreenIcon.vue')['default']
FormDescription: typeof import('./src/components/FormDescription.vue')['default']
FormDialog: typeof import('./src/components/FormDialog.vue')['default']
GlobeOff: typeof import('./src/components/Icons/GlobeOff.vue')['default']
Grid: typeof import('./src/components/Grid.vue')['default']
Header: typeof import('./src/components/AppLayout/Header.vue')['default']
HTML: typeof import('./src/components/AppLayout/HTML.vue')['default']
Expand All @@ -75,6 +76,7 @@ declare module 'vue' {
PlacementControl: typeof import('./src/components/PlacementControl.vue')['default']
PropsEditor: typeof import('./src/components/PropsEditor.vue')['default']
ProxyDialog: typeof import('./src/components/ProxyComponents/ProxyDialog.vue')['default']
PublishButton: typeof import('./src/components/PublishButton.vue')['default']
Repeater: typeof import('./src/components/AppLayout/Repeater.vue')['default']
RepeaterContextProvider: typeof import('./src/components/AppLayout/RepeaterContextProvider.vue')['default']
ResourceDialog: typeof import('./src/components/ResourceDialog.vue')['default']
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/components/Icons/GlobeOff.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-globe-off-icon lucide-globe-off"
>
<path d="M10.114 4.462A14.5 14.5 0 0 1 12 2a10 10 0 0 1 9.313 13.643" />
<path d="M15.557 15.556A14.5 14.5 0 0 1 12 22 10 10 0 0 1 4.929 4.929" />
<path d="M15.892 10.234A14.5 14.5 0 0 0 12 2a10 10 0 0 0-3.643.687" />
<path d="M17.656 12H22" />
<path d="M19.071 19.071A10 10 0 0 1 12 22 14.5 14.5 0 0 1 8.44 8.45" />
<path d="M2 12h10" />
<path d="m2 2 20 20" />
</svg>
</template>
13 changes: 10 additions & 3 deletions frontend/src/components/PagesPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@
class="group flex cursor-pointer items-center gap-2 truncate rounded px-2 py-2 transition duration-300 ease-in-out"
:class="[isPageActive(page) ? 'border-[1px] border-gray-300' : 'hover:bg-gray-50']"
>
<Tooltip :text="page.published ? 'Published' : 'Draft'" placement="top">
<div
class="h-2 w-2 flex-shrink-0 rounded-full"
:class="page.published ? 'bg-green-500' : 'bg-gray-400'"
></div>
</Tooltip>
<div
class="flex items-center gap-1 truncate text-base"
:class="[isPageActive(page) ? 'font-medium text-gray-700' : 'text-gray-500']"
>
{{ page.page_title }} -
<span class="text-xs">{{ page.route }}</span>
</div>
<Badge v-if="isAppHome(page)" variant="outline" size="sm" class="text-xs" theme="blue">
App Home
</Badge>
<Tooltip text="App Home" placement="top">
<Badge v-if="isAppHome(page)" variant="subtle" size="sm" class="text-xs">Home</Badge>
</Tooltip>

<!-- Menu -->
<div
Expand Down Expand Up @@ -51,6 +57,7 @@ import useStudioStore from "@/stores/studioStore"
import type { StudioPage } from "@/types/Studio/StudioPage"
import { isObjectEmpty } from "@/utils/helpers"
import { useRouter } from "vue-router"
import { Dropdown, Button, Badge, Tooltip } from "frappe-ui"

const store = useStudioStore()
const router = useRouter()
Expand Down
84 changes: 84 additions & 0 deletions frontend/src/components/PublishButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<template>
<div class="flex items-center">
<Button
size="sm"
variant="solid"
:disabled="disabled || publishingPage"
:loading="publishingPage || publishingApp"
class="rounded-br-none rounded-tr-none border-0"
@click="
() => {
publishingPage = true
store.publishPage().finally(() => (publishingPage = false))
}
"
>
{{ publishingApp ? "Publishing App..." : publishingPage ? "Publishing Page..." : "Publish Page" }}
</Button>
<Dropdown
:options="[
{
group: 'Publish',
hideLabel: true,
items: [
{
label: 'Publish App',
icon: LucideGlobe,
onClick: () => {
publishingApp = true
store.publishApp().finally(() => (publishingApp = false))
},
},
],
},
{
group: 'Unpublish',
hideLabel: true,
items: [
{
label: 'Unpublish Page',
icon: LucideCircleDashed,
onClick: () => store.unpublishPage(),
condition: () => Boolean(store.activePage?.published),
},
{
label: 'Unpublish App',
icon: GlobeOff,
onClick: () => store.unpublishApp(),
},
],
},
]"
size="sm"
placement="right"
>
<template v-slot="{ open }">
<Button
size="sm"
variant="solid"
@click="open"
:disabled="disabled || publishingPage || publishingApp"
icon="chevron-down"
class="!w-6 justify-start rounded-bl-none rounded-tl-none border-0 pr-0 text-xs"
/>
</template>
</Dropdown>
</div>
</template>

<script setup lang="ts">
import { ref } from "vue"
import { Dropdown } from "frappe-ui"
import useStudioStore from "@/stores/studioStore"
import LucideCircleDashed from "~icons/lucide/circle-dashed"
import LucideGlobe from "~icons/lucide/globe"
import GlobeOff from "@/components/Icons/GlobeOff.vue"

defineProps<{
disabled?: boolean
}>()

const store = useStudioStore()
const publishingPage = ref(false)
const publishingApp = ref(false)
</script>
19 changes: 3 additions & 16 deletions frontend/src/components/StudioToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,7 @@
>
Preview
</Button>
<Button
size="sm"
variant="solid"
:disabled="canvasStore.showFragmentCanvas"
:loading="publishing"
@click="
() => {
publishing = true
store.publishPage().finally(() => (publishing = false))
}
"
>
Publish
</Button>
<PublishButton :disabled="canvasStore.showFragmentCanvas" />
</div>
<AppDialog
v-model:showDialog="showAppDialog"
Expand All @@ -147,13 +134,14 @@

<script setup lang="ts">
import { computed, ref } from "vue"
import { Tooltip, Popover } from "frappe-ui"
import { Tooltip, Popover, Dropdown } from "frappe-ui"
import useStudioStore from "@/stores/studioStore"
import useCanvasStore from "@/stores/canvasStore"

import PageOptions from "@/components/PageOptions.vue"
import StudioLogo from "@/components/Icons/StudioLogo.vue"
import ExportAppDialog from "@/components/ExportAppDialog.vue"
import PublishButton from "@/components/PublishButton.vue"

import type { StudioMode } from "@/types"
import session from "@/utils/session"
Expand All @@ -162,7 +150,6 @@ import { isObjectEmpty, openInDesk } from "@/utils/helpers"

const store = useStudioStore()
const canvasStore = useCanvasStore()
const publishing = ref(false)

const routeString = computed(() => store.activePage?.route || "/")
const showExportAppDialog = ref(false)
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/data/studioPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createListResource } from "frappe-ui"
const studioPages = createListResource({
method: "GET",
doctype: "Studio Page",
fields: ["name", "page_title", "route", "studio_app", "creation", "modified"],
fields: ["name", "page_title", "route", "studio_app", "creation", "modified", "published"],
auto: true,
cache: "pages",
orderBy: "creation asc",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/stores/canvasStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const useCanvasStore = defineStore("canvasStore", () => {
})

const showFragmentCanvas = computed(() => {
return editingMode.value === "fragment" || editingMode.value === "component" && fragmentData.value?.block
return Boolean(editingMode.value === "fragment" || (editingMode.value === "component" && fragmentData.value?.block))
})

async function editOnCanvas(
Expand Down
81 changes: 81 additions & 0 deletions frontend/src/stores/studioStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,84 @@ const useStudioStore = defineStore("store", () => {
})
}

async function unpublishPage() {
if (!activePage.value) return
const confirmed = await confirm(
`Are you sure you want to unpublish the page <b>${activePage.value.page_title}</b>? It will no longer be publicly accessible.`,
)
if (!confirmed) {
return
}
return studioPages.runDocMethod.submit(
{
name: selectedPage.value,
method: "unpublish",
},
{
onSuccess() {
activePage.value!.published = 0
setAppPages(activeApp.value!.name)
toast.success("Page unpublished")
},
onError(error: any) {
toast.error("Failed to unpublish the page", {
description: error.messages.join(", "),
})
},
}
)
}

async function publishApp() {
if (!activeApp.value) return
return studioApps.runDocMethod.submit(
{
name: activeApp.value.name,
method: "publish_app",
},
{
async onSuccess(data: any) {
activePage.value = await fetchPage(selectedPage.value!)
setAppPages(activeApp.value!.name)
openPageInBrowser(activeApp.value!, activePage.value!)
toast.success(`App published successfully (${data?.message?.published_pages} pages)`)
},
onError(error: any) {
toast.error("Failed to publish the app", {
description: error?.messages?.join(", "),
})
},
},
)
}

async function unpublishApp() {
if (!activeApp.value) return
const confirmed = await confirm(
`Are you sure you want to unpublish the app <b>${activeApp.value.app_name}</b>? It will no longer be publicly accessible.`,
)
if (!confirmed) {
return
}
return studioApps.runDocMethod.submit(
{
name: activeApp.value.name,
method: "unpublish_app",
},
{
onSuccess() {
setAppPages(activeApp.value!.name)
toast.success("App unpublished")
},
onError(error: any) {
toast.error("Failed to unpublish the app", {
description: error?.messages?.join(", "),
})
},
},
)
}

function openPageInBrowser(app: StudioApp, page: StudioPage, preview: boolean = false) {
let route = `/${app.route}${page.route}`
if (preview) {
Expand Down Expand Up @@ -374,6 +452,9 @@ const useStudioStore = defineStore("store", () => {
savePage,
updateActivePage,
publishPage,
unpublishPage,
publishApp,
unpublishApp,
openPageInBrowser,
routeObject,
// app build
Expand Down
26 changes: 24 additions & 2 deletions studio/studio/doctype/studio_app/studio_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os

import frappe
from frappe import _
from frappe.commands import popen
from frappe.website.page_renderers.document_page import DocumentPage
from frappe.website.website_generator import WebsiteGenerator
Expand Down Expand Up @@ -137,7 +138,7 @@ def get_studio_pages(self):
@frappe.whitelist()
def generate_app_build(self):
if not frappe.has_permission("Studio App", ptype="write"):
frappe.throw("You do not have permission to generate the app build", frappe.PermissionError)
frappe.throw(_("You do not have permission to generate the app build"), frappe.PermissionError)

try:
components = get_app_components(self.name)
Expand All @@ -147,6 +148,27 @@ def generate_app_build(self):
except Exception as e:
raise Exception(f"Build process failed: {str(e)}")

@frappe.whitelist()
def publish_app(self):
pages = frappe.get_all("Studio Page", filters={"studio_app": self.name}, pluck="name")
for page_name in pages:
page_doc = frappe.get_doc("Studio Page", page_name)
page_doc.publish()

try:
self.generate_app_build()
except Exception:
pass

return {"published_pages": len(pages)}

@frappe.whitelist()
def unpublish_app(self):
pages = frappe.get_all("Studio Page", filters={"studio_app": self.name}, pluck="name")
for page_name in pages:
page_doc = frappe.get_doc("Studio Page", page_name)
page_doc.unpublish()

def get_assets_from_manifest(self):
"""
Read the Vite manifest file for this app and return asset paths
Expand Down Expand Up @@ -217,7 +239,7 @@ def export_app(self):
return

if not self.frappe_app:
frappe.throw("Frappe App must be set to export the Studio App.")
frappe.throw(_("Frappe App must be set to export the Studio App."))

app_path = self.create_app_folder()
self.export_studio_pages(app_path)
Expand Down
7 changes: 6 additions & 1 deletion studio/studio/doctype/studio_page/studio_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ def publish(self, **kwargs):
self.draft_blocks = None
self.save()

@frappe.whitelist()
def unpublish(self):
self.published = 0
self.save()

def validate_conflicts_with_other_pages(self):
other_pages = frappe.get_all(
"Studio Page",
Expand Down Expand Up @@ -270,7 +275,7 @@ def find_page_with_route(app_name: str, page_route: str) -> str | None:
@frappe.whitelist()
def duplicate_page(page_name: str, app_name: str | None):
if not frappe.has_permission("Studio Page", ptype="write"):
frappe.throw("You do not have permission to duplicate a page.")
frappe.throw(_("You do not have permission to duplicate a page."))

page = frappe.get_doc("Studio Page", page_name)
new_page = frappe.copy_doc(page)
Expand Down
Loading