update select

This commit is contained in:
Nate Kelley 2025-07-16 14:48:33 -06:00
parent 4fdcdc4573
commit bc5b0e4f84
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
15 changed files with 411 additions and 167 deletions

View File

@ -162,6 +162,7 @@
"jsdom": "^26.1.0",
"msw-storybook-addon": "^2.0.5",
"prettier-eslint": "^16.4.2",
"storybook": "^8.6.14",
"vitest": "catalog:"
},
"msw": {

View File

@ -19,7 +19,7 @@ import {
} from '../collections/queryRequests';
import type { RustApiError } from '../errors';
import { prefetchGetMetricDataClient } from '../metrics/queryRequests';
import { useGetUserFavorites } from '../users/queryRequestFavorites';
import { useGetUserFavorites } from '../users/favorites';
import {
deleteChat,
duplicateChat,

View File

@ -1,25 +1,13 @@
import { QueryClient, useQuery } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { userQueryKeys } from '@/api/query_keys/users';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { getUserList, getUserList_server } from './requests';
import { getUserToOrganization } from './requests';
export const useGetUserList = (params: Parameters<typeof getUserList>[0]) => {
const queryFn = useMemoizedFn(() => getUserList(params));
export const useGetUserToOrganization = (params: Parameters<typeof getUserToOrganization>[0]) => {
const queryFn = useMemoizedFn(() => getUserToOrganization(params));
return useQuery({
...userQueryKeys.userGetUserList(params),
...userQueryKeys.userGetUserToOrganization(params),
queryFn
});
};
export const prefetchGetUserList = async (
params: Parameters<typeof getUserList>[0],
queryClientProp?: QueryClient
) => {
const queryClient = queryClientProp || new QueryClient();
await queryClient.prefetchQuery({
...userQueryKeys.userGetUserList(params),
queryFn: () => getUserList_server(params)
});
return queryClient;
};

View File

@ -1,17 +1,11 @@
import { serverFetch } from '../../../createServerInstance';
import { mainApi } from '../../instances';
import type { UserListResponse } from '@buster/server-shared/user';
import { mainApiV2 } from '../../instances';
import {
GetUserToOrganizationResponse,
GetUserToOrganizationRequest
} from '@buster/server-shared/user';
export const getUserList = async (payload: {
team_id: string;
page?: number;
page_size?: number;
}) => {
return mainApi
.get<UserListResponse>('/users', { params: payload })
export const getUserToOrganization = async (payload: GetUserToOrganizationRequest) => {
return mainApiV2
.get<GetUserToOrganizationResponse>('/users/organization', { params: payload })
.then((response) => response.data);
};
export const getUserList_server = async (payload: Parameters<typeof getUserList>[0]) => {
return serverFetch<UserListResponse>('/users', { params: payload });
};

View File

@ -1,80 +0,0 @@
import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { userQueryKeys } from '@/api/query_keys/users';
import { useUserConfigContextSelector } from '@/context/Users/BusterUserConfigProvider';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import {
createUserFavorite,
deleteUserFavorite,
getUserFavorites,
getUserFavorites_server,
updateUserFavorites
} from './requests';
export const useGetUserFavorites = () => {
const queryFn = useMemoizedFn(async () => getUserFavorites());
const organizationId = useUserConfigContextSelector((state) => state.userOrganizations?.id);
return useQuery({
...userQueryKeys.favoritesGetList,
queryFn,
enabled: !!organizationId
});
};
export const prefetchGetUserFavorites = async (queryClientProp?: QueryClient) => {
const queryClient = queryClientProp || new QueryClient();
await queryClient.prefetchQuery({
...userQueryKeys.favoritesGetList,
queryFn: () => getUserFavorites_server()
});
return queryClient;
};
export const useAddUserFavorite = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createUserFavorite,
onMutate: (params) => {
queryClient.setQueryData(userQueryKeys.favoritesGetList.queryKey, (prev) => {
const prevIds = prev?.map((p) => p.id) || [];
const dedupedAdd = params.filter((p) => !prevIds.includes(p.id));
return [...dedupedAdd, ...(prev || [])];
});
},
onSuccess: (data) => {
queryClient.setQueryData(userQueryKeys.favoritesGetList.queryKey, data);
}
});
};
export const useDeleteUserFavorite = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteUserFavorite,
onMutate: (id) => {
queryClient.setQueryData(userQueryKeys.favoritesGetList.queryKey, (prev) => {
return prev?.filter((fav) => !id.includes(fav.id));
});
},
onSuccess: (data) => {
queryClient.setQueryData(userQueryKeys.favoritesGetList.queryKey, data);
}
});
};
export const useUpdateUserFavorites = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUserFavorites,
onMutate: (params) => {
queryClient.setQueryData(userQueryKeys.favoritesGetList.queryKey, (prev) => {
return prev?.filter((fav, index) => {
const id = fav.id;
const favorite = (prev || []).find((f) => f.id === id);
if (!favorite) return false;
return { ...favorite, index };
});
});
}
});
};

