From 1a927afe6cddf42a9280073cc3f0b90b9e757f94 Mon Sep 17 00:00:00 2001 From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:05:19 +0530 Subject: [PATCH 01/40] chore: setup CI and semantic release (#139) --- .github/workflows/ci.yml | 102 ++++++++++++++++++++++++++++ .github/workflows/on_release.yml | 32 +++++++++ .github/workflows/release_notes.yml | 40 +++++++++++ 3 files changed, 174 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/on_release.yml create mode 100644 .github/workflows/release_notes.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..688a338 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ + +name: CI + +on: + push: + branches: + - develop + pull_request: + +concurrency: + group: develop-print-designer-${{ github.event.number }} + cancel-in-progress: true + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + name: Server + + services: + redis-cache: + image: redis:alpine + ports: + - 13000:6379 + redis-queue: + image: redis:alpine + ports: + - 11000:6379 + redis-socketio: + image: redis:alpine + ports: + - 12000:6379 + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 20 + check-latest: true + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' + + - uses: actions/cache@v3 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Setup + run: | + pip install frappe-bench + bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench + mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" + mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + + - name: Install + working-directory: /home/runner/frappe-bench + run: | + bench get-app print_designer $GITHUB_WORKSPACE + bench setup requirements --dev + bench new-site --db-root-password root --admin-password admin test_site + bench --site test_site install-app print_designer + bench build + env: + CI: 'Yes' + + - name: Run Tests + working-directory: /home/runner/frappe-bench + run: | + bench --site test_site set-config allow_tests true + bench --site test_site run-tests --app print_designer + env: + TYPE: server diff --git a/.github/workflows/on_release.yml b/.github/workflows/on_release.yml new file mode 100644 index 0000000..385ed0a --- /dev/null +++ b/.github/workflows/on_release.yml @@ -0,0 +1,32 @@ +name: Generate Semantic Release +on: + workflow_dispatch: + push: + branches: + - main +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: 20 + - name: Setup dependencies + run: | + npm install @semantic-release/git @semantic-release/exec --no-save + - name: Create Release + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GIT_AUTHOR_NAME: "Frappe PR Bot" + GIT_AUTHOR_EMAIL: "developers@frappe.io" + GIT_COMMITTER_NAME: "Frappe PR Bot" + GIT_COMMITTER_EMAIL: "developers@frappe.io" + run: npx semantic-release \ No newline at end of file diff --git a/.github/workflows/release_notes.yml b/.github/workflows/release_notes.yml new file mode 100644 index 0000000..b22b609 --- /dev/null +++ b/.github/workflows/release_notes.yml @@ -0,0 +1,40 @@ +# This action: +# +# 1. Generates release notes using github API. +# 2. Strips unnecessary info like chore/style etc from notes. +# 3. Updates release info. + +name: 'Release Notes' + +on: + workflow_dispatch: + inputs: + tag_name: + description: 'Tag of release like v1.0.0' + required: true + type: string + release: + types: [released] + +permissions: + contents: read + +jobs: + regen-notes: + name: 'Regenerate release notes' + runs-on: ubuntu-latest + + steps: + - name: Update notes + run: | + NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/print_designer/releases/generate-notes -f tag_name=$RELEASE_TAG \ + | jq -r '.body' \ + | sed -E '/^\* (chore|ci|test|docs|style)/d' \ + | sed -E 's/by @mergify //' + ) + RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/print_designer/releases/tags/$RELEASE_TAG | jq -r '.id') + gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/print_designer/releases/$RELEASE_ID -f body="$NEW_NOTES" + + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }} \ No newline at end of file From fb19b9f62ca38b043cd0f5c06d83ec55d98ac746 Mon Sep 17 00:00:00 2001 From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:12:02 +0530 Subject: [PATCH 02/40] chore: setup github codespaces (#140) --- .devcontainer/devcontainer.json | 21 +++++++++++++++ .devcontainer/docker-compose.yml | 46 ++++++++++++++++++++++++++++++++ scripts/init.sh | 46 ++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 scripts/init.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..245ce21 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +{ + "name": "Frappe Bench", + "forwardPorts": [8000, 9000, 6787], + "remoteUser": "frappe", + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "debug.node.autoAttach": "disabled" + }, + "dockerComposeFile": "./docker-compose.yml", + "service": "frappe", + "workspaceFolder": "/workspace/frappe-bench", + "postCreateCommand": "bash /workspace/scripts/init.sh", + "shutdownAction": "stopCompose", + "extensions": [ + "ms-python.python", + "ms-vscode.live-server", + "grapecity.gc-excelviewer", + "mtxr.sqltools", + "visualstudioexptteam.vscodeintellicode" + ] +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..e18f84b --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,46 @@ +version: "3.7" +services: + mariadb: + image: mariadb:10.6 + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --skip-character-set-client-handshake + - --skip-innodb-read-only-compressed # Temporary fix for MariaDB 10.6 + environment: + MYSQL_ROOT_PASSWORD: 123 + volumes: + - mariadb-data:/var/lib/mysql + + # Enable PostgreSQL only if you use it, see development/README.md for more information. + # postgresql: + # image: postgres:11.8 + # environment: + # POSTGRES_PASSWORD: 123 + # volumes: + # - postgresql-data:/var/lib/postgresql/data + + redis-cache: + image: redis:alpine + + redis-queue: + image: redis:alpine + + redis-socketio: + image: redis:alpine + + frappe: + image: frappe/bench:latest + command: sleep infinity + environment: + - SHELL=/bin/bash + volumes: + - ..:/workspace:cached + working_dir: /workspace/frappe-bench + ports: + - 8000-8005:8000-8005 + - 9000-9005:9000-9005 + +volumes: + mariadb-data: + postgresql-data: \ No newline at end of file diff --git a/scripts/init.sh b/scripts/init.sh new file mode 100644 index 0000000..6e30880 --- /dev/null +++ b/scripts/init.sh @@ -0,0 +1,46 @@ +#!bin/bash + +set -e + +if [[ -f "/workspaces/frappe_codespace/frappe-bench/apps/frappe" ]] +then + echo "Bench already exists, skipping init" + exit 0 +fi + +rm -rf /workspaces/frappe_codespace/.git + +source /home/frappe/.nvm/nvm.sh +nvm alias default 18 +nvm use 18 + +echo "nvm use 18" >> ~/.bashrc +cd /workspace + +bench init \ +--ignore-exist \ +--skip-redis-config-generation \ +frappe-bench + +cd frappe-bench + +# Use containers instead of localhost +bench set-mariadb-host mariadb +bench set-redis-cache-host redis-cache:6379 +bench set-redis-queue-host redis-queue:6379 +bench set-redis-socketio-host redis-socketio:6379 + +# Remove redis from Procfile +sed -i '/redis/d' ./Procfile + + +bench new-site dev.localhost \ +--mariadb-root-password 123 \ +--admin-password admin \ +--no-mariadb-socket + +bench --site dev.localhost set-config developer_mode 1 +bench --site dev.localhost clear-cache +bench use dev.localhost +bench get-app print_designer +bench --site dev.localhost install-app print_designer \ No newline at end of file From 0f7ccfe75c3cfc53af47cf09566bd3b795bc0649 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 21 Mar 2024 17:22:40 +0530 Subject: [PATCH 03/40] feat: add isDynamicHeight property for user control This property is how user can configure if some element should have fixed height or should be dynamically adjusted. This will not ensure that it will have dynamically calculated height as it will depend on several factors This is just to give them control to choose between static and dynamic height. --- print_designer/patches.txt | 3 +- .../patches/introduce_dynamic_height.py | 19 +++++++++ .../js/print_designer/PropertiesPanelState.js | 42 ++++++++++++++++++- .../js/print_designer/defaultObjects.js | 2 + 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 print_designer/patches/introduce_dynamic_height.py diff --git a/print_designer/patches.txt b/print_designer/patches.txt index 1bd155f..cabe39a 100644 --- a/print_designer/patches.txt +++ b/print_designer/patches.txt @@ -4,4 +4,5 @@ print_designer.patches.introduce_jinja print_designer.patches.introduce_schema_versioning print_designer.patches.rerun_introduce_jinja print_designer.patches.introduce_table_alt_row_styles -print_designer.patches.introduce_column_style \ No newline at end of file +print_designer.patches.introduce_column_style +print_designer.patches.introduce_dynamic_height \ No newline at end of file diff --git a/print_designer/patches/introduce_dynamic_height.py b/print_designer/patches/introduce_dynamic_height.py new file mode 100644 index 0000000..e65c2d3 --- /dev/null +++ b/print_designer/patches/introduce_dynamic_height.py @@ -0,0 +1,19 @@ +import frappe + +from print_designer.patches.patch_utils import patch_formats + + +def execute(): + """Updating Table and Dynamic Text Elements to have property isDynamicHeight with default value as True""" + + def element_callback(el): + if el.get("type") == "text" and not el.get("isDynamic"): + return + + if not "isDynamicHeight" in el: + el["isDynamicHeight"] = True + + patch_formats( + {"element": element_callback}, + types=["text", "table"], + ) diff --git a/print_designer/public/js/print_designer/PropertiesPanelState.js b/print_designer/public/js/print_designer/PropertiesPanelState.js index 4624b9c..6473cac 100644 --- a/print_designer/public/js/print_designer/PropertiesPanelState.js +++ b/print_designer/public/js/print_designer/PropertiesPanelState.js @@ -433,6 +433,43 @@ export const createPropertiesPanel = () => { transformInput("H", "transformHeight", "height"), transformInput("W", "transformWidth", "width"), ], + [ + { + label: "Height", + name: "isDynamicHeight", + isLabelled: true, + labelDirection: "column", + condtional: () => MainStore.getCurrentElementsValues[0], + frappeControl: (ref, name) => { + const MainStore = useMainStore(); + makeFeild({ + name, + ref, + fieldtype: "Select", + requiredData: [MainStore.getCurrentElementsValues[0]], + reactiveObject: () => MainStore.getCurrentElementsValues[0], + propertyName: "isDynamicHeight", + isStyle: false, + options: () => [ + { label: "Auto", value: "auto" }, + { label: "Fixed", value: "fixed" }, + ], + formatValue: (object, property, isStyle) => { + if (!object) return; + return object[property] ? "auto" : "fixed"; + }, + onChangeCallback: (value = null) => { + if (value && MainStore.getCurrentElementsValues[0]) { + MainStore.getCurrentElementsValues[0]["isDynamicHeight"] = + value === "auto"; + MainStore.frappeControls[name].$input.blur(); + } + }, + }); + }, + flex: 1, + }, + ], ], }); MainStore.propertiesPanel.push({ @@ -663,7 +700,10 @@ export const createPropertiesPanel = () => { } } } - if (MainStore.getCurrentElementsValues[0]?.selectedColumn && value != "main"){ + if ( + MainStore.getCurrentElementsValues[0]?.selectedColumn && + value != "main" + ) { MainStore.getCurrentElementsValues[0].selectedColumn = null; } }, diff --git a/print_designer/public/js/print_designer/defaultObjects.js b/print_designer/public/js/print_designer/defaultObjects.js index 2b64547..1121d0b 100644 --- a/print_designer/public/js/print_designer/defaultObjects.js +++ b/print_designer/public/js/print_designer/defaultObjects.js @@ -166,6 +166,7 @@ export const createTable = (cordinates, parent = null) => { labelStyle: {}, headerStyle: {}, altStyle: {}, + isDynamicHeight: true, classes: [], }; @@ -258,6 +259,7 @@ export const createDynamicText = (cordinates, parent = null) => { labelDisplayStyle: "standard", style: {}, labelStyle: {}, + isDynamicHeight: true, classes: [], }; parent !== ElementStore.Elements From 0e81ef7eb1abb90a36304376100435744b5ba987 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 21 Mar 2024 17:28:59 +0530 Subject: [PATCH 04/40] refactor(minor): make childernSave & childernLoad actions childernSave, childernLoad & handleDynamicContent can be used at other places so converted them to actions. --- .../js/print_designer/store/ElementStore.js | 163 +++++++++--------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index c5061c0..ad2e61d 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -34,32 +34,33 @@ export const useElementStore = defineStore("ElementStore", { } return newElement; }, + childrensSave(element, printFonts) { + let saveEl = { ...element }; + delete saveEl.DOMRef; + delete saveEl.index; + delete saveEl.snapPoints; + delete saveEl.snapEdges; + delete saveEl.parent; + if (printFonts && ["text", "table"].indexOf(saveEl.type) != -1) { + handlePrintFonts(saveEl, printFonts); + } + if (saveEl.type == "rectangle") { + const childrensArray = saveEl.childrens; + saveEl.childrens = []; + childrensArray.forEach((el) => { + const child = this.childrensSave(el, printFonts); + child && saveEl.childrens.push(child); + }); + } + + return saveEl; + }, async saveElements() { const MainStore = useMainStore(); if (MainStore.mode == "preview") return; let mainPrintFonts = {}; let headerPrintFonts = {}; let footerprintFonts = {}; - const childrensSave = (element, printFonts) => { - let saveEl = { ...element }; - delete saveEl.DOMRef; - delete saveEl.index; - delete saveEl.snapPoints; - delete saveEl.snapEdges; - delete saveEl.parent; - ["text", "table"].indexOf(saveEl.type) != -1 && - handlePrintFonts(saveEl, printFonts); - if (saveEl.type == "rectangle") { - const childrensArray = saveEl.childrens; - saveEl.childrens = []; - childrensArray.forEach((el) => { - const child = childrensSave(el, printFonts); - child && saveEl.childrens.push(child); - }); - } - - return saveEl; - }; const headerElements = []; const mainElements = []; const afterTableElements = []; @@ -209,7 +210,7 @@ export const useElementStore = defineStore("ElementStore", { : is_footer ? footerprintFonts : mainPrintFonts; - let newElement = childrensSave(element, printFonts); + let newElement = this.childrensSave(element, printFonts); newElement.classes = newElement.classes.filter( (name) => ["inHeaderFooter", "overlappingHeaderFooter"].indexOf(name) == -1 ); @@ -217,7 +218,7 @@ export const useElementStore = defineStore("ElementStore", { let childrensArray = element.childrens; newElement.childrens = []; childrensArray.forEach((el) => { - newElement.childrens.push(childrensSave(el, printFonts)); + newElement.childrens.push(this.childrensSave(el, printFonts)); }); } if (is_header) { @@ -336,6 +337,63 @@ export const useElementStore = defineStore("ElementStore", { 5 ); }, + handleDynamicContent(element) { + const MainStore = useMainStore(); + if ( + element.type == "table" || + (["text", "image", "barcode"].indexOf(element.type) != -1 && element.isDynamic) + ) { + if (["text", "barcode"].indexOf(element.type) != -1) { + element.dynamicContent = [ + ...element.dynamicContent.map((el) => { + return { ...el }; + }), + ]; + element.selectedDynamicText = null; + MainStore.dynamicData.push(...element.dynamicContent); + } else if (element.type === "table") { + element.columns = [ + ...element.columns.map((el) => { + return { ...el }; + }), + ]; + element.columns.forEach((col) => { + if (!col.dynamicContent) return; + col.dynamicContent = [ + ...col.dynamicContent.map((el) => { + return { ...el }; + }), + ]; + col.selectedDynamicText = null; + MainStore.dynamicData.push(...col.dynamicContent); + }); + } else { + element.image = { ...element.image }; + MainStore.dynamicData.push(element.image); + } + } + }, + childrensLoad(element, parent) { + element.parent = parent; + element.DOMRef = null; + delete element.printY; + element.isDraggable = true; + element.isResizable = true; + this.handleDynamicContent(element); + if (element.type == "rectangle") { + element.isDropZone = true; + const childrensArray = element.childrens; + element.childrens = []; + childrensArray.forEach((el) => { + const child = this.childrensLoad(el, element); + child && element.childrens.push(child); + }); + } else if (element.type == "text" && !element.isDynamic) { + element.contenteditable = false; + } + + return element; + }, async loadElements(printDesignName) { const MainStore = useMainStore(); frappe.dom.freeze(__("Loading Print Format")); @@ -363,63 +421,6 @@ export const useElementStore = defineStore("ElementStore", { MainStore.old_schema_version = settings[key]; } }); - const handleDynamicContent = (element) => { - const MainStore = useMainStore(); - if ( - element.type == "table" || - (["text", "image", "barcode"].indexOf(element.type) != -1 && element.isDynamic) - ) { - if (["text", "barcode"].indexOf(element.type) != -1) { - element.dynamicContent = [ - ...element.dynamicContent.map((el) => { - return { ...el }; - }), - ]; - element.selectedDynamicText = null; - MainStore.dynamicData.push(...element.dynamicContent); - } else if (element.type === "table") { - element.columns = [ - ...element.columns.map((el) => { - return { ...el }; - }), - ]; - element.columns.forEach((col) => { - if (!col.dynamicContent) return; - col.dynamicContent = [ - ...col.dynamicContent.map((el) => { - return { ...el }; - }), - ]; - col.selectedDynamicText = null; - MainStore.dynamicData.push(...col.dynamicContent); - }); - } else { - element.image = { ...element.image }; - MainStore.dynamicData.push(element.image); - } - } - }; - const childrensLoad = (element, parent) => { - element.parent = parent; - element.DOMRef = null; - delete element.printY; - element.isDraggable = true; - element.isResizable = true; - handleDynamicContent(element); - if (element.type == "rectangle") { - element.isDropZone = true; - const childrensArray = element.childrens; - element.childrens = []; - childrensArray.forEach((el) => { - const child = childrensLoad(el, element); - child && element.childrens.push(child); - }); - } else if (element.type == "text" && !element.isDynamic) { - element.contenteditable = false; - } - - return element; - }; this.Elements = [ ...(ElementsHeader || []), ...(ElementsBody || []), @@ -505,14 +506,14 @@ export const useElementStore = defineStore("ElementStore", { delete element.printY; element.isDraggable = true; element.isResizable = true; - handleDynamicContent(element); + this.handleDynamicContent(element); if (element.type == "rectangle") { element.isDropZone = true; if (element.childrens.length) { let childrensArray = element.childrens; element.childrens = []; childrensArray.forEach((el) => { - element.childrens.push(childrensLoad(el, element)); + element.childrens.push(this.childrensLoad(el, element)); }); } } else if (element.type == "text" && !element.isDynamic) { From 24a6c5ea79ce0648759b0088cfefe7f12df4e556 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Mar 2024 14:20:16 +0530 Subject: [PATCH 05/40] chore: added print_designer_print_format field now, when we save Print Format, data that jinja template uses will be saved in print_designer_print_format field. misc - updated default for isDynamicHeight to False. --- print_designer/custom_fields.py | 7 +++++++ print_designer/patches.txt | 1 + .../patches/introduce_dynamic_containers.py | 18 ++++++++++++++++++ .../patches/introduce_dynamic_height.py | 2 +- 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 print_designer/patches/introduce_dynamic_containers.py diff --git a/print_designer/custom_fields.py b/print_designer/custom_fields.py index c016259..981b556 100644 --- a/print_designer/custom_fields.py +++ b/print_designer/custom_fields.py @@ -7,6 +7,13 @@ "hidden": 1, "label": "Print Designer", }, + { + "fieldname": "print_designer_print_format", + "fieldtype": "JSON", + "hidden": 1, + "label": "Print Designer Print Format", + "description": "This has json object that is used by main.html jinja template to render the print format.", + }, { "fieldname": "print_designer_header", "fieldtype": "JSON", diff --git a/print_designer/patches.txt b/print_designer/patches.txt index cabe39a..d8288b4 100644 --- a/print_designer/patches.txt +++ b/print_designer/patches.txt @@ -5,4 +5,5 @@ print_designer.patches.introduce_schema_versioning print_designer.patches.rerun_introduce_jinja print_designer.patches.introduce_table_alt_row_styles print_designer.patches.introduce_column_style +print_designer.patches.introduce_dynamic_containers print_designer.patches.introduce_dynamic_height \ No newline at end of file diff --git a/print_designer/patches/introduce_dynamic_containers.py b/print_designer/patches/introduce_dynamic_containers.py new file mode 100644 index 0000000..4a2581b --- /dev/null +++ b/print_designer/patches/introduce_dynamic_containers.py @@ -0,0 +1,18 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def execute(): + """Add print_designer_print_format field for Print Format.""" + CUSTOM_FIELDS = { + "Print Format": [ + { + "fieldname": "print_designer_print_format", + "fieldtype": "JSON", + "hidden": 1, + "label": "Print Designer Print Format", + "description": "This has json object that is used by jinja template to render the print format.", + } + ] + } + create_custom_fields(CUSTOM_FIELDS, ignore_validate=True) diff --git a/print_designer/patches/introduce_dynamic_height.py b/print_designer/patches/introduce_dynamic_height.py index e65c2d3..e8378f1 100644 --- a/print_designer/patches/introduce_dynamic_height.py +++ b/print_designer/patches/introduce_dynamic_height.py @@ -11,7 +11,7 @@ def element_callback(el): return if not "isDynamicHeight" in el: - el["isDynamicHeight"] = True + el["isDynamicHeight"] = False patch_formats( {"element": element_callback}, From 8ee59ca6bd30be1506dc0e9c71392cfb354091c6 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Mar 2024 14:41:50 +0530 Subject: [PATCH 06/40] fix(minor): remove unused rectangle globalStyles display: inline-flex is not supported by wkhtmltopdf. It was added in hope that we will make it work somehow in the future. This commit removes the globalStyles rectangle style as it was conflicting with relative containers. --- print_designer/patches.txt | 3 ++- .../remove_unused_rectangle_gs_properties.py | 23 +++++++++++++++++++ .../public/js/print_designer/globalStyles.js | 5 ---- 3 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 print_designer/patches/remove_unused_rectangle_gs_properties.py diff --git a/print_designer/patches.txt b/print_designer/patches.txt index d8288b4..4fd1548 100644 --- a/print_designer/patches.txt +++ b/print_designer/patches.txt @@ -6,4 +6,5 @@ print_designer.patches.rerun_introduce_jinja print_designer.patches.introduce_table_alt_row_styles print_designer.patches.introduce_column_style print_designer.patches.introduce_dynamic_containers -print_designer.patches.introduce_dynamic_height \ No newline at end of file +print_designer.patches.introduce_dynamic_height +print_designer.patches.remove_unused_rectangle_gs_properties diff --git a/print_designer/patches/remove_unused_rectangle_gs_properties.py b/print_designer/patches/remove_unused_rectangle_gs_properties.py new file mode 100644 index 0000000..71fa82f --- /dev/null +++ b/print_designer/patches/remove_unused_rectangle_gs_properties.py @@ -0,0 +1,23 @@ +import frappe + + +def execute(): + """Remove unused style properties in globalStyles for rectangle of print formats that uses print designer""" + print_formats = frappe.get_all( + "Print Format", + filters={"print_designer": 1}, + fields=["name", "print_designer_settings"], + as_list=1, + ) + for pf in print_formats: + settings = frappe.parse_json(pf[1]) + if settings: + # If globalStyles is not present, skip + if not (gs := settings.get("globalStyles")): + continue + + for key in ["display", "justifyContent", "alignItems", "alignContent", "flex"]: + if gs["rectangle"]["style"].get(key, False): + del gs["rectangle"]["style"][key] + + frappe.db.set_value("Print Format", pf[0], "print_designer_settings", frappe.as_json(settings)) diff --git a/print_designer/public/js/print_designer/globalStyles.js b/print_designer/public/js/print_designer/globalStyles.js index b753b54..8d8c4b9 100644 --- a/print_designer/public/js/print_designer/globalStyles.js +++ b/print_designer/public/js/print_designer/globalStyles.js @@ -120,11 +120,6 @@ export const globalStyles = { isDynamic: false, mainRuleSelector: ".rectangle", style: { - display: "inline-flex", - justifyContent: "flex-start", - alignItems: "flex-start", - alignContent: "flex-start", - flex: 1, paddingTop: "0px", paddingBottom: "0px", paddingLeft: "0px", From 918bd7b37e9fa7d4504a34954b9ca7ea0020742e Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Mar 2024 17:41:32 +0530 Subject: [PATCH 07/40] chore: added schema compare function Added function in JS and Python to check if schema version in format is older than current schema version. This is useful for checking if a schema update is needed or handle different logic based on version. misc: bumped version to 1.1.0 --- print_designer/pdf.py | 20 ++++++++++++++++ .../js/print_designer/store/MainStore.js | 23 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/print_designer/pdf.py b/print_designer/pdf.py index f10684f..5da4148 100644 --- a/print_designer/pdf.py +++ b/print_designer/pdf.py @@ -78,6 +78,26 @@ def pdf_body_html(print_format, jenv, args, template): return template.render(args, filters={"len": len}) +def is_older_schema(settings, current_version): + format_version = settings.get("schema_version") + format_version = format_version.split(".") + current_version = current_version.split(".") + if int(format_version[0]) < int(current_version[0]): + return True + elif int(format_version[0]) == int(current_version[0]) and int(format_version[1]) < int( + current_version[1] + ): + return True + elif ( + int(format_version[0]) == int(current_version[0]) + and int(format_version[1]) == int(current_version[1]) + and int(format_version[2]) < int(current_version[2]) + ): + return True + else: + return False + + def get_print_format_template(jenv, print_format): # if print format is created using print designer, then use print designer template if print_format and print_format.print_designer and print_format.print_designer_body: diff --git a/print_designer/public/js/print_designer/store/MainStore.js b/print_designer/public/js/print_designer/store/MainStore.js index 1e00a86..568f3c2 100644 --- a/print_designer/public/js/print_designer/store/MainStore.js +++ b/print_designer/public/js/print_designer/store/MainStore.js @@ -17,7 +17,7 @@ export const useMainStore = defineStore("MainStore", { /** * @type {'editing'|'pdfSetup'|'preview'} mode */ - schema_version: "1.0.1", + schema_version: "1.1.0", mode: "editing", cursor: "url('/assets/print_designer/images/mouse-pointer.svg'), default !important", isMarqueeActive: false, @@ -446,6 +446,27 @@ export const useMainStore = defineStore("MainStore", { } } }, + isOlderSchema: (state) => (currentVersion) => { + if (!state.old_schema_version) return false; + let formatVersion = state.old_schema_version.split("."); + currentVersion = currentVersion.split("."); + if (parseInt(formatVersion[0]) < parseInt(currentVersion[0])) { + return true; + } else if ( + parseInt(formatVersion[0]) === parseInt(currentVersion[0]) && + parseInt(formatVersion[1]) < parseInt(currentVersion[1]) + ) { + return true; + } else if ( + parseInt(formatVersion[0]) === parseInt(currentVersion[0]) && + parseInt(formatVersion[1]) === parseInt(currentVersion[1]) && + parseInt(formatVersion[2]) < parseInt(currentVersion[2]) + ) { + return true; + } else { + return false; + } + }, }, actions: { /** From bf740612f450cca7510c25ed5e81d90f9b67f6db Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Mar 2024 18:32:55 +0530 Subject: [PATCH 08/40] feat: check and update element overlapping - added isElementOverlapping function takes elements and checks if any other element is overlapping with it or not. - created checkUpdateElementOverlapping util function when called it will loop over all the parent level elements and check and update any element's isElementOverlapping property if it is changed. - updated isDynamicHeight in propertiesPanel to show only when element is table or dynamicText and isElementOverlapping is false. - Subscribed to ElementStore in onMounted of canvas to call checkUpdateElementOverlapping when any new elements are added. - when any element is deleted, dragged or resized using mouse or keyboard shortcuts run checkUpdateElementOverlapping --- .../js/print_designer/PropertiesPanelState.js | 13 +++++++++- .../components/layout/AppCanvas.vue | 17 +++++++++++- .../composables/AttachKeyBindings.js | 4 ++- .../print_designer/composables/Draggable.js | 3 ++- .../print_designer/composables/Resizable.js | 3 ++- .../js/print_designer/store/ElementStore.js | 26 +++++++++++++++++++ .../public/js/print_designer/utils.js | 21 ++++++++++++++- 7 files changed, 81 insertions(+), 6 deletions(-) diff --git a/print_designer/public/js/print_designer/PropertiesPanelState.js b/print_designer/public/js/print_designer/PropertiesPanelState.js index 6473cac..31ad16b 100644 --- a/print_designer/public/js/print_designer/PropertiesPanelState.js +++ b/print_designer/public/js/print_designer/PropertiesPanelState.js @@ -10,6 +10,7 @@ import { } from "./utils"; export const createPropertiesPanel = () => { const MainStore = useMainStore(); + const ElementStore = useElementStore(); const iconControl = ({ name, size, @@ -439,7 +440,17 @@ export const createPropertiesPanel = () => { name: "isDynamicHeight", isLabelled: true, labelDirection: "column", - condtional: () => MainStore.getCurrentElementsValues[0], + condtional: () => { + const currentEl = MainStore.getCurrentElementsValues[0]; + if ( + (currentEl.parent === ElementStore.Elements && + currentEl?.type === "table") || + (currentEl.type === "text" && currentEl.isDynamic) + ) { + return !currentEl.isElementOverlapping; + } + return false; + }, frappeControl: (ref, name) => { const MainStore = useMainStore(); makeFeild({ diff --git a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue index 9768b87..9ef43e5 100644 --- a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue +++ b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue @@ -73,7 +73,12 @@ import { useMainStore } from "../../store/MainStore"; import { useElementStore } from "../../store/ElementStore"; import { useMarqueeSelection } from "../../composables/MarqueeSelectionTool"; import { useDraw } from "../../composables/Draw"; -import { updateElementParameters, setCurrentElement, recursiveChildrens } from "../../utils"; +import { + updateElementParameters, + setCurrentElement, + recursiveChildrens, + checkUpdateElementOverlapping, +} from "../../utils"; import { useChangeValueUnit } from "../../composables/ChangeValueUnit"; import BaseBarcode from "../base/BaseBarcode.vue"; const isComponent = Object.freeze({ @@ -457,6 +462,7 @@ onMounted(() => { } } ); + watchEffect(() => { if (MainStore.screenStyleSheet) { if (MainStore.screenStyleSheet.CssRuleIndex != null) { @@ -508,6 +514,15 @@ onMounted(() => { ]); } }); + + ElementStore.$subscribe((mutation, state) => { + if ( + (mutation.events.type === "set" && mutation.events.key == "Elements") || + (mutation.events.type === "add" && mutation.events.newValue.parent == state.Elements) + ) { + checkUpdateElementOverlapping(); + } + }); }); watchEffect(() => { if (MainStore.printStyleSheet && MainStore.page) { diff --git a/print_designer/public/js/print_designer/composables/AttachKeyBindings.js b/print_designer/public/js/print_designer/composables/AttachKeyBindings.js index 594f2fd..1449d43 100644 --- a/print_designer/public/js/print_designer/composables/AttachKeyBindings.js +++ b/print_designer/public/js/print_designer/composables/AttachKeyBindings.js @@ -1,7 +1,7 @@ import { onMounted, onUnmounted } from "vue"; import { useMainStore } from "../store/MainStore"; import { useElementStore } from "../store/ElementStore"; -import { deleteCurrentElements } from "../utils"; +import { checkUpdateElementOverlapping, deleteCurrentElements } from "../utils"; export function useAttachKeyBindings() { const MainStore = useMainStore(); @@ -28,6 +28,7 @@ export function useAttachKeyBindings() { element[`start${axis}`] += value; } }); + checkUpdateElementOverlapping(); } function updateWidthHeight(key, value) { MainStore.getCurrentElementsValues.forEach((element) => { @@ -48,6 +49,7 @@ export function useAttachKeyBindings() { element[key] += value; } }); + checkUpdateElementOverlapping(); } const handleKeyDown = async (e) => { MainStore.isAltKey = e.altKey; diff --git a/print_designer/public/js/print_designer/composables/Draggable.js b/print_designer/public/js/print_designer/composables/Draggable.js index 8398514..05fb6bc 100644 --- a/print_designer/public/js/print_designer/composables/Draggable.js +++ b/print_designer/public/js/print_designer/composables/Draggable.js @@ -4,7 +4,7 @@ import "@interactjs/auto-start"; import "@interactjs/modifiers"; import { useMainStore } from "../store/MainStore"; import { useElementStore } from "../store/ElementStore"; -import { recursiveChildrens } from "../utils"; +import { recursiveChildrens, checkUpdateElementOverlapping } from "../utils"; export function useDraggable({ element, @@ -85,6 +85,7 @@ export function useDraggable({ droppedElement[splicedElement.id] = splicedElement; MainStore.isDropped = droppedElement; } + checkUpdateElementOverlapping(element); }); return; } diff --git a/print_designer/public/js/print_designer/composables/Resizable.js b/print_designer/public/js/print_designer/composables/Resizable.js index 99802b2..91e3f01 100644 --- a/print_designer/public/js/print_designer/composables/Resizable.js +++ b/print_designer/public/js/print_designer/composables/Resizable.js @@ -4,7 +4,7 @@ import "@interactjs/auto-start"; import "@interactjs/modifiers"; import { useMainStore } from "../store/MainStore"; import { useElementStore } from "../store/ElementStore"; -import { recursiveChildrens } from "../utils"; +import { recursiveChildrens, checkUpdateElementOverlapping } from "../utils"; export function useResizable({ element, @@ -44,6 +44,7 @@ export function useResizable({ if (element.DOMRef.className == "modal-dialog modal-sm") { return; } + checkUpdateElementOverlapping(element); if (element.parent == e.target.piniaElementRef.parent) return; if ( !e.dropzone && diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index ad2e61d..08b791c 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -533,5 +533,31 @@ export const useElementStore = defineStore("ElementStore", { t.isPrimaryTable = t == tableEl; }); }, + // This is called to check if the element is overlapping with any other element + isElementOverlapping(currentEl, elements = this.Elements) { + const currentElIndex = + currentEl.index || this.Elements.findIndex((el) => el === currentEl); + const currentStartY = parseInt(currentEl.startY); + const currentEndY = currentEl.endY || parseInt(currentEl.startY + currentEl.height); + + return ( + elements.findIndex((el, index) => { + if (index == currentElIndex) return false; + const elStartY = parseInt(el.startY); + const elEndY = el.endY || parseInt(el.startY + el.height); + if (currentStartY <= elStartY && elStartY <= currentEndY) { + return true; + } else if (currentStartY <= elEndY && elEndY <= currentEndY) { + return true; + } else if (elStartY <= currentStartY && currentStartY <= elEndY) { + return true; + } else if (elStartY <= currentEndY && currentEndY <= elEndY) { + return true; + } else { + return false; + } + }) != -1 + ); + }, }, }); diff --git a/print_designer/public/js/print_designer/utils.js b/print_designer/public/js/print_designer/utils.js index 920e180..1b5ba26 100644 --- a/print_designer/public/js/print_designer/utils.js +++ b/print_designer/public/js/print_designer/utils.js @@ -37,7 +37,7 @@ import { useElementStore } from "./store/ElementStore"; import { useDraggable } from "./composables/Draggable"; import { useResizable } from "./composables/Resizable"; import { useDropZone } from "./composables/DropZone"; -import { isRef } from "vue"; +import { isRef, nextTick } from "vue"; export const changeDraggable = (element) => { if ( @@ -346,6 +346,7 @@ export const deleteCurrentElements = () => { MainStore.getCurrentElementsId.forEach((element) => { delete MainStore.currentElements[element]; }); + checkUpdateElementOverlapping(); }; export const cloneElement = () => { @@ -806,3 +807,21 @@ export const selectElementContents = (el) => { sel.removeAllRanges(); sel.addRange(range); }; + +export const checkUpdateElementOverlapping = (element = null) => { + const MainStore = useMainStore(); + const ElementStore = useElementStore(); + nextTick(() => { + if (element && element.parent != ElementStore.Elements) return; + isOlderSchema = MainStore.isOlderSchema("1.1.0"); + ElementStore.Elements.forEach((el) => { + const isElementOverlapping = ElementStore.isElementOverlapping(el); + if (el.isElementOverlapping != isElementOverlapping) { + el.isElementOverlapping = isElementOverlapping; + } + if (isOlderSchema && el.type == "table" && !isElementOverlapping) { + el.isDynamicHeight = true; + } + }); + }, {}); +}; From 743f15b36505a6ea1e0ae55c8fd5764d0da95d82 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Mar 2024 18:44:38 +0530 Subject: [PATCH 09/40] fix(minor): warn user if page_info is in body - added .error class which basically makes text red if dynamic text includes "page_info" and is in body container. --- .../components/base/BaseDynamicText.vue | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/print_designer/public/js/print_designer/components/base/BaseDynamicText.vue b/print_designer/public/js/print_designer/components/base/BaseDynamicText.vue index 3ba71f9..3737a13 100644 --- a/print_designer/public/js/print_designer/components/base/BaseDynamicText.vue +++ b/print_designer/public/js/print_designer/components/base/BaseDynamicText.vue @@ -158,6 +158,43 @@ watch( } ); +const showWarning = frappe.utils.debounce((pageInfoInBody) => { + frappe.show_alert({ + message: "Please move " + pageInfoInBody.join(", ") + " to header / footer", + indicator: "orange", + }); +}, 500); +// Remove If Becomes Unnecessary Overhead. This will be fine once temparary state is introduced. +watch( + () => [startY.value], + () => { + if ( + startY.value + height.value < MainStore.page.headerHeight + MainStore.page.marginTop || + startY.value > + MainStore.page.height - + MainStore.page.footerHeight - + MainStore.page.marginTop - + MainStore.page.marginBottom + ) { + classes.value.splice(classes.value.indexOf("error"), 1); + return; + } + let pageInfoInBody = []; + dynamicContent.value + .filter((el) => ["page", "topage", "date", "time"].indexOf(el.fieldname) != -1) + .forEach((field) => { + pageInfoInBody.push(field.fieldname); + }); + if (pageInfoInBody.length) { + if (classes.value.indexOf("error") == -1) { + classes.value.push("error"); + } + showWarning(pageInfoInBody); + } + }, + { flush: "post" } +); + const handleMouseDown = (e, element) => { lockAxis(element, e.shiftKey); if (MainStore.openModal) return; @@ -215,6 +252,12 @@ p:empty:before { [contenteditable]:empty:focus:before { content: ""; } + +.error { + color: var(--red-500) !important; + border: 1px solid var(--red-500) !important; +} + .text-hover:hover { box-sizing: border-box !important; border-bottom: 1px solid var(--primary-color) !important; From f79baba786246ba03f8219022d0245f91252d4b7 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Mar 2024 19:49:03 +0530 Subject: [PATCH 10/40] refactor!: logic for saving elements. - computeLayout function loops over this.Elements divides them into possible rows ( dynamic containers ) based on their start and end locations. returns three arrays, header, body ( can have multiple containers ) and footer. - separated logic into smaller functions and changed structure for save a bit for dynamic containers. - checkIfAnyTableIsEmpty - handleHeaderFooterOverlapping - autoCalculateHeaderFooter - cleanUpElementsForSave - checkIfPrintFormatIsEmpty - childrensSave --- .../js/print_designer/store/ElementStore.js | 473 ++++++++---------- 1 file changed, 217 insertions(+), 256 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 08b791c..8912576 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -8,7 +8,7 @@ import { createTable, createBarcode, } from "../defaultObjects"; -import { handlePrintFonts } from "../utils"; +import { handlePrintFonts, setCurrentElement } from "../utils"; export const useElementStore = defineStore("ElementStore", { state: () => ({ Elements: new Array(), @@ -34,260 +34,30 @@ export const useElementStore = defineStore("ElementStore", { } return newElement; }, - childrensSave(element, printFonts) { - let saveEl = { ...element }; - delete saveEl.DOMRef; - delete saveEl.index; - delete saveEl.snapPoints; - delete saveEl.snapEdges; - delete saveEl.parent; - if (printFonts && ["text", "table"].indexOf(saveEl.type) != -1) { - handlePrintFonts(saveEl, printFonts); - } - if (saveEl.type == "rectangle") { - const childrensArray = saveEl.childrens; - saveEl.childrens = []; - childrensArray.forEach((el) => { - const child = this.childrensSave(el, printFonts); - child && saveEl.childrens.push(child); - }); - } - - return saveEl; - }, async saveElements() { const MainStore = useMainStore(); - if (MainStore.mode == "preview") return; - let mainPrintFonts = {}; - let headerPrintFonts = {}; - let footerprintFonts = {}; - const headerElements = []; - const mainElements = []; - const afterTableElements = []; - const footerElements = []; - let tableElement = this.Elements.filter((el) => el.type == "table"); - if (tableElement.some((el) => el.table == null)) { - let message = __("You have Empty Table. Please add table fields or remove table."); - frappe.show_alert( - { - message: message, - indicator: "red", - }, - 5 - ); - return; - } - let isOverlapping = false; - if (tableElement.length == 1 && MainStore.isHeaderFooterAuto) { - this.Elements.forEach((element) => { - if ( - (element.startY < tableElement[0].startY + MainStore.page.marginTop && - element.startY + element.height > - tableElement[0].startY + MainStore.page.marginTop) || - (element.startY < - MainStore.page.height - - (MainStore.page.height - - (tableElement[0].startY + tableElement[0].height)) - - MainStore.page.marginTop - - MainStore.page.marginBottom && - element.startY + element.height > - MainStore.page.height - - (MainStore.page.height - - (tableElement[0].startY + tableElement[0].height)) - - MainStore.page.marginTop - - MainStore.page.marginBottom) - ) { - isOverlapping = true; - } - }); - if (isOverlapping) { - MainStore.mode = "editing"; - } else if (!isOverlapping) { - MainStore.page.headerHeight = tableElement[0].startY; - MainStore.page.footerHeight = - MainStore.page.height - - (tableElement[0].startY + - tableElement[0].height + - MainStore.page.marginTop + - MainStore.page.marginBottom); - } - } else { - let isHeaderOverlapping = false; - let isFooterOverlapping = false; - this.Elements.forEach((element) => { - if ( - element.startY < MainStore.page.headerHeight && - element.startY + element.height > MainStore.page.headerHeight - ) { - isHeaderOverlapping = true; - } - if ( - element.startY < - MainStore.page.height - - MainStore.page.footerHeight - - MainStore.page.marginTop - - MainStore.page.marginBottom && - element.startY + element.height > - MainStore.page.height - - MainStore.page.footerHeight - - MainStore.page.marginTop - - MainStore.page.marginBottom - ) { - isFooterOverlapping = true; - } - }); - if (isHeaderOverlapping || isFooterOverlapping) { - MainStore.mode = "pdfSetup"; - frappe.show_alert( - { - message: `Please resolve overlapping ${ - isHeaderOverlapping ? "header" : "" - } ${isHeaderOverlapping && isFooterOverlapping ? " and " : ""} ${ - isFooterOverlapping ? "footer" : "" - }`, - indicator: "red", - }, - 5 - ); - return; - } - } - let isHeaderEmpty = true; - let isBodyEmpty = true; - let isFooterEmpty = true; - let pageInfoInBody = []; - if (tableElement.length == 1) { - tableElement[0].isPrimaryTable = true; - } else if (tableElement.length > 1) { - let primaryTableEl = tableElement.filter((el) => el.isPrimaryTable); - if (primaryTableEl.length == 1) { - tableElement = primaryTableEl; - } else { - const message = __( - "As You have multiple tables, you have to select Primary Table.

1. Go to Table Element that you wish to set as Primary.

2. Select it and from properties panel select Set as Primary Table as Yes " - ); - frappe.msgprint( - { - title: __("Multiple Tables."), - message: message, - indicator: "red", - }, - 5 - ); - return; - } - } - this.Elements.forEach((element) => { - let is_header = false; - let is_footer = false; - if ( - element.startY + element.height < - MainStore.page.headerHeight + MainStore.page.marginTop - ) { - is_header = true; - } else if ( - element.startY > - MainStore.page.height - - MainStore.page.footerHeight - - MainStore.page.marginTop - - MainStore.page.marginBottom - ) { - is_footer = true; - } else { - if (element.type == "text" && element.isDynamic) { - element.dynamicContent - .filter( - (el) => - ["page", "topage", "date", "time"].indexOf(el.fieldname) != -1 - ) - .forEach((field) => { - pageInfoInBody.push(field.fieldname); - }); - } - } - let printFonts = is_header - ? headerPrintFonts - : is_footer - ? footerprintFonts - : mainPrintFonts; - let newElement = this.childrensSave(element, printFonts); - newElement.classes = newElement.classes.filter( - (name) => ["inHeaderFooter", "overlappingHeaderFooter"].indexOf(name) == -1 - ); - if (element.type == "rectangle" && element.childrens.length) { - let childrensArray = element.childrens; - newElement.childrens = []; - childrensArray.forEach((el) => { - newElement.childrens.push(this.childrensSave(el, printFonts)); - }); - } - if (is_header) { - newElement.printY = newElement.startY + MainStore.page.marginTop; - MainStore.printHeaderFonts = printFonts; - headerElements.push(newElement); - isHeaderEmpty = false; - } else if (is_footer) { - newElement.printY = - newElement.startY - - (MainStore.page.height - - MainStore.page.footerHeight - - MainStore.page.marginBottom - - MainStore.page.marginTop); - MainStore.printFooterFonts = printFonts; - footerElements.push(newElement); - isFooterEmpty = false; - } else { - newElement.printY = newElement.startY - MainStore.page.headerHeight; - MainStore.printBodyFonts = printFonts; - if ( - tableElement.length == 1 && - tableElement[0].startY + tableElement[0].height <= newElement.startY + 2 - ) { - newElement.printY = - newElement.startY - (tableElement[0].startY + tableElement[0].height); - newElement.printX = newElement.startX - tableElement[0].startX; + if (this.checkIfAnyTableIsEmpty()) return; + const [headerElements, bodyElements, footerElements] = await this.computeLayout(); + if (!this.handleHeaderFooterOverlapping(headerElements.flat())) return; + if (!this.handleHeaderFooterOverlapping(bodyElements.flat())) return; + if (!this.handleHeaderFooterOverlapping(footerElements.flat())) return; + + const header = this.cleanUpElementsForSave(headerElements, "header"); + const body = this.cleanUpElementsForSave(bodyElements, "body"); + const footer = this.cleanUpElementsForSave(footerElements, "footer"); + + if (!body) return; + + const [cleanedBodyElements, bodyFonts] = body; + const [cleanedHeaderElements, headerFonts] = header || [[], null]; + const [cleanedFooterElements, footerFonts] = footer || [[], null]; - afterTableElements.push(newElement); - } else { - mainElements.push(newElement); - } - isBodyEmpty = false; - } - }); - if (isHeaderEmpty) { - MainStore.printHeaderFonts = null; - } - if (isBodyEmpty) { - if (!isHeaderEmpty || !isFooterEmpty) { - MainStore.mode = "pdfSetup"; - frappe.show_alert( - { - message: "Atleast 1 element is required inside body", - indicator: "red", - }, - 5 - ); - return; - } - MainStore.printBodyFonts = null; - } - if (pageInfoInBody.length) { - frappe.show_alert({ - message: - "Please move " + pageInfoInBody.join(", ") + " to header / footer", - indicator: "orange", - }); - return; - } - if (isFooterEmpty) { - MainStore.printFooterFonts = null; - } MainStore.currentFonts.length = 0; MainStore.currentFonts.push( ...Object.keys({ - ...(headerPrintFonts || {}), - ...(mainPrintFonts || {}), - ...(footerprintFonts || {}), + ...(headerFonts || {}), + ...(bodyFonts || {}), + ...(footerFonts || {}), }) ); const updatedPage = { ...MainStore.page }; @@ -320,14 +90,17 @@ export const useElementStore = defineStore("ElementStore", { const css = convertCsstoString(MainStore.screenStyleSheet) + convertCsstoString(MainStore.printStyleSheet); - await frappe.db.set_value("Print Format", MainStore.printDesignName, { - print_designer_header: JSON.stringify(headerElements), - print_designer_body: JSON.stringify(mainElements), - print_designer_after_table: JSON.stringify(afterTableElements), - print_designer_footer: JSON.stringify(footerElements), + + const objectToSave = { + print_designer_header: JSON.stringify(cleanedHeaderElements[0]), + print_designer_body: JSON.stringify(cleanedBodyElements.flat()), + print_designer_after_table: null, + print_designer_footer: JSON.stringify(cleanedFooterElements[0]), print_designer_settings: JSON.stringify(settingsForSave), - css, - }); + css: css, + }; + + await frappe.db.set_value("Print Format", MainStore.printDesignName, objectToSave); await frappe.dom.unfreeze(); frappe.show_alert( { @@ -337,6 +110,194 @@ export const useElementStore = defineStore("ElementStore", { 5 ); }, + checkIfAnyTableIsEmpty() { + const emptyTable = this.Elements.find((el) => el.type == "table" && el.table == null); + if (emptyTable) { + let message = __("You have Empty Table. Please add table fields or remove table."); + setCurrentElement({}, emptyTable); + frappe.show_alert( + { + message: message, + indicator: "red", + }, + 5 + ); + return true; + } + return false; + }, + async computeLayout(element = null) { + const MainStore = useMainStore(); + const elements = [...this.Elements].map((el, index) => { + return { + index, + startY: parseInt(el.startY), + endY: parseInt(el.startY + el.height), + element: el, + }; + }); + elements.sort((a, b) => { + return a.startY < b.startY ? -1 : 1; + }); + const fullWidthElements = elements.filter( + (currentEl) => !currentEl.element.isElementOverlapping + ); + const headerContainer = []; + const bodyContainer = []; + const footerContainer = []; + const tempElementsArray = []; + elements.forEach((currentEl) => { + if (MainStore.page.headerHeight && currentEl.endY <= MainStore.page.headerHeight) { + headerContainer.push(currentEl); + } else if ( + MainStore.page.footerHeight && + currentEl.startY >= MainStore.page.height - MainStore.page.footerHeight + ) { + footerContainer.push(currentEl); + } else if ( + fullWidthElements.includes(currentEl) && + currentEl.element.isDynamicHeight + ) { + if (tempElementsArray.length) { + bodyContainer.push([...tempElementsArray]); + } + bodyContainer.push([currentEl]); + tempElementsArray.length = 0; + } else { + tempElementsArray.push(currentEl); + } + }); + if (tempElementsArray.length) { + bodyContainer.push(tempElementsArray); + } + return [[headerContainer], bodyContainer, [footerContainer]]; + }, + handleHeaderFooterOverlapping(elements) { + const MainStore = useMainStore(); + const tableElement = this.Elements.filter((el) => el.type == "table"); + let isOverlapping = false; + + if (tableElement.length == 1 && MainStore.isHeaderFooterAuto) { + isOverlapping = !this.autoCalculateHeaderFooter(tableElement[0]); + } else { + isOverlapping = elements.some((element) => { + element = element.element; + if ( + (element.startY < MainStore.page.headerHeight && + element.startY + element.height > MainStore.page.headerHeight) || + (element.startY < + MainStore.page.height - + MainStore.page.footerHeight - + MainStore.page.marginTop - + MainStore.page.marginBottom && + element.startY + element.height > + MainStore.page.height - + MainStore.page.footerHeight - + MainStore.page.marginTop - + MainStore.page.marginBottom) + ) { + return true; + } + return false; + }); + } + if (!isOverlapping) return true; + MainStore.mode = "pdfSetup"; + frappe.show_alert( + { + message: "Please resolve overlapping header/footer elements", + indicator: "red", + }, + 5 + ); + }, + autoCalculateHeaderFooter(tableEl) { + const MainStore = useMainStore(); + + if (this.isElementOverlapping(tableEl)) return false; + + MainStore.page.headerHeight = tableEl.startY; + MainStore.page.footerHeight = + MainStore.page.height - + (tableEl.startY + + tableEl.height + + MainStore.page.marginTop + + MainStore.page.marginBottom); + + return true; + }, + cleanUpElementsForSave(elements, type) { + if (this.checkIfPrintFormatIsEmpty(elements, type)) return; + const fontsArray = []; + const cleanedElements = []; + elements.forEach((container) => { + const cleanedContainer = []; + container.forEach((element) => { + let newElement = this.childrensSave(element.element, fontsArray); + newElement.classes = newElement.classes.filter( + (name) => ["inHeaderFooter", "overlappingHeaderFooter"].indexOf(name) == -1 + ); + if (element.type == "rectangle" && element.childrens.length) { + let childrensArray = element.childrens; + newElement.childrens = []; + childrensArray.forEach((el) => { + newElement.childrens.push(this.childrensSave(el, printFonts)); + }); + } + cleanedContainer.push(newElement); + }); + cleanedElements.push(cleanedContainer); + }); + return [cleanedElements, fontsArray]; + }, + checkIfPrintFormatIsEmpty(elements, type) { + const MainStore = useMainStore(); + if (elements.length == 0) { + switch (type) { + case "header": + MainStore.printHeaderFonts = null; + break; + case "body": + MainStore.printBodyFonts = null; + frappe.show_alert( + { + message: "Atleast 1 element is required inside body", + indicator: "red", + }, + 5 + ); + // This is intentionally using throw to stop the execution + throw new Error(__("Atleast 1 element is required inside body")); + case "footer": + MainStore.printFooterFonts = null; + break; + } + return true; + } + return false; + }, + childrensSave(element, printFonts) { + let saveEl = { ...element }; + delete saveEl.DOMRef; + delete saveEl.index; + delete saveEl.snapPoints; + delete saveEl.snapEdges; + delete saveEl.parent; + if (printFonts && ["text", "table"].indexOf(saveEl.type) != -1) { + handlePrintFonts(saveEl, printFonts); + } + if (saveEl.type == "rectangle") { + const childrensArray = saveEl.childrens; + saveEl.childrens = []; + childrensArray.forEach((el) => { + const child = this.childrensSave(el, printFonts); + child && saveEl.childrens.push(child); + }); + } + + return saveEl; + }, + handleDynamicContent(element) { const MainStore = useMainStore(); if ( From 7742697cf283524d6b1aeea9c4781d5638594018 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Mar 2024 20:09:32 +0530 Subject: [PATCH 11/40] feat: calculate & create dynamic containers - calculate dimensions of the container based on the childerns dimensions - create wrapper element for the container that will have position relative height will be decided based on it's childerns condition - save this data in print_designer_print_format that will be used by new jinja template. --- .../js/print_designer/store/ElementStore.js | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 8912576..92167df 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -42,6 +42,10 @@ export const useElementStore = defineStore("ElementStore", { if (!this.handleHeaderFooterOverlapping(bodyElements.flat())) return; if (!this.handleHeaderFooterOverlapping(footerElements.flat())) return; + const headerDimensions = this.computeElementDimensions(headerElements, "header"); + const bodyDimensions = this.computeElementDimensions(bodyElements, "body"); + const footerDimensions = this.computeElementDimensions(footerElements, "footer"); + const header = this.cleanUpElementsForSave(headerElements, "header"); const body = this.cleanUpElementsForSave(bodyElements, "body"); const footer = this.cleanUpElementsForSave(footerElements, "footer"); @@ -99,6 +103,22 @@ export const useElementStore = defineStore("ElementStore", { print_designer_settings: JSON.stringify(settingsForSave), css: css, }; + const PrintFormatData = this.getPrintFormatData({ + header: { + elements: cleanedHeaderElements, + dimensions: headerDimensions, + }, + body: { + elements: cleanedBodyElements, + dimensions: bodyDimensions, + }, + footer: { + elements: cleanedFooterElements, + dimensions: footerDimensions, + }, + }); + + objectToSave.print_designer_print_format = PrintFormatData; await frappe.db.set_value("Print Format", MainStore.printDesignName, objectToSave); await frappe.dom.unfreeze(); @@ -226,6 +246,61 @@ export const useElementStore = defineStore("ElementStore", { return true; }, + computeElementDimensions(elements, containerType = "body") { + const dimensions = []; + elements.reduce( + (prevDimensions, container, index) => { + const calculatedDimensions = this.calculateWrapperElementDimensions( + prevDimensions, + container, + containerType, + index + ); + dimensions.push(calculatedDimensions); + return calculatedDimensions; + }, + { top: 0, bottom: 0 } + ); + return dimensions; + }, + calculateWrapperElementDimensions(prevDimensions, children, containerType, index) { + // basically returns lowest left - top highest right - bottom from all of the children elements + const MainStore = useMainStore(); + const parentRect = MainStore.mainContainer.getBoundingClientRect(); + let offsetRect = children.reduce( + (offset, currentElement) => { + currentElement = currentElement.element; + let currentElementRect = currentElement.DOMRef.getBoundingClientRect(); + currentElementRect.left < offset.left && + (offset.left = currentElementRect.left); + currentElementRect.top < offset.top && (offset.top = currentElementRect.top); + currentElementRect.right > offset.right && + (offset.right = currentElementRect.right); + currentElementRect.bottom > offset.bottom && + (offset.bottom = currentElementRect.bottom); + return offset; + }, + { left: 9999, top: 9999, right: 0, bottom: 0 } + ); + (offsetRect.top -= parentRect.top), (offsetRect.left -= parentRect.left); + (offsetRect.right -= parentRect.left), (offsetRect.bottom -= parentRect.top); + + if (containerType == "header") { + offsetRect.top = 0; + offsetRect.bottom = MainStore.page.headerHeight; + } + // if its the first element then update top to header height + // also checking if element is below header ( just safe gaurd ) + if (containerType == "body") { + if (index == 0 && offsetRect.top >= MainStore.page.headerHeight) { + offsetRect.top = MainStore.page.headerHeight; + } + if (index != 0) { + offsetRect.top = prevDimensions.bottom; + } + } + return offsetRect; + }, cleanUpElementsForSave(elements, type) { if (this.checkIfPrintFormatIsEmpty(elements, type)) return; const fontsArray = []; @@ -297,6 +372,54 @@ export const useElementStore = defineStore("ElementStore", { return saveEl; }, + getPrintFormatData({ header, body, footer }) { + const headerElements = this.createWrapperElement( + header.elements, + header.dimensions, + "header" + ); + const bodyElements = this.createWrapperElement(body.elements, body.dimensions, "body"); + const footerElements = this.createWrapperElement( + footer.elements, + footer.dimensions, + "footer" + ); + return JSON.stringify({ + header: headerElements, + body: bodyElements, + footer: footerElements, + }); + }, + createWrapperElement(containers, dimensions, containerType = "body") { + const MainStore = useMainStore(); + const wrapperContainers = { childrens: [] }; + containers.forEach((container, index) => { + const calculatedDimensions = dimensions[index]; + const cordinates = { + startY: calculatedDimensions.top, + pageY: calculatedDimensions.top, + startX: 0, + pageX: 0, + }; + const wrapperRectangleEl = createRectangle(cordinates, wrapperContainers); + wrapperRectangleEl.height = calculatedDimensions.bottom - calculatedDimensions.top; + wrapperRectangleEl.width = + MainStore.page.width - MainStore.page.marginLeft - MainStore.page.marginRight; + wrapperRectangleEl.childrens = container; + if ( + containerType == "body" && + wrapperRectangleEl.childrens.length == 1 && + wrapperRectangleEl.childrens[0].isDynamicHeight == true + ) { + wrapperRectangleEl.isDynamicHeight = true; + } + wrapperRectangleEl.childrens.forEach((el) => { + el.startY -= cordinates.startY; + }); + wrapperRectangleEl.style.backgroundColor = ""; + }); + return wrapperContainers.childrens.map((el) => this.childrensSave(el)); + }, handleDynamicContent(element) { const MainStore = useMainStore(); From d74674c6c0f97f01cf04c8f5bacf56acde0337eb Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Mar 2024 20:14:30 +0530 Subject: [PATCH 12/40] refactor(minor): logic for element load - removed auto generated table element - moved settings load logic to a separate function to avoid race condition and simplify the code --- .../js/print_designer/store/ElementStore.js | 112 ++++-------------- 1 file changed, 25 insertions(+), 87 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 92167df..74e3755 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -478,8 +478,30 @@ export const useElementStore = defineStore("ElementStore", { return element; }, - async loadElements(printDesignName) { + loadSettings(settings) { const MainStore = useMainStore(); + if (!settings) return; + Object.keys(settings).forEach((key) => { + switch (key) { + case "schema_version": + MainStore.old_schema_version = settings["schema_version"]; + case "currentDoc": + frappe.db + .exists(MainStore.doctype, settings["currentDoc"]) + .then((exists) => { + if (exists) { + MainStore.currentDoc = settings["currentDoc"]; + } + }); + break; + default: + MainStore[key] = settings[key]; + break; + } + }); + return; + }, + async loadElements(printDesignName) { frappe.dom.freeze(__("Loading Print Format")); const printFormat = await frappe.db.get_value("Print Format", printDesignName, [ "print_designer_header", @@ -493,97 +515,13 @@ export const useElementStore = defineStore("ElementStore", { let ElementsAfterTable = JSON.parse(printFormat.message.print_designer_after_table); let ElementsFooter = JSON.parse(printFormat.message.print_designer_footer); let settings = JSON.parse(printFormat.message.print_designer_settings); - settings && - Object.keys(settings).forEach(async (key) => { - if ( - ["currentDoc", "schema_version"].indexOf(key) == -1 || - (await frappe.db.exists(MainStore.doctype, settings[key])) - ) { - MainStore[key] = settings[key]; - } - if (key == "schema_version" && settings[key] != MainStore.schema_version) { - MainStore.old_schema_version = settings[key]; - } - }); + this.loadSettings(settings); this.Elements = [ ...(ElementsHeader || []), ...(ElementsBody || []), - ...(ElementsFooter || []), ...(ElementsAfterTable || []), + ...(ElementsFooter || []), ]; - if (this.Elements.length === 0 && !!MainStore.getTableMetaFields.length) { - const newTable = { - id: frappe.utils.get_random(10), - type: "table", - DOMRef: null, - parent: this.Elements, - isDraggable: true, - isResizable: true, - isDropZone: false, - table: null, - columns: [ - { - id: 0, - label: "", - style: {}, - applyStyleToHeader: false, - }, - { - id: 1, - label: "", - style: {}, - applyStyleToHeader: false, - }, - { - id: 2, - label: "", - style: {}, - applyStyleToHeader: false, - }, - { - id: 3, - label: "", - style: {}, - applyStyleToHeader: false, - }, - { - id: 4, - label: "", - style: {}, - applyStyleToHeader: false, - }, - { - id: 5, - label: "", - style: {}, - applyStyleToHeader: false, - }, - { - id: 6, - label: "", - style: {}, - applyStyleToHeader: false, - }, - ], - PreviewRowNo: 1, - selectedColumn: null, - selectedDynamicText: null, - startX: 11.338582677, - startY: 393.826771658, - pageX: 228, - pageY: 435, - width: 771.0236220564, - height: 442.20472441469997, - styleEditMode: "main", - labelDisplayStyle: "standard", - style: {}, - labelStyle: {}, - headerStyle: {}, - altStyle: {}, - classes: [], - }; - this.Elements.push(newTable); - } this.Elements.map((element) => { element.DOMRef = null; element.parent = this.Elements; From f9550162aae7735f536bf4fa78f0b9980419ee1e Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Mar 2024 20:17:50 +0530 Subject: [PATCH 13/40] feat: jinja template for dynamic containers - refactored jinja template and separated it to smaller files for maintainability - added concept of relative containers that are rendered on main/parent array loop. - added logic to decide if a container has dynamicHeight or not - If main/parent container's isDynamicHeight = true if children's type is dynamic-text or table, it will not set height property and set margin-top instead of top property ( top property's space is not calculated as part of parent's DOM height, so it will not work as expected ) - If main/parent container's isDynamicHeight = false logic will be same as before. it will be inside a dynamic container. - new template is not compatible with old schema ( formats created/updated for this PR ). - if print_formats schema version is less then 1.1.0, it will use old template. - misc: removed duplicate code and called get_print_format_template. --- print_designer/pdf.py | 24 +++++-- .../print_designer/jinja/macros/barcode.html | 32 ++++++++++ .../jinja/macros/dynamictext.html | 14 +++++ .../print_designer/jinja/macros/image.html | 14 +++++ .../jinja/macros/rectangle.html | 10 +++ .../jinja/macros/relative_containers.html | 12 ++++ .../print_designer/jinja/macros/render.html | 9 +++ .../jinja/macros/render_element.html | 26 ++++++++ .../jinja/macros/render_google_fonts.html | 26 ++++++++ .../print_designer/jinja/macros/spantag.html | 41 ++++++++++++ .../jinja/macros/statictext.html | 14 +++++ .../print_designer/jinja/macros/styles.html | 62 +++++++++++++++++++ .../print_designer/jinja/macros/table.html | 33 ++++++++++ .../{main.html => old_print_format.html} | 0 .../print_designer/jinja/print_format.html | 25 ++++++++ 15 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/barcode.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/dynamictext.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/image.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/rectangle.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/render.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/render_element.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/render_google_fonts.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/spantag.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/statictext.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/styles.html create mode 100644 print_designer/print_designer/page/print_designer/jinja/macros/table.html rename print_designer/print_designer/page/print_designer/jinja/{main.html => old_print_format.html} (100%) create mode 100644 print_designer/print_designer/page/print_designer/jinja/print_format.html diff --git a/print_designer/pdf.py b/print_designer/pdf.py index 5da4148..fd32773 100644 --- a/print_designer/pdf.py +++ b/print_designer/pdf.py @@ -50,16 +50,24 @@ def pdf_body_html(print_format, jenv, args, template): add_data_to_monitor(print_designer=print_format_name, print_designer_action="download_pdf") # DEPRECATED: remove this in few months added for backward compatibility incase user didn't update frappe framework. if not frappe.get_hooks("get_print_format_template"): - template = jenv.loader.get_source(jenv, "print_designer/page/print_designer/jinja/main.html")[0] + template = get_print_format_template(jenv, print_format) + + settings = json.loads(print_format.print_designer_settings) + args.update( { "headerElement": json.loads(print_format.print_designer_header), "bodyElement": json.loads(print_format.print_designer_body), - "afterTableElement": json.loads(print_format.print_designer_after_table), "footerElement": json.loads(print_format.print_designer_footer), - "settings": json.loads(print_format.print_designer_settings), + "settings": settings, } ) + + if not is_older_schema(settings=settings, current_version="1.1.0"): + args.update({"pd_format": json.loads(print_format.print_designer_print_format)}) + else: + args.update({"afterTableElement": json.loads(print_format.print_designer_after_table or "[]")}) + # replace placeholder comment with user provided jinja code template_source = template.replace( "", args["settings"].get("userProvidedJinja", "") @@ -101,4 +109,12 @@ def is_older_schema(settings, current_version): def get_print_format_template(jenv, print_format): # if print format is created using print designer, then use print designer template if print_format and print_format.print_designer and print_format.print_designer_body: - return jenv.loader.get_source(jenv, "print_designer/page/print_designer/jinja/main.html")[0] + settings = json.loads(print_format.print_designer_settings) + if is_older_schema(settings, "1.1.0"): + return jenv.loader.get_source( + jenv, "print_designer/page/print_designer/jinja/old_print_format.html" + )[0] + else: + return jenv.loader.get_source( + jenv, "print_designer/page/print_designer/jinja/print_format.html" + )[0] diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/barcode.html b/print_designer/print_designer/page/print_designer/jinja/macros/barcode.html new file mode 100644 index 0000000..2044a32 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/barcode.html @@ -0,0 +1,32 @@ +{% macro barcode(element, send_to_jinja) -%} + {%- set field = element.dynamicContent[0] -%} + {%- if field.is_static -%} + {% if field.parseJinja %} + {%- set value = render_user_text(field.value, doc, {}, send_to_jinja).get("message", "") -%} + {% else %} + {%- set value = _(field.value) -%} + {% endif %} + {%- elif field.doctype -%} + {%- set value = frappe.db.get_value(field.doctype, doc[field.parentField], field.fieldname) -%} + {%- else -%} + {%- set value = doc.get_formatted(field.fieldname) -%} + {%- endif -%} + +
+
+ {% if value %}{{get_barcode(element.barcodeFormat, value|string, { + "module_color": element.barcodeColor or "#000000", + "foreground": element.barcodeColor or "#ffffff", + "background": element.barcodeBackgroundColor or "#ffffff", + "quiet_zone": 1, + }).value}}{% endif %} +
+
+{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/dynamictext.html b/print_designer/print_designer/page/print_designer/jinja/macros/dynamictext.html new file mode 100644 index 0000000..2b2c054 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/dynamictext.html @@ -0,0 +1,14 @@ +{% from 'print_designer/page/print_designer/jinja/macros/spantag.html' import span_tag with context %} + +{% macro dynamictext(element, send_to_jinja, is_parent_dynamic_height) -%} +
+
+ {% for field in element.dynamicContent %} + + {{ span_tag(field, element, {}, send_to_jinja)}} + {% endfor %} +
+
+{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/image.html b/print_designer/print_designer/page/print_designer/jinja/macros/image.html new file mode 100644 index 0000000..7662b9f --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/image.html @@ -0,0 +1,14 @@ +{% macro image(element) -%} +
+
+
+{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/rectangle.html b/print_designer/print_designer/page/print_designer/jinja/macros/rectangle.html new file mode 100644 index 0000000..6f15065 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/rectangle.html @@ -0,0 +1,10 @@ +{% macro rectangle(element, render_element, send_to_jinja) -%} +
+ {% if element.childrens %} + {% for object in element.childrens %} + {{ render_element(object, send_to_jinja) }} + {% endfor %} + {% endif %} +
+{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html b/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html new file mode 100644 index 0000000..6327524 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html @@ -0,0 +1,12 @@ +{% from 'print_designer/page/print_designer/jinja/macros/render_element.html' import render_element with context %} + +{% macro relative_containers(element, send_to_jinja) -%} +
+ {% if element.childrens %} + {% for object in element.childrens %} + {{ render_element(object, send_to_jinja, element.get("isDynamicHeight", false)) }} + {% endfor %} + {% endif %} +
+{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/render.html b/print_designer/print_designer/page/print_designer/jinja/macros/render.html new file mode 100644 index 0000000..df73527 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/render.html @@ -0,0 +1,9 @@ +{% from 'print_designer/page/print_designer/jinja/macros/relative_containers.html' import relative_containers with context %} + +{% macro render(elements, send_to_jinja) -%} + {% if element is iterable and (element is not string and element is not mapping) %} + {% for object in elements %} + {{ relative_containers(object, send_to_jinja) }} + {% endfor %} + {% endif %} +{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/render_element.html b/print_designer/print_designer/page/print_designer/jinja/macros/render_element.html new file mode 100644 index 0000000..71b9f14 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/render_element.html @@ -0,0 +1,26 @@ +{% from 'print_designer/page/print_designer/jinja/macros/statictext.html' import statictext with context %} +{% from 'print_designer/page/print_designer/jinja/macros/dynamictext.html' import dynamictext with context %} +{% from 'print_designer/page/print_designer/jinja/macros/spantag.html' import span_tag with context %} +{% from 'print_designer/page/print_designer/jinja/macros/image.html' import image with context %} +{% from 'print_designer/page/print_designer/jinja/macros/barcode.html' import barcode with context %} +{% from 'print_designer/page/print_designer/jinja/macros/rectangle.html' import rectangle with context %} +{% from 'print_designer/page/print_designer/jinja/macros/table.html' import table with context %} + + +{% macro render_element(element, send_to_jinja, is_parent_dynamic_height = false) -%} + {% if element.type == "rectangle" %} + {{ rectangle(element, render_element, send_to_jinja) }} + {% elif element.type == "image" %} + {{image(element)}} + {% elif element.type == "table" %} + {{table(element, send_to_jinja, is_parent_dynamic_height)}} + {% elif element.type == "text" %} + {% if element.isDynamic %} + {{dynamictext(element, send_to_jinja, is_parent_dynamic_height)}} + {% else%} + {{statictext(element, send_to_jinja, is_parent_dynamic_height)}} + {% endif %} + {% elif element.type == "barcode" %} + {{barcode(element, send_to_jinja)}} + {% endif %} +{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/render_google_fonts.html b/print_designer/print_designer/page/print_designer/jinja/macros/render_google_fonts.html new file mode 100644 index 0000000..6b11721 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/render_google_fonts.html @@ -0,0 +1,26 @@ +{% macro getFontStyles(fonts) -%}{%for key, value in fonts.items()%}{{key ~ ':ital,wght@'}}{%for index, size in enumerate(value.weight)%}{%if index > 0%};{%endif%}0,{{size}}{%endfor%}{%for index, size in enumerate(value.italic)%}{%if index > 0%};{%endif%}1,{{size}}{%endfor%}{% if not loop.last %}{{'&display=swap&family='}}{%endif%}{%endfor%}{%- endmacro %} + +{% macro render_google_fonts(settings) %} + + {% if settings.printHeaderFonts %} + + {%endif%} + {% if settings.printBodyFonts %} + + {%endif%} + {% if settings.printFooterFonts %} + + {%endif%} +{% endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/spantag.html b/print_designer/print_designer/page/print_designer/jinja/macros/spantag.html new file mode 100644 index 0000000..04297f5 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/spantag.html @@ -0,0 +1,41 @@ +{% macro page_class(field) %} + {% if field.fieldname in ['page', 'topage', 'time', 'date'] %} + page_info_{{ field.fieldname }} + {% endif %} +{% endmacro %} + +{%- macro spanvalue(field, element, row, send_to_jinja) -%} + {%- if field.is_static -%} + {% if field.parseJinja %} + {{ render_user_text(field.value, doc, row, send_to_jinja).get("message", "") }} + {% else %} + {{ _(field.value) }} + {% endif %} + {%- elif field.doctype -%} + {%- set value = _(frappe.db.get_value(field.doctype, doc[field.parentField], field.fieldname)) -%} + {{ frappe.format(value, {'fieldtype': field.fieldtype, 'options': field.options}) }} + {%- elif row -%} + {{row.get_formatted(field.fieldname)}} + {%- else -%} + {{doc.get_formatted(field.fieldname)}} + {%- endif -%} +{%- endmacro -%} + + +{% macro span_tag(field, element, row = {}, send_to_jinja = {}) -%} + {% set span_value = spanvalue(field, element, row, send_to_jinja) %} + + {% if not field.is_static and field.is_labelled and span_value %} + + {{ _(field.label) }} + + {% endif %} + + {{ span_value }} + + {% if field.nextLine %} +
+ {% endif %} +
+{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/statictext.html b/print_designer/print_designer/page/print_designer/jinja/macros/statictext.html new file mode 100644 index 0000000..3620ba4 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/statictext.html @@ -0,0 +1,14 @@ + +{% macro statictext(element, send_to_jinja, dynamic_containers) -%} +
+

+ {% if element.parseJinja %} + {{ render_user_text(element.content, doc, {}, send_to_jinja).get("message", "") }} + {% else %} + {{_(element.content)}} + {% endif %} +

+
+{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/styles.html b/print_designer/print_designer/page/print_designer/jinja/macros/styles.html new file mode 100644 index 0000000..46ac754 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/styles.html @@ -0,0 +1,62 @@ +{% macro render_styles(settings) %} + +{% endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/table.html b/print_designer/print_designer/page/print_designer/jinja/macros/table.html new file mode 100644 index 0000000..b864d49 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/macros/table.html @@ -0,0 +1,33 @@ +{% macro table(element, send_to_jinja, is_parent_dynamic_height) -%} + + + {% if element.columns %} + + {% for column in element.columns%} + + {% endfor %} + + {% endif %} + + + {% if element.columns %} + {% for row in doc.get(element.table.fieldname)%} + + {% set isLastRow = loop.last %} + {% for column in element.columns%} + + {% endfor %} + + {% endfor %} + {% endif %} + +
+ {{ _(column.label) }} +
+ {% if column is mapping %} + {% for field in column.dynamicContent%} + {{ span_tag(field, element, row, send_to_jinja) }} + {% endfor %} + {% endif %} +
+{%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/main.html b/print_designer/print_designer/page/print_designer/jinja/old_print_format.html similarity index 100% rename from print_designer/print_designer/page/print_designer/jinja/main.html rename to print_designer/print_designer/page/print_designer/jinja/old_print_format.html diff --git a/print_designer/print_designer/page/print_designer/jinja/print_format.html b/print_designer/print_designer/page/print_designer/jinja/print_format.html new file mode 100644 index 0000000..485cb37 --- /dev/null +++ b/print_designer/print_designer/page/print_designer/jinja/print_format.html @@ -0,0 +1,25 @@ +{% from 'print_designer/page/print_designer/jinja/macros/render.html' import render with context %} +{% from 'print_designer/page/print_designer/jinja/macros/render_google_fonts.html' import render_google_fonts with context %} +{% from 'print_designer/page/print_designer/jinja/macros/styles.html' import render_styles with context %} + +{{ render_google_fonts(settings) }} + + + + + +
+
+
+ {% if headerElement %}{{ render(pd_format.header, send_to_jinja) }}{%endif%} +
+
+ {% if bodyElement %}{{ render(pd_format.body, send_to_jinja) }}{%endif%} + +
+ +{{ render_styles(settings) }} \ No newline at end of file From 7924edbcba1867222755066f998079c88d43e53f Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 29 Mar 2024 11:23:28 +0530 Subject: [PATCH 14/40] fix: handle page margin - update page header/footer height with margin before computing layout - update startY of header elements with margin - update footer conditional to account for margin --- .../js/print_designer/store/ElementStore.js | 19 ++++++++++++++----- .../js/print_designer/store/MainStore.js | 2 ++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 74e3755..3a92bc6 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -37,6 +37,13 @@ export const useElementStore = defineStore("ElementStore", { async saveElements() { const MainStore = useMainStore(); if (this.checkIfAnyTableIsEmpty()) return; + + // Update the header and footer height with margin + MainStore.page.headerHeightWithMargin = + MainStore.page.headerHeight + MainStore.page.marginTop; + MainStore.page.footerHeightWithMargin = + MainStore.page.footerHeight + MainStore.page.marginBottom; + const [headerElements, bodyElements, footerElements] = await this.computeLayout(); if (!this.handleHeaderFooterOverlapping(headerElements.flat())) return; if (!this.handleHeaderFooterOverlapping(bodyElements.flat())) return; @@ -65,10 +72,6 @@ export const useElementStore = defineStore("ElementStore", { }) ); const updatedPage = { ...MainStore.page }; - updatedPage.headerHeightWithMargin = - MainStore.page.headerHeight + MainStore.page.marginTop; - updatedPage.footerHeightWithMargin = - MainStore.page.footerHeight + MainStore.page.marginBottom; const settingsForSave = { page: updatedPage, pdfPrintDPI: MainStore.pdfPrintDPI, @@ -171,7 +174,10 @@ export const useElementStore = defineStore("ElementStore", { headerContainer.push(currentEl); } else if ( MainStore.page.footerHeight && - currentEl.startY >= MainStore.page.height - MainStore.page.footerHeight + currentEl.startY >= + MainStore.page.height - + MainStore.page.footerHeightWithMargin - + MainStore.page.marginTop ) { footerContainer.push(currentEl); } else if ( @@ -415,6 +421,9 @@ export const useElementStore = defineStore("ElementStore", { } wrapperRectangleEl.childrens.forEach((el) => { el.startY -= cordinates.startY; + if (containerType == "header") { + el.startY += MainStore.page.marginTop; + } }); wrapperRectangleEl.style.backgroundColor = ""; }); diff --git a/print_designer/public/js/print_designer/store/MainStore.js b/print_designer/public/js/print_designer/store/MainStore.js index 568f3c2..725f6f5 100644 --- a/print_designer/public/js/print_designer/store/MainStore.js +++ b/print_designer/public/js/print_designer/store/MainStore.js @@ -87,6 +87,8 @@ export const useMainStore = defineStore("MainStore", { marginRight: 0, headerHeight: 0, footerHeight: 0, + headerHeightWithMargin: 0, + footerHeightWithMargin: 0, UOM: "mm", }, controls: { From c4e649bdb8e75d985d90c704de3b44e9091339c7 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 29 Mar 2024 11:29:01 +0530 Subject: [PATCH 15/40] fix: styles to avoid print-view layout issues for a long time, print designer formats only looked correct in pdf, not in print-view. this commit fixes print-view to look similar to print designer designs of-course repeating header/footer won't work in print-view, but that's a different issue fixes https://github.com/frappe/print_designer/issues/191 --- .../print_designer/jinja/macros/styles.html | 27 ++++++++++++++++--- .../components/layout/AppCanvas.vue | 6 +++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/styles.html b/print_designer/print_designer/page/print_designer/jinja/macros/styles.html index 46ac754..284901a 100644 --- a/print_designer/print_designer/page/print_designer/jinja/macros/styles.html +++ b/print_designer/print_designer/page/print_designer/jinja/macros/styles.html @@ -1,16 +1,22 @@ {% macro render_styles(settings) %} {% endmacro %} \ No newline at end of file diff --git a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue index 9ef43e5..8d0f24d 100644 --- a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue +++ b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue @@ -526,6 +526,9 @@ onMounted(() => { }); watchEffect(() => { if (MainStore.printStyleSheet && MainStore.page) { + for (let index = 0; index < MainStore.printStyleSheet.cssRules.length; index++) { + MainStore.printStyleSheet.deleteRule(index); + } const convertToMM = (input) => { let convertedUnit = useChangeValueUnit({ inputString: input, @@ -557,6 +560,9 @@ watchEffect(() => { }); watchEffect(() => { if (MainStore.screenStyleSheet && MainStore.modalLocation) { + for (let index = 0; index < MainStore.screenStyleSheet.cssRules.length; index++) { + MainStore.screenStyleSheet.deleteRule(index); + } MainStore.addStylesheetRules([ [ ":root", From cf3fbb83e8f26ee5a2719179dad6abcad791380d Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Sat, 30 Mar 2024 11:25:04 +0530 Subject: [PATCH 16/40] chore: typo in styles.html use css comments syntax :D --- .../page/print_designer/jinja/macros/styles.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/styles.html b/print_designer/print_designer/page/print_designer/jinja/macros/styles.html index 284901a..f6c878a 100644 --- a/print_designer/print_designer/page/print_designer/jinja/macros/styles.html +++ b/print_designer/print_designer/page/print_designer/jinja/macros/styles.html @@ -1,8 +1,8 @@ {% macro render_styles(settings) %} {% endmacro %} \ No newline at end of file From 737604e655a628c51d8a0147ef6452d7ac71cd6c Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Mon, 1 Apr 2024 16:23:33 +0530 Subject: [PATCH 19/40] feat: add columns in dynamic containers This is continuation of the #205 PR. There are some issue with PDF generated by table component and I am working on it. I will update the PR once I fix the issue. --- .../jinja/macros/relative_containers.html | 15 +- .../js/print_designer/PropertiesPanelState.js | 2 +- .../components/base/BaseTable.vue | 1 + .../components/layout/AppCanvas.vue | 11 + .../js/print_designer/store/ElementStore.js | 469 ++++++++++++------ 5 files changed, 352 insertions(+), 146 deletions(-) diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html b/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html index 6327524..87daf0e 100644 --- a/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html +++ b/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html @@ -1,7 +1,7 @@ {% from 'print_designer/page/print_designer/jinja/macros/render_element.html' import render_element with context %} -{% macro relative_containers(element, send_to_jinja) -%} -
{% if element.childrens %} {% for object in element.childrens %} @@ -9,4 +9,15 @@ {% endfor %} {% endif %}
+{%- endmacro %} + +{% macro relative_containers(element, send_to_jinja) -%} +
+ {% if element.childrens %} + {% for object in element.childrens %} + {{ relative_columns(object, send_to_jinja) }} + {% endfor %} + {% endif %} +
{%- endmacro %} \ No newline at end of file diff --git a/print_designer/public/js/print_designer/PropertiesPanelState.js b/print_designer/public/js/print_designer/PropertiesPanelState.js index 31ad16b..2a95ae0 100644 --- a/print_designer/public/js/print_designer/PropertiesPanelState.js +++ b/print_designer/public/js/print_designer/PropertiesPanelState.js @@ -447,7 +447,7 @@ export const createPropertiesPanel = () => { currentEl?.type === "table") || (currentEl.type === "text" && currentEl.isDynamic) ) { - return !currentEl.isElementOverlapping; + return true; } return false; }, diff --git a/print_designer/public/js/print_designer/components/base/BaseTable.vue b/print_designer/public/js/print_designer/components/base/BaseTable.vue index 5e48a9d..6f21eda 100644 --- a/print_designer/public/js/print_designer/components/base/BaseTable.vue +++ b/print_designer/public/js/print_designer/components/base/BaseTable.vue @@ -163,6 +163,7 @@ const { selectedColumn, selectedDynamicText, DOMRef, + isDynamicHeight, } = toRefs(props.object); watch( diff --git a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue index 8d0f24d..4aa0711 100644 --- a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue +++ b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue @@ -644,4 +644,15 @@ watch( margin-right: calc(var(--print-margin-right) * -1); margin-bottom: calc(var(--print-margin-bottom) * -1); } +.relative-row { + background-color: transparent !important; + border: none !important; + z-index: 9999 !important; +} +.relative-column { + background-color: transparent !important; + border: none !important; + z-index: 9999 !important; + outline: 1px solid var(--primary) !important; +} diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 06490b7..ff6d832 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -34,35 +34,54 @@ export const useElementStore = defineStore("ElementStore", { } return newElement; }, - async saveElements() { - const MainStore = useMainStore(); - if (this.checkIfAnyTableIsEmpty()) return; - - // Update the header and footer height with margin - MainStore.page.headerHeightWithMargin = - MainStore.page.headerHeight + MainStore.page.marginTop; - MainStore.page.footerHeightWithMargin = - MainStore.page.footerHeight + MainStore.page.marginBottom; + async computedLayoutForSave() { + const { headerRowElements, bodyRowElements, footerRowElements } = + await this.computeRowLayout(); + // check if any element is overlapping with header or footer and raise errors. + if (!this.handleHeaderFooterOverlapping(headerRowElements.flat())) return; + if (!this.handleHeaderFooterOverlapping(bodyRowElements.flat())) return; + if (!this.handleHeaderFooterOverlapping(footerRowElements.flat())) return; - const [headerElements, bodyElements, footerElements] = await this.computeLayout(); - if (!this.handleHeaderFooterOverlapping(headerElements.flat())) return; - if (!this.handleHeaderFooterOverlapping(bodyElements.flat())) return; - if (!this.handleHeaderFooterOverlapping(footerElements.flat())) return; - - const headerDimensions = this.computeElementDimensions(headerElements, "header"); - const bodyDimensions = this.computeElementDimensions(bodyElements, "body"); - const footerDimensions = this.computeElementDimensions(footerElements, "footer"); - - const header = this.cleanUpElementsForSave(headerElements, "header"); - const body = this.cleanUpElementsForSave(bodyElements, "body"); - const footer = this.cleanUpElementsForSave(footerElements, "footer"); - - if (!body) return; + // calculate dimensions for rows + const headerDimensions = this.computeRowElementDimensions(headerRowElements, "header"); + const bodyDimensions = this.computeRowElementDimensions(bodyRowElements, "body"); + const footerDimensions = this.computeRowElementDimensions(footerRowElements, "footer"); + // calculate columns inside rows and update dimensions to passed array + const headerColumnsInsideRows = await this.computeColumnLayout( + headerRowElements, + headerDimensions + ); + const bodyColumnsInsideRows = await this.computeColumnLayout( + bodyRowElements, + bodyDimensions + ); + const footerColumnsInsideRows = await this.computeColumnLayout( + footerRowElements, + footerDimensions + ); - const [cleanedBodyElements, bodyFonts] = body; - const [cleanedHeaderElements, headerFonts] = header || [[], null]; - const [cleanedFooterElements, footerFonts] = footer || [[], null]; + const headerFonts = []; + const bodyFonts = []; + const footerFonts = []; + // clean up elements for save + const cleanedHeaderElements = this.cleanUpElementsForSave( + headerColumnsInsideRows, + "header", + headerFonts + ); + const cleanedBodyElements = this.cleanUpElementsForSave( + bodyColumnsInsideRows, + "body", + bodyFonts + ); + const cleanedFooterElements = this.cleanUpElementsForSave( + footerColumnsInsideRows, + "footer", + footerFonts + ); + // update fonts in store + const MainStore = useMainStore(); MainStore.currentFonts.length = 0; MainStore.currentFonts.push( ...Object.keys({ @@ -71,6 +90,34 @@ export const useElementStore = defineStore("ElementStore", { ...(footerFonts || {}), }) ); + return { + header: { + layout: cleanedHeaderElements, + dimensions: headerDimensions, + }, + body: { + layout: cleanedBodyElements, + dimensions: bodyDimensions, + }, + footer: { + layout: cleanedFooterElements, + dimensions: footerDimensions, + }, + }; + }, + async saveElements() { + const MainStore = useMainStore(); + if (this.checkIfAnyTableIsEmpty()) return; + + // Update the header and footer height with margin + MainStore.page.headerHeightWithMargin = + MainStore.page.headerHeight + MainStore.page.marginTop; + MainStore.page.footerHeightWithMargin = + MainStore.page.footerHeight + MainStore.page.marginBottom; + const layout = await this.computedLayoutForSave(); + if (!layout) return; + const { header, body, footer } = layout; + const updatedPage = { ...MainStore.page }; const settingsForSave = { page: updatedPage, @@ -99,27 +146,15 @@ export const useElementStore = defineStore("ElementStore", { convertCsstoString(MainStore.printStyleSheet); const objectToSave = { - print_designer_header: JSON.stringify(cleanedHeaderElements[0]), - print_designer_body: JSON.stringify(cleanedBodyElements.flat()), - print_designer_after_table: null, - print_designer_footer: JSON.stringify(cleanedFooterElements[0]), + // flatten the layout array to 2 levels to remove row and column structure + print_designer_header: JSON.stringify(header.layout?.flat(2) || []), + print_designer_body: JSON.stringify(body.layout.flat(2)), + print_designer_footer: JSON.stringify(footer.layout?.flat(2) || []), print_designer_settings: JSON.stringify(settingsForSave), + print_designer_after_table: null, css: css, }; - const PrintFormatData = this.getPrintFormatData({ - header: { - elements: cleanedHeaderElements, - dimensions: headerDimensions, - }, - body: { - elements: cleanedBodyElements, - dimensions: bodyDimensions, - }, - footer: { - elements: cleanedFooterElements, - dimensions: footerDimensions, - }, - }); + const PrintFormatData = this.getPrintFormatData({ header, body, footer }); objectToSave.print_designer_print_format = PrintFormatData; @@ -149,54 +184,111 @@ export const useElementStore = defineStore("ElementStore", { } return false; }, - async computeLayout(element = null) { + async computeRowLayout(columnContainer = null, activeSection = null) { const MainStore = useMainStore(); - const elements = [...this.Elements].map((el, index) => { - return { - index, - startY: parseInt(el.startY), - endY: parseInt(el.startY + el.height), - element: el, - }; - }); - elements.sort((a, b) => { + if (!columnContainer) { + columnContainer = [...this.Elements].map((el, index) => { + return { + index, + startY: parseInt(el.startY), + endY: parseInt(el.startY + el.height), + startX: parseInt(el.startX), + endX: parseInt(el.startX + el.width), + element: el, + }; + }); + } + columnContainer.sort((a, b) => { return a.startY < b.startY ? -1 : 1; }); - const fullWidthElements = elements.filter( - (currentEl) => !currentEl.element.isElementOverlapping - ); - const headerContainer = []; - const bodyContainer = []; - const footerContainer = []; - const tempElementsArray = []; - elements.forEach((currentEl) => { - if (MainStore.page.headerHeight && currentEl.endY <= MainStore.page.headerHeight) { - headerContainer.push(currentEl); - } else if ( - MainStore.page.footerHeight && - currentEl.startY >= - MainStore.page.height - - MainStore.page.footerHeightWithMargin - - MainStore.page.marginTop - ) { - footerContainer.push(currentEl); - } else if ( - fullWidthElements.includes(currentEl) && - currentEl.element.isDynamicHeight - ) { - if (tempElementsArray.length) { - bodyContainer.push([...tempElementsArray]); + return columnContainer.reduce( + (computedLayout, currentEl) => { + let rows = computedLayout[activeSection || computedLayout.activeSection]; + if ( + !activeSection && + computedLayout.activeSection == "headerRowElements" && + currentEl.startY >= MainStore.page.headerHeight + ) { + // handle empty headerRowElements + rows.length == 0 && rows.push([]); + // change activeSection and rows to bodyRowElements + computedLayout.activeSection = "bodyRowElements"; + rows = computedLayout["bodyRowElements"]; } - bodyContainer.push([currentEl]); - tempElementsArray.length = 0; - } else { - tempElementsArray.push(currentEl); + if ( + !activeSection && + computedLayout.activeSection == "bodyRowElements" && + currentEl.startY >= + MainStore.page.height - + MainStore.page.marginTop - + MainStore.page.footerHeightWithMargin + ) { + // no need to handle empty bodyRowElements as it will throw error and never reach here + // change activeSection and rows to footerRowElements + computedLayout.activeSection = "footerRowElements"; + rows = computedLayout["footerRowElements"]; + } + if (rows.length == 0) { + rows.push([currentEl]); + return computedLayout; + } + + // replace with .at() after checking compatibility for our user base. + const lastRow = rows[rows.length - 1]; + const elementWithMaxEndY = lastRow[lastRow.length - 1]; + + if (currentEl.startY >= elementWithMaxEndY.endY) { + rows.push([currentEl]); + return computedLayout; + } + + if (currentEl.endY > elementWithMaxEndY.endY) { + lastRow.push(currentEl); + } else { + lastRow.splice(-1, 0, currentEl); + } + + return computedLayout; + }, + { + headerRowElements: [], + bodyRowElements: [], + footerRowElements: [], + activeSection: "headerRowElements", } + ); + }, + async computeColumnLayout(rows, rowDimensions) { + const columns = rows.map((elements) => { + elements.sort((a, b) => { + return a.startX < b.startX ? -1 : 1; + }); + return elements.reduce((columns, currentEl) => { + if (columns.length == 0) { + columns.push([currentEl]); + return columns; + } + // replace with .at() after checking compatibility for our user base. + const lastColumn = columns[columns.length - 1]; + const elementWithMaxEndX = lastColumn[lastColumn.length - 1]; + + if (currentEl.startX >= elementWithMaxEndX.endX) { + columns.push([currentEl]); + return columns; + } + if (currentEl.endX > elementWithMaxEndX.endX) { + lastColumn.push(currentEl); + } else { + lastColumn.splice(-1, 0, currentEl); + } + return columns; + }, []); }); - if (tempElementsArray.length) { - bodyContainer.push(tempElementsArray); - } - return [[headerContainer], bodyContainer, [footerContainer]]; + // sort elements inside columns by startY + columns.forEach((column) => column.sort((a, b) => (a.startY < b.startY ? -1 : 1))); + // This will add column dimensions under key columnDimensions inside bodyDimensions. + this.computeColumnElementDimensions(columns, rowDimensions); + return columns; }, handleHeaderFooterOverlapping(elements) { const MainStore = useMainStore(); @@ -252,7 +344,7 @@ export const useElementStore = defineStore("ElementStore", { return true; }, - computeElementDimensions(elements, containerType = "body") { + computeRowElementDimensions(elements, containerType = "body") { const dimensions = []; elements.reduce( (prevDimensions, container, index) => { @@ -265,10 +357,35 @@ export const useElementStore = defineStore("ElementStore", { dimensions.push(calculatedDimensions); return calculatedDimensions; }, - { top: 0, bottom: 0 } + { bottom: 0 } ); return dimensions; }, + computeColumnElementDimensions(rows, rowDimensions) { + const MainStore = useMainStore(); + rows.forEach((row, index) => { + const dimensions = []; + row.reduceRight( + (prevDimensions, container, index) => { + const calculatedDimensions = this.calculateWrapperElementDimensions( + prevDimensions, + container, + "column", + index + ); + dimensions.push(calculatedDimensions); + return calculatedDimensions; + }, + { + left: + MainStore.page.width - + MainStore.page.marginRight - + MainStore.page.marginLeft, + } + ); + rowDimensions[index]["columnDimensions"] = dimensions.reverse(); + }); + }, calculateWrapperElementDimensions(prevDimensions, children, containerType, index) { // basically returns lowest left - top highest right - bottom from all of the children elements const MainStore = useMainStore(); @@ -303,45 +420,49 @@ export const useElementStore = defineStore("ElementStore", { (offsetRect.top -= parentRect.top), (offsetRect.left -= parentRect.left); (offsetRect.right -= parentRect.left), (offsetRect.bottom -= parentRect.top); - if (containerType == "header") { + if (containerType == "header" && index == 0) { offsetRect.top = 0; - offsetRect.bottom = MainStore.page.headerHeight; } - // if its the first element then update top to header height - // also checking if element is below header ( just safe guard ) if (containerType == "body") { if (index == 0 && offsetRect.top >= MainStore.page.headerHeight) { offsetRect.top = MainStore.page.headerHeight; } - if (index != 0) { - offsetRect.top = prevDimensions.bottom; - } + } + if (containerType == "footer" && index == 0) { + offsetRect.top = + MainStore.page.height - + MainStore.page.footerHeightWithMargin - + MainStore.page.marginTop; + } + if (index != 0) { + offsetRect.top = prevDimensions.bottom; + } + if (containerType == "column") { + offsetRect.right = prevDimensions.left; } return offsetRect; }, - cleanUpElementsForSave(elements, type) { - if (this.checkIfPrintFormatIsEmpty(elements, type)) return; - const fontsArray = []; - const cleanedElements = []; - elements.forEach((container) => { - const cleanedContainer = []; - container.forEach((element) => { - let newElement = this.childrensSave(element.element, fontsArray); - newElement.classes = newElement.classes.filter( - (name) => ["inHeaderFooter", "overlappingHeaderFooter"].indexOf(name) == -1 - ); - if (element.type == "rectangle" && element.childrens.length) { - let childrensArray = element.childrens; - newElement.childrens = []; - childrensArray.forEach((el) => { - newElement.childrens.push(this.childrensSave(el, printFonts)); - }); - } - cleanedContainer.push(newElement); + cleanUpElementsForSave(rows, type, fontsArray = null) { + if (this.checkIfPrintFormatIsEmpty(rows, type)) return; + return rows.map((columns) => { + return columns.map((column) => { + return column.map((element) => { + let newElement = this.childrensSave(element.element, fontsArray); + newElement.classes = newElement.classes.filter( + (name) => + ["inHeaderFooter", "overlappingHeaderFooter"].indexOf(name) == -1 + ); + if (element.type == "rectangle" && element.childrens.length) { + let childrensArray = element.childrens; + newElement.childrens = []; + childrensArray.forEach((el) => { + newElement.childrens.push(this.childrensSave(el, printFonts)); + }); + } + return newElement; + }); }); - cleanedElements.push(cleanedContainer); }); - return [cleanedElements, fontsArray]; }, checkIfPrintFormatIsEmpty(elements, type) { const MainStore = useMainStore(); @@ -369,7 +490,7 @@ export const useElementStore = defineStore("ElementStore", { } return false; }, - childrensSave(element, printFonts) { + childrensSave(element, printFonts = null) { let saveEl = { ...element }; delete saveEl.DOMRef; delete saveEl.index; @@ -391,27 +512,42 @@ export const useElementStore = defineStore("ElementStore", { return saveEl; }, getPrintFormatData({ header, body, footer }) { - const headerElements = this.createWrapperElement( - header.elements, + const headerElements = this.createRowWrapperElement( + header.layout, header.dimensions, "header" ); - const bodyElements = this.createWrapperElement(body.elements, body.dimensions, "body"); - const footerElements = this.createWrapperElement( - footer.elements, + const bodyElements = this.createRowWrapperElement( + body.layout, + body.dimensions, + "body" + ); + const footerElements = this.createRowWrapperElement( + footer.layout, footer.dimensions, "footer" ); - return JSON.stringify({ + const data = JSON.stringify({ header: headerElements, body: bodyElements, footer: footerElements, }); + const layoutElements = [...headerElements, ...bodyElements, ...footerElements]; + this.Elements.push( + ...layoutElements.map((row) => { + row.childrens.map((column) => { + column.childrens = []; + }); + return this.childrensLoad(row); + }) + ); + return data; }, - createWrapperElement(containers, dimensions, containerType = "body") { + createRowWrapperElement(rows, dimensions, containerType = "body") { + if (!rows) return []; const MainStore = useMainStore(); - const wrapperContainers = { childrens: [] }; - containers.forEach((container, index) => { + const wrapperContainer = { childrens: [] }; + rows.forEach((row, index) => { const calculatedDimensions = dimensions[index]; const cordinates = { startY: calculatedDimensions.top, @@ -419,29 +555,64 @@ export const useElementStore = defineStore("ElementStore", { startX: 0, pageX: 0, }; - const wrapperRectangleEl = createRectangle(cordinates, wrapperContainers); - wrapperRectangleEl.height = calculatedDimensions.bottom - calculatedDimensions.top; + const wrapperRectangleEl = createRectangle(cordinates, wrapperContainer); wrapperRectangleEl.width = MainStore.page.width - MainStore.page.marginLeft - MainStore.page.marginRight; - wrapperRectangleEl.childrens = container; + wrapperRectangleEl.height = calculatedDimensions.bottom - calculatedDimensions.top; + wrapperRectangleEl.childrens = this.createColumnWrapperElement( + row, + calculatedDimensions.columnDimensions, + wrapperRectangleEl + ); + if (wrapperRectangleEl.childrens.some((el) => el.isDynamicHeight == true)) { + wrapperRectangleEl.isDynamicHeight = true; + } + wrapperRectangleEl.classes.push("relative-row"); + }); + return wrapperContainer.childrens.map((el) => this.childrensSave(el)); + }, + createColumnWrapperElement(row, dimensions, rowContainer = null) { + return row.map((column, index) => { + const calculatedDimensions = dimensions[index]; + if (index == 0) { + calculatedDimensions.left = 0; + } + const cordinates = { + startY: 0, // parentDimensions.top, + pageY: 0, + startX: calculatedDimensions.left, + pageX: calculatedDimensions.left, + }; + const wrapperRectangleEl = createRectangle(cordinates, rowContainer); + wrapperRectangleEl.width = calculatedDimensions.right - calculatedDimensions.left; + wrapperRectangleEl.height = rowContainer.height; + wrapperRectangleEl.childrens = column; if ( - containerType == "body" && wrapperRectangleEl.childrens.length == 1 && wrapperRectangleEl.childrens[0].isDynamicHeight == true ) { wrapperRectangleEl.isDynamicHeight = true; } wrapperRectangleEl.childrens.forEach((el) => { - el.startY -= cordinates.startY; - if (containerType == "header") { - el.startY += MainStore.page.marginTop; - } + el.startY -= rowContainer.startY; + el.startX -= cordinates.startX; + ["startX", "startY", "height", "width"].forEach((property) => { + if (typeof el[property] == "string") { + el[property] = parseFloat(el[property]); + } + el[property] = parseFloat(el[property].toFixed(3)); + }); }); - wrapperRectangleEl.style.backgroundColor = ""; + ["startX", "startY", "height", "width"].forEach((property) => { + wrapperRectangleEl[property] = parseFloat( + wrapperRectangleEl[property].toFixed(3) + ); + }); + wrapperRectangleEl.classes.push("relative-column"); + wrapperRectangleEl.relativeColumn = true; + return wrapperRectangleEl; }); - return wrapperContainers.childrens.map((el) => this.childrensSave(el)); }, - handleDynamicContent(element) { const MainStore = useMainStore(); if ( @@ -576,7 +747,8 @@ export const useElementStore = defineStore("ElementStore", { t.isPrimaryTable = t == tableEl; }); }, - // This is called to check if the element is overlapping with any other element + // This is called to check if the element is overlapping with any other element (row only) + // TODO: add column calculations isElementOverlapping(currentEl, elements = this.Elements) { const currentElIndex = currentEl.index || this.Elements.findIndex((el) => el === currentEl); @@ -588,13 +760,24 @@ export const useElementStore = defineStore("ElementStore", { if (index == currentElIndex) return false; const elStartY = parseInt(el.startY); const elEndY = el.endY || parseInt(el.startY + el.height); - if (currentStartY <= elStartY && elStartY <= currentEndY) { - return true; - } else if (currentStartY <= elEndY && elEndY <= currentEndY) { + if ( + currentStartY <= elStartY && + elStartY <= currentEndY && + !(currentStartY <= elEndY && elEndY <= currentEndY) + ) { return true; - } else if (elStartY <= currentStartY && currentStartY <= elEndY) { + } else if ( + !(currentStartY <= elStartY && elStartY <= currentEndY) && + currentStartY <= elEndY && + elEndY <= currentEndY + ) { return true; - } else if (elStartY <= currentEndY && currentEndY <= elEndY) { + } else if ( + elStartY <= currentStartY && + currentStartY <= elEndY && + elStartY <= currentEndY && + currentEndY <= elEndY + ) { return true; } else { return false; From bf7a6b17c83aeb7a16513047a166896477c1c322 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Mon, 1 Apr 2024 17:48:14 +0530 Subject: [PATCH 20/40] fix: handle save of print format with old schema on save of older schema show dialog and make copy of print format it will keep making copy with incremented version number ( Copy 1 ), ( Copy 2 ), ( Copy 3 ) etc. misc: added try catch block in removing deleteRule --- .../components/layout/AppCanvas.vue | 18 ++++- .../js/print_designer/store/ElementStore.js | 73 ++++++++++++++++--- .../js/print_designer/store/MainStore.js | 1 + 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue index 8d0f24d..4828bc3 100644 --- a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue +++ b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue @@ -466,7 +466,11 @@ onMounted(() => { watchEffect(() => { if (MainStore.screenStyleSheet) { if (MainStore.screenStyleSheet.CssRuleIndex != null) { - MainStore.screenStyleSheet.deleteRule(MainStore.screenStyleSheet.CssRuleIndex); + try { + MainStore.screenStyleSheet.deleteRule(MainStore.screenStyleSheet.CssRuleIndex); + } catch (error) { + console.warn("Error Deleting Rule", error); + } } MainStore.screenStyleSheet.CssRuleIndex = MainStore.addStylesheetRules([ [ @@ -527,7 +531,11 @@ onMounted(() => { watchEffect(() => { if (MainStore.printStyleSheet && MainStore.page) { for (let index = 0; index < MainStore.printStyleSheet.cssRules.length; index++) { - MainStore.printStyleSheet.deleteRule(index); + try { + MainStore.printStyleSheet.deleteRule(index); + } catch (error) { + console.warn("Error Deleting Rule", error); + } } const convertToMM = (input) => { let convertedUnit = useChangeValueUnit({ @@ -561,7 +569,11 @@ watchEffect(() => { watchEffect(() => { if (MainStore.screenStyleSheet && MainStore.modalLocation) { for (let index = 0; index < MainStore.screenStyleSheet.cssRules.length; index++) { - MainStore.screenStyleSheet.deleteRule(index); + try { + MainStore.screenStyleSheet.deleteRule(index); + } catch (error) { + console.warn("Error Deleting Rule", error); + } } MainStore.addStylesheetRules([ [ diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 06490b7..40b8b51 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -87,7 +87,6 @@ export const useElementStore = defineStore("ElementStore", { userProvidedJinja: MainStore.userProvidedJinja, schema_version: MainStore.schema_version, }; - await frappe.dom.freeze(); const convertCsstoString = (stylesheet) => { let cssRule = Array.from(stylesheet.cssRules) .map((rule) => rule.cssText || "") @@ -122,16 +121,18 @@ export const useElementStore = defineStore("ElementStore", { }); objectToSave.print_designer_print_format = PrintFormatData; - - await frappe.db.set_value("Print Format", MainStore.printDesignName, objectToSave); - await frappe.dom.unfreeze(); - frappe.show_alert( - { - message: `Print Format Saved Successfully`, - indicator: "green", - }, - 5 - ); + if (MainStore.isOlderSchema("1.1.0")) { + await this.printFormatCopyOnOlderSchema(objectToSave); + } else { + await frappe.db.set_value("Print Format", MainStore.printDesignName, objectToSave); + frappe.show_alert( + { + message: `Print Format Saved Successfully`, + indicator: "green", + }, + 5 + ); + } }, checkIfAnyTableIsEmpty() { const emptyTable = this.Elements.find((el) => el.type == "table" && el.table == null); @@ -441,6 +442,56 @@ export const useElementStore = defineStore("ElementStore", { }); return wrapperContainers.childrens.map((el) => this.childrensSave(el)); }, + async printFormatCopyOnOlderSchema(objectToSave) { + const MainStore = useMainStore(); + let nextFormatCopyNumber = 0; + for (let i = 0; i < 100; i++) { + const pf_exists = await frappe.db.exists( + "Print Format", + MainStore.printDesignName + " ( Copy " + (i ? i : "") + " )" + ); + if (pf_exists) continue; + nextFormatCopyNumber = i; + break; + } + const newName = + MainStore.printDesignName + + " ( Copy " + + (nextFormatCopyNumber ? nextFormatCopyNumber : "") + + " )"; + // TODO: have better message. + let message = __( + "This Print Format was created from older version of Print Designer." + ); + message += "
"; + message += __( + "It is not compatible with current version so instead we will make copy of this format for you using new version" + ); + message += "
"; + message += __(`Do you want to save it as ${newName} ?`); + + frappe.confirm( + message, + async () => { + await frappe.db.insert({ + doctype: "Print Format", + name: newName, + doc_type: MainStore.doctype, + print_designer: 1, + print_designer_header: objectToSave.print_designer_header, + print_designer_body: objectToSave.print_designer_body, + print_designer_after_table: null, + print_designer_footer: objectToSave.print_designer_footer, + print_designer_print_format: objectToSave.print_designer_print_format, + print_designer_settings: objectToSave.print_designer_settings, + }); + frappe.set_route("print-designer", newName); + }, + async () => { + throw new Error(__("Print Format not saved")); + } + ); + }, handleDynamicContent(element) { const MainStore = useMainStore(); diff --git a/print_designer/public/js/print_designer/store/MainStore.js b/print_designer/public/js/print_designer/store/MainStore.js index 725f6f5..af50f21 100644 --- a/print_designer/public/js/print_designer/store/MainStore.js +++ b/print_designer/public/js/print_designer/store/MainStore.js @@ -451,6 +451,7 @@ export const useMainStore = defineStore("MainStore", { isOlderSchema: (state) => (currentVersion) => { if (!state.old_schema_version) return false; let formatVersion = state.old_schema_version.split("."); + if (currentVersion == formatVersion) return false; currentVersion = currentVersion.split("."); if (parseInt(formatVersion[0]) < parseInt(currentVersion[0])) { return true; From 858919e82616464bdb55ac9a62b75f0055528b9a Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 15 Mar 2024 16:01:58 +0530 Subject: [PATCH 21/40] feat: Import / Export standard formats - Added Custom Field for Print Designer Template Location - Added Hook to define folder location for Print Designer Templates ( location fallbacks to default_templates if not defined in hooks for export ) - when print designer is installed, loop over all apps and import standard templates. - when any new app is installed, check if it has print designer templates and install them. - it also checks print_designer app for new templates and installs them. - Modified export functionality for print formats that are made using print_designer. - it will export print format in custom directory if defined in hooks else in default_templates folder. - user / developer can define app where print format should be exported in print_designer_template_app field. --- print_designer/custom_fields.py | 8 +++ print_designer/default_formats.py | 72 +++++++++++++++++++ print_designer/hooks.py | 9 ++- print_designer/install.py | 7 ++ print_designer/patches.txt | 1 + .../patches/create_custom_fields.py | 7 ++ .../client_scripts/print_format.js | 10 +++ .../print_designer/overrides/print_format.py | 54 ++++++++++++++ 8 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 print_designer/default_formats.py create mode 100644 print_designer/patches/create_custom_fields.py create mode 100644 print_designer/print_designer/overrides/print_format.py diff --git a/print_designer/custom_fields.py b/print_designer/custom_fields.py index 981b556..b511c30 100644 --- a/print_designer/custom_fields.py +++ b/print_designer/custom_fields.py @@ -44,5 +44,13 @@ "fieldtype": "JSON", "label": "Print Designer Settings", }, + { + "depends_on": "eval:doc.print_designer && doc.standard == 'Yes'", + "fieldname": "print_designer_template_app", + "fieldtype": "Select", + "label": "Print Designer Template Location", + "default": "print_designer", + "insert_after": "standard", + }, ] } diff --git a/print_designer/default_formats.py b/print_designer/default_formats.py new file mode 100644 index 0000000..6393a04 --- /dev/null +++ b/print_designer/default_formats.py @@ -0,0 +1,72 @@ +import os +from pathlib import Path + +import frappe +from frappe.modules.import_file import import_file_by_path + +""" +features: + - Print Designer App can have default formats for all installed apps. + - Any Custom/Standard App can have default formats for any installed apps + ( This will only install formats if print_designer is installed ). + - This will be useful when we have standalone formats that can be used without print designer app. + +when print_designer app is installed + - get hooks from all installed apps including pd and load default formats from defined folders. + +when any new app is installed + - if exists in print_designer/default_templates, load default formats for newly installed app. + - get hooks from new app and load default formats for all installed apps from app's format dir. +""" + +# TODO: handle override of default formats from different apps or even Custom Formats with same name. + +# add default formats for all installed apps. +def on_print_designer_install(): + for app in frappe.get_installed_apps(): + install_default_formats(app=app, load_pd_formats=False) + + +# called after install of any new app. +def install_default_formats(app, filter_by="", load_pd_formats=True): + if load_pd_formats: + # load formats from print_designer app if some new app is installed and have default formats + install_default_formats(app="print_designer", filter_by=app, load_pd_formats=False) + + # get dir path and load formats from installed app + pd_folder = frappe.get_hooks("pd_standard_format_folder", app_name=app) + if len(pd_folder) == 0: + return + + print_formats = get_filtered_formats_by_app( + app=app, templates_folder=pd_folder[0], filter_by=filter_by + ) + + for json_file_path in print_formats: + import_file_by_path(json_file_path) + frappe.db.commit() + + +def get_filtered_formats_by_app(app, templates_folder, filter_by=""): + app_path = frappe.get_app_path(app) + if filter_by == "": + folders = Path(os.path.join(app_path, templates_folder)) + return get_formats_from_folders(folders=folders) + else: + folder = Path(os.path.join(app_path, templates_folder, filter_by)) + return get_json_files(folder) + + +def get_formats_from_folders(folders): + formats = set() + for folder in folders.iterdir(): + if folder.is_dir() and folder.name in frappe.get_installed_apps(): + formats.update(get_json_files(folder)) + return formats + + +def get_json_files(folder): + formats = set() + for json_file in folder.glob("*.json"): + formats.add(json_file) + return formats diff --git a/print_designer/hooks.py b/print_designer/hooks.py index 68e9d5e..4b1079b 100644 --- a/print_designer/hooks.py +++ b/print_designer/hooks.py @@ -72,6 +72,7 @@ before_install = "print_designer.install.before_install" after_install = "print_designer.install.after_install" +after_app_install = "print_designer.install.after_app_install" # Uninstallation # ------------ @@ -110,10 +111,12 @@ # --------------- # Override standard doctype classes -# override_doctype_class = { -# "ToDo": "custom_app.overrides.CustomToDo" -# } +override_doctype_class = { + "Print Format": "print_designer.print_designer.overrides.print_format.PDPrintFormat", +} +# Path Relative to the app folder where default templates should be stored +pd_standard_format_folder = "default_templates" # Document Events # --------------- # Hook on document methods and events diff --git a/print_designer/install.py b/print_designer/install.py index af9200c..22ebc80 100644 --- a/print_designer/install.py +++ b/print_designer/install.py @@ -3,6 +3,7 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from print_designer.custom_fields import CUSTOM_FIELDS +from print_designer.default_formats import install_default_formats, on_print_designer_install def check_frappe_version(): @@ -27,3 +28,9 @@ def before_install(): def after_install(): create_custom_fields(CUSTOM_FIELDS, ignore_validate=True) + on_print_designer_install() + + +def after_app_install(app): + if app != "print_designer": + install_default_formats(app) diff --git a/print_designer/patches.txt b/print_designer/patches.txt index 1c006ff..6f8c620 100644 --- a/print_designer/patches.txt +++ b/print_designer/patches.txt @@ -9,3 +9,4 @@ print_designer.patches.introduce_suffix_dynamic_content print_designer.patches.introduce_dynamic_containers print_designer.patches.introduce_dynamic_height print_designer.patches.remove_unused_rectangle_gs_properties +execute:from print_designer.patches.create_custom_fields import custom_field_patch; custom_field_patch() diff --git a/print_designer/patches/create_custom_fields.py b/print_designer/patches/create_custom_fields.py new file mode 100644 index 0000000..304b916 --- /dev/null +++ b/print_designer/patches/create_custom_fields.py @@ -0,0 +1,7 @@ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + +from print_designer.custom_fields import CUSTOM_FIELDS + + +def custom_field_patch(): + create_custom_fields(CUSTOM_FIELDS, ignore_validate=True) diff --git a/print_designer/print_designer/client_scripts/print_format.js b/print_designer/print_designer/client_scripts/print_format.js index 6e88365..c4bacc9 100644 --- a/print_designer/print_designer/client_scripts/print_format.js +++ b/print_designer/print_designer/client_scripts/print_format.js @@ -1,6 +1,16 @@ +const set_template_app_options = (frm) => { + frappe.xcall("frappe.core.doctype.module_def.module_def.get_installed_apps").then((r) => { + frm.set_df_property("print_designer_template_app", "options", JSON.parse(r)); + if (!frm.doc.print_designer_template_app) { + frm.set_value("print_designer_template_app", "print_designer"); + } + }); +}; + frappe.ui.form.on("Print Format", { refresh: function (frm) { frm.trigger("render_buttons"); + set_template_app_options(frm); }, render_buttons: function (frm) { frm.page.clear_inner_toolbar(); diff --git a/print_designer/print_designer/overrides/print_format.py b/print_designer/print_designer/overrides/print_format.py new file mode 100644 index 0000000..c9fa03f --- /dev/null +++ b/print_designer/print_designer/overrides/print_format.py @@ -0,0 +1,54 @@ +import os + +import frappe +from frappe.modules.utils import scrub +from frappe.printing.doctype.print_format.print_format import PrintFormat + + +class PDPrintFormat(PrintFormat): + def export_doc(self): + if ( + not self.standard + or not frappe.conf.developer_mode + or frappe.flags.in_patch + or frappe.flags.in_install + or frappe.flags.in_migrate + or frappe.flags.in_import + or frappe.flags.in_setup_wizard + ): + return + + if not self.print_designer: + return super().export_doc() + + self.write_document_file() + + def write_document_file(self): + doc = self + doc_export = doc.as_dict(no_nulls=True) + doc.run_method("before_export", doc_export) + + # create folder + folder = self.create_folder(doc.doc_type, doc.name) + + fname = scrub(doc.name) + + # write the data file + path = os.path.join(folder, f"{fname}.json") + with open(path, "w+") as jsonfile: + jsonfile.write(frappe.as_json(doc_export)) + print(f"Wrote document file for {doc.doctype} {doc.name} at {path}") + + def create_folder(self, dt, dn): + app = scrub(frappe.get_doctype_app(dt)) + dn = scrub(dn) + pd_folder = frappe.get_hooks( + "pd_standard_format_folder", app_name=self.print_designer_template_app + ) + if len(pd_folder) == 0: + pd_folder = ["default_templates"] + folder = os.path.join( + frappe.get_app_path(self.print_designer_template_app), os.path.join(pd_folder[0], app) + ) + frappe.create_folder(folder) + return folder From 5a716fcf77aa2a56f4af050c086dbb1510bed452 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Sun, 17 Mar 2024 21:51:27 +0530 Subject: [PATCH 22/40] feat: generate and store format pdf preview. added new custom field "print_designer_preview_img" to store the format preview image. added html2canvas on save - make copy of main-container dom and convert to canvas using html2canvas. - create file instance and pass blob to it from canvas.toBlob() - send file to /api/method/upload_file , client side function is just modified version of FileUploader. - remove old format preview image after new one is uploaded successfully. This will be used to display the format preview in the format Grid View. --- package.json | 3 +- print_designer/custom_fields.py | 6 ++ .../js/print_designer/store/ElementStore.js | 81 +++++++++++++++++++ yarn.lock | 34 ++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bd1cd93..e7e7c5f 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,11 @@ "author": "Frappe Technologies Pvt. Ltd.", "license": "AGPL-3.0-or-later", "dependencies": { - "@interactjs/interact": "^1.10.17", "@interactjs/actions": "^1.10.17", "@interactjs/auto-start": "^1.10.17", + "@interactjs/interact": "^1.10.17", "@interactjs/modifiers": "^1.10.17", + "html2canvas": "^1.4.1", "pdfjs-dist": "v3.4.120" } } diff --git a/print_designer/custom_fields.py b/print_designer/custom_fields.py index b511c30..c0c1045 100644 --- a/print_designer/custom_fields.py +++ b/print_designer/custom_fields.py @@ -44,6 +44,12 @@ "fieldtype": "JSON", "label": "Print Designer Settings", }, + { + "fieldname": "print_designer_preview_img", + "hidden": 1, + "fieldtype": "Attach Image", + "label": "Print Designer Preview Image", + }, { "depends_on": "eval:doc.print_designer && doc.standard == 'Yes'", "fieldname": "print_designer_template_app", diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 40b8b51..b358fc7 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -9,6 +9,9 @@ import { createBarcode, } from "../defaultObjects"; import { handlePrintFonts, setCurrentElement } from "../utils"; + +import html2canvas from "html2canvas"; + export const useElementStore = defineStore("ElementStore", { state: () => ({ Elements: new Array(), @@ -34,6 +37,83 @@ export const useElementStore = defineStore("ElementStore", { } return newElement; }, + // This is mofiied version of upload function used in frappe/FileUploader.vue + async upload_file(file) { + const MainStore = useMainStore(); + const filter = { + attached_to_doctype: "Print Format", + attached_to_name: MainStore.printDesignName, + attached_to_field: "print_designer_preview_img", + }; + // get filename before uploading new file + let old_filename = await frappe.db.get_value("File", filter, "name"); + old_filename = old_filename.message.name; + + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState == XMLHttpRequest.DONE) { + if (xhr.status === 200) { + // delete old preview image when new image is successfully uploaded + old_filename && frappe.db.delete_doc("File", old_filename); + } + } + }; + xhr.open("POST", "/api/method/upload_file", true); + xhr.setRequestHeader("Accept", "application/json"); + xhr.setRequestHeader("X-Frappe-CSRF-Token", frappe.csrf_token); + + let form_data = new FormData(); + if (file.file_obj) { + form_data.append("file", file.file_obj, file.name); + } + form_data.append("is_private", 1); + + form_data.append("doctype", "Print Format"); + form_data.append("docname", MainStore.printDesignName); + + form_data.append("fieldname", "print_designer_preview_img"); + + if (file.optimize) { + form_data.append("optimize", true); + } + xhr.send(form_data); + }); + }, + async generatePreview() { + const MainStore = useMainStore(); + const options = { + backgroundColor: "#ffffff", + height: MainStore.page.height / 2, + width: MainStore.page.width, + }; + const print_stylesheet = document.createElement("style"); + print_stylesheet.rel = "stylesheet"; + let st = `.main-container::after { + display: none; + }`; + document.getElementsByClassName("main-container")[0].appendChild(print_stylesheet); + print_stylesheet.sheet.insertRule(st, 0); + const preview_canvas = await html2canvas( + document.getElementsByClassName("main-container")[0], + options + ); + document.getElementsByClassName("main-container")[0].removeChild(print_stylesheet); + preview_canvas.toBlob((blob) => { + const file = new File( + [blob], + `${MainStore.printDesignName}_${MainStore.currentDoc}.jpg`, + { type: "image/jpeg" } + ); + const file_data = { + file_obj: file, + optimize: 1, + name: file.name, + private: true, + }; + this.upload_file(file_data); + }); + }, async saveElements() { const MainStore = useMainStore(); if (this.checkIfAnyTableIsEmpty()) return; @@ -133,6 +213,7 @@ export const useElementStore = defineStore("ElementStore", { 5 ); } + this.generatePreview(); }, checkIfAnyTableIsEmpty() { const emptyTable = this.Elements.find((el) => el.type == "table" && el.table == null); diff --git a/yarn.lock b/yarn.lock index 8f1cafd..8893f97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,6 +106,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -143,6 +148,13 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + debug@4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -216,6 +228,14 @@ has-unicode@^2.0.1: resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== +html2canvas@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -451,6 +471,13 @@ tar@^6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -461,6 +488,13 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + web-streams-polyfill@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" From 5da772833efe11aa65b72b975c63c178a6cc0292 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Sun, 17 Mar 2024 23:11:49 +0530 Subject: [PATCH 23/40] fix: don't save value of dynamic text. dynamic text value is fetched from db so there is no need to save it as part of format. --- .../js/print_designer/store/ElementStore.js | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index b358fc7..b748e76 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -117,6 +117,7 @@ export const useElementStore = defineStore("ElementStore", { async saveElements() { const MainStore = useMainStore(); if (this.checkIfAnyTableIsEmpty()) return; + if (MainStore.mode == "preview") return; // Update the header and footer height with margin MainStore.page.headerHeightWithMargin = @@ -458,6 +459,7 @@ export const useElementStore = defineStore("ElementStore", { delete saveEl.snapPoints; delete saveEl.snapEdges; delete saveEl.parent; + this.cleanUpDynamicContent(saveEl); if (printFonts && ["text", "table"].indexOf(saveEl.type) != -1) { handlePrintFonts(saveEl, printFonts); } @@ -472,6 +474,48 @@ export const useElementStore = defineStore("ElementStore", { return saveEl; }, + cleanUpDynamicContent(element) { + if ( + element.type == "table" || + (["text", "image", "barcode"].indexOf(element.type) != -1 && element.isDynamic) + ) { + if (["text", "barcode"].indexOf(element.type) != -1) { + element.dynamicContent = [ + ...element.dynamicContent.map((el) => { + const newEl = { ...el }; + if (!el.is_static) { + newEl.value = ""; + } + return newEl; + }), + ]; + element.selectedDynamicText = null; + } else if (element.type === "table") { + element.columns = [ + ...element.columns.map((el) => { + const newEl = { ...el }; + delete newEl.DOMRef; + return newEl; + }), + ]; + element.columns.forEach((col) => { + if (!col.dynamicContent) return; + col.dynamicContent = [ + ...col.dynamicContent.map((el) => { + const newEl = { ...el }; + if (!el.is_static) { + newEl.value = ""; + } + return newEl; + }), + ]; + col.selectedDynamicText = null; + }); + } else { + element.image = { ...element.image }; + } + } + }, getPrintFormatData({ header, body, footer }) { const headerElements = this.createWrapperElement( header.elements, @@ -583,7 +627,11 @@ export const useElementStore = defineStore("ElementStore", { if (["text", "barcode"].indexOf(element.type) != -1) { element.dynamicContent = [ ...element.dynamicContent.map((el) => { - return { ...el }; + const newEl = { ...el }; + if (!el.is_static) { + newEl.value = ""; + } + return newEl; }), ]; element.selectedDynamicText = null; @@ -598,7 +646,11 @@ export const useElementStore = defineStore("ElementStore", { if (!col.dynamicContent) return; col.dynamicContent = [ ...col.dynamicContent.map((el) => { - return { ...el }; + const newEl = { ...el }; + if (!el.is_static) { + newEl.value = ""; + } + return newEl; }), ]; col.selectedDynamicText = null; From e98ea4ddf5fc9cb3656430284a8b0c601c6ec200 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Mon, 18 Mar 2024 02:53:54 +0530 Subject: [PATCH 24/40] fix: don't save child table's metaFields. we have to consider and load metaFields from db so remove it from Print Format JSON. --- .../public/js/print_designer/store/ElementStore.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index b748e76..0091490 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -460,6 +460,10 @@ export const useElementStore = defineStore("ElementStore", { delete saveEl.snapEdges; delete saveEl.parent; this.cleanUpDynamicContent(saveEl); + if (saveEl.type == "table") { + delete saveEl.table.childfields; + delete saveEl.table.default_layout; + } if (printFonts && ["text", "table"].indexOf(saveEl.type) != -1) { handlePrintFonts(saveEl, printFonts); } @@ -637,6 +641,13 @@ export const useElementStore = defineStore("ElementStore", { element.selectedDynamicText = null; MainStore.dynamicData.push(...element.dynamicContent); } else if (element.type === "table") { + const mf = MainStore.metaFields.find( + (field) => field.fieldname == element.table.fieldname + ); + if (mf) { + element.table = mf; + } + element.columns = [ ...element.columns.map((el) => { return { ...el }; From 07ad4baa4898094c95a95c8dc94d2e90f6787cec Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Mon, 18 Mar 2024 04:18:04 +0530 Subject: [PATCH 25/40] fix: don't save file_values in format - if standard is set to true, remove static file urls as it can't be used across site. - only try to load file if url value exists for the file. - This doesn't check if image file exists or not. --- .../print_designer/jinja/macros/image.html | 18 +++++++++++++++--- .../print_designer/jinja/old_print_format.html | 17 ++++++++++++++++- .../js/print_designer/store/ElementStore.js | 17 ++++++++++++++--- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/image.html b/print_designer/print_designer/page/print_designer/jinja/macros/image.html index 7662b9f..0515361 100644 --- a/print_designer/print_designer/page/print_designer/jinja/macros/image.html +++ b/print_designer/print_designer/page/print_designer/jinja/macros/image.html @@ -1,14 +1,26 @@ {% macro image(element) -%} +{%- if element.image.file_url -%} + {%- set value = element.image.file_url -%} +{%- elif element.image.fieldname -%} + {%- if element.image.parent == doc.doctype -%} + {%- set value = doc.get(element.image.fieldname) -%} + {%- else -%} + {%- set value = frappe.db.get_value(element.image.doctype, doc[element.image.parentField], element.image.fieldname) -%} + {%- endif -%} +{%- else -%} + {%- set value = "" -%} +{%- endif -%} + +{%- if value -%}
+{%- endif -%} {%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/old_print_format.html b/print_designer/print_designer/page/print_designer/jinja/old_print_format.html index c1fb537..901e99d 100644 --- a/print_designer/print_designer/page/print_designer/jinja/old_print_format.html +++ b/print_designer/print_designer/page/print_designer/jinja/old_print_format.html @@ -40,6 +40,20 @@ {%- endmacro %} {% macro render_image(element) -%} + +{%- if element.image.file_url -%} + {%- set value = element.image.file_url -%} +{%- elif element.image.fieldname -%} + {%- if element.image.parent == doc.doctype -%} + {%- set value = doc.get(element.image.fieldname) -%} + {%- else -%} + {%- set value = frappe.db.get_value(element.image.doctype, doc[element.image.parentField], element.image.fieldname) -%} + {%- endif -%} +{%- else -%} + {%- set value = "" -%} +{%- endif -%} + +{%- if value -%}
+{%- endif -%} {%- endmacro %} {% macro render_barcode(element, send_to_jinja) -%} {%- set field = element.dynamicContent[0] -%} diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 0091490..1a38edb 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -118,7 +118,12 @@ export const useElementStore = defineStore("ElementStore", { const MainStore = useMainStore(); if (this.checkIfAnyTableIsEmpty()) return; if (MainStore.mode == "preview") return; - + let is_standard = await frappe.db.get_value( + "Print Format", + MainStore.printDesignName, + "standard" + ); + is_standard = is_standard.message.standard; // Update the header and footer height with margin MainStore.page.headerHeightWithMargin = MainStore.page.headerHeight + MainStore.page.marginTop; @@ -480,8 +485,8 @@ export const useElementStore = defineStore("ElementStore", { }, cleanUpDynamicContent(element) { if ( - element.type == "table" || - (["text", "image", "barcode"].indexOf(element.type) != -1 && element.isDynamic) + ["table", "image"].includes(element.type) || + (["text", "barcode"].includes(element.type) && element.isDynamic) ) { if (["text", "barcode"].indexOf(element.type) != -1) { element.dynamicContent = [ @@ -517,6 +522,12 @@ export const useElementStore = defineStore("ElementStore", { }); } else { element.image = { ...element.image }; + if (is_standard) { + // remove file_url and file_name if format is standard + ["value", "name", "file_name", "file_url", "modified"].forEach((key) => { + element.image[key] = ""; + }); + } } } }, From dd4e48d3c10a6635f2b36a5fb32e369f6c31d2eb Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Mon, 18 Mar 2024 07:50:55 +0530 Subject: [PATCH 26/40] fix: typo and set file url in print format As standard field returns Yes or No, update the logic to compare string instead of boolean. update preview image url in print_designer_preview_img field --- .../js/print_designer/store/ElementStore.js | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 1a38edb..7ede3e2 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -56,6 +56,34 @@ export const useElementStore = defineStore("ElementStore", { if (xhr.status === 200) { // delete old preview image when new image is successfully uploaded old_filename && frappe.db.delete_doc("File", old_filename); + try { + r = JSON.parse(xhr.responseText); + if (r.message.doctype === "File") { + file_doc = r.message; + frappe.db.set_value( + "Print Format", + MainStore.printDesignName, + "print_designer_preview_img", + file_doc.file_url + ); + } + } catch (e) { + r = xhr.responseText; + } + try { + r = JSON.parse(xhr.responseText); + if (r.message.doctype === "File") { + file_doc = r.message; + frappe.db.set_value( + "Print Format", + MainStore.printDesignName, + "print_designer_preview_img", + file_doc.file_url + ); + } + } catch (e) { + r = xhr.responseText; + } } } }; @@ -123,7 +151,7 @@ export const useElementStore = defineStore("ElementStore", { MainStore.printDesignName, "standard" ); - is_standard = is_standard.message.standard; + is_standard = is_standard.message.standard == "Yes"; // Update the header and footer height with margin MainStore.page.headerHeightWithMargin = MainStore.page.headerHeight + MainStore.page.marginTop; From 14746f49f3f5ba480668fb129d30ad80178ce376 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Mon, 18 Mar 2024 07:55:00 +0530 Subject: [PATCH 27/40] chore: load childfield meta when table is present First check if table exists before trying to load childfield meta --- .../public/js/print_designer/store/ElementStore.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 7ede3e2..802c281 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -680,11 +680,13 @@ export const useElementStore = defineStore("ElementStore", { element.selectedDynamicText = null; MainStore.dynamicData.push(...element.dynamicContent); } else if (element.type === "table") { - const mf = MainStore.metaFields.find( - (field) => field.fieldname == element.table.fieldname - ); - if (mf) { - element.table = mf; + if (element.table) { + const mf = MainStore.metaFields.find( + (field) => field.fieldname == element.table.fieldname + ); + if (mf) { + element.table = mf; + } } element.columns = [ From d8ecc52df31aa6a650f52e321e3c4ce150a3770d Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Mon, 18 Mar 2024 10:23:32 +0530 Subject: [PATCH 28/40] chore: copy image to export directory Copy the image to the export directory so that it can be used for preview while installing the format. --- .../print_designer/overrides/print_format.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/print_designer/print_designer/overrides/print_format.py b/print_designer/print_designer/overrides/print_format.py index c9fa03f..16b3948 100644 --- a/print_designer/print_designer/overrides/print_format.py +++ b/print_designer/print_designer/overrides/print_format.py @@ -1,4 +1,5 @@ import os +import shutil import frappe from frappe.modules.utils import scrub @@ -38,6 +39,7 @@ def write_document_file(self): with open(path, "w+") as jsonfile: jsonfile.write(frappe.as_json(doc_export)) print(f"Wrote document file for {doc.doctype} {doc.name} at {path}") + self.export_preview(folder=folder, fname=fname) def create_folder(self, dt, dn): app = scrub(frappe.get_doctype_app(dt)) @@ -52,3 +54,11 @@ def create_folder(self, dt, dn): ) frappe.create_folder(folder) return folder + + def export_preview(self, folder, fname): + if self.print_designer_preview_img: + file = frappe.get_doc("File", {"file_url": self.print_designer_preview_img}) + org_path = file.get_full_path() + target_path = os.path.join(folder, f"{fname}-preview.jpg") + shutil.copy2(org_path, target_path) + print(f"Wrote preview file for {self.doctype} {self.name} at {target_path}") From 9883c6d180e8c96e65650fc99b7041c9848817bc Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Mon, 1 Apr 2024 13:33:10 +0530 Subject: [PATCH 29/40] fix: export import preview images when print_designer format is saved in developer mode and format is standard. preview images and " File " document is exported as per the location defined in the selected app's hook. after install once the formats are imported, copy the file to private/files folder and update the file path in the format. --- print_designer/default_formats.py | 45 ++++++++++++++++++- .../print_designer/overrides/print_format.py | 30 ++++++++++--- .../js/print_designer/store/ElementStore.js | 13 ++++-- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/print_designer/default_formats.py b/print_designer/default_formats.py index 6393a04..f6ed704 100644 --- a/print_designer/default_formats.py +++ b/print_designer/default_formats.py @@ -1,8 +1,10 @@ import os +import shutil from pathlib import Path import frappe from frappe.modules.import_file import import_file_by_path +from frappe.utils import get_files_path """ features: @@ -27,6 +29,27 @@ def on_print_designer_install(): install_default_formats(app=app, load_pd_formats=False) +def get_preview_image_folder_path(print_format): + app = frappe.scrub(frappe.get_doctype_app(print_format.doc_type)) + pd_folder = frappe.get_hooks( + "pd_standard_format_folder", app_name=print_format.print_designer_template_app + ) + if len(pd_folder) == 0: + pd_folder = ["default_templates"] + return os.path.join( + frappe.get_app_path(print_format.print_designer_template_app), os.path.join(pd_folder[0], app) + ) + + +def update_preview_img(file): + print_format = frappe.get_doc(file.attached_to_doctype, file.attached_to_name) + folder = get_preview_image_folder_path(print_format) + file_name = print_format.print_designer_preview_img.split("/")[-1] + org_path = os.path.join(folder, file_name) + target_path = get_files_path(file_name, is_private=1) + shutil.copy2(org_path, target_path) + + # called after install of any new app. def install_default_formats(app, filter_by="", load_pd_formats=True): if load_pd_formats: @@ -42,10 +65,30 @@ def install_default_formats(app, filter_by="", load_pd_formats=True): app=app, templates_folder=pd_folder[0], filter_by=filter_by ) + preview_files = [f for f in print_formats if f.name.endswith("-preview.json")] + print_formats = [f for f in print_formats if not f.name.endswith("-preview.json")] + for json_file_path in print_formats: - import_file_by_path(json_file_path) + import_file_by_path(path=json_file_path) frappe.db.commit() + for json_file_path in preview_files: + import_file_by_path(path=json_file_path, pre_process=update_preview_img) + frappe.db.commit() + + for pf in frappe.db.get_all("Print Format", filters={"standard": "Yes", "print_designer": 1}): + updated_url = frappe.db.get_value( + "File", + { + "attached_to_doctype": "Print Format", + "attached_to_name": pf.name, + "attached_to_field": "print_designer_preview_img", + }, + "file_url", + ) + if updated_url: + frappe.set_value("Print Format", pf.name, "print_designer_preview_img", updated_url) + def get_filtered_formats_by_app(app, templates_folder, filter_by=""): app_path = frappe.get_app_path(app) diff --git a/print_designer/print_designer/overrides/print_format.py b/print_designer/print_designer/overrides/print_format.py index 16b3948..6b3ca46 100644 --- a/print_designer/print_designer/overrides/print_format.py +++ b/print_designer/print_designer/overrides/print_format.py @@ -9,7 +9,7 @@ class PDPrintFormat(PrintFormat): def export_doc(self): if ( - not self.standard + not self.standard == "Yes" or not frappe.conf.developer_mode or frappe.flags.in_patch or frappe.flags.in_install @@ -36,8 +36,8 @@ def write_document_file(self): # write the data file path = os.path.join(folder, f"{fname}.json") - with open(path, "w+") as jsonfile: - jsonfile.write(frappe.as_json(doc_export)) + with open(path, "w+") as json_file: + json_file.write(frappe.as_json(doc_export)) print(f"Wrote document file for {doc.doctype} {doc.name} at {path}") self.export_preview(folder=folder, fname=fname) @@ -57,8 +57,28 @@ def create_folder(self, dt, dn): def export_preview(self, folder, fname): if self.print_designer_preview_img: - file = frappe.get_doc("File", {"file_url": self.print_designer_preview_img}) + try: + file = frappe.get_doc( + "File", + { + "file_url": self.print_designer_preview_img, + "attached_to_doctype": self.doctype, + "attached_to_name": self.name, + "attached_to_field": "print_designer_preview_img", + }, + ) + except frappe.DoesNotExistError: + file = None + if not file: + return + file_export = file.as_dict(no_nulls=True) + file.run_method("before_export", file_export) org_path = file.get_full_path() - target_path = os.path.join(folder, f"{fname}-preview.jpg") + target_path = os.path.join(folder, org_path.split("/")[-1]) shutil.copy2(org_path, target_path) print(f"Wrote preview file for {self.doctype} {self.name} at {target_path}") + # write the data file + path = os.path.join(folder, f"print_designer-{fname}-preview.json") + with open(path, "w+") as json_file: + json_file.write(frappe.as_json(file_export)) + print(f"Wrote document file for {file.doctype} {file.name} at {path}") diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 802c281..f11845d 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -48,14 +48,21 @@ export const useElementStore = defineStore("ElementStore", { // get filename before uploading new file let old_filename = await frappe.db.get_value("File", filter, "name"); old_filename = old_filename.message.name; + if (old_filename) { + frappe.db.delete_doc("File", old_filename); + frappe.db.set_value( + "Print Format", + MainStore.printDesignName, + "print_designer_preview_img", + null + ); + } return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if (xhr.readyState == XMLHttpRequest.DONE) { if (xhr.status === 200) { - // delete old preview image when new image is successfully uploaded - old_filename && frappe.db.delete_doc("File", old_filename); try { r = JSON.parse(xhr.responseText); if (r.message.doctype === "File") { @@ -130,7 +137,7 @@ export const useElementStore = defineStore("ElementStore", { preview_canvas.toBlob((blob) => { const file = new File( [blob], - `${MainStore.printDesignName}_${MainStore.currentDoc}.jpg`, + `print_designer-${frappe.scrub(MainStore.printDesignName)}-preview.jpg`, { type: "image/jpeg" } ); const file_data = { From bbe07f9d8856d914fead33f6469ca16c3c301017 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 2 Apr 2024 01:27:45 +0530 Subject: [PATCH 30/40] fix: merge conflict and file_url After merging relative containers when this in this PR conflicts were manually resolved. There were some incorrect conflict resolutions, which are fixed in this commit. Also, removed multiple updates for print format document caused by updating file_url. instead get latest file_url from the file document else set to null. --- .../js/print_designer/store/ElementStore.js | 80 ++++++++----------- 1 file changed, 34 insertions(+), 46 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index f11845d..298a926 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -37,26 +37,9 @@ export const useElementStore = defineStore("ElementStore", { } return newElement; }, - // This is mofiied version of upload function used in frappe/FileUploader.vue + // This is modified version of upload function used in frappe/FileUploader.vue async upload_file(file) { const MainStore = useMainStore(); - const filter = { - attached_to_doctype: "Print Format", - attached_to_name: MainStore.printDesignName, - attached_to_field: "print_designer_preview_img", - }; - // get filename before uploading new file - let old_filename = await frappe.db.get_value("File", filter, "name"); - old_filename = old_filename.message.name; - if (old_filename) { - frappe.db.delete_doc("File", old_filename); - frappe.db.set_value( - "Print Format", - MainStore.printDesignName, - "print_designer_preview_img", - null - ); - } return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); @@ -65,29 +48,6 @@ export const useElementStore = defineStore("ElementStore", { if (xhr.status === 200) { try { r = JSON.parse(xhr.responseText); - if (r.message.doctype === "File") { - file_doc = r.message; - frappe.db.set_value( - "Print Format", - MainStore.printDesignName, - "print_designer_preview_img", - file_doc.file_url - ); - } - } catch (e) { - r = xhr.responseText; - } - try { - r = JSON.parse(xhr.responseText); - if (r.message.doctype === "File") { - file_doc = r.message; - frappe.db.set_value( - "Print Format", - MainStore.printDesignName, - "print_designer_preview_img", - file_doc.file_url - ); - } } catch (e) { r = xhr.responseText; } @@ -117,6 +77,19 @@ export const useElementStore = defineStore("ElementStore", { }, async generatePreview() { const MainStore = useMainStore(); + // first delete old preview image + const filter = { + attached_to_doctype: "Print Format", + attached_to_name: MainStore.printDesignName, + attached_to_field: "print_designer_preview_img", + }; + // get filename before uploading new file + let old_filename = await frappe.db.get_value("File", filter, "name"); + old_filename = old_filename.message.name; + if (old_filename) { + frappe.db.delete_doc("File", old_filename); + } + const options = { backgroundColor: "#ffffff", height: MainStore.page.height / 2, @@ -134,7 +107,7 @@ export const useElementStore = defineStore("ElementStore", { options ); document.getElementsByClassName("main-container")[0].removeChild(print_stylesheet); - preview_canvas.toBlob((blob) => { + preview_canvas.toBlob(async (blob) => { const file = new File( [blob], `print_designer-${frappe.scrub(MainStore.printDesignName)}-preview.jpg`, @@ -146,7 +119,7 @@ export const useElementStore = defineStore("ElementStore", { name: file.name, private: true, }; - this.upload_file(file_data); + await this.upload_file(file_data); }); }, async saveElements() { @@ -158,7 +131,7 @@ export const useElementStore = defineStore("ElementStore", { MainStore.printDesignName, "standard" ); - is_standard = is_standard.message.standard == "Yes"; + MainStore.is_standard = is_standard.message.standard == "Yes"; // Update the header and footer height with margin MainStore.page.headerHeightWithMargin = MainStore.page.headerHeight + MainStore.page.marginTop; @@ -241,7 +214,22 @@ export const useElementStore = defineStore("ElementStore", { }, }); + await this.generatePreview(); + objectToSave.print_designer_print_format = PrintFormatData; + const filter = { + attached_to_doctype: "Print Format", + attached_to_name: MainStore.printDesignName, + attached_to_field: "print_designer_preview_img", + }; + // update filename from file + let print_designer_preview_img = await frappe.db.get_value("File", filter, "file_url"); + if (print_designer_preview_img.message.file_url) { + objectToSave.print_designer_preview_img = + print_designer_preview_img.message.file_url; + } else { + objectToSave.print_designer_preview_img = null; + } if (MainStore.isOlderSchema("1.1.0")) { await this.printFormatCopyOnOlderSchema(objectToSave); } else { @@ -254,7 +242,6 @@ export const useElementStore = defineStore("ElementStore", { 5 ); } - this.generatePreview(); }, checkIfAnyTableIsEmpty() { const emptyTable = this.Elements.find((el) => el.type == "table" && el.table == null); @@ -519,6 +506,7 @@ export const useElementStore = defineStore("ElementStore", { return saveEl; }, cleanUpDynamicContent(element) { + const MainStore = useMainStore(); if ( ["table", "image"].includes(element.type) || (["text", "barcode"].includes(element.type) && element.isDynamic) @@ -557,7 +545,7 @@ export const useElementStore = defineStore("ElementStore", { }); } else { element.image = { ...element.image }; - if (is_standard) { + if (MainStore.is_standard) { // remove file_url and file_name if format is standard ["value", "name", "file_name", "file_url", "modified"].forEach((key) => { element.image[key] = ""; From 4d0f3084563a4d3ae8bbb38f7950f20c1824f332 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 2 Apr 2024 02:27:04 +0530 Subject: [PATCH 31/40] fix: handle folder not found error in import --- print_designer/default_formats.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/print_designer/default_formats.py b/print_designer/default_formats.py index f6ed704..f0fbc6a 100644 --- a/print_designer/default_formats.py +++ b/print_designer/default_formats.py @@ -102,6 +102,8 @@ def get_filtered_formats_by_app(app, templates_folder, filter_by=""): def get_formats_from_folders(folders): formats = set() + if not folders.exists(): + return formats for folder in folders.iterdir(): if folder.is_dir() and folder.name in frappe.get_installed_apps(): formats.update(get_json_files(folder)) From fbd488ea9adb139205bd83946495bd7b997d3d9e Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 2 Apr 2024 05:23:31 +0530 Subject: [PATCH 32/40] fix: typo printFonts is object while refactoring the code, by mistake it was changed to an array. This commit fixes it. --- .../public/js/print_designer/store/ElementStore.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 298a926..3fc020f 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -156,6 +156,9 @@ export const useElementStore = defineStore("ElementStore", { const [cleanedBodyElements, bodyFonts] = body; const [cleanedHeaderElements, headerFonts] = header || [[], null]; const [cleanedFooterElements, footerFonts] = footer || [[], null]; + MainStore.printHeaderFonts = headerFonts; + MainStore.printBodyFonts = bodyFonts; + MainStore.printFooterFonts = footerFonts; MainStore.currentFonts.length = 0; MainStore.currentFonts.push( @@ -165,6 +168,7 @@ export const useElementStore = defineStore("ElementStore", { ...(footerFonts || {}), }) ); + debugger; const updatedPage = { ...MainStore.page }; const settingsForSave = { page: updatedPage, @@ -431,12 +435,12 @@ export const useElementStore = defineStore("ElementStore", { }, cleanUpElementsForSave(elements, type) { if (this.checkIfPrintFormatIsEmpty(elements, type)) return; - const fontsArray = []; + const fontsObject = {}; const cleanedElements = []; elements.forEach((container) => { const cleanedContainer = []; container.forEach((element) => { - let newElement = this.childrensSave(element.element, fontsArray); + let newElement = this.childrensSave(element.element, fontsObject); newElement.classes = newElement.classes.filter( (name) => ["inHeaderFooter", "overlappingHeaderFooter"].indexOf(name) == -1 ); @@ -451,7 +455,7 @@ export const useElementStore = defineStore("ElementStore", { }); cleanedElements.push(cleanedContainer); }); - return [cleanedElements, fontsArray]; + return [cleanedElements, fontsObject]; }, checkIfPrintFormatIsEmpty(elements, type) { const MainStore = useMainStore(); From 6e8469ac06cd61e1ab724830125550c676f99157 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 2 Apr 2024 05:24:37 +0530 Subject: [PATCH 33/40] fix(minor): don't remove cssRules unintended cssRules were removed by mistake so until that can be fixed removing deleteRule code. --- .../components/layout/AppCanvas.vue | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue index 4828bc3..c5065ce 100644 --- a/print_designer/public/js/print_designer/components/layout/AppCanvas.vue +++ b/print_designer/public/js/print_designer/components/layout/AppCanvas.vue @@ -465,13 +465,6 @@ onMounted(() => { watchEffect(() => { if (MainStore.screenStyleSheet) { - if (MainStore.screenStyleSheet.CssRuleIndex != null) { - try { - MainStore.screenStyleSheet.deleteRule(MainStore.screenStyleSheet.CssRuleIndex); - } catch (error) { - console.warn("Error Deleting Rule", error); - } - } MainStore.screenStyleSheet.CssRuleIndex = MainStore.addStylesheetRules([ [ ":root, ::after, ::before", @@ -530,13 +523,6 @@ onMounted(() => { }); watchEffect(() => { if (MainStore.printStyleSheet && MainStore.page) { - for (let index = 0; index < MainStore.printStyleSheet.cssRules.length; index++) { - try { - MainStore.printStyleSheet.deleteRule(index); - } catch (error) { - console.warn("Error Deleting Rule", error); - } - } const convertToMM = (input) => { let convertedUnit = useChangeValueUnit({ inputString: input, @@ -568,13 +554,6 @@ watchEffect(() => { }); watchEffect(() => { if (MainStore.screenStyleSheet && MainStore.modalLocation) { - for (let index = 0; index < MainStore.screenStyleSheet.cssRules.length; index++) { - try { - MainStore.screenStyleSheet.deleteRule(index); - } catch (error) { - console.warn("Error Deleting Rule", error); - } - } MainStore.addStylesheetRules([ [ ":root", From 7c31f7da57acf57aca452f694bd06909e3aff8a1 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 2 Apr 2024 10:37:01 +0530 Subject: [PATCH 34/40] fix: handle race condition in preview image image upload sometime happen before the format is saved and sometimes after. handled both cases based on isFormatSaving flag. --- .../js/print_designer/store/ElementStore.js | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 3fc020f..1ca42f2 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -40,7 +40,7 @@ export const useElementStore = defineStore("ElementStore", { // This is modified version of upload function used in frappe/FileUploader.vue async upload_file(file) { const MainStore = useMainStore(); - + MainStore.print_designer_preview_img = null; return new Promise((resolve, reject) => { let xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { @@ -48,6 +48,21 @@ export const useElementStore = defineStore("ElementStore", { if (xhr.status === 200) { try { r = JSON.parse(xhr.responseText); + if (r.message.doctype === "File") { + file_url = r.message.file_url; + // if format is not saved then update the preview image value with it + if (MainStore.isFormatSaving) { + MainStore.print_designer_preview_img = file_url; + } else { + // if format is already saved then update the print_designer_preview_img field + frappe.db.set_value( + "Print Format", + MainStore.printDesignName, + "print_designer_preview_img", + file_url + ); + } + } } catch (e) { r = xhr.responseText; } @@ -92,7 +107,7 @@ export const useElementStore = defineStore("ElementStore", { const options = { backgroundColor: "#ffffff", - height: MainStore.page.height / 2, + height: MainStore.page.height, width: MainStore.page.width, }; const print_stylesheet = document.createElement("style"); @@ -168,7 +183,6 @@ export const useElementStore = defineStore("ElementStore", { ...(footerFonts || {}), }) ); - debugger; const updatedPage = { ...MainStore.page }; const settingsForSave = { page: updatedPage, @@ -217,27 +231,17 @@ export const useElementStore = defineStore("ElementStore", { dimensions: footerDimensions, }, }); + MainStore.isFormatSaving = true; await this.generatePreview(); objectToSave.print_designer_print_format = PrintFormatData; - const filter = { - attached_to_doctype: "Print Format", - attached_to_name: MainStore.printDesignName, - attached_to_field: "print_designer_preview_img", - }; - // update filename from file - let print_designer_preview_img = await frappe.db.get_value("File", filter, "file_url"); - if (print_designer_preview_img.message.file_url) { - objectToSave.print_designer_preview_img = - print_designer_preview_img.message.file_url; - } else { - objectToSave.print_designer_preview_img = null; - } + objectToSave.print_designer_preview_img = MainStore.print_designer_preview_img; if (MainStore.isOlderSchema("1.1.0")) { await this.printFormatCopyOnOlderSchema(objectToSave); } else { await frappe.db.set_value("Print Format", MainStore.printDesignName, objectToSave); + MainStore.isFormatSaving = false; frappe.show_alert( { message: `Print Format Saved Successfully`, From a1e112a6da4ef27f4bc0056f79ac83382bca2285 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 2 Apr 2024 23:52:28 +0530 Subject: [PATCH 35/40] chore: defer image import until fw pr is merged revert this after https://github.com/frappe/frappe/pull/25779 is released in v15 --- print_designer/default_formats.py | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/print_designer/default_formats.py b/print_designer/default_formats.py index f0fbc6a..2e0e9f9 100644 --- a/print_designer/default_formats.py +++ b/print_designer/default_formats.py @@ -65,29 +65,29 @@ def install_default_formats(app, filter_by="", load_pd_formats=True): app=app, templates_folder=pd_folder[0], filter_by=filter_by ) - preview_files = [f for f in print_formats if f.name.endswith("-preview.json")] + # preview_files = [f for f in print_formats if f.name.endswith("-preview.json")] print_formats = [f for f in print_formats if not f.name.endswith("-preview.json")] for json_file_path in print_formats: import_file_by_path(path=json_file_path) frappe.db.commit() - - for json_file_path in preview_files: - import_file_by_path(path=json_file_path, pre_process=update_preview_img) - frappe.db.commit() - - for pf in frappe.db.get_all("Print Format", filters={"standard": "Yes", "print_designer": 1}): - updated_url = frappe.db.get_value( - "File", - { - "attached_to_doctype": "Print Format", - "attached_to_name": pf.name, - "attached_to_field": "print_designer_preview_img", - }, - "file_url", - ) - if updated_url: - frappe.set_value("Print Format", pf.name, "print_designer_preview_img", updated_url) + # TODO: enable this after this is released in v15 https://github.com/frappe/frappe/pull/25779 + # for json_file_path in preview_files: + # import_file_by_path(path=json_file_path, pre_process=update_preview_img) + # frappe.db.commit() + + # for pf in frappe.db.get_all("Print Format", filters={"standard": "Yes", "print_designer": 1}): + # updated_url = frappe.db.get_value( + # "File", + # { + # "attached_to_doctype": "Print Format", + # "attached_to_name": pf.name, + # "attached_to_field": "print_designer_preview_img", + # }, + # "file_url", + # ) + # if updated_url: + # frappe.set_value("Print Format", pf.name, "print_designer_preview_img", updated_url) def get_filtered_formats_by_app(app, templates_folder, filter_by=""): From 6afc817441d5f2258833b908458e7dc6e6fd1405 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 3 Apr 2024 11:00:13 +0530 Subject: [PATCH 36/40] fix: allow user to choose the name on copy when we create copy when user updates the formats with old schema. it was implicitly setting the name with copy + number suffix. now we set that name as default and prompt user if they want to change the name. --- .../js/print_designer/store/ElementStore.js | 106 +++++++++++++----- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index 1ca42f2..eede374 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -614,56 +614,100 @@ export const useElementStore = defineStore("ElementStore", { return wrapperContainers.childrens.map((el) => this.childrensSave(el)); }, async printFormatCopyOnOlderSchema(objectToSave) { - const MainStore = useMainStore(); - let nextFormatCopyNumber = 0; - for (let i = 0; i < 100; i++) { - const pf_exists = await frappe.db.exists( - "Print Format", - MainStore.printDesignName + " ( Copy " + (i ? i : "") + " )" - ); - if (pf_exists) continue; - nextFormatCopyNumber = i; - break; - } - const newName = - MainStore.printDesignName + - " ( Copy " + - (nextFormatCopyNumber ? nextFormatCopyNumber : "") + - " )"; // TODO: have better message. let message = __( "This Print Format was created from older version of Print Designer." ); message += "
"; message += __( - "It is not compatible with current version so instead we will make copy of this format for you using new version" + "It is not compatible with current version so instead make copy of this format using new version" ); message += "
"; - message += __(`Do you want to save it as ${newName} ?`); + message += __(`Do you want to save copy of it ?`); frappe.confirm( message, async () => { - await frappe.db.insert({ - doctype: "Print Format", - name: newName, - doc_type: MainStore.doctype, - print_designer: 1, - print_designer_header: objectToSave.print_designer_header, - print_designer_body: objectToSave.print_designer_body, - print_designer_after_table: null, - print_designer_footer: objectToSave.print_designer_footer, - print_designer_print_format: objectToSave.print_designer_print_format, - print_designer_settings: objectToSave.print_designer_settings, - }); - frappe.set_route("print-designer", newName); + this.promptUserForNewFormatName(objectToSave); }, async () => { + frappe.show_alert( + { + message: `Print Format not saved`, + indicator: "red", + }, + 5 + ); + // intentionally throwing error to stop the saving the format throw new Error(__("Print Format not saved")); } ); }, + async promptUserForNewFormatName(objectToSave) { + const MainStore = useMainStore(); + let nextFormatCopyNumber = 0; + for (let i = 0; i < 100; i++) { + const pf_exists = await frappe.db.exists( + "Print Format", + MainStore.printDesignName + " ( Copy " + (i ? i : "") + " )" + ); + if (pf_exists) continue; + nextFormatCopyNumber = i; + break; + } + // This is just default value for the new print format name + const print_format_name = + MainStore.printDesignName + + " ( Copy " + + (nextFormatCopyNumber ? nextFormatCopyNumber : "") + + " )"; + let d = new frappe.ui.Dialog({ + title: "New Print Format", + fields: [ + { + label: "Name", + fieldname: "print_format_name", + fieldtype: "Data", + reqd: 1, + default: print_format_name, + }, + ], + size: "small", + primary_action_label: "Save", + static: true, + async primary_action(values) { + try { + await frappe.db.insert({ + doctype: "Print Format", + name: values.print_format_name, + doc_type: MainStore.doctype, + print_designer: 1, + print_designer_header: objectToSave.print_designer_header, + print_designer_body: objectToSave.print_designer_body, + print_designer_after_table: null, + print_designer_footer: objectToSave.print_designer_footer, + print_designer_print_format: objectToSave.print_designer_print_format, + print_designer_settings: objectToSave.print_designer_settings, + }); + d.hide(); + frappe.set_route("print-designer", values.print_format_name); + } catch (error) { + console.error(error); + } + }, + }); + d.get_close_btn().on("click", () => { + frappe.show_alert( + { + message: `Print Format not saved`, + indicator: "red", + }, + 5 + ); + }); + d.show(); + }, handleDynamicContent(element) { const MainStore = useMainStore(); if ( From 7d023283266e62ebe1e891f8c36b2e36d13fa5dd Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 3 Apr 2024 11:34:38 +0530 Subject: [PATCH 37/40] fix: only render new line if value is present In dynamic content new line / `
` was rendering even if value was empty. which leaves empty line in the content. this commit fixes that for old_print_format jinja template. also, added missing jinja code for suffix in new template. --- .../print_designer/jinja/macros/spantag.html | 28 ++++++++----- .../jinja/old_print_format.html | 40 ++++++++++--------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/spantag.html b/print_designer/print_designer/page/print_designer/jinja/macros/spantag.html index 04297f5..68c91ff 100644 --- a/print_designer/print_designer/page/print_designer/jinja/macros/spantag.html +++ b/print_designer/print_designer/page/print_designer/jinja/macros/spantag.html @@ -24,18 +24,26 @@ {% macro span_tag(field, element, row = {}, send_to_jinja = {}) -%} {% set span_value = spanvalue(field, element, row, send_to_jinja) %} - - {% if not field.is_static and field.is_labelled and span_value %} - - {{ _(field.label) }} - - {% endif %} + {%- if span_value -%} + + {% if not field.is_static and field.is_labelled%} + + {{ _(field.label) }} + + {% endif %} {{ span_value }} - {% if field.nextLine %} -
- {% endif %} -
+ {% if field.suffix %} + + {{ _(field.suffix) }} + + {% endif %} + {% if field.nextLine %} +
+ {% endif %} +
+ {% endif %} {%- endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/old_print_format.html b/print_designer/print_designer/page/print_designer/jinja/old_print_format.html index c1fb537..0a54666 100644 --- a/print_designer/print_designer/page/print_designer/jinja/old_print_format.html +++ b/print_designer/print_designer/page/print_designer/jinja/old_print_format.html @@ -140,26 +140,28 @@ {% macro render_spantag(field, element, row = {}, send_to_jinja = {}) -%} {% set span_value = render_spanvalue(field, element, row, send_to_jinja) %} - - {% if not field.is_static and field.is_labelled and span_value %} - - {{ _(field.label) }} - - {% endif %} - - {{ span_value }} - - {% if field.suffix and span_value %} - - {{ _(field.suffix) }} +{%- if span_value -%} + + {% if not field.is_static and field.is_labelled %} + + {{ _(field.label) }} + + {% endif %} + + {{ span_value }} + + {% if field.suffix %} + + {{ _(field.suffix) }} + + {% endif %} + {% if field.nextLine %} +
+ {% endif %}
- {% endif %} - {% if field.nextLine %} -
- {% endif %} -
+{% endif %} {%- endmacro %} {% macro render_element(element, send_to_jinja) -%} {% if element is iterable and (element is not string and element is not mapping) %} From 84af5555c10b7b01a40ed47ea36272ed81406c1f Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 4 Apr 2024 21:50:42 +0530 Subject: [PATCH 38/40] chore: fix typo from merge conflict --- print_designer/public/js/print_designer/store/ElementStore.js | 1 + 1 file changed, 1 insertion(+) diff --git a/print_designer/public/js/print_designer/store/ElementStore.js b/print_designer/public/js/print_designer/store/ElementStore.js index fd344fb..1dc0e59 100644 --- a/print_designer/public/js/print_designer/store/ElementStore.js +++ b/print_designer/public/js/print_designer/store/ElementStore.js @@ -549,6 +549,7 @@ export const useElementStore = defineStore("ElementStore", { }, cleanUpElementsForSave(rows, type) { if (this.checkIfPrintFormatIsEmpty(rows, type)) return; + const MainStore = useMainStore(); const fontsObject = {}; switch (type) { case "header": From 0f1510788f32aead5fb5ca3d3250025dbd177212 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 5 Apr 2024 02:45:28 +0530 Subject: [PATCH 39/40] fix: printview and table header overlap issue wkhtmltopdf have issue with table header overlapping if it is inside a div with display: inline. wkhtmltopdf doesn't support flex but it has some old experimental support for https://developer.mozilla.org/en-US/docs/Web/CSS/box-align As it doesn't support proper implementation of flex it is not used anywhere in print designer. This PR uses display: -webkit-box; to fix the particular issue. hopefully can be used to add more features in the future. --- .../jinja/macros/relative_containers.html | 6 ++--- .../print_designer/jinja/macros/styles.html | 23 ++++++++++++++++++- .../print_designer/jinja/print_format.html | 4 +++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html b/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html index 87daf0e..3204af7 100644 --- a/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html +++ b/print_designer/print_designer/page/print_designer/jinja/macros/relative_containers.html @@ -1,7 +1,7 @@ {% from 'print_designer/page/print_designer/jinja/macros/render_element.html' import render_element with context %} {% macro relative_columns(element, send_to_jinja) -%} -
{% if element.childrens %} {% for object in element.childrens %} @@ -12,8 +12,8 @@ {%- endmacro %} {% macro relative_containers(element, send_to_jinja) -%} -
+
{% if element.childrens %} {% for object in element.childrens %} {{ relative_columns(object, send_to_jinja) }} diff --git a/print_designer/print_designer/page/print_designer/jinja/macros/styles.html b/print_designer/print_designer/page/print_designer/jinja/macros/styles.html index 0dcb4e8..7b479aa 100644 --- a/print_designer/print_designer/page/print_designer/jinja/macros/styles.html +++ b/print_designer/print_designer/page/print_designer/jinja/macros/styles.html @@ -24,6 +24,16 @@ margin: auto !important; } } + /* set margin to 0 for print (Ctrl + p) on client browsers + and remove margin container that was added for screen ( viewing ) */ + @media print { + .print-format { + margin: 0 !important; + } + .printview-header-margin { + display: none; + } + } .print-designer-container { position: absolute; } @@ -71,8 +81,19 @@ .flexDirectionColumn .baseSpanTag .valueSpanTag { display: block; } + /* https://github.com/wkhtmltopdf/wkhtmltopdf/issues/1522 */ + .relative-row { + display: -webkit-box; + display: -webkit-flex; + display: flex; + border-width: 0 !important; + } .relative-column { - margin-right: -4px; + border-width: 1px !important; + border-color: white !important; + } + * { + -webkit-box-sizing: border-box; } {% endmacro %} \ No newline at end of file diff --git a/print_designer/print_designer/page/print_designer/jinja/print_format.html b/print_designer/print_designer/page/print_designer/jinja/print_format.html index 485cb37..8624edb 100644 --- a/print_designer/print_designer/page/print_designer/jinja/print_format.html +++ b/print_designer/print_designer/page/print_designer/jinja/print_format.html @@ -11,12 +11,14 @@
+
+
{% if headerElement %}{{ render(pd_format.header, send_to_jinja) }}{%endif%}
{% if bodyElement %}{{ render(pd_format.body, send_to_jinja) }}{%endif%}