seitime-frappe/frappe/public/js/form_builder/components/Section.vue
Fisher Yu ea61af2ceb
[fix] Add missing translate function call (#25747)
* Update Properties.vue

* Update store.js

* Update Section.vue

* Update list_settings.js

* Update grid_row.js

* Update list_settings.js

* chore: add in missing context for some strings

Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com>

* chore: formatting and lint fixes

Signed-off-by: Akhil Narang <me@akhilnarang.dev>

---------

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
Co-authored-by: Akhil Narang <me@akhilnarang.dev>
Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com>
2024-04-12 14:39:02 +05:30

390 lines
9.6 KiB
Vue

<template>
<div
class="form-section-container"
:style="{ borderBottom: props.section.df.hide_border ? 'none' : '' }"
>
<div
:class="[
'form-section',
hovered ? 'hovered' : '',
store.selected(section.df.name) ? 'selected' : '',
]"
:title="section.df.fieldname"
@click.stop="select_section"
@mouseover.stop="hovered = true"
@mouseout.stop="hovered = false"
>
<div
:class="[
'section-header',
section.df.label || section.df.collapsible ? 'has-label' : '',
collapsed ? 'collapsed' : '',
]"
:hidden="!section.df.label && store.read_only"
>
<div class="section-label">
<EditableInput
:text="section.df.label"
:placeholder="__('Section Title')"
v-model="section.df.label"
/>
<div
v-if="section.df.collapsible"
class="collapse-indicator"
v-html="frappe.utils.icon(collapsed ? 'down' : 'up-line', 'sm')"
></div>
</div>
<Dropdown v-if="!store.read_only" :options="options" @click.stop />
</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,
}"
>
<draggable
class="section-columns-container"
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"
>
<template #item="{ element }">
<Column
:section="section"
:column="element"
:data-is-user-generated="store.is_user_generated_field(element)"
/>
</template>
</draggable>
</div>
</div>
</div>
</template>
<script setup>
import draggable from "vuedraggable";
import Column from "./Column.vue";
import EditableInput from "./EditableInput.vue";
import Dropdown from "./Dropdown.vue";
import { ref, computed } from "vue";
import { useStore } from "../store";
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"]);
const store = useStore();
// 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));
const column = computed(() => props.section.columns[props.section.columns.length - 1]);
// section
function add_section_below() {
let index = props.tab.sections.indexOf(props.section);
props.tab.sections.splice(index + 1, 0, section_boilerplate());
}
function is_section_empty() {
return !props.section.columns.some(
(column) => (store.is_customize_form && !column.df.is_custom_field) || column.fields.length
);
}
function remove_section() {
if (store.is_customize_form && props.section.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(props.section)) {
delete_section();
} else if (is_section_empty()) {
delete_section(true);
} 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"
),
() => delete_section(),
__("Delete section", null, "Button text"),
() => delete_section(true),
__("Delete entire section with fields", null, "Button text")
);
}
}
function delete_section(with_children) {
let sections = props.tab.sections;
let index = sections.indexOf(props.section);
if (!with_children) {
if (index > 0) {
let prev_section = sections[index - 1];
if (!is_section_empty()) {
// move all columns from current section to previous section
prev_section.columns = [...prev_section.columns, ...props.section.columns];
}
} else if (index == 0 && !is_section_empty()) {
// create a new section and push columns to it
sections.unshift({
df: store.get_df("Section Break"),
columns: props.section.columns,
is_first: true,
});
index++;
}
}
// remove section
sections.splice(index, 1);
store.form.selected_field = null;
}
function select_section() {
if (props.section.df.collapsible) {
collapsed.value = !collapsed.value;
}
store.form.selected_field = props.section.df;
}
function move_sections_to_tab() {
let new_tab = move_children_to_parent(props, "tab", "section", store.form.layout);
// activate tab
store.form.active_tab = new_tab;
}
// column
function add_column() {
props.section.columns.push({
fields: [],
df: store.get_df("Column Break"),
});
}
function remove_column() {
if (store.is_customize_form && column.value.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 (column.value.fields.length == 0 || store.has_standard_field(column.value)) {
delete_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"
),
() => delete_column(),
__("Delete column", null, "Button text"),
() => delete_column(true),
__("Delete entire column with fields", null, "Button text")
);
}
}
function delete_column(with_children) {
// move all fields to previous column
let columns = props.section.columns;
let index = columns.length - 1;
if (with_children && index == 0 && columns.length == 1) {
if (column.value.fields.length == 0) {
frappe.msgprint(__("Section must have at least one column"));
throw "section must have at least one column";
}
columns.unshift({
df: store.get_df("Column Break"),
fields: [],
is_first: true,
});
index++;
}
if (!with_children) {
if (index > 0) {
let prev_column = columns[index - 1];
prev_column.fields = [...prev_column.fields, ...column.value.fields];
} else {
if (column.value.fields.length == 0) {
// set next column as first column
let next_column = columns[index + 1];
if (next_column) {
next_column.is_first = true;
} else {
frappe.msgprint(__("Section must have at least one column"));
throw "section must have at least one column";
}
} else {
// create a new column if current column has fields and push fields to it
columns.unshift({
df: store.get_df("Column Break"),
fields: column.value.fields,
is_first: true,
});
index++;
}
}
}
// remove column
columns.splice(index, 1);
store.form.selected_field = null;
}
const options = computed(() => {
let groups = [
{
group: __("Section"),
items: [
{ label: __("Add section below"), onClick: add_section_below },
{ label: __("Remove section"), onClick: remove_section },
],
},
{
group: __("Column"),
items: [{ label: __("Add column"), onClick: add_column }],
},
];
// add remove column option if there are more than one columns
if (props.section.columns.length > 1) {
groups[1].items.push({
label: __("Remove column"),
tooltip: __("Remove last column"),
onClick: remove_column,
});
} else if (props.section.columns[0].fields.length) {
// add remove all fields option if there is only one column and it has fields
groups[1].items.push({
label: __("Empty column"),
tooltip: __("Remove all fields in the column"),
onClick: () => delete_column(true),
});
}
// add move to tab option if the current section is not the first section
if (props.tab.sections.indexOf(props.section) > 0) {
groups[0].items.push({
label: __("Move sections to new tab"),
tooltip: __("Move current and all subsequent sections to a new tab"),
onClick: move_sections_to_tab,
});
}
return groups;
});
</script>
<style lang="scss" scoped>
.form-section-container {
border-bottom: 1px solid var(--border-color);
background-color: var(--fg-color);
&:last-child {
border-bottom: none;
}
.form-section {
background-color: inherit;
border: 1px solid transparent;
border-radius: var(--border-radius);
padding: 1rem;
cursor: pointer;
&:last-child {
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
&.hovered,
&.selected {
border-color: var(--border-primary);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.75rem;
&.collapsed {
padding-bottom: 0;
}
&.has-label {
display: flex;
}
.section-label {
display: flex;
:deep(span) {
font-weight: 600;
color: var(--heading-color);
}
.collapse-indicator {
margin-left: 7px;
}
}
.section-actions {
display: flex;
gap: 4px;
align-items: center;
}
// .btn-section {
// padding: var(--padding-xs);
// box-shadow: none;
// &:hover {
// background-color: var(--bg-light-gray);
// }
// }
.btn-section {
display: inline-flex;
gap: 2px;
}
}
.section-description {
margin-bottom: 10px;
font-size: var(--text-xs);
color: var(--text-muted);
}
.section-columns-container {
display: flex;
min-height: 2rem;
border-radius: var(--border-radius);
}
}
}
</style>