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: {
enumsAsConst: true,
useTypeImports: true,
scalars: {
SubscriberTaskType: {
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 { Button } from '@/components/ui/button';
import {
@ -20,16 +31,6 @@ import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
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 {
type CronBuilderProps,
CronField,
@ -345,7 +346,7 @@ const CronBuilder: FC<CronBuilderProps> = ({
<div className={cn(withCard && 'space-y-6', className)}>
<Tabs
value={activeTab}
onValueChange={(value) => handlePeriodChange(value as CronPeriod)}
onValueChange={(v) => handlePeriodChange(v as CronPeriod)}
>
<div className="overflow-x-auto">
<TabsList
@ -516,15 +517,114 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
const currentValue = fields[field];
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">
{field.replace(/([A-Z])/g, ' $1').toLowerCase()}
</Label>
{field === 'month' || field === 'dayOfWeek' ? (
{(field === 'month' || field === 'dayOfWeek') && (
<Select
value={currentValue}
onValueChange={(value) => onChange(field, value)}
value={innerValue}
onValueChange={handleChange}
disabled={disabled}
>
<SelectTrigger>
@ -539,12 +639,12 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
))}
</SelectContent>
</Select>
// biome-ignore lint/nursery/noNestedTernary: <explanation>
) : field === 'dayOfMonth' ? (
)}
{field === 'dayOfMonth' && (
<div className="space-y-2">
<Select
value={currentValue}
onValueChange={(value) => onChange(field, value)}
value={innerValue}
onValueChange={handleChange}
disabled={disabled}
>
<SelectTrigger>
@ -564,36 +664,39 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
</SelectContent>
</Select>
</div>
) : (
)}
{!(
field === 'month' ||
field === 'dayOfWeek' ||
field === 'dayOfMonth'
) && (
<div className="space-y-2">
<ToggleGroup
type="single"
value={currentValue === '*' ? '*' : 'specific'}
onValueChange={(value) => {
if (value === '*') {
onChange(field, '*');
} else if (value === 'specific' && currentValue === '*') {
onChange(field, '0');
}
}}
value={anyOrSpecificOption}
onValueChange={setAnyOrSpecificOption}
disabled={disabled}
>
<ToggleGroupItem value="*" className="min-w-fit text-xs">
<ToggleGroupItem
value={CronFieldItemAnyOrSpecificOption.Any}
className="min-w-fit text-xs"
>
Any
</ToggleGroupItem>
<ToggleGroupItem
value="specific"
value={CronFieldItemAnyOrSpecificOption.Specific}
className="min-w-fit text-xs"
>
Specific
</ToggleGroupItem>
</ToggleGroup>
{currentValue !== '*' && (
{anyOrSpecificOption ===
CronFieldItemAnyOrSpecificOption.Specific && (
<Input
type="text"
value={currentValue}
onChange={(e) => onChange(field, e.target.value)}
value={innerValue}
onChange={(e) => handleChange(e.target.value)}
placeholder={`0-${config.max}`}
disabled={disabled}
className="font-mono text-sm"
@ -608,18 +711,15 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
</span>
</div>
<div className="mt-1">
Supports: *, numbers, ranges (1-5), lists (1,3,5), steps
(*/5)
Supports: *, numbers, ranges (1-5), lists (1,3,5), steps (*/5)
</div>
</div>
</div>
)}
</div>
);
})}
</div>
);
};
}
);
function parseCronExpression(expression: string): Record<CronField, string> {
const parts = expression.split(' ');

View File

@ -33,7 +33,11 @@ const CronInput = forwardRef<HTMLInputElement, CronInputProps>(
const validationResult = useMemo((): CronValidationResult => {
if (!internalValue.trim()) {
return { isValid: false, error: 'Expression is required' };
return {
isValid: false,
error: 'Expression is required',
isEmpty: true,
};
}
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 { Button } from '@/components/ui/button';
import {
@ -10,17 +21,6 @@ import {
import { Separator } from '@/components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
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 { CronDisplay } from './cron-display';
import { CronInput } from './cron-input';
@ -55,7 +55,7 @@ const Cron: FC<CronProps> = ({
showPresets,
withCard = true,
isFirstSibling = false,
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: false
}) => {
const [internalValue, setInternalValue] = useState(value || '');
const [internalActiveMode, setInternalActiveMode] =
@ -106,9 +106,9 @@ const Cron: FC<CronProps> = ({
);
const handleActiveModeChange = useCallback(
(mode: CronPrimitiveMode) => {
setInternalActiveMode(mode);
onActiveModeChange?.(mode);
(m: CronPrimitiveMode) => {
setInternalActiveMode(m);
onActiveModeChange?.(m);
},
[onActiveModeChange]
);
@ -122,8 +122,8 @@ const Cron: FC<CronProps> = ({
await navigator.clipboard.writeText(internalValue);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.warn('Failed to copy to clipboard:', error);
} catch (e) {
console.warn('Failed to copy to clipboard:', e);
}
}, [internalValue]);
@ -241,8 +241,8 @@ const Cron: FC<CronProps> = ({
<CardContent className={cn(!withCard && 'px-0')}>
<Tabs
value={internalActiveMode}
onValueChange={(value) =>
handleActiveModeChange(value as 'input' | 'builder')
onValueChange={(v) =>
handleActiveModeChange(v as 'input' | 'builder')
}
>
<TabsList className="grid w-full grid-cols-2">

View File

@ -1,20 +1,20 @@
export { Cron } from './cron';
export { CronInput } from './cron-input';
export { CronBuilder } from './cron-builder';
export { CronDisplay } from './cron-display';
export { CronExample } from './cron-example';
export { CronInput } from './cron-input';
export {
type CronProps,
type CronInputProps,
type CronBuilderProps,
type CronDisplayProps,
type CronExpression,
CronField,
type CronFieldConfig,
type CronInputProps,
type CronNextRun,
CronPeriod,
type CronPreset,
type CronProps,
type CronValidationResult,
type CronNextRun,
type CronFieldConfig,
CronField,
type PeriodConfig,
} 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 type { GetCronsQuery } from '@/infra/graphql/gql/graphql';
export const GET_CRONS = gql`
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 {
type GetSubscriptionsQuery,
SubscriptionCategoryEnum,
} from '@/infra/graphql/gql/graphql';
import { gql } from '@apollo/client';
import { type } from 'arktype';
import {
extractMikanSubscriptionBangumiSourceUrl,
extractMikanSubscriptionSubscriberSourceUrl,
MikanSubscriptionBangumiSourceUrlSchema,
MikanSubscriptionSeasonSourceUrlSchema,
MikanSubscriptionSubscriberSourceUrlSchema,
extractMikanSubscriptionBangumiSourceUrl,
extractMikanSubscriptionSubscriberSourceUrl,
} from './mikan';
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 type { GetTasksQuery } from '@/infra/graphql/gql/graphql';
export const GET_TASKS = gql`
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 */
import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import { FragmentDefinitionNode } from 'graphql';
import { Incremental } from './graphql';
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { FragmentDefinitionNode } from 'graphql';
import type { Incremental } from './graphql';
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<

View File

@ -1,6 +1,6 @@
/* eslint-disable */
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.
@ -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 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,
"\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 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,
@ -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 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,
"\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 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,
@ -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.
*/
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.
*/

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, InMemoryCache, createHttpLink } from '@apollo/client';
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { Injectable, inject } from '@outposts/injection-js';
import { firstValueFrom } from 'rxjs';
import { AUTH_PROVIDER } from '@/infra/auth/auth.provider';
@Injectable()
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 {
Card,
@ -24,7 +34,9 @@ import {
INSERT_CREDENTIAL_3RD,
} from '@/domains/recorder/schema/credential3rd';
import { useInject } from '@/infra/di/inject';
import { compatFormDefaultValues } from '@/infra/forms/compat';
import {
type Credential3rdInsertInput,
Credential3rdTypeEnum,
type InsertCredential3rdMutation,
type InsertCredential3rdMutationVariables,
@ -35,16 +47,6 @@ import {
CreateCompleteActionSchema,
} from '@/infra/routes/nav';
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({
completeAction: CreateCompleteActionSchema.optional(),
@ -98,21 +100,24 @@ function CredentialCreateRouteComponent() {
});
const form = useAppForm({
defaultValues: {
defaultValues: compatFormDefaultValues<
Credential3rdInsertInput,
'credentialType' | 'username' | 'password' | 'userAgent'
>({
credentialType: Credential3rdTypeEnum.Mikan,
username: '',
password: '',
userAgent: '',
},
}),
validators: {
onChangeAsync: Credential3rdInsertSchema,
onChangeAsyncDebounceMs: 300,
onSubmit: Credential3rdInsertSchema,
},
onSubmit: async (form) => {
onSubmit: async (submittedForm) => {
const value = {
...form.value,
userAgent: form.value.userAgent || platformService.userAgent,
...submittedForm.value,
userAgent: submittedForm.value.userAgent || platformService.userAgent,
};
await insertCredential3rd({
variables: {
@ -183,7 +188,7 @@ function CredentialCreateRouteComponent() {
<Input
id={field.name}
name={field.name}
value={field.state.value}
value={field.state.value ?? ''}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Please enter username"
@ -207,7 +212,7 @@ function CredentialCreateRouteComponent() {
id={field.name}
name={field.name}
type="password"
value={field.state.value}
value={field.state.value ?? ''}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
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 { Button } from '@/components/ui/button';
import {
@ -32,18 +37,15 @@ import {
apolloErrorToMessage,
getApolloQueryError,
} from '@/infra/errors/apollo';
import { compatFormDefaultValues } from '@/infra/forms/compat';
import type {
Credential3rdTypeEnum,
Credential3rdUpdateInput,
GetCredential3rdDetailQuery,
UpdateCredential3rdMutation,
UpdateCredential3rdMutationVariables,
} from '@/infra/graphql/gql/graphql';
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')({
component: Credential3rdEditRouteComponent,
@ -77,18 +79,21 @@ function FormView({
});
const form = useAppForm({
defaultValues: {
defaultValues: compatFormDefaultValues<
Credential3rdUpdateInput,
'credentialType' | 'username' | 'password' | 'userAgent'
>({
credentialType: credential.credentialType,
username: credential.username,
password: credential.password,
userAgent: credential.userAgent,
},
username: credential.username ?? '',
password: credential.password ?? '',
userAgent: credential.userAgent ?? '',
}),
validators: {
onBlur: Credential3rdUpdateSchema,
onSubmit: Credential3rdUpdateSchema,
},
onSubmit: (form) => {
const value = form.value;
onSubmit: (submittedForm) => {
const value = submittedForm.value;
updateCredential({
variables: {
data: value,
@ -238,7 +243,7 @@ function Credential3rdEditRouteComponent() {
const { loading, error, data, refetch } =
useQuery<GetCredential3rdDetailQuery>(GET_CREDENTIAL_3RD_DETAIL, {
variables: {
id: Number.parseInt(id),
id: Number.parseInt(id, 10),
},
});
@ -246,10 +251,10 @@ function Credential3rdEditRouteComponent() {
const onCompleted = useCallback(async () => {
const refetchResult = await refetch();
const error = getApolloQueryError(refetchResult);
if (error) {
const _error = getApolloQueryError(refetchResult);
if (_error) {
toast.error('Update credential failed', {
description: apolloErrorToMessage(error),
description: apolloErrorToMessage(_error),
});
} else {
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 {
Card,
@ -27,6 +31,7 @@ import {
} from '@/domains/recorder/schema/subscriptions';
import { SubscriptionService } from '@/domains/recorder/services/subscription.service';
import { useInject } from '@/infra/di/inject';
import { compatFormDefaultValues } from '@/infra/forms/compat';
import {
Credential3rdTypeEnum,
type InsertSubscriptionMutation,
@ -34,11 +39,6 @@ import {
SubscriptionCategoryEnum,
} from '@/infra/graphql/gql/graphql';
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';
export const Route = createFileRoute('/_app/subscriptions/create')({
@ -71,22 +71,24 @@ function SubscriptionCreateRouteComponent() {
});
const form = useAppForm({
defaultValues: {
defaultValues: compatFormDefaultValues<SubscriptionForm>({
displayName: '',
category: undefined,
category: '',
enabled: true,
sourceUrl: '',
credentialId: '',
year: undefined,
credentialId: Number.NaN,
year: Number.NaN,
seasonStr: '',
} as unknown as SubscriptionForm,
}),
validators: {
onChangeAsync: SubscriptionFormSchema,
onChangeAsyncDebounceMs: 300,
onSubmit: SubscriptionFormSchema,
},
onSubmit: async (form) => {
const input = subscriptionService.transformInsertFormToInput(form.value);
onSubmit: async (submittedForm) => {
const input = subscriptionService.transformInsertFormToInput(
submittedForm.value
);
await insertSubscription({
variables: {
data: input,
@ -119,30 +121,6 @@ function SubscriptionCreateRouteComponent() {
}}
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">
{(field) => (
<div className="space-y-2">
@ -192,7 +170,7 @@ function SubscriptionCreateRouteComponent() {
<Select
value={field.state.value.toString()}
onValueChange={(value) =>
field.handleChange(Number.parseInt(value))
field.handleChange(Number.parseInt(value, 10))
}
>
<SelectTrigger>
@ -227,7 +205,7 @@ function SubscriptionCreateRouteComponent() {
onBlur={field.handleBlur}
onChange={(e) =>
field.handleChange(
Number.parseInt(e.target.value)
Number.parseInt(e.target.value, 10)
)
}
placeholder={`Please enter full year (e.g. ${new Date().getFullYear()})`}
@ -315,6 +293,29 @@ function SubscriptionCreateRouteComponent() {
);
}}
</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">
{(field) => (
<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 { Button } from '@/components/ui/button';
import {
@ -36,6 +41,7 @@ import {
apolloErrorToMessage,
getApolloQueryError,
} from '@/infra/errors/apollo';
import { compatFormDefaultValues } from '@/infra/forms/compat';
import {
Credential3rdTypeEnum,
type GetSubscriptionDetailQuery,
@ -44,11 +50,6 @@ import {
type UpdateSubscriptionsMutationVariables,
} from '@/infra/graphql/gql/graphql';
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';
export const Route = createFileRoute('/_app/subscriptions/edit/$id')({
@ -100,7 +101,9 @@ function FormView({
category: subscription.category,
enabled: subscription.enabled,
sourceUrl: subscription.sourceUrl,
credentialId: subscription.credential3rd?.id || '',
credentialId: subscription.credential3rd?.id ?? Number.NaN,
year: Number.NaN,
seasonStr: '',
};
if (
@ -118,14 +121,16 @@ function FormView({
}, [subscription, sourceUrlMeta]);
const form = useAppForm({
defaultValues: defaultValues as unknown as SubscriptionForm,
defaultValues: compatFormDefaultValues<SubscriptionForm>(defaultValues),
validators: {
onChangeAsync: SubscriptionFormSchema,
onChangeAsyncDebounceMs: 300,
onSubmit: SubscriptionFormSchema,
},
onSubmit: async (form) => {
const input = subscriptionService.transformInsertFormToInput(form.value);
onSubmit: async (submittedForm) => {
const input = subscriptionService.transformInsertFormToInput(
submittedForm.value
);
await updateSubscription({
variables: {
@ -217,7 +222,7 @@ function FormView({
<Select
value={field.state.value.toString()}
onValueChange={(value) =>
field.handleChange(Number.parseInt(value))
field.handleChange(Number.parseInt(value, 10))
}
>
<SelectTrigger>
@ -249,7 +254,9 @@ function FormView({
min={1970}
onBlur={field.handleBlur}
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()})`}
autoComplete="off"
@ -359,7 +366,7 @@ function SubscriptionEditRouteComponent() {
const { loading, error, data, refetch } =
useQuery<GetSubscriptionDetailQuery>(GET_SUBSCRIPTION_DETAIL, {
variables: {
id: Number.parseInt(id),
id: Number.parseInt(id, 10),
},
});
@ -367,10 +374,10 @@ function SubscriptionEditRouteComponent() {
const onCompleted = useCallback(async () => {
const refetchResult = await refetch();
const error = getApolloQueryError(refetchResult);
if (error) {
const _error = getApolloQueryError(refetchResult);
if (_error) {
toast.error('Update subscription failed', {
description: apolloErrorToMessage(error),
description: apolloErrorToMessage(_error),
});
} else {
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')({
component: RouteComponent,
})
component: CronDetailRouteComponent,
staticData: {
breadcrumb: { label: 'Detail' },
} satisfies RouteStateDataOption,
});
function RouteComponent() {
return <div>Hello "/_app/tasks/cron/detail/$id"!</div>
function CronDetailRouteComponent() {
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 { Button } from '@/components/ui/button';
import { ContainerHeader } from '@/components/ui/container-header';
import { DataTablePagination } from '@/components/ui/data-table-pagination';
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
import { QueryErrorView } from '@/components/ui/query-error-view';
import { Skeleton } from '@/components/ui/skeleton';
@ -25,21 +39,6 @@ import {
} from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits';
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';
export const Route = createFileRoute('/_app/tasks/cron/manage')({
@ -88,18 +87,18 @@ function TaskCronManageRouteComponent() {
>(DELETE_CRONS, {
onCompleted: async () => {
const refetchResult = await refetch();
const error = getApolloQueryError(refetchResult);
if (error) {
const errorResult = getApolloQueryError(refetchResult);
if (errorResult) {
toast.error('Failed to delete tasks', {
description: apolloErrorToMessage(error),
description: apolloErrorToMessage(errorResult),
});
return;
}
toast.success('Tasks deleted');
},
onError: (error) => {
onError: (mutationError) => {
toast.error('Failed to delete tasks', {
description: error.message,
description: mutationError.message,
});
},
});
@ -168,16 +167,16 @@ function TaskCronManageRouteComponent() {
<div className="space-y-3">
{showSkeleton &&
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 ? (
table.getRowModel().rows.map((row, index) => {
table.getRowModel().rows.map((row) => {
const cron = row.original;
return (
<div
className="space-y-3 rounded-lg border bg-card p-4"
key={`${cron.id}-${index}`}
key={cron.id}
>
{/* Header with status and priority */}
<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>

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"],
"javascript": {
"globals": ["Liveblocks"]
},
"assist": {
"actions": {
"source": {
"useSortedAttributes": "off"
}
}
},
"linter": {
"rules": {
"nursery": {
"noEnum": "off"
},
"nursery": {},
"style": {
"noParameterProperties": "off",
"noNonNullAssertion": "off"
"noNonNullAssertion": "off",
"noEnum": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"suspicious": {
"noArrayIndexKey": "off",
"noEmptyBlockStatements": "off",
"noExplicitAny": "off",
"noConsole": "off",
"noConsoleLog": "off"
"noConsole": "off"
},
"a11y": {
"noSvgWithoutTitle": "off"
@ -45,7 +50,7 @@
},
"overrides": [
{
"include": ["**/tsconfig.json", "**/tsconfig.*.json"],
"includes": ["**/tsconfig.json", "**/tsconfig.*.json"],
"json": {
"parser": {
"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": {
"rules": {
"style": {
"useShorthandArrayType": "off",
"useConsistentArrayType": "off",
"useImportType": "off"
}
@ -65,7 +76,7 @@
}
},
{
"include": ["apps/webui/src/components/ui/**/*"],
"includes": ["**/apps/webui/src/components/ui/**/*"],
"javascript": {
"formatter": {
"quoteStyle": "double"
@ -75,10 +86,10 @@
"rules": {
"style": {
"useBlockStatements": "off",
"useImportType": "off"
"useImportType": "off",
"noNestedTernary": "off"
},
"nursery": {
"noNestedTernary": "off",
"useSortedClasses": "off"
},
"a11y": {
@ -94,6 +105,6 @@
}
],
"files": {
"ignore": [".vscode/*.json"]
"includes": ["**", "!**/.vscode/**/*.json"]
}
}

View File

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

304
pnpm-lock.yaml generated
View File

@ -16,8 +16,8 @@ importers:
version: 1.39.6
devDependencies:
'@biomejs/biome':
specifier: 1.9.4
version: 1.9.4
specifier: 2.1.1
version: 2.1.1
'@types/node':
specifier: ^24.0.10
version: 24.0.10
@ -37,8 +37,8 @@ importers:
specifier: ^5.8.3
version: 5.8.3
ultracite:
specifier: ^4.2.13
version: 4.2.13
specifier: ^5.0.32
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: {}
@ -601,59 +601,65 @@ packages:
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
engines: {node: '>=6.9.0'}
'@biomejs/biome@1.9.4':
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
'@biomejs/biome@2.1.1':
resolution: {integrity: sha512-HFGYkxG714KzG+8tvtXCJ1t1qXQMzgWzfvQaUjxN6UeKv+KvMEuliInnbZLJm6DXFXwqVi6446EGI0sGBLIYng==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@1.9.4':
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
'@biomejs/cli-darwin-arm64@2.1.1':
resolution: {integrity: sha512-2Muinu5ok4tWxq4nu5l19el48cwCY/vzvI7Vjbkf3CYIQkjxZLyj0Ad37Jv2OtlXYaLvv+Sfu1hFeXt/JwRRXQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@1.9.4':
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
'@biomejs/cli-darwin-x64@2.1.1':
resolution: {integrity: sha512-cC8HM5lrgKQXLAK+6Iz2FrYW5A62pAAX6KAnRlEyLb+Q3+Kr6ur/sSuoIacqlp1yvmjHJqjYfZjPvHWnqxoEIA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.9.4':
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
'@biomejs/cli-linux-arm64-musl@2.1.1':
resolution: {integrity: sha512-/7FBLnTswu4jgV9ttI3AMIdDGqVEPIZd8I5u2D4tfCoj8rl9dnjrEQbAIDlWhUXdyWlFSz8JypH3swU9h9P+2A==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
'@biomejs/cli-linux-arm64@2.1.1':
resolution: {integrity: sha512-tw4BEbhAUkWPe4WBr6IX04DJo+2jz5qpPzpW/SWvqMjb9QuHY8+J0M23V8EPY/zWU4IG8Ui0XESapR1CB49Q7g==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
'@biomejs/cli-linux-x64-musl@2.1.1':
resolution: {integrity: sha512-kUu+loNI3OCD2c12cUt7M5yaaSjDnGIksZwKnueubX6c/HWUyi/0mPbTBHR49Me3F0KKjWiKM+ZOjsmC+lUt9g==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
'@biomejs/cli-linux-x64@2.1.1':
resolution: {integrity: sha512-3WJ1GKjU7NzZb6RTbwLB59v9cTIlzjbiFLDB0z4376TkDqoNYilJaC37IomCr/aXwuU8QKkrYoHrgpSq5ffJ4Q==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
'@biomejs/cli-win32-arm64@2.1.1':
resolution: {integrity: sha512-vEHK0v0oW+E6RUWLoxb2isI3rZo57OX9ZNyyGH701fZPj6Il0Rn1f5DMNyCmyflMwTnIQstEbs7n2BxYSqQx4Q==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@1.9.4':
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
'@biomejs/cli-win32-x64@2.1.1':
resolution: {integrity: sha512-i2PKdn70kY++KEF/zkQFvQfX1e8SkA8hq4BgC+yE9dZqyLzB/XStY2MvwI3qswlRgnGpgncgqe0QYKVS1blksg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
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':
resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==}
@ -3211,6 +3217,9 @@ packages:
'@vitest/expect@3.2.3':
resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==}
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
'@vitest/mocker@3.2.3':
resolution: {integrity: sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==}
peerDependencies:
@ -3222,21 +3231,47 @@ packages:
vite:
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':
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':
resolution: {integrity: sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==}
'@vitest/runner@3.2.4':
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
'@vitest/snapshot@3.2.3':
resolution: {integrity: sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==}
'@vitest/snapshot@3.2.4':
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
'@vitest/spy@3.2.3':
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':
resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==}
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
@ -4876,6 +4911,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsonfile@2.4.0:
resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==}
@ -5002,6 +5040,9 @@ packages:
loupe@3.1.3:
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
loupe@3.1.4:
resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==}
lower-case-first@2.0.2:
resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==}
@ -5952,6 +5993,9 @@ packages:
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@ -6260,6 +6304,10 @@ packages:
resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==}
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:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
@ -6384,8 +6432,8 @@ packages:
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ultracite@4.2.13:
resolution: {integrity: sha512-j49R1z3xXIPhdvU19x0z0Z4hNewJYn4F1h42ULeaCOylBuxwGVE401piPxe3aVapwue7+Ec3J6wnL/+mW4zwww==}
ultracite@5.0.32:
resolution: {integrity: sha512-JjVNswL1mkIaOkPVh1nuEGnbEaCa94+ftqJ9hpRX2Y+jt72pcv32JeWg3Dqhkz/e3l449f7KAzMHO4x6IbxFZQ==}
hasBin: true
unbox-primitive@1.1.0:
@ -6492,6 +6540,11 @@ packages:
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
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:
resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -6551,6 +6604,34 @@ packages:
jsdom:
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:
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
@ -7133,41 +7214,52 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@biomejs/biome@1.9.4':
'@biomejs/biome@2.1.1':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.9.4
'@biomejs/cli-darwin-x64': 1.9.4
'@biomejs/cli-linux-arm64': 1.9.4
'@biomejs/cli-linux-arm64-musl': 1.9.4
'@biomejs/cli-linux-x64': 1.9.4
'@biomejs/cli-linux-x64-musl': 1.9.4
'@biomejs/cli-win32-arm64': 1.9.4
'@biomejs/cli-win32-x64': 1.9.4
'@biomejs/cli-darwin-arm64': 2.1.1
'@biomejs/cli-darwin-x64': 2.1.1
'@biomejs/cli-linux-arm64': 2.1.1
'@biomejs/cli-linux-arm64-musl': 2.1.1
'@biomejs/cli-linux-x64': 2.1.1
'@biomejs/cli-linux-x64-musl': 2.1.1
'@biomejs/cli-win32-arm64': 2.1.1
'@biomejs/cli-win32-x64': 2.1.1
'@biomejs/cli-darwin-arm64@1.9.4':
'@biomejs/cli-darwin-arm64@2.1.1':
optional: true
'@biomejs/cli-darwin-x64@1.9.4':
'@biomejs/cli-darwin-x64@2.1.1':
optional: true
'@biomejs/cli-linux-arm64-musl@1.9.4':
'@biomejs/cli-linux-arm64-musl@2.1.1':
optional: true
'@biomejs/cli-linux-arm64@1.9.4':
'@biomejs/cli-linux-arm64@2.1.1':
optional: true
'@biomejs/cli-linux-x64-musl@1.9.4':
'@biomejs/cli-linux-x64-musl@2.1.1':
optional: true
'@biomejs/cli-linux-x64@1.9.4':
'@biomejs/cli-linux-x64@2.1.1':
optional: true
'@biomejs/cli-win32-arm64@1.9.4':
'@biomejs/cli-win32-arm64@2.1.1':
optional: true
'@biomejs/cli-win32-x64@1.9.4':
'@biomejs/cli-win32-x64@2.1.1':
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':
dependencies:
'@codemirror/state': 6.5.2
@ -9898,6 +9990,14 @@ snapshots:
chai: 5.2.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))':
dependencies:
'@vitest/spy': 3.2.3
@ -9906,32 +10006,66 @@ snapshots:
optionalDependencies:
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':
dependencies:
tinyrainbow: 2.0.0
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.2.3':
dependencies:
'@vitest/utils': 3.2.3
pathe: 2.0.3
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':
dependencies:
'@vitest/pretty-format': 3.2.3
magic-string: 0.30.17
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':
dependencies:
tinyspy: 4.0.3
'@vitest/spy@3.2.4':
dependencies:
tinyspy: 4.0.3
'@vitest/utils@3.2.3':
dependencies:
'@vitest/pretty-format': 3.2.3
loupe: 3.1.3
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':
dependencies:
'@webassemblyjs/helper-numbers': 1.13.2
@ -11790,6 +11924,8 @@ snapshots:
json5@2.2.3: {}
jsonc-parser@3.3.1: {}
jsonfile@2.4.0:
optionalDependencies:
graceful-fs: 4.2.11
@ -11907,6 +12043,8 @@ snapshots:
loupe@3.1.3: {}
loupe@3.1.4: {}
lower-case-first@2.0.2:
dependencies:
tslib: 2.8.1
@ -12943,6 +13081,8 @@ snapshots:
is-arrayish: 0.3.2
optional: true
sisteransi@1.0.5: {}
slash@3.0.0: {}
slice-ansi@3.0.0:
@ -13264,6 +13404,8 @@ snapshots:
tinypool@1.1.0: {}
tinypool@1.1.1: {}
tinyrainbow@2.0.0: {}
tinyspy@4.0.3: {}
@ -13390,9 +13532,30 @@ snapshots:
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:
'@clack/prompts': 0.11.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:
dependencies:
@ -13527,6 +13690,24 @@ snapshots:
- supports-color
- 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):
dependencies:
esbuild: 0.21.5
@ -13578,6 +13759,45 @@ snapshots:
- supports-color
- 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: {}
w3c-keyname@2.2.8: {}