fix: add new field using + button on field

This commit is contained in:
Shariq Ansari 2023-10-30 21:48:12 +05:30
parent a28c8e9745
commit bdbb11fbb9
3 changed files with 203 additions and 105 deletions

View 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>

View file

@ -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>

View file

@ -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>