fix: fix cron builder

This commit is contained in:
master 2025-07-07 01:34:56 +08:00
parent 6cdd8c27ce
commit 5be5b9f634
22 changed files with 3633 additions and 541 deletions

View File

@ -12,6 +12,7 @@ const config: CodegenConfig = {
}, },
config: { config: {
enumsAsConst: true, enumsAsConst: true,
useTypeImports: true,
scalars: { scalars: {
SubscriberTaskType: { SubscriberTaskType: {
input: 'recorder/bindings/SubscriberTaskInput#SubscriberTaskInput', input: 'recorder/bindings/SubscriberTaskInput#SubscriberTaskInput',

View File

@ -1,3 +1,14 @@
import { getFutureMatches } from '@datasert/cronjs-matcher';
import { Calendar, Clock, Info, Settings, Zap } from 'lucide-react';
import {
type CSSProperties,
type FC,
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@ -20,16 +31,6 @@ import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { cn } from '@/presentation/utils'; import { cn } from '@/presentation/utils';
import { getFutureMatches } from '@datasert/cronjs-matcher';
import { Calendar, Clock, Info, Settings, Zap } from 'lucide-react';
import {
type CSSProperties,
type FC,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { import {
type CronBuilderProps, type CronBuilderProps,
CronField, CronField,
@ -345,7 +346,7 @@ const CronBuilder: FC<CronBuilderProps> = ({
<div className={cn(withCard && 'space-y-6', className)}> <div className={cn(withCard && 'space-y-6', className)}>
<Tabs <Tabs
value={activeTab} value={activeTab}
onValueChange={(value) => handlePeriodChange(value as CronPeriod)} onValueChange={(v) => handlePeriodChange(v as CronPeriod)}
> >
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<TabsList <TabsList
@ -516,15 +517,114 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
const currentValue = fields[field]; const currentValue = fields[field];
return ( return (
<div key={field} className="space-y-2"> <CronFieldItemEditor
key={field}
config={config}
field={field}
value={currentValue}
onChange={onChange}
disabled={disabled}
/>
);
})}
</div>
);
};
const CronFieldItemAnyOrSpecificOption = {
Any: 'any',
Specific: 'specific',
} as const;
type CronFieldItemAnyOrSpecificOption =
(typeof CronFieldItemAnyOrSpecificOption)[keyof typeof CronFieldItemAnyOrSpecificOption];
interface CronFieldItemEditorProps {
config: CronFieldConfig;
field: CronField;
value: string;
onChange: (field: CronField, value: string) => void;
disabled?: boolean;
}
function encodeCronFieldItem(value: string): string {
if (value === '') {
return '<meta:empty>';
}
if (value.includes(' ')) {
return `<meta:contains-space:${encodeURIComponent(value)}>`;
}
return value;
}
function decodeCronFieldItem(value: string): string {
if (value.startsWith('<meta:contains')) {
return decodeURIComponent(
// biome-ignore lint/performance/useTopLevelRegex: false
value.replace(/^<meta:contains-space:([^>]+)>$/, '$1')
);
}
if (value === '<meta:empty>') {
return '';
}
return value;
}
export const CronFieldItemEditor: FC<CronFieldItemEditorProps> = memo(
({ field, value, onChange, config, disabled = false }) => {
const [innerValue, _setInnerValue] = useState(() =>
decodeCronFieldItem(value)
);
const [anyOrSpecificOption, _setAnyOrSpecificOption] =
useState<CronFieldItemAnyOrSpecificOption>(() =>
innerValue === '*'
? CronFieldItemAnyOrSpecificOption.Any
: CronFieldItemAnyOrSpecificOption.Specific
);
// biome-ignore lint/correctness/useExhaustiveDependencies: false
useEffect(() => {
const nextValue = decodeCronFieldItem(value);
if (nextValue !== innerValue) {
_setInnerValue(nextValue);
}
}, [value]);
const handleChange = useCallback(
(v: string) => {
_setInnerValue(v);
onChange(field, encodeCronFieldItem(v));
},
[field, onChange]
);
const setAnyOrSpecificOption = useCallback(
(v: CronFieldItemAnyOrSpecificOption) => {
_setAnyOrSpecificOption(v);
if (v === CronFieldItemAnyOrSpecificOption.Any) {
handleChange('*');
} else if (v === CronFieldItemAnyOrSpecificOption.Specific) {
handleChange('0');
}
},
[handleChange]
);
return (
<div className="space-y-2">
<Label className="font-medium text-sm capitalize"> <Label className="font-medium text-sm capitalize">
{field.replace(/([A-Z])/g, ' $1').toLowerCase()} {field.replace(/([A-Z])/g, ' $1').toLowerCase()}
</Label> </Label>
{field === 'month' || field === 'dayOfWeek' ? ( {(field === 'month' || field === 'dayOfWeek') && (
<Select <Select
value={currentValue} value={innerValue}
onValueChange={(value) => onChange(field, value)} onValueChange={handleChange}
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
@ -539,12 +639,12 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
// biome-ignore lint/nursery/noNestedTernary: <explanation> )}
) : field === 'dayOfMonth' ? ( {field === 'dayOfMonth' && (
<div className="space-y-2"> <div className="space-y-2">
<Select <Select
value={currentValue} value={innerValue}
onValueChange={(value) => onChange(field, value)} onValueChange={handleChange}
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
@ -564,36 +664,39 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
) : ( )}
{!(
field === 'month' ||
field === 'dayOfWeek' ||
field === 'dayOfMonth'
) && (
<div className="space-y-2"> <div className="space-y-2">
<ToggleGroup <ToggleGroup
type="single" type="single"
value={currentValue === '*' ? '*' : 'specific'} value={anyOrSpecificOption}
onValueChange={(value) => { onValueChange={setAnyOrSpecificOption}
if (value === '*') {
onChange(field, '*');
} else if (value === 'specific' && currentValue === '*') {
onChange(field, '0');
}
}}
disabled={disabled} disabled={disabled}
> >
<ToggleGroupItem value="*" className="min-w-fit text-xs"> <ToggleGroupItem
value={CronFieldItemAnyOrSpecificOption.Any}
className="min-w-fit text-xs"
>
Any Any
</ToggleGroupItem> </ToggleGroupItem>
<ToggleGroupItem <ToggleGroupItem
value="specific" value={CronFieldItemAnyOrSpecificOption.Specific}
className="min-w-fit text-xs" className="min-w-fit text-xs"
> >
Specific Specific
</ToggleGroupItem> </ToggleGroupItem>
</ToggleGroup> </ToggleGroup>
{currentValue !== '*' && ( {anyOrSpecificOption ===
CronFieldItemAnyOrSpecificOption.Specific && (
<Input <Input
type="text" type="text"
value={currentValue} value={innerValue}
onChange={(e) => onChange(field, e.target.value)} onChange={(e) => handleChange(e.target.value)}
placeholder={`0-${config.max}`} placeholder={`0-${config.max}`}
disabled={disabled} disabled={disabled}
className="font-mono text-sm" className="font-mono text-sm"
@ -608,18 +711,15 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
</span> </span>
</div> </div>
<div className="mt-1"> <div className="mt-1">
Supports: *, numbers, ranges (1-5), lists (1,3,5), steps Supports: *, numbers, ranges (1-5), lists (1,3,5), steps (*/5)
(*/5)
</div> </div>
</div> </div>
</div> </div>
)} )}
</div> </div>
); );
})} }
</div>
); );
};
function parseCronExpression(expression: string): Record<CronField, string> { function parseCronExpression(expression: string): Record<CronField, string> {
const parts = expression.split(' '); const parts = expression.split(' ');

View File

@ -33,7 +33,11 @@ const CronInput = forwardRef<HTMLInputElement, CronInputProps>(
const validationResult = useMemo((): CronValidationResult => { const validationResult = useMemo((): CronValidationResult => {
if (!internalValue.trim()) { if (!internalValue.trim()) {
return { isValid: false, error: 'Expression is required' }; return {
isValid: false,
error: 'Expression is required',
isEmpty: true,
};
} }
try { try {

View File

@ -1,3 +1,14 @@
import { parse } from '@datasert/cronjs-parser';
import {
AlertCircle,
Bolt,
Check,
Code2,
Copy,
Settings,
Type,
} from 'lucide-react';
import { type FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@ -10,17 +21,6 @@ import {
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/presentation/utils'; import { cn } from '@/presentation/utils';
import { parse } from '@datasert/cronjs-parser';
import {
AlertCircle,
Bolt,
Check,
Code2,
Copy,
Settings,
Type,
} from 'lucide-react';
import { type FC, useCallback, useEffect, useMemo, useState } from 'react';
import { CronBuilder } from './cron-builder'; import { CronBuilder } from './cron-builder';
import { CronDisplay } from './cron-display'; import { CronDisplay } from './cron-display';
import { CronInput } from './cron-input'; import { CronInput } from './cron-input';
@ -55,7 +55,7 @@ const Cron: FC<CronProps> = ({
showPresets, showPresets,
withCard = true, withCard = true,
isFirstSibling = false, isFirstSibling = false,
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation> // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: false
}) => { }) => {
const [internalValue, setInternalValue] = useState(value || ''); const [internalValue, setInternalValue] = useState(value || '');
const [internalActiveMode, setInternalActiveMode] = const [internalActiveMode, setInternalActiveMode] =
@ -106,9 +106,9 @@ const Cron: FC<CronProps> = ({
); );
const handleActiveModeChange = useCallback( const handleActiveModeChange = useCallback(
(mode: CronPrimitiveMode) => { (m: CronPrimitiveMode) => {
setInternalActiveMode(mode); setInternalActiveMode(m);
onActiveModeChange?.(mode); onActiveModeChange?.(m);
}, },
[onActiveModeChange] [onActiveModeChange]
); );
@ -122,8 +122,8 @@ const Cron: FC<CronProps> = ({
await navigator.clipboard.writeText(internalValue); await navigator.clipboard.writeText(internalValue);
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
} catch (error) { } catch (e) {
console.warn('Failed to copy to clipboard:', error); console.warn('Failed to copy to clipboard:', e);
} }
}, [internalValue]); }, [internalValue]);
@ -241,8 +241,8 @@ const Cron: FC<CronProps> = ({
<CardContent className={cn(!withCard && 'px-0')}> <CardContent className={cn(!withCard && 'px-0')}>
<Tabs <Tabs
value={internalActiveMode} value={internalActiveMode}
onValueChange={(value) => onValueChange={(v) =>
handleActiveModeChange(value as 'input' | 'builder') handleActiveModeChange(v as 'input' | 'builder')
} }
> >
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">

View File

@ -1,20 +1,20 @@
export { Cron } from './cron'; export { Cron } from './cron';
export { CronInput } from './cron-input';
export { CronBuilder } from './cron-builder'; export { CronBuilder } from './cron-builder';
export { CronDisplay } from './cron-display'; export { CronDisplay } from './cron-display';
export { CronExample } from './cron-example'; export { CronExample } from './cron-example';
export { CronInput } from './cron-input';
export { export {
type CronProps,
type CronInputProps,
type CronBuilderProps, type CronBuilderProps,
type CronDisplayProps, type CronDisplayProps,
type CronExpression, type CronExpression,
CronField,
type CronFieldConfig,
type CronInputProps,
type CronNextRun,
CronPeriod, CronPeriod,
type CronPreset, type CronPreset,
type CronProps,
type CronValidationResult, type CronValidationResult,
type CronNextRun,
type CronFieldConfig,
CronField,
type PeriodConfig, type PeriodConfig,
} from './types'; } from './types';

View File

@ -1,6 +1,5 @@
import type { CronPreset } from '@/components/domains/cron';
import type { GetCronsQuery } from '@/infra/graphql/gql/graphql';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import type { GetCronsQuery } from '@/infra/graphql/gql/graphql';
export const GET_CRONS = gql` export const GET_CRONS = gql`
query GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) { query GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {

View File

@ -1,16 +1,16 @@
import { gql } from '@apollo/client';
import { type } from 'arktype';
import { arkValidatorToTypeNarrower } from '@/infra/errors/arktype'; import { arkValidatorToTypeNarrower } from '@/infra/errors/arktype';
import { import {
type GetSubscriptionsQuery, type GetSubscriptionsQuery,
SubscriptionCategoryEnum, SubscriptionCategoryEnum,
} from '@/infra/graphql/gql/graphql'; } from '@/infra/graphql/gql/graphql';
import { gql } from '@apollo/client';
import { type } from 'arktype';
import { import {
extractMikanSubscriptionBangumiSourceUrl,
extractMikanSubscriptionSubscriberSourceUrl,
MikanSubscriptionBangumiSourceUrlSchema, MikanSubscriptionBangumiSourceUrlSchema,
MikanSubscriptionSeasonSourceUrlSchema, MikanSubscriptionSeasonSourceUrlSchema,
MikanSubscriptionSubscriberSourceUrlSchema, MikanSubscriptionSubscriberSourceUrlSchema,
extractMikanSubscriptionBangumiSourceUrl,
extractMikanSubscriptionSubscriberSourceUrl,
} from './mikan'; } from './mikan';
export const GET_SUBSCRIPTIONS = gql` export const GET_SUBSCRIPTIONS = gql`

View File

@ -1,5 +1,5 @@
import type { GetTasksQuery } from '@/infra/graphql/gql/graphql';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import type { GetTasksQuery } from '@/infra/graphql/gql/graphql';
export const GET_TASKS = gql` export const GET_TASKS = gql`
query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) { query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {

View File

@ -0,0 +1,30 @@
type AllKeys<T> = T extends any ? keyof T : never;
type ToDefaultable<T> = Exclude<
T extends string | undefined
? T | ''
: T extends number | undefined
? T | number
: T extends undefined
? T | null
: T,
undefined
>;
type PickFieldFormUnion<T, K extends keyof T> = T extends any
? T[keyof T & K]
: never;
// compact more types;
export type FormDefaultValues<T> = {
-readonly [K in AllKeys<T>]-?: ToDefaultable<PickFieldFormUnion<T, K>>;
};
/**
* https://github.com/shadcn-ui/ui/issues/427
*/
export function compatFormDefaultValues<T, K extends AllKeys<T> = AllKeys<T>>(
d: FormDefaultValues<Pick<T, K>>
): T {
return d as unknown as T;
}

View File

@ -1,7 +1,7 @@
/* eslint-disable */ /* eslint-disable */
import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import { FragmentDefinitionNode } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql';
import { Incremental } from './graphql'; import type { Incremental } from './graphql';
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration< export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<

View File

@ -1,6 +1,6 @@
/* eslint-disable */ /* eslint-disable */
import * as types from './graphql'; import * as types from './graphql';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
/** /**
* Map of all GraphQL operations in the project. * Map of all GraphQL operations in the project.
@ -31,7 +31,7 @@ type Documents = {
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": typeof types.DeleteSubscriptionsDocument, "\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": typeof types.DeleteSubscriptionsDocument,
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument, "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument, "\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n cron {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument,
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": typeof types.InsertSubscriberTaskDocument, "\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": typeof types.InsertSubscriberTaskDocument,
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": typeof types.DeleteTasksDocument, "\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": typeof types.DeleteTasksDocument,
"\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n }\n": typeof types.RetryTasksDocument, "\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n }\n": typeof types.RetryTasksDocument,
@ -54,7 +54,7 @@ const documents: Documents = {
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": types.DeleteSubscriptionsDocument, "\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": types.DeleteSubscriptionsDocument,
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument, "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n enabled\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument, "\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n cron {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument,
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": types.InsertSubscriberTaskDocument, "\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": types.InsertSubscriberTaskDocument,
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": types.DeleteTasksDocument, "\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": types.DeleteTasksDocument,
"\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n }\n": types.RetryTasksDocument, "\n mutation RetryTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksRetryOne(filter: $filter) {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n }\n": types.RetryTasksDocument,
@ -145,7 +145,7 @@ export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subs
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function gql(source: "\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"]; export function gql(source: "\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n cron {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n cron {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
/** /**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
import { AUTH_PROVIDER } from '@/infra/auth/auth.provider'; import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context'; import { setContext } from '@apollo/client/link/context';
import { Injectable, inject } from '@outposts/injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { AUTH_PROVIDER } from '@/infra/auth/auth.provider';
@Injectable() @Injectable()
export class GraphQLService { export class GraphQLService {

View File

@ -1,3 +1,13 @@
import { useMutation } from '@apollo/client';
import {
createFileRoute,
useCanGoBack,
useNavigate,
useRouter,
} from '@tanstack/react-router';
import { type } from 'arktype';
import { Loader2, Save } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Card, Card,
@ -24,7 +34,9 @@ import {
INSERT_CREDENTIAL_3RD, INSERT_CREDENTIAL_3RD,
} from '@/domains/recorder/schema/credential3rd'; } from '@/domains/recorder/schema/credential3rd';
import { useInject } from '@/infra/di/inject'; import { useInject } from '@/infra/di/inject';
import { compatFormDefaultValues } from '@/infra/forms/compat';
import { import {
type Credential3rdInsertInput,
Credential3rdTypeEnum, Credential3rdTypeEnum,
type InsertCredential3rdMutation, type InsertCredential3rdMutation,
type InsertCredential3rdMutationVariables, type InsertCredential3rdMutationVariables,
@ -35,16 +47,6 @@ import {
CreateCompleteActionSchema, CreateCompleteActionSchema,
} from '@/infra/routes/nav'; } from '@/infra/routes/nav';
import type { RouteStateDataOption } from '@/infra/routes/traits'; import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useMutation } from '@apollo/client';
import {
createFileRoute,
useCanGoBack,
useNavigate,
useRouter,
} from '@tanstack/react-router';
import { type } from 'arktype';
import { Loader2, Save } from 'lucide-react';
import { toast } from 'sonner';
const RouteSearchSchema = type({ const RouteSearchSchema = type({
completeAction: CreateCompleteActionSchema.optional(), completeAction: CreateCompleteActionSchema.optional(),
@ -98,21 +100,24 @@ function CredentialCreateRouteComponent() {
}); });
const form = useAppForm({ const form = useAppForm({
defaultValues: { defaultValues: compatFormDefaultValues<
Credential3rdInsertInput,
'credentialType' | 'username' | 'password' | 'userAgent'
>({
credentialType: Credential3rdTypeEnum.Mikan, credentialType: Credential3rdTypeEnum.Mikan,
username: '', username: '',
password: '', password: '',
userAgent: '', userAgent: '',
}, }),
validators: { validators: {
onChangeAsync: Credential3rdInsertSchema, onChangeAsync: Credential3rdInsertSchema,
onChangeAsyncDebounceMs: 300, onChangeAsyncDebounceMs: 300,
onSubmit: Credential3rdInsertSchema, onSubmit: Credential3rdInsertSchema,
}, },
onSubmit: async (form) => { onSubmit: async (submittedForm) => {
const value = { const value = {
...form.value, ...submittedForm.value,
userAgent: form.value.userAgent || platformService.userAgent, userAgent: submittedForm.value.userAgent || platformService.userAgent,
}; };
await insertCredential3rd({ await insertCredential3rd({
variables: { variables: {
@ -183,7 +188,7 @@ function CredentialCreateRouteComponent() {
<Input <Input
id={field.name} id={field.name}
name={field.name} name={field.name}
value={field.state.value} value={field.state.value ?? ''}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
placeholder="Please enter username" placeholder="Please enter username"
@ -207,7 +212,7 @@ function CredentialCreateRouteComponent() {
id={field.name} id={field.name}
name={field.name} name={field.name}
type="password" type="password"
value={field.state.value} value={field.state.value ?? ''}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)} onChange={(e) => field.handleChange(e.target.value)}
placeholder="Please enter password" placeholder="Please enter password"

View File

@ -1,3 +1,8 @@
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute } from '@tanstack/react-router';
import { Eye, EyeOff, Save } from 'lucide-react';
import { useCallback, useState } from 'react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@ -32,18 +37,15 @@ import {
apolloErrorToMessage, apolloErrorToMessage,
getApolloQueryError, getApolloQueryError,
} from '@/infra/errors/apollo'; } from '@/infra/errors/apollo';
import { compatFormDefaultValues } from '@/infra/forms/compat';
import type { import type {
Credential3rdTypeEnum, Credential3rdTypeEnum,
Credential3rdUpdateInput,
GetCredential3rdDetailQuery, GetCredential3rdDetailQuery,
UpdateCredential3rdMutation, UpdateCredential3rdMutation,
UpdateCredential3rdMutationVariables, UpdateCredential3rdMutationVariables,
} from '@/infra/graphql/gql/graphql'; } from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits'; import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute } from '@tanstack/react-router';
import { Eye, EyeOff, Save } from 'lucide-react';
import { useCallback, useState } from 'react';
import { toast } from 'sonner';
export const Route = createFileRoute('/_app/credential3rd/edit/$id')({ export const Route = createFileRoute('/_app/credential3rd/edit/$id')({
component: Credential3rdEditRouteComponent, component: Credential3rdEditRouteComponent,
@ -77,18 +79,21 @@ function FormView({
}); });
const form = useAppForm({ const form = useAppForm({
defaultValues: { defaultValues: compatFormDefaultValues<
Credential3rdUpdateInput,
'credentialType' | 'username' | 'password' | 'userAgent'
>({
credentialType: credential.credentialType, credentialType: credential.credentialType,
username: credential.username, username: credential.username ?? '',
password: credential.password, password: credential.password ?? '',
userAgent: credential.userAgent, userAgent: credential.userAgent ?? '',
}, }),
validators: { validators: {
onBlur: Credential3rdUpdateSchema, onBlur: Credential3rdUpdateSchema,
onSubmit: Credential3rdUpdateSchema, onSubmit: Credential3rdUpdateSchema,
}, },
onSubmit: (form) => { onSubmit: (submittedForm) => {
const value = form.value; const value = submittedForm.value;
updateCredential({ updateCredential({
variables: { variables: {
data: value, data: value,
@ -238,7 +243,7 @@ function Credential3rdEditRouteComponent() {
const { loading, error, data, refetch } = const { loading, error, data, refetch } =
useQuery<GetCredential3rdDetailQuery>(GET_CREDENTIAL_3RD_DETAIL, { useQuery<GetCredential3rdDetailQuery>(GET_CREDENTIAL_3RD_DETAIL, {
variables: { variables: {
id: Number.parseInt(id), id: Number.parseInt(id, 10),
}, },
}); });
@ -246,10 +251,10 @@ function Credential3rdEditRouteComponent() {
const onCompleted = useCallback(async () => { const onCompleted = useCallback(async () => {
const refetchResult = await refetch(); const refetchResult = await refetch();
const error = getApolloQueryError(refetchResult); const _error = getApolloQueryError(refetchResult);
if (error) { if (_error) {
toast.error('Update credential failed', { toast.error('Update credential failed', {
description: apolloErrorToMessage(error), description: apolloErrorToMessage(_error),
}); });
} else { } else {
toast.success('Update credential successfully'); toast.success('Update credential successfully');

View File

@ -1,3 +1,7 @@
import { useMutation } from '@apollo/client';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { Loader2, Save } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Card, Card,
@ -27,6 +31,7 @@ import {
} from '@/domains/recorder/schema/subscriptions'; } from '@/domains/recorder/schema/subscriptions';
import { SubscriptionService } from '@/domains/recorder/services/subscription.service'; import { SubscriptionService } from '@/domains/recorder/services/subscription.service';
import { useInject } from '@/infra/di/inject'; import { useInject } from '@/infra/di/inject';
import { compatFormDefaultValues } from '@/infra/forms/compat';
import { import {
Credential3rdTypeEnum, Credential3rdTypeEnum,
type InsertSubscriptionMutation, type InsertSubscriptionMutation,
@ -34,11 +39,6 @@ import {
SubscriptionCategoryEnum, SubscriptionCategoryEnum,
} from '@/infra/graphql/gql/graphql'; } from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits'; import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useMutation } from '@apollo/client';
import { createFileRoute } from '@tanstack/react-router';
import { useNavigate } from '@tanstack/react-router';
import { Loader2, Save } from 'lucide-react';
import { toast } from 'sonner';
import { Credential3rdSelectContent } from './-credential3rd-select'; import { Credential3rdSelectContent } from './-credential3rd-select';
export const Route = createFileRoute('/_app/subscriptions/create')({ export const Route = createFileRoute('/_app/subscriptions/create')({
@ -71,22 +71,24 @@ function SubscriptionCreateRouteComponent() {
}); });
const form = useAppForm({ const form = useAppForm({
defaultValues: { defaultValues: compatFormDefaultValues<SubscriptionForm>({
displayName: '', displayName: '',
category: undefined, category: '',
enabled: true, enabled: true,
sourceUrl: '', sourceUrl: '',
credentialId: '', credentialId: Number.NaN,
year: undefined, year: Number.NaN,
seasonStr: '', seasonStr: '',
} as unknown as SubscriptionForm, }),
validators: { validators: {
onChangeAsync: SubscriptionFormSchema, onChangeAsync: SubscriptionFormSchema,
onChangeAsyncDebounceMs: 300, onChangeAsyncDebounceMs: 300,
onSubmit: SubscriptionFormSchema, onSubmit: SubscriptionFormSchema,
}, },
onSubmit: async (form) => { onSubmit: async (submittedForm) => {
const input = subscriptionService.transformInsertFormToInput(form.value); const input = subscriptionService.transformInsertFormToInput(
submittedForm.value
);
await insertSubscription({ await insertSubscription({
variables: { variables: {
data: input, data: input,
@ -119,30 +121,6 @@ function SubscriptionCreateRouteComponent() {
}} }}
className="space-y-6" className="space-y-6"
> >
<form.Field name="displayName">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Display Name *</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Please enter display name"
autoComplete="off"
/>
{field.state.meta.errors && (
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={form.state.submissionAttempts}
/>
)}
</div>
)}
</form.Field>
<form.Field name="category"> <form.Field name="category">
{(field) => ( {(field) => (
<div className="space-y-2"> <div className="space-y-2">
@ -192,7 +170,7 @@ function SubscriptionCreateRouteComponent() {
<Select <Select
value={field.state.value.toString()} value={field.state.value.toString()}
onValueChange={(value) => onValueChange={(value) =>
field.handleChange(Number.parseInt(value)) field.handleChange(Number.parseInt(value, 10))
} }
> >
<SelectTrigger> <SelectTrigger>
@ -227,7 +205,7 @@ function SubscriptionCreateRouteComponent() {
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => onChange={(e) =>
field.handleChange( field.handleChange(
Number.parseInt(e.target.value) Number.parseInt(e.target.value, 10)
) )
} }
placeholder={`Please enter full year (e.g. ${new Date().getFullYear()})`} placeholder={`Please enter full year (e.g. ${new Date().getFullYear()})`}
@ -315,6 +293,29 @@ function SubscriptionCreateRouteComponent() {
); );
}} }}
</form.Subscribe> </form.Subscribe>
<form.Field name="displayName">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Display Name *</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Please enter display name"
autoComplete="off"
/>
{field.state.meta.errors && (
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={form.state.submissionAttempts}
/>
)}
</div>
)}
</form.Field>
<form.Field name="enabled"> <form.Field name="enabled">
{(field) => ( {(field) => (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@ -1,3 +1,8 @@
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute } from '@tanstack/react-router';
import { Save } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@ -36,6 +41,7 @@ import {
apolloErrorToMessage, apolloErrorToMessage,
getApolloQueryError, getApolloQueryError,
} from '@/infra/errors/apollo'; } from '@/infra/errors/apollo';
import { compatFormDefaultValues } from '@/infra/forms/compat';
import { import {
Credential3rdTypeEnum, Credential3rdTypeEnum,
type GetSubscriptionDetailQuery, type GetSubscriptionDetailQuery,
@ -44,11 +50,6 @@ import {
type UpdateSubscriptionsMutationVariables, type UpdateSubscriptionsMutationVariables,
} from '@/infra/graphql/gql/graphql'; } from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits'; import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute } from '@tanstack/react-router';
import { Save } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { toast } from 'sonner';
import { Credential3rdSelectContent } from './-credential3rd-select'; import { Credential3rdSelectContent } from './-credential3rd-select';
export const Route = createFileRoute('/_app/subscriptions/edit/$id')({ export const Route = createFileRoute('/_app/subscriptions/edit/$id')({
@ -100,7 +101,9 @@ function FormView({
category: subscription.category, category: subscription.category,
enabled: subscription.enabled, enabled: subscription.enabled,
sourceUrl: subscription.sourceUrl, sourceUrl: subscription.sourceUrl,
credentialId: subscription.credential3rd?.id || '', credentialId: subscription.credential3rd?.id ?? Number.NaN,
year: Number.NaN,
seasonStr: '',
}; };
if ( if (
@ -118,14 +121,16 @@ function FormView({
}, [subscription, sourceUrlMeta]); }, [subscription, sourceUrlMeta]);
const form = useAppForm({ const form = useAppForm({
defaultValues: defaultValues as unknown as SubscriptionForm, defaultValues: compatFormDefaultValues<SubscriptionForm>(defaultValues),
validators: { validators: {
onChangeAsync: SubscriptionFormSchema, onChangeAsync: SubscriptionFormSchema,
onChangeAsyncDebounceMs: 300, onChangeAsyncDebounceMs: 300,
onSubmit: SubscriptionFormSchema, onSubmit: SubscriptionFormSchema,
}, },
onSubmit: async (form) => { onSubmit: async (submittedForm) => {
const input = subscriptionService.transformInsertFormToInput(form.value); const input = subscriptionService.transformInsertFormToInput(
submittedForm.value
);
await updateSubscription({ await updateSubscription({
variables: { variables: {
@ -217,7 +222,7 @@ function FormView({
<Select <Select
value={field.state.value.toString()} value={field.state.value.toString()}
onValueChange={(value) => onValueChange={(value) =>
field.handleChange(Number.parseInt(value)) field.handleChange(Number.parseInt(value, 10))
} }
> >
<SelectTrigger> <SelectTrigger>
@ -249,7 +254,9 @@ function FormView({
min={1970} min={1970}
onBlur={field.handleBlur} onBlur={field.handleBlur}
onChange={(e) => onChange={(e) =>
field.handleChange(Number.parseInt(e.target.value)) field.handleChange(
Number.parseInt(e.target.value, 10)
)
} }
placeholder={`Please enter full year (e.g. ${new Date().getFullYear()})`} placeholder={`Please enter full year (e.g. ${new Date().getFullYear()})`}
autoComplete="off" autoComplete="off"
@ -359,7 +366,7 @@ function SubscriptionEditRouteComponent() {
const { loading, error, data, refetch } = const { loading, error, data, refetch } =
useQuery<GetSubscriptionDetailQuery>(GET_SUBSCRIPTION_DETAIL, { useQuery<GetSubscriptionDetailQuery>(GET_SUBSCRIPTION_DETAIL, {
variables: { variables: {
id: Number.parseInt(id), id: Number.parseInt(id, 10),
}, },
}); });
@ -367,10 +374,10 @@ function SubscriptionEditRouteComponent() {
const onCompleted = useCallback(async () => { const onCompleted = useCallback(async () => {
const refetchResult = await refetch(); const refetchResult = await refetch();
const error = getApolloQueryError(refetchResult); const _error = getApolloQueryError(refetchResult);
if (error) { if (_error) {
toast.error('Update subscription failed', { toast.error('Update subscription failed', {
description: apolloErrorToMessage(error), description: apolloErrorToMessage(_error),
}); });
} else { } else {
toast.success('Update subscription successfully'); toast.success('Update subscription successfully');

View File

@ -1,9 +1,313 @@
import { createFileRoute } from '@tanstack/react-router' import { useQuery } from '@apollo/client';
import { createFileRoute } from '@tanstack/react-router';
import { format } from 'date-fns';
import { RefreshCw } from 'lucide-react';
import { useMemo } from 'react';
import { CronDisplay } from '@/components/domains/cron';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { ContainerHeader } from '@/components/ui/container-header';
import { DetailCardSkeleton } from '@/components/ui/detail-card-skeleton';
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
import { Label } from '@/components/ui/label';
import { QueryErrorView } from '@/components/ui/query-error-view';
import { Separator } from '@/components/ui/separator';
import { GET_CRONS } from '@/domains/recorder/schema/cron';
import {
CronStatusEnum,
type GetCronsQuery,
type GetCronsQueryVariables,
} from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { getStatusBadge } from './-status-badge';
export const Route = createFileRoute('/_app/tasks/cron/detail/$id')({ export const Route = createFileRoute('/_app/tasks/cron/detail/$id')({
component: RouteComponent, component: CronDetailRouteComponent,
}) staticData: {
breadcrumb: { label: 'Detail' },
} satisfies RouteStateDataOption,
});
function RouteComponent() { function CronDetailRouteComponent() {
return <div>Hello "/_app/tasks/cron/detail/$id"!</div> const { id } = Route.useParams();
const { data, loading, error, refetch } = useQuery<
GetCronsQuery,
GetCronsQueryVariables
>(GET_CRONS, {
variables: {
filter: {
id: {
eq: Number.parseInt(id, 10),
},
},
pagination: {
page: {
page: 0,
limit: 1,
},
},
orderBy: {},
},
pollInterval: 5000, // Auto-refresh every 5 seconds for running crons
});
const cron = data?.cron?.nodes?.[0];
const subscriberTaskCron = useMemo(() => {
if (!cron) {
return null;
}
return cron.subscriberTaskCron;
}, [cron]);
if (loading) {
return <DetailCardSkeleton />;
}
if (error) {
return <QueryErrorView message={error.message} onRetry={refetch} />;
}
if (!cron) {
return <DetailEmptyView message="Not found Cron task" />;
}
return (
<div className="container mx-auto max-w-4xl py-6">
<ContainerHeader
title="Cron task detail"
description={`View Cron task #${cron.id}`}
defaultBackTo="/tasks/cron/manage"
actions={
<Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
}
/>
<div className="space-y-6">
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Cron task information</CardTitle>
<CardDescription className="mt-2">
View Cron task execution details
</CardDescription>
</div>
<div className="flex items-center gap-2">
{getStatusBadge(cron.status)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Basic Information */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label className="font-medium text-sm">Cron ID</Label>
<div className="rounded-md bg-muted p-3">
<code className="text-sm">{cron.id}</code>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Priority</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">{cron.priority}</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Retry count</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{cron.attempts} / {cron.maxAttempts}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Enabled</Label>
<div className="rounded-md bg-muted p-3">
<Badge variant={cron.enabled ? 'default' : 'secondary'}>
{cron.enabled ? 'Enabled' : 'Disabled'}
</Badge>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Next run time</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{cron.nextRun
? format(new Date(cron.nextRun), 'yyyy-MM-dd HH:mm:ss')
: '-'}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Last run time</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{cron.lastRun
? format(new Date(cron.lastRun), 'yyyy-MM-dd HH:mm:ss')
: '-'}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Locked time</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{cron.lockedAt
? format(new Date(cron.lockedAt), 'yyyy-MM-dd HH:mm:ss')
: '-'}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Locked by</Label>
<div className="rounded-md bg-muted p-3">
<code className="text-sm">{cron.lockedBy || '-'}</code>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Timeout</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{cron.timeoutMs ? `${cron.timeoutMs}ms` : 'No limit'}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Created at</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{format(new Date(cron.createdAt), 'yyyy-MM-dd HH:mm:ss')}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Updated at</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{format(new Date(cron.updatedAt), 'yyyy-MM-dd HH:mm:ss')}
</span>
</div>
</div>
</div>
{/* Cron Expression Display */}
{cron.cronExpr && (
<>
<Separator />
<div className="space-y-2">
<Label className="font-medium text-sm">
Cron expression
</Label>
<CronDisplay
expression={cron.cronExpr}
timezone="UTC"
showDescription={true}
showNextRuns={true}
withCard={false}
/>
</div>
</>
)}
{/* Subscriber Task Details */}
{subscriberTaskCron && (
<>
<Separator />
<div className="space-y-2">
<Label className="font-medium text-sm">
Subscriber task details
</Label>
<div className="rounded-md bg-muted p-3">
<pre className="overflow-x-auto whitespace-pre-wrap text-sm">
<code>
{JSON.stringify(subscriberTaskCron, null, 2)}
</code>
</pre>
</div>
</div>
</>
)}
{/* Related Subscriber Tasks */}
{cron.subscriberTask?.nodes &&
cron.subscriberTask.nodes.length > 0 && (
<>
<Separator />
<div className="space-y-2">
<Label className="font-medium text-sm">
Associated tasks
</Label>
<div className="space-y-2">
{cron.subscriberTask.nodes.map((task, index) => (
<div
key={`${task.id}-${index}`}
className="rounded-md border bg-muted/50 p-3"
>
<div className="flex items-center justify-between">
<code className="text-sm">{task.id}</code>
<Badge variant="outline">{task.status}</Badge>
</div>
<div className="mt-2 text-muted-foreground text-sm">
Priority: {task.priority} | Retry: {task.attempts}
/{task.maxAttempts}
</div>
{task.subscription && (
<div className="mt-1 text-sm">
<span className="font-medium">
Subscription:
</span>{' '}
{task.subscription.displayName}
</div>
)}
</div>
))}
</div>
</div>
</>
)}
{/* Error Information */}
{cron.status === CronStatusEnum.Failed && cron.lastError && (
<>
<Separator />
<div className="space-y-2">
<Label className="font-medium text-sm"></Label>
<div className="rounded-md bg-destructive/10 p-3">
<p className="text-destructive text-sm">
{cron.lastError}
</p>
</div>
</div>
</>
)}
</div>
</CardContent>
</Card>
</div>
</div>
);
} }

View File

@ -1,9 +1,23 @@
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import {
type ColumnDef,
getCoreRowModel,
getPaginationRowModel,
type PaginationState,
type SortingState,
useReactTable,
type VisibilityState,
} from '@tanstack/react-table';
import { format } from 'date-fns';
import { RefreshCw } from 'lucide-react';
import { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ContainerHeader } from '@/components/ui/container-header'; import { ContainerHeader } from '@/components/ui/container-header';
import { DataTablePagination } from '@/components/ui/data-table-pagination'; import { DataTablePagination } from '@/components/ui/data-table-pagination';
import { DetailEmptyView } from '@/components/ui/detail-empty-view'; import { DetailEmptyView } from '@/components/ui/detail-empty-view';
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions'; import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
import { QueryErrorView } from '@/components/ui/query-error-view'; import { QueryErrorView } from '@/components/ui/query-error-view';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
@ -25,21 +39,6 @@ import {
} from '@/infra/graphql/gql/graphql'; } from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits'; import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton'; import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import {
type ColumnDef,
type PaginationState,
type SortingState,
type VisibilityState,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from '@tanstack/react-table';
import { format } from 'date-fns';
import { RefreshCw } from 'lucide-react';
import { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { getStatusBadge } from './-status-badge'; import { getStatusBadge } from './-status-badge';
export const Route = createFileRoute('/_app/tasks/cron/manage')({ export const Route = createFileRoute('/_app/tasks/cron/manage')({
@ -88,18 +87,18 @@ function TaskCronManageRouteComponent() {
>(DELETE_CRONS, { >(DELETE_CRONS, {
onCompleted: async () => { onCompleted: async () => {
const refetchResult = await refetch(); const refetchResult = await refetch();
const error = getApolloQueryError(refetchResult); const errorResult = getApolloQueryError(refetchResult);
if (error) { if (errorResult) {
toast.error('Failed to delete tasks', { toast.error('Failed to delete tasks', {
description: apolloErrorToMessage(error), description: apolloErrorToMessage(errorResult),
}); });
return; return;
} }
toast.success('Tasks deleted'); toast.success('Tasks deleted');
}, },
onError: (error) => { onError: (mutationError) => {
toast.error('Failed to delete tasks', { toast.error('Failed to delete tasks', {
description: error.message, description: mutationError.message,
}); });
}, },
}); });
@ -168,16 +167,16 @@ function TaskCronManageRouteComponent() {
<div className="space-y-3"> <div className="space-y-3">
{showSkeleton && {showSkeleton &&
Array.from(new Array(10)).map((_, index) => ( Array.from(new Array(10)).map((_, index) => (
<Skeleton key={index} className="h-32 w-full" /> <Skeleton key={`skeleton-${index}`} className="h-32 w-full" />
))} ))}
{!showSkeleton && table.getRowModel().rows?.length > 0 ? ( {!showSkeleton && table.getRowModel().rows?.length > 0 ? (
table.getRowModel().rows.map((row, index) => { table.getRowModel().rows.map((row) => {
const cron = row.original; const cron = row.original;
return ( return (
<div <div
className="space-y-3 rounded-lg border bg-card p-4" className="space-y-3 rounded-lg border bg-card p-4"
key={`${cron.id}-${index}`} key={cron.id}
> >
{/* Header with status and priority */} {/* Header with status and priority */}
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@ -215,17 +214,7 @@ function TaskCronManageRouteComponent() {
}, },
}) })
} }
> />
{cron.status === CronStatusEnum.Failed && (
<DropdownMenuItem
onSelect={() => {
// TODO: Retry cron
}}
>
Retry
</DropdownMenuItem>
)}
</DropdownMenuActions>
</div> </div>
</div> </div>

View File

@ -1,27 +1,32 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "$schema": "https://biomejs.dev/schemas/2.1.1/schema.json",
"extends": ["ultracite"], "extends": ["ultracite"],
"javascript": { "javascript": {
"globals": ["Liveblocks"] "globals": ["Liveblocks"]
}, },
"assist": {
"actions": {
"source": {
"useSortedAttributes": "off"
}
}
},
"linter": { "linter": {
"rules": { "rules": {
"nursery": { "nursery": {},
"noEnum": "off"
},
"style": { "style": {
"noParameterProperties": "off", "noParameterProperties": "off",
"noNonNullAssertion": "off" "noNonNullAssertion": "off",
"noEnum": "off"
}, },
"security": { "security": {
"noDangerouslySetInnerHtml": "off" "noDangerouslySetInnerHtml": "off"
}, },
"suspicious": { "suspicious": {
"noArrayIndexKey": "off",
"noEmptyBlockStatements": "off", "noEmptyBlockStatements": "off",
"noExplicitAny": "off", "noExplicitAny": "off",
"noConsole": "off", "noConsole": "off"
"noConsoleLog": "off"
}, },
"a11y": { "a11y": {
"noSvgWithoutTitle": "off" "noSvgWithoutTitle": "off"
@ -45,7 +50,7 @@
}, },
"overrides": [ "overrides": [
{ {
"include": ["**/tsconfig.json", "**/tsconfig.*.json"], "includes": ["**/tsconfig.json", "**/tsconfig.*.json"],
"json": { "json": {
"parser": { "parser": {
"allowComments": true "allowComments": true
@ -53,11 +58,17 @@
} }
}, },
{ {
"include": ["apps/webui/src/infra/graphql/gql/**/*"], "includes": ["**/apps/webui/src/infra/graphql/gql/**/*"],
"assist": {
"actions": {
"source": {
"organizeImports": "off"
}
}
},
"linter": { "linter": {
"rules": { "rules": {
"style": { "style": {
"useShorthandArrayType": "off",
"useConsistentArrayType": "off", "useConsistentArrayType": "off",
"useImportType": "off" "useImportType": "off"
} }
@ -65,7 +76,7 @@
} }
}, },
{ {
"include": ["apps/webui/src/components/ui/**/*"], "includes": ["**/apps/webui/src/components/ui/**/*"],
"javascript": { "javascript": {
"formatter": { "formatter": {
"quoteStyle": "double" "quoteStyle": "double"
@ -75,10 +86,10 @@
"rules": { "rules": {
"style": { "style": {
"useBlockStatements": "off", "useBlockStatements": "off",
"useImportType": "off" "useImportType": "off",
"noNestedTernary": "off"
}, },
"nursery": { "nursery": {
"noNestedTernary": "off",
"useSortedClasses": "off" "useSortedClasses": "off"
}, },
"a11y": { "a11y": {
@ -94,6 +105,6 @@
} }
], ],
"files": { "files": {
"ignore": [".vscode/*.json"] "includes": ["**", "!**/.vscode/**/*.json"]
} }
} }

View File

@ -3,10 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"description": "Kono bangumi?", "description": "Kono bangumi?",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": ["packages/*", "apps/*"],
"packages/*",
"apps/*"
],
"type": "module", "type": "module",
"repository": { "repository": {
"type": "git", "type": "git",
@ -18,22 +15,22 @@
"bump-deps": "npx --yes npm-check-updates --deep -u && pnpm install", "bump-deps": "npx --yes npm-check-updates --deep -u && pnpm install",
"clean": "git clean -xdf node_modules" "clean": "git clean -xdf node_modules"
}, },
"packageManager": "pnpm@10.12.1", "packageManager": "pnpm@10.12.4",
"engines": { "engines": {
"node": ">=22" "node": ">=24"
}, },
"dependencies": { "dependencies": {
"es-toolkit": "^1.39.6" "es-toolkit": "^1.39.6"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "2.1.1",
"@types/node": "^24.0.10", "@types/node": "^24.0.10",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"kill-port": "^2.0.1", "kill-port": "^2.0.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"tsx": "^4.20.3", "tsx": "^4.20.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"ultracite": "^4.2.13" "ultracite": "^5.0.32"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {

304
pnpm-lock.yaml generated
View File

@ -16,8 +16,8 @@ importers:
version: 1.39.6 version: 1.39.6
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: 1.9.4 specifier: 2.1.1
version: 1.9.4 version: 2.1.1
'@types/node': '@types/node':
specifier: ^24.0.10 specifier: ^24.0.10
version: 24.0.10 version: 24.0.10
@ -37,8 +37,8 @@ importers:
specifier: ^5.8.3 specifier: ^5.8.3
version: 5.8.3 version: 5.8.3
ultracite: ultracite:
specifier: ^4.2.13 specifier: ^5.0.32
version: 4.2.13 version: 5.0.32(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
apps/docs: {} apps/docs: {}
@ -601,59 +601,65 @@ packages:
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==} resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@biomejs/biome@1.9.4': '@biomejs/biome@2.1.1':
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} resolution: {integrity: sha512-HFGYkxG714KzG+8tvtXCJ1t1qXQMzgWzfvQaUjxN6UeKv+KvMEuliInnbZLJm6DXFXwqVi6446EGI0sGBLIYng==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
hasBin: true hasBin: true
'@biomejs/cli-darwin-arm64@1.9.4': '@biomejs/cli-darwin-arm64@2.1.1':
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} resolution: {integrity: sha512-2Muinu5ok4tWxq4nu5l19el48cwCY/vzvI7Vjbkf3CYIQkjxZLyj0Ad37Jv2OtlXYaLvv+Sfu1hFeXt/JwRRXQ==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@biomejs/cli-darwin-x64@1.9.4': '@biomejs/cli-darwin-x64@2.1.1':
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} resolution: {integrity: sha512-cC8HM5lrgKQXLAK+6Iz2FrYW5A62pAAX6KAnRlEyLb+Q3+Kr6ur/sSuoIacqlp1yvmjHJqjYfZjPvHWnqxoEIA==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.9.4': '@biomejs/cli-linux-arm64-musl@2.1.1':
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} resolution: {integrity: sha512-/7FBLnTswu4jgV9ttI3AMIdDGqVEPIZd8I5u2D4tfCoj8rl9dnjrEQbAIDlWhUXdyWlFSz8JypH3swU9h9P+2A==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-arm64@1.9.4': '@biomejs/cli-linux-arm64@2.1.1':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} resolution: {integrity: sha512-tw4BEbhAUkWPe4WBr6IX04DJo+2jz5qpPzpW/SWvqMjb9QuHY8+J0M23V8EPY/zWU4IG8Ui0XESapR1CB49Q7g==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64-musl@1.9.4': '@biomejs/cli-linux-x64-musl@2.1.1':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} resolution: {integrity: sha512-kUu+loNI3OCD2c12cUt7M5yaaSjDnGIksZwKnueubX6c/HWUyi/0mPbTBHR49Me3F0KKjWiKM+ZOjsmC+lUt9g==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64@1.9.4': '@biomejs/cli-linux-x64@2.1.1':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} resolution: {integrity: sha512-3WJ1GKjU7NzZb6RTbwLB59v9cTIlzjbiFLDB0z4376TkDqoNYilJaC37IomCr/aXwuU8QKkrYoHrgpSq5ffJ4Q==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-win32-arm64@1.9.4': '@biomejs/cli-win32-arm64@2.1.1':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} resolution: {integrity: sha512-vEHK0v0oW+E6RUWLoxb2isI3rZo57OX9ZNyyGH701fZPj6Il0Rn1f5DMNyCmyflMwTnIQstEbs7n2BxYSqQx4Q==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@biomejs/cli-win32-x64@1.9.4': '@biomejs/cli-win32-x64@2.1.1':
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} resolution: {integrity: sha512-i2PKdn70kY++KEF/zkQFvQfX1e8SkA8hq4BgC+yE9dZqyLzB/XStY2MvwI3qswlRgnGpgncgqe0QYKVS1blksg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@clack/core@0.5.0':
resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==}
'@clack/prompts@0.11.0':
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
'@codemirror/language@6.11.1': '@codemirror/language@6.11.1':
resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==} resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==}
@ -3211,6 +3217,9 @@ packages:
'@vitest/expect@3.2.3': '@vitest/expect@3.2.3':
resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==} resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==}
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
'@vitest/mocker@3.2.3': '@vitest/mocker@3.2.3':
resolution: {integrity: sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==} resolution: {integrity: sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==}
peerDependencies: peerDependencies:
@ -3222,21 +3231,47 @@ packages:
vite: vite:
optional: true optional: true
'@vitest/mocker@3.2.4':
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@3.2.3': '@vitest/pretty-format@3.2.3':
resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==} resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==}
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/runner@3.2.3': '@vitest/runner@3.2.3':
resolution: {integrity: sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==} resolution: {integrity: sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==}
'@vitest/runner@3.2.4':
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
'@vitest/snapshot@3.2.3': '@vitest/snapshot@3.2.3':
resolution: {integrity: sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==} resolution: {integrity: sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==}
'@vitest/snapshot@3.2.4':
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
'@vitest/spy@3.2.3': '@vitest/spy@3.2.3':
resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==}
'@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
'@vitest/utils@3.2.3': '@vitest/utils@3.2.3':
resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==}
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
'@webassemblyjs/ast@1.14.1': '@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
@ -4876,6 +4911,9 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsonfile@2.4.0: jsonfile@2.4.0:
resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==}
@ -5002,6 +5040,9 @@ packages:
loupe@3.1.3: loupe@3.1.3:
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
loupe@3.1.4:
resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==}
lower-case-first@2.0.2: lower-case-first@2.0.2:
resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==}
@ -5952,6 +5993,9 @@ packages:
simple-swizzle@0.2.2: simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slash@3.0.0: slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -6260,6 +6304,10 @@ packages:
resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
tinypool@1.1.1:
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyrainbow@2.0.0: tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -6384,8 +6432,8 @@ packages:
uc.micro@2.1.0: uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ultracite@4.2.13: ultracite@5.0.32:
resolution: {integrity: sha512-j49R1z3xXIPhdvU19x0z0Z4hNewJYn4F1h42ULeaCOylBuxwGVE401piPxe3aVapwue7+Ec3J6wnL/+mW4zwww==} resolution: {integrity: sha512-JjVNswL1mkIaOkPVh1nuEGnbEaCa94+ftqJ9hpRX2Y+jt72pcv32JeWg3Dqhkz/e3l449f7KAzMHO4x6IbxFZQ==}
hasBin: true hasBin: true
unbox-primitive@1.1.0: unbox-primitive@1.1.0:
@ -6492,6 +6540,11 @@ packages:
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true hasBin: true
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite@5.4.11: vite@5.4.11:
resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
@ -6551,6 +6604,34 @@ packages:
jsdom: jsdom:
optional: true optional: true
vitest@3.2.4:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
'@vitest/browser': 3.2.4
'@vitest/ui': 3.2.4
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/debug':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
vscode-languageserver-types@3.17.5: vscode-languageserver-types@3.17.5:
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
@ -7133,41 +7214,52 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1 '@babel/helper-validator-identifier': 7.27.1
'@biomejs/biome@1.9.4': '@biomejs/biome@2.1.1':
optionalDependencies: optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.9.4 '@biomejs/cli-darwin-arm64': 2.1.1
'@biomejs/cli-darwin-x64': 1.9.4 '@biomejs/cli-darwin-x64': 2.1.1
'@biomejs/cli-linux-arm64': 1.9.4 '@biomejs/cli-linux-arm64': 2.1.1
'@biomejs/cli-linux-arm64-musl': 1.9.4 '@biomejs/cli-linux-arm64-musl': 2.1.1
'@biomejs/cli-linux-x64': 1.9.4 '@biomejs/cli-linux-x64': 2.1.1
'@biomejs/cli-linux-x64-musl': 1.9.4 '@biomejs/cli-linux-x64-musl': 2.1.1
'@biomejs/cli-win32-arm64': 1.9.4 '@biomejs/cli-win32-arm64': 2.1.1
'@biomejs/cli-win32-x64': 1.9.4 '@biomejs/cli-win32-x64': 2.1.1
'@biomejs/cli-darwin-arm64@1.9.4': '@biomejs/cli-darwin-arm64@2.1.1':
optional: true optional: true
'@biomejs/cli-darwin-x64@1.9.4': '@biomejs/cli-darwin-x64@2.1.1':
optional: true optional: true
'@biomejs/cli-linux-arm64-musl@1.9.4': '@biomejs/cli-linux-arm64-musl@2.1.1':
optional: true optional: true
'@biomejs/cli-linux-arm64@1.9.4': '@biomejs/cli-linux-arm64@2.1.1':
optional: true optional: true
'@biomejs/cli-linux-x64-musl@1.9.4': '@biomejs/cli-linux-x64-musl@2.1.1':
optional: true optional: true
'@biomejs/cli-linux-x64@1.9.4': '@biomejs/cli-linux-x64@2.1.1':
optional: true optional: true
'@biomejs/cli-win32-arm64@1.9.4': '@biomejs/cli-win32-arm64@2.1.1':
optional: true optional: true
'@biomejs/cli-win32-x64@1.9.4': '@biomejs/cli-win32-x64@2.1.1':
optional: true optional: true
'@clack/core@0.5.0':
dependencies:
picocolors: 1.1.1
sisteransi: 1.0.5
'@clack/prompts@0.11.0':
dependencies:
'@clack/core': 0.5.0
picocolors: 1.1.1
sisteransi: 1.0.5
'@codemirror/language@6.11.1': '@codemirror/language@6.11.1':
dependencies: dependencies:
'@codemirror/state': 6.5.2 '@codemirror/state': 6.5.2
@ -9898,6 +9990,14 @@ snapshots:
chai: 5.2.0 chai: 5.2.0
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.2
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.3(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))': '@vitest/mocker@3.2.3(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))':
dependencies: dependencies:
'@vitest/spy': 3.2.3 '@vitest/spy': 3.2.3
@ -9906,32 +10006,66 @@ snapshots:
optionalDependencies: optionalDependencies:
vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1) vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
'@vitest/mocker@3.2.4(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
'@vitest/pretty-format@3.2.3': '@vitest/pretty-format@3.2.3':
dependencies: dependencies:
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.2.3': '@vitest/runner@3.2.3':
dependencies: dependencies:
'@vitest/utils': 3.2.3 '@vitest/utils': 3.2.3
pathe: 2.0.3 pathe: 2.0.3
strip-literal: 3.0.0 strip-literal: 3.0.0
'@vitest/runner@3.2.4':
dependencies:
'@vitest/utils': 3.2.4
pathe: 2.0.3
strip-literal: 3.0.0
'@vitest/snapshot@3.2.3': '@vitest/snapshot@3.2.3':
dependencies: dependencies:
'@vitest/pretty-format': 3.2.3 '@vitest/pretty-format': 3.2.3
magic-string: 0.30.17 magic-string: 0.30.17
pathe: 2.0.3 pathe: 2.0.3
'@vitest/snapshot@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
magic-string: 0.30.17
pathe: 2.0.3
'@vitest/spy@3.2.3': '@vitest/spy@3.2.3':
dependencies: dependencies:
tinyspy: 4.0.3 tinyspy: 4.0.3
'@vitest/spy@3.2.4':
dependencies:
tinyspy: 4.0.3
'@vitest/utils@3.2.3': '@vitest/utils@3.2.3':
dependencies: dependencies:
'@vitest/pretty-format': 3.2.3 '@vitest/pretty-format': 3.2.3
loupe: 3.1.3 loupe: 3.1.3
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
loupe: 3.1.4
tinyrainbow: 2.0.0
'@webassemblyjs/ast@1.14.1': '@webassemblyjs/ast@1.14.1':
dependencies: dependencies:
'@webassemblyjs/helper-numbers': 1.13.2 '@webassemblyjs/helper-numbers': 1.13.2
@ -11790,6 +11924,8 @@ snapshots:
json5@2.2.3: {} json5@2.2.3: {}
jsonc-parser@3.3.1: {}
jsonfile@2.4.0: jsonfile@2.4.0:
optionalDependencies: optionalDependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@ -11907,6 +12043,8 @@ snapshots:
loupe@3.1.3: {} loupe@3.1.3: {}
loupe@3.1.4: {}
lower-case-first@2.0.2: lower-case-first@2.0.2:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@ -12943,6 +13081,8 @@ snapshots:
is-arrayish: 0.3.2 is-arrayish: 0.3.2
optional: true optional: true
sisteransi@1.0.5: {}
slash@3.0.0: {} slash@3.0.0: {}
slice-ansi@3.0.0: slice-ansi@3.0.0:
@ -13264,6 +13404,8 @@ snapshots:
tinypool@1.1.0: {} tinypool@1.1.0: {}
tinypool@1.1.1: {}
tinyrainbow@2.0.0: {} tinyrainbow@2.0.0: {}
tinyspy@4.0.3: {} tinyspy@4.0.3: {}
@ -13390,9 +13532,30 @@ snapshots:
uc.micro@2.1.0: {} uc.micro@2.1.0: {}
ultracite@4.2.13: ultracite@5.0.32(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1):
dependencies: dependencies:
'@clack/prompts': 0.11.0
commander: 14.0.0 commander: 14.0.0
deepmerge: 4.3.1
jsonc-parser: 3.3.1
vitest: 3.2.4(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
transitivePeerDependencies:
- '@edge-runtime/vm'
- '@types/debug'
- '@types/node'
- '@vitest/browser'
- '@vitest/ui'
- happy-dom
- jsdom
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
unbox-primitive@1.1.0: unbox-primitive@1.1.0:
dependencies: dependencies:
@ -13527,6 +13690,24 @@ snapshots:
- supports-color - supports-color
- terser - terser
vite-node@3.2.4(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1):
dependencies:
cac: 6.7.14
debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1): vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1):
dependencies: dependencies:
esbuild: 0.21.5 esbuild: 0.21.5
@ -13578,6 +13759,45 @@ snapshots:
- supports-color - supports-color
- terser - terser
vitest@3.2.4(@types/node@24.0.10)(jsdom@25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.2.0
debug: 4.4.1
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 2.0.3
picomatch: 4.0.2
std-env: 3.9.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.14
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 5.4.11(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
vite-node: 3.2.4(@types/node@24.0.10)(lightningcss@1.30.1)(sass@1.77.4)(terser@5.43.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.0.10
jsdom: 25.0.1(bufferutil@4.0.9)(utf-8-validate@6.0.5)
transitivePeerDependencies:
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
vscode-languageserver-types@3.17.5: {} vscode-languageserver-types@3.17.5: {}
w3c-keyname@2.2.8: {} w3c-keyname@2.2.8: {}