diff --git a/doppio/commands/__init__.py b/doppio/commands/__init__.py
index b257cd2..6884cf1 100644
--- a/doppio/commands/__init__.py
+++ b/doppio/commands/__init__.py
@@ -1,7 +1,17 @@
+import os
import click
+import frappe
import subprocess
from .spa_generator import SPAGenerator
+from frappe.commands import get_site, pass_context
+from frappe import get_module_path, scrub
from .utils import add_build_command_to_package_json, add_routing_rule_to_hooks
+from .boilerplates import (
+ CUSTOM_PAGE_APP_COMPONENT_BOILERPLATE,
+ CUSTOM_PAGE_JS_TEMPLATE,
+ CUSTOM_PAGE_JS_BUNDLE_TEMPLATE,
+)
+from pathlib import Path
@click.command("add-spa")
@@ -62,4 +72,102 @@ def add_frappe_ui_starter(name, app):
add_routing_rule_to_hooks(app, name)
-commands = [generate_spa, add_frappe_ui]
+@click.command("add-custom-page")
+@click.option("--page-name", prompt="Custom Page Name")
+@click.option("--app", prompt="App Name")
+@click.option(
+ "--starter",
+ type=click.Choice(["vue", "simple"]),
+ default="vue",
+ prompt="Which framework do you want to use?",
+ help="Setup a custom page with the framework of your choice",
+)
+@pass_context
+def add_custom_page(context, app, page_name, starter):
+ site = get_site(context)
+ frappe.init(site=site)
+
+ try:
+ frappe.connect()
+ setup_custom_page(site, app, page_name, starter)
+ finally:
+ frappe.destroy()
+
+
+def setup_custom_page(site, app_name, page_name, starter):
+ if not frappe.conf.developer_mode:
+ click.echo("Please enable developer mode to add custom page")
+ return
+
+ module_name = frappe.get_all(
+ "Module Def",
+ filters={"app_name": app_name},
+ limit=1,
+ pluck="name",
+ order_by="creation",
+ )[0]
+
+ # create page doc
+ page = frappe.new_doc("Page")
+ page.module = module_name
+ page.standard = "Yes"
+ page.page_name = page_name
+ page.title = page_name
+ page.insert()
+ frappe.db.commit()
+
+ if starter == "vue":
+ setup_vue_custom_page_starter(page, app_name)
+
+ print("Opening", page.title, "in browser...")
+ page_url = f"{frappe.utils.get_site_url(site)}/app/{page.name}"
+ click.launch(page_url)
+
+
+def setup_vue_custom_page_starter(page_doc, app_name):
+ context = {
+ "pascal_cased_name": page_doc.name.replace("-", " ").title().replace(" ", ""),
+ "scrubbed_name": page_doc.name.replace("-", "_"),
+ "page_title": page_doc.title,
+ "page_name": page_doc.name,
+ }
+
+ custom_page_js_file_content = frappe.render_template(CUSTOM_PAGE_JS_TEMPLATE, context)
+ custom_page_js_bundle_file_content = frappe.render_template(
+ CUSTOM_PAGE_JS_BUNDLE_TEMPLATE, context
+ )
+
+ js_file_path = os.path.join(
+ frappe.get_module_path(app_name),
+ scrub(page_doc.doctype),
+ scrub(page_doc.name),
+ scrub(page_doc.name) + ".js",
+ )
+ js_bundle_file_path = os.path.join(
+ frappe.get_app_path(app_name),
+ "public",
+ "js",
+ scrub(page_doc.name),
+ scrub(page_doc.name) + ".bundle.js",
+ )
+
+ with Path(js_file_path).open("w") as f:
+ f.write(custom_page_js_file_content)
+
+ # create dir if not exists
+ Path(js_bundle_file_path).parent.mkdir(parents=True, exist_ok=True)
+ with Path(js_bundle_file_path).open("w") as f:
+ f.write(custom_page_js_bundle_file_content)
+
+ app_component_path = os.path.join(
+ frappe.get_app_path(app_name), "public", "js", scrub(page_doc.name), "App.vue"
+ )
+
+ with Path(app_component_path).open("w") as f:
+ f.write(CUSTOM_PAGE_APP_COMPONENT_BOILERPLATE)
+
+ from frappe.build import bundle
+ bundle("development", apps=app_name)
+
+
+commands = [generate_spa, add_frappe_ui, add_custom_page]
diff --git a/doppio/commands/boilerplates.py b/doppio/commands/boilerplates.py
index 5645c1f..3443201 100644
--- a/doppio/commands/boilerplates.py
+++ b/doppio/commands/boilerplates.py
@@ -271,4 +271,84 @@
}
export default App
-"""
\ No newline at end of file
+"""
+
+CUSTOM_PAGE_JS_TEMPLATE = """frappe.pages["{{ page_name }}"].on_page_load = function (wrapper) {
+ frappe.ui.make_app_page({
+ parent: wrapper,
+ title: __("{{ page_title }}"),
+ single_column: true,
+ });
+
+ // hot reload in development
+ if (frappe.boot.developer_mode) {
+ frappe.hot_update = frappe.hot_update || [];
+ frappe.hot_update.push(() => load_custom_page(wrapper));
+ }
+};
+
+frappe.pages["{{ page_name }}"].on_page_show = function (wrapper) {
+ load_custom_page(wrapper);
+};
+
+function load_custom_page(wrapper) {
+ let $parent = $(wrapper).find(".layout-main-section");
+ $parent.empty();
+
+ frappe.require("{{ scrubbed_name }}.bundle.js").then(() => {
+ frappe.{{ scrubbed_name }} = new frappe.ui.{{ pascal_cased_name }}({
+ wrapper: $parent,
+ page: wrapper.page,
+ });
+ });
+}
+"""
+
+CUSTOM_PAGE_JS_BUNDLE_TEMPLATE = """import { createApp } from "vue";
+import App from "./App.vue";
+
+
+class {{ pascal_cased_name }} {
+ constructor({ page, wrapper }) {
+ this.$wrapper = $(wrapper);
+ this.page = page;
+
+ this.init();
+ }
+
+ init() {
+ this.setup_page_actions();
+ this.setup_app();
+ }
+
+ setup_page_actions() {
+ // setup page actions
+ this.primary_btn = this.page.set_primary_action(__("Print Message"), () =>
+ frappe.msgprint("Hello Custom Page!")
+ );
+ }
+
+ setup_app() {
+ // create a vue instance
+ let app = createApp(App);
+ // mount the app
+ this.${{ scrubbed_name }} = app.mount(this.$wrapper.get(0));
+ }
+}
+
+frappe.provide("frappe.ui");
+frappe.ui.{{ pascal_cased_name }} = {{ pascal_cased_name }};
+export default {{ pascal_cased_name }};
+"""
+
+CUSTOM_PAGE_APP_COMPONENT_BOILERPLATE = """
+
+
+
{{ dynamicMessage }}
+
+
+"""