ListFormEdit ekranında sütunların sıralaması
This commit is contained in:
parent
7be0b4ed88
commit
d331fb62d4
4 changed files with 369 additions and 118 deletions
|
|
@ -74903,6 +74903,20 @@
|
|||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.ExpectedKm",
|
||||
"Value": "Olması Gereken Km"
|
||||
},
|
||||
{
|
||||
"Id": "a4130afd-d823-57b0-91e8-c0c11369801d",
|
||||
"CultureName": "en",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.ListFormField.DragToReorder",
|
||||
"Value": "Drag to reorder"
|
||||
},
|
||||
{
|
||||
"Id": "59388a6c-8dcf-583e-b706-896644410dfe",
|
||||
"CultureName": "tr",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.ListFormField.DragToReorder",
|
||||
"Value": "Sürükleyerek sırala"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { AdaptableCard, Container } from '@/components/shared'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
|
|
@ -8,12 +7,12 @@ import {
|
|||
FormItem,
|
||||
Input,
|
||||
Notification,
|
||||
Segment,
|
||||
Table,
|
||||
toast,
|
||||
} from '@/components/ui'
|
||||
import TBody from '@/components/ui/Table/TBody'
|
||||
import THead from '@/components/ui/Table/THead'
|
||||
import Td from '@/components/ui/Table/Td'
|
||||
import Th from '@/components/ui/Table/Th'
|
||||
import Tr from '@/components/ui/Table/Tr'
|
||||
import {
|
||||
|
|
@ -26,6 +25,7 @@ import {
|
|||
import {
|
||||
ColumnFormatEditDto,
|
||||
CopyListFormFieldColumnDto,
|
||||
ListFormFieldEditTabs,
|
||||
} from '@/proxy/admin/list-form-field/models'
|
||||
import { SelectBoxOption } from '@/types/shared'
|
||||
import { useStoreState } from '@/store'
|
||||
|
|
@ -33,8 +33,24 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
|||
import setNull from '@/utils/setNull'
|
||||
import classNames from 'classnames'
|
||||
import { Field, Form, Formik, FormikHelpers } from 'formik'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { FaFileMedical, FaCopy, FaEyeSlash, FaMinus, FaTimes, FaTable } from 'react-icons/fa'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { FaFileMedical, FaCopy, FaTable } from 'react-icons/fa'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import SortableFieldRow from './SortableFieldRow'
|
||||
import { FieldOrderMode, reorderFields, sortFields } from './fieldOrder'
|
||||
import { number, object, string } from 'yup'
|
||||
import FormFieldEdit from './FormFieldEdit'
|
||||
import { dbSourceTypeOptions, sqlDataTypeToDbType } from '../options'
|
||||
|
|
@ -85,7 +101,7 @@ function FormFields({
|
|||
columns: DatabaseColumnDto[]
|
||||
}) {
|
||||
const { translate } = useLocalization()
|
||||
const { canCreate, canDelete } = useFieldPermission()
|
||||
const { canCreate, canUpdate, canDelete } = useFieldPermission()
|
||||
const [fields, setFields] = useState<ColumnFormatEditDto[]>()
|
||||
const [deleteRowId, setDeleteRowId] = useState<string | null>(null)
|
||||
const [row, setRow] = useState<ColumnFormatEditDto | null>(null)
|
||||
|
|
@ -93,6 +109,14 @@ function FormFields({
|
|||
const [isCopyField, setIsCopyField] = useState(false)
|
||||
const [fieldList, setFieldList] = useState<SelectBoxOption[]>([])
|
||||
const cultureName = useStoreState((state) => state.locale.currentLang)
|
||||
const [orderMode, setOrderMode] = useState<FieldOrderMode>('list')
|
||||
const [isSavingOrder, setIsSavingOrder] = useState(false)
|
||||
// Siralama kaydedildikten sonra liste kullanicinin son uyguladigi filtreyle yeniden okunur.
|
||||
const filterRef = useRef<{ cultureName?: string; roleId?: string; userId?: string }>({})
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
)
|
||||
|
||||
// ── All DB Columns (for "Tüm Sütunları Ekle") ───────────────────────────────
|
||||
const listFormValues = useStoreState((s) => s.admin.lists.values)
|
||||
|
|
@ -308,10 +332,50 @@ function FormFields({
|
|||
}
|
||||
}
|
||||
|
||||
const onDragEnd = async ({ active, over }: DragEndEvent) => {
|
||||
if (!fields || !over) {
|
||||
return
|
||||
}
|
||||
|
||||
const changed = reorderFields(fields, orderMode, String(active.id), String(over.id))
|
||||
if (changed.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setFields(fields.map((f) => changed.find((c) => c.id === f.id) ?? f))
|
||||
setIsSavingOrder(true)
|
||||
try {
|
||||
// Her kayit wizard seed dosyasini yeniden yazar; paralel istek ayni dosyada yarisirdi.
|
||||
for (const field of changed) {
|
||||
await putListFormField({ ...field, editType: ListFormFieldEditTabs.OptionsForm })
|
||||
}
|
||||
toast.push(
|
||||
<Notification type="success" duration={2000}>
|
||||
{translate('::App.Platform.FormBilgileriKaydedildi')}
|
||||
</Notification>,
|
||||
{ placement: 'bottom-end' },
|
||||
)
|
||||
} catch (error: any) {
|
||||
toast.push(
|
||||
<Notification type="danger">
|
||||
{translate('::App.Platform.Error')}
|
||||
<code>{error}</code>
|
||||
</Notification>,
|
||||
{ placement: 'bottom-end' },
|
||||
)
|
||||
} finally {
|
||||
const { cultureName, roleId, userId } = filterRef.current
|
||||
await getFields(cultureName, roleId, userId)
|
||||
setIsSavingOrder(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!fields) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const orderedFields = sortFields(fields, orderMode)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Formik
|
||||
|
|
@ -333,6 +397,7 @@ function FormFields({
|
|||
}}
|
||||
onSubmit={async ({ cultureName, roleId, userId }, { setSubmitting }) => {
|
||||
setSubmitting(true)
|
||||
filterRef.current = { cultureName, roleId, userId }
|
||||
getFields(cultureName, roleId, userId).then(() => {
|
||||
toast.push(
|
||||
<Notification type="success" duration={100}>
|
||||
|
|
@ -384,6 +449,35 @@ function FormFields({
|
|||
<AdaptableCard className="h-full" bodyClass="h-full">
|
||||
<div className="grid 2xl:grid-cols-5 xl:grid-cols-3 lg:grid-cols-5 gap-4 h-full border-t-2">
|
||||
<div className="2xl:col-span-2 xl:col-span-1 lg:col-span-2 border-r-2">
|
||||
<div className="flex justify-end px-2 py-2">
|
||||
<Segment
|
||||
size="xs"
|
||||
value={[orderMode]}
|
||||
onChange={(value) => {
|
||||
// Tekli Segment secili ogeye tekrar tiklaninca bos deger gonderir.
|
||||
const next = Array.isArray(value) ? value[0] : value
|
||||
if (next === 'list' || next === 'edit') {
|
||||
setOrderMode(next)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Segment.Item value="list">
|
||||
{translate('::App.ListFormFieldEdit.OptionsListOrderNo')}
|
||||
</Segment.Item>
|
||||
<Segment.Item value="edit">
|
||||
{translate('::App.ListFormFieldEdit.EditFormOrderNo')}
|
||||
</Segment.Item>
|
||||
</Segment>
|
||||
</div>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={orderedFields.map((f) => f.id!)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<Table compact>
|
||||
<THead>
|
||||
<Tr>
|
||||
|
|
@ -438,66 +532,36 @@ function FormFields({
|
|||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{fields.map((field) => (
|
||||
<Tr
|
||||
{orderedFields.map((field) => (
|
||||
<SortableFieldRow
|
||||
key={field.id}
|
||||
className={classNames(
|
||||
'cursor-pointer',
|
||||
field.id === row?.id ? 'bg-sozsoft-active dark:bg-gray-600' : '',
|
||||
)}
|
||||
onClick={() => {
|
||||
setRow(field)
|
||||
}}
|
||||
>
|
||||
<Td className="flex items-center !py-1 !px-1">
|
||||
{canDelete && (
|
||||
<Button
|
||||
shape="circle"
|
||||
variant="plain"
|
||||
type="button"
|
||||
size="sm"
|
||||
title="Delete"
|
||||
icon={<FaMinus />}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault()
|
||||
if (field.id) {
|
||||
setDeleteRowId(field.id)
|
||||
field={field}
|
||||
isSelected={field.id === row?.id}
|
||||
canDelete={canDelete}
|
||||
dragDisabled={
|
||||
!canUpdate ||
|
||||
isSavingOrder ||
|
||||
(orderMode === 'edit' && field.editOrderNo == null)
|
||||
}
|
||||
}}
|
||||
dragTitle={translate('::App.ListFormField.DragToReorder')}
|
||||
roleLabel={
|
||||
field.roleId
|
||||
? (roleList.find((a) => a.value === field.roleId)?.label ?? 'X')
|
||||
: undefined
|
||||
}
|
||||
userLabel={
|
||||
field.userId
|
||||
? (userList.find((a) => a.value === field.userId)?.label ?? 'X')
|
||||
: undefined
|
||||
}
|
||||
onSelect={() => setRow(field)}
|
||||
onDelete={() => field.id && setDeleteRowId(field.id)}
|
||||
/>
|
||||
)}
|
||||
<span style={{ visibility: !field?.visible ? 'visible' : 'hidden' }}>
|
||||
<FaEyeSlash />
|
||||
</span>
|
||||
<span style={{ visibility: !field?.isActive ? 'visible' : 'hidden' }}>
|
||||
<FaTimes />
|
||||
</span>
|
||||
<span className="ml-5">
|
||||
{field.listOrderNo}
|
||||
{field.editOrderNo ? ` (${field.editOrderNo})` : ''}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="!py-1 !px-1">{field.fieldName}</Td>
|
||||
<Td className="align-middle text-center !py-1 !px-1">
|
||||
<Avatar
|
||||
size={18}
|
||||
shape="circle"
|
||||
src={`/img/countries/${field.cultureName ?? 'default'}.png`}
|
||||
title={field.cultureName}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="hidden sm:table-cell !py-1 !px-1">
|
||||
{field.roleId &&
|
||||
(roleList.find((a) => a.value === field.roleId)?.label ?? 'X')}
|
||||
</Td>
|
||||
<Td className="hidden sm:table-cell !py-1 !px-1">
|
||||
{field.userId &&
|
||||
(userList.find((a) => a.value === field.userId)?.label ?? 'X')}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
<div className="2xl:col-span-3 lg:col-span-3 xl:col-span-2">
|
||||
<FormFieldEdit
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
import { Avatar, Button } from '@/components/ui'
|
||||
import Td from '@/components/ui/Table/Td'
|
||||
import Tr from '@/components/ui/Table/Tr'
|
||||
import { ColumnFormatEditDto } from '@/proxy/admin/list-form-field/models'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import classNames from 'classnames'
|
||||
import { FaEyeSlash, FaGripVertical, FaMinus, FaTimes } from 'react-icons/fa'
|
||||
|
||||
function SortableFieldRow({
|
||||
field,
|
||||
isSelected,
|
||||
canDelete,
|
||||
dragDisabled,
|
||||
dragTitle,
|
||||
roleLabel,
|
||||
userLabel,
|
||||
onSelect,
|
||||
onDelete,
|
||||
}: {
|
||||
field: ColumnFormatEditDto
|
||||
isSelected: boolean
|
||||
canDelete: boolean
|
||||
dragDisabled: boolean
|
||||
dragTitle: string
|
||||
roleLabel?: string
|
||||
userLabel?: string
|
||||
onSelect: () => void
|
||||
onDelete: () => void
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: field.id!,
|
||||
disabled: dragDisabled,
|
||||
})
|
||||
|
||||
return (
|
||||
<Tr
|
||||
ref={setNodeRef}
|
||||
style={{ transform: CSS.Translate.toString(transform), transition }}
|
||||
className={classNames(
|
||||
'cursor-pointer',
|
||||
isSelected ? 'bg-sozsoft-active dark:bg-gray-600' : '',
|
||||
isDragging ? 'relative z-10 opacity-60' : '',
|
||||
)}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Td className="flex items-center !py-1 !px-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
size="xs"
|
||||
title={dragTitle}
|
||||
disabled={dragDisabled}
|
||||
icon={<FaGripVertical />}
|
||||
className={dragDisabled ? 'invisible' : 'cursor-grab'}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
{canDelete && (
|
||||
<Button
|
||||
shape="circle"
|
||||
variant="plain"
|
||||
type="button"
|
||||
size="sm"
|
||||
title="Delete"
|
||||
icon={<FaMinus />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onDelete()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span style={{ visibility: !field?.visible ? 'visible' : 'hidden' }}>
|
||||
<FaEyeSlash />
|
||||
</span>
|
||||
<span style={{ visibility: !field?.isActive ? 'visible' : 'hidden' }}>
|
||||
<FaTimes />
|
||||
</span>
|
||||
<span className="ml-5">
|
||||
{field.listOrderNo}
|
||||
{field.editOrderNo ? ` (${field.editOrderNo})` : ''}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="!py-1 !px-1">{field.fieldName}</Td>
|
||||
<Td className="align-middle text-center !py-1 !px-1">
|
||||
<Avatar
|
||||
size={18}
|
||||
shape="circle"
|
||||
src={`/img/countries/${field.cultureName ?? 'default'}.png`}
|
||||
title={field.cultureName}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="hidden sm:table-cell !py-1 !px-1">{roleLabel}</Td>
|
||||
<Td className="hidden sm:table-cell !py-1 !px-1">{userLabel}</Td>
|
||||
</Tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default SortableFieldRow
|
||||
73
ui/src/views/admin/listForm/edit/form-fields/fieldOrder.ts
Normal file
73
ui/src/views/admin/listForm/edit/form-fields/fieldOrder.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { ColumnFormatEditDto } from '@/proxy/admin/list-form-field/models'
|
||||
|
||||
export type FieldOrderMode = 'list' | 'edit'
|
||||
|
||||
/**
|
||||
* Sürükleme sonucunu sıra numaralarına çevirir. Liste sırası bütün alanlar üzerindedir; form sırası
|
||||
* yalnızca aynı form grubundaki, forma giren (editOrderNo dolu) alanlar arasındadır — gruplar arası
|
||||
* taşıma grup değişikliğidir ve Options sekmesinden yapılır. Numaralar kapsam içinde 1'den yeniden
|
||||
* dağıtılır, yalnızca değeri değişen alanlar döner.
|
||||
*/
|
||||
export function reorderFields(
|
||||
fields: ColumnFormatEditDto[],
|
||||
mode: FieldOrderMode,
|
||||
activeId: string,
|
||||
overId: string,
|
||||
): ColumnFormatEditDto[] {
|
||||
const active = fields.find((f) => f.id === activeId)
|
||||
const over = fields.find((f) => f.id === overId)
|
||||
if (!active || !over || active === over) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Filtresiz listede ayni alanin baska kultur/rol/kullanici kayitlari da durur; her biri kendi
|
||||
// sirasini tasir.
|
||||
const sameOwner = (f: ColumnFormatEditDto) =>
|
||||
f.cultureName === active.cultureName &&
|
||||
(f.roleId ?? null) === (active.roleId ?? null) &&
|
||||
(f.userId ?? null) === (active.userId ?? null)
|
||||
|
||||
const scope = !sameOwner(over)
|
||||
? []
|
||||
: mode === 'list'
|
||||
? sortFields(fields.filter(sameOwner), 'list')
|
||||
: active.editOrderNo != null &&
|
||||
over.editOrderNo != null &&
|
||||
active.editGroupOrderNo === over.editGroupOrderNo
|
||||
? sortFields(
|
||||
fields.filter(
|
||||
(f) =>
|
||||
sameOwner(f) &&
|
||||
f.editOrderNo != null &&
|
||||
f.editGroupOrderNo === active.editGroupOrderNo,
|
||||
),
|
||||
'edit',
|
||||
)
|
||||
: []
|
||||
|
||||
const from = scope.indexOf(active)
|
||||
const to = scope.indexOf(over)
|
||||
if (from < 0 || to < 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const moved = [...scope]
|
||||
moved.splice(to, 0, ...moved.splice(from, 1))
|
||||
|
||||
const key = mode === 'list' ? 'listOrderNo' : 'editOrderNo'
|
||||
return moved
|
||||
.map((f, index) => ({ field: f, order: index + 1 }))
|
||||
.filter(({ field, order }) => field[key] !== order)
|
||||
.map(({ field, order }) => ({ ...field, [key]: order }))
|
||||
}
|
||||
|
||||
export function sortFields(fields: ColumnFormatEditDto[], mode: FieldOrderMode) {
|
||||
const last = Number.MAX_SAFE_INTEGER
|
||||
return [...fields].sort((a, b) =>
|
||||
mode === 'list'
|
||||
? (a.listOrderNo ?? last) - (b.listOrderNo ?? last)
|
||||
: (a.editGroupOrderNo ?? last) - (b.editGroupOrderNo ?? last) ||
|
||||
(a.editOrderNo ?? last) - (b.editOrderNo ?? last) ||
|
||||
(a.listOrderNo ?? last) - (b.listOrderNo ?? last),
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue