Skip to content

Commit 18efeb1

Browse files
committed
Optimize AutoQueryGrid by using a single typeContext to reduce reactive surface area
1 parent c515d0c commit 18efeb1

File tree

5 files changed

+178
-120
lines changed

5 files changed

+178
-120
lines changed

src/components/AutoQueryGrid.vue

Lines changed: 42 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,7 @@
145145
<FilterColumn :definitions="definitions" :column="showFilters.column" :top-left="showFilters.topLeft" @done="onFilterDone" @save="onFilterSave" />
146146
</div>
147147

148-
<DataGrid v-if="results.length" :id="id" :items="results" :type="type" :selected-columns="filteredColumns" class="mt-1"
149-
@filters-changed="update"
148+
<DataGrid v-if="typeContext" :id="id" :items="results" :type="dataModelName" :ctx="typeContext" :selected-columns="filteredColumns" class="mt-1"
150149
:tableStyle="tableStyle" :gridClass="gridClass" :grid2Class="grid2Class" :grid3Class="grid3Class" :grid4Class="grid4Class"
151150
:tableClass="tableClass" :theadClass="theadClass" :theadRowClass="theadRowClass" :theadCellClass="theadCellClass" :tbodyClass="tbodyClass"
152151
:rowClass="getTableRowClass" @row-selected="onRowSelected" :rowStyle="rowStyle"
@@ -177,9 +176,9 @@
177176
import type { JsonServiceClient } from '@servicestack/client'
178177
import type { ApiPrefs, ApiResponse, Column, ColumnSettings, MetadataPropertyType, GridAllowOptions, GridShowOptions } from '@/types'
179178
import type { AutoQueryGridProps, AutoQueryGridEmits } from '@/components/types'
180-
import { computed, inject, nextTick, onMounted, ref, useSlots, getCurrentInstance, type Slots } from 'vue'
179+
import { computed, inject, nextTick, onMounted, ref, shallowRef, markRaw, useSlots, getCurrentInstance, type Slots } from 'vue'
181180
import { ApiResult, appendQueryString, combinePaths, delaySet, leftPart, mapGet, queryString, rightPart } from '@servicestack/client'
182-
import { Apis, createDto, getPrimaryKey, isComplexProp, typeProperties, useMetadata } from '@/use/metadata'
181+
import { Apis, createDto, isComplexProp } from '@/use/metadata'
183182
import { a, grid } from './css'
184183
import { asOptions, asStrings, copyText, getTypeName, parseJson, pushState, uniqueIgnoreCase } from '@/use/utils'
185184
import { canAccess, useAuth } from '@/use/auth'
@@ -206,6 +205,14 @@ const emit = defineEmits<AutoQueryGridEmits>()
206205
207206
const client = inject<JsonServiceClient>('client')!
208207
208+
// Consolidate all type-derived state into a single computed to minimize reactive surface
209+
const ctx = computed(() => markRaw(props.ctx ?? Apis.createContext({
210+
id:props.id,
211+
type:props.type,
212+
apis:props.apis,
213+
})))
214+
215+
209216
const allAllow = 'filtering,queryString,queryFilters'.split(',') as GridAllowOptions[]
210217
const allShow = 'copyApiUrl,downloadCsv,filtersView,newItem,pagingInfo,pagingNav,preferences,refresh,resetPreferences,toolbar,forms'.split(',') as GridShowOptions[]
211218
@@ -239,14 +246,12 @@ function getTableRowClass(item:any, i:number) {
239246
}
240247
241248
const slots: Slots = useSlots()
242-
243-
//const dataModel = computed(() => typeOf(apis.value.AnyQuery!.dataModel.name))
244-
const viewModel = computed(() => typeOf(apis.value.AnyQuery!.viewModel?.name || apis.value.AnyQuery!.dataModel.name))
249+
const slotKeys = Object.keys(slots) as string[]
250+
const slotKeysLower = slotKeys.map(x => x.toLowerCase())
245251
246252
const columnSlots = computed(() => {
247-
const slotColumns = Object.keys(slots).map(x => x.toLowerCase())
248-
return typeProperties(viewModel.value)
249-
.filter(p => slotColumns.includes(p.name.toLowerCase()) || slotColumns.includes(p.name.toLowerCase()+'-header'))
253+
return ctx.value.viewModelProps
254+
.filter(p => slotKeysLower.includes(p.name.toLowerCase()) || slotKeysLower.includes(p.name.toLowerCase()+'-header'))
250255
.map(x => x.name)
251256
})
252257
@@ -262,14 +267,14 @@ function getSelectedColumns() {
262267
const viewModelColumns = computed(() => {
263268
let selectedCols = getSelectedColumns()
264269
let selectedLower = selectedCols.map(x => x.toLowerCase())
265-
const viewProps = typeProperties(viewModel.value)
270+
const viewProps = ctx.value.viewModelProps
266271
return selectedLower.length > 0
267272
? selectedLower.map(x => viewProps.find(p => p.name.toLowerCase() === x)).filter(x => x != null) as MetadataPropertyType[]
268273
: viewProps
269274
})
270275
const filteredColumns = computed(() => {
271276
// Get view columns directly from typeProperties to avoid circular dependency
272-
const viewProps = typeProperties(viewModel.value)
277+
const viewProps = ctx.value.viewModelProps
273278
let selectedCols = getSelectedColumns()
274279
let selectedLower = selectedCols.map(x => x.toLowerCase())
275280
let viewColumns = selectedLower.length > 0
@@ -283,8 +288,8 @@ const filteredColumns = computed(() => {
283288
})
284289
285290
const columns = ref<Column[]>([])
286-
const api = ref<ApiResponse>(new ApiResult<any>())
287-
const editApi = ref<ApiResponse>(new ApiResult<any>())
291+
const api = shallowRef<ApiResponse>(new ApiResult<any>())
292+
const editApi = shallowRef<ApiResponse>(new ApiResult<any>())
288293
289294
const open = ref<"filters"|null>()
290295
const create = ref(false)
@@ -299,11 +304,11 @@ const defaultTake = 25
299304
const apiPrefs = ref<ApiPrefs>({ take:defaultTake })
300305
const apiLoading = ref(false)
301306
302-
const hasPrefs = computed(() => columns.value.some(x => x.settings.filters.length > 0 || !!x.settings.sort)
307+
const hasPrefs = computed(() => columns.value.some(x => x.settings.filters.length > 0 || !!x.settings.sort)
303308
|| apiPrefs.value.selectedColumns)
304309
const filtersCount = computed(() => columns.value.map(x => x.settings.filters.length).reduce((acc,x) => acc + x, 0))
305-
const properties = computed(() => typeProperties(typeOf(typeName.value || apis.value.AnyQuery?.dataModel.name)))
306-
const primaryKey = computed(() => getPrimaryKey(typeOf(typeName.value || apis.value.AnyQuery?.dataModel.name)))
310+
const properties = computed(() => ctx.value.dataModelProps)
311+
const primaryKey = computed(() => ctx.value.dataModelPrimaryKey)
307312
308313
const take = computed(() => apiPrefs.value.take ?? defaultTake)
309314
const results = computed<any[]>(() => (api.value.response ? mapGet(api.value.response, 'results') : null) ?? [])
@@ -321,8 +326,8 @@ const Errors = {
321326
NoQuery: `No Query API was found`
322327
}
323328
324-
defineExpose({
325-
update, search, createRequestArgs, reset, createDone, createSave, editDone, editSave, forceUpdate, setEdit,
329+
defineExpose({
330+
update, search, createRequestArgs, reset, createDone, createSave, editDone, editSave, forceUpdate, setEdit,
326331
edit, createForm, editForm, apiPrefs, results, skip, take, total,
327332
})
328333
@@ -332,10 +337,10 @@ function canFilter(column:string) {
332337
if (column) {
333338
if (props.canFilter)
334339
return props.canFilter(column)
335-
340+
336341
const prop = properties.value.find(x => x.name.toLowerCase() == column.toLowerCase())
337342
if (prop) {
338-
return !isComplexProp(prop)
343+
return !isComplexProp(prop)
339344
}
340345
}
341346
return false
@@ -360,11 +365,11 @@ async function skipTo(value:number) {
360365
await update()
361366
}
362367
363-
async function setEditId(pkName:string, pkValue:any) {
368+
async function setEditId(pkName:string, pkValue:any) {
364369
edit.value = null
365370
editId.value = pkValue
366371
if (!pkName || !pkValue) return
367-
372+
368373
let requestDto = createDto(apis.value.AnyQuery!, { [pkName]: pkValue })
369374
const api = await client.api(requestDto)
370375
if (api.succeeded) {
@@ -491,7 +496,7 @@ function createRequestArgs() {
491496
let pk = primaryKey.value
492497
if (pk && !selectedColumns.includes(pk.name))
493498
selectedColumns = [pk.name, ...selectedColumns]
494-
499+
495500
// Include FK Id for [Ref] complex props
496501
const metaProps = properties.value
497502
const refProps:string[] = []
@@ -541,11 +546,12 @@ function createRequestArgs() {
541546
if (typeof qs.skip != 'undefined') {
542547
const num = parseInt(qs.skip)
543548
if (!isNaN(num)) {
544-
skip.value = args.skip = num
549+
args.skip = num
550+
// Avoid mutating reactive state here to prevent re-entrant updates
545551
}
546552
}
547553
}
548-
if (typeof args.skip == 'undefined' && skip.value > 0) {
554+
if (typeof args.skip == 'undefined' && skip.value > 0) {
549555
args.skip = skip.value
550556
}
551557
@@ -589,32 +595,22 @@ function onShowNewItem() {
589595
updateUrl({ create:null })
590596
}
591597
592-
const typeName = computed(() => getTypeName(props.type))
593-
const dataModelName = computed(() => typeName.value || apis.value.AnyQuery?.dataModel.name)
598+
const dataModelName = computed(() => ctx.value.dataModelName)
594599
const modelTitle = computed(() => props.modelTitle || dataModelName.value)
595600
const newButtonLabel = computed(() => props.newButtonLabel || `New ${modelTitle.value}`)
596-
const prefsCacheKey = () => `${props.id}/ApiPrefs/${typeName.value || apis.value.AnyQuery?.dataModel.name}`
597-
const columnCacheKey = (name:string) => `Column/${props.id}:${typeName.value || apis.value.AnyQuery?.dataModel.name}.${name}`
598-
599-
const { metadataApi, typeOf, apiOf, filterDefinitions } = useMetadata()
601+
const prefsCacheKey = () => ctx.value.prefsCacheKey()
602+
const columnCacheKey = (name:string) => ctx.value.columnCacheKey(name)
600603
601604
const { invalidAccessMessage } = useAuth()
602-
const definitions = computed(() => props.filterDefinitions || filterDefinitions.value)
603-
605+
const definitions = computed(() => props.filterDefinitions || ctx.value.filterDefinitions)
604606
605-
const apis = computed(() => {
606-
let opNames = asStrings(props.apis)
607-
return opNames.length > 0
608-
? Apis.from(opNames.map(x => apiOf(x)).filter(x => x != null).map(x => x!))
609-
: Apis.forType(typeName.value, metadataApi.value)
610-
})
607+
const apis = computed(() => ctx.value.apis)
611608
612609
const warn = (msg:string) => `<span class="text-yellow-700">${msg}</span>`
613610
const invalidState = computed(() => {
614-
if (!metadataApi.value)
611+
if (!ctx.value.metadataApi)
615612
return warn(`AppMetadata not loaded, see <a class="${a.blue}" href="https://docs.servicestack.net/vue/use-metadata" target="_blank">useMetadata()</a>`)
616-
let opNames = asStrings(props.apis)
617-
let invalidApis = opNames.map(op => apiOf(op) == null ? op : null).filter(x => x != null)
613+
let invalidApis = ctx.value.invalidApis
618614
if (invalidApis.length > 0)
619615
return warn(`Unknown API${invalidApis.length > 1 ? 's' : ''}: ${invalidApis.join(', ')}`)
620616
let aq = apis.value
@@ -674,7 +670,7 @@ function reset() {
674670
meta: p,
675671
settings: Object.assign({
676672
filters: []
677-
},
673+
},
678674
parseJson(storage.getItem(columnCacheKey(p.name)))
679675
)
680676
}))
@@ -697,12 +693,12 @@ function reset() {
697693
}
698694
if (pkName && props.edit != null) {
699695
setEditId(pkName, props.edit)
700-
}
696+
}
701697
}
702698
703699
onMounted(async () => {
704700
reset()
701+
await nextTick()
705702
await update()
706703
})
707-
708704
</script>

src/components/DataGrid.vue

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div v-if="items.length" ref="refResults" :class="gridClass">
2+
<div v-if="items.length" ref="refResults" :class="gridClass">
33
<div :class="grid2Class">
44
<div :class="grid3Class">
55
<div :class="grid4Class">
@@ -27,8 +27,8 @@
2727
<td v-for="column in visibleColumns" :class="[cellClass(column), grid.tableCellClass]">
2828
<slot v-if="slots[column]" :name="column" v-bind="item"></slot>
2929
<slot v-else-if="slotColumn(column)" :name="slotColumn(column)" v-bind="item"></slot>
30-
<CellFormat v-else-if="columnProp(column)" :type="metaType" :propType="columnProp(column)" :modelValue="item" />
31-
<PreviewFormat v-else :value="mapGet(item,column)" :format="columnFormat(column)" />
30+
<CellFormat v-else-if="columnProp(column)" :type="metaType!" :propType="columnProp(column)" :modelValue="item" />
31+
<PreviewFormat v-else :value="mapGet(item,column)" :format="columnFormat(column)" :modelValue="item" />
3232
</td>
3333
</tr>
3434
</tbody>
@@ -40,14 +40,15 @@
4040
</template>
4141

4242
<script setup lang="ts">
43+
defineOptions({ inheritAttrs: false })
4344
4445
import type { Breakpoint, FormatInfo, MetadataPropertyType } from '@/types'
4546
import type { DataGridProps, DataGridEmits } from '@/components/types'
4647
47-
import { form, grid } from './css'
48-
import { computed, ref, useSlots, type Slots, type StyleValue } from 'vue'
48+
import { grid } from './css'
49+
import { computed, ref, useSlots, type Slots } from 'vue'
4950
import { humanify, map, uniqueKeys, mapGet } from '@servicestack/client'
50-
import { useMetadata } from '@/use/metadata'
51+
import { typeOf, typeProperties } from '@/use/metadata'
5152
import { getTypeName } from '@/use/utils'
5253
5354
const props = withDefaults(defineProps<DataGridProps>(), {
@@ -63,18 +64,20 @@ const showFilters = ref<string|null>(null)
6364
const isOpen = (column:string) => showFilters.value === column
6465
6566
const slots:Slots = useSlots()
66-
const slotHeader = (column:string) => Object.keys(slots).find(x => x.toLowerCase() == column.toLowerCase()+'-header')
67-
const slotColumn = (column:string) => Object.keys(slots).find(x => x.toLowerCase() == column.toLowerCase())
68-
const columnSlots = computed(() => uniqueKeys(props.items).filter(k => !!(slots[k] || slots[k+'-header'])))
67+
const slotKeys = Object.keys(slots) as string[]
68+
const slotKeysLower = slotKeys.map(x => x.toLowerCase())
69+
const hasSlotName = (name:string) => slotKeysLower.includes(name.toLowerCase())
70+
const slotHeader = (column:string) => slotKeys.find(x => x.toLowerCase() == column.toLowerCase()+'-header')
71+
const slotColumn = (column:string) => slotKeys.find(x => x.toLowerCase() == column.toLowerCase())
72+
const columnSlots = computed(() => uniqueKeys(props.items).filter(k => hasSlotName(k) || hasSlotName(k+'-header')))
6973
70-
const { typeOf, typeProperties } = useMetadata()
71-
const typeName = computed(() => getTypeName(props.type))
72-
const metaType = computed(() => typeOf(typeName.value))
73-
const typeProps = computed(() => typeProperties(metaType.value))
74+
const typeName = computed(() => props.ctx?.dataModelName || getTypeName(props.type))
75+
const metaType = computed(() => props.ctx?.dataModel || typeOf(typeName.value))
76+
const typeProps = computed(() => props.ctx?.dataModelProps || typeProperties(metaType.value))
7477
7578
function headerFormat(column:string) {
7679
const title = props.headerTitles && mapGet(props.headerTitles,column) || column
77-
return props.headerTitle
80+
return props.headerTitle
7881
? props.headerTitle(title)
7982
: humanify(title)
8083
}
@@ -93,7 +96,7 @@ function columnFormat(column:string) {
9396
return null
9497
}
9598
96-
const cellBreakpoints = {
99+
const cellBreakpoints = {
97100
xs:'xs:table-cell',
98101
sm:'sm:table-cell',
99102
md:'md:table-cell',
@@ -118,7 +121,7 @@ const theadRowClass = computed(() => props.theadRowClass ?? grid.getTheadRowClas
118121
const theadCellClass = computed(() => props.theadCellClass ?? grid.getTheadCellClass(props.tableStyle))
119122
120123
function getTableRowClass(item:any, i:number) {
121-
return props.rowClass
124+
return props.rowClass
122125
? props.rowClass(item, i)
123126
: grid.getTableRowClass(props.tableStyle, i, props.isSelected && props.isSelected(item) ? true : false, props.isSelected != null)
124127
}
@@ -129,10 +132,10 @@ function getTableRowStyle(item:any, i:number) {
129132
}
130133
131134
const visibleColumns = computed(() => {
132-
const ret = (typeof props.selectedColumns == 'string' ? props.selectedColumns.split(',') : props.selectedColumns) ||
135+
const ret = (typeof props.selectedColumns == 'string' ? props.selectedColumns.split(',') : props.selectedColumns) ||
133136
(columnSlots.value.length > 0 ? columnSlots.value : uniqueKeys(props.items))
134-
135-
const formatMap = typeProps.value.reduce((acc:{[k:string]:FormatInfo|undefined},x:MetadataPropertyType) =>
137+
138+
const formatMap = typeProps.value.reduce((acc:{[k:string]:FormatInfo|undefined},x:MetadataPropertyType) =>
136139
{ acc[x.name!.toLowerCase()] = x.format; return acc }, {})
137140
return ret.filter(x => formatMap[x.toLowerCase()]?.method != 'hidden')
138141
})

src/components/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export interface OutlineButtonProps {
118118

119119
// Form Components
120120
export interface AutoFormProps {
121-
type: string|InstanceType<any>|Function
121+
type?: string|InstanceType<any>|Function
122122
modelValue?: ApiRequest|any
123123
heading?: string
124124
subHeading?: string
@@ -262,6 +262,7 @@ export type LookupInputEmits = EmitsUpdateModelValue<any>
262262
export interface DataGridProps {
263263
items: any[]
264264
id?: string
265+
ctx?: ReturnType<typeof Apis.createContext>
265266
type?: string|InstanceType<any>|Function
266267
tableStyle?: TableStyleOptions
267268
selectedColumns?:string[]|string
@@ -290,6 +291,7 @@ export interface DataGridEmits {
290291
export interface AutoQueryGridProps {
291292
filterDefinitions?: AutoQueryConvention[]
292293
id?: string
294+
ctx?: ReturnType<typeof Apis.createContext>
293295
apis?: string|string[]
294296
type?: string|InstanceType<any>|Function
295297
prefs?: ApiPrefs

src/use/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AppMetadata, AuthenticateResponse, AutoQueryGridDefaults, UiConfig } from "@/types"
2-
import { ref, computed, type Component } from "vue"
2+
import { ref, shallowRef, computed, type Component } from "vue"
33
import { getFormatters } from "./formatters"
44
import { enumFlagsConverter } from "./metadata"
55
import { createBus, toKebabCase } from "@servicestack/client"
@@ -73,7 +73,7 @@ export class Sole {
7373

7474
static events = createBus()
7575
static user = ref<AuthenticateResponse|null>(null)
76-
static metadata = ref<AppMetadata|null>(null)
76+
static metadata = shallowRef<AppMetadata|null>(null)
7777
static components:{[k:string]:Component} = {
7878
RouterLink,
7979
}
@@ -122,7 +122,7 @@ export function useConfig() {
122122
const events = Sole.events
123123

124124
return {
125-
config, setConfig, events,
125+
Sole, config, setConfig, events,
126126
autoQueryGridDefaults, setAutoQueryGridDefaults,
127127
assetsPathResolver, fallbackPathResolver,
128128
registerInterceptor,

0 commit comments

Comments
 (0)