Skip to content

Commit c177dca

Browse files
committed
more feature complete + improve compatibility with ServiceStack Vue
1 parent 84454b5 commit c177dca

19 files changed

+457
-76
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
"build": "run-p type-check build-only build-style && run-p build-minify",
2727
"build-minify": "uglifyjs dist/servicestack-react.mjs --compress --mangle --webkit -o dist/servicestack-react.min.mjs",
2828
"build-style": "postcss src/tailwind.css -o dist/styles.css",
29-
"build-copy": "npm run build && rm -rf /home/mythz/src/NetCoreTemplates/react-spa/MyApp.Client/node_modules/@servicestack/react && mkdir -p /home/mythz/src/NetCoreTemplates/react-spa/MyApp.Client/node_modules/@servicestack/react/dist && cp ./dist/* /home/mythz/src/NetCoreTemplates/react-spa/MyApp.Client/node_modules/@servicestack/react/dist/ && cp package.json /home/mythz/src/NetCoreTemplates/react-spa/MyApp.Client/node_modules/@servicestack/react/",
29+
"build-copy": "npm run build && npm run copy-react-spa && npm run copy-react-vite",
30+
"copy-react-spa": "rm /home/mythz/src/NetCoreTemplates/react-spa/MyApp.Client/node_modules/@servicestack/react/dist/* && cp ./dist/* /home/mythz/src/NetCoreTemplates/react-spa/MyApp.Client/node_modules/@servicestack/react/dist/ && rm -rf /home/mythz/src/NetCoreTemplates/react-spa/MyApp.Client/node_modules/.vite",
31+
"copy-react-vite": "rm /home/mythz/src/NetCoreTemplates/react-vite/MyApp.Client/node_modules/@servicestack/react/dist/* && cp ./dist/* /home/mythz/src/NetCoreTemplates/react-vite/MyApp.Client/node_modules/@servicestack/react/dist/ && rm -rf /home/mythz/src/NetCoreTemplates/react-vite/MyApp.Client/node_modules/.vite",
3032
"preview": "vite preview",
3133
"build-only": "vite build -l error",
3234
"type-check": "tsc --noEmit",

src/components/Alert.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ export default function Alert({ type = "warn", hideIcon, className, children }:
5353
</div>
5454
)}
5555
<div>
56-
<p className={`${textColor} text-sm`}>
56+
<div className={`${textColor} text-sm`}>
5757
{children}
58-
</p>
58+
</div>
5959
</div>
6060
</div>
6161
</div>

src/components/AutoCreateForm.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,13 @@ const AutoCreateForm = forwardRef<AutoCreateFormRef, AutoCreateFormProps & AutoC
112112
}, [forceUpdate])
113113

114114
const update = useCallback((value: ApiRequest) => {
115-
// Model update handled internally
115+
setModel(prev => {
116+
// Mutate the DTO instance to preserve prototype methods like getTypeName()
117+
Object.assign(prev, value)
118+
// Create new instance with same prototype to trigger React re-render
119+
const ctor = prev.constructor as any
120+
return new ctor(prev)
121+
})
116122
}, [])
117123

