fix: add new field using + button on field
This commit is contained in:
parent
a28c8e9745
commit
bdbb11fbb9
3 changed files with 203 additions and 105 deletions
97
frappe/public/js/form_builder/components/Dropdown.vue
Normal file
97
frappe/public/js/form_builder/components/Dropdown.vue
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<div class="drop-down">
|
||||
<div class="search-box">
|
||||
<input
|
||||
ref="searchInput"
|
||||
class="search-input form-control"
|
||||
type="text"
|
||||
:placeholder="__('Search field...')"
|
||||
@input="(event) => $emit('update:modelValue', event.target.value)"
|
||||
:value="modelValue"
|
||||
@click.stop
|
||||
/>
|
||||
<span class="search-icon">
|
||||
<div v-html="frappe.utils.icon('search', 'sm')"></div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="drop-down-list">
|
||||
<button
|
||||
class="btn drop-down-item"
|
||||
v-for="(item, i) in items"
|
||||
:key="i"
|
||||
@click.stop="(i) => item.onClick(i)"
|
||||
>
|
||||
<slot>{{ item.label }}</slot>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const searchInput = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
searchInput.value.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drop-down {
|
||||
display: inline-block;
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: var(--shadow-base) !important;
|
||||
border-radius: var(--border-radius-sm);
|
||||
top: 30px;
|
||||
right: 0;
|
||||
width: 170px;
|
||||
z-index: 99999999;
|
||||
}
|
||||
|
||||
.drop-down-list {
|
||||
overflow-y: auto;
|
||||
max-height: 250px;
|
||||
text-align: left;
|
||||
padding: 0 6px 6px;
|
||||
}
|
||||
|
||||
.drop-down-item {
|
||||
font-size: small;
|
||||
text-align: left;
|
||||
border-radius: var(--border-radius-sm);
|
||||
margin: 1px 0px;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-light-gray);
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding: 6px;
|
||||
.search-input {
|
||||
padding-left: 30px;
|
||||
font-size: small;
|
||||
width: 100% !important;
|
||||
background-color: var(--control-bg) !important;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 13px;
|
||||
top: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,17 +1,75 @@
|
|||
<script setup>
|
||||
import EditableInput from "./EditableInput.vue";
|
||||
import { ref, computed } from "vue";
|
||||
import Dropdown from "./Dropdown.vue";
|
||||
import { useStore } from "../store";
|
||||
import { move_children_to_parent, clone_field } from "../utils";
|
||||
import { createPopper } from "@popperjs/core";
|
||||
import { ref, computed, watch } from "vue";
|
||||
|
||||
const props = defineProps(["column", "field"]);
|
||||
let store = useStore();
|
||||
const store = useStore();
|
||||
|
||||
let hovered = ref(false);
|
||||
let component = computed(() => {
|
||||
const hovered = ref(false);
|
||||
const component = computed(() => {
|
||||
return props.field.df.fieldtype.replace(" ", "") + "Control";
|
||||
});
|
||||
|
||||
const show_fieldtype_dropdown = ref(false);
|
||||
const search_text = ref("");
|
||||
const 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 = {
|
||||
label: df,
|
||||
onClick: () => {
|
||||
let new_field = {
|
||||
df: store.get_df(df),
|
||||
table_columns: [],
|
||||
};
|
||||
|
||||
add_new_field(clone_field(new_field));
|
||||
},
|
||||
};
|
||||
return out;
|
||||
});
|
||||
return [...fields];
|
||||
});
|
||||
|
||||
const add_field_btn_ref = ref(null);
|
||||
const dropdown_ref = ref(null);
|
||||
const popper = ref(null);
|
||||
|
||||
function setupPopper() {
|
||||
if (!popper.value) {
|
||||
popper.value = createPopper(add_field_btn_ref.value, dropdown_ref.value, {
|
||||
placement: "bottom-end",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 4],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
popper.value.update();
|
||||
}
|
||||
}
|
||||
|
||||
function remove_field() {
|
||||
if (store.is_customize_form && props.field.df.is_custom_field == 0) {
|
||||
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
|
||||
|
|
@ -23,8 +81,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,6 +111,27 @@ function duplicate_field() {
|
|||
props.column.fields.splice(index + 1, 0, duplicate_field);
|
||||
store.form.selected_field = duplicate_field.df;
|
||||
}
|
||||
|
||||
function add_new_field(field) {
|
||||
// insert new field after current field
|
||||
let index = props.column.fields.indexOf(props.field);
|
||||
props.column.fields.splice(index + 1, 0, field);
|
||||
store.form.selected_field = field.df;
|
||||
show_fieldtype_dropdown.value = false;
|
||||
hovered.value = false;
|
||||
}
|
||||
|
||||
function toggle_fieldtype_dropdown() {
|
||||
show_fieldtype_dropdown.value = !show_fieldtype_dropdown.value;
|
||||
search_text.value = "";
|
||||
setupPopper();
|
||||
}
|
||||
|
||||
watch(hovered, (val) => {
|
||||
if (val && store.form.selected_field && store.form.selected_field != props.field.df) {
|
||||
show_fieldtype_dropdown.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -60,7 +139,7 @@ function duplicate_field() {
|
|||
:class="[
|
||||
'field',
|
||||
hovered ? 'hovered' : '',
|
||||
store.selected(field.df.name) ? 'selected' : ''
|
||||
store.selected(field.df.name) ? 'selected' : '',
|
||||
]"
|
||||
:title="field.df.fieldname"
|
||||
@click.stop="store.form.selected_field = field.df"
|
||||
|
|
@ -82,17 +161,28 @@ function duplicate_field() {
|
|||
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"
|
||||
ref="add_field_btn_ref"
|
||||
class="add-field-btn btn btn-xs btn-icon"
|
||||
@click="toggle_fieldtype_dropdown"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('edit', 'sm')"></div>
|
||||
<div v-html="frappe.utils.icon('add', 'sm')" />
|
||||
<div class="drop-down" ref="dropdown_ref">
|
||||
<Dropdown
|
||||
v-if="show_fieldtype_dropdown"
|
||||
:items="fields"
|
||||
v-model="search_text"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="column.fields.indexOf(field)"
|
||||
|
|
@ -177,4 +267,8 @@ function duplicate_field() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drop-down {
|
||||
z-index: 99999;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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 - 250px);
|
||||
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>
|
||||
Loading…
Add table
Reference in a new issue