CardView komponenti

This commit is contained in:
Sedat ÖZTÜRK 2026-07-16 11:19:17 +03:00
parent d38e4f851a
commit 412ad236cc
20 changed files with 1446 additions and 83 deletions

View file

@ -3,6 +3,7 @@
public class LayoutDto
{
public bool Grid { get; set; } = true;
public bool Card { get; set; } = true;
public bool Pivot { get; set; } = true;
public bool Chart { get; set; } = true;
public bool Tree { get; set; } = true;

View file

@ -22,6 +22,7 @@ public class ListFormWizardDto
public string DefaultLayout { get; set; }
public bool Grid { get; set; }
public bool Card { get; set; }
public bool Pivot { get; set; }
public bool Tree { get; set; }
public bool Chart { get; set; }

View file

@ -281,7 +281,7 @@ public class ListFormWizardAppService(
ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false,
ShowNote = input.SubForms.Count > 0 || input.WorkflowCriteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode,
Name = nameLangKey,

View file

@ -4680,6 +4680,12 @@
"en": "Grid Layout",
"tr": "Liste Düzeni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DetailsLayoutDto.CardLayout",
"en": "Card Layout",
"tr": "Kart Düzeni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout",

View file

@ -67,6 +67,7 @@ public static class ListFormSeeder_DefaultJsons
public static string DefaultLayoutJson(string DefaultLayout = "grid") => JsonSerializer.Serialize(new LayoutDto()
{
Grid = true,
Card = true,
Pivot = true,
Chart = true,
Tree = true,

View file

@ -393,7 +393,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false,
ShowNote = input.SubForms.Count > 0 || input.WorkflowCriteria.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode,
Name = nameLangKey,

View file

@ -84,9 +84,10 @@ public static class WizardConsts
Margin = 10
});
public static string DefaultLayoutJson(string DefaultLayout = "grid", bool Grid = true, bool Pivot = true, bool Chart = true, bool Tree = true, bool Gantt = true, bool Scheduler = true) => JsonSerializer.Serialize(new
public static string DefaultLayoutJson(string DefaultLayout = "grid", bool Grid = true, bool Card = true, bool Pivot = true, bool Chart = true, bool Tree = true, bool Gantt = true, bool Scheduler = true) => JsonSerializer.Serialize(new
{
Grid = Grid,
Card = Card,
Pivot = Pivot,
Chart = Chart,
Tree = Tree,

View file

@ -4,6 +4,22 @@ body {
font-size: 14px; /* bodyye font-size uygulanmaz */
}
.list-card-view.dx-cardview {
padding: 0;
border-radius: 0;
background-color: transparent;
}
.list-card-view .dx-cardview-card,
.list-card-view .dx-cardview-card-header .dx-toolbar,
.list-card-view .dx-cardview-card-footer {
border-radius: 0 !important;
}
.list-card-view .dx-toolbar .dx-button-text {
text-transform: none !important;
}
.dx-form-group-with-caption > .dx-form-group-content {
padding-top: 2px !important;
}

View file

@ -1,9 +1,9 @@
import { SelectCommandTypeEnum } from '@/proxy/form/models'
import {
ListFormWorkflowCriteriaDto,
SubFormDto,
WidgetEditDto,
WorkflowDto,
SelectCommandTypeEnum
} from '@/proxy/form/models'
import { ListViewLayoutType } from '@/views/admin/listForm/edit/types'
@ -39,6 +39,7 @@ export interface ListFormWizardDto {
defaultLayout: ListViewLayoutType
grid: boolean
card: boolean
pivot: boolean
tree: boolean
chart: boolean

View file

@ -951,6 +951,7 @@ export interface ListFormWorkflowCriteriaDto {
export interface LayoutDto {
grid: boolean
card: boolean
pivot: boolean
tree: boolean
chart: boolean

View file

@ -26,6 +26,7 @@ const schema = Yup.object().shape({
defaultLayout: Yup.string(),
layoutDto: Yup.object().shape({
grid: Yup.boolean(),
card: Yup.boolean(),
pivot: Yup.boolean(),
chart: Yup.boolean(),
tree: Yup.boolean(),
@ -312,6 +313,22 @@ function FormTabDetails(
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.CardLayout')}
invalid={errors.layoutDto?.card && touched.layoutDto?.card}
errorMessage={errors.layoutDto?.card}
>
<Field
className="w-20"
autoComplete="off"
name="layoutDto.card"
placeholder={translate(
'::ListForms.ListFormEdit.DetailsLayoutDto.CardLayout',
)}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout')}
invalid={errors.layoutDto?.pivot && touched.layoutDto?.pivot}

View file

@ -224,6 +224,7 @@ export const listFormTypeOptions = [
export const listFormDefaultLayoutOptions = [
{ value: 'grid', label: 'Grid' },
{ value: 'card', label: 'Card' },
{ value: 'pivot', label: 'Pivot' },
{ value: 'chart', label: 'Chart' },
{ value: 'tree', label: 'Tree' },

View file

@ -1,9 +1,17 @@
export type ChartOperation = '' | 'select' | 'insert' | 'update' | 'delete'
export type ChartDialogType = '' | 'pane' | 'serie' | 'annotation' | 'axis'
export type ListViewLayoutType = 'grid' | 'pivot' | 'tree' | 'chart' | 'gantt' | 'scheduler'
export type ListViewLayoutType =
| 'grid'
| 'card'
| 'pivot'
| 'tree'
| 'chart'
| 'gantt'
| 'scheduler'
export const layoutTypes = {
grid: 'Grid',
card: 'Card',
pivot: 'Pivot',
tree: 'Tree',
chart: 'Chart',

View file

@ -516,6 +516,20 @@ const WizardStep2 = ({
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.CardLayout')}
invalid={errors?.card && touched?.card}
errorMessage={errors?.card}
>
<Field
className="w-20"
autoComplete="off"
name="card"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.CardLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.PivotLayout')}
invalid={errors?.pivot && touched?.pivot}

View file

@ -3,6 +3,40 @@ import React from 'react'
const NO_IMAGE = '/img/others/no-image.png'
let previewElement: HTMLDivElement | null = null
let previewTarget: HTMLElement | null = null
let previewListenersBound = false
const hidePreviewElement = () => {
previewTarget = null
if (previewElement) {
previewElement.style.opacity = '0'
previewElement.style.display = 'none'
}
}
const bindPreviewDismissListeners = () => {
if (previewListenersBound) return
previewListenersBound = true
document.addEventListener(
'pointermove',
(event) => {
const target = event.target as Node | null
if (
previewTarget &&
(!previewTarget.isConnected || !target || !previewTarget.contains(target))
) {
hidePreviewElement()
}
},
{ passive: true },
)
document.addEventListener('scroll', hidePreviewElement, true)
window.addEventListener('blur', hidePreviewElement)
document.addEventListener('visibilitychange', () => {
if (document.hidden) hidePreviewElement()
})
}
const getPreviewElement = () => {
if (!previewElement) {
@ -31,6 +65,7 @@ const getPreviewElement = () => {
element.appendChild(image)
document.body.appendChild(element)
previewElement = element
bindPreviewDismissListeners()
}
return previewElement
@ -38,6 +73,7 @@ const getPreviewElement = () => {
export const showImageHoverPreview = (src: string, event: React.MouseEvent<HTMLElement>) => {
const element = getPreviewElement()
previewTarget = event.currentTarget
const image = element.querySelector('img') as HTMLImageElement
image.onerror = () => {
image.onerror = null
@ -65,13 +101,11 @@ export const showImageHoverPreview = (src: string, event: React.MouseEvent<HTMLE
}
export const hideImageHoverPreview = () => {
if (previewElement) {
previewElement.style.opacity = '0'
previewElement.style.display = 'none'
}
hidePreviewElement()
}
export const openImageInNewTab = (url: string) => {
hidePreviewElement()
if (!url.startsWith('data:image/')) {
window.open(url, '_blank', 'noopener,noreferrer')
return

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,7 @@ import { ListViewLayoutType } from '../admin/listForm/edit/types'
import {
FaChartArea,
FaThLarge,
FaList,
FaSitemap,
FaTable,
@ -26,6 +27,7 @@ import {
🔥 LAZY VIEW IMPORTS
======================= */
const Grid = React.lazy(() => import('./Grid'))
const CardView = React.lazy(() => import('./CardView'))
const Pivot = React.lazy(() => import('./Pivot'))
const Tree = React.lazy(() => import('./Tree'))
const Chart = React.lazy(() => import('./Chart'))
@ -34,6 +36,7 @@ const SchedulerView = React.lazy(() => import('./SchedulerView'))
function isLayoutValid(dto: GridDto, layout: ListViewLayoutType | undefined): boolean {
if (!layout || layout === 'grid') return true
if (layout === 'card') return true
if (layout === 'pivot') return !!dto.gridOptions?.layoutDto?.pivot
if (layout === 'chart') return !!dto.gridOptions?.layoutDto?.chart
if (layout === 'tree')
@ -125,6 +128,7 @@ const List: React.FC = () => {
const preload = useMemo(
() => ({
grid: () => import('./Grid'),
card: () => import('./CardView'),
pivot: () => import('./Pivot'),
tree: () => import('./Tree'),
chart: () => import('./Chart'),
@ -200,6 +204,16 @@ const List: React.FC = () => {
onMouseEnter={() => preload.grid()}
></Button>
{gridDto.gridOptions.layoutDto.card && (
<Button
size="sm"
icon={<FaThLarge />}
variant={viewMode === 'card' ? 'solid' : 'default'}
onClick={() => setLayout('card')}
onMouseEnter={() => preload.card()}
></Button>
)}
{gridDto.gridOptions.layoutDto.pivot && (
<Button
size="sm"
@ -235,6 +249,15 @@ const List: React.FC = () => {
/>
)}
{viewMode === 'card' && (
<CardView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
)}
{viewMode === 'pivot' && (
<Pivot
listFormCode={listFormCode}

View file

@ -6,7 +6,7 @@ import {
GridDto,
PlatformEditorTypes,
UiLookupDataSourceTypeEnum,
EditingFormItemDto
EditingFormItemDto,
} from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Scheduler, {
@ -307,7 +307,9 @@ const SchedulerView = (props: SchedulerViewProps) => {
if (i.editorOptions) {
editorOptions = JSON.parse(i.editorOptions)
}
} catch {}
} catch (err) {
console.log(err)
}
const fieldName = i.dataField.split(':')[0]
const listFormField = gridDto.columnFormats.find((x: any) => x.fieldName === fieldName)

View file

@ -14,12 +14,14 @@ import dxDataGrid from 'devextreme/ui/data_grid'
import dxPivotGrid from 'devextreme/ui/pivot_grid'
import dxTreeList from 'devextreme/ui/tree_list'
import dxGantt from 'devextreme/ui/gantt'
import dxCardView from 'devextreme/ui/card_view'
import { Dispatch, MutableRefObject, SetStateAction, useEffect, useState } from 'react'
import { setGridPanelColor } from './Utils'
import { usePermission } from '@/utils/hooks/usePermission'
import { usePWA } from '@/utils/hooks/usePWA'
import { ROUTES_ENUM } from '@/routes/route.constant'
import type { GanttRef } from 'devextreme-react/cjs/gantt'
import type { CardViewRef } from 'devextreme-react/card-view'
import { useStoreState } from '@/store'
import { MULTIVALUE_DELIMITER } from '@/constants/app.constant'
import { createReportQueryString } from '../report/reportRouteParams'
@ -29,7 +31,7 @@ export interface ISelectBoxData {
label?: string
}
type GridInstance = dxDataGrid | dxPivotGrid | dxTreeList | dxGantt
type GridInstance = dxDataGrid | dxPivotGrid | dxTreeList | dxGantt | dxCardView
type FilterToolbarActionId =
| 'openDynamicGridReport'
| 'saveActiveFilter'
@ -50,68 +52,46 @@ type FilterToolbarAction = {
// Grid tipini kontrol eden yardımcı fonksiyonlar
const isDataGrid = (grid: GridInstance): grid is dxDataGrid => {
return 'clearFilter' in grid
return 'state' in grid && 'getVisibleColumns' in grid && !('getRootNode' in grid)
}
const isTreeList = (grid: GridInstance): grid is dxTreeList => {
return 'getRootNode' in grid
}
const isCardView = (grid: GridInstance): grid is dxCardView => {
return 'getSelectedCardKeys' in grid
}
const supportsState = (grid: GridInstance): grid is dxDataGrid | dxTreeList => {
return 'state' in grid && typeof (grid as any).state === 'function'
}
const setToolbarItemValue = (grid: GridInstance, itemName: string, value: any) => {
if (isDataGrid(grid)) {
const toolbarOptions = grid.option('toolbar')
if (toolbarOptions?.items) {
const index = toolbarOptions.items.findIndex((item: any) => item.name === itemName)
if (index > -1 && toolbarOptions.items[index]) {
const item = toolbarOptions.items[index] as any
if (item.options) {
item.options.value = value
grid.option('toolbar', toolbarOptions)
}
}
}
} else if (isTreeList(grid)) {
const toolbarOptions = grid.option('toolbar')
if (toolbarOptions?.items) {
const index = toolbarOptions.items.findIndex((item: any) => item.name === itemName)
if (index > -1 && toolbarOptions.items[index]) {
const item = toolbarOptions.items[index] as any
if (item.options) {
item.options.value = value
grid.option('toolbar', toolbarOptions)
}
const component = grid as any
const toolbarOptions = component.option('toolbar')
if (toolbarOptions?.items) {
const index = toolbarOptions.items.findIndex((item: any) => item.name === itemName)
if (index > -1 && toolbarOptions.items[index]) {
const item = toolbarOptions.items[index] as any
if (item.options) {
item.options.value = value
component.option('toolbar', toolbarOptions)
}
}
}
}
const setToolbarItemItems = (grid: GridInstance, itemName: string, items: any[]) => {
if (isDataGrid(grid)) {
const toolbarOptions = grid.option('toolbar')
if (toolbarOptions?.items) {
const index = toolbarOptions.items.findIndex((item: any) => item.name === itemName)
if (index > -1 && toolbarOptions.items[index]) {
const item = toolbarOptions.items[index] as any
if (item.options) {
item.options.items = items
grid.option('toolbar', toolbarOptions)
}
}
}
} else if (isTreeList(grid)) {
const toolbarOptions = grid.option('toolbar')
if (toolbarOptions?.items) {
const index = toolbarOptions.items.findIndex((item: any) => item.name === itemName)
if (index > -1 && toolbarOptions.items[index]) {
const item = toolbarOptions.items[index] as any
if (item.options) {
item.options.items = items
grid.option('toolbar', toolbarOptions)
}
const component = grid as any
const toolbarOptions = component.option('toolbar')
if (toolbarOptions?.items) {
const index = toolbarOptions.items.findIndex((item: any) => item.name === itemName)
if (index > -1 && toolbarOptions.items[index]) {
const item = toolbarOptions.items[index] as any
if (item.options) {
item.options.items = items
component.option('toolbar', toolbarOptions)
}
}
}
@ -140,34 +120,23 @@ const fitColumns = (grid: GridInstance) => {
}
const clearGridFilter = (grid: GridInstance) => {
if (isDataGrid(grid)) {
grid.clearFilter()
} else if (isTreeList(grid)) {
grid.clearFilter()
}
;(grid as any).clearFilter?.()
}
const setFilterPanelVisible = (grid: GridInstance, visible: boolean) => {
if (isDataGrid(grid)) {
grid.option('filterPanel', { visible })
} else if (isTreeList(grid)) {
grid.option('filterPanel', { visible })
}
;(grid as any).option('filterPanel', { visible })
}
const setFilterValue = (grid: GridInstance, value: any) => {
if (isDataGrid(grid)) {
grid.option('filterValue', value)
} else if (isTreeList(grid)) {
grid.option('filterValue', value)
}
;(grid as any).option('filterValue', value)
}
const resetGridState = (grid: GridInstance) => {
if (isDataGrid(grid)) {
grid.state(null)
} else if (isTreeList(grid)) {
if (supportsState(grid)) {
grid.state(null)
} else if (isCardView(grid)) {
grid.option('filterValue', undefined)
grid.option('selectedCardKeys', [])
}
}
@ -305,6 +274,7 @@ const useFilters = ({
| MutableRefObject<PivotGridRef | undefined>
| MutableRefObject<TreeListRef<any, any> | undefined>
| MutableRefObject<GanttRef | undefined>
| MutableRefObject<CardViewRef | undefined>
listFormCode: string
saveGridState?: (state: any) => Promise<void>
}): {

View file

@ -68,6 +68,40 @@ const cellTemplateMultiValue = (
// Hover preview overlay — singleton, tüm grid hücreleri tarafından paylaşılır
let __imgPreviewEl: HTMLDivElement | null = null
let __imgPreviewTarget: HTMLElement | null = null
let __imgPreviewListenersBound = false
function hideImgPreview() {
__imgPreviewTarget = null
if (__imgPreviewEl) {
__imgPreviewEl.style.opacity = '0'
__imgPreviewEl.style.display = 'none'
}
}
function bindImgPreviewDismissListeners() {
if (__imgPreviewListenersBound) return
__imgPreviewListenersBound = true
document.addEventListener(
'pointermove',
(event) => {
const target = event.target as Node | null
if (
__imgPreviewTarget &&
(!__imgPreviewTarget.isConnected || !target || !__imgPreviewTarget.contains(target))
) {
hideImgPreview()
}
},
{ passive: true },
)
document.addEventListener('scroll', hideImgPreview, true)
window.addEventListener('blur', hideImgPreview)
document.addEventListener('visibilitychange', () => {
if (document.hidden) hideImgPreview()
})
}
function getImgPreview(): HTMLDivElement {
if (!__imgPreviewEl) {
@ -97,12 +131,14 @@ function getImgPreview(): HTMLDivElement {
el.appendChild(img)
document.body.appendChild(el)
__imgPreviewEl = el
bindImgPreviewDismissListeners()
}
return __imgPreviewEl
}
function showImgPreview(src: string, e: MouseEvent) {
const el = getImgPreview()
__imgPreviewTarget = e.currentTarget instanceof HTMLElement ? e.currentTarget : null
const imgEl = el.querySelector('img') as HTMLImageElement
imgEl.onerror = () => {
imgEl.onerror = null
@ -132,13 +168,6 @@ function showImgPreview(src: string, e: MouseEvent) {
el.style.opacity = '1'
}
function hideImgPreview() {
if (__imgPreviewEl) {
__imgPreviewEl.style.opacity = '0'
__imgPreviewEl.style.display = 'none'
}
}
const cellTemplateImage = (
cellElement: HTMLElement,
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
@ -166,7 +195,10 @@ const cellTemplateImage = (
img.addEventListener('mouseenter', (e) => showImgPreview(url, e as MouseEvent))
img.addEventListener('mousemove', (e) => showImgPreview(url, e as MouseEvent))
img.addEventListener('mouseleave', hideImgPreview)
img.addEventListener('click', () => openImageSource(url))
img.addEventListener('click', () => {
hideImgPreview()
openImageSource(url)
})
cellElement.appendChild(img)
})