118124
const openModal = useCallback((info: { name: string } & any, done: (result: any) => any) => {
@@ -210,7 +216,7 @@ const AutoCreateForm = forwardRef<AutoCreateFormRef, AutoCreateFormProps & AutoC
210216
}
211217

212218
useEffect(() => {
213-
doTransition(rule1, { value: transition1 } as any, show)
219+
doTransition(rule1, setTransition1, show)
214220
if (!show) {
215221
const timer = setTimeout(done, 700)
216222
return () => clearTimeout(timer)
@@ -277,6 +283,7 @@ const AutoCreateForm = forwardRef<AutoCreateFormRef, AutoCreateFormProps & AutoC
277283
<AutoFormFields
278284
ref={formFieldsRef}
279285
key={formFieldsKey}
286+
type={typeName}
280287
value={model}
281288
onChange={update}
282289
api={api}
@@ -309,6 +316,7 @@ const AutoCreateForm = forwardRef<AutoCreateFormRef, AutoCreateFormProps & AutoC
309316
<AutoFormFields
310317
ref={formFieldsRef}
311318
key={formFieldsKey}
319+
type={typeName}
312320
value={model}
313321
onChange={update}
314322
api={api}

src/components/AutoEditForm.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,13 @@ const AutoEditForm = forwardRef<AutoEditFormRef, AutoEditFormProps & AutoEditFor
117117
}, [])
118118

119119
const update = useCallback((value: ApiRequest) => {
120-
// Model update handled internally
120+
setModel(prev => {
121+
// Mutate the DTO instance to preserve prototype methods like getTypeName()
122+
Object.assign(prev, value)
123+
// Create new instance with same prototype to trigger React re-render
124+
const ctor = prev.constructor as any
125+
return new ctor(prev)
126+
})
121127
}, [])
122128

123129
const openModal = useCallback((info: { name: string } & any, done: (result: any) => any) => {
@@ -317,7 +323,7 @@ const AutoEditForm = forwardRef<AutoEditFormRef, AutoEditFormProps & AutoEditFor
317323
}
318324

319325
useEffect(() => {
320-
doTransition(rule1, { value: transition1 } as any, show)
326+
doTransition(rule1, setTransition1, show)
321327
if (!show) {
322328
const timer = setTimeout(done, 700)
323329
return () => clearTimeout(timer)
@@ -384,6 +390,7 @@ const AutoEditForm = forwardRef<AutoEditFormRef, AutoEditFormProps & AutoEditFor
384390
<AutoFormFields
385391
ref={formFieldsRef}
386392
key={formFieldsKey}
393+
type={typeName}
387394
value={model}
388395
onChange={update}
389396
api={api}
@@ -416,6 +423,7 @@ const AutoEditForm = forwardRef<AutoEditFormRef, AutoEditFormProps & AutoEditFor
416423
<AutoFormFields
417424
ref={formFieldsRef}
418425
key={formFieldsKey}
426+
type={typeName}
419427
value={model}
420428
onChange={update}
421429
api={api}

src/components/AutoQueryGrid.tsx

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export const AutoQueryGrid = forwardRef<AutoQueryGridRef, AutoQueryGridComponent
194194
}, [props.rowClass, props.tableStyle, editId])
195195

196196
const { metadataApi, typeOf, apiOf, filterDefinitions } = useMetadata()
197-
const { invalidAccessMessage } = useAuth()
197+
const { invalidAccessMessage, user } = useAuth()
198198

199199
const typeName = useMemo(() => getTypeName(props.type), [props.type])
200200

@@ -285,13 +285,13 @@ export const AutoQueryGrid = forwardRef<AutoQueryGridRef, AutoQueryGridComponent
285285
return null
286286
}, [metadataApi, props.apis, apis, apiOf])
287287

288-
const invalidAccess = useMemo(() => apis.AnyQuery && invalidAccessMessage(apis.AnyQuery), [apis.AnyQuery, invalidAccessMessage])
289-
const invalidCreateAccess = useMemo(() => apis.Create && invalidAccessMessage(apis.Create), [apis.Create, invalidAccessMessage])
290-
const invalidUpdateAccess = useMemo(() => apis.AnyUpdate && invalidAccessMessage(apis.AnyUpdate), [apis.AnyUpdate, invalidAccessMessage])
288+
const invalidAccess = useMemo(() => apis.AnyQuery && invalidAccessMessage(apis.AnyQuery), [apis.AnyQuery, invalidAccessMessage, user])
289+
const invalidCreateAccess = useMemo(() => apis.Create && invalidAccessMessage(apis.Create), [apis.Create, invalidAccessMessage, user])
290+
const invalidUpdateAccess = useMemo(() => apis.AnyUpdate && invalidAccessMessage(apis.AnyUpdate), [apis.AnyUpdate, invalidAccessMessage, user])
291291

292-
const canCreate = useMemo(() => canAccess(apis.Create), [apis.Create])
293-
const canUpdate = useMemo(() => canAccess(apis.AnyUpdate), [apis.AnyUpdate])
294-
const canDelete = useMemo(() => canAccess(apis.Delete), [apis.Delete])
292+
const canCreate = useMemo(() => canAccess(apis.Create), [apis.Create, user])
293+
const canUpdate = useMemo(() => canAccess(apis.AnyUpdate), [apis.AnyUpdate, user])
294+
const canDelete = useMemo(() => canAccess(apis.Delete), [apis.Delete, user])
295295

296296
// Helper functions
297297
const prefsCacheKey = useCallback(() =>
@@ -388,21 +388,6 @@ export const AutoQueryGrid = forwardRef<AutoQueryGridRef, AutoQueryGridComponent
388388
setShowFilters(null)
389389
}, [])
390390

391-
const onFilterSave = useCallback(async (settings: ColumnSettings) => {
392-
let column = showFilters?.column
393-
if (column) {
394-
column.settings = settings
395-
storage.setItem(columnCacheKey(column.name), JSON.stringify(column.settings))
396-
await update()
397-
}
398-
setShowFilters(null)
399-
}, [showFilters, columnCacheKey, storage])
400-
401-
const filtersChanged = useCallback(async (column: Column) => {
402-
storage.setItem(columnCacheKey(column.name), JSON.stringify(column.settings))
403-
await update()
404-
}, [columnCacheKey, storage])
405-
406391
const saveApiPrefs = useCallback(async (prefs: ApiPrefs) => {
407392
setShowQueryPrefs(false)
408393
setApiPrefs(prefs)
@@ -535,6 +520,39 @@ export const AutoQueryGrid = forwardRef<AutoQueryGridRef, AutoQueryGridComponent
535520
await update()
536521
}, [update])
537522

523+
const onFilterSave = useCallback(async (settings: ColumnSettings) => {
524+
let column = showFilters?.column
525+
if (column) {
526+
// Update the column object directly first (for immediate use in createRequestArgs)
527+
column.settings = settings
528+
storage.setItem(columnCacheKey(column.name), JSON.stringify(settings))
529+
530+
// Then update the columns state immutably to trigger re-render
531+
setColumns(prevColumns => prevColumns.map(col =>
532+
col.name === column.name
533+
? { ...col, settings }
534+
: col
535+
))
536+
537+
await update()
538+
}
539+
setShowFilters(null)
540+
}, [showFilters, columnCacheKey, storage, update])
541+
542+
const filtersChanged = useCallback(async (column: Column) => {
543+
// Column is already mutated by the caller, just save to storage
544+
storage.setItem(columnCacheKey(column.name), JSON.stringify(column.settings))
545+
546+
// Update the columns state immutably to trigger re-render
547+
setColumns(prevColumns => prevColumns.map(col =>
548+
col.name === column.name
549+
? { ...col, settings: column.settings }
550+
: col
551+
))
552+
553+
await update()
554+
}, [columnCacheKey, storage, update])
555+
538556
const downloadCsv = useCallback(() => {
539557
const apiUrl = createApiUrl("csv")
540558
copyText(apiUrl)
@@ -559,14 +577,22 @@ export const AutoQueryGrid = forwardRef<AutoQueryGridRef, AutoQueryGridComponent
559577
}, [createRequestArgs, apis, client.baseUrl])
560578

561579
const resetPreferences = useCallback(async () => {
580+
// Mutate columns directly first (for immediate use in createRequestArgs)
562581
columns.forEach(column => {
563582
column.settings = { filters: [] }
564583
storage.removeItem(columnCacheKey(column.name))
565584
})
585+
586+
// Then update columns state immutably to trigger re-render
587+
setColumns(prevColumns => prevColumns.map(column => ({
588+
...column,
589+
settings: { filters: [] }
590+
})))
591+
566592
setApiPrefs({ take: defaultTake })
567593
storage.removeItem(prefsCacheKey())
568594
await update()
569-
}, [columns, columnCacheKey, prefsCacheKey, storage, update])
595+
}, [columns, columnCacheKey, prefsCacheKey, storage, update, defaultTake])
570596

