refactor: Print Format Builder Beta code to vue 3

This commit is contained in:
Shariq Ansari 2022-10-05 15:41:14 +05:30
parent bab42ffd80
commit 45e7e92ffa
12 changed files with 1027 additions and 1079 deletions

View file

@ -18,78 +18,74 @@
:group="df.fieldname"
handle=".icon-drag"
>
<div
class="mt-2 row align-center column-row"
v-for="column in df.table_columns"
>
<div class="col-8">
<div class="column-label d-flex align-center">
<div class="px-2 icon-drag ml-n2">
<svg class="icon icon-xs">
<use href="#icon-drag"></use>
</svg>
</div>
<div class="mt-1 ml-1">
<input
class="input-column-label"
:class="{ 'text-danger': column.invalid_width }"
type="text"
v-model="column.label"
/>
<template #item="{ element }">
<div
class="mt-2 row align-center column-row"
v-for="column in df.table_columns"
>
<div class="col-8">
<div class="column-label d-flex align-center">
<div class="px-2 icon-drag ml-n2">
<svg class="icon icon-xs">
<use href="#icon-drag"></use>
</svg>
</div>
<div class="mt-1 ml-1">
<input
class="input-column-label"
:class="{ 'text-danger': column.invalid_width }"
type="text"
v-model="column.label"
/>
</div>
</div>
</div>
<div class="col-4 d-flex align-items-center">
<input
type="number"
class="text-right form-control"
:class="{ 'text-danger is-invalid': column.invalid_width }"
v-model.number="column.width"
min="0"
max="100"
step="5"
/>
<button
class="ml-2 btn btn-xs btn-icon"
@click="remove_column(column)"
>
<svg class="icon icon-sm">
<use href="#icon-close"></use>
</svg>
</button>
</div>
</div>
<div class="col-4 d-flex align-items-center">
<input
type="number"
class="text-right form-control"
:class="{ 'text-danger is-invalid': column.invalid_width }"
v-model.number="column.width"
min="0"
max="100"
step="5"
/>
<button
class="ml-2 btn btn-xs btn-icon"
@click="remove_column(column)"
>
<svg class="icon icon-sm">
<use href="#icon-close"></use>
</svg>
</button>
</div>
</div>
</template>
</draggable>
</div>
</template>
<script>
<script setup>
import { computed } from "vue";
import draggable from "vuedraggable";
export default {
name: "ConfigureColumns",
props: ["df"],
components: {
draggable
},
methods: {
remove_column(column) {
this.$set(
this.df,
"table_columns",
this.df.table_columns.filter(_column => _column !== column)
);
}
},
computed: {
help_message() {
// prettier-ignore
return __("Drag columns to set order. Column width is set in percentage. The total width should not be more than 100. Columns marked in red will be removed.");
},
total_width() {
return this.df.table_columns.reduce((total, tf) => total + tf.width, 0);
}
}
};
// props
let props = defineProps(["df"]);
// methods
function remove_column(column) {
props.df["table_columns"] = props.df.table_columns.filter(_column => _column !== column)
}
// computed
let help_message = computed(() => {
// prettier-ignore
return __("Drag columns to set order. Column width is set in percentage. The total width should not be more than 100. Columns marked in red will be removed.");
});
let total_width = computed(() => {
return props.df.table_columns.reduce((total, tf) => total + tf.width, 0);
});
</script>
<style scoped>
.icon-drag {
cursor: grab;

View file

@ -1,5 +1,5 @@
<template>
<div class="field" :title="df.fieldname" @click="editing = true">
<div class="field" v-show="!df.remove" :title="df.fieldname" @click="editing = true">
<div class="field-controls">
<div>
<div
@ -15,7 +15,7 @@
</div>
<input
v-else-if="editing && df.fieldtype != 'HTML'"
ref="label-input"
ref="label_input"
class="label-input"
type="text"
:placeholder="__('Label')"
@ -47,7 +47,7 @@
</button>
<button
class="btn btn-xs btn-icon"
@click="$set(df, 'remove', true)"
@click="df['remove'] = true"
>
<svg class="icon icon-sm">
<use href="#icon-close"></use>
@ -73,170 +73,150 @@
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
import ConfigureColumnsVue from "./ConfigureColumns.vue";
import { storeMixin } from "./store";
export default {
name: "Field",
mixins: [storeMixin],
props: ["df"],
components: {
draggable
},
data() {
return {
editing: false
};
},
watch: {
editing(value) {
if (value) {
this.$nextTick(() => this.$refs["label-input"].focus());
}
},
"df.table_columns": {
deep: true,
handler() {
this.validate_table_columns();
<script setup>
import ConfigureColumnsVue from "./ConfigureColumns.vue";
import { createApp, ref, nextTick, watch } from "vue";
// props
let props = defineProps(["df"]);
// variables
let editing = ref(false);
let label_input = ref(null);
// methods
function edit_html() {
let d = new frappe.ui.Dialog({
title: __("Edit HTML"),
fields: [
{
label: __("HTML"),
fieldname: "html",
fieldtype: "Code",
options: "HTML"
}
],
primary_action: ({ html }) => {
html = frappe.dom.remove_script_and_style(html);
props.df["html"] = html;
d.hide();
}
},
methods: {
edit_html() {
let d = new frappe.ui.Dialog({
title: __("Edit HTML"),
fields: [
{
label: __("HTML"),
fieldname: "html",
fieldtype: "Code",
options: "HTML"
}
],
primary_action: ({ html }) => {
html = frappe.dom.remove_script_and_style(html);
this.$set(this.df, "html", html);
d.hide();
}
});
d.set_value("html", this.df.html);
d.show();
},
configure_columns() {
let dialog = new frappe.ui.Dialog({
title: __("Configure columns for {0}", [this.df.label]),
fields: [
{
fieldtype: "HTML",
fieldname: "columns_area"
},
{
label: "",
fieldtype: "Autocomplete",
placeholder: __("Add Column"),
fieldname: "add_column",
options: this.get_all_columns(),
onchange: () => {
let fieldname = dialog.get_value("add_column");
if (fieldname) {
let column = this.get_column_to_add(fieldname);
if (column) {
this.df.table_columns.push(column);
this.$set(
this.df,
"table_columns",
this.df.table_columns
);
dialog.set_value("add_column", "");
}
}
});
d.set_value("html", props.df.html);
d.show();
}
function configure_columns() {
let dialog = new frappe.ui.Dialog({
title: __("Configure columns for {0}", [props.df.label]),
fields: [
{
fieldtype: "HTML",
fieldname: "columns_area"
},
{
label: "",
fieldtype: "Autocomplete",
placeholder: __("Add Column"),
fieldname: "add_column",
options: get_all_columns(),
onchange: () => {
let fieldname = dialog.get_value("add_column");
if (fieldname) {
let column = get_column_to_add(fieldname);
if (column) {
props.df.table_columns.push(column);
props.df["table_columns"] = props.df.table_columns;
dialog.set_value("add_column", "");
}
}
],
on_page_show: () => {
new Vue({
el: dialog.get_field("columns_area").$wrapper.get(0),
render: h =>
h(ConfigureColumnsVue, {
props: {
df: this.df
}
})
});
},
on_hide: () => {
this.$set(
this.df,
"table_columns",
this.df.table_columns.filter(col => !col.invalid_width)
);
}
});
dialog.show();
},
get_all_columns() {
let meta = frappe.get_meta(this.df.options);
let more_columns = [
{
label: __("Sr No."),
value: "idx"
}
];
return more_columns.concat(
meta.fields
.map(tf => {
if (frappe.model.no_value_type.includes(tf.fieldtype)) {
return;
}
return {
label: tf.label,
value: tf.fieldname
};
})
.filter(Boolean)
}
],
on_page_show: () => {
createApp(ConfigureColumnsVue, { df: props.df }).mount(
dialog.get_field("columns_area").$wrapper.get(0)
);
},
get_column_to_add(fieldname) {
let standard_columns = {
idx: {
label: __("Sr No."),
fieldtype: "Data",
fieldname: "idx",
width: 10
on_hide: () => {
props.df["table_columns"] = props.df.table_columns.filter(col => !col.invalid_width);
}
});
dialog.show();
}
function get_all_columns() {
let meta = frappe.get_meta(props.df.options);
let more_columns = [
{
label: __("Sr No."),
value: "idx"
}
];
return more_columns.concat(
meta.fields
.map(tf => {
if (frappe.model.no_value_type.includes(tf.fieldtype)) {
return;
}
};
return {
label: tf.label,
value: tf.fieldname
};
})
.filter(Boolean)
);
}
function get_column_to_add(fieldname) {
let standard_columns = {
idx: {
label: __("Sr No."),
fieldtype: "Data",
fieldname: "idx",
width: 10
}
};
if (fieldname in standard_columns) {
return standard_columns[fieldname];
}
if (fieldname in standard_columns) {
return standard_columns[fieldname];
}
return {
...frappe.meta.get_docfield(this.df.options, fieldname),
width: 10
};
},
validate_table_columns() {
if (this.df.fieldtype != "Table") return;
return {
...frappe.meta.get_docfield(props.df.options, fieldname),
width: 10
};
}
function validate_table_columns() {
if (props.df.fieldtype != "Table") return;
let columns = this.df.table_columns;
let total_width = 0;
for (let column of columns) {
if (!column.width) {
column.width = 10;
}
total_width += column.width;
if (total_width > 100) {
column.invalid_width = true;
} else {
column.invalid_width = false;
}
}
let columns = props.df.table_columns;
let total_width = 0;
for (let column of columns) {
if (!column.width) {
column.width = 10;
}
total_width += column.width;
if (total_width > 100) {
column.invalid_width = true;
} else {
column.invalid_width = false;
}
}
};
}
// watch
watch(editing, (value) => {
if (value) {
nextTick(() => label_input.value.focus());
}
});
watch(
() => props.df.table_columns,
() => validate_table_columns(),
{ deep: true }
);
</script>
<style>
.field {
text-align: left;

View file

@ -12,47 +12,52 @@
<div v-show="editing" ref="editor"></div>
</div>
</template>
<script>
export default {
name: "HTMLEditor",
props: ["value", "button-label"],
data() {
return {
editing: false
};
},
methods: {
toggle_edit() {
if (this.editing) {
this.$emit("change", this.get_value());
this.editing = false;
return;
}
this.editing = true;
if (!this.control) {
this.control = frappe.ui.form.make_control({
parent: this.$refs.editor,
df: {
fieldname: "editor",
fieldtype: "HTML Editor",
min_lines: 10,
max_lines: 30,
change: () => {
this.$emit("change", this.get_value());
}
},
render_input: true
});
}
this.control.set_value(this.value);
},
get_value() {
return frappe.dom.remove_script_and_style(this.control.get_value());
}
<script setup>
import { ref } from "vue";
// props
let props = defineProps(["value", "button-label"]);
// emits
let emit = defineEmits(["change"]);
// variables
let editing = ref(false);
let control = ref(null);
let editor = ref(null);
// methods
function toggle_edit() {
if (editing.value) {
emit("change", get_value());
editing.value = false;
return;
}
};
editing.value = true;
if (!control.value) {
control.value = frappe.ui.form.make_control({
parent: editor.value,
df: {
fieldname: "editor",
fieldtype: "HTML Editor",
min_lines: 10,
max_lines: 30,
change: () => {
emit("change", get_value());
}
},
render_input: true
});
}
control.value.set_value(props.value);
}
function get_value() {
return frappe.dom.remove_script_and_style(control.value.get_value());
}
</script>
<style>
.html-editor {
position: relative;

View file

@ -3,7 +3,7 @@
<div class="mb-4 d-flex justify-content-between">
<div class="d-flex align-items-center">
<div
v-if="letterhead && $store.edit_letterhead"
v-if="letterhead && store.edit_letterhead"
class="btn-group"
role="group"
aria-label="Align Letterhead"
@ -24,7 +24,7 @@
</div>
<input
class="ml-4 custom-range"
v-if="letterhead && $store.edit_letterhead"
v-if="letterhead && store.edit_letterhead"
type="range"
name="image-resize"
min="20"
@ -41,13 +41,13 @@
<div>
<button
class="ml-2 btn btn-default btn-xs"
v-if="letterhead && $store.edit_letterhead"
v-if="letterhead && store.edit_letterhead"
@click="upload_image"
>
{{ __("Change Image") }}
</button>
<button
v-if="letterhead && $store.edit_letterhead"
v-if="letterhead && store.edit_letterhead"
class="ml-2 btn btn-default btn-xs btn-change-letterhead"
@click="change_letterhead"
>
@ -59,7 +59,7 @@
@click="toggle_edit_letterhead"
>
{{
!$store.edit_letterhead
!store.edit_letterhead
? __("Edit Letter Head")
: __("Done")
}}
@ -74,13 +74,13 @@
</div>
</div>
<div
v-if="letterhead && !$store.edit_letterhead"
v-if="letterhead && !store.edit_letterhead"
v-html="letterhead.content"
></div>
<!-- <div v-show="letterhead && $store.edit_letterhead" ref="editor"></div> -->
<!-- <div v-show="letterhead && store.edit_letterhead" ref="editor"></div> -->
<div
class="edit-letterhead"
v-if="letterhead && $store.edit_letterhead"
v-if="letterhead && store.edit_letterhead"
:style="{
justifyContent: {
Left: 'flex-start',
@ -112,199 +112,183 @@
</div>
</div>
</template>
<script>
import { storeMixin } from "./store";
<script setup>
import { useStore } from "./store";
import { get_image_dimensions } from "./utils";
export default {
name: "LetterHeadEditor",
mixins: [storeMixin],
data() {
return {
range_input_field: null,
aspect_ratio: null
};
},
watch: {
letterhead: {
deep: true,
immediate: true,
handler(letterhead) {
if (!letterhead) return;
if (letterhead.image_width && letterhead.image_height) {
let dimension =
letterhead.image_width > letterhead.image_height
? "width"
: "height";
let dimension_value = letterhead["image_" + dimension];
letterhead.content = `
<div style="text-align: ${letterhead.align.toLowerCase()};">
<img
src="${letterhead.image}"
alt="${letterhead.name}"
${dimension}="${dimension_value}"
style="${dimension}: ${dimension_value}px;">
</div>
`;
import { ref, watch, onMounted } from "vue";
// mixin
let { letterhead, store } = useStore();
// variables
let range_input_field = ref(null);
let aspect_ratio = ref(null);
let control = ref(null);
let editor = ref(null);
// methods
function toggle_edit_letterhead() {
if (store.value.edit_letterhead) {
store.value.edit_letterhead = false;
return;
}
store.value.edit_letterhead = true;
if (!control.value) {
control.value = frappe.ui.form.make_control({
parent: editor.value,
df: {
fieldname: "letterhead",
fieldtype: "Comment",
change: () => {
letterhead.value._dirty = true;
letterhead.value.content = control.value.get_value();
}
}
}
},
mounted() {
if (!this.letterhead && frappe.boot.sysdefaults.letter_head) {
this.set_letterhead(frappe.boot.sysdefaults.letter_head);
}
this.$watch(
function() {
return this.letterhead
? this.letterhead[this.range_input_field]
: null;
},
function() {
if (this.aspect_ratio === null) return;
render_input: true,
only_input: true,
no_wrapper: true
});
}
control.value.set_value(letterhead.value.content);
};
function change_letterhead() {
let d = new frappe.ui.Dialog({
title: __("Change Letter Head"),
fields: [
{
label: __("Letter Head"),
fieldname: "letterhead",
fieldtype: "Link",
options: "Letter Head"
}
],
primary_action: ({ letterhead }) => {
if (letterhead) {
set_letterhead(letterhead);
}
d.hide();
}
});
d.show();
};
function upload_image() {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
on_success: file_doc => {
get_image_dimensions(file_doc.file_url).then(
({ width, height }) => {
letterhead.value["image"] = file_doc.file_url;
let new_width = width;
let new_height = height;
aspect_ratio.value = width / height;
range_input_field.value =
aspect_ratio.value > 1
? "image_width"
: "image_height";
let update_field =
this.range_input_field == "image_width"
? "image_height"
: "image_width";
this.letterhead[update_field] =
update_field == "image_width"
? this.aspect_ratio * this.letterhead.image_height
: this.letterhead.image_width / this.aspect_ratio;
if (width > 200) {
new_width = 200;
new_height = new_width / aspect_ratio.value;
}
if (height > 80) {
new_height = 80;
new_width = aspect_ratio.value * new_height;
}
letterhead.value["image_height"] = new_height;
letterhead.value["image_width"] = new_width;
}
);
}
});
};
function set_letterhead(_letterhead) {
store.value.change_letterhead(_letterhead).then(() => {
get_image_dimensions(letterhead.value.image).then(
({ width, height }) => {
aspect_ratio.value = width / height;
range_input_field.value =
aspect_ratio.value > 1
? "image_width"
: "image_height";
}
);
},
methods: {
toggle_edit_letterhead() {
if (this.$store.edit_letterhead) {
this.$store.edit_letterhead = false;
return;
}
this.$store.edit_letterhead = true;
if (!this.control) {
this.control = frappe.ui.form.make_control({
parent: this.$refs.editor,
df: {
fieldname: "letterhead",
fieldtype: "Comment",
change: () => {
this.letterhead._dirty = true;
this.letterhead.content = this.control.get_value();
}
},
render_input: true,
only_input: true,
no_wrapper: true
});
}
this.control.set_value(this.letterhead.content);
},
change_letterhead() {
let d = new frappe.ui.Dialog({
title: __("Change Letter Head"),
fields: [
{
label: __("Letter Head"),
fieldname: "letterhead",
fieldtype: "Link",
options: "Letter Head"
}
],
primary_action: ({ letterhead }) => {
if (letterhead) {
this.set_letterhead(letterhead);
}
d.hide();
}
});
d.show();
},
upload_image() {
new frappe.ui.FileUploader({
folder: "Home/Attachments",
on_success: file_doc => {
get_image_dimensions(file_doc.file_url).then(
({ width, height }) => {
this.$set(
this.letterhead,
"image",
file_doc.file_url
);
let new_width = width;
let new_height = height;
this.aspect_ratio = width / height;
this.range_input_field =
this.aspect_ratio > 1
? "image_width"
: "image_height";
if (width > 200) {
new_width = 200;
new_height = new_width / aspect_ratio;
}
if (height > 80) {
new_height = 80;
new_width = aspect_ratio * new_height;
}
this.$set(
this.letterhead,
"image_height",
new_height
);
this.$set(
this.letterhead,
"image_width",
new_width
);
}
);
}
});
},
set_letterhead(letterhead) {
this.$store.change_letterhead(letterhead).then(() => {
get_image_dimensions(this.letterhead.image).then(
({ width, height }) => {
this.aspect_ratio = width / height;
this.range_input_field =
this.aspect_ratio > 1
? "image_width"
: "image_height";
}
);
});
},
create_letterhead() {
let d = new frappe.ui.Dialog({
title: __("Create Letter Head"),
fields: [
{
label: __("Letter Head Name"),
fieldname: "name",
fieldtype: "Data"
}
],
primary_action: ({ name }) => {
return frappe.db
.insert({
doctype: "Letter Head",
letter_head_name: name,
source: "Image"
})
.then(doc => {
d.hide();
this.$store.change_letterhead(doc.name).then(() => {
this.toggle_edit_letterhead();
});
});
}
});
d.show();
}
}
});
};
function create_letterhead() {
let d = new frappe.ui.Dialog({
title: __("Create Letter Head"),
fields: [
{
label: __("Letter Head Name"),
fieldname: "name",
fieldtype: "Data"
}
],
primary_action: ({ name }) => {
return frappe.db
.insert({
doctype: "Letter Head",
letter_head_name: name,
source: "Image"
})
.then(doc => {
d.hide();
store.value.change_letterhead(doc.name).then(() => {
toggle_edit_letterhead();
});
});
}
});
d.show();
}
// mounted
onMounted(() => {
if (!letterhead.value && frappe.boot.sysdefaults.letter_head) {
set_letterhead(frappe.boot.sysdefaults.letter_head);
}
watch(() => {
return letterhead.value
? letterhead.value[range_input_field.value]
: null;
}, () => {
if (aspect_ratio.value === null) return;
let update_field =
range_input_field.value == "image_width"
? "image_height"
: "image_width";
letterhead.value[update_field] =
update_field == "image_width"
? aspect_ratio.value * letterhead.value.image_height
: letterhead.value.image_width / aspect_ratio.value;
});
});
// watch
watch(letterhead, () => {
if (!letterhead.value) return;
if (letterhead.value.image_width && letterhead.value.image_height) {
let dimension =
letterhead.value.image_width > letterhead.value.image_height
? "width"
: "height";
let dimension_value = letterhead.value["image_" + dimension];
letterhead.value.content = `
<div style="text-align: ${letterhead.value.align.toLowerCase()};">
<img
src="${letterhead.value.image}"
alt="${letterhead.value.name}"
${dimension}="${dimension_value}"
style="${dimension}: ${dimension_value}px;">
</div>
`;
}
}, { deep: true }, { immediate: true });
</script>
<style scoped>
.letterhead {
position: relative;

View file

@ -2,10 +2,10 @@
<div class="h-100">
<div class="row">
<div class="col">
<div class="preview-control" ref="doc-select"></div>
<div class="preview-control" ref="doc_select_ref"></div>
</div>
<div class="col">
<div class="preview-control" ref="preview-type"></div>
<div class="preview-control" ref="preview_type_ref"></div>
</div>
<div class="col d-flex">
<a
@ -36,85 +36,89 @@
></iframe>
</div>
</template>
<script>
import { storeMixin } from "./store";
export default {
name: "Preview",
mixins: [storeMixin],
data() {
return {
type: "PDF",
docname: null,
preview_loaded: false
};
},
mounted() {
this.doc_select = frappe.ui.form.make_control({
parent: this.$refs["doc-select"],
df: {
label: __("Select {0}", [__(this.doctype)]),
fieldname: "docname",
fieldtype: "Link",
options: this.doctype,
change: () => {
this.docname = this.doc_select.get_value();
}
},
render_input: true
});
this.preview_type = frappe.ui.form.make_control({
parent: this.$refs["preview-type"],
df: {
label: __("Preview type"),
fieldname: "docname",
fieldtype: "Select",
options: ["PDF", "HTML"],
change: () => {
this.type = this.preview_type.get_value();
}
},
render_input: true
});
this.preview_type.set_value(this.type);
this.get_default_docname().then(
docname => docname && this.doc_select.set_value(docname)
);
this.$store.$on("after_save", () => {
this.refresh();
});
},
methods: {
refresh() {
this.$refs.iframe.contentWindow.location.reload();
},
get_default_docname() {
return frappe.db.get_list(this.doctype, { limit: 1 }).then(doc => {
return doc.length > 0 ? doc[0].name : null;
});
}
},
computed: {
doctype() {
return this.print_format.doc_type;
},
url() {
if (!this.docname) return null;
let params = new URLSearchParams();
params.append("doctype", this.doctype);
params.append("name", this.docname);
params.append("print_format", this.print_format.name);
if (this.$store.letterhead) {
params.append("letterhead", this.$store.letterhead.name);
}
let url =
this.type == "PDF"
? `/api/method/frappe.utils.weasyprint.download_pdf`
: "/printpreview";
return `${url}?${params.toString()}`;
}
<script setup>
import { useStore } from "./store";
import { ref, computed, onMounted } from "vue";
// mixin
let { print_format, store } = useStore();
// variables
let type = ref("PDF");
let docname = ref(null);
let preview_loaded = ref(false);
let iframe = ref(null);
let doc_select_ref = ref(null);
let preview_type_ref = ref(null);
let doc_select = ref(null);
let preview_type = ref(null);
// methods
function refresh() {
iframe.value?.contentWindow.location.reload();
}
function get_default_docname() {
return frappe.db.get_list(doctype.value, { limit: 1 }).then(doc => {
return doc.length > 0 ? doc[0].name : null;
});
}
// computed
let doctype = computed(() => {
return print_format.value.doc_type;
});
let url = computed(() => {
if (!docname.value) return null;
let params = new URLSearchParams();
params.append("doctype", doctype.value);
params.append("name", docname.value);
params.append("print_format", print_format.value.name);
if (store.value.letterhead) {
params.append("letterhead", store.value.letterhead.name);
}
};
let _url =
type.value == "PDF"
? `/api/method/frappe.utils.weasyprint.download_pdf`
: "/printpreview";
return `${_url}?${params.toString()}`;
});
// mounted
onMounted(() => {
doc_select.value = frappe.ui.form.make_control({
parent: doc_select_ref.value,
df: {
label: __("Select {0}", [__(doctype.value)]),
fieldname: "docname",
fieldtype: "Link",
options: doctype.value,
change: () => {
docname.value = doc_select.value.get_value();
}
},
render_input: true
});
preview_type.value = frappe.ui.form.make_control({
parent: preview_type_ref.value,
df: {
label: __("Preview type"),
fieldname: "docname",
fieldtype: "Select",
options: ["PDF", "HTML"],
change: () => {
type.value = preview_type.value.get_value();
}
},
render_input: true
});
preview_type.value.set_value(type.value);
get_default_docname().then(doc_name => {
doc_name && doc_select.value.set_value(doc_name);
});
});
</script>
<style scoped>
.preview-iframe {
width: 100%;

View file

@ -5,7 +5,7 @@
<LetterHeadEditor type="Header" />
<HTMLEditor
:value="layout.header"
@change="$set(layout, 'header', $event)"
@change="layout.header = $event"
:button-label="__('Edit Header')"
/>
<draggable
@ -14,17 +14,18 @@
group="sections"
filter=".section-columns, .column, .field"
:animation="200"
item-key="id"
>
<PrintFormatSection
v-for="(section, i) in layout.sections"
:key="i"
:section="section"
@add_section_above="add_section_above(section)"
/>
<template #item="{ element }">
<PrintFormatSection
:section="element"
@add_section_above="add_section_above(element)"
/>
</template>
</draggable>
<HTMLEditor
:value="layout.footer"
@change="$set(layout, 'footer', $event)"
@change="layout.footer = $event"
:button-label="__('Edit Footer')"
/>
<HTMLEditor
@ -36,92 +37,87 @@
</div>
</template>
<script>
<script setup>
import draggable from "vuedraggable";
import HTMLEditor from "./HTMLEditor.vue";
import LetterHeadEditor from "./LetterHeadEditor.vue";
import PrintFormatSection from "./PrintFormatSection.vue";
import { storeMixin } from "./store";
import { useStore } from "./store";
import { computed, inject, watch } from "vue";
export default {
name: "PrintFormat",
mixins: [storeMixin],
components: {
draggable,
PrintFormatSection,
LetterHeadEditor,
HTMLEditor
},
computed: {
rootStyles() {
let {
margin_top = 0,
margin_bottom = 0,
margin_left = 0,
margin_right = 0
} = this.print_format;
return {
padding: `${margin_top}mm ${margin_right}mm ${margin_bottom}mm ${margin_left}mm`,
width: "210mm",
minHeight: "297mm"
};
},
page_number_style() {
let style = {
position: "absolute",
background: "white",
padding: "4px",
borderRadius: "var(--border-radius)",
border: "1px solid var(--border-color)"
};
if (this.print_format.page_number.includes("Top")) {
style.top = this.print_format.margin_top / 2 + "mm";
style.transform = "translateY(-50%)";
}
if (this.print_format.page_number.includes("Left")) {
style.left = this.print_format.margin_left + "mm";
}
if (this.print_format.page_number.includes("Right")) {
style.right = this.print_format.margin_right + "mm";
}
if (this.print_format.page_number.includes("Bottom")) {
style.bottom = this.print_format.margin_bottom / 2 + "mm";
style.transform = "translateY(50%)";
}
if (this.print_format.page_number.includes("Center")) {
style.left = "50%";
style.transform += " translateX(-50%)";
}
if (this.print_format.page_number.includes("Hide")) {
style.display = "none";
}
return style;
}
},
methods: {
add_section_above(section) {
let sections = [];
for (let _section of this.layout.sections) {
if (_section === section) {
sections.push({
label: "",
columns: [
{ label: "", fields: [] },
{ label: "", fields: [] }
]
});
}
sections.push(_section);
}
this.$set(this.layout, "sections", sections);
},
update_letterhead_footer(val) {
this.letterhead.footer = val;
this.letterhead._dirty = true;
// mixins
let { layout, letterhead, print_format } = useStore();
let store = inject("$store");
// methods
function add_section_above(section) {
let sections = [];
for (let _section of layout.value.sections) {
if (_section === section) {
sections.push({
label: "",
columns: [
{ label: "", fields: [] },
{ label: "", fields: [] }
]
});
}
sections.push(_section);
}
};
layout.value["sections"] = sections;
}
function update_letterhead_footer(val) {
letterhead.value.footer = val;
}
// computed
let rootStyles = computed(() => {
let {
margin_top = 0,
margin_bottom = 0,
margin_left = 0,
margin_right = 0
} = print_format.value;
return {
padding: `${margin_top}mm ${margin_right}mm ${margin_bottom}mm ${margin_left}mm`,
width: "210mm",
minHeight: "297mm"
};
});
let page_number_style = computed(() => {
let style = {
position: "absolute",
background: "white",
padding: "4px",
borderRadius: "var(--border-radius)",
border: "1px solid var(--border-color)"
};
if (print_format.value.page_number.includes("Top")) {
style.top = print_format.value.margin_top / 2 + "mm";
style.transform = "translateY(-50%)";
}
if (print_format.value.page_number.includes("Left")) {
style.left = print_format.value.margin_left + "mm";
}
if (print_format.value.page_number.includes("Right")) {
style.right = print_format.value.margin_right + "mm";
}
if (print_format.value.page_number.includes("Bottom")) {
style.bottom = print_format.value.margin_bottom / 2 + "mm";
style.transform = "translateY(50%)";
}
if (print_format.value.page_number.includes("Center")) {
style.left = "50%";
style.transform += " translateX(-50%)";
}
if (print_format.value.page_number.includes("Hide")) {
style.display = "none";
}
return style;
});
watch(layout, () => (store.dirty.value = true), { deep: true });
watch(print_format, () => (store.dirty.value = true), { deep: true });
</script>
<style scoped>

View file

@ -4,64 +4,59 @@
<PrintFormatControls />
</div>
<div class="print-format-container col-9">
<keep-alive>
<Preview v-if="show_preview" />
<PrintFormat v-else />
</keep-alive>
<KeepAlive>
<component :is="Preview" v-if="show_preview" />
<component :is="PrintFormat" v-else />
</KeepAlive>
</div>
</div>
</template>
<script>
<script setup>
import PrintFormat from "./PrintFormat.vue";
import Preview from "./Preview.vue";
import PrintFormatControls from "./PrintFormatControls.vue";
import { getStore } from "./store";
import { computed, ref, onMounted, provide } from "vue";
export default {
name: "PrintFormatBuilder",
props: ["print_format_name"],
components: {
PrintFormat,
PrintFormatControls,
Preview
},
data() {
return {
show_preview: false
};
},
provide() {
return {
$store: this.$store
};
},
mounted() {
this.$store.fetch().then(() => {
if (!this.$store.layout) {
this.$store.layout = this.$store.get_default_layout();
this.$store.save_changes();
}
});
},
methods: {
toggle_preview() {
this.show_preview = !this.show_preview;
// props
let props = defineProps(["print_format_name"]);
// variables
let show_preview = ref(false);
// computed
let $store = computed(() => {
return getStore(props.print_format_name)
});
let shouldRender = computed(() => {
return Boolean(
$store.value.print_format.value &&
$store.value.meta.value &&
$store.value.layout.value
);
});
// provide
provide("$store", $store.value);
// methods
function toggle_preview() {
show_preview.value = !show_preview.value;
}
// mounted
onMounted(() => {
$store.value.fetch().then(() => {
if (!$store.value.layout.value) {
$store.value.layout.value = $store.value.get_default_layout();
$store.value.save_changes();
}
},
computed: {
$store() {
return getStore(this.print_format_name);
},
shouldRender() {
return Boolean(
this.$store.print_format &&
this.$store.meta &&
this.$store.layout
);
}
}
};
});
});
defineExpose({ toggle_preview, $store });
</script>
<style scoped>

View file

@ -109,182 +109,184 @@
:group="{ name: 'fields', pull: 'clone', put: false }"
:sort="false"
:clone="clone_field"
item-key="id"
>
<div
class="field"
v-for="df in fields"
:key="df.fieldname"
:title="df.fieldname"
>
{{ df.label }}
</div>
<template #item="{ element }">
<div
class="field"
:title="element.fieldname"
>
{{ element.label }}
</div>
</template>
</draggable>
</div>
</div>
</div>
</template>
<script>
<script setup>
import draggable from "vuedraggable";
import { get_table_columns, pluck } from "./utils";
import { storeMixin } from "./store";
import { useStore } from "./store";
import { computed, onMounted, ref, watch, inject } from "vue";
export default {
name: "PrintFormatControls",
mixins: [storeMixin],
data() {
return {
search_text: "",
google_fonts: []
};
},
components: {
draggable
},
mounted() {
let method =
"frappe.printing.page.print_format_builder_beta.print_format_builder_beta.get_google_fonts";
frappe.call(method).then(r => {
this.google_fonts = r.message || [];
if (!this.google_fonts.includes(this.print_format.font)) {
this.google_fonts.push(this.print_format.font);
}
});
},
methods: {
update_margin(fieldname, value) {
value = parseFloat(value);
if (value < 0) {
value = 0;
}
this.$store.print_format[fieldname] = value;
},
clone_field(df) {
let cloned = pluck(df, [
"label",
"fieldname",
"fieldtype",
"options",
"table_columns",
"html",
"field_template"
]);
if (cloned.custom) {
// generate unique fieldnames for custom blocks
cloned.fieldname += "_" + frappe.utils.get_random(8);
}
return cloned;
}
},
computed: {
margins() {
return [
{ label: __("Top"), fieldname: "margin_top" },
{ label: __("Bottom"), fieldname: "margin_bottom" },
{ label: __("Left", null, 'alignment'), fieldname: "margin_left" },
{ label: __("Right", null, 'alignment'), fieldname: "margin_right" }
];
},
fields() {
let fields = this.meta.fields
.filter(df => {
if (
["Section Break", "Column Break"].includes(df.fieldtype)
) {
return false;
}
if (this.search_text) {
if (df.fieldname.includes(this.search_text)) {
return true;
}
if (df.label && df.label.includes(this.search_text)) {
return true;
}
return false;
} else {
return true;
}
})
.map(df => {
let out = {
label: df.label,
fieldname: df.fieldname,
fieldtype: df.fieldtype,
options: df.options
};
if (df.fieldtype == "Table") {
out.table_columns = get_table_columns(df);
}
return out;
});
// variables
let search_text = ref("");
let google_fonts = ref([]);
return [
{
label: __("Custom HTML"),
fieldname: "custom_html",
fieldtype: "HTML",
html: "",
custom: 1
},
{
label: __("ID (name)"),
fieldname: "name",
fieldtype: "Data"
},
{
label: __("Spacer"),
fieldname: "spacer",
fieldtype: "Spacer",
custom: 1
},
{
label: __("Divider"),
fieldname: "divider",
fieldtype: "Divider",
custom: 1
},
...this.print_templates,
...fields
];
},
print_templates() {
let templates = this.print_format.__onload.print_templates || {};
let out = [];
for (let template of templates) {
let df;
if (template.field) {
df = frappe.meta.get_docfield(
this.meta.name,
template.field
);
} else {
df = {
label: template.name,
fieldname: frappe.scrub(template.name)
};
// inject
let store = inject("$store");
// mixins
let { meta, print_format } = useStore();
// methods
function update_margin(fieldname, value) {
value = parseFloat(value);
if (value < 0) {
value = 0;
}
print_format.value[fieldname] = value;
}
function clone_field(df) {
let cloned = pluck(df, [
"label",
"fieldname",
"fieldtype",
"options",
"table_columns",
"html",
"field_template"
]);
if (cloned.custom) {
// generate unique fieldnames for custom blocks
cloned.fieldname += "_" + frappe.utils.get_random(8);
}
return cloned;
}
// computed
let margins = computed(() => {
return [
{ label: __("Top"), fieldname: "margin_top" },
{ label: __("Bottom"), fieldname: "margin_bottom" },
{ label: __("Left", null, 'alignment'), fieldname: "margin_left" },
{ label: __("Right", null, 'alignment'), fieldname: "margin_right" }
];
});
let fields = computed(() => {
let fields = meta.value.fields
.filter(df => {
if (
["Section Break", "Column Break"].includes(df.fieldtype)
) {
return false;
}
if (search_text.value) {
if (df.fieldname.includes(search_text.value)) {
return true;
}
out.push({
label: `${__(df.label)} (${__("Field Template")})`,
fieldname: df.fieldname + "_template",
fieldtype: "Field Template",
field_template: template.name
});
if (df.label && df.label.includes(search_text.value)) {
return true;
}
return false;
} else {
return true;
}
})
.map(df => {
let out = {
label: df.label,
fieldname: df.fieldname,
fieldtype: df.fieldtype,
options: df.options
};
if (df.fieldtype == "Table") {
out.table_columns = get_table_columns(df);
}
return out;
});
return [
{
label: __("Custom HTML"),
fieldname: "custom_html",
fieldtype: "HTML",
html: "",
custom: 1
},
page_number_positions() {
return [
{ label: __("Hide"), value: "Hide" },
{ label: __("Top Left"), value: "Top Left" },
{ label: __("Top Center"), value: "Top Center" },
{ label: __("Top Right"), value: "Top Right" },
{ label: __("Bottom Left"), value: "Bottom Left" },
{ label: __("Bottom Center"), value: "Bottom Center" },
{ label: __("Bottom Right"), value: "Bottom Right" }
];
{
label: __("ID (name)"),
fieldname: "name",
fieldtype: "Data"
},
{
label: __("Spacer"),
fieldname: "spacer",
fieldtype: "Spacer",
custom: 1
},
{
label: __("Divider"),
fieldname: "divider",
fieldtype: "Divider",
custom: 1
},
...print_templates.value,
...fields
];
});
let print_templates = computed(() => {
let templates = print_format.value.__onload.print_templates || {};
let out = [];
for (let template of templates) {
let df;
if (template.field) {
df = frappe.meta.get_docfield(
meta.value.name,
template.field
);
} else {
df = {
label: template.name,
fieldname: frappe.scrub(template.name)
};
}
out.push({
label: `${__(df.label)} (${__("Field Template")})`,
fieldname: df.fieldname + "_template",
fieldtype: "Field Template",
field_template: template.name
});
}
};
return out;
});
let page_number_positions = computed(() => {
return [
{ label: __("Hide"), value: "Hide" },
{ label: __("Top Left"), value: "Top Left" },
{ label: __("Top Center"), value: "Top Center" },
{ label: __("Top Right"), value: "Top Right" },
{ label: __("Bottom Left"), value: "Bottom Left" },
{ label: __("Bottom Center"), value: "Bottom Center" },
{ label: __("Bottom Right"), value: "Bottom Right" }
];
});
// mounted
onMounted(() => {
let method =
"frappe.printing.page.print_format_builder_beta.print_format_builder_beta.get_google_fonts";
frappe.call(method).then(r => {
google_fonts.value = r.message || [];
if (!google_fonts.value.includes(print_format.value.font)) {
google_fonts.value.push(print_format.value.font);
}
});
});
watch(print_format, () => (store.dirty.value = true), { deep: true });
</script>
<style scoped>

View file

@ -59,12 +59,11 @@
v-model="column.fields"
group="fields"
:animation="150"
item-key="id"
>
<Field
v-for="df in get_fields(column)"
:key="df.fieldname"
:df="df"
/>
<template #item="{ element }">
<Field :df="element" />
</template>
</draggable>
</div>
</div>
@ -78,102 +77,90 @@
</div>
</template>
<script>
<script setup>
import draggable from "vuedraggable";
import Field from "./Field.vue";
import { storeMixin } from "./store";
import { computed } from "vue";
export default {
name: "PrintFormatSection",
mixins: [storeMixin],
props: ["section"],
components: {
draggable,
Field
},
methods: {
add_column() {
if (this.section.columns.length < 4) {
this.section.columns.push({
label: "",
fields: []
});
}
},
remove_column() {
if (this.section.columns.length <= 1) return;
// props
let props = defineProps(["section"]);
let columns = this.section.columns.slice();
let last_column_fields = columns.slice(-1)[0].fields.slice();
let index = columns.length - 1;
columns = columns.slice(0, index);
let last_column = columns[index - 1];
last_column.fields = [...last_column.fields, ...last_column_fields];
// emits
let emit = defineEmits(["add_section_above"]);
this.$set(this.section, "columns", columns);
},
add_page_break() {
this.$set(this.section, "page_break", true);
},
remove_page_break() {
this.$set(this.section, "page_break", false);
},
get_fields(column) {
return column.fields.filter(df => !df.remove);
}
},
computed: {
section_options() {
return [
{
label: __("Add section above"),
action: () => this.$emit("add_section_above")
},
{
label: __("Add column"),
action: this.add_column,
condition: () => this.section.columns.length < 4
},
{
label: __("Remove column"),
action: this.remove_column,
condition: () => this.section.columns.length > 1
},
{
label: __("Add page break"),
action: this.add_page_break,
condition: () => !this.section.page_break
},
{
label: __("Remove page break"),
action: this.remove_page_break,
condition: () => this.section.page_break
},
{
label: __("Remove section"),
action: () => this.$set(this.section, "remove", true)
},
{
label: __("Field Orientation (Left-Right)"),
condition: () => !this.section.field_orientation,
action: () =>
this.$set(
this.section,
"field_orientation",
"left-right"
)
},
{
label: __("Field Orientation (Top-Down)"),
condition: () =>
this.section.field_orientation == "left-right",
action: () =>
this.$set(this.section, "field_orientation", "")
}
].filter(option => (option.condition ? option.condition() : true));
}
// methods
function add_column() {
if (props.section.columns.length < 4) {
props.section.columns.push({
label: "",
fields: []
});
}
};
}
function remove_column() {
if (props.section.columns.length <= 1) return;
let columns = props.section.columns.slice();
let last_column_fields = columns.slice(-1)[0].fields.slice();
let index = columns.length - 1;
columns = columns.slice(0, index);
let last_column = columns[index - 1];
last_column.fields = [...last_column.fields, ...last_column_fields];
props.section["columns"] = columns;
}
function add_page_break() {
props.section["page_break"] = true;
}
function remove_page_break() {
props.section["page_break"] = false;
}
// computed
let section_options = computed(() => {
return [
{
label: __("Add section above"),
action: () => emit("add_section_above")
},
{
label: __("Add column"),
action: add_column,
condition: () => props.section.columns.length < 4
},
{
label: __("Remove column"),
action: remove_column,
condition: () => props.section.columns.length > 1
},
{
label: __("Add page break"),
action: add_page_break,
condition: () => !props.section.page_break
},
{
label: __("Remove page break"),
action: remove_page_break,
condition: () => props.section.page_break
},
{
label: __("Remove section"),
action: () => { props.section["remove"] = true }
},
{
label: __("Field Orientation (Left-Right)"),
condition: () => !props.section.field_orientation,
action: () => { props.section["field_orientation"] = "left-right" }
},
{
label: __("Field Orientation (Top-Down)"),
condition: () =>
props.section.field_orientation == "left-right",
action: () => { props.section["field_orientation"] = "" }
}
].filter(option => (option.condition ? option.condition() : true));
})
</script>
<style scoped>

View file

@ -1,5 +1,5 @@
import { createApp } from "vue";
import PrintFormatBuilderComponent from "./PrintFormatBuilder.vue";
import { getStore } from "./store";
class PrintFormatBuilder {
constructor({ wrapper, page, print_format }) {
@ -28,28 +28,26 @@ class PrintFormatBuilder {
frappe.set_route("print-format-builder-beta");
});
let $vm = new Vue({
el: this.$wrapper.get(0),
render: (h) =>
h(PrintFormatBuilderComponent, {
props: {
print_format_name: print_format,
},
}),
});
this.$component = $vm.$children[0];
let store = getStore(print_format);
store.$watch("dirty", (value) => {
if (value) {
this.page.set_indicator("Not Saved", "orange");
$toggle_preview_btn.hide();
$reset_changes_btn.show();
} else {
this.page.clear_indicator();
$toggle_preview_btn.show();
$reset_changes_btn.hide();
}
});
let app = createApp(PrintFormatBuilderComponent, { print_format_name: print_format });
SetVueGlobals(app);
this.$component = app.mount(this.$wrapper.get(0));
this.$component.$watch(
"$store.dirty",
(dirty) => {
if (dirty.value) {
this.page.set_indicator("Not Saved", "orange");
$toggle_preview_btn.hide();
$reset_changes_btn.show();
} else {
this.page.clear_indicator();
$toggle_preview_btn.show();
$reset_changes_btn.hide();
}
},
{ deep: true }
);
this.$component.$watch("show_preview", (value) => {
$toggle_preview_btn.text(value ? __("Hide Preview") : __("Show Preview"));
});

View file

@ -1,158 +1,159 @@
import { create_default_layout, pluck } from "./utils";
let stores = {};
import { watch, ref, inject, computed, nextTick } from "vue";
export function getStore(print_format_name) {
if (stores[print_format_name]) {
return stores[print_format_name];
}
// variables
let letterhead_name = ref(null);
let print_format = ref(null);
let letterhead = ref(null);
let doctype = ref(null);
let meta = ref(null);
let layout = ref(null);
let dirty = ref(false);
let edit_letterhead = ref(false);
let options = {
data() {
return {
print_format_name,
letterhead_name: null,
print_format: null,
letterhead: null,
doctype: null,
meta: null,
layout: null,
dirty: false,
edit_letterhead: false,
};
},
watch: {
layout: {
deep: true,
handler() {
this.dirty = true;
},
},
print_format: {
deep: true,
handler() {
this.dirty = true;
},
},
},
methods: {
fetch() {
return new Promise((resolve) => {
frappe.model.clear_doc("Print Format", this.print_format_name);
frappe.model.with_doc("Print Format", this.print_format_name, () => {
let print_format = frappe.get_doc("Print Format", this.print_format_name);
frappe.model.with_doctype(print_format.doc_type, () => {
this.meta = frappe.get_meta(print_format.doc_type);
this.print_format = print_format;
this.layout = this.get_layout();
this.$nextTick(() => (this.dirty = false));
this.edit_letterhead = false;
resolve();
});
});
// methods
function fetch() {
return new Promise((resolve) => {
frappe.model.clear_doc("Print Format", print_format_name);
frappe.model.with_doc("Print Format", print_format_name, () => {
let _print_format = frappe.get_doc("Print Format", print_format_name);
frappe.model.with_doctype(_print_format.doc_type, () => {
meta.value = frappe.get_meta(_print_format.doc_type);
print_format.value = _print_format;
layout.value = get_layout();
nextTick(() => (dirty.value = false));
edit_letterhead.value = false;
resolve();
});
},
update({ fieldname, value }) {
this.$set(this.print_format, fieldname, value);
},
save_changes() {
frappe.dom.freeze(__("Saving..."));
});
});
}
function update({ fieldname, value }) {
print_format.value[fieldname] = value;
}
function save_changes() {
frappe.dom.freeze(__("Saving..."));
this.layout.sections = this.layout.sections
.filter((section) => !section.remove)
.map((section) => {
section.columns = section.columns.map((column) => {
column.fields = column.fields
.filter((df) => !df.remove)
.map((df) => {
if (df.table_columns) {
df.table_columns = df.table_columns.map((tf) => {
return pluck(tf, [
"label",
"fieldname",
"fieldtype",
"options",
"width",
"field_template",
]);
});
}
return pluck(df, [
layout.value.sections = layout.value.sections
.filter((section) => !section.remove)
.map((section) => {
section.columns = section.columns.map((column) => {
column.fields = column.fields
.filter((df) => !df.remove)
.map((df) => {
if (df.table_columns) {
df.table_columns = df.table_columns.map((tf) => {
return pluck(tf, [
"label",
"fieldname",
"fieldtype",
"options",
"table_columns",
"html",
"width",
"field_template",
]);
});
return column;
}
return pluck(df, [
"label",
"fieldname",
"fieldtype",
"options",
"table_columns",
"html",
"field_template",
]);
});
return section;
});
this.print_format.format_data = JSON.stringify(this.layout);
frappe
.call("frappe.client.save", {
doc: this.print_format,
})
.then(() => {
if (this.letterhead && this.letterhead._dirty) {
return frappe
.call("frappe.client.save", {
doc: this.letterhead,
})
.then((r) => (this.letterhead = r.message));
}
})
.then(() => this.fetch())
.always(() => {
frappe.dom.unfreeze();
this.$emit("after_save");
});
},
reset_changes() {
this.fetch();
},
get_layout() {
if (this.print_format) {
if (typeof this.print_format.format_data == "string") {
return JSON.parse(this.print_format.format_data);
}
return this.print_format.format_data;
}
return null;
},
get_default_layout() {
return create_default_layout(this.meta, this.print_format);
},
change_letterhead(letterhead) {
return frappe.db.get_doc("Letter Head", letterhead).then((doc) => {
this.letterhead = doc;
return column;
});
},
},
return section;
});
print_format.value.format_data = JSON.stringify(layout.value);
frappe
.call("frappe.client.save", {
doc: print_format.value,
})
.then(() => {
if (letterhead.value && letterhead.value._dirty) {
return frappe
.call("frappe.client.save", {
doc: letterhead.value,
})
.then((r) => (letterhead.value = r.message));
}
})
.then(() => fetch())
.always(() => {
frappe.dom.unfreeze();
});
}
function reset_changes() {
fetch();
}
function get_layout() {
if (print_format.value) {
if (typeof print_format.value.format_data == "string") {
return JSON.parse(print_format.value.format_data);
}
return print_format.value.format_data;
}
return null;
}
function get_default_layout() {
return create_default_layout(meta.value, print_format.value);
}
function change_letterhead(_letterhead) {
return frappe.db.get_doc("Letter Head", _letterhead).then((doc) => {
letterhead.value = doc;
});
}
// watch
watch(layout, () => {
dirty.value = true;
});
watch(print_format, () => {
dirty.value = true;
});
return {
letterhead_name,
print_format,
letterhead,
doctype,
meta,
layout,
dirty,
edit_letterhead,
fetch,
update,
save_changes,
reset_changes,
get_layout,
get_default_layout,
change_letterhead,
};
stores[print_format_name] = new Vue(options);
return stores[print_format_name];
}
export let storeMixin = {
inject: ["$store"],
computed: {
print_format() {
return this.$store.print_format;
},
layout() {
return this.$store.layout;
},
letterhead() {
return this.$store.letterhead;
},
meta() {
return this.$store.meta;
},
},
};
export function useStore() {
// inject store
let store = ref(inject("$store"));
// computed
let print_format = computed(() => {
return store.value.print_format;
});
let layout = computed(() => {
return store.value.layout;
});
let letterhead = computed(() => {
return store.value.letterhead;
});
let meta = computed(() => {
return store.value.meta;
});
return { print_format, layout, letterhead, meta, store };
}

View file

@ -60,7 +60,7 @@
"touch": "^3.1.0",
"vue": "3.2.39",
"vue-router": "^4.1.5",
"vuedraggable": "^2.24.3",
"vuedraggable": "^4.1.0",
"vuex": "4.0.2",
"@frappe/esbuild-plugin-postcss2": "^0.1.3",
"@vue/component-compiler": "^4.2.4",