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'
|
} from './errors'
|
||||||
import { authStore } from '$lib/stores/auth.store'
|
import { authStore } from '$lib/stores/auth.store'
|
||||||
import { browser } from '$app/environment'
|
import { browser } from '$app/environment'
|
||||||
|
import { optionalProps } from '$lib/utils/typeShims'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base adapter class that all resource-specific adapters extend from.
|
* 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 = {
|
const fetchOptions: RequestInit = {
|
||||||
...options, // Allow overriding defaults
|
...optionalProps(options), // Allow overriding defaults, filter undefined
|
||||||
credentials: 'include', // Still include cookies for CORS and refresh token
|
credentials: 'include', // Still include cookies for CORS and refresh token
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ export class SearchResource {
|
||||||
this.activeRequests.set(type, controller)
|
this.activeRequests.set(type, controller)
|
||||||
|
|
||||||
// Update loading state
|
// Update loading state
|
||||||
this[type] = { ...this[type], loading: true, error: undefined }
|
this[type] = { ...this[type], loading: true }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Merge base params with provided params
|
// Merge base params with provided params
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { userAdapter } from '../adapters/user.adapter'
|
import { userAdapter } from '../adapters/user.adapter'
|
||||||
|
|
||||||
export interface UserUpdateParams {
|
export interface UserUpdateParams {
|
||||||
picture?: string
|
picture?: string | undefined
|
||||||
element?: string
|
element?: string | undefined
|
||||||
gender?: number
|
gender?: number | undefined
|
||||||
language?: string
|
language?: string | undefined
|
||||||
theme?: string
|
theme?: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserResponse {
|
export interface UserResponse {
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
iconOnly
|
iconOnly
|
||||||
shape="circle"
|
shape="circle"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
element={userElement}
|
{...(userElement ? { element: userElement } : {})}
|
||||||
elementStyle={Boolean(userElement)}
|
elementStyle={Boolean(userElement)}
|
||||||
class="new-team-button"
|
class="new-team-button"
|
||||||
aria-label="New team"
|
aria-label="New team"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
import type { UserCookie } from '$lib/types/UserCookie'
|
import type { UserCookie } from '$lib/types/UserCookie'
|
||||||
import { setUserCookie } from '$lib/auth/cookies'
|
import { setUserCookie } from '$lib/auth/cookies'
|
||||||
import { invalidateAll } from '$app/navigation'
|
import { invalidateAll } from '$app/navigation'
|
||||||
|
import { optionalProps } from '$lib/utils/typeShims'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|
@ -72,14 +73,14 @@
|
||||||
saving = true
|
saving = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prepare the update data
|
// Prepare the update data (filter undefined to satisfy exactOptionalPropertyTypes)
|
||||||
const updateData = {
|
const updateData = optionalProps({
|
||||||
picture,
|
picture,
|
||||||
element: currentPicture?.element,
|
element: currentPicture?.element,
|
||||||
gender,
|
gender,
|
||||||
language,
|
language,
|
||||||
theme
|
theme
|
||||||
}
|
})
|
||||||
|
|
||||||
// Call API to update user settings
|
// Call API to update user settings
|
||||||
const response = await users.update(userId, updateData)
|
const response = await users.update(userId, updateData)
|
||||||
|
|
@ -131,7 +132,12 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog bind:open {onOpenChange} title="@{username}" description="Account Settings">
|
<Dialog
|
||||||
|
bind:open
|
||||||
|
{...(onOpenChange ? { onOpenChange } : {})}
|
||||||
|
title="@{username}"
|
||||||
|
description="Account Settings"
|
||||||
|
>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<form onsubmit={handleSave} class="settings-form">
|
<form onsubmit={handleSave} class="settings-form">
|
||||||
{#if error}
|
{#if error}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||||
|
import { optionalProps } from '$lib/utils/typeShims'
|
||||||
|
|
||||||
export type GridItemType = 'character' | 'weapon' | 'summon'
|
export type GridItemType = 'character' | 'weapon' | 'summon'
|
||||||
export type GridItem = GridCharacter | GridWeapon | GridSummon
|
export type GridItem = GridCharacter | GridWeapon | GridSummon
|
||||||
|
|
@ -30,13 +31,13 @@ export interface DragOperation {
|
||||||
container: string
|
container: string
|
||||||
position: number
|
position: number
|
||||||
itemId: string
|
itemId: string
|
||||||
type?: GridItemType
|
type?: GridItemType | undefined
|
||||||
}
|
}
|
||||||
target: {
|
target: {
|
||||||
container: string
|
container: string
|
||||||
position: number
|
position: number
|
||||||
itemId?: string
|
itemId?: string | undefined
|
||||||
type?: GridItemType
|
type?: GridItemType | undefined
|
||||||
}
|
}
|
||||||
status: 'pending' | 'synced' | 'failed'
|
status: 'pending' | 'synced' | 'failed'
|
||||||
retryCount: number
|
retryCount: number
|
||||||
|
|
@ -224,12 +225,12 @@ export function createDragDropContext(handlers: DragDropHandlers = {}) {
|
||||||
itemId: state.draggedItem.data.id,
|
itemId: state.draggedItem.data.id,
|
||||||
type: state.draggedItem.source.type
|
type: state.draggedItem.source.type
|
||||||
},
|
},
|
||||||
target: {
|
target: optionalProps({
|
||||||
container: state.hoveredOver.container,
|
container: state.hoveredOver.container,
|
||||||
position: state.hoveredOver.position,
|
position: state.hoveredOver.position,
|
||||||
itemId: targetItem?.id || undefined,
|
itemId: targetItem?.id,
|
||||||
type: state.hoveredOver.type
|
type: state.hoveredOver.type
|
||||||
},
|
}),
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
retryCount: 0
|
retryCount: 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party'
|
import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party'
|
||||||
import { gridAdapter } from '$lib/api/adapters/grid.adapter'
|
import { gridAdapter } from '$lib/api/adapters/grid.adapter'
|
||||||
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
||||||
|
import { optionalProps } from '$lib/utils/typeShims'
|
||||||
|
|
||||||
export interface GridOperation {
|
export interface GridOperation {
|
||||||
type: 'add' | 'replace' | 'remove' | 'move' | 'swap'
|
type: 'add' | 'replace' | 'remove' | 'move' | 'swap'
|
||||||
|
|
@ -38,13 +39,13 @@ export class GridService {
|
||||||
): Promise<GridUpdateResult> {
|
): Promise<GridUpdateResult> {
|
||||||
try {
|
try {
|
||||||
// Note: The backend computes the correct uncap level based on the weapon's FLB/ULB/transcendence flags
|
// 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,
|
partyId,
|
||||||
weaponId,
|
weaponId,
|
||||||
position,
|
position,
|
||||||
mainhand: options?.mainhand,
|
mainhand: options?.mainhand,
|
||||||
transcendenceStep: 0
|
transcendenceStep: 0
|
||||||
}, this.buildHeaders(editKey))
|
}), this.buildHeaders(editKey))
|
||||||
|
|
||||||
console.log('[GridService] Created grid weapon:', gridWeapon)
|
console.log('[GridService] Created grid weapon:', gridWeapon)
|
||||||
|
|
||||||
|
|
@ -122,12 +123,12 @@ export class GridService {
|
||||||
editKey?: string,
|
editKey?: string,
|
||||||
options?: { shortcode?: string }
|
options?: { shortcode?: string }
|
||||||
): Promise<Party | null> {
|
): Promise<Party | null> {
|
||||||
await gridAdapter.updateWeapon(gridWeaponId, {
|
await gridAdapter.updateWeapon(gridWeaponId, optionalProps({
|
||||||
position: updates.position,
|
position: updates.position,
|
||||||
uncapLevel: updates.uncapLevel,
|
uncapLevel: updates.uncapLevel,
|
||||||
transcendenceStep: updates.transcendenceStep,
|
transcendenceStep: updates.transcendenceStep,
|
||||||
element: updates.element
|
element: updates.element
|
||||||
}, this.buildHeaders(editKey))
|
}), this.buildHeaders(editKey))
|
||||||
|
|
||||||
// Clear party cache if shortcode provided
|
// Clear party cache if shortcode provided
|
||||||
if (options?.shortcode) {
|
if (options?.shortcode) {
|
||||||
|
|
@ -207,14 +208,14 @@ export class GridService {
|
||||||
options?: { main?: boolean; friend?: boolean; shortcode?: string }
|
options?: { main?: boolean; friend?: boolean; shortcode?: string }
|
||||||
): Promise<Party> {
|
): Promise<Party> {
|
||||||
// Note: The backend computes the correct uncap level based on the summon's FLB/ULB/transcendence flags
|
// 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,
|
partyId,
|
||||||
summonId,
|
summonId,
|
||||||
position,
|
position,
|
||||||
main: options?.main,
|
main: options?.main,
|
||||||
friend: options?.friend,
|
friend: options?.friend,
|
||||||
transcendenceStage: 0
|
transcendenceStep: 0
|
||||||
}, this.buildHeaders(editKey))
|
}), this.buildHeaders(editKey))
|
||||||
|
|
||||||
console.log('[GridService] Created grid summon:', gridSummon)
|
console.log('[GridService] Created grid summon:', gridSummon)
|
||||||
|
|
||||||
|
|
@ -270,12 +271,12 @@ export class GridService {
|
||||||
editKey?: string,
|
editKey?: string,
|
||||||
options?: { shortcode?: string }
|
options?: { shortcode?: string }
|
||||||
): Promise<Party | null> {
|
): Promise<Party | null> {
|
||||||
await gridAdapter.updateSummon(gridSummonId, {
|
await gridAdapter.updateSummon(gridSummonId, optionalProps({
|
||||||
position: updates.position,
|
position: updates.position,
|
||||||
quickSummon: updates.quickSummon,
|
quickSummon: updates.quickSummon,
|
||||||
uncapLevel: updates.uncapLevel,
|
uncapLevel: updates.uncapLevel,
|
||||||
transcendenceStage: updates.transcendenceStep
|
transcendenceStep: updates.transcendenceStep
|
||||||
}, this.buildHeaders(editKey))
|
}), this.buildHeaders(editKey))
|
||||||
|
|
||||||
// Clear party cache if shortcode provided
|
// Clear party cache if shortcode provided
|
||||||
if (options?.shortcode) {
|
if (options?.shortcode) {
|
||||||
|
|
@ -436,12 +437,12 @@ export class GridService {
|
||||||
editKey?: string,
|
editKey?: string,
|
||||||
options?: { shortcode?: string }
|
options?: { shortcode?: string }
|
||||||
): Promise<GridCharacter | null> {
|
): Promise<GridCharacter | null> {
|
||||||
const updated = await gridAdapter.updateCharacter(gridCharacterId, {
|
const updated = await gridAdapter.updateCharacter(gridCharacterId, optionalProps({
|
||||||
position: updates.position,
|
position: updates.position,
|
||||||
uncapLevel: updates.uncapLevel,
|
uncapLevel: updates.uncapLevel,
|
||||||
transcendenceStep: updates.transcendenceStep,
|
transcendenceStep: updates.transcendenceStep,
|
||||||
perpetuity: updates.perpetuity
|
perpetuity: updates.perpetuity
|
||||||
}, this.buildHeaders(editKey))
|
}), this.buildHeaders(editKey))
|
||||||
|
|
||||||
// Clear party cache if shortcode provided
|
// Clear party cache if shortcode provided
|
||||||
if (options?.shortcode) {
|
if (options?.shortcode) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { Party } from '$lib/types/api/party'
|
||||||
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
||||||
import { authStore } from '$lib/stores/auth.store'
|
import { authStore } from '$lib/stores/auth.store'
|
||||||
import { browser } from '$app/environment'
|
import { browser } from '$app/environment'
|
||||||
|
import { optionalProps } from '$lib/utils/typeShims'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context type for party-related operations in components
|
* Context type for party-related operations in components
|
||||||
|
|
@ -66,18 +67,18 @@ export class PartyService {
|
||||||
party: Party
|
party: Party
|
||||||
editKey?: string
|
editKey?: string
|
||||||
}> {
|
}> {
|
||||||
const apiPayload = this.mapToApiPayload(payload)
|
const apiPayload = optionalProps(this.mapToApiPayload(payload))
|
||||||
const party = await partyAdapter.create(apiPayload)
|
const party = await partyAdapter.create(apiPayload)
|
||||||
|
|
||||||
// Note: Edit key handling may need to be adjusted based on how the API returns it
|
// 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
|
* Update party details
|
||||||
*/
|
*/
|
||||||
async update(id: string, payload: PartyUpdatePayload, editKey?: string): Promise<Party> {
|
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 })
|
return partyAdapter.update({ shortcode: id, ...apiPayload })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,7 +111,7 @@ export class PartyService {
|
||||||
const party = await partyAdapter.remix(shortcode)
|
const party = await partyAdapter.remix(shortcode)
|
||||||
|
|
||||||
// Note: Edit key handling may need to be adjusted
|
// 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