fix: fix cron builder
This commit is contained in:
@@ -12,6 +12,7 @@ const config: CodegenConfig = {
|
||||
},
|
||||
config: {
|
||||
enumsAsConst: true,
|
||||
useTypeImports: true,
|
||||
scalars: {
|
||||
SubscriberTaskType: {
|
||||
input: 'recorder/bindings/SubscriberTaskInput#SubscriberTaskInput',
|
||||
|
||||
@@ -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,111 +517,210 @@ const CronFieldEditor: FC<CronFieldEditorProps> = ({
|
||||
const currentValue = fields[field];
|
||||
|
||||
return (
|
||||
<div key={field} className="space-y-2">
|
||||
<Label className="font-medium text-sm capitalize">
|
||||
{field.replace(/([A-Z])/g, ' $1').toLowerCase()}
|
||||
</Label>
|
||||
|
||||
{field === 'month' || field === 'dayOfWeek' ? (
|
||||
<Select
|
||||
value={currentValue}
|
||||
onValueChange={(value) => onChange(field, value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="*">Any</SelectItem>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
// biome-ignore lint/nursery/noNestedTernary: <explanation>
|
||||
) : field === 'dayOfMonth' ? (
|
||||
<div className="space-y-2">
|
||||
<Select
|
||||
value={currentValue}
|
||||
onValueChange={(value) => onChange(field, value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
|
||||
<SelectItem key={day} value={day.toString()}>
|
||||
{day}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<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');
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ToggleGroupItem value="*" className="min-w-fit text-xs">
|
||||
Any
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="specific"
|
||||
className="min-w-fit text-xs"
|
||||
>
|
||||
Specific
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
{currentValue !== '*' && (
|
||||
<Input
|
||||
type="text"
|
||||
value={currentValue}
|
||||
onChange={(e) => onChange(field, e.target.value)}
|
||||
placeholder={`0-${config.max}`}
|
||||
disabled={disabled}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
<span>
|
||||
Range: {config.min}-{config.max}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
Supports: *, numbers, ranges (1-5), lists (1,3,5), steps
|
||||
(*/5)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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') && (
|
||||
<Select
|
||||
value={innerValue}
|
||||
onValueChange={handleChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="*">Any</SelectItem>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{field === 'dayOfMonth' && (
|
||||
<div className="space-y-2">
|
||||
<Select
|
||||
value={innerValue}
|
||||
onValueChange={handleChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
|
||||
<SelectItem key={day} value={day.toString()}>
|
||||
{day}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{!(
|
||||
field === 'month' ||
|
||||
field === 'dayOfWeek' ||
|
||||
field === 'dayOfMonth'
|
||||
) && (
|
||||
<div className="space-y-2">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={anyOrSpecificOption}
|
||||
onValueChange={setAnyOrSpecificOption}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value={CronFieldItemAnyOrSpecificOption.Any}
|
||||
className="min-w-fit text-xs"
|
||||
>
|
||||
Any
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value={CronFieldItemAnyOrSpecificOption.Specific}
|
||||
className="min-w-fit text-xs"
|
||||
>
|
||||
Specific
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
{anyOrSpecificOption ===
|
||||
CronFieldItemAnyOrSpecificOption.Specific && (
|
||||
<Input
|
||||
type="text"
|
||||
value={innerValue}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder={`0-${config.max}`}
|
||||
disabled={disabled}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
<span>
|
||||
Range: {config.min}-{config.max}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
Supports: *, numbers, ranges (1-5), lists (1,3,5), steps (*/5)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function parseCronExpression(expression: string): Record<CronField, string> {
|
||||
const parts = expression.split(' ');
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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!) {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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!) {
|
||||
|
||||
30
apps/webui/src/infra/forms/compat.ts
Normal file
30
apps/webui/src/infra/forms/compat.ts
Normal 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;
|
||||
}
|
||||
@@ -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<
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user