snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
This commit is contained in:
677
frontend/src/components/FieldLayoutEditor.vue
Normal file
677
frontend/src/components/FieldLayoutEditor.vue
Normal file
@@ -0,0 +1,677 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-5.5">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 text-base bg-surface-gray-2 rounded py-2 px-2.5 overflow-x-auto max-w-full"
|
||||
>
|
||||
<Draggable
|
||||
v-if="tabs.length && tabs[tabIndex].label"
|
||||
:list="tabs"
|
||||
item-key="name"
|
||||
class="flex items-center gap-2 w-full overflow-auto py-1 [&::-webkit-scrollbar]:h-0"
|
||||
@end="(e) => (tabIndex = e.newIndex)"
|
||||
>
|
||||
<template #item="{ element: tab, index: i }">
|
||||
<div
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) tabItemRefs[i] = el
|
||||
}
|
||||
"
|
||||
class="flex items-center gap-2 cursor-pointer rounded shrink-0"
|
||||
:class="[
|
||||
tabIndex == i
|
||||
? 'text-ink-gray-9 bg-surface-base shadow-sm'
|
||||
: 'text-ink-gray-5 hover:text-ink-gray-9 hover:bg-surface-base hover:shadow-sm',
|
||||
tab.editingLabel ? 'p-1' : 'px-2 py-1',
|
||||
]"
|
||||
@click="tabIndex = i"
|
||||
@dragenter.prevent="isDragging && (tabIndex = i)"
|
||||
>
|
||||
<div @dblclick="() => (tab.editingLabel = true)">
|
||||
<div v-if="!tab.editingLabel" class="flex items-center gap-2">
|
||||
{{ __(tab.label) || __('Untitled') }}
|
||||
</div>
|
||||
<div v-else class="flex gap-1 items-center">
|
||||
<Input
|
||||
v-model="tab.label"
|
||||
@keydown.enter="tab.editingLabel = false"
|
||||
@blur="tab.editingLabel = false"
|
||||
@click.stop
|
||||
/>
|
||||
<Button
|
||||
v-if="tab.editingLabel"
|
||||
icon="lucide-check"
|
||||
variant="ghost"
|
||||
@click="tab.editingLabel = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Dropdown
|
||||
v-if="tab.label && tabIndex == i"
|
||||
:options="getTabOptions(tab)"
|
||||
class="!h-4"
|
||||
@click.stop
|
||||
>
|
||||
<template #default>
|
||||
<Button variant="ghost" class="!p-1 !h-4">
|
||||
<span class="lucide-more-horizontal h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="!h-6.5 !text-ink-gray-5 hover:!text-ink-gray-9"
|
||||
:label="__('Add Tab')"
|
||||
@click="addTab"
|
||||
>
|
||||
<template #[slotName]>
|
||||
<span class="lucide-plus h-4" aria-hidden="true" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
v-for="(tab, i) in tabs"
|
||||
v-show="tabIndex == i"
|
||||
:key="tab.name"
|
||||
class="min-h-[34rem]"
|
||||
>
|
||||
<Draggable
|
||||
:list="tab.sections"
|
||||
item-key="name"
|
||||
:group="
|
||||
tab.sections.length === 0
|
||||
? { name: 'sections', put: ['sections', 'columns', 'fields'] }
|
||||
: 'sections'
|
||||
"
|
||||
handle=".section-drag-handle"
|
||||
:class="
|
||||
tab.sections.length === 0
|
||||
? 'rounded border-2 border-dashed border-outline-gray-2 mb-5.5 p-3'
|
||||
: 'flex flex-col gap-5.5'
|
||||
"
|
||||
@start="isDragging = true"
|
||||
@end="isDragging = false"
|
||||
@add="(evt) => onFieldDroppedToEmptyTab(tab)"
|
||||
>
|
||||
<template #item="{ element: section }">
|
||||
<div
|
||||
v-if="Array.isArray(section.columns)"
|
||||
class="section flex flex-col gap-1.5 p-2.5 bg-surface-gray-2 rounded"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="flex h-7 max-w-fit items-center gap-2 text-base-medium leading-4 text-ink-gray-9"
|
||||
>
|
||||
<DragVerticalIcon
|
||||
class="section-drag-handle h-3.5 cursor-grab shrink-0 text-ink-gray-3"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center gap-2 cursor-pointer"
|
||||
@dblclick="() => (section.editingLabel = true)"
|
||||
>
|
||||
<div
|
||||
v-if="!section.editingLabel"
|
||||
class="flex items-center gap-2"
|
||||
:class="{
|
||||
'text-ink-gray-3': section.hideLabel || !section.label,
|
||||
italic: !section.label,
|
||||
}"
|
||||
>
|
||||
{{ __(section.label) || __('No Label') }}
|
||||
<span
|
||||
v-if="section.collapsible"
|
||||
class="lucide-chevron-down h-4 transition-all duration-300 ease-in-out"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex gap-2 items-center">
|
||||
<Input
|
||||
v-model="section.label"
|
||||
@keydown.enter="section.editingLabel = false"
|
||||
@blur="section.editingLabel = false"
|
||||
@click.stop
|
||||
/>
|
||||
<Button
|
||||
v-if="section.editingLabel"
|
||||
icon="lucide-check"
|
||||
variant="ghost"
|
||||
@click="section.editingLabel = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
v-if="
|
||||
section.columns.reduce((n, c) => n + c.fields.length, 0) > 0
|
||||
"
|
||||
class="text-xs text-ink-gray-4 bg-surface-gray-3 rounded px-1.5 py-0.5 leading-none"
|
||||
>
|
||||
{{ section.columns.reduce((n, c) => n + c.fields.length, 0) }}
|
||||
{{
|
||||
section.columns.reduce((n, c) => n + c.fields.length, 0) ===
|
||||
1
|
||||
? __('field')
|
||||
: __('fields')
|
||||
}}
|
||||
</span>
|
||||
<Dropdown :options="getSectionOptions(i, section, tab)">
|
||||
<template #default>
|
||||
<Button variant="ghost">
|
||||
<span
|
||||
class="lucide-more-horizontal h-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<Draggable
|
||||
class="flex gap-2"
|
||||
:list="section.columns"
|
||||
group="columns"
|
||||
item-key="name"
|
||||
@start="isDragging = true"
|
||||
@end="isDragging = false"
|
||||
>
|
||||
<template #item="{ element: column }">
|
||||
<div
|
||||
class="flex flex-col gap-1.5 flex-1 p-2 border border-dashed border-outline-gray-2 rounded bg-surface-elevation-2 cursor-grab"
|
||||
>
|
||||
<Draggable
|
||||
:list="column.fields"
|
||||
group="fields"
|
||||
item-key="fieldname"
|
||||
class="flex flex-col gap-1.5 flex-1 min-h-8.5"
|
||||
handle=".field-drag-handle"
|
||||
@start="isDragging = true"
|
||||
@end="isDragging = false"
|
||||
>
|
||||
<template #item="{ element: field }">
|
||||
<div
|
||||
class="field px-2.5 py-2 border border-outline-gray-2 rounded text-base bg-surface-elevation-2 text-ink-gray-8 flex items-center leading-4 justify-between gap-2 cursor-auto"
|
||||
>
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<DragVerticalIcon
|
||||
class="field-drag-handle h-3.5 cursor-grab"
|
||||
/>
|
||||
<div class="truncate">{{ field.label }}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="!size-4 rounded-sm"
|
||||
icon="lucide-x"
|
||||
@click="
|
||||
column.fields.splice(
|
||||
column.fields.indexOf(field),
|
||||
1,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<Combobox
|
||||
:model-value="null"
|
||||
:options="fields"
|
||||
@update:selected-option="(e) => addField(column, e)"
|
||||
>
|
||||
<template #trigger="{ open, setOpen }">
|
||||
<Button
|
||||
class="w-full !h-8 !bg-surface-elevation-2"
|
||||
variant="outline"
|
||||
:label="__('Add Field')"
|
||||
iconLeft="plus"
|
||||
@click="setOpen(!open)"
|
||||
/>
|
||||
</template>
|
||||
<template #item-label="{ item }">
|
||||
<div class="flex flex-col gap-1 text-ink-gray-9">
|
||||
<div>{{ item.label }}</div>
|
||||
<div class="text-ink-gray-4 text-sm">
|
||||
{{ `${item.fieldname} - ${item.fieldtype}` }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Combobox>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div
|
||||
v-if="tab.sections.length === 0"
|
||||
class="flex items-center justify-center min-h-20 text-sm text-ink-gray-4 pointer-events-none select-none"
|
||||
>
|
||||
{{ __('Drag a section or a field here to get started') }}
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
<div class="mt-5.5">
|
||||
<Button
|
||||
class="w-full h-8"
|
||||
variant="subtle"
|
||||
:label="__('Add Section')"
|
||||
iconLeft="plus"
|
||||
@click="
|
||||
tabs[tabIndex].sections.push({
|
||||
label: '',
|
||||
name: 'section_' + getRandom(),
|
||||
opened: true,
|
||||
columns: [{ name: 'column_' + getRandom(), fields: [] }],
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import DragVerticalIcon from '@/components/Icons/DragVerticalIcon.vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { getRandom } from '@/utils'
|
||||
import { getMeta } from '@/stores/meta'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { Combobox, Dropdown } from 'frappe-ui'
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
doctype: { type: String, default: 'CRM Lead' },
|
||||
onlyRequired: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const tabs = defineModel({ type: Array, default: () => [] })
|
||||
|
||||
const { $dialog } = globalStore()
|
||||
|
||||
const tabIndex = ref(0)
|
||||
const isDragging = ref(false)
|
||||
const tabItemRefs = ref([])
|
||||
|
||||
watch(tabIndex, (idx) => {
|
||||
nextTick(() => {
|
||||
tabItemRefs.value[idx]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: 'nearest',
|
||||
block: 'nearest',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const slotName = computed(() => {
|
||||
if (tabs.value.length == 1 && !tabs.value[0].label) {
|
||||
return 'prefix'
|
||||
}
|
||||
return 'default'
|
||||
})
|
||||
|
||||
const restrictedFieldTypes = [
|
||||
'Tab Break',
|
||||
'Section Break',
|
||||
'Column Break',
|
||||
'Signature',
|
||||
]
|
||||
|
||||
const { getFields } = getMeta(props.doctype)
|
||||
|
||||
const fields = computed(() => {
|
||||
const _fields =
|
||||
getFields({ restrictNoValueFields: false, restrictedFieldTypes }) || []
|
||||
if (!_fields.length) return []
|
||||
|
||||
let existingFields = []
|
||||
|
||||
tabs.value?.forEach((tab) => {
|
||||
tab.sections?.forEach((section) => {
|
||||
section.columns?.forEach((column) => {
|
||||
existingFields = existingFields.concat(column.fields)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return _fields
|
||||
.filter((field) => {
|
||||
return (
|
||||
!existingFields.find((f) => f.fieldname === field.fieldname) &&
|
||||
(props.onlyRequired ? field.reqd : true)
|
||||
)
|
||||
})
|
||||
.map((field) => {
|
||||
return {
|
||||
label: field.label,
|
||||
value: field.fieldname,
|
||||
fieldname: field.fieldname,
|
||||
fieldtype: field.fieldtype,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function addTab() {
|
||||
if (tabs.value.length == 1 && !tabs.value[0].label) {
|
||||
tabs.value[0].label = __('New Tab')
|
||||
tabIndex.value = 0
|
||||
nextTick(() => {
|
||||
tabItemRefs.value[0]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: 'nearest',
|
||||
block: 'nearest',
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tabs.value.push({
|
||||
label: __('New Tab'),
|
||||
name: 'tab_' + getRandom(),
|
||||
sections: [],
|
||||
})
|
||||
tabIndex.value = tabs.value.length ? tabs.value.length - 1 : 0
|
||||
nextTick(() => {
|
||||
tabItemRefs.value[tabIndex.value]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: 'nearest',
|
||||
block: 'nearest',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function addField(column, field) {
|
||||
if (!field) return
|
||||
column.fields.push(field)
|
||||
}
|
||||
|
||||
function onFieldDroppedToEmptyTab(tab) {
|
||||
const droppedItem = tab.sections[0]
|
||||
if (!droppedItem) return
|
||||
|
||||
let columns = []
|
||||
|
||||
if (Object.hasOwn(droppedItem, 'fields')) {
|
||||
columns = [droppedItem]
|
||||
} else if (Object.hasOwn(droppedItem, 'fieldname')) {
|
||||
columns = [{ name: 'column_' + getRandom(), fields: [droppedItem] }]
|
||||
}
|
||||
|
||||
if (!columns.length) return
|
||||
|
||||
tab.sections.splice(0, 1)
|
||||
tab.sections.push({
|
||||
label: '',
|
||||
name: 'section_' + getRandom(),
|
||||
opened: true,
|
||||
columns,
|
||||
})
|
||||
}
|
||||
|
||||
function getTabOptions(tab) {
|
||||
return [
|
||||
{
|
||||
label: __('Edit'),
|
||||
icon: 'edit',
|
||||
onClick: () => (tab.editingLabel = true),
|
||||
},
|
||||
{
|
||||
label: __('Remove Tab'),
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
if (tabs.value.length == 1) {
|
||||
tabs.value[0].label = ''
|
||||
return
|
||||
}
|
||||
$dialog({
|
||||
title: __('Remove Tab'),
|
||||
message: __(
|
||||
'Are you sure you want to remove this tab and all its content?',
|
||||
),
|
||||
variant: 'danger',
|
||||
actions: [
|
||||
{
|
||||
label: __('Remove'),
|
||||
variant: 'solid',
|
||||
theme: 'red',
|
||||
onClick: (close) => {
|
||||
tabs.value.splice(tabIndex.value, 1)
|
||||
tabIndex.value = tabIndex.value ? tabIndex.value - 1 : 0
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function getSectionOptions(i, section, tab) {
|
||||
let column = section.columns[section.columns.length - 1]
|
||||
return [
|
||||
{
|
||||
group: __('Section'),
|
||||
items: [
|
||||
{
|
||||
label: __('Edit'),
|
||||
icon: 'edit',
|
||||
onClick: () => (section.editingLabel = true),
|
||||
},
|
||||
{
|
||||
label: section.collapsible ? __('Uncollapsible') : __('Collapsible'),
|
||||
icon: section.collapsible ? 'chevron-up' : 'chevron-down',
|
||||
onClick: () => (section.collapsible = !section.collapsible),
|
||||
},
|
||||
{
|
||||
label: section.hideLabel ? __('Show Label') : __('Hide Label'),
|
||||
icon: section.hideLabel ? 'eye' : 'eye-off',
|
||||
onClick: () => (section.hideLabel = !section.hideLabel),
|
||||
},
|
||||
{
|
||||
label: section.hideBorder ? __('Show Border') : __('Hide Border'),
|
||||
icon: 'minus',
|
||||
onClick: () => (section.hideBorder = !section.hideBorder),
|
||||
},
|
||||
{
|
||||
label: __('Remove Section'),
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
const hasFields = section.columns.some((c) => c.fields.length)
|
||||
const doRemove = () =>
|
||||
tab.sections.splice(tab.sections.indexOf(section), 1)
|
||||
if (hasFields) {
|
||||
$dialog({
|
||||
title: __('Remove Section'),
|
||||
message: __(
|
||||
'This section contains fields. Are you sure you want to remove it?',
|
||||
),
|
||||
variant: 'danger',
|
||||
actions: [
|
||||
{
|
||||
label: __('Remove'),
|
||||
variant: 'solid',
|
||||
theme: 'red',
|
||||
onClick: (close) => {
|
||||
doRemove()
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
} else {
|
||||
doRemove()
|
||||
}
|
||||
},
|
||||
condition: () => section.editable !== false,
|
||||
},
|
||||
{
|
||||
label: __('Remove and move columns to {0} section', [
|
||||
i == 0 ? __('next') : __('previous'),
|
||||
]),
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
let targetSection = tab.sections[i == 0 ? i + 1 : i - 1]
|
||||
if (i == 0) {
|
||||
targetSection.columns = section.columns.concat(
|
||||
targetSection.columns,
|
||||
)
|
||||
} else {
|
||||
targetSection.columns = targetSection.columns.concat(
|
||||
section.columns,
|
||||
)
|
||||
}
|
||||
tab.sections.splice(tab.sections.indexOf(section), 1)
|
||||
},
|
||||
condition: () => section.editable !== false && section.columns.length,
|
||||
},
|
||||
{
|
||||
label: __('Move to Previous Tab'),
|
||||
icon: 'corner-up-left',
|
||||
onClick: () => {
|
||||
let previousTab = tabs.value[tabIndex.value - 1]
|
||||
previousTab.sections.push(section)
|
||||
tabs.value[tabIndex.value].sections.splice(
|
||||
tabs.value[tabIndex.value].sections.indexOf(section),
|
||||
1,
|
||||
)
|
||||
tabIndex.value -= 1
|
||||
},
|
||||
condition: () => tabs.value[tabIndex.value - 1],
|
||||
},
|
||||
{
|
||||
label: __('Move to Next Tab'),
|
||||
icon: 'corner-up-right',
|
||||
onClick: () => {
|
||||
let nextTab = tabs.value[tabIndex.value + 1]
|
||||
nextTab.sections.push(section)
|
||||
tabs.value[tabIndex.value].sections.splice(
|
||||
tabs.value[tabIndex.value].sections.indexOf(section),
|
||||
1,
|
||||
)
|
||||
tabIndex.value += 1
|
||||
},
|
||||
condition: () => tabs.value[tabIndex.value + 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: __('Column'),
|
||||
items: [
|
||||
{
|
||||
label: __('Add Column'),
|
||||
icon: 'columns',
|
||||
onClick: () => {
|
||||
section.columns.push({
|
||||
label: '',
|
||||
name: 'column_' + getRandom(),
|
||||
fields: [],
|
||||
})
|
||||
},
|
||||
condition: () => section.columns.length < 4,
|
||||
},
|
||||
{
|
||||
label: __('Remove Last Column'),
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
const doRemove = () => section.columns.pop()
|
||||
if (column.fields.length) {
|
||||
$dialog({
|
||||
title: __('Remove Last Column'),
|
||||
message: __(
|
||||
'This column contains fields. Are you sure you want to remove it?',
|
||||
),
|
||||
variant: 'danger',
|
||||
actions: [
|
||||
{
|
||||
label: __('Remove'),
|
||||
variant: 'solid',
|
||||
theme: 'red',
|
||||
onClick: (close) => {
|
||||
doRemove()
|
||||
close()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
} else {
|
||||
doRemove()
|
||||
}
|
||||
},
|
||||
condition: () => section.columns.length > 1,
|
||||
},
|
||||
{
|
||||
label: __('Remove Last Column (move fields to previous)'),
|
||||
icon: 'trash-2',
|
||||
onClick: () => {
|
||||
let previousColumn = section.columns[section.columns.length - 2]
|
||||
previousColumn.fields = previousColumn.fields.concat(column.fields)
|
||||
section.columns.pop()
|
||||
},
|
||||
condition: () => section.columns.length > 1 && column.fields.length,
|
||||
},
|
||||
{
|
||||
label: __('Move Last Column to Next Section'),
|
||||
icon: 'corner-up-right',
|
||||
onClick: () => {
|
||||
let nextSection = tab.sections[i + 1]
|
||||
nextSection.columns.push(column)
|
||||
section.columns.pop()
|
||||
},
|
||||
condition: () => tab.sections[i + 1],
|
||||
},
|
||||
{
|
||||
label: __('Move Last Column to Previous Section'),
|
||||
icon: 'corner-up-left',
|
||||
onClick: () => {
|
||||
let previousSection = tab.sections[i - 1]
|
||||
previousSection.columns.push(column)
|
||||
section.columns.pop()
|
||||
},
|
||||
condition: () => tab.sections[i - 1],
|
||||
},
|
||||
{
|
||||
label: __('Move Last Column to Previous Tab'),
|
||||
icon: 'corner-up-left',
|
||||
onClick: () => {
|
||||
let targetTab = tabs.value[tabIndex.value - 1]
|
||||
if (!targetTab.sections.length) {
|
||||
targetTab.sections.push({
|
||||
label: '',
|
||||
name: 'section_' + getRandom(),
|
||||
opened: true,
|
||||
columns: [],
|
||||
})
|
||||
}
|
||||
targetTab.sections[targetTab.sections.length - 1].columns.push(
|
||||
column,
|
||||
)
|
||||
section.columns.pop()
|
||||
},
|
||||
condition: () => tabs.value[tabIndex.value - 1],
|
||||
},
|
||||
{
|
||||
label: __('Move Last Column to Next Tab'),
|
||||
icon: 'corner-up-right',
|
||||
onClick: () => {
|
||||
let targetTab = tabs.value[tabIndex.value + 1]
|
||||
if (!targetTab.sections.length) {
|
||||
targetTab.sections.push({
|
||||
label: '',
|
||||
name: 'section_' + getRandom(),
|
||||
opened: true,
|
||||
columns: [],
|
||||
})
|
||||
}
|
||||
targetTab.sections[targetTab.sections.length - 1].columns.push(
|
||||
column,
|
||||
)
|
||||
section.columns.pop()
|
||||
},
|
||||
condition: () => tabs.value[tabIndex.value + 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user