571597
const onShowNewItem = useCallback(() => {
572598
setCreate(true)
@@ -1060,15 +1086,32 @@ export const AutoQueryGrid = forwardRef<AutoQueryGridRef, AutoQueryGridComponent
10601086
headerTitles={props.headerTitles}
10611087
visibleFrom={props.visibleFrom}
10621088
onHeaderSelected={handleHeaderSelected}
1089+
slots={{
1090+
header: ({ column, label }: { column: string, label: string }) => {
1091+
if (allow('filtering') && canFilter(column)) {
1092+
const col = columns.find((x: Column) => x.name.toLowerCase() === column.toLowerCase())
1093+
const isOpen = showFilters?.column.name === column
1094+
return (
1095+
<div className="cursor-pointer flex justify-between items-center hover:text-gray-900 dark:hover:text-gray-50">
1096+
<span className="mr-1 select-none">
1097+
{label}
1098+
</span>
1099+
<SettingsIcons column={col} isOpen={isOpen} />
1100+
</div>
1101+
)
1102+
} else {
1103+
return (
1104+
<div className="flex justify-between items-center">
1105+
<span className="mr-1 select-none">{label}</span>
1106+
</div>
1107+
)
1108+
}
1109+
},
1110+
...headerSlots,
1111+
...columnSlots
1112+
}}
10631113
>
1064-
{columnSlots && Object.keys(columnSlots).map(slotName => {
1065-
const slotFn = columnSlots[slotName]
1066-
return (
1067-
<React.Fragment key={slotName}>
1068-
{slotFn({})}
1069-
</React.Fragment>
1070-
)
1071-
})}
1114+
{children}
10721115
</DataGrid>
10731116
)}
10741117
</div>

src/components/AutoViewForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ const AutoViewForm: React.FC<AutoViewFormProps & AutoViewFormSlots> = (props) =>
116116
}
117117

