Merge pull request #22968 from shariquerik/form-builder-ui-refactor
This commit is contained in:
commit
45236a98d7
22 changed files with 873 additions and 336 deletions
|
|
@ -56,12 +56,18 @@ context("Form Builder", () => {
|
|||
cy.visit(`/app/doctype/${doctype_name}`);
|
||||
cy.findByRole("tab", { name: "Form" }).click();
|
||||
|
||||
let first_field =
|
||||
".tab-content.active .section-columns-container:first .column:first .field:first";
|
||||
let first_column = ".tab-content.active .section-columns-container:first .column:first";
|
||||
|
||||
cy.get(".fields-container .field[title='Table']").drag(first_field, {
|
||||
target: { x: 100, y: 10 },
|
||||
});
|
||||
let first_field = first_column + " .field:first";
|
||||
let last_field = first_column + " .field:last";
|
||||
|
||||
let add_new_field_btn = first_field + " .field-actions .add-field-btn";
|
||||
|
||||
// add new field
|
||||
cy.get(add_new_field_btn).click();
|
||||
|
||||
// type table and press enter
|
||||
cy.get(".combo-box-options:first .search-box > input").type("table{enter}");
|
||||
|
||||
// save
|
||||
cy.click_doc_primary_button("Save");
|
||||
|
|
@ -70,7 +76,7 @@ context("Form Builder", () => {
|
|||
cy.get_open_dialog().find(".msgprint").should("contain", "Options is required");
|
||||
cy.hide_dialog();
|
||||
|
||||
cy.get(first_field).click({ force: true });
|
||||
cy.get(last_field).click({ force: true });
|
||||
|
||||
cy.get(".sidebar-container .frappe-control[data-fieldname='options'] input")
|
||||
.click()
|
||||
|
|
@ -78,13 +84,10 @@ context("Form Builder", () => {
|
|||
cy.get("@input").clear({ force: true }).type("Web Form Field", { delay: 200 });
|
||||
cy.wait("@search_link");
|
||||
|
||||
cy.get(first_field).click({ force: true });
|
||||
cy.get(last_field).click({ force: true });
|
||||
|
||||
cy.get(first_field)
|
||||
.find(".table-controls .table-column")
|
||||
.contains("Field")
|
||||
.should("exist");
|
||||
cy.get(first_field)
|
||||
cy.get(last_field).find(".table-controls .table-column").contains("Field").should("exist");
|
||||
cy.get(last_field)
|
||||
.find(".table-controls .table-column")
|
||||
.contains("Fieldtype")
|
||||
.should("exist");
|
||||
|
|
@ -98,7 +101,7 @@ context("Form Builder", () => {
|
|||
cy.get_open_dialog().find(".msgprint").should("contain", "In List View");
|
||||
cy.hide_dialog();
|
||||
|
||||
cy.get(first_field).click({ force: true });
|
||||
cy.get(last_field).click({ force: true });
|
||||
cy.get(".sidebar-container .field label .label-area").contains("In List View").click();
|
||||
|
||||
// validate In Global Search
|
||||
|
|
@ -188,7 +191,7 @@ context("Form Builder", () => {
|
|||
// add new column
|
||||
cy.get(first_section).find(".column:first").click(15, 10);
|
||||
cy.get(first_section).find(".column:first .column-actions button:first").click();
|
||||
cy.get(first_section).find(".column").should("have.length", 3);
|
||||
cy.get(first_section).find(".column").should("have.length", 2);
|
||||
});
|
||||
|
||||
it("Remove Tab/Section/Column", () => {
|
||||
|
|
@ -197,7 +200,7 @@ context("Form Builder", () => {
|
|||
// remove column
|
||||
cy.get(first_section).find(".column:first").click(15, 10);
|
||||
cy.get(first_section).find(".column:first .column-actions button:last").click();
|
||||
cy.get(first_section).find(".column").should("have.length", 2);
|
||||
cy.get(first_section).find(".column").should("have.length", 1);
|
||||
|
||||
// remove section
|
||||
cy.get(first_section).click(15, 10);
|
||||
|
|
@ -205,7 +208,7 @@ context("Form Builder", () => {
|
|||
cy.get(".tab-content.active .form-section-container").should("have.length", 1);
|
||||
|
||||
// remove tab
|
||||
cy.get(".tab-header").realHover().find(".tab-actions .remove-tab-btn").click();
|
||||
cy.get(".tab-header .tab:last").realHover().find(".remove-tab-btn").click();
|
||||
cy.get(".tab-header .tabs .tab").should("have.length", 2);
|
||||
});
|
||||
|
||||
|
|
@ -231,14 +234,20 @@ context("Form Builder", () => {
|
|||
cy.visit(`/app/doctype/${doctype_name}`);
|
||||
cy.findByRole("tab", { name: "Form" }).click();
|
||||
|
||||
let first_field =
|
||||
".tab-content.active .section-columns-container:first .column:first .field:first";
|
||||
let first_column = ".tab-content.active .section-columns-container:first .column:first";
|
||||
|
||||
cy.get(".fields-container .field[title='Data']").drag(first_field, {
|
||||
target: { x: 100, y: 10 },
|
||||
});
|
||||
let first_field = first_column + " .field:first";
|
||||
let last_field = first_column + " .field:last";
|
||||
|
||||
cy.get(first_field).click();
|
||||
let add_new_field_btn = first_field + " .field-actions .add-field-btn";
|
||||
|
||||
// add new field
|
||||
cy.get(add_new_field_btn).click();
|
||||
|
||||
// type data and press enter
|
||||
cy.get(".combo-box-options:first .search-box > input").type("data{enter}");
|
||||
|
||||
cy.get(last_field).click();
|
||||
|
||||
// validate duplicate name
|
||||
cy.get(".sidebar-container .frappe-control[data-fieldname='fieldname'] input")
|
||||
|
|
@ -251,7 +260,7 @@ context("Form Builder", () => {
|
|||
cy.click_doc_primary_button("Save");
|
||||
cy.get_open_dialog().find(".msgprint").should("contain", "appears multiple times");
|
||||
cy.hide_dialog();
|
||||
cy.get(first_field).click();
|
||||
cy.get(last_field).click();
|
||||
cy.get(".sidebar-container .frappe-control[data-fieldname='fieldname'] input").clear({
|
||||
force: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
// MIT License. See license.txt
|
||||
|
||||
frappe.ui.form.on("DocType", {
|
||||
onload: function (frm) {
|
||||
if (frm.is_new()) {
|
||||
frappe.listview_settings["DocType"].new_doctype_dialog();
|
||||
}
|
||||
},
|
||||
|
||||
before_save: function (frm) {
|
||||
let form_builder = frappe.form_builder;
|
||||
if (form_builder?.store) {
|
||||
|
|
@ -13,6 +19,7 @@ frappe.ui.form.on("DocType", {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
after_save: function (frm) {
|
||||
if (
|
||||
frappe.form_builder &&
|
||||
|
|
@ -22,6 +29,7 @@ frappe.ui.form.on("DocType", {
|
|||
frappe.form_builder.store.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.set_query("role", "permissions", function (doc) {
|
||||
if (doc.custom && frappe.session.user != "Administrator") {
|
||||
|
|
@ -119,6 +127,20 @@ frappe.ui.form.on("DocType", {
|
|||
setup_default_views: (frm) => {
|
||||
frappe.model.set_default_views_for_doctype(frm.doc.name, frm);
|
||||
},
|
||||
|
||||
on_tab_change: (frm) => {
|
||||
let current_tab = frm.get_active_tab().label;
|
||||
|
||||
if (current_tab === "Form") {
|
||||
frm.footer.wrapper.hide();
|
||||
frm.form_wrapper.find(".form-message").hide();
|
||||
frm.form_wrapper.addClass("mb-1");
|
||||
} else {
|
||||
frm.footer.wrapper.show();
|
||||
frm.form_wrapper.find(".form-message").show();
|
||||
frm.form_wrapper.removeClass("mb-1");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("DocField", {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"form_builder_tab",
|
||||
"form_builder",
|
||||
"settings_tab",
|
||||
"sb0",
|
||||
"module",
|
||||
"is_submittable",
|
||||
|
|
@ -32,32 +35,6 @@
|
|||
"column_break_15",
|
||||
"description",
|
||||
"documentation",
|
||||
"sb2",
|
||||
"permissions",
|
||||
"restrict_to_domain",
|
||||
"read_only",
|
||||
"in_create",
|
||||
"actions_section",
|
||||
"actions",
|
||||
"links_section",
|
||||
"links",
|
||||
"document_states_section",
|
||||
"states",
|
||||
"web_view",
|
||||
"has_web_view",
|
||||
"allow_guest_to_view",
|
||||
"index_web_pages_for_search",
|
||||
"route",
|
||||
"is_published_field",
|
||||
"website_search_field",
|
||||
"advanced",
|
||||
"engine",
|
||||
"migration_hash",
|
||||
"form_builder_tab",
|
||||
"form_builder",
|
||||
"fields_section",
|
||||
"fields",
|
||||
"settings_tab",
|
||||
"form_settings_section",
|
||||
"image_field",
|
||||
"timeline_field",
|
||||
|
|
@ -92,6 +69,29 @@
|
|||
"email_append_to",
|
||||
"sender_field",
|
||||
"subject_field",
|
||||
"sb2",
|
||||
"permissions",
|
||||
"restrict_to_domain",
|
||||
"read_only",
|
||||
"in_create",
|
||||
"actions_section",
|
||||
"actions",
|
||||
"links_section",
|
||||
"links",
|
||||
"document_states_section",
|
||||
"states",
|
||||
"web_view",
|
||||
"has_web_view",
|
||||
"allow_guest_to_view",
|
||||
"index_web_pages_for_search",
|
||||
"route",
|
||||
"is_published_field",
|
||||
"website_search_field",
|
||||
"advanced",
|
||||
"engine",
|
||||
"migration_hash",
|
||||
"fields_section",
|
||||
"fields",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
|
|
@ -640,6 +640,7 @@
|
|||
"label": "Settings"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "form_builder_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Form"
|
||||
|
|
@ -742,7 +743,7 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2023-08-29 12:27:06.587523",
|
||||
"modified": "2023-11-01 16:45:14.960949",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
|
|||
122
frappe/core/doctype/doctype/doctype_list.js
Normal file
122
frappe/core/doctype/doctype/doctype_list.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
frappe.listview_settings["DocType"] = {
|
||||
primary_action: function () {
|
||||
this.new_doctype_dialog();
|
||||
},
|
||||
|
||||
new_doctype_dialog() {
|
||||
let non_developer = frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
|
||||
let fields = [
|
||||
{
|
||||
label: __("DocType Name"),
|
||||
fieldname: "name",
|
||||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
},
|
||||
{ fieldtype: "Column Break" },
|
||||
{
|
||||
label: __("Module"),
|
||||
fieldname: "module",
|
||||
fieldtype: "Link",
|
||||
options: "Module Def",
|
||||
reqd: 1,
|
||||
},
|
||||
{ fieldtype: "Section Break" },
|
||||
{
|
||||
label: __("Is Submittable"),
|
||||
fieldname: "is_submittable",
|
||||
fieldtype: "Check",
|
||||
description: __(
|
||||
"Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended."
|
||||
),
|
||||
depends_on: "eval:!doc.istable && !doc.issingle",
|
||||
},
|
||||
{
|
||||
label: __("Is Child Table"),
|
||||
fieldname: "istable",
|
||||
fieldtype: "Check",
|
||||
description: __("Child Tables are shown as a Grid in other DocTypes"),
|
||||
depends_on: "eval:!doc.is_submittable && !doc.issingle",
|
||||
},
|
||||
{
|
||||
label: __("Editable Grid"),
|
||||
fieldname: "editable_grid",
|
||||
fieldtype: "Check",
|
||||
depends_on: "istable",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
label: __("Is Single"),
|
||||
fieldname: "issingle",
|
||||
fieldtype: "Check",
|
||||
description: __(
|
||||
"Single Types have only one record no tables associated. Values are stored in tabSingles"
|
||||
),
|
||||
depends_on: "eval:!doc.istable && !doc.is_submittable",
|
||||
},
|
||||
{
|
||||
label: "Is Tree",
|
||||
fieldname: "is_tree",
|
||||
fieldtype: "Check",
|
||||
default: "0",
|
||||
depends_on: "eval:!doc.istable",
|
||||
description: "Tree structures are implemented using Nested Set",
|
||||
},
|
||||
{
|
||||
label: __("Custom?"),
|
||||
fieldname: "custom",
|
||||
fieldtype: "Check",
|
||||
default: non_developer,
|
||||
read_only: non_developer,
|
||||
},
|
||||
];
|
||||
|
||||
if (!non_developer) {
|
||||
fields.push({
|
||||
label: "Is Virtual",
|
||||
fieldname: "is_virtual",
|
||||
fieldtype: "Check",
|
||||
default: "0",
|
||||
});
|
||||
}
|
||||
|
||||
let new_d = new frappe.ui.Dialog({
|
||||
title: __("Create New DocType"),
|
||||
fields: fields,
|
||||
primary_action_label: __("Create & Continue"),
|
||||
primary_action(values) {
|
||||
if (!values.istable) values.editable_grid = 0;
|
||||
frappe.db
|
||||
.insert({
|
||||
doctype: "DocType",
|
||||
...values,
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
export: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
report: 1,
|
||||
role: "System Manager",
|
||||
share: 1,
|
||||
write: 1,
|
||||
},
|
||||
],
|
||||
fields: [{ fieldtype: "Section Break" }],
|
||||
})
|
||||
.then((doc) => {
|
||||
frappe.set_route("Form", "DocType", doc.name);
|
||||
});
|
||||
},
|
||||
secondary_action_label: __("Cancel"),
|
||||
secondary_action() {
|
||||
new_d.hide();
|
||||
if (frappe.get_route()[0] === "Form") {
|
||||
frappe.set_route("List", "DocType");
|
||||
}
|
||||
},
|
||||
});
|
||||
new_d.show();
|
||||
},
|
||||
};
|
||||
|
|
@ -180,6 +180,7 @@
|
|||
"depends_on": "doc_type",
|
||||
"fieldname": "fields_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "Fields"
|
||||
},
|
||||
{
|
||||
|
|
@ -393,7 +394,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-29 12:31:55.808848",
|
||||
"modified": "2023-10-31 02:04:25.955931",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
|
|||
|
|
@ -30,22 +30,23 @@ onMounted(() => store.fetch());
|
|||
class="form-builder-container"
|
||||
@click="store.form.selected_field = null"
|
||||
>
|
||||
<div class="form-controls" @click.stop>
|
||||
<div class="form-sidebar">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-container">
|
||||
<div class="form-main" :class="[store.preview ? 'preview' : '']">
|
||||
<Tabs />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-controls" @click.stop>
|
||||
<div class="form-sidebar">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="autocomplete-area" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-builder-container {
|
||||
margin: -12px -20px -5px;
|
||||
margin: -15px -20px -5px;
|
||||
display: flex;
|
||||
|
||||
&.resizing {
|
||||
|
|
@ -59,18 +60,19 @@ onMounted(() => store.fetch());
|
|||
|
||||
.form-container {
|
||||
flex: 1;
|
||||
background-color: var(--disabled-control-bg);
|
||||
}
|
||||
|
||||
.form-sidebar {
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-left: 1px solid var(--border-color);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.form-main {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--card-bg);
|
||||
margin: 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.form-sidebar,
|
||||
|
|
@ -171,8 +173,6 @@ onMounted(() => store.fetch());
|
|||
}
|
||||
|
||||
:deep(.preview) {
|
||||
--field-placeholder-color: var(--fg-bg-color);
|
||||
|
||||
.tab,
|
||||
.column,
|
||||
.field {
|
||||
|
|
@ -270,7 +270,7 @@ onMounted(() => store.fetch());
|
|||
}
|
||||
|
||||
.form-main > :deep(div:first-child:not(.tab-header)) {
|
||||
max-height: calc(100vh - 160px);
|
||||
max-height: calc(100vh - 175px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
135
frappe/public/js/form_builder/components/AddFieldButton.vue
Normal file
135
frappe/public/js/form_builder/components/AddFieldButton.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<button
|
||||
ref="add_field_btn_ref"
|
||||
class="add-field-btn btn btn-xs btn-icon"
|
||||
:title="__('Add field')"
|
||||
@click.stop="toggle_fieldtype_options"
|
||||
>
|
||||
<slot>
|
||||
{{ __("Add a field") }}
|
||||
</slot>
|
||||
<Teleport to="#autocomplete-area">
|
||||
<div class="autocomplete" ref="autocomplete_ref">
|
||||
<div v-show="show">
|
||||
<Autocomplete
|
||||
v-model:show="show"
|
||||
:value="autocomplete_value"
|
||||
:options="fields"
|
||||
@change="add_new_field"
|
||||
placeholder="Search fieldtypes..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Autocomplete from "./Autocomplete.vue";
|
||||
import { useStore } from "../store";
|
||||
import { clone_field } from "../utils";
|
||||
import { createPopper } from "@popperjs/core";
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps({
|
||||
column: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const selected = computed(() => {
|
||||
let fieldname = props.field ? props.field.df.name : props.column.df.name;
|
||||
return store.selected(fieldname);
|
||||
});
|
||||
|
||||
const show = ref(false);
|
||||
const autocomplete_value = ref("");
|
||||
const fields = computed(() => {
|
||||
let fields = frappe.model.all_fieldtypes
|
||||
.filter((df) => {
|
||||
if (in_list(frappe.model.layout_fields, df)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((df) => {
|
||||
let out = { label: df };
|
||||
return out;
|
||||
});
|
||||
return [...fields];
|
||||
});
|
||||
|
||||
const add_field_btn_ref = ref(null);
|
||||
const autocomplete_ref = ref(null);
|
||||
const popper = ref(null);
|
||||
|
||||
onClickOutside(add_field_btn_ref, () => (show.value = false), { ignore: [autocomplete_ref] });
|
||||
|
||||
function setupPopper() {
|
||||
if (!popper.value) {
|
||||
popper.value = createPopper(add_field_btn_ref.value, autocomplete_ref.value, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 4],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
popper.value.update();
|
||||
}
|
||||
}
|
||||
|
||||
function toggle_fieldtype_options() {
|
||||
show.value = !show.value;
|
||||
autocomplete_value.value = "";
|
||||
nextTick(() => setupPopper());
|
||||
}
|
||||
|
||||
function add_new_field(field) {
|
||||
fieldtype = field?.label;
|
||||
|
||||
if (!fieldtype) return;
|
||||
|
||||
let new_field = {
|
||||
df: store.get_df(fieldtype),
|
||||
table_columns: [],
|
||||
};
|
||||
|
||||
let cloned_field = clone_field(new_field);
|
||||
|
||||
// insert new field after current field
|
||||
let index = 0;
|
||||
if (props.field) {
|
||||
index = props.column.fields.indexOf(props.field);
|
||||
}
|
||||
props.column.fields.splice(index + 1, 0, cloned_field);
|
||||
store.form.selected_field = cloned_field.df;
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
watch(selected, (val) => {
|
||||
if (!val) show.value = false;
|
||||
});
|
||||
|
||||
defineExpose({ open: toggle_fieldtype_options });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.autocomplete {
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
159
frappe/public/js/form_builder/components/Autocomplete.vue
Normal file
159
frappe/public/js/form_builder/components/Autocomplete.vue
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<template>
|
||||
<Combobox v-model="selectedValue" nullable>
|
||||
<ComboboxOptions class="combo-box-options" static>
|
||||
<div class="search-box">
|
||||
<ComboboxInput
|
||||
ref="search"
|
||||
class="search-input form-control"
|
||||
type="text"
|
||||
@change="(e) => (query = e.target.value)"
|
||||
:value="query"
|
||||
:placeholder="props.placeholder"
|
||||
autocomplete="off"
|
||||
@click.stop
|
||||
/>
|
||||
<button class="clear-button btn btn-sm" @click="clear_search">
|
||||
<div v-html="frappe.utils.icon('close', 'sm')" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="combo-box-items">
|
||||
<ComboboxOption
|
||||
as="template"
|
||||
v-for="(field, i) in filteredOptions"
|
||||
:key="i"
|
||||
:value="field"
|
||||
v-slot="{ active }"
|
||||
>
|
||||
<li :class="['combo-box-option', active ? 'active' : '']">
|
||||
{{ field.label }}
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</div>
|
||||
</ComboboxOptions>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Combobox, ComboboxInput, ComboboxOptions, ComboboxOption } from "@headlessui/vue";
|
||||
import { computed, ref, useAttrs, watch, nextTick } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "update:show", "change"]);
|
||||
const attrs = useAttrs();
|
||||
|
||||
const query = ref(null);
|
||||
const search = ref(null);
|
||||
|
||||
const showOptions = computed({
|
||||
get() {
|
||||
return props.show;
|
||||
},
|
||||
set(val) {
|
||||
emit("update:show", val);
|
||||
},
|
||||
});
|
||||
|
||||
const selectedValue = computed({
|
||||
get() {
|
||||
return attrs.value;
|
||||
},
|
||||
set(val) {
|
||||
query.value = "";
|
||||
if (val) {
|
||||
showOptions.value = false;
|
||||
}
|
||||
emit("change", val);
|
||||
},
|
||||
});
|
||||
|
||||
const filteredOptions = computed(() => {
|
||||
return query.value
|
||||
? props.options.filter((option) => {
|
||||
return option.label.toLowerCase().includes(query.value.toLowerCase());
|
||||
})
|
||||
: props.options;
|
||||
});
|
||||
|
||||
function clear_search() {
|
||||
selectedValue.value = "";
|
||||
search.value.el.focus();
|
||||
}
|
||||
|
||||
watch(showOptions, (val) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
search.value.el.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.combo-box {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.combo-box-options {
|
||||
width: 100%;
|
||||
background-color: var(--white);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-2xl);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.combo-box-option {
|
||||
font-size: small;
|
||||
text-align: left;
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 6px 10px;
|
||||
width: 100%;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--bg-light-gray);
|
||||
}
|
||||
}
|
||||
|
||||
.combo-box-items {
|
||||
max-height: 200px;
|
||||
padding: 5px;
|
||||
padding-top: 0px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
.clear-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,15 +1,26 @@
|
|||
<script setup>
|
||||
import draggable from "vuedraggable";
|
||||
import Field from "./Field.vue";
|
||||
import AddFieldButton from "./AddFieldButton.vue";
|
||||
import EditableInput from "./EditableInput.vue";
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { move_children_to_parent, confirm_dialog } from "../utils";
|
||||
import { move_children_to_parent, confirm_dialog, is_touch_screen_device } from "../utils";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core";
|
||||
|
||||
const props = defineProps(["section", "column"]);
|
||||
let store = useStore();
|
||||
const store = useStore();
|
||||
|
||||
let hovered = ref(false);
|
||||
// delete/backspace to delete the field
|
||||
const { Backspace } = useMagicKeys();
|
||||
whenever(Backspace, (value) => {
|
||||
if (value && selected.value && store.not_using_input) {
|
||||
remove_column();
|
||||
}
|
||||
});
|
||||
|
||||
const hovered = ref(false);
|
||||
const selected = computed(() => store.selected(props.column.df.name));
|
||||
|
||||
function add_column() {
|
||||
// insert new column after the current column
|
||||
|
|
@ -29,7 +40,11 @@ function remove_column() {
|
|||
} else {
|
||||
confirm_dialog(
|
||||
__("Delete Column", null, "Title of confirmation dialog"),
|
||||
__("Are you sure you want to delete the column? All the fields in the column will be moved to the previous column.", null, "Confirmation dialog message"),
|
||||
__(
|
||||
"Are you sure you want to delete the column? All the fields in the column will be moved to the previous column.",
|
||||
null,
|
||||
"Confirmation dialog message"
|
||||
),
|
||||
() => delete_column(),
|
||||
__("Delete column", null, "Button text"),
|
||||
() => delete_column(true),
|
||||
|
|
@ -95,21 +110,14 @@ function move_columns_to_section() {
|
|||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'column',
|
||||
hovered ? 'hovered' : '',
|
||||
store.selected(column.df.name) ? 'selected' : ''
|
||||
]"
|
||||
:class="['column', selected ? 'selected' : hovered ? 'hovered' : '']"
|
||||
:title="column.df.fieldname"
|
||||
@click.stop="store.form.selected_field = column.df"
|
||||
@mouseover.stop="hovered = true"
|
||||
@mouseout.stop="hovered = false"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'column-header',
|
||||
column.df.label ? 'has-label' : '',
|
||||
]"
|
||||
:class="['column-header', column.df.label ? 'has-label' : '']"
|
||||
:hidden="!column.df.label && store.read_only"
|
||||
>
|
||||
<div class="column-label">
|
||||
|
|
@ -120,6 +128,9 @@ function move_columns_to_section() {
|
|||
/>
|
||||
</div>
|
||||
<div class="column-actions">
|
||||
<button class="btn btn-xs btn-icon" :title="__('Add Column')" @click="add_column">
|
||||
<div v-html="frappe.utils.icon('add', 'sm')"></div>
|
||||
</button>
|
||||
<button
|
||||
v-if="section.columns.indexOf(column)"
|
||||
class="btn btn-xs btn-icon"
|
||||
|
|
@ -128,9 +139,6 @@ function move_columns_to_section() {
|
|||
>
|
||||
<div v-html="frappe.utils.icon('move', 'sm')"></div>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-icon" :title="__('Add Column')" @click="add_column">
|
||||
<div v-html="frappe.utils.icon('add', 'sm')"></div>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-icon"
|
||||
:title="__('Remove Column')"
|
||||
|
|
@ -145,9 +153,9 @@ function move_columns_to_section() {
|
|||
</div>
|
||||
<draggable
|
||||
class="column-container"
|
||||
:style="{ backgroundColor: column.fields.length ? '' : 'var(--field-placeholder-color)' }"
|
||||
v-model="column.fields"
|
||||
group="fields"
|
||||
:delay="is_touch_screen_device() ? 200 : 0"
|
||||
:animation="200"
|
||||
:easing="store.get_animation"
|
||||
item-key="id"
|
||||
|
|
@ -161,11 +169,19 @@ function move_columns_to_section() {
|
|||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
class="empty-column"
|
||||
:hidden="store.read_only"
|
||||
:style="store.selected(column.df.name) ? { top: '35px' } : { top: 0 }"
|
||||
>
|
||||
<AddFieldButton :column="column" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.column {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
|
@ -250,6 +266,35 @@ function move_columns_to_section() {
|
|||
flex: 1;
|
||||
min-height: 2rem;
|
||||
border-radius: var(--border-radius);
|
||||
z-index: 1;
|
||||
|
||||
&:empty {
|
||||
& + .empty-column {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
|
||||
button {
|
||||
background-color: var(--bg-color);
|
||||
z-index: 2;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-default-hover-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& + .empty-column {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ let store = useStore();
|
|||
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
placeholder: {
|
||||
default: __("No Label")
|
||||
default: __("No Label"),
|
||||
},
|
||||
empty_label: {
|
||||
default: __("No Label")
|
||||
default: __("No Label"),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -35,6 +35,8 @@ function focus_on_label() {
|
|||
nextTick(() => input_text.value.focus());
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ focus_on_label });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -48,12 +50,12 @@ function focus_on_label() {
|
|||
:placeholder="placeholder"
|
||||
:value="text"
|
||||
:style="{ width: hidden_span_width }"
|
||||
@input="event => $emit('update:modelValue', event.target.value)"
|
||||
@input="(event) => $emit('update:modelValue', event.target.value)"
|
||||
@keydown.enter="editing = false"
|
||||
@blur="editing = false"
|
||||
@click.stop
|
||||
/>
|
||||
<span v-else-if="text" v-html="text" ></span>
|
||||
<span v-else-if="text" v-html="text"></span>
|
||||
<i v-else class="text-muted">
|
||||
{{ empty_label }}
|
||||
</i>
|
||||
|
|
@ -70,7 +72,6 @@ function focus_on_label() {
|
|||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,35 @@
|
|||
<script setup>
|
||||
import EditableInput from "./EditableInput.vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { move_children_to_parent, clone_field } from "../utils";
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import AddFieldButton from "./AddFieldButton.vue";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core";
|
||||
|
||||
const props = defineProps(["column", "field"]);
|
||||
let store = useStore();
|
||||
const store = useStore();
|
||||
|
||||
let hovered = ref(false);
|
||||
let component = computed(() => {
|
||||
const add_field_ref = ref(null);
|
||||
|
||||
// cmd/ctrl + shift + n to open the add field autocomplete
|
||||
const { ctrl_shift_n, Backspace } = useMagicKeys();
|
||||
whenever(ctrl_shift_n, (value) => {
|
||||
if (value && selected.value) {
|
||||
add_field_ref.value.open();
|
||||
}
|
||||
});
|
||||
|
||||
// delete/backspace to delete the field
|
||||
whenever(Backspace, (value) => {
|
||||
if (value && selected.value && store.not_using_input) {
|
||||
remove_field();
|
||||
}
|
||||
});
|
||||
|
||||
const label_input = ref(null);
|
||||
const hovered = ref(false);
|
||||
const selected = computed(() => store.selected(props.field.df.name));
|
||||
const component = computed(() => {
|
||||
return props.field.df.fieldtype.replace(" ", "") + "Control";
|
||||
});
|
||||
|
||||
|
|
@ -23,8 +44,8 @@ function remove_field() {
|
|||
}
|
||||
|
||||
function move_fields_to_column() {
|
||||
let current_section = store.current_tab.sections.find(section =>
|
||||
section.columns.find(column => column == props.column)
|
||||
let current_section = store.current_tab.sections.find((section) =>
|
||||
section.columns.find((column) => column == props.column)
|
||||
);
|
||||
move_children_to_parent(props, "column", "field", current_section);
|
||||
}
|
||||
|
|
@ -53,15 +74,13 @@ function duplicate_field() {
|
|||
props.column.fields.splice(index + 1, 0, duplicate_field);
|
||||
store.form.selected_field = duplicate_field.df;
|
||||
}
|
||||
|
||||
onMounted(() => selected.value && label_input.value.focus_on_label());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'field',
|
||||
hovered ? 'hovered' : '',
|
||||
store.selected(field.df.name) ? 'selected' : ''
|
||||
]"
|
||||
:class="['field', selected ? 'selected' : hovered ? 'hovered' : '']"
|
||||
:title="field.df.fieldname"
|
||||
@click.stop="store.form.selected_field = field.df"
|
||||
@mouseover.stop="hovered = true"
|
||||
|
|
@ -76,24 +95,25 @@ function duplicate_field() {
|
|||
<template #label>
|
||||
<div class="field-label">
|
||||
<EditableInput
|
||||
ref="label_input"
|
||||
:text="field.df.label"
|
||||
:placeholder="__('Label')"
|
||||
:empty_label="`${__('No Label')} (${field.df.fieldtype})`"
|
||||
v-model="field.df.label"
|
||||
/>
|
||||
<div class="reqd-asterisk" v-if="field.df.reqd">*</div>
|
||||
<div class="help-icon" v-if="field.df.documentation_url" v-html="frappe.utils.icon('help', 'sm')"></div>
|
||||
<div
|
||||
class="help-icon"
|
||||
v-if="field.df.documentation_url"
|
||||
v-html="frappe.utils.icon('help', 'sm')"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="field-actions" :hidden="store.read_only">
|
||||
<button
|
||||
v-if="field.df.fieldtype == 'HTML'"
|
||||
class="btn btn-xs btn-icon"
|
||||
@click="edit_html"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('edit', 'sm')"></div>
|
||||
</button>
|
||||
<AddFieldButton ref="add_field_ref" :column="column" :field="field">
|
||||
<div v-html="frappe.utils.icon('add', 'sm')" />
|
||||
</AddFieldButton>
|
||||
<button
|
||||
v-if="column.fields.indexOf(field)"
|
||||
class="btn btn-xs btn-icon"
|
||||
|
|
@ -104,10 +124,18 @@ function duplicate_field() {
|
|||
>
|
||||
<div v-html="frappe.utils.icon('move', 'sm')"></div>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-icon" @click.stop="duplicate_field">
|
||||
<button
|
||||
class="btn btn-xs btn-icon"
|
||||
:title="__('Duplicate field')"
|
||||
@click.stop="duplicate_field"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('duplicate', 'sm')"></div>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-icon" @click.stop="remove_field">
|
||||
<button
|
||||
class="btn btn-xs btn-icon"
|
||||
:title="__('Remove field')"
|
||||
@click.stop="remove_field"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -68,7 +68,16 @@ let docfield_df = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<SearchBox v-model="search_text" />
|
||||
<div class="header">
|
||||
<SearchBox class="flex-1" v-model="search_text" />
|
||||
<button
|
||||
class="close-btn btn btn-xs"
|
||||
:title="__('Close properties')"
|
||||
@click="store.form.selected_field = null"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-data">
|
||||
<div v-if="store.form.selected_field">
|
||||
<div class="field" v-for="(df, i) in docfield_df" :key="i">
|
||||
|
|
@ -88,8 +97,17 @@ let docfield_df = computed(() => {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.close-btn {
|
||||
margin-right: -5px;
|
||||
}
|
||||
}
|
||||
.control-data {
|
||||
height: calc(100vh - 150px);
|
||||
height: calc(100vh - 202px);
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
<script setup>
|
||||
import SearchBox from "./SearchBox.vue";
|
||||
import draggable from "vuedraggable";
|
||||
import { ref, computed } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { clone_field } from "../utils";
|
||||
|
||||
let store = useStore();
|
||||
let search_text = ref("");
|
||||
|
||||
let fields = computed(() => {
|
||||
let fields = frappe.model.all_fieldtypes
|
||||
.filter(df => {
|
||||
if (in_list(frappe.model.layout_fields, df)) {
|
||||
return false;
|
||||
}
|
||||
if (search_text.value) {
|
||||
if (df.toLowerCase().includes(search_text.value.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.map(df => {
|
||||
let out = {
|
||||
df: store.get_df(df),
|
||||
table_columns: [],
|
||||
};
|
||||
return out;
|
||||
});
|
||||
|
||||
return [...fields];
|
||||
});
|
||||
|
||||
function on_drag_start(evt) {
|
||||
$(evt.item).html('<div class="drop-it-here"></div>');
|
||||
}
|
||||
|
||||
function on_drag_end(evt) {
|
||||
let old_html = evt.clone.innerHTML;
|
||||
$(evt.item).html(old_html);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SearchBox v-model="search_text" />
|
||||
<draggable
|
||||
class="fields-container"
|
||||
:list="fields"
|
||||
:group="{ name: 'fields', pull: 'clone', put: false }"
|
||||
:sort="false"
|
||||
:clone="clone_field"
|
||||
item-key="id"
|
||||
:remove-clone-on-hide="false"
|
||||
@start="on_drag_start"
|
||||
@end="on_drag_end"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div class="field" :title="element.df.fieldtype">
|
||||
{{ element.df.fieldtype }}
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fields-container {
|
||||
height: calc(100vh - 133px);
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-auto-rows: max-content;
|
||||
|
||||
.field {
|
||||
display: block !important;
|
||||
background-color: var(--bg-light-gray);
|
||||
border-radius: var(--border-radius);
|
||||
border: 0.5px solid var(--dark-border-color);
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
|
||||
&.sortable-ghost {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<input
|
||||
class="search-input form-control"
|
||||
type="text"
|
||||
:placeholder="__('Search fields')"
|
||||
:placeholder="__('Search properties...')"
|
||||
@input="event => $emit('update:modelValue', event.target.value)"
|
||||
/>
|
||||
<span class="search-icon">
|
||||
|
|
@ -18,9 +18,8 @@
|
|||
.search-box {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0px 9px 9px;
|
||||
background-color: var(--fg-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
width: 100%;
|
||||
|
||||
.search-input {
|
||||
padding-left: 30px;
|
||||
|
|
@ -28,7 +27,7 @@
|
|||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
left: 7px;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,25 @@
|
|||
import draggable from "vuedraggable";
|
||||
import Column from "./Column.vue";
|
||||
import EditableInput from "./EditableInput.vue";
|
||||
import { ref } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { section_boilerplate, move_children_to_parent, confirm_dialog } from "../utils";
|
||||
import { section_boilerplate, move_children_to_parent, confirm_dialog, is_touch_screen_device } from "../utils";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core";
|
||||
|
||||
const props = defineProps(["tab", "section"]);
|
||||
let store = useStore();
|
||||
const store = useStore();
|
||||
|
||||
let hovered = ref(false);
|
||||
let collapsed = ref(false);
|
||||
// delete/backspace to delete the field
|
||||
const { Backspace } = useMagicKeys();
|
||||
whenever(Backspace, (value) => {
|
||||
if (value && selected.value && store.not_using_input) {
|
||||
remove_section();
|
||||
}
|
||||
});
|
||||
|
||||
const hovered = ref(false);
|
||||
const collapsed = ref(false);
|
||||
const selected = computed(() => store.selected(props.section.df.name));
|
||||
|
||||
function add_section_above() {
|
||||
let index = props.tab.sections.indexOf(props.section);
|
||||
|
|
@ -19,7 +29,7 @@ function add_section_above() {
|
|||
|
||||
function is_section_empty() {
|
||||
return !props.section.columns.some(
|
||||
column => (store.is_customize_form && !column.df.is_custom_field) || column.fields.length
|
||||
(column) => (store.is_customize_form && !column.df.is_custom_field) || column.fields.length
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -34,11 +44,15 @@ function remove_section() {
|
|||
} else {
|
||||
confirm_dialog(
|
||||
__("Delete Section", null, "Title of confirmation dialog"),
|
||||
__("Are you sure you want to delete the section? All the columns along with fields in the section will be moved to the previous section.", null, "Confirmation dialog message"),
|
||||
__(
|
||||
"Are you sure you want to delete the section? All the columns along with fields in the section will be moved to the previous section.",
|
||||
null,
|
||||
"Confirmation dialog message"
|
||||
),
|
||||
() => delete_section(),
|
||||
__("Delete section", null, "Button text"),
|
||||
() => delete_section(true),
|
||||
__("Delete entire section with columns", null, "Button text")
|
||||
__("Delete entire section with fields", null, "Button text")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -94,7 +108,7 @@ function move_sections_to_tab() {
|
|||
:class="[
|
||||
'form-section',
|
||||
hovered ? 'hovered' : '',
|
||||
store.selected(section.df.name) ? 'selected' : ''
|
||||
store.selected(section.df.name) ? 'selected' : '',
|
||||
]"
|
||||
:title="section.df.fieldname"
|
||||
@click.stop="select_section"
|
||||
|
|
@ -105,7 +119,7 @@ function move_sections_to_tab() {
|
|||
:class="[
|
||||
'section-header',
|
||||
section.df.label || section.df.collapsible ? 'has-label' : '',
|
||||
collapsed ? 'collapsed' : ''
|
||||
collapsed ? 'collapsed' : '',
|
||||
]"
|
||||
:hidden="!section.df.label && store.read_only"
|
||||
>
|
||||
|
|
@ -118,18 +132,10 @@ function move_sections_to_tab() {
|
|||
<div
|
||||
v-if="section.df.collapsible"
|
||||
class="collapse-indicator"
|
||||
v-html="frappe.utils.icon( collapsed ? 'down' : 'up-line', 'sm' )"
|
||||
v-html="frappe.utils.icon(collapsed ? 'down' : 'up-line', 'sm')"
|
||||
></div>
|
||||
</div>
|
||||
<div class="section-actions" :hidden="store.read_only">
|
||||
<button
|
||||
v-if="tab.sections.indexOf(section)"
|
||||
class="btn btn-xs btn-section"
|
||||
:title="__('Move the current section and the following sections to a new tab')"
|
||||
@click="move_sections_to_tab"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('move', 'sm')"></div>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-section"
|
||||
:title="__('Add section above')"
|
||||
|
|
@ -137,6 +143,16 @@ function move_sections_to_tab() {
|
|||
>
|
||||
<div v-html="frappe.utils.icon('add', 'sm')"></div>
|
||||
</button>
|
||||
<button
|
||||
v-if="tab.sections.indexOf(section)"
|
||||
class="btn btn-xs btn-section"
|
||||
:title="
|
||||
__('Move the current section and the following sections to a new tab')
|
||||
"
|
||||
@click="move_sections_to_tab"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('move', 'sm')"></div>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-section"
|
||||
:title="__('Remove section')"
|
||||
|
|
@ -146,22 +162,22 @@ function move_sections_to_tab() {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="section.df.description" class="section-description">{{ section.df.description }}</div>
|
||||
<div v-if="section.df.description" class="section-description">
|
||||
{{ section.df.description }}
|
||||
</div>
|
||||
<div
|
||||
class="section-columns"
|
||||
:class="{
|
||||
hidden: section.df.collapsible && collapsed,
|
||||
'has-one-column': section.columns.length === 1
|
||||
'has-one-column': section.columns.length === 1,
|
||||
}"
|
||||
>
|
||||
<draggable
|
||||
class="section-columns-container"
|
||||
:style="{
|
||||
backgroundColor: section.columns.length ? null : 'var(--field-placeholder-color)'
|
||||
}"
|
||||
v-model="section.columns"
|
||||
group="columns"
|
||||
item-key="id"
|
||||
:delay="is_touch_screen_device() ? 200 : 0"
|
||||
:animation="200"
|
||||
:easing="store.get_animation"
|
||||
:disabled="store.read_only"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
<script setup>
|
||||
import FieldTypes from "./FieldTypes.vue";
|
||||
import FieldProperties from "./FieldProperties.vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { ref } from "vue";
|
||||
|
||||
let store = useStore();
|
||||
|
||||
let tab_titles = [__("Field Types"), __("Field Properties")];
|
||||
let active_tab = ref(tab_titles[0]);
|
||||
let sidebar_width = ref(272);
|
||||
let sidebar_resizing = ref(false);
|
||||
|
||||
|
|
@ -23,7 +20,8 @@ function start_resize() {
|
|||
function resize(e) {
|
||||
sidebar_resizing.value = true;
|
||||
$(".form-builder-container").addClass("resizing");
|
||||
sidebar_width.value = e.clientX - 90;
|
||||
let screen_width = e.view.innerWidth;
|
||||
sidebar_width.value = screen_width - e.clientX - 90;
|
||||
|
||||
if (sidebar_width.value < 16 * 16) {
|
||||
sidebar_width.value = 16 * 16;
|
||||
|
|
@ -32,14 +30,6 @@ function resize(e) {
|
|||
sidebar_width.value = 24 * 16;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => store.form.selected_field,
|
||||
value => {
|
||||
active_tab.value = value ? tab_titles[1] : tab_titles[0];
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -48,21 +38,20 @@ watch(
|
|||
@mousedown="start_resize"
|
||||
/>
|
||||
<div class="sidebar-container" :style="{ width: `${sidebar_width}px` }">
|
||||
<div class="tab-header">
|
||||
<div
|
||||
:class="['tab', active_tab == tab ? 'active' : '']"
|
||||
v-for="(tab, i) in tab_titles"
|
||||
:key="i"
|
||||
@click="active_tab = tab"
|
||||
>
|
||||
{{ tab }}
|
||||
<FieldProperties v-if="store.form.selected_field" />
|
||||
<div class="default-state" v-else>
|
||||
<div class="actions" v-if="store.form.layout.tabs.length == 1 && !store.read_only">
|
||||
<button
|
||||
class="new-tab-btn btn btn-default btn-xs"
|
||||
:title="__('Add new tab')"
|
||||
@click="store.add_new_tab"
|
||||
>
|
||||
{{ __("Add tab") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div>Select a field to edit its properties.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['tab-content', active_tab == tab_titles[0] ? 'active' : '']">
|
||||
<FieldTypes />
|
||||
</div>
|
||||
<div :class="['tab-content', active_tab == tab_titles[1] ? 'active' : '']">
|
||||
<FieldProperties />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -71,7 +60,7 @@ watch(
|
|||
.sidebar-resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -6px;
|
||||
left: -5px;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
|
|
@ -80,10 +69,12 @@ watch(
|
|||
z-index: 4;
|
||||
cursor: col-resize;
|
||||
|
||||
&:hover, &.resizing {
|
||||
&:hover,
|
||||
&.resizing {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -127,4 +118,25 @@ watch(
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.default-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 163px);
|
||||
|
||||
.actions {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
<script setup>
|
||||
import Section from "./Section.vue";
|
||||
import EditableInput from "./EditableInput.vue";
|
||||
import draggable from "vuedraggable";
|
||||
import { useStore } from "../store";
|
||||
import { section_boilerplate, confirm_dialog } from "../utils";
|
||||
import { ref, computed, nextTick } from "vue";
|
||||
import { section_boilerplate, confirm_dialog, is_touch_screen_device } from "../utils";
|
||||
import draggable from "vuedraggable";
|
||||
import { ref, computed } from "vue";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core";
|
||||
|
||||
let store = useStore();
|
||||
const store = useStore();
|
||||
|
||||
let dragged = ref(false);
|
||||
let has_tabs = computed(() => store.form.layout.tabs.length > 1);
|
||||
// delete/backspace to delete the field
|
||||
const { Backspace } = useMagicKeys();
|
||||
whenever(Backspace, (value) => {
|
||||
if (value && selected.value && store.not_using_input) {
|
||||
remove_tab(store.current_tab, '', true);
|
||||
}
|
||||
});
|
||||
|
||||
const dragged = ref(false);
|
||||
const selected = computed(() => store.selected(store.current_tab.df.name));
|
||||
const has_tabs = computed(() => store.form.layout.tabs.length > 1);
|
||||
store.form.active_tab = store.form.layout.tabs[0].df.name;
|
||||
|
||||
function activate_tab(tab) {
|
||||
store.form.active_tab = tab.df.name;
|
||||
store.form.selected_field = tab.df;
|
||||
|
||||
// scroll to active tab
|
||||
nextTick(() => {
|
||||
$(".tabs .tab.active")[0].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
inline: "center",
|
||||
block: "nearest",
|
||||
});
|
||||
});
|
||||
store.activate_tab(tab);
|
||||
}
|
||||
|
||||
function drag_over(tab) {
|
||||
|
|
@ -34,13 +34,7 @@ function drag_over(tab) {
|
|||
}
|
||||
|
||||
function add_new_tab() {
|
||||
let tab = {
|
||||
df: store.get_df("Tab Break", "", "Tab " + (store.form.layout.tabs.length + 1)),
|
||||
sections: [section_boilerplate()],
|
||||
};
|
||||
|
||||
store.form.layout.tabs.push(tab);
|
||||
activate_tab(tab);
|
||||
store.add_new_tab();
|
||||
}
|
||||
|
||||
function add_new_section() {
|
||||
|
|
@ -49,49 +43,56 @@ function add_new_section() {
|
|||
store.form.selected_field = section.df;
|
||||
}
|
||||
|
||||
function is_current_tab_empty() {
|
||||
function is_tab_empty(tab) {
|
||||
// check if sections have columns and it contains fields
|
||||
return !store.current_tab.sections.some(
|
||||
section => section.columns.some(column => column.fields.length)
|
||||
return !tab.sections.some((section) =>
|
||||
section.columns.some((column) => column.fields.length)
|
||||
);
|
||||
}
|
||||
|
||||
function remove_tab() {
|
||||
function remove_tab(tab, event, force=false) {
|
||||
// is remove_tab_btn is not visible then return
|
||||
if (!event?.currentTarget?.offsetParent && !force) return;
|
||||
|
||||
if (store.is_customize_form && store.current_tab.df.is_custom_field == 0) {
|
||||
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
|
||||
throw "cannot delete standard field";
|
||||
} else if (store.has_standard_field(store.current_tab)) {
|
||||
delete_tab();
|
||||
} else if (is_current_tab_empty()) {
|
||||
delete_tab(true);
|
||||
delete_tab(tab);
|
||||
} else if (is_tab_empty(tab)) {
|
||||
delete_tab(tab, true);
|
||||
} else {
|
||||
confirm_dialog(
|
||||
__("Delete Tab", null, "Title of confirmation dialog"),
|
||||
__("Are you sure you want to delete the tab? All the sections along with fields in the tab will be moved to the previous tab.", null, "Confirmation dialog message"),
|
||||
() => delete_tab(),
|
||||
__(
|
||||
"Are you sure you want to delete the tab? All the sections along with fields in the tab will be moved to the previous tab.",
|
||||
null,
|
||||
"Confirmation dialog message"
|
||||
),
|
||||
() => delete_tab(tab),
|
||||
__("Delete tab", null, "Button text"),
|
||||
() => delete_tab(true),
|
||||
__("Delete entire tab with sections", null, "Button text")
|
||||
() => delete_tab(tab, true),
|
||||
__("Delete entire tab with fields", null, "Button text")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function delete_tab(with_children) {
|
||||
function delete_tab(tab, with_children) {
|
||||
let tabs = store.form.layout.tabs;
|
||||
let index = tabs.indexOf(store.current_tab);
|
||||
let index = tabs.indexOf(tab);
|
||||
|
||||
if (!with_children) {
|
||||
if (index > 0) {
|
||||
let prev_tab = tabs[index - 1];
|
||||
if (!is_current_tab_empty()) {
|
||||
if (!is_tab_empty(tab)) {
|
||||
// move all sections from current tab to previous tab
|
||||
prev_tab.sections = [...prev_tab.sections, ...store.current_tab.sections];
|
||||
prev_tab.sections = [...prev_tab.sections, ...tab.sections];
|
||||
}
|
||||
} else {
|
||||
// create a new tab and push sections to it
|
||||
tabs.unshift({
|
||||
df: store.get_df("Tab Break", "", __("Details")),
|
||||
sections: store.current_tab.sections,
|
||||
sections: tab.sections,
|
||||
is_first: true,
|
||||
});
|
||||
index++;
|
||||
|
|
@ -109,12 +110,13 @@ function delete_tab(with_children) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tab-header" v-if="!(store.form.layout.tabs.length == 1 && store.read_only)">
|
||||
<div class="tab-header" v-if="store.form.layout.tabs.length > 1">
|
||||
<draggable
|
||||
v-show="has_tabs"
|
||||
class="tabs"
|
||||
v-model="store.form.layout.tabs"
|
||||
group="tabs"
|
||||
:delay="is_touch_screen_device() ? 200 : 0"
|
||||
:animation="200"
|
||||
:easing="store.get_animation"
|
||||
item-key="id"
|
||||
|
|
@ -135,6 +137,14 @@ function delete_tab(with_children) {
|
|||
:placeholder="__('Tab Label')"
|
||||
v-model="element.df.label"
|
||||
/>
|
||||
<button
|
||||
class="remove-tab-btn btn btn-xs"
|
||||
:title="__('Remove tab')"
|
||||
@click.stop="remove_tab(element, $event)"
|
||||
:hidden="store.read_only"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('remove', 'xs')"></div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
|
@ -145,19 +155,10 @@ function delete_tab(with_children) {
|
|||
:title="__('Add new tab')"
|
||||
@click="add_new_tab"
|
||||
>
|
||||
<div v-if="has_tabs" v-html="frappe.utils.icon('add', 'sm')"></div>
|
||||
<div class="add-btn-text" v-else>
|
||||
{{ __("Add new tab") }}
|
||||
<div class="add-btn-text">
|
||||
{{ __("Add tab") }}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="has_tabs"
|
||||
class="remove-tab-btn btn btn-xs"
|
||||
:title="__('Remove selected tab')"
|
||||
@click="remove_tab"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -172,6 +173,7 @@ function delete_tab(with_children) {
|
|||
class="tab-content-container"
|
||||
v-model="tab.sections"
|
||||
group="sections"
|
||||
:delay="is_touch_screen_device() ? 200 : 0"
|
||||
:animation="200"
|
||||
:easing="store.get_animation"
|
||||
item-key="id"
|
||||
|
|
@ -186,8 +188,8 @@ function delete_tab(with_children) {
|
|||
</template>
|
||||
</draggable>
|
||||
<div class="empty-tab" :hidden="store.read_only">
|
||||
<div>{{ __("Drag & Drop a section here from another tab") }}</div>
|
||||
<div>{{ __("OR") }}</div>
|
||||
<div v-if="has_tabs">{{ __("Drag & Drop a section here from another tab") }}</div>
|
||||
<div v-if="has_tabs">{{ __("OR") }}</div>
|
||||
<button class="btn btn-default btn-sm" @click="add_new_section">
|
||||
{{ __("Add a new section") }}
|
||||
</button>
|
||||
|
|
@ -200,7 +202,7 @@ function delete_tab(with_children) {
|
|||
.tab-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 53px;
|
||||
min-height: 42px;
|
||||
align-items: center;
|
||||
background-color: var(--fg-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
|
@ -208,12 +210,6 @@ function delete_tab(with_children) {
|
|||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
|
||||
&:hover {
|
||||
.tab-actions .btn {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
|
@ -225,7 +221,7 @@ function delete_tab(with_children) {
|
|||
margin-right: 20px;
|
||||
|
||||
.btn {
|
||||
opacity: 0;
|
||||
background-color: var(--control-bg);
|
||||
padding: 2px;
|
||||
margin-left: 4px;
|
||||
box-shadow: none;
|
||||
|
|
@ -235,7 +231,7 @@ function delete_tab(with_children) {
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
background-color: var(--btn-default-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,8 +242,10 @@ function delete_tab(with_children) {
|
|||
}
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: var(--padding-md);
|
||||
padding: 10px 18px 10px 15px;
|
||||
color: var(--text-muted);
|
||||
min-width: max-content;
|
||||
cursor: pointer;
|
||||
|
|
@ -275,11 +273,22 @@ function delete_tab(with_children) {
|
|||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .remove-tab-btn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.remove-tab-btn {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
display: none;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-contents {
|
||||
max-height: calc(100vh - 110px);
|
||||
max-height: calc(100vh - 217px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border-radius: var(--border-radius);
|
||||
|
|
@ -290,13 +299,14 @@ function delete_tab(with_children) {
|
|||
position: relative;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tab-content-container {
|
||||
flex: 1;
|
||||
min-height: 4rem;
|
||||
background-color: var(--field-placeholder-color);
|
||||
border-radius: var(--border-radius);
|
||||
z-index: 1;
|
||||
|
||||
&:empty {
|
||||
height: 7rem;
|
||||
|
|
@ -306,14 +316,16 @@ function delete_tab(with_children) {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
|
||||
&button:hover {
|
||||
background-color: var(--border-color);
|
||||
button {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { create_layout, scrub_field_names, load_doctype_model } from "./utils";
|
||||
import {
|
||||
create_layout,
|
||||
scrub_field_names,
|
||||
load_doctype_model,
|
||||
section_boilerplate,
|
||||
} from "./utils";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import { useDebouncedRefHistory, onKeyDown } from "@vueuse/core";
|
||||
import { useDebouncedRefHistory, onKeyDown, useActiveElement } from "@vueuse/core";
|
||||
|
||||
export const useStore = defineStore("form-builder-store", () => {
|
||||
let doctype = ref("");
|
||||
|
|
@ -31,6 +36,15 @@ export const useStore = defineStore("form-builder-store", () => {
|
|||
return form.value.layout.tabs.find((tab) => tab.df.name == form.value.active_tab);
|
||||
});
|
||||
|
||||
const active_element = useActiveElement();
|
||||
const not_using_input = computed(
|
||||
() =>
|
||||
active_element.value?.readOnly ||
|
||||
active_element.value?.disabled ||
|
||||
(active_element.value?.tagName !== "INPUT" &&
|
||||
active_element.value?.tagName !== "TEXTAREA")
|
||||
);
|
||||
|
||||
// Actions
|
||||
function selected(name) {
|
||||
return form.value.selected_field?.name == name;
|
||||
|
|
@ -297,6 +311,31 @@ export const useStore = defineStore("form-builder-store", () => {
|
|||
return create_layout(doc.value.fields);
|
||||
}
|
||||
|
||||
// Tab actions
|
||||
function add_new_tab() {
|
||||
let tab = {
|
||||
df: get_df("Tab Break", "", "Tab " + (form.value.layout.tabs.length + 1)),
|
||||
sections: [section_boilerplate()],
|
||||
};
|
||||
|
||||
form.value.layout.tabs.push(tab);
|
||||
activate_tab(tab);
|
||||
}
|
||||
|
||||
function activate_tab(tab) {
|
||||
form.value.active_tab = tab.df.name;
|
||||
form.value.selected_field = tab.df;
|
||||
|
||||
// scroll to active tab
|
||||
nextTick(() => {
|
||||
$(".tabs .tab.active")[0]?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
inline: "center",
|
||||
block: "nearest",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
doctype,
|
||||
frm,
|
||||
|
|
@ -310,6 +349,7 @@ export const useStore = defineStore("form-builder-store", () => {
|
|||
get_animation,
|
||||
get_docfields,
|
||||
current_tab,
|
||||
not_using_input,
|
||||
selected,
|
||||
get_df,
|
||||
has_standard_field,
|
||||
|
|
@ -320,5 +360,7 @@ export const useStore = defineStore("form-builder-store", () => {
|
|||
get_updated_fields,
|
||||
is_df_updated,
|
||||
get_layout,
|
||||
add_new_tab,
|
||||
activate_tab,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -242,10 +242,6 @@ export function section_boilerplate() {
|
|||
df: store.get_df("Column Break"),
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
df: store.get_df("Column Break"),
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
|
@ -351,3 +347,7 @@ export function confirm_dialog(
|
|||
d.show();
|
||||
d.set_message(message);
|
||||
}
|
||||
|
||||
export function is_touch_screen_device() {
|
||||
return "ontouchstart" in document.documentElement;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2039,6 +2039,8 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.active_tab_map = {};
|
||||
}
|
||||
this.active_tab_map[this.docname] = tab;
|
||||
|
||||
this.script_manager.trigger("on_tab_change");
|
||||
}
|
||||
get_active_tab() {
|
||||
return this.active_tab_map && this.active_tab_map[this.docname];
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@
|
|||
},
|
||||
"homepage": "https://frappeframework.com",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.2",
|
||||
"@editorjs/editorjs": "^2.26.3",
|
||||
"@frappe/esbuild-plugin-postcss2": "^0.1.3",
|
||||
"@headlessui/vue": "^1.7.16",
|
||||
"@popperjs/core": "^2.11.2",
|
||||
"@redis/client": "^1.5.8",
|
||||
"@vue-flow/background": "^1.1.0",
|
||||
"@vue-flow/core": "^1.16.2",
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@
|
|||
stylus "^0.x"
|
||||
tmp "^0.2.1"
|
||||
|
||||
"@headlessui/vue@^1.7.16":
|
||||
version "1.7.16"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/vue/-/vue-1.7.16.tgz#bdc9d32d329248910325539b99e6abfce0c69f89"
|
||||
integrity sha512-nKT+nf/q6x198SsyK54mSszaQl/z+QxtASmgMEJtpxSX2Q0OPJX0upS/9daDyiECpeAsvjkoOrm2O/6PyBQ+Qg==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.13":
|
||||
version "1.4.15"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue