From 7e4d7a8fde4bb5fa3442569ccdb10a18f7b8cd8f Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Sat, 23 Sep 2023 14:47:27 +0530 Subject: [PATCH] feat: custom vue page first cut! --- doppio/commands/__init__.py | 110 +++++++++++++++++++++++++++++++- doppio/commands/boilerplates.py | 82 +++++++++++++++++++++++- 2 files changed, 190 insertions(+), 2 deletions(-) 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 = """ + +"""