118118
useEffect(() => {
119-
doTransition(rule1, { value: transition1 } as any, show)
119+
doTransition(rule1, setTransition1, show)
120120
if (!show) {
121121
const timer = setTimeout(done, 700)
122122
return () => clearTimeout(timer)

src/components/LookupInput.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ const LookupInput: React.FC<LookupInputProps> = ({
114114
return
115115
}
116116

117-
setRefInfoValue('')
118117
let refIdValue = refInfo.selfId == null
119118
? mapGet(model, prop.name)
120119
: mapGet(model, refInfo.selfId)
@@ -123,8 +122,20 @@ const LookupInput: React.FC<LookupInputProps> = ({
123122
if (isRefType) {
124123
refIdValue = mapGet(model, refInfo.refId)
125124
}
126-
if (refIdValue == null)
125+
if (refIdValue == null) {
126+
setRefInfoValue('')
127127
return
128+
}
129+
130+
// Try to get cached value first
131+
if (refInfo.refLabel != null) {
132+
const cachedLabel = LookupValues.getValue(refInfo.model, refIdValue, refInfo.refLabel)
133+
if (cachedLabel) {
134+
setRefInfoValue(cachedLabel)
135+
setRefPropertyName(prop.name)
136+
return
137+
}
138+
}
128139

129140
const queryOp = metadataApi?.operations.find(x => x.dataModel?.name === refInfo.model)
130141
console.debug('LookupInput queryOp', queryOp)
@@ -171,7 +182,7 @@ const LookupInput: React.FC<LookupInputProps> = ({
171182
}
172183

173184
initialize()
174-
}, []) // Only run on mount
185+
}, [modelValue, id, property, useRef, metadataApi, client, metadataType, getTypeProperties, value])
175186

176187
return (
177188
<div className="lookup-field">

src/components/ModalLookup.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useMemo, useRef } from 'react'
1+
import React, { useState, useEffect, useMemo, useRef, useContext } from 'react'
22
import type { JsonServiceClient } from '@servicestack/client'
33
import type { ApiPrefs, ApiResponse, Column, ColumnSettings, MetadataPropertyType } from '@/types'
44
import type { ModalLookupProps } from './types'
@@ -8,6 +8,7 @@ import { useConfig } from '@/use/config'
88
import { Apis, createDto, Crud, getPrimaryKey, typeOf, typeProperties, useMetadata } from '@/use/metadata'
99
import { grid } from './css'
1010
import { canAccess } from '@/use/auth'
11+
import { ClientContext } from '@/use/context'
1112
import ModalDialog from './ModalDialog'
1213

1314
import FilterColumn from './grids/FilterColumn'
@@ -42,7 +43,7 @@ export default function ModalLookup({
4243
}: ModalLookupProps) {
4344
const { config } = useConfig()
4445
const { metadataApi, filterDefinitions } = useMetadata()
45-
const client = useRef<JsonServiceClient>((window as any).client).current
46+
const client = useContext(ClientContext)
4647
const storage = config.storage!
4748

4849
// State
@@ -216,6 +217,10 @@ export default function ModalLookup({
216217
console.error(`No Query API was found for ${refInfo.model}`)
217218
return
218219
}
220+
if (!client) {
221+
console.error('JsonServiceClient is not available. Make sure to wrap your app with ClientContext.Provider')
222+
return
223+
}
219224
const requestDto = createDto(op, args)
220225
const complete = delaySet(x => {
221226
setApi(prev => ({ ...prev, response: undefined, error: undefined }))
@@ -277,6 +282,28 @@ export default function ModalLookup({
277282

278283
async function createSave(result: any) {
279284
createDone()
285+
286+
// Fetch the newly created entity using its ID
287+
if (result && result.id && queryOp && client) {
288+
const pk = getPrimaryKey(viewModel)
289+
if (pk) {
290+
const pkName = pk.name
291+
const pkValue = result.id
292+
293+
const requestDto = createDto(queryOp, { [pkName]: pkValue })
294+
const apiResult = await client.api(requestDto)
295+
296+
if (apiResult.succeeded) {
297+
const entity = mapGet(apiResult.response, 'results')?.[0]
298+
if (entity) {
299+
onDone?.(entity)
300+
return
301+
}
302+
}
303+
}
304+
}
305+
306+
// Fallback to returning the result as-is if we couldn't fetch the entity
280307
onDone?.(result)
281308
}
282309

src/components/TextLink.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ const TextLink: React.FC<TextLinkProps & React.AnchorHTMLAttributes<HTMLAnchorEl
66
color = 'blue',
77
children,
88
href,
9+
className,
910
...attrs
1011
}) => {
11-
const cls = useMemo(() =>
12-
(a[color] || a.blue) + (!href ? ' cursor-pointer' : ''),
13-
[color, href]
14-
)
12+
const cls = useMemo(() => {
13+
const baseClasses = (a[color] || a.blue) + (!href ? ' cursor-pointer' : '')
14+
return className ? `${baseClasses} ${className}` : baseClasses
15+
}, [color, href, className])
1516

1617
return (
1718
<a className={cls} href={href} {...attrs}>

0 commit comments

Comments
 (0)