View File

@ -77,6 +77,7 @@ export const useInviteUser = () => {
onSuccess: () => {
const user = queryClient.getQueryData(userQueryKeys.userGetUserMyself.queryKey);
// Invalidate organization users for all user's organizations
for (const organization of user?.organizations || []) {
queryClient.invalidateQueries({
queryKey: [organizationQueryKeys.organizationUsers(organization.id).queryKey],
@ -84,12 +85,11 @@ export const useInviteUser = () => {
});
}
for (const team of user?.teams || []) {
queryClient.invalidateQueries({
queryKey: [userQueryKeys.userGetUserList({ team_id: team.id }).queryKey],
refetchType: 'all'
});
}
// Invalidate all userGetUserToOrganization queries (any params)
queryClient.invalidateQueries({
queryKey: userQueryKeys.userGetUserToOrganization({}).queryKey.slice(0, -1),
refetchType: 'all'
});
}
});
};

View File

@ -8,9 +8,10 @@ import type {
} from '@/api/asset_interfaces/users';
import type { OrganizationUser } from '@buster/server-shared/organization';
import type {
GetUserToOrganizationRequest,
GetUserToOrganizationResponse,
UserFavoriteResponse,
UserResponse,
UserListResponse
UserResponse
} from '@buster/server-shared/user';
const favoritesGetList = queryOptions<UserFavoriteResponse>({
@ -55,9 +56,9 @@ const userGetUserDatasetGroups = (userId: string) =>
queryKey: ['users', userId, 'datasetGroups'] as const
});
const userGetUserList = (params: { team_id: string; page?: number; page_size?: number }) =>
queryOptions<UserListResponse>({
queryKey: ['users', 'list', params] as const
const userGetUserToOrganization = (params: GetUserToOrganizationRequest) =>
queryOptions<GetUserToOrganizationResponse>({
queryKey: ['users', 'organization', params] as const
});
export const userQueryKeys = {
@ -69,5 +70,5 @@ export const userQueryKeys = {
userGetUserAttributes,
userGetUserDatasets,
userGetUserDatasetGroups,
userGetUserList
userGetUserToOrganization
};

View File

@ -56,7 +56,7 @@ export interface DropdownProps<T = string> extends DropdownMenuProps {
onSelect?: (value: T) => void;
align?: 'start' | 'center' | 'end';
side?: 'top' | 'right' | 'bottom' | 'left';
emptyStateText?: string;
emptyStateText?: string | React.ReactNode;
className?: string;
footerContent?: React.ReactNode;
showIndex?: boolean;

View File

@ -0,0 +1,206 @@
import type { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { useState } from 'react';
import { InputSearchDropdown } from './InputSearchDropdown';
const meta: Meta<typeof InputSearchDropdown> = {
title: 'UI/inputs/InputSearchDropdown',
component: InputSearchDropdown,
parameters: {
layout: 'centered'
},
tags: ['autodocs'],
argTypes: {
placement: {
control: { type: 'select' },
options: ['top', 'bottom']
},
popoverMatchWidth: {
control: { type: 'boolean' }
},
value: {
control: { type: 'text' }
},
onSelect: {
action: 'selected'
},
onSearch: {
action: 'searched'
}
}
};
export default meta;
type Story = StoryObj<typeof meta>;
const sampleOptions = [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' },
{ label: 'Date', value: 'date' },
{ label: 'Elderberry', value: 'elderberry' },
{ label: 'Fig', value: 'fig' },
{ label: 'Grape', value: 'grape' },
{ label: 'Honeydew', value: 'honeydew' }
];
// Interactive story with state management
const InputSearchDropdownWithState = (args: any) => {
const [value, setValue] = useState(args.value || '');
const [filteredOptions, setFilteredOptions] = useState(sampleOptions);
const handleSearch = (searchValue: string) => {
action('searched')(searchValue);
const filtered = sampleOptions.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
);
setFilteredOptions(filtered);
};
const handleSelect = (selectedValue: string) => {
action('selected')(selectedValue);
const selectedOption = sampleOptions.find((option) => option.value === selectedValue);
setValue(selectedOption?.label || selectedValue);
};
return (
<div className="w-80">
<InputSearchDropdown
{...args}
options={filteredOptions}
value={value}
onSearch={handleSearch}
onSelect={handleSelect}
/>
</div>
);
};
export const Default: Story = {
render: (args) => <InputSearchDropdownWithState {...args} />,
args: {
placeholder: 'Search fruits...',
value: '',
placement: 'bottom',
popoverMatchWidth: true
}
};
export const WithInitialValue: Story = {
render: (args) => <InputSearchDropdownWithState {...args} />,
args: {
placeholder: 'Search fruits...',
value: 'Apple',
placement: 'bottom',
popoverMatchWidth: true
}
};
export const TopPlacement: Story = {
render: (args) => <InputSearchDropdownWithState {...args} />,
args: {
placeholder: 'Search fruits...',
value: '',
placement: 'top',
popoverMatchWidth: true
}
};
export const CustomEmptyState: Story = {
render: (args) => <InputSearchDropdownWithState {...args} />,
args: {
placeholder: 'Search fruits...',
value: '',
placement: 'bottom',
popoverMatchWidth: true,
emptyState: 'No fruits found matching your search'
}
};
export const CustomStyling: Story = {
render: (args) => <InputSearchDropdownWithState {...args} />,
args: {
placeholder: 'Search fruits...',
value: '',
placement: 'bottom',
popoverMatchWidth: true,
className: 'border-2 border-blue-500 rounded-lg',
popoverClassName: 'bg-blue-50 border border-blue-200'
}
};
// Story with complex options (React nodes)
const complexOptions = [
{
label: (
<div className="flex items-center gap-2">
<span className="h-4 w-4 rounded-full bg-red-500"></span>
<span>Apple</span>
<span className="text-sm text-gray-500">(Red fruit)</span>
</div>
),
value: 'apple'
},
{
label: (
<div className="flex items-center gap-2">
<span className="h-4 w-4 rounded-full bg-yellow-500"></span>
<span>Banana</span>
<span className="text-sm text-gray-500">(Yellow fruit)</span>
</div>
),
value: 'banana'
},
{
label: (
<div className="flex items-center gap-2">
<span className="h-4 w-4 rounded-full bg-red-600"></span>
<span>Cherry</span>
<span className="text-sm text-gray-500">(Small red fruit)</span>
</div>
),
value: 'cherry'
}
];
const ComplexInputSearchDropdown = (args: any) => {
const [value, setValue] = useState(args.value || '');
const [filteredOptions, setFilteredOptions] = useState(complexOptions);
const handleSearch = (searchValue: string) => {
action('searched')(searchValue);
const filtered = complexOptions.filter((option) => {
const labelText = typeof option.label === 'string' ? option.label : 'Apple Banana Cherry'; // Simplified for demo
return labelText.toLowerCase().includes(searchValue.toLowerCase());
});
setFilteredOptions(filtered);
};
const handleSelect = (selectedValue: string) => {
action('selected')(selectedValue);
const selectedOption = complexOptions.find((option) => option.value === selectedValue);
setValue(selectedValue);
};
return (
<div className="w-80">
<InputSearchDropdown
{...args}
options={filteredOptions}
value={value}
onSearch={handleSearch}
onSelect={handleSelect}
/>
</div>
);
};
export const ComplexOptions: Story = {
render: (args) => <ComplexInputSearchDropdown {...args} />,
args: {
placeholder: 'Search fruits with icons...',
value: '',
placement: 'bottom',
popoverMatchWidth: true
}
};

View File

@ -0,0 +1,88 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { InputSearchDropdown } from './InputSearchDropdown';
// Mock the hooks
vi.mock('@/hooks', () => ({
useMemoizedFn: (fn: any) => fn
}));
// Mock the classMerge utility
vi.mock('@/lib/classMerge', () => ({
cn: (...classes: string[]) => classes.filter(Boolean).join(' ')
}));
describe('InputSearchDropdown', () => {
const mockOptions = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' }
];
const defaultProps = {
options: mockOptions,
onSelect: vi.fn(),
onSearch: vi.fn(),
value: '',
placeholder: 'Search...'
};
it('renders input field with placeholder', () => {
render(<InputSearchDropdown {...defaultProps} />);
const input = screen.getByPlaceholderText('Search...');
expect(input).toBeInTheDocument();
});
it('shows dropdown when user starts typing', async () => {
render(<InputSearchDropdown {...defaultProps} />);
const input = screen.getByPlaceholderText('Search...');
fireEvent.change(input, { target: { value: 'test' } });
await waitFor(() => {
expect(defaultProps.onSearch).toHaveBeenCalledWith('test');
});
});
it('hides dropdown when input is empty', () => {
render(<InputSearchDropdown {...defaultProps} />);
const input = screen.getByPlaceholderText('Search...');
fireEvent.change(input, { target: { value: '' } });
// Dropdown should not be visible when input is empty
expect(screen.queryByText('Option 1')).not.toBeInTheDocument();
});
it('calls onSelect when an option is clicked', async () => {
render(<InputSearchDropdown {...defaultProps} />);
const input = screen.getByPlaceholderText('Search...');
fireEvent.change(input, { target: { value: 'test' } });
// Wait for dropdown to appear and click an option
await waitFor(() => {
const option = screen.getByText('Option 1');
fireEvent.click(option);
});
expect(defaultProps.onSelect).toHaveBeenCalledWith('option1');
});
it('updates input value when value prop changes', () => {
const { rerender } = render(<InputSearchDropdown {...defaultProps} />);
rerender(<InputSearchDropdown {...defaultProps} value="new value" />);
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(input.value).toBe('new value');
});
it('applies custom className', () => {
render(<InputSearchDropdown {...defaultProps} className="custom-class" />);
const input = screen.getByPlaceholderText('Search...');
expect(input).toHaveClass('custom-class');
});
});

View File

@ -0,0 +1,47 @@
import React, { useState, useEffect, useRef } from 'react';
import { Select } from '../select/Select';
interface InputSearchDropdownProps {
options: {
label: string | React.ReactNode;
value: string;
}[];
onSelect: (value: string) => void;
placeholder?: string;
emptyMessage?: string;
onSearch: (value: string) => void;
value: string;
className?: string;
disabled?: boolean;
matchPopUpWidth?: boolean;
}
export const InputSearchDropdown = ({
options,
onSelect,
placeholder = 'Search...',
emptyMessage = 'No options found',
matchPopUpWidth = true,
onSearch,
value,
className,
disabled = false
}: InputSearchDropdownProps) => {
const [inputValue, setInputValue] = useState(value);
return (
<Select
items={options}
placeholder={placeholder}
onChange={onSelect}
disabled={disabled}
clearable={false}
className={className}
matchPopUpWidth={matchPopUpWidth}
emptyMessage={emptyMessage}
inputValue={inputValue}
onInputValueChange={setInputValue}
search={true}
/>
);
};

View File

@ -37,12 +37,13 @@ interface BaseSelectProps<T> {
open?: boolean;
showIndex?: boolean;
className?: string;
defaultValue?: string;
dataTestId?: string;
loading?: boolean;
search?: boolean | SearchFunction<T>;
emptyMessage?: string;
matchPopUpWidth?: boolean;
inputValue?: string;
onInputValueChange?: (value: string) => void;
}
// Clearable version - onChange can return null
@ -140,20 +141,25 @@ function SelectComponent<T = string>({
open: controlledOpen,
showIndex = false,
className,
defaultValue,
dataTestId,
loading = false,
search = false,
clearable = false,
matchPopUpWidth = false
matchPopUpWidth = false,
inputValue,
onInputValueChange
}: SelectProps<T>) {
const [internalOpen, setInternalOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState('');
const [internalInputValue, setInternalInputValue] = React.useState('');
const [isFocused, setIsFocused] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const commandRef = React.useRef<HTMLDivElement>(null);
const listboxId = React.useId();
// Use provided inputValue or internal state
const currentInputValue = inputValue ?? internalInputValue;
const setInputValue = onInputValueChange ?? setInternalInputValue;
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
const handleOpenChange = React.useCallback(
@ -162,12 +168,13 @@ function SelectComponent<T = string>({
setInternalOpen(newOpen);
onOpenChange?.(newOpen);
if (!newOpen) {
setSearchValue('');
// Clear search value when closing
setInputValue('');
setIsFocused(false);
}
}
},
[disabled, onOpenChange]
[disabled, onOpenChange, setInputValue]
);
// Get all items in a flat array for easier processing
@ -187,15 +194,15 @@ function SelectComponent<T = string>({
// Filter items based on search
const filterItem = React.useCallback(
(item: SelectItem<T>): boolean => {
if (!search || !searchValue) return true;
if (!search || !currentInputValue) return true;
if (typeof search === 'function') {
return search(item, searchValue);
return search(item, currentInputValue);
}
return defaultSearchFunction(item, searchValue);
return defaultSearchFunction(item, currentInputValue);
},
[search, searchValue]
[search, currentInputValue]
);
const handleSelect = React.useCallback(
@ -204,11 +211,11 @@ function SelectComponent<T = string>({
if (item) {
onChange(item.value);
handleOpenChange(false);
setSearchValue('');
setInputValue('');
inputRef.current?.blur();
}
},
[flatItems, onChange, handleOpenChange]
[flatItems, onChange, handleOpenChange, setInputValue]
);
const handleClear = React.useCallback(
@ -218,23 +225,23 @@ function SelectComponent<T = string>({
// Type assertion is safe here because handleClear is only called when clearable is true
if (clearable) {
(onChange as (value: T | null) => void)(null);
setSearchValue('');
setInputValue('');
handleOpenChange(false);
}
},
[onChange, handleOpenChange, clearable]
[onChange, handleOpenChange, clearable, setInputValue]
);
const handleInputChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setSearchValue(newValue);
setInputValue(newValue);
if (search !== false && newValue && !open) {
handleOpenChange(true);
}
},
[search, open, handleOpenChange]
[search, open, handleOpenChange, setInputValue]
);
const handleInputFocus = React.useCallback(() => {
@ -286,7 +293,7 @@ function SelectComponent<T = string>({
if (isGroupedItems(items)) {
return items.map((group, groupIndex) => {
const filteredItems = group.items.filter(filterItem);
if (filteredItems.length === 0 && searchValue) return null;
if (filteredItems.length === 0 && currentInputValue) return null;
return (
<CommandGroup key={`${group.label}-${groupIndex}`} heading={group.label}>
@ -316,18 +323,18 @@ function SelectComponent<T = string>({
onSelect={handleSelect}
/>
));
}, [items, flatItems, filterItem, searchValue, value, showIndex, handleSelect]);
}, [items, flatItems, filterItem, currentInputValue, value, showIndex, handleSelect]);
// Display value in input when not focused/searching
const inputDisplayValue = React.useMemo(() => {
if (isFocused || searchValue) {
return searchValue;
if (isFocused || currentInputValue) {
return currentInputValue;
}
if (selectedItem) {
return typeof selectedItem.label === 'string' ? selectedItem.label : '';
}
return '';
}, [isFocused, searchValue, selectedItem]);
}, [isFocused, currentInputValue, selectedItem]);
// Compute placeholder once
const computedPlaceholder = React.useMemo(() => {
@ -358,7 +365,7 @@ function SelectComponent<T = string>({
'bg-background cursor-pointer transition-all duration-300',
'focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
disabled ? 'bg-disabled text-gray-light' : '',
!selectedItem && !searchValue && 'text-text-secondary'
!selectedItem && !currentInputValue && 'text-text-secondary'
)}
/>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
@ -394,8 +401,8 @@ function SelectComponent<T = string>({
<Command ref={commandRef} shouldFilter={false}>
{/* Hidden input that Command uses for keyboard navigation */}
<CommandInput
value={searchValue}
onValueChange={setSearchValue}
value={currentInputValue}
onValueChange={setInputValue}
parentClassName="sr-only hidden h-0 border-0 p-0"
aria-hidden="true"
/>

View File

@ -51,14 +51,14 @@ export const GetUserListRequestSchema = z.object({
export type GetUserListRequest = z.infer<typeof GetUserListRequestSchema>;
export const GetUserToOrganizationRequestSchema = z.object({
page: z.coerce.number().min(1).optional().default(1),
page_size: z.coerce.number().min(1).max(5000).optional().default(25),
page: z.coerce.number().min(1).default(1).optional(),
page_size: z.coerce.number().min(1).max(5000).default(25).optional(),
user_name: z.string().optional(),
email: z.string().optional(),
//We need this because the frontend sends the roles as a comma-separated string in the query params
role: createOptionalQueryArrayPreprocessor(OrganizationRoleSchema),
role: createOptionalQueryArrayPreprocessor(OrganizationRoleSchema).optional(),
//We need this because the frontend sends the status as a comma-separated string in the query params
status: createOptionalQueryArrayPreprocessor(OrganizationStatusSchema),
status: createOptionalQueryArrayPreprocessor(OrganizationStatusSchema).optional(),
});
export type GetUserToOrganizationRequest = z.infer<typeof GetUserToOrganizationRequestSchema>;

View File

@ -17,20 +17,10 @@ export const UserResponseSchema = z.object({
organizations: z.array(OrganizationWithUserRoleSchema).nullable(),
});
export const UserListResponseSchema = z.array(
z.object({
email: z.string(),
id: z.string(),
name: z.string(),
role: OrganizationRoleSchema.nullable(),
})
);
export const UserFavoriteResponseSchema = z.array(UserFavoriteSchema);
export const GetUserToOrganizationResponseSchema = PaginatedResponseSchema(OrganizationUserSchema);
export type UserResponse = z.infer<typeof UserResponseSchema>;
export type UserListResponse = z.infer<typeof UserListResponseSchema>;
export type UserFavoriteResponse = z.infer<typeof UserFavoriteResponseSchema>;
export type GetUserToOrganizationResponse = z.infer<typeof GetUserToOrganizationResponseSchema>;

View File

@ -614,6 +614,9 @@ importers:
prettier-eslint:
specifier: ^16.4.2
version: 16.4.2(typescript@5.8.3)
storybook:
specifier: ^8.6.14
version: 8.6.14(prettier@3.6.2)
vitest:
specifier: 'catalog:'
version: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.0.10)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.4(@types/node@24.0.10)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
@ -5947,7 +5950,6 @@ packages:
bun@1.2.18:
resolution: {integrity: sha512-OR+EpNckoJN4tHMVZPaTPxDj2RgpJgJwLruTIFYbO3bQMguLd0YrmkWKYqsiihcLgm2ehIjF/H1RLfZiRa7+qQ==}
cpu: [arm64, x64, aarch64]
os: [darwin, linux, win32]
hasBin: true
@ -17069,14 +17071,14 @@ snapshots:
msw: 2.10.4(@types/node@20.19.4)(typescript@5.8.3)
vite: 6.3.5(@types/node@20.19.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
'@vitest/mocker@3.2.4(msw@2.10.4(@types/node@24.0.10)(typescript@5.8.3))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))':
'@vitest/mocker@3.2.4(msw@2.10.4(@types/node@24.0.10)(typescript@5.8.3))(vite@6.3.5(@types/node@20.19.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
msw: 2.10.4(@types/node@24.0.10)(typescript@5.8.3)
vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
vite: 6.3.5(@types/node@20.19.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
'@vitest/pretty-format@2.0.5':
dependencies:
@ -23634,7 +23636,7 @@ snapshots:
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@24.0.10)(typescript@5.8.3))(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
'@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@24.0.10)(typescript@5.8.3))(vite@6.3.5(@types/node@20.19.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4