fix: More features
- Show selection dialog if Print Format not selected - Field component - Common store - Edit Field label inline - Configure table columns - Edit Custom HTML - Preview
This commit is contained in:
parent
f58254db78
commit
f536a1ff91
10 changed files with 732 additions and 159 deletions
|
|
@ -18,6 +18,26 @@ frappe.pages["print-format-builder-beta"].on_page_load = function(wrapper) {
|
|||
print_format: route[1]
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Select Print Format to edit"),
|
||||
fields: [
|
||||
{
|
||||
label: __("Print Format"),
|
||||
fieldname: "print_format",
|
||||
fieldtype: "Link",
|
||||
options: "Print Format",
|
||||
filters: {
|
||||
print_format_builder_beta: 1
|
||||
}
|
||||
}
|
||||
],
|
||||
primary_action({ print_format }) {
|
||||
if (!print_format) return;
|
||||
frappe.set_route("print-format-builder-beta", print_format);
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
86
frappe/public/js/print_format_builder/ConfigureColumns.vue
Normal file
86
frappe/public/js/print_format_builder/ConfigureColumns.vue
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div>
|
||||
<p class="text-muted">
|
||||
{{ help_message }}
|
||||
</p>
|
||||
<div class="row font-weight-bold">
|
||||
<div class="col-8">
|
||||
{{ __("Column") }}
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{{ __("Width") }}
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
:list="df.table_columns"
|
||||
:animation="200"
|
||||
:group="df.fieldname"
|
||||
handle=".icon-drag"
|
||||
>
|
||||
<div
|
||||
class="row mt-2 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="icon-drag px-2 ml-n2">
|
||||
<svg class="icon icon-xs">
|
||||
<use xlink:href="#icon-drag"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-1" :class="{ 'text-danger': column.invalid_width }">
|
||||
{{ column.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 d-flex align-items-center">
|
||||
<input
|
||||
type="number"
|
||||
class="form-control text-right"
|
||||
v-model.number="column.width"
|
||||
min="1"
|
||||
max="12"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-xs btn-icon ml-2"
|
||||
@click="remove_column(column)"
|
||||
>
|
||||
<svg class="icon icon-sm">
|
||||
<use xlink:href="#icon-close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
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. The total width should be 12, columns marked in red will be removed.");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.icon-drag {
|
||||
cursor: grab;
|
||||
}
|
||||
</style>
|
||||
345
frappe/public/js/print_format_builder/Field.vue
Normal file
345
frappe/public/js/print_format_builder/Field.vue
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
<template>
|
||||
<div class="field" :title="df.fieldname" @click="editing = true">
|
||||
<div class="field-controls">
|
||||
<div>
|
||||
<div
|
||||
class="custom-html"
|
||||
v-if="df.fieldtype == 'HTML' && df.html"
|
||||
v-html="df.html"
|
||||
></div>
|
||||
<input
|
||||
v-else-if="editing && df.fieldtype != 'HTML'"
|
||||
ref="label-input"
|
||||
class="label-input"
|
||||
type="text"
|
||||
:placeholder="__('Label')"
|
||||
v-model="df.label"
|
||||
@keydown.enter="editing = false"
|
||||
@blur="editing = false"
|
||||
/>
|
||||
<span v-else-if="df.label">{{ df.label }}</span>
|
||||
<i class="text-muted" v-else>
|
||||
{{ __("No Label") }} ({{ df.fieldname }})
|
||||
</i>
|
||||
</div>
|
||||
<div class="field-actions">
|
||||
<button
|
||||
v-if="df.fieldtype == 'HTML'"
|
||||
class="btn btn-xs btn-icon"
|
||||
@click="edit_html"
|
||||
>
|
||||
<svg class="icon icon-sm">
|
||||
<use xlink:href="#icon-edit"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="df.fieldtype == 'Table'"
|
||||
class="btn btn-xs"
|
||||
@click="configure_columns"
|
||||
>
|
||||
Configure columns
|
||||
</button>
|
||||
<button class="btn btn-xs btn-icon" @click="$set(df, 'remove', true)">
|
||||
<svg class="icon icon-sm">
|
||||
<use xlink:href="#icon-close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="df.fieldtype == 'Table'"
|
||||
class="table-controls row no-gutters"
|
||||
:style="{ opacity: 1 }"
|
||||
>
|
||||
<div
|
||||
:class="`col-${tf.width} table-column`"
|
||||
v-for="(tf, i) in df.table_columns"
|
||||
:key="tf.fieldname"
|
||||
>
|
||||
<div class="table-field">
|
||||
{{ tf.label }}
|
||||
</div>
|
||||
</div>
|
||||
</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();
|
||||
}
|
||||
}
|
||||
},
|
||||
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", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
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)
|
||||
);
|
||||
},
|
||||
get_column_to_add(fieldname) {
|
||||
let standard_columns = {
|
||||
idx: {
|
||||
label: __("Sr No."),
|
||||
fieldname: "Data",
|
||||
fieldname: "idx",
|
||||
width: 1
|
||||
}
|
||||
};
|
||||
|
||||
if (fieldname in standard_columns) {
|
||||
return standard_columns[fieldname];
|
||||
}
|
||||
|
||||
return {
|
||||
...frappe.meta.get_docfield(this.df.options, fieldname),
|
||||
width: 1
|
||||
};
|
||||
},
|
||||
validate_table_columns() {
|
||||
if (this.df.fieldtype != "Table") return;
|
||||
|
||||
let columns = this.df.table_columns;
|
||||
let total_columns = 0;
|
||||
for (let column of columns) {
|
||||
if (!column.width) {
|
||||
column.width = 1;
|
||||
}
|
||||
total_columns += column.width;
|
||||
if (total_columns > 12) {
|
||||
column.invalid_width = true;
|
||||
} else {
|
||||
column.invalid_width = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.field {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
background-color: var(--bg-light-gray);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px dashed var(--gray-400);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.field-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.field:not(:first-child) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-html {
|
||||
padding-right: var(--padding-xs);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.label-input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.label-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field:focus-within {
|
||||
border-style: solid;
|
||||
border-color: var(--gray-600);
|
||||
}
|
||||
|
||||
.field-actions {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.field-actions .btn {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.field-actions .btn-icon {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.field:hover .btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.table-controls {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.table-column {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-field {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px dashed var(--gray-400);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: var(--text-sm);
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.column-resize {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 6px;
|
||||
border-radius: 2px;
|
||||
height: 80%;
|
||||
background-color: var(--gray-600);
|
||||
transform: translate(50%, 10%);
|
||||
z-index: 999;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.column-resize-actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.column-resize-actions .btn-icon {
|
||||
background: white;
|
||||
}
|
||||
.column-resize-actions .btn-icon:hover {
|
||||
background: var(--bg-light-gray);
|
||||
}
|
||||
|
||||
.columns-input {
|
||||
padding: var(--padding-sm);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -19,13 +19,14 @@
|
|||
<script>
|
||||
import draggable from "vuedraggable";
|
||||
import PrintFormatSection from "./PrintFormatSection.vue";
|
||||
import { storeMixin } from "./store";
|
||||
|
||||
export default {
|
||||
name: "PrintFormat",
|
||||
props: ["print_format", "meta", "layout"],
|
||||
mixins: [storeMixin],
|
||||
components: {
|
||||
draggable,
|
||||
PrintFormatSection,
|
||||
PrintFormatSection
|
||||
},
|
||||
computed: {
|
||||
rootStyles() {
|
||||
|
|
@ -33,14 +34,14 @@ export default {
|
|||
margin_top = 0,
|
||||
margin_bottom = 0,
|
||||
margin_left = 0,
|
||||
margin_right = 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",
|
||||
minHeight: "297mm"
|
||||
};
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add_section_above(section) {
|
||||
|
|
@ -51,15 +52,15 @@ export default {
|
|||
label: "",
|
||||
columns: [
|
||||
{ label: "", fields: [] },
|
||||
{ label: "", fields: [] },
|
||||
],
|
||||
{ label: "", fields: [] }
|
||||
]
|
||||
});
|
||||
}
|
||||
sections.push(_section);
|
||||
}
|
||||
this.$set(this.layout, "sections", sections);
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -70,4 +71,4 @@ export default {
|
|||
box-shadow: var(--shadow-lg);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
<template>
|
||||
<div class="layout-main-section row" v-if="print_format && meta && layout">
|
||||
<div class="layout-main-section row" v-if="shouldRender">
|
||||
<div class="col-3">
|
||||
<PrintFormatControls
|
||||
:print_format="print_format"
|
||||
:meta="meta"
|
||||
@update="update($event)"
|
||||
/>
|
||||
<PrintFormatControls />
|
||||
</div>
|
||||
<div class="print-format-container col-9">
|
||||
<PrintFormat :print_format="print_format" :meta="meta" :layout="layout" />
|
||||
<PrintFormat />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -16,87 +12,33 @@
|
|||
<script>
|
||||
import PrintFormat from "./PrintFormat.vue";
|
||||
import PrintFormatControls from "./PrintFormatControls.vue";
|
||||
import { create_default_layout } from "./utils";
|
||||
import { getStore } from "./store";
|
||||
|
||||
export default {
|
||||
name: "PrintFormatBuilder",
|
||||
props: ["print_format_name"],
|
||||
components: {
|
||||
PrintFormat,
|
||||
PrintFormatControls,
|
||||
PrintFormatControls
|
||||
},
|
||||
data() {
|
||||
provide() {
|
||||
return {
|
||||
print_format: null,
|
||||
doctype: null,
|
||||
meta: null,
|
||||
layout: null,
|
||||
$store: this.$store
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetch();
|
||||
this.$store.fetch();
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
frappe.dom.freeze(__("Loading..."));
|
||||
frappe.model.clear_doc("Print Format", this.print_format_name);
|
||||
frappe.model.with_doc("Print Format", this.print_format_name, () => {
|
||||
this.print_format = frappe.get_doc(
|
||||
"Print Format",
|
||||
this.print_format_name
|
||||
);
|
||||
frappe.model.with_doctype(this.print_format.doc_type, () => {
|
||||
this.meta = frappe.get_meta(this.print_format.doc_type);
|
||||
this.layout = this.get_layout();
|
||||
frappe.dom.unfreeze();
|
||||
});
|
||||
});
|
||||
computed: {
|
||||
$store() {
|
||||
return getStore(this.print_format_name);
|
||||
},
|
||||
update({ fieldname, value }) {
|
||||
this.$set(this.print_format, fieldname, value);
|
||||
},
|
||||
save_changes() {
|
||||
frappe.dom.freeze();
|
||||
|
||||
this.layout.sections = this.layout.sections
|
||||
.map((section) => {
|
||||
section.columns = section.columns.map((column) => {
|
||||
column.fields = column.fields.filter((df) => !df.remove);
|
||||
return column;
|
||||
});
|
||||
return section.remove ? null : section;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
this.print_format.format_data = JSON.stringify(this.layout);
|
||||
|
||||
frappe
|
||||
.call("frappe.client.save", {
|
||||
doc: this.print_format,
|
||||
})
|
||||
.then(() => {
|
||||
this.fetch();
|
||||
})
|
||||
.always(() => {
|
||||
frappe.dom.unfreeze();
|
||||
});
|
||||
},
|
||||
reset_changes() {
|
||||
this.fetch();
|
||||
},
|
||||
get_layout() {
|
||||
if (this.print_format) {
|
||||
if (!this.print_format.format_data) {
|
||||
return create_default_layout(this.meta);
|
||||
}
|
||||
if (typeof this.print_format.format_data == "string") {
|
||||
return JSON.parse(this.print_format.format_data);
|
||||
}
|
||||
return this.print_format.format_data;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
shouldRender() {
|
||||
return Boolean(
|
||||
this.$store.print_format && this.$store.meta && this.$store.layout
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -107,4 +49,4 @@ export default {
|
|||
padding-top: 0.5rem;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
type="number"
|
||||
class="form-control form-control-sm"
|
||||
:value="print_format[df.fieldname]"
|
||||
@change="(e) => update_margin(df.fieldname, e.target.value)"
|
||||
@change="e => update_margin(df.fieldname, e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
<div class="sidebar-menu">
|
||||
<div class="sidebar-label">{{ __("Fields") }}</div>
|
||||
<input
|
||||
class="form-control form-control-sm mb-2"
|
||||
class="mb-2 form-control form-control-sm"
|
||||
type="text"
|
||||
:placeholder="__('Search fields')"
|
||||
v-model="search_text"
|
||||
|
|
@ -34,8 +34,14 @@
|
|||
:list="fields"
|
||||
:group="{ name: 'fields', pull: 'clone', put: false }"
|
||||
:sort="false"
|
||||
:clone="clone_field"
|
||||
>
|
||||
<div class="field" v-for="df in fields" :key="df.fieldname">
|
||||
<div
|
||||
class="field"
|
||||
v-for="df in fields"
|
||||
:key="df.fieldname"
|
||||
:title="df.fieldname"
|
||||
>
|
||||
{{ df.label }}
|
||||
</div>
|
||||
</draggable>
|
||||
|
|
@ -46,18 +52,19 @@
|
|||
|
||||
<script>
|
||||
import draggable from "vuedraggable";
|
||||
import { get_table_columns } from "./utils";
|
||||
import { get_table_columns, pluck } from "./utils";
|
||||
import { storeMixin } from "./store";
|
||||
|
||||
export default {
|
||||
name: "PrintFormatControls",
|
||||
props: ["print_format", "meta"],
|
||||
mixins: [storeMixin],
|
||||
data() {
|
||||
return {
|
||||
search_text: "",
|
||||
search_text: ""
|
||||
};
|
||||
},
|
||||
components: {
|
||||
draggable,
|
||||
draggable
|
||||
},
|
||||
methods: {
|
||||
update_margin(fieldname, value) {
|
||||
|
|
@ -67,6 +74,19 @@ export default {
|
|||
}
|
||||
this.$emit("update", { fieldname, value });
|
||||
},
|
||||
clone_field(df) {
|
||||
let cloned = pluck(df, [
|
||||
"label",
|
||||
"fieldname",
|
||||
"fieldtype",
|
||||
"options",
|
||||
"table_columns"
|
||||
]);
|
||||
if (cloned.fieldname == "custom_html") {
|
||||
cloned.fieldname += "_" + frappe.utils.get_random(8);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
margins() {
|
||||
|
|
@ -74,12 +94,12 @@ export default {
|
|||
{ label: __("Top"), fieldname: "margin_top" },
|
||||
{ label: __("Bottom"), fieldname: "margin_bottom" },
|
||||
{ label: __("Left"), fieldname: "margin_left" },
|
||||
{ label: __("Right"), fieldname: "margin_right" },
|
||||
{ label: __("Right"), fieldname: "margin_right" }
|
||||
];
|
||||
},
|
||||
fields() {
|
||||
let fields = this.meta.fields
|
||||
.filter((df) => {
|
||||
.filter(df => {
|
||||
if (["Section Break", "Column Break"].includes(df.fieldtype)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -95,12 +115,12 @@ export default {
|
|||
return true;
|
||||
}
|
||||
})
|
||||
.map((df) => {
|
||||
.map(df => {
|
||||
let out = {
|
||||
label: df.label,
|
||||
fieldname: df.fieldname,
|
||||
options: df.options,
|
||||
reqd: df.reqd,
|
||||
fieldtype: df.fieldtype,
|
||||
options: df.options
|
||||
};
|
||||
if (df.fieldtype == "Table") {
|
||||
out.table_columns = get_table_columns(df);
|
||||
|
|
@ -110,15 +130,20 @@ export default {
|
|||
|
||||
return [
|
||||
{
|
||||
label: "Custom HTML",
|
||||
label: __("Custom HTML"),
|
||||
fieldname: "custom_html",
|
||||
fieldtype: "HTML",
|
||||
html: "",
|
||||
html: ""
|
||||
},
|
||||
...fields,
|
||||
{
|
||||
label: __("ID (name)"),
|
||||
fieldname: "name",
|
||||
fieldtype: "Data"
|
||||
},
|
||||
...fields
|
||||
];
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -160,4 +185,4 @@ export default {
|
|||
.sidebar-menu:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -49,23 +49,11 @@
|
|||
group="fields"
|
||||
:animation="150"
|
||||
>
|
||||
<button
|
||||
class="field"
|
||||
<Field
|
||||
v-for="df in get_fields(column)"
|
||||
:key="df.fieldname"
|
||||
>
|
||||
<div>
|
||||
{{ df.label }}
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-xs btn-remove-field"
|
||||
@click="$set(df, 'remove', true)"
|
||||
>
|
||||
<svg class="icon icon-sm">
|
||||
<use xlink:href="#icon-close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</button>
|
||||
:df="df"
|
||||
/>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -74,19 +62,23 @@
|
|||
|
||||
<script>
|
||||
import draggable from "vuedraggable";
|
||||
import Field from "./Field.vue";
|
||||
import { storeMixin } from "./store";
|
||||
|
||||
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: [],
|
||||
fields: []
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
@ -103,9 +95,9 @@ export default {
|
|||
this.$set(this.section, "columns", columns);
|
||||
},
|
||||
get_fields(column) {
|
||||
return column.fields.filter((df) => !df.remove);
|
||||
},
|
||||
},
|
||||
return column.fields.filter(df => !df.remove);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -166,38 +158,8 @@ export default {
|
|||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: var(--bg-light-gray);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px dashed var(--gray-400);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.field:not(:first-child) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-remove-field {
|
||||
opacity: 0;
|
||||
padding: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-remove-field:hover {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.field:hover .btn-remove-field {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.drag-container {
|
||||
height: 100%;
|
||||
min-height: 2rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,24 @@ class PrintFormatBuilder {
|
|||
this.$wrapper = $(wrapper);
|
||||
this.page = page;
|
||||
this.print_format = print_format;
|
||||
|
||||
this.page.clear_actions()
|
||||
this.page.clear_custom_actions()
|
||||
|
||||
this.page.set_title(__("Editing {0}", [this.print_format]));
|
||||
this.page.set_primary_action(__("Save changes"), () => {
|
||||
this.$component.save_changes();
|
||||
this.$component.$store.save_changes();
|
||||
});
|
||||
this.page.set_secondary_action(__("Reset changes"), () => {
|
||||
this.$component.reset_changes();
|
||||
this.$component.$store.reset_changes();
|
||||
});
|
||||
this.page.add_button(
|
||||
__("Preview"),
|
||||
() => {
|
||||
this.preview();
|
||||
},
|
||||
{ icon: "small-file" }
|
||||
);
|
||||
|
||||
let $vm = new Vue({
|
||||
el: this.$wrapper.get(0),
|
||||
|
|
@ -24,6 +35,46 @@ class PrintFormatBuilder {
|
|||
});
|
||||
this.$component = $vm.$children[0];
|
||||
}
|
||||
|
||||
async preview() {
|
||||
let doctype = this.$component.$store.print_format.doc_type;
|
||||
let default_doc = await frappe.db.get_list(doctype, {
|
||||
limit: 1
|
||||
});
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Preview Print Format"),
|
||||
fields: [
|
||||
{
|
||||
label: __("Type"),
|
||||
fieldname: "type",
|
||||
fieldtype: "Select",
|
||||
options: ["PDF", "HTML"],
|
||||
default: "PDF"
|
||||
},
|
||||
{
|
||||
label: __("Select Document"),
|
||||
fieldname: "docname",
|
||||
fieldtype: "Link",
|
||||
options: doctype,
|
||||
reqd: 1,
|
||||
default: default_doc.length > 0 ? default_doc[0].name : null
|
||||
}
|
||||
],
|
||||
primary_action: ({ docname, type }) => {
|
||||
let params = new URLSearchParams();
|
||||
params.append("doctype", doctype);
|
||||
params.append("name", docname);
|
||||
params.append("print_format", this.print_format);
|
||||
let url =
|
||||
type == "PDF"
|
||||
? `/api/method/frappe.utils.weasyprint.download_pdf`
|
||||
: "/printpreview";
|
||||
window.open(`${url}?${params.toString()}`, "_blank");
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
}
|
||||
|
||||
frappe.provide("frappe.ui");
|
||||
|
|
|
|||
125
frappe/public/js/print_format_builder/store.js
Normal file
125
frappe/public/js/print_format_builder/store.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { create_default_layout, pluck } from "./utils";
|
||||
|
||||
let stores = {};
|
||||
|
||||
export function getStore(print_format_name) {
|
||||
if (stores[print_format_name]) {
|
||||
return stores[print_format_name];
|
||||
}
|
||||
|
||||
let options = {
|
||||
data() {
|
||||
return {
|
||||
print_format_name,
|
||||
print_format: null,
|
||||
doctype: null,
|
||||
meta: null,
|
||||
layout: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
frappe.model.clear_doc("Print Format", this.print_format_name);
|
||||
frappe.model.with_doc(
|
||||
"Print Format",
|
||||
this.print_format_name,
|
||||
() => {
|
||||
this.print_format = frappe.get_doc(
|
||||
"Print Format",
|
||||
this.print_format_name
|
||||
);
|
||||
frappe.model.with_doctype(
|
||||
this.print_format.doc_type,
|
||||
() => {
|
||||
this.meta = frappe.get_meta(
|
||||
this.print_format.doc_type
|
||||
);
|
||||
this.layout = this.get_layout();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
update({ fieldname, value }) {
|
||||
this.$set(this.print_format, fieldname, value);
|
||||
},
|
||||
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"
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
return pluck(df, [
|
||||
"label",
|
||||
"fieldname",
|
||||
"fieldtype",
|
||||
"options",
|
||||
"table_columns"
|
||||
]);
|
||||
});
|
||||
return column;
|
||||
});
|
||||
return section;
|
||||
});
|
||||
|
||||
this.print_format.format_data = JSON.stringify(this.layout);
|
||||
|
||||
frappe
|
||||
.call("frappe.client.save", {
|
||||
doc: this.print_format
|
||||
})
|
||||
.then(() => this.fetch())
|
||||
.always(() => frappe.dom.unfreeze());
|
||||
},
|
||||
reset_changes() {
|
||||
this.fetch();
|
||||
},
|
||||
get_layout() {
|
||||
if (this.print_format) {
|
||||
if (!this.print_format.format_data) {
|
||||
return create_default_layout(this.meta);
|
||||
}
|
||||
if (typeof this.print_format.format_data == "string") {
|
||||
return JSON.parse(this.print_format.format_data);
|
||||
}
|
||||
return this.print_format.format_data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
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;
|
||||
},
|
||||
meta() {
|
||||
return this.$store.meta;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -59,6 +59,7 @@ export function create_default_layout(meta) {
|
|||
let field = {
|
||||
label: df.label,
|
||||
fieldname: df.fieldname,
|
||||
fieldtype: df.fieldtype,
|
||||
options: df.options
|
||||
};
|
||||
|
||||
|
|
@ -81,20 +82,35 @@ export function create_default_layout(meta) {
|
|||
export function get_table_columns(df) {
|
||||
let table_columns = [];
|
||||
let table_fields = frappe.get_meta(df.options).fields;
|
||||
|
||||
let total_columns = 0;
|
||||
for (let tf of table_fields) {
|
||||
if (
|
||||
!in_list(["Section Break", "Column Break"], tf.fieldtype) &&
|
||||
!tf.print_hide &&
|
||||
df.label
|
||||
df.label &&
|
||||
total_columns < 12
|
||||
) {
|
||||
let columns =
|
||||
typeof tf.width == "number" && tf.width < 12 ? tf.width : 2;
|
||||
table_columns.push({
|
||||
label: tf.label,
|
||||
fieldname: tf.fieldname,
|
||||
fieldtype: tf.fieldtype,
|
||||
options: tf.options,
|
||||
width: tf.width || 0
|
||||
width: columns
|
||||
});
|
||||
total_columns += columns;
|
||||
}
|
||||
}
|
||||
return table_columns;
|
||||
}
|
||||
|
||||
export function pluck(object, keys) {
|
||||
let out = {};
|
||||
for (let key of keys) {
|
||||
if (key in object) {
|
||||
out[key] = object[key];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue