fix: apply exactOptionalPropertyTypes shims to adapters and services
- Add optionalProps shim to base.adapter.ts for RequestInit compatibility - Apply optionalProps to UserSettingsModal updateData - Use conditional spreading for Navigation Button element prop - Apply optionalProps to drag-drop target object creation - Apply optionalProps to party.service create/update methods - Apply optionalProps to grid.service CRUD operations - Fix transcendenceStage -> transcendenceStep typo in grid.service - Update UserUpdateParams interface to include | undefined - Update DragOperation interface properties to include | undefined 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
16e24e337b
commit
a74653ee93
9 changed files with 154 additions and 35 deletions
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from './errors'
|
||||
import { authStore } from '$lib/stores/auth.store'
|
||||
import { browser } from '$app/environment'
|
||||
import { optionalProps } from '$lib/utils/typeShims'
|
||||
|
||||
/**
|
||||
* Base adapter class that all resource-specific adapters extend from.
|
||||
|
|
@ -124,9 +125,9 @@ export abstract class BaseAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
// Prepare request options
|
||||
// Prepare request options (filter out undefined to satisfy exactOptionalPropertyTypes)
|
||||
const fetchOptions: RequestInit = {
|
||||
...options, // Allow overriding defaults
|
||||
...optionalProps(options), // Allow overriding defaults, filter undefined
|
||||
credentials: 'include', // Still include cookies for CORS and refresh token
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export class SearchResource {
|
|||
this.activeRequests.set(type, controller)
|
||||
|
||||
// Update loading state
|
||||
this[type] = { ...this[type], loading: true, error: undefined }
|
||||
this[type] = { ...this[type], loading: true }
|
||||
|
||||
try {
|
||||
// Merge base params with provided params
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { userAdapter } from '../adapters/user.adapter'
|
||||
|
||||
export interface UserUpdateParams {
|
||||
picture?: string
|
||||
element?: string
|
||||
gender?: number
|
||||
language?: string
|
||||
theme?: string
|
||||
picture?: string | undefined
|
||||
element?: string | undefined
|
||||
gender?: number | undefined
|
||||
language?: string | undefined
|
||||
theme?: string | undefined
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@
|
|||
iconOnly
|
||||
shape="circle"
|
||||
variant="primary"
|
||||
element={userElement}
|
||||
{...(userElement ? { element: userElement } : {})}
|
||||
elementStyle={Boolean(userElement)}
|
||||
class="new-team-button"
|
||||
aria-label="New team"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import type { UserCookie } from '$lib/types/UserCookie'
|
||||
import { setUserCookie } from '$lib/auth/cookies'
|
||||
import { invalidateAll } from '$app/navigation'
|
||||
import { optionalProps } from '$lib/utils/typeShims'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
|
|
@ -72,14 +73,14 @@
|
|||
saving = true
|
||||
|
||||
try {
|
||||
// Prepare the update data
|
||||
const updateData = {
|
||||
// Prepare the update data (filter undefined to satisfy exactOptionalPropertyTypes)
|
||||
const updateData = optionalProps({
|
||||
picture,
|
||||
element: currentPicture?.element,
|
||||
gender,
|
||||
language,
|
||||
theme
|
||||
}
|
||||
})
|
||||
|
||||
// Call API to update user settings
|
||||
const response = await users.update(userId, updateData)
|
||||
|
|
@ -131,7 +132,12 @@
|
|||
|
||||
</script>
|
||||
|
||||
<Dialog bind:open {onOpenChange} title="@{username}" description="Account Settings">
|
||||
<Dialog
|
||||
bind:open
|
||||
{...(onOpenChange ? { onOpenChange } : {})}
|
||||
title="@{username}"
|
||||
description="Account Settings"
|
||||
>
|
||||
{#snippet children()}
|
||||
<form onsubmit={handleSave} class="settings-form">
|
||||
{#if error}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||
import { optionalProps } from '$lib/utils/typeShims'
|
||||
|
||||
export type GridItemType = 'character' | 'weapon' | 'summon'
|
||||
export type GridItem = GridCharacter | GridWeapon | GridSummon
|
||||
|
|
@ -30,13 +31,13 @@ export interface DragOperation {
|
|||
container: string
|
||||
position: number
|
||||
itemId: string
|
||||
type?: GridItemType
|
||||
type?: GridItemType | undefined
|
||||
}
|
||||
target: {
|
||||
container: string
|
||||
position: number
|
||||
itemId?: string
|
||||
type?: GridItemType
|
||||
itemId?: string | undefined
|
||||
type?: GridItemType | undefined
|
||||
}
|
||||
status: 'pending' | 'synced' | 'failed'
|
||||
retryCount: number
|
||||
|
|
@ -224,12 +225,12 @@ export function createDragDropContext(handlers: DragDropHandlers = {}) {
|
|||
itemId: state.draggedItem.data.id,
|
||||
type: state.draggedItem.source.type
|
||||
},
|
||||
target: {
|
||||
target: optionalProps({
|
||||
container: state.hoveredOver.container,
|
||||
position: state.hoveredOver.position,
|
||||
itemId: targetItem?.id || undefined,
|
||||
itemId: targetItem?.id,
|
||||
type: state.hoveredOver.type
|
||||
},
|
||||
}),
|
||||
status: 'pending',
|
||||
retryCount: 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party'
|
||||
import { gridAdapter } from '$lib/api/adapters/grid.adapter'
|
||||
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
||||
import { optionalProps } from '$lib/utils/typeShims'
|
||||
|
||||
export interface GridOperation {
|
||||
type: 'add' | 'replace' | 'remove' | 'move' | 'swap'
|
||||
|
|
@ -38,13 +39,13 @@ export class GridService {
|
|||
): Promise<GridUpdateResult> {
|
||||
try {
|
||||
// Note: The backend computes the correct uncap level based on the weapon's FLB/ULB/transcendence flags
|
||||
const gridWeapon = await gridAdapter.createWeapon({
|
||||
const gridWeapon = await gridAdapter.createWeapon(optionalProps({
|
||||
partyId,
|
||||
weaponId,
|
||||
position,
|
||||
mainhand: options?.mainhand,
|
||||
transcendenceStep: 0
|
||||
}, this.buildHeaders(editKey))
|
||||
}), this.buildHeaders(editKey))
|
||||
|
||||
console.log('[GridService] Created grid weapon:', gridWeapon)
|
||||
|
||||
|
|
@ -122,12 +123,12 @@ export class GridService {
|
|||
editKey?: string,
|
||||
options?: { shortcode?: string }
|
||||
): Promise<Party | null> {
|
||||
await gridAdapter.updateWeapon(gridWeaponId, {
|
||||
await gridAdapter.updateWeapon(gridWeaponId, optionalProps({
|
||||
position: updates.position,
|
||||
uncapLevel: updates.uncapLevel,
|
||||
transcendenceStep: updates.transcendenceStep,
|
||||
element: updates.element
|
||||
}, this.buildHeaders(editKey))
|
||||
}), this.buildHeaders(editKey))
|
||||
|
||||
// Clear party cache if shortcode provided
|
||||
if (options?.shortcode) {
|
||||
|
|
@ -207,14 +208,14 @@ export class GridService {
|
|||
options?: { main?: boolean; friend?: boolean; shortcode?: string }
|
||||
): Promise<Party> {
|
||||
// Note: The backend computes the correct uncap level based on the summon's FLB/ULB/transcendence flags
|
||||
const gridSummon = await gridAdapter.createSummon({
|
||||
const gridSummon = await gridAdapter.createSummon(optionalProps({
|
||||
partyId,
|
||||
summonId,
|
||||
position,
|
||||
main: options?.main,
|
||||
friend: options?.friend,
|
||||
transcendenceStage: 0
|
||||
}, this.buildHeaders(editKey))
|
||||
transcendenceStep: 0
|
||||
}), this.buildHeaders(editKey))
|
||||
|
||||
console.log('[GridService] Created grid summon:', gridSummon)
|
||||
|
||||
|
|
@ -270,12 +271,12 @@ export class GridService {
|
|||
editKey?: string,
|
||||
options?: { shortcode?: string }
|
||||
): Promise<Party | null> {
|
||||
await gridAdapter.updateSummon(gridSummonId, {
|
||||
await gridAdapter.updateSummon(gridSummonId, optionalProps({
|
||||
position: updates.position,
|
||||
quickSummon: updates.quickSummon,
|
||||
uncapLevel: updates.uncapLevel,
|
||||
transcendenceStage: updates.transcendenceStep
|
||||
}, this.buildHeaders(editKey))
|
||||
transcendenceStep: updates.transcendenceStep
|
||||
}), this.buildHeaders(editKey))
|
||||
|
||||
// Clear party cache if shortcode provided
|
||||
if (options?.shortcode) {
|
||||
|
|
@ -436,12 +437,12 @@ export class GridService {
|
|||
editKey?: string,
|
||||
options?: { shortcode?: string }
|
||||
): Promise<GridCharacter | null> {
|
||||
const updated = await gridAdapter.updateCharacter(gridCharacterId, {
|
||||
const updated = await gridAdapter.updateCharacter(gridCharacterId, optionalProps({
|
||||
position: updates.position,
|
||||
uncapLevel: updates.uncapLevel,
|
||||
transcendenceStep: updates.transcendenceStep,
|
||||
perpetuity: updates.perpetuity
|
||||
}, this.buildHeaders(editKey))
|
||||
}), this.buildHeaders(editKey))
|
||||
|
||||
// Clear party cache if shortcode provided
|
||||
if (options?.shortcode) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Party } from '$lib/types/api/party'
|
|||
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
||||
import { authStore } from '$lib/stores/auth.store'
|
||||
import { browser } from '$app/environment'
|
||||
import { optionalProps } from '$lib/utils/typeShims'
|
||||
|
||||
/**
|
||||
* Context type for party-related operations in components
|
||||
|
|
@ -66,18 +67,18 @@ export class PartyService {
|
|||
party: Party
|
||||
editKey?: string
|
||||
}> {
|
||||
const apiPayload = this.mapToApiPayload(payload)
|
||||
const apiPayload = optionalProps(this.mapToApiPayload(payload))
|
||||
const party = await partyAdapter.create(apiPayload)
|
||||
|
||||
// Note: Edit key handling may need to be adjusted based on how the API returns it
|
||||
return { party, editKey: undefined }
|
||||
return { party }
|
||||
}
|
||||
|
||||
/**
|
||||
* Update party details
|
||||
*/
|
||||
async update(id: string, payload: PartyUpdatePayload, editKey?: string): Promise<Party> {
|
||||
const apiPayload = this.mapToApiPayload(payload)
|
||||
const apiPayload = optionalProps(this.mapToApiPayload(payload))
|
||||
return partyAdapter.update({ shortcode: id, ...apiPayload })
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +111,7 @@ export class PartyService {
|
|||
const party = await partyAdapter.remix(shortcode)
|
||||
|
||||
// Note: Edit key handling may need to be adjusted
|
||||
return { party, editKey: undefined }
|
||||
return { party }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
109
src/lib/utils/typeShims.ts
Normal file
109
src/lib/utils/typeShims.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* Type utilities for handling exactOptionalPropertyTypes: true
|
||||
*
|
||||
* These utilities help work with third-party libraries and components
|
||||
* that don't properly support exactOptionalPropertyTypes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Recursively converts optional properties from { key?: T } to { key?: T | undefined }
|
||||
* This is needed for libraries like bits-ui that don't include undefined in optional props.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Original = { optional?: string }
|
||||
* type Fixed = DeepAddUndefined<Original> // { optional?: string | undefined }
|
||||
* ```
|
||||
*/
|
||||
export type DeepAddUndefined<Type> =
|
||||
Type extends (infer Element)[]
|
||||
? DeepAddUndefined<Element>[]
|
||||
: Type extends (...args: unknown[]) => unknown
|
||||
? Type
|
||||
: Type extends object
|
||||
? {
|
||||
[Key in keyof Type]: undefined extends Type[Key]
|
||||
? never
|
||||
: DeepAddUndefined<Type[Key]>
|
||||
} & {
|
||||
[Key in keyof Type]?: undefined extends Type[Key]
|
||||
? undefined | DeepAddUndefined<Type[Key]>
|
||||
: never
|
||||
}
|
||||
: Type
|
||||
|
||||
/**
|
||||
* Shim function that "fixes" types for exactOptionalPropertyTypes compatibility.
|
||||
* Use this when passing props to third-party components that don't support exactOptionalPropertyTypes.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* <Dialog {...exactOptionalShim({ onOpenChange: myHandler })} />
|
||||
* ```
|
||||
*/
|
||||
export function exactOptionalShim<Type>(value: Type): DeepAddUndefined<Type> {
|
||||
return value as never
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple single-level optional property fixer.
|
||||
* Faster alternative to DeepAddUndefined when you don't need recursion.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Original = { optional?: string; required: number }
|
||||
* type Fixed = OptionalUndefined<Original> // { optional?: string | undefined; required: number }
|
||||
* ```
|
||||
*/
|
||||
export type OptionalUndefined<Type> = {
|
||||
[Key in keyof Type as undefined extends Type[Key] ? never : Key]: Type[Key]
|
||||
} & {
|
||||
[Key in keyof Type as undefined extends Type[Key] ? Key : never]?: Type[Key] | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to conditionally include optional properties only when they're defined.
|
||||
* This avoids assigning undefined to optional properties.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const props = {
|
||||
* required: 'value',
|
||||
* ...optionalProp('optional', maybeUndefined),
|
||||
* ...optionalProp('other', maybeOther)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function optionalProp<K extends string, V>(
|
||||
key: K,
|
||||
value: V | undefined
|
||||
): V extends undefined ? {} : { [P in K]: V } {
|
||||
if (value === undefined) {
|
||||
return {} as any
|
||||
}
|
||||
return { [key]: value } as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to build objects with optional properties that are only included when defined.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const props = optionalProps({
|
||||
* required: 'always included',
|
||||
* optional: maybeUndefined, // only included if defined
|
||||
* other: maybeOther // only included if defined
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function optionalProps<T extends Record<string, unknown>>(
|
||||
obj: T
|
||||
): Partial<T> {
|
||||
const result: Partial<T> = {}
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value !== undefined) {
|
||||
result[key as keyof T] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Loading…
Reference in a new issue