SchedulerView ve TodoBoard komponenti useListFormColumns ile çalıştırıldı

This commit is contained in:
Sedat Öztürk 2026-07-31 22:39:52 +03:00
parent 3a03c8e1e8
commit f8dcf25b26
3 changed files with 139 additions and 61 deletions

View file

@ -19743,8 +19743,8 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.TodoBoard.Status", "key": "ListForms.TodoBoard.Status",
"en": "Kanban / Status", "en": "Status",
"tr": "Kanban / Durum" "tr": "Durum"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",

View file

@ -7,7 +7,6 @@ import {
FieldCustomValueTypeEnum, FieldCustomValueTypeEnum,
GridDto, GridDto,
PlatformEditorTypes, PlatformEditorTypes,
UiLookupDataSourceTypeEnum,
} from '@/proxy/form/models' } from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import Scheduler, { import Scheduler, {
@ -38,6 +37,8 @@ import type { ValidationRule } from 'devextreme/common'
import { FaAlignLeft, FaClock, FaHeading } from 'react-icons/fa' import { FaAlignLeft, FaClock, FaHeading } from 'react-icons/fa'
import { Avatar } from '@/components/ui' import { Avatar } from '@/components/ui'
import { getUsers } from '@/services/identity.service' import { getUsers } from '@/services/identity.service'
import { useListFormColumns } from './useListFormColumns'
import type { GridColumnData } from './GridColumnData'
interface SchedulerViewProps { interface SchedulerViewProps {
listFormCode: string listFormCode: string
@ -82,6 +83,11 @@ const getValueByField = (data: Record<string, any>, field?: string) => {
return resolvedField ? data[resolvedField] : undefined return resolvedField ? data[resolvedField] : undefined
} }
const flattenColumns = (columns: GridColumnData[] = []): GridColumnData[] =>
columns.flatMap((column) =>
column.columns?.length ? flattenColumns(column.columns as GridColumnData[]) : [column],
)
const SchedulerView = (props: SchedulerViewProps) => { const SchedulerView = (props: SchedulerViewProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization() const { translate } = useLocalization()
@ -158,6 +164,13 @@ const SchedulerView = (props: SchedulerViewProps) => {
}, [listFormCode]) }, [listFormCode])
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef })
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef: schedulerRef,
})
const listFormColumns = useMemo(() => flattenColumns(getBandedColumns()), [getBandedColumns])
useEffect(() => { useEffect(() => {
if (!gridDto) { if (!gridDto) {
@ -643,8 +656,10 @@ const SchedulerView = (props: SchedulerViewProps) => {
console.log(err) console.log(err)
} }
const fieldName = i.fieldName!
const listFormField = i const listFormField = i
const listFormColumn = listFormColumns.find(
(column) => column.dataField?.toLowerCase() === i.fieldName?.toLowerCase(),
)
if (listFormField?.sourceDbType === DbTypeEnum.Date) { if (listFormField?.sourceDbType === DbTypeEnum.Date) {
editorOptions = { editorOptions = {
@ -678,48 +693,14 @@ const SchedulerView = (props: SchedulerViewProps) => {
editorType = i.editorType2 editorType = i.editorType2
} }
// Lookup DataSource oluştur const lookup = listFormColumn?.lookup
if (listFormField?.lookupDto) { if (lookup) {
const lookup = listFormField.lookupDto editorOptions.dataSource =
typeof lookup.dataSource === 'function'
if (lookup.dataSourceType === UiLookupDataSourceTypeEnum.Query) { ? lookup.dataSource({ data: e.appointmentData ?? {} })
editorOptions.dataSource = new CustomStore({ : lookup.dataSource
key: 'key', editorOptions.valueExpr = lookup.valueExpr
loadMode: 'raw', editorOptions.displayExpr = lookup.displayExpr
load: async () => {
try {
const { dynamicFetch } = await import('@/services/form.service')
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
listFormCode,
listFormFieldName: fieldName,
filters: [],
})
return (response.data ?? []).map((a: any) => ({
key: a.Key,
name: a.Name,
group: a.Group,
}))
} catch (error) {
console.error('Lookup load error:', error)
return []
}
},
})
editorOptions.valueExpr = 'key'
editorOptions.displayExpr = 'name'
} else if (lookup.dataSourceType === UiLookupDataSourceTypeEnum.StaticData) {
if (lookup.lookupQuery) {
try {
const staticData = JSON.parse(lookup.lookupQuery)
editorOptions.dataSource = staticData
editorOptions.valueExpr = lookup.valueExpr || 'key'
editorOptions.displayExpr = lookup.displayExpr || 'name'
} catch (error) {
console.error('Static data parse error:', error)
}
}
}
} }
const item: SimpleItemWithColData = { const item: SimpleItemWithColData = {
@ -822,7 +803,7 @@ const SchedulerView = (props: SchedulerViewProps) => {
e.form.option('showValidationSummary', false) e.form.option('showValidationSummary', false)
e.form.option('items', result) e.form.option('items', result)
}, },
[gridDto, translate, isPopupFullScreen, listFormCode, useMobileEditPopup], [gridDto, translate, isPopupFullScreen, listFormColumns, useMobileEditPopup],
) )
const configuredEditPopup = gridDto?.gridOptions.editingOptionDto?.popup const configuredEditPopup = gridDto?.gridOptions.editingOptionDto?.popup

View file

@ -45,6 +45,9 @@ import { getTenants } from '@/services/tenant.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission' import { usePermission } from '@/utils/hooks/usePermission'
import { usePWA } from '@/utils/hooks/usePWA' import { usePWA } from '@/utils/hooks/usePWA'
import { useListFormColumns } from './useListFormColumns'
import type { GridColumnData } from './GridColumnData'
import { useStoreState } from '@/store/store'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
@ -78,6 +81,13 @@ interface UserOption {
avatarUrl?: string avatarUrl?: string
} }
type LookupDisplayValues = Record<string, Map<string, string>>
const flattenColumns = (columns: GridColumnData[] = []): GridColumnData[] =>
columns.flatMap((column) =>
column.columns?.length ? flattenColumns(column.columns as GridColumnData[]) : [column],
)
const fieldValue = (row: TodoRow, field?: string) => (field ? row[field] : undefined) const fieldValue = (row: TodoRow, field?: string) => (field ? row[field] : undefined)
const tenantIdValue = (row: TodoRow) => row.TenantId ?? row.tenantId const tenantIdValue = (row: TodoRow) => row.TenantId ?? row.tenantId
@ -157,7 +167,7 @@ const priorityLabel = (
: translate('::ListForms.TodoBoard.NoPriority') : translate('::ListForms.TodoBoard.NoPriority')
} }
const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => { const TodoBoard = ({ listFormCode, searchParams, isSubForm, gridDto }: TodoBoardProps) => {
const options = gridDto.gridOptions.todoOptionDto as TodoOptionDto const options = gridDto.gridOptions.todoOptionDto as TodoOptionDto
const editingOptions = gridDto.gridOptions.editingOptionDto const editingOptions = gridDto.gridOptions.editingOptionDto
const canAdd = const canAdd =
@ -167,9 +177,40 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
const canDelete = const canDelete =
editingOptions?.allowDeleting === true && Boolean(gridDto.gridOptions.deleteServiceAddress) editingOptions?.allowDeleting === true && Boolean(gridDto.gridOptions.deleteServiceAddress)
const keyField = gridDto.gridOptions.keyFieldName ?? 'id' const keyField = gridDto.gridOptions.keyFieldName ?? 'id'
const userNameField =
gridDto.columnFormats?.find((column) => column.fieldName?.toLowerCase() === 'username')
?.fieldName ?? 'UserName'
const stateStoring = gridDto.gridOptions.stateStoringDto const stateStoring = gridDto.gridOptions.stateStoringDto
const currentUserName = useStoreState((state) => state.auth.user.userName)
const gridRef = useRef<DataGridRef<any, any>>() const gridRef = useRef<DataGridRef<any, any>>()
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef,
})
const getBandedColumnsRef = useRef(getBandedColumns)
const loadLookupDisplayValuesRef = useRef(loadLookupDisplayValues)
getBandedColumnsRef.current = getBandedColumns
loadLookupDisplayValuesRef.current = loadLookupDisplayValues
const listFormColumnsCacheRef = useRef<{
gridDto?: GridDto
listFormCode: string
columns: GridColumnData[]
}>()
if (
!listFormColumnsCacheRef.current ||
listFormColumnsCacheRef.current.gridDto !== gridDto ||
listFormColumnsCacheRef.current.listFormCode !== listFormCode
) {
listFormColumnsCacheRef.current = {
gridDto,
listFormCode,
columns: flattenColumns(getBandedColumnsRef.current()),
}
}
const listFormColumns = listFormColumnsCacheRef.current.columns
const [store, setStore] = useState<CustomStore<any, any>>() const [store, setStore] = useState<CustomStore<any, any>>()
const [rows, setRows] = useState<TodoRow[]>([]) const [rows, setRows] = useState<TodoRow[]>([])
const [statuses, setStatuses] = useState<string[]>([]) const [statuses, setStatuses] = useState<string[]>([])
@ -194,6 +235,7 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
const [userOptions, setUserOptions] = useState<UserOption[]>([]) const [userOptions, setUserOptions] = useState<UserOption[]>([])
const [tenantNames, setTenantNames] = useState<Map<string, string>>(new Map()) const [tenantNames, setTenantNames] = useState<Map<string, string>>(new Map())
const [usersLoading, setUsersLoading] = useState(false) const [usersLoading, setUsersLoading] = useState(false)
const [lookupDisplayValues, setLookupDisplayValues] = useState<LookupDisplayValues>({})
const columnConfigRef = useRef<StoredBoardColumns>({ columns: [], retired: [] }) const columnConfigRef = useRef<StoredBoardColumns>({ columns: [], retired: [] })
const dragActiveRef = useRef(false) const dragActiveRef = useRef(false)
const { translate } = useLocalization() const { translate } = useLocalization()
@ -201,6 +243,23 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
const isPwaMode = usePWA() const isPwaMode = usePWA()
const uncategorized = translate('::ListForms.TodoBoard.Uncategorized') const uncategorized = translate('::ListForms.TodoBoard.Uncategorized')
const lookupValuesForField = useCallback(
(field?: string) => {
if (!field) return undefined
const dataField = listFormColumns.find(
(column) => column.dataField?.toLowerCase() === field.toLowerCase(),
)?.dataField
return dataField ? lookupDisplayValues[dataField] : undefined
},
[listFormColumns, lookupDisplayValues],
)
const lookupText = useCallback(
(field: string | undefined, value: unknown) =>
lookupValuesForField(field)?.get(String(value)) ?? String(value ?? ''),
[lookupValuesForField],
)
const clearDragState = useCallback(() => { const clearDragState = useCallback(() => {
dragActiveRef.current = false dragActiveRef.current = false
setDraggedKey(undefined) setDraggedKey(undefined)
@ -267,6 +326,22 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
} }
}, []) }, [])
useEffect(() => {
let active = true
setLookupDisplayValues({})
loadLookupDisplayValuesRef
.current(listFormColumns)
.then((values) => {
if (active) setLookupDisplayValues(values)
})
.catch((error) => console.error('TodoBoard lookup load error:', error))
return () => {
active = false
}
}, [listFormColumns])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@ -384,14 +459,23 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
[options.assigneeExpr, rows], [options.assigneeExpr, rows],
) )
const assigneeOptions = useMemo<UserOption[]>(() => { const assigneeOptions = useMemo<UserOption[]>(() => {
const knownUsers = new Set(userOptions.map((option) => option.value)) const lookupValues = lookupValuesForField(options.assigneeExpr)
const avatarUrls = new Map(userOptions.map((option) => [option.value, option.avatarUrl]))
const configuredOptions = lookupValues
? Array.from(lookupValues, ([value, label]) => ({
value,
label,
avatarUrl: avatarUrls.get(value),
}))
: userOptions
const knownUsers = new Set(configuredOptions.map((option) => option.value))
return [ return [
...userOptions, ...configuredOptions,
...assigneeSuggestions ...assigneeSuggestions
.filter((value) => !knownUsers.has(value)) .filter((value) => !knownUsers.has(value))
.map((value) => ({ value, label: value })), .map((value) => ({ value, label: value })),
] ]
}, [assigneeSuggestions, userOptions]) }, [assigneeSuggestions, lookupValuesForField, options.assigneeExpr, userOptions])
const assigneeOptionMap = useMemo( const assigneeOptionMap = useMemo(
() => new Map(assigneeOptions.map((option) => [option.value, option])), () => new Map(assigneeOptions.map((option) => [option.value, option])),
[assigneeOptions], [assigneeOptions],
@ -681,6 +765,7 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
const values: Record<string, any> = { const values: Record<string, any> = {
[options.titleExpr]: title, [options.titleExpr]: title,
[options.statusExpr]: status, [options.statusExpr]: status,
[userNameField]: currentUserName,
} }
if (options.completedExpr) values[options.completedExpr] = false if (options.completedExpr) values[options.completedExpr] = false
if (options.orderExpr) values[options.orderExpr] = itemsForStatus(status).length if (options.orderExpr) values[options.orderExpr] = itemsForStatus(status).length
@ -778,12 +863,11 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
<section className="min-h-[420px] rounded-xl shadow-sm"> <section className="min-h-[420px] rounded-xl shadow-sm">
{renderToolbar(true)} {renderToolbar(true)}
<div className="mb-6 flex justify-end border-b border-gray-200 dark:border-gray-700"> <div className="mb-6 flex justify-end border-b border-gray-200 dark:border-gray-700">
{editingOptions?.allowUpdating === true && {editingOptions?.allowUpdating === true && !gridDto.gridOptions.updateServiceAddress && (
!gridDto.gridOptions.updateServiceAddress && ( <span className="text-xs text-amber-600">
<span className="text-xs text-amber-600"> {translate('::ListForms.TodoBoard.EditRequiresUpdateService')}
{translate('::ListForms.TodoBoard.EditRequiresUpdateService')} </span>
</span> )}
)}
</div> </div>
<div className="grid gap-5 lg:grid-cols-3"> <div className="grid gap-5 lg:grid-cols-3">
<div className="space-y-5 lg:col-span-2"> <div className="space-y-5 lg:col-span-2">
@ -941,6 +1025,16 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
/> />
</div> </div>
)} )}
<div>
<label className={labelClass}>
{translate('::App.Listform.ListformField.UserName')}
</label>
<input
readOnly
className={`${inputClass} cursor-default`}
value={String(draft[userNameField] ?? '')}
/>
</div>
{options.statusExpr && ( {options.statusExpr && (
<div> <div>
<label className={labelClass}>{translate('::ListForms.TodoBoard.Status')}</label> <label className={labelClass}>{translate('::ListForms.TodoBoard.Status')}</label>
@ -957,7 +1051,7 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
> >
{statuses.map((status) => ( {statuses.map((status) => (
<option key={status} value={status}> <option key={status} value={status}>
{status} {lookupText(options.statusExpr, status)}
</option> </option>
))} ))}
</select> </select>
@ -1176,8 +1270,11 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
</form> </form>
) : ( ) : (
<> <>
<h4 className="min-w-0 truncate font-sm" title={status}> <h4
{status} className="min-w-0 truncate font-sm"
title={lookupText(options.statusExpr, status)}
>
{lookupText(options.statusExpr, status)}
</h4> </h4>
{canUpdate && ( {canUpdate && (
<button <button
@ -1412,7 +1509,7 @@ const TodoBoard = ({ listFormCode, searchParams, gridDto }: TodoBoardProps) => {
className="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2 py-0.5 text-[11px] text-blue-700 dark:bg-blue-950 dark:text-blue-200" className="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2 py-0.5 text-[11px] text-blue-700 dark:bg-blue-950 dark:text-blue-200"
> >
<FaTag /> <FaTag />
{tag} {lookupText(options.tagExpr, tag)}
</span> </span>
))} ))}
{tags.length > 3 && ( {tags.length > 3 && (