IsLookupSelfSourced ve Widget tazelendi
This commit is contained in:
parent
6d5071237a
commit
c62e2c8586
15 changed files with 271 additions and 39 deletions
|
|
@ -428,6 +428,10 @@ Sorgu ekranın bağlantısında (`IsTenant` + `DataSourceCode` ile çözülen, `
|
||||||
ve isteğe bağlı `Value` ifadesi; alt başlık ekranın adıdır, değer tip çakışmasın diye metne
|
ve isteğe bağlı `Value` ifadesi; alt başlık ekranın adıdır, değer tip çakışmasın diye metne
|
||||||
çevrilir. Gruplamalı sorgu için `DefaultWidgetsJson(sql, kartSayısı)`. Genel kart başlıkları
|
çevrilir. Gruplamalı sorgu için `DefaultWidgetsJson(sql, kartSayısı)`. Genel kart başlıkları
|
||||||
`App.Widget.*` dil anahtarlarındadır.
|
`App.Widget.*` dil anahtarlarındadır.
|
||||||
|
- Kartlar GridDto ile gelir; Grid, Tree, Card, Gantt ve Scheduler görünümünde Yenile düğmesi ve
|
||||||
|
satır ekleme/güncelleme/silme kartları yeniden çeker (`useListFormWidgets`). Yalnızca widget
|
||||||
|
listesi değişir, GridDto değiştirilmez — görünüm durumu (sayfa, seçim) korunur. Kart sorgusu
|
||||||
|
ekranın filtresine bağlı olmadığı için filtre değişikliği kartları yenilemez.
|
||||||
|
|
||||||
### 5.4 `LayoutJson` (`LayoutDto`)
|
### 5.4 `LayoutJson` (`LayoutDto`)
|
||||||
|
|
||||||
|
|
|
||||||
10
.github/instructions/lowcode.instructions.md
vendored
10
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -1207,6 +1207,16 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir.
|
||||||
hata vermez, **sessizce yok sayılır** — bu yüzden `columns` bir dizi, `maxDisplayedTags` bir
|
hata vermez, **sessizce yok sayılır** — bu yüzden `columns` bir dizi, `maxDisplayedTags` bir
|
||||||
sayı, `multiple` bir boolean olmalıdır.
|
sayı, `multiple` bir boolean olmalıdır.
|
||||||
|
|
||||||
|
Lookup kolonu hücre metnini açılışta yüklenen lookup listesinden bulur. Lookup'ı ekranın kendi
|
||||||
|
kayıt kaynağından beslenen kolonda (ör. `SELECT DISTINCT CompanyName … FROM Hr_T_Visitor` ile
|
||||||
|
Autocomplete) kaydedilen yeni değer bu listede yoktur. Sunucu bu kolonları ekran açılırken
|
||||||
|
işaretler (`ColumnFormatDto.IsLookupSelfSourced`: Query lookup'ının FROM/JOIN nesnesi ekranın
|
||||||
|
`SelectCommand` nesnesiyle aynı; yalnızca `true` yazılır, seed dosyasına girmez). Grid, Tree ve
|
||||||
|
Card görünümünde satır eklenince/güncellenince yalnızca işaretli kolonlar ve yalnızca kaydedilen
|
||||||
|
değer listede yoksa lookup yeniden yüklenir (`refreshLookupsForRow`) — hücre, editörün açılır
|
||||||
|
listesi ve filtre satırı sayfa yenilenmeden güncellenir. Başka kaynaktan beslenen lookup, statik
|
||||||
|
veri, WebService ve cascade lookup için kayıtta hiç lookup isteği atılmaz.
|
||||||
|
|
||||||
**Lookup zorunlu** yazan editörlerde `LookupDataSourceType` + `LookupQuery` + `ValueExpr` +
|
**Lookup zorunlu** yazan editörlerde `LookupDataSourceType` + `LookupQuery` + `ValueExpr` +
|
||||||
`DisplayExpr` dolu olmalıdır; aksi hâlde alan boş bir liste gösterir:
|
`DisplayExpr` dolu olmalıdır; aksi hâlde alan boş bir liste gösterir:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,14 @@ public class ColumnFormatDto : AuditedEntityDto<Guid>
|
||||||
set { LookupJson = JsonSerializer.Serialize(value); }
|
set { LookupJson = JsonSerializer.Serialize(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lookup sorgusu ekranın kendi kayıt kaynağını okuyor; kaydedilen yeni değer lookup listesine
|
||||||
|
/// ancak kayıttan sonra girdiği için istemci lookup'ı yalnızca bu alanlarda yeniler. Ekran
|
||||||
|
/// açılırken hesaplanır; false yazılmadığı için seed dosyasına girmez.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
public bool IsLookupSelfSourced { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string ValidationRuleJson { get; set; }
|
public string ValidationRuleJson { get; set; }
|
||||||
public List<ValidationRuleDto> ValidationRuleDto
|
public List<ValidationRuleDto> ValidationRuleDto
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Sozsoft.Platform.DynamicData;
|
using Sozsoft.Platform.DynamicData;
|
||||||
using Sozsoft.Platform.Entities;
|
using Sozsoft.Platform.Entities;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
using Sozsoft.Platform.Queries;
|
using Sozsoft.Platform.Queries;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.Users;
|
using Volo.Abp.Users;
|
||||||
|
|
@ -282,6 +283,33 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
||||||
return Widgets;
|
return Widgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query lookup'ı ekranın veri kaynağındaki bir nesneyi (FROM/JOIN) okuyan alanları işaretler.
|
||||||
|
/// WebService lookup'ının kaynağı sorgudan okunamadığı için dışarıda kalır.
|
||||||
|
/// </summary>
|
||||||
|
private static void MarkSelfSourcedLookups(ListForm listForm, List<ColumnFormatDto> columnFormats)
|
||||||
|
{
|
||||||
|
var sourceObjects = listForm.SelectCommandType == SelectCommandTypeEnum.Query
|
||||||
|
? WizardExportCollector.ExtractQueryObjectNames(listForm.SelectCommand).ToHashSet(StringComparer.OrdinalIgnoreCase)
|
||||||
|
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (listForm.SelectCommandType != SelectCommandTypeEnum.Query
|
||||||
|
&& WizardExportCollector.NormalizeDatabaseObjectName(listForm.SelectCommand) is { } objectName)
|
||||||
|
{
|
||||||
|
sourceObjects.Add(objectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceObjects.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var columnFormat in columnFormats.Where(c => !string.IsNullOrWhiteSpace(c.LookupJson)))
|
||||||
|
{
|
||||||
|
var lookup = columnFormat.LookupDto;
|
||||||
|
columnFormat.IsLookupSelfSourced = lookup.DataSourceType == UiLookupDataSourceTypeEnum.Query
|
||||||
|
&& WizardExportCollector.ExtractQueryObjectNames(lookup.LookupQuery).Any(sourceObjects.Contains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<GridDto> GetGridAsync(string ListFormCode)
|
public async Task<GridDto> GetGridAsync(string ListFormCode)
|
||||||
{
|
{
|
||||||
//Izin logic process
|
//Izin logic process
|
||||||
|
|
@ -306,6 +334,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
||||||
ColumnFormats = ObjectMapper.Map<List<ListFormField>, List<ColumnFormatDto>>(fields),
|
ColumnFormats = ObjectMapper.Map<List<ListFormField>, List<ColumnFormatDto>>(fields),
|
||||||
Widgets = await GetWidgetsAsync(listForm)
|
Widgets = await GetWidgetsAsync(listForm)
|
||||||
};
|
};
|
||||||
|
MarkSelfSourcedLookups(listForm, result.ColumnFormats);
|
||||||
|
|
||||||
//Workflow kriterlerini alıp GridOptionsDto içine atıyoruz ki frontend'de kullanabilelim
|
//Workflow kriterlerini alıp GridOptionsDto içine atıyoruz ki frontend'de kullanabilelim
|
||||||
var workflowDto = result.GridOptions.WorkflowDto;
|
var workflowDto = result.GridOptions.WorkflowDto;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -227,6 +227,8 @@ export interface ColumnFormatDto extends AuditedEntityDto<string> {
|
||||||
columnGroupSummaryDto: ColumnTotalSummaryDto
|
columnGroupSummaryDto: ColumnTotalSummaryDto
|
||||||
lookupJson?: string
|
lookupJson?: string
|
||||||
lookupDto: LookupDto
|
lookupDto: LookupDto
|
||||||
|
/** Lookup ekranın kendi kayıt kaynağından besleniyor; kayıttan sonra lookup yenilenir. */
|
||||||
|
isLookupSelfSourced?: boolean
|
||||||
validationRuleJson?: string
|
validationRuleJson?: string
|
||||||
validationRuleDto: ValidationRuleDto[]
|
validationRuleDto: ValidationRuleDto[]
|
||||||
columnStylingJson?: string
|
columnStylingJson?: string
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ import {
|
||||||
import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
|
import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
|
||||||
import { createEditingFormHandlers } from './shared/editingFormScripts'
|
import { createEditingFormHandlers } from './shared/editingFormScripts'
|
||||||
import { flattenColumns } from './shared/columns'
|
import { flattenColumns } from './shared/columns'
|
||||||
import { getInitialExtraFilters, mergeFilterTriplets } from './shared/hooks'
|
import { getInitialExtraFilters, mergeFilterTriplets, useListFormWidgets } from './shared/hooks'
|
||||||
import { useFilters } from './useFilters'
|
import { useFilters } from './useFilters'
|
||||||
import { useListFormColumns } from './useListFormColumns'
|
import { useListFormColumns } from './useListFormColumns'
|
||||||
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
||||||
|
|
@ -373,6 +373,7 @@ const CardView = (props: CardViewProps) => {
|
||||||
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
||||||
const [formData, setFormData] = useState<Record<string, any>>()
|
const [formData, setFormData] = useState<Record<string, any>>()
|
||||||
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
|
||||||
const formDataRef = useRef<Record<string, any> | undefined>(undefined)
|
const formDataRef = useRef<Record<string, any> | undefined>(undefined)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formDataRef.current = formData
|
formDataRef.current = formData
|
||||||
|
|
@ -447,7 +448,12 @@ const CardView = (props: CardViewProps) => {
|
||||||
;(cardDataSource?.store() as any)?.invalidateSelectCache?.()
|
;(cardDataSource?.store() as any)?.invalidateSelectCache?.()
|
||||||
const reload = cardDataSource?.reload()
|
const reload = cardDataSource?.reload()
|
||||||
Promise.resolve(reload).finally(() => refreshParentData?.())
|
Promise.resolve(reload).finally(() => refreshParentData?.())
|
||||||
}, [refreshParentData])
|
void refreshWidgets()
|
||||||
|
}, [refreshParentData, refreshWidgets])
|
||||||
|
const onCardChanged = useCallback(() => {
|
||||||
|
refreshParentData?.()
|
||||||
|
void refreshWidgets()
|
||||||
|
}, [refreshParentData, refreshWidgets])
|
||||||
const setCardColumnCount = useCallback(
|
const setCardColumnCount = useCallback(
|
||||||
(cardColumnCount: number) => {
|
(cardColumnCount: number) => {
|
||||||
setListState({
|
setListState({
|
||||||
|
|
@ -477,7 +483,7 @@ const CardView = (props: CardViewProps) => {
|
||||||
[gridDto, listFormCode],
|
[gridDto, listFormCode],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
|
const { getBandedColumns, loadLookupDisplayValues, refreshLookupsForRow } = useListFormColumns({
|
||||||
gridDto,
|
gridDto,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
isSubForm,
|
isSubForm,
|
||||||
|
|
@ -729,6 +735,17 @@ const CardView = (props: CardViewProps) => {
|
||||||
]).then(([values, cascadeValues]) => applyLookupDisplayValues({ ...values, ...cascadeValues }))
|
]).then(([values, cascadeValues]) => applyLookupDisplayValues({ ...values, ...cascadeValues }))
|
||||||
}, [applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues])
|
}, [applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues])
|
||||||
|
|
||||||
|
// Kart metinleri lookup haritasından okunur; yenilenen lookup haritaya işlenip kartlar yeniden çizilir.
|
||||||
|
const refreshCardLookups = useCallback(
|
||||||
|
async (instance: any, rowData?: Record<string, any>) => {
|
||||||
|
if (!(await refreshLookupsForRow(instance, rowData))) return
|
||||||
|
|
||||||
|
applyLookupDisplayValues(await loadLookupDisplayValues(getLookupColumns()))
|
||||||
|
instance.repaint?.()
|
||||||
|
},
|
||||||
|
[applyLookupDisplayValues, getLookupColumns, loadLookupDisplayValues, refreshLookupsForRow],
|
||||||
|
)
|
||||||
|
|
||||||
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: cardViewRef })
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: cardViewRef })
|
||||||
const dataSource = useMemo<CustomStore<any, any> | undefined>(() => {
|
const dataSource = useMemo<CustomStore<any, any> | undefined>(() => {
|
||||||
if (!gridDto) return undefined
|
if (!gridDto) return undefined
|
||||||
|
|
@ -1102,8 +1119,8 @@ const CardView = (props: CardViewProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
|
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
|
||||||
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
|
<WidgetGroup widgetGroups={widgets} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Container className={DX_CLASSNAMES}>
|
<Container className={DX_CLASSNAMES}>
|
||||||
|
|
@ -1162,9 +1179,15 @@ const CardView = (props: CardViewProps) => {
|
||||||
{ placement: 'bottom-end' },
|
{ placement: 'bottom-end' },
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
onCardInserted={() => refreshParentData?.()}
|
onCardInserted={(e) => {
|
||||||
onCardUpdated={() => refreshParentData?.()}
|
onCardChanged()
|
||||||
onCardRemoved={() => refreshParentData?.()}
|
void refreshCardLookups(e.component, e.data)
|
||||||
|
}}
|
||||||
|
onCardUpdated={(e) => {
|
||||||
|
onCardChanged()
|
||||||
|
void refreshCardLookups(e.component, e.data)
|
||||||
|
}}
|
||||||
|
onCardRemoved={onCardChanged}
|
||||||
>
|
>
|
||||||
<CardHeader template={renderCardHeader} />
|
<CardHeader template={renderCardHeader} />
|
||||||
<Editing
|
<Editing
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import { useFilters } from './useFilters'
|
||||||
import {
|
import {
|
||||||
useListFormCustomSources,
|
useListFormCustomSources,
|
||||||
useListFormGridDto,
|
useListFormGridDto,
|
||||||
|
useListFormWidgets,
|
||||||
useWidgetGroupHeight,
|
useWidgetGroupHeight,
|
||||||
} from './shared/hooks'
|
} from './shared/hooks'
|
||||||
import GridFilterDialogs from './GridFilterDialogs'
|
import GridFilterDialogs from './GridFilterDialogs'
|
||||||
|
|
@ -101,7 +102,8 @@ const GanttView = (props: GanttViewProps) => {
|
||||||
const pendingStateRef = useRef<Record<string, any> | undefined>(undefined)
|
const pendingStateRef = useRef<Record<string, any> | undefined>(undefined)
|
||||||
const loadedStateKeyRef = useRef('')
|
const loadedStateKeyRef = useRef('')
|
||||||
|
|
||||||
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
|
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
|
||||||
|
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
|
||||||
useListFormCustomSources(gridDto)
|
useListFormCustomSources(gridDto)
|
||||||
|
|
||||||
const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({
|
const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({
|
||||||
|
|
@ -249,9 +251,10 @@ const GanttView = (props: GanttViewProps) => {
|
||||||
stylingMode: 'icon',
|
stylingMode: 'icon',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
gridRef.current?.instance()?.refresh()
|
gridRef.current?.instance()?.refresh()
|
||||||
|
void refreshWidgets()
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[translate],
|
[translate, refreshWidgets],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Gantt'in kendi sutun secicisi yok; ici TreeList oldugu icin onun secicisi acilir.
|
// Gantt'in kendi sutun secicisi yok; ici TreeList oldugu icin onun secicisi acilir.
|
||||||
|
|
@ -424,8 +427,8 @@ const GanttView = (props: GanttViewProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
|
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
|
||||||
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
|
<WidgetGroup widgetGroups={widgets} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Container className={DX_CLASSNAMES}>
|
<Container className={DX_CLASSNAMES}>
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ import {
|
||||||
useExtraFilters,
|
useExtraFilters,
|
||||||
useListFormCustomSources,
|
useListFormCustomSources,
|
||||||
useListFormGridDto,
|
useListFormGridDto,
|
||||||
|
useListFormWidgets,
|
||||||
useWidgetGroupHeight,
|
useWidgetGroupHeight,
|
||||||
} from './shared/hooks'
|
} from './shared/hooks'
|
||||||
import { startWorkflowForInsertedRow } from './shared/workflowRows'
|
import { startWorkflowForInsertedRow } from './shared/workflowRows'
|
||||||
|
|
@ -127,7 +128,8 @@ const Grid = (props: GridProps) => {
|
||||||
entityId: string
|
entityId: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
|
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
|
||||||
|
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
|
||||||
useListFormCustomSources(gridDto)
|
useListFormCustomSources(gridDto)
|
||||||
|
|
||||||
const getGridInstance = useCallback(() => gridRef.current?.instance(), [])
|
const getGridInstance = useCallback(() => gridRef.current?.instance(), [])
|
||||||
|
|
@ -175,7 +177,7 @@ const Grid = (props: GridProps) => {
|
||||||
[gridDto, listFormCode],
|
[gridDto, listFormCode],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { getBandedColumns } = useListFormColumns({
|
const { getBandedColumns, refreshLookupsForRow } = useListFormColumns({
|
||||||
gridDto,
|
gridDto,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
isSubForm,
|
isSubForm,
|
||||||
|
|
@ -205,7 +207,8 @@ const Grid = (props: GridProps) => {
|
||||||
const refreshData = useCallback(() => {
|
const refreshData = useCallback(() => {
|
||||||
const grid = getGridInstance()
|
const grid = getGridInstance()
|
||||||
Promise.resolve(grid?.refresh()).finally(() => updateWorkflowApprovalButtons(grid))
|
Promise.resolve(grid?.refresh()).finally(() => updateWorkflowApprovalButtons(grid))
|
||||||
}, [getGridInstance, updateWorkflowApprovalButtons])
|
void refreshWidgets()
|
||||||
|
}, [getGridInstance, updateWorkflowApprovalButtons, refreshWidgets])
|
||||||
|
|
||||||
const getSelectedRowKeys = useCallback(
|
const getSelectedRowKeys = useCallback(
|
||||||
async () => (await getGridInstance()?.getSelectedRowKeys()) ?? [],
|
async () => (await getGridInstance()?.getSelectedRowKeys()) ?? [],
|
||||||
|
|
@ -559,8 +562,8 @@ const Grid = (props: GridProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
|
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
|
||||||
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
|
<WidgetGroup widgetGroups={widgets} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Container className={DX_CLASSNAMES}>
|
<Container className={DX_CLASSNAMES}>
|
||||||
|
|
@ -623,9 +626,18 @@ const Grid = (props: GridProps) => {
|
||||||
onRowInserted={(e) => {
|
onRowInserted={(e) => {
|
||||||
startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshGrid)
|
startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshGrid)
|
||||||
props.refreshData?.()
|
props.refreshData?.()
|
||||||
|
void refreshWidgets()
|
||||||
|
void refreshLookupsForRow(e.component, e.data)
|
||||||
|
}}
|
||||||
|
onRowUpdated={(e) => {
|
||||||
|
props.refreshData?.()
|
||||||
|
void refreshWidgets()
|
||||||
|
void refreshLookupsForRow(e.component, e.data)
|
||||||
|
}}
|
||||||
|
onRowRemoved={() => {
|
||||||
|
props.refreshData?.()
|
||||||
|
void refreshWidgets()
|
||||||
}}
|
}}
|
||||||
onRowUpdated={() => props.refreshData?.()}
|
|
||||||
onRowRemoved={() => props.refreshData?.()}
|
|
||||||
onEditorPreparing={onEditorPreparing}
|
onEditorPreparing={onEditorPreparing}
|
||||||
>
|
>
|
||||||
<Export
|
<Export
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,12 @@ import {
|
||||||
} from './shared/editPopup'
|
} from './shared/editPopup'
|
||||||
import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
|
import { getValueByField, resolveEditorType, withEditorTypeDefaults } from './shared/editingForm'
|
||||||
import { flattenGridColumns } from './shared/columns'
|
import { flattenGridColumns } from './shared/columns'
|
||||||
import { useListFormCustomSources, useListFormGridDto, useWidgetGroupHeight } from './shared/hooks'
|
import {
|
||||||
|
useListFormCustomSources,
|
||||||
|
useListFormGridDto,
|
||||||
|
useListFormWidgets,
|
||||||
|
useWidgetGroupHeight,
|
||||||
|
} from './shared/hooks'
|
||||||
|
|
||||||
interface SchedulerViewProps {
|
interface SchedulerViewProps {
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
|
|
@ -68,7 +73,8 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({})
|
const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({})
|
||||||
const layout = layoutTypes.scheduler || 'scheduler'
|
const layout = layoutTypes.scheduler || 'scheduler'
|
||||||
|
|
||||||
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
|
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
|
||||||
|
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
|
||||||
useListFormCustomSources(gridDto)
|
useListFormCustomSources(gridDto)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -161,7 +167,8 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
dataSource.reload()
|
dataSource.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
void refreshWidgets()
|
||||||
|
}, [refreshWidgets])
|
||||||
|
|
||||||
const onCurrentViewChange = useCallback((value: string) => {
|
const onCurrentViewChange = useCallback((value: string) => {
|
||||||
setCurrentView(value)
|
setCurrentView(value)
|
||||||
|
|
@ -769,8 +776,8 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
|
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
|
||||||
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
|
<WidgetGroup widgetGroups={widgets} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Container className={DX_CLASSNAMES}>
|
<Container className={DX_CLASSNAMES}>
|
||||||
|
|
@ -820,6 +827,9 @@ const SchedulerView = (props: SchedulerViewProps) => {
|
||||||
onAppointmentDeleting={() => {
|
onAppointmentDeleting={() => {
|
||||||
props.refreshData?.()
|
props.refreshData?.()
|
||||||
}}
|
}}
|
||||||
|
onAppointmentAdded={() => void refreshWidgets()}
|
||||||
|
onAppointmentUpdated={() => void refreshWidgets()}
|
||||||
|
onAppointmentDeleted={() => void refreshWidgets()}
|
||||||
height={
|
height={
|
||||||
gridDto.gridOptions.height > 0
|
gridDto.gridOptions.height > 0
|
||||||
? gridDto.gridOptions.height
|
? gridDto.gridOptions.height
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ import {
|
||||||
useExtraFilters,
|
useExtraFilters,
|
||||||
useListFormCustomSources,
|
useListFormCustomSources,
|
||||||
useListFormGridDto,
|
useListFormGridDto,
|
||||||
|
useListFormWidgets,
|
||||||
useWidgetGroupHeight,
|
useWidgetGroupHeight,
|
||||||
} from './shared/hooks'
|
} from './shared/hooks'
|
||||||
import { startWorkflowForInsertedRow } from './shared/workflowRows'
|
import { startWorkflowForInsertedRow } from './shared/workflowRows'
|
||||||
|
|
@ -117,7 +118,8 @@ const Tree = (props: TreeProps) => {
|
||||||
entityId: string
|
entityId: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
|
const { widgets, refreshWidgets } = useListFormWidgets(listFormCode, gridDto)
|
||||||
|
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, widgets)
|
||||||
useListFormCustomSources(gridDto)
|
useListFormCustomSources(gridDto)
|
||||||
|
|
||||||
const getTreeInstance = useCallback(() => gridRef.current?.instance(), [])
|
const getTreeInstance = useCallback(() => gridRef.current?.instance(), [])
|
||||||
|
|
@ -165,7 +167,7 @@ const Tree = (props: TreeProps) => {
|
||||||
[gridDto, listFormCode],
|
[gridDto, listFormCode],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { getBandedColumns } = useListFormColumns({
|
const { getBandedColumns, refreshLookupsForRow } = useListFormColumns({
|
||||||
gridDto,
|
gridDto,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
isSubForm,
|
isSubForm,
|
||||||
|
|
@ -195,7 +197,8 @@ const Tree = (props: TreeProps) => {
|
||||||
const refreshData = useCallback(() => {
|
const refreshData = useCallback(() => {
|
||||||
const tree = getTreeInstance()
|
const tree = getTreeInstance()
|
||||||
Promise.resolve(tree?.refresh()).finally(() => updateWorkflowApprovalButtons(tree))
|
Promise.resolve(tree?.refresh()).finally(() => updateWorkflowApprovalButtons(tree))
|
||||||
}, [getTreeInstance, updateWorkflowApprovalButtons])
|
void refreshWidgets()
|
||||||
|
}, [getTreeInstance, updateWorkflowApprovalButtons, refreshWidgets])
|
||||||
|
|
||||||
const getSelectedRowKeys = useCallback(
|
const getSelectedRowKeys = useCallback(
|
||||||
async () => (await getTreeInstance()?.getSelectedRowKeys()) ?? [],
|
async () => (await getTreeInstance()?.getSelectedRowKeys()) ?? [],
|
||||||
|
|
@ -535,8 +538,8 @@ const Tree = (props: TreeProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
|
<div ref={widgetGroupRef} className={widgets.length > 0 ? 'mt-2' : ''}>
|
||||||
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
|
<WidgetGroup widgetGroups={widgets} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Container className={DX_CLASSNAMES}>
|
<Container className={DX_CLASSNAMES}>
|
||||||
|
|
@ -605,9 +608,18 @@ const Tree = (props: TreeProps) => {
|
||||||
onRowInserted={(e) => {
|
onRowInserted={(e) => {
|
||||||
startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshTree)
|
startWorkflowForInsertedRow(e, gridDto, listFormCode, refreshTree)
|
||||||
props.refreshData?.()
|
props.refreshData?.()
|
||||||
|
void refreshWidgets()
|
||||||
|
void refreshLookupsForRow(e.component, e.data)
|
||||||
|
}}
|
||||||
|
onRowUpdated={(e) => {
|
||||||
|
props.refreshData?.()
|
||||||
|
void refreshWidgets()
|
||||||
|
void refreshLookupsForRow(e.component, e.data)
|
||||||
|
}}
|
||||||
|
onRowRemoved={() => {
|
||||||
|
props.refreshData?.()
|
||||||
|
void refreshWidgets()
|
||||||
}}
|
}}
|
||||||
onRowUpdated={() => props.refreshData?.()}
|
|
||||||
onRowRemoved={() => props.refreshData?.()}
|
|
||||||
onEditorPreparing={onEditorPreparing}
|
onEditorPreparing={onEditorPreparing}
|
||||||
onContentReady={(e) => {
|
onContentReady={(e) => {
|
||||||
updateWorkflowApprovalButtons(e.component)
|
updateWorkflowApprovalButtons(e.component)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Liste görünümlerinin paylaştığı küçük React hook'ları.
|
* Liste görünümlerinin paylaştığı küçük React hook'ları.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import type { RefObject } from 'react'
|
import type { RefObject } from 'react'
|
||||||
import type { GridDto } from '@/proxy/form/models'
|
import type { GridDto } from '@/proxy/form/models'
|
||||||
import { getList } from '@/services/form.service'
|
import { getList } from '@/services/form.service'
|
||||||
|
|
@ -48,6 +48,37 @@ export const useListFormGridDto = (listFormCode: string, externalGridDto?: GridD
|
||||||
return gridDto
|
return gridDto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget değerleri GridDto ile bir kez hesaplanır; görünüm yenilendiğinde ya da satır
|
||||||
|
* eklendiğinde/değiştiğinde değerler bayatlamasın diye `refreshWidgets` yalnızca
|
||||||
|
* widget'ları yeniden çeker. GridDto'nun kendisi değiştirilmez: yeni referans kolonları
|
||||||
|
* ve veri kaynağını baştan kurar, görünüm durumunu (sayfa, seçim, açık satır) sıfırlardı.
|
||||||
|
*/
|
||||||
|
export const useListFormWidgets = (listFormCode: string, gridDto?: GridDto) => {
|
||||||
|
const [state, setState] = useState<{ source?: GridDto; widgets: GridDto['widgets'] }>({
|
||||||
|
widgets: [],
|
||||||
|
})
|
||||||
|
const requestIdRef = useRef(0)
|
||||||
|
|
||||||
|
const widgets = state.source === gridDto ? state.widgets : (gridDto?.widgets ?? [])
|
||||||
|
const hasWidgets = (gridDto?.widgets?.length ?? 0) > 0
|
||||||
|
|
||||||
|
const refreshWidgets = useCallback(async () => {
|
||||||
|
if (!listFormCode || !gridDto || !hasWidgets) return
|
||||||
|
|
||||||
|
const requestId = ++requestIdRef.current
|
||||||
|
try {
|
||||||
|
const response = await getList({ listFormCode })
|
||||||
|
if (requestId !== requestIdRef.current) return
|
||||||
|
setState({ source: gridDto, widgets: response.data?.widgets ?? [] })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Widget refresh error:', error)
|
||||||
|
}
|
||||||
|
}, [listFormCode, gridDto, hasWidgets])
|
||||||
|
|
||||||
|
return { widgets, refreshWidgets }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WidgetGroup alanının yüksekliğini izler; `fullHeight` grid'lerinin
|
* WidgetGroup alanının yüksekliğini izler; `fullHeight` grid'lerinin
|
||||||
* `calc(100vh - …)` hesabında kullanılır.
|
* `calc(100vh - …)` hesabında kullanılır.
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@ import {
|
||||||
UiLookupDataSourceTypeEnum,
|
UiLookupDataSourceTypeEnum,
|
||||||
} from '@/proxy/form/models'
|
} from '@/proxy/form/models'
|
||||||
import { addCss, getEditingFormFields } from './Utils'
|
import { addCss, getEditingFormFields } from './Utils'
|
||||||
import { cachedLookupLoad, clearLookupCache } from '../shared/lookupCache'
|
import {
|
||||||
|
cachedLookupLoad,
|
||||||
|
clearLookupCache,
|
||||||
|
clearLookupCacheByPrefix,
|
||||||
|
} from '../shared/lookupCache'
|
||||||
import {
|
import {
|
||||||
NO_IMAGE,
|
NO_IMAGE,
|
||||||
normalizeImageSize,
|
normalizeImageSize,
|
||||||
|
|
@ -269,6 +273,80 @@ const useListFormColumns = ({
|
||||||
[listFormCode, lookupDataSource],
|
[listFormCode, lookupDataSource],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup'ı ekranın kendi kayıt kaynağından beslenen kolonda (`isLookupSelfSourced`, sunucu
|
||||||
|
* hesaplar) kaydedilen yeni değer, açılışta yüklenen lookup listesinde olmadığı için hücrede ve
|
||||||
|
* editörün açılır listesinde görünmez. Başka kaynaktan beslenen lookup kayıtla değişmez; kontrol
|
||||||
|
* yalnızca bu kolonlarda yapılır ve istek yalnızca kaydedilen değer listede yoksa atılır.
|
||||||
|
* Bir kolon yenilendiyse `true` döner.
|
||||||
|
*/
|
||||||
|
const refreshLookupsForRow = useCallback(
|
||||||
|
async (instance: any, rowData?: Record<string, any>) => {
|
||||||
|
if (!instance?.columnOption || !rowData || !gridDto) return false
|
||||||
|
|
||||||
|
const getCachePrefix = (colData: ColumnFormatDto) => {
|
||||||
|
const { lookupDto } = colData
|
||||||
|
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.Query) {
|
||||||
|
return `query:${listFormCode}:${colData.fieldName}:`
|
||||||
|
}
|
||||||
|
if (lookupDto.dataSourceType == UiLookupDataSourceTypeEnum.WebService) {
|
||||||
|
return `api:${lookupDto.lookupQuery}:`
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await Promise.all(
|
||||||
|
gridDto.columnFormats
|
||||||
|
.filter(
|
||||||
|
(colData) =>
|
||||||
|
colData.fieldName &&
|
||||||
|
colData.lookupDto?.dataSourceType &&
|
||||||
|
!colData.lookupDto.cascadeParentFields &&
|
||||||
|
colData.isLookupSelfSourced &&
|
||||||
|
rowData[colData.fieldName] !== undefined &&
|
||||||
|
rowData[colData.fieldName] !== null &&
|
||||||
|
rowData[colData.fieldName] !== '',
|
||||||
|
)
|
||||||
|
.map(async (colData) => {
|
||||||
|
const prefix = getCachePrefix(colData)
|
||||||
|
if (!prefix) return
|
||||||
|
|
||||||
|
const source = lookupDataSource({}, colData, listFormCode)
|
||||||
|
const store =
|
||||||
|
source instanceof CustomStore
|
||||||
|
? source
|
||||||
|
: (source as { store?: CustomStore<any, any> } | undefined)?.store
|
||||||
|
const items = ((await store?.load?.()) ?? []) as Record<string, any>[]
|
||||||
|
const valueExpr = colData.lookupDto.valueExpr?.toLowerCase()
|
||||||
|
const knownValues = new Set(
|
||||||
|
items.map((item) => {
|
||||||
|
const key = Object.keys(item).find((itemKey) => itemKey.toLowerCase() === valueExpr)
|
||||||
|
return key ? String(item[key]) : undefined
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const rawValue = rowData[colData.fieldName!]
|
||||||
|
const values = Array.isArray(rawValue) ? rawValue : String(rawValue).split('|')
|
||||||
|
if (values.every((value) => knownValues.has(String(value)))) return
|
||||||
|
|
||||||
|
clearLookupCacheByPrefix(prefix)
|
||||||
|
// `columnOption('lookup.dataSource', …)` kolonu yeniden kursa da DevExtreme yalnızca
|
||||||
|
// valueMap'i boş lookup'ları yükler; hücre metnini tutan valueMap ancak lookup'ın
|
||||||
|
// kendi update()'i ile yenilenir. CardView'da DevExtreme lookup'ı yoktur; metin
|
||||||
|
// haritasını çağıran taraf yeniden kurar.
|
||||||
|
const lookup = instance.columnOption(colData.fieldName, 'lookup')
|
||||||
|
if (lookup?.update) await lookup.update()
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const changed = updated.some(Boolean)
|
||||||
|
if (changed) instance.repaint?.()
|
||||||
|
return changed
|
||||||
|
},
|
||||||
|
[gridDto, listFormCode, lookupDataSource],
|
||||||
|
)
|
||||||
|
|
||||||
const createLookupStaticDataSource = (
|
const createLookupStaticDataSource = (
|
||||||
load: () => any,
|
load: () => any,
|
||||||
filter: any = null,
|
filter: any = null,
|
||||||
|
|
@ -901,6 +979,7 @@ const useListFormColumns = ({
|
||||||
return {
|
return {
|
||||||
getBandedColumns,
|
getBandedColumns,
|
||||||
loadLookupDisplayValues,
|
loadLookupDisplayValues,
|
||||||
|
refreshLookupsForRow,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -374,11 +374,13 @@ const useListFormCustomDataSource = ({
|
||||||
}
|
}
|
||||||
const updateUrl = getServiceAddress(gridOptions.updateServiceAddress)
|
const updateUrl = getServiceAddress(gridOptions.updateServiceAddress)
|
||||||
|
|
||||||
|
// DevExtreme, update'in döndürdüğü nesneyi onRowUpdated'ın `data`'sı yapar ve
|
||||||
|
// refreshMode reshape/repaint'te store'a iter; axios yanıtı değil gönderilen değerler dönmeli.
|
||||||
return dynamicFetch(updateUrl, 'POST', searchParams, {
|
return dynamicFetch(updateUrl, 'POST', searchParams, {
|
||||||
keys: [toPrimaryKey(key)],
|
keys: [toPrimaryKey(key)],
|
||||||
data: values,
|
data: values,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
})
|
}).then(() => values)
|
||||||
},
|
},
|
||||||
insert: function (values) {
|
insert: function (values) {
|
||||||
if (!gridOptions.insertServiceAddress) {
|
if (!gridOptions.insertServiceAddress) {
|
||||||
|
|
|
||||||
|
|
@ -65,3 +65,10 @@ export const clearLookupCache = (key?: string) => {
|
||||||
lookupCache.delete(key)
|
lookupCache.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Anahtarı verilen önekle başlayan girdileri (ör. bir kolonun bütün filtre varyantları) temizler. */
|
||||||
|
export const clearLookupCacheByPrefix = (prefix: string) => {
|
||||||
|
Array.from(lookupCache.keys())
|
||||||
|
.filter((key) => key.startsWith(prefix))
|
||||||
|
.forEach((key) => lookupCache.delete(key))
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue