refactor: refactor subscriptions
This commit is contained in:
@@ -32,8 +32,8 @@ import { Route as AppCredential3rdCreateImport } from './routes/_app/credential3
|
||||
import { Route as AppBangumiManageImport } from './routes/_app/bangumi/manage'
|
||||
import { Route as AppExploreFeedImport } from './routes/_app/_explore/feed'
|
||||
import { Route as AppExploreExploreImport } from './routes/_app/_explore/explore'
|
||||
import { Route as AppSubscriptionsEditSubscriptionIdImport } from './routes/_app/subscriptions/edit.$subscriptionId'
|
||||
import { Route as AppSubscriptionsDetailSubscriptionIdImport } from './routes/_app/subscriptions/detail.$subscriptionId'
|
||||
import { Route as AppSubscriptionsEditIdImport } from './routes/_app/subscriptions/edit.$id'
|
||||
import { Route as AppSubscriptionsDetailIdImport } from './routes/_app/subscriptions/detail.$id'
|
||||
import { Route as AppCredential3rdEditIdImport } from './routes/_app/credential3rd/edit.$id'
|
||||
import { Route as AppCredential3rdDetailIdImport } from './routes/_app/credential3rd/detail.$id'
|
||||
|
||||
@@ -166,19 +166,17 @@ const AppExploreExploreRoute = AppExploreExploreImport.update({
|
||||
getParentRoute: () => AppRouteRoute,
|
||||
} as any)
|
||||
|
||||
const AppSubscriptionsEditSubscriptionIdRoute =
|
||||
AppSubscriptionsEditSubscriptionIdImport.update({
|
||||
id: '/edit/$subscriptionId',
|
||||
path: '/edit/$subscriptionId',
|
||||
getParentRoute: () => AppSubscriptionsRouteRoute,
|
||||
} as any)
|
||||
const AppSubscriptionsEditIdRoute = AppSubscriptionsEditIdImport.update({
|
||||
id: '/edit/$id',
|
||||
path: '/edit/$id',
|
||||
getParentRoute: () => AppSubscriptionsRouteRoute,
|
||||
} as any)
|
||||
|
||||
const AppSubscriptionsDetailSubscriptionIdRoute =
|
||||
AppSubscriptionsDetailSubscriptionIdImport.update({
|
||||
id: '/detail/$subscriptionId',
|
||||
path: '/detail/$subscriptionId',
|
||||
getParentRoute: () => AppSubscriptionsRouteRoute,
|
||||
} as any)
|
||||
const AppSubscriptionsDetailIdRoute = AppSubscriptionsDetailIdImport.update({
|
||||
id: '/detail/$id',
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppSubscriptionsRouteRoute,
|
||||
} as any)
|
||||
|
||||
const AppCredential3rdEditIdRoute = AppCredential3rdEditIdImport.update({
|
||||
id: '/edit/$id',
|
||||
@@ -357,18 +355,18 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppCredential3rdEditIdImport
|
||||
parentRoute: typeof AppCredential3rdRouteImport
|
||||
}
|
||||
'/_app/subscriptions/detail/$subscriptionId': {
|
||||
id: '/_app/subscriptions/detail/$subscriptionId'
|
||||
path: '/detail/$subscriptionId'
|
||||
fullPath: '/subscriptions/detail/$subscriptionId'
|
||||
preLoaderRoute: typeof AppSubscriptionsDetailSubscriptionIdImport
|
||||
'/_app/subscriptions/detail/$id': {
|
||||
id: '/_app/subscriptions/detail/$id'
|
||||
path: '/detail/$id'
|
||||
fullPath: '/subscriptions/detail/$id'
|
||||
preLoaderRoute: typeof AppSubscriptionsDetailIdImport
|
||||
parentRoute: typeof AppSubscriptionsRouteImport
|
||||
}
|
||||
'/_app/subscriptions/edit/$subscriptionId': {
|
||||
id: '/_app/subscriptions/edit/$subscriptionId'
|
||||
path: '/edit/$subscriptionId'
|
||||
fullPath: '/subscriptions/edit/$subscriptionId'
|
||||
preLoaderRoute: typeof AppSubscriptionsEditSubscriptionIdImport
|
||||
'/_app/subscriptions/edit/$id': {
|
||||
id: '/_app/subscriptions/edit/$id'
|
||||
path: '/edit/$id'
|
||||
fullPath: '/subscriptions/edit/$id'
|
||||
preLoaderRoute: typeof AppSubscriptionsEditIdImport
|
||||
parentRoute: typeof AppSubscriptionsRouteImport
|
||||
}
|
||||
}
|
||||
@@ -432,17 +430,15 @@ const AppSettingsRouteRouteWithChildren =
|
||||
interface AppSubscriptionsRouteRouteChildren {
|
||||
AppSubscriptionsCreateRoute: typeof AppSubscriptionsCreateRoute
|
||||
AppSubscriptionsManageRoute: typeof AppSubscriptionsManageRoute
|
||||
AppSubscriptionsDetailSubscriptionIdRoute: typeof AppSubscriptionsDetailSubscriptionIdRoute
|
||||
AppSubscriptionsEditSubscriptionIdRoute: typeof AppSubscriptionsEditSubscriptionIdRoute
|
||||
AppSubscriptionsDetailIdRoute: typeof AppSubscriptionsDetailIdRoute
|
||||
AppSubscriptionsEditIdRoute: typeof AppSubscriptionsEditIdRoute
|
||||
}
|
||||
|
||||
const AppSubscriptionsRouteRouteChildren: AppSubscriptionsRouteRouteChildren = {
|
||||
AppSubscriptionsCreateRoute: AppSubscriptionsCreateRoute,
|
||||
AppSubscriptionsManageRoute: AppSubscriptionsManageRoute,
|
||||
AppSubscriptionsDetailSubscriptionIdRoute:
|
||||
AppSubscriptionsDetailSubscriptionIdRoute,
|
||||
AppSubscriptionsEditSubscriptionIdRoute:
|
||||
AppSubscriptionsEditSubscriptionIdRoute,
|
||||
AppSubscriptionsDetailIdRoute: AppSubscriptionsDetailIdRoute,
|
||||
AppSubscriptionsEditIdRoute: AppSubscriptionsEditIdRoute,
|
||||
}
|
||||
|
||||
const AppSubscriptionsRouteRouteWithChildren =
|
||||
@@ -498,8 +494,8 @@ export interface FileRoutesByFullPath {
|
||||
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
|
||||
'/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
|
||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
|
||||
'/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
|
||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
@@ -526,8 +522,8 @@ export interface FileRoutesByTo {
|
||||
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
|
||||
'/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
|
||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
|
||||
'/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
|
||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
@@ -555,8 +551,8 @@ export interface FileRoutesById {
|
||||
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
|
||||
'/_app/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
|
||||
'/_app/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/_app/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
|
||||
'/_app/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
|
||||
'/_app/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/_app/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
@@ -585,8 +581,8 @@ export interface FileRouteTypes {
|
||||
| '/auth/oidc/callback'
|
||||
| '/credential3rd/detail/$id'
|
||||
| '/credential3rd/edit/$id'
|
||||
| '/subscriptions/detail/$subscriptionId'
|
||||
| '/subscriptions/edit/$subscriptionId'
|
||||
| '/subscriptions/detail/$id'
|
||||
| '/subscriptions/edit/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@@ -612,8 +608,8 @@ export interface FileRouteTypes {
|
||||
| '/auth/oidc/callback'
|
||||
| '/credential3rd/detail/$id'
|
||||
| '/credential3rd/edit/$id'
|
||||
| '/subscriptions/detail/$subscriptionId'
|
||||
| '/subscriptions/edit/$subscriptionId'
|
||||
| '/subscriptions/detail/$id'
|
||||
| '/subscriptions/edit/$id'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@@ -639,8 +635,8 @@ export interface FileRouteTypes {
|
||||
| '/auth/oidc/callback'
|
||||
| '/_app/credential3rd/detail/$id'
|
||||
| '/_app/credential3rd/edit/$id'
|
||||
| '/_app/subscriptions/detail/$subscriptionId'
|
||||
| '/_app/subscriptions/edit/$subscriptionId'
|
||||
| '/_app/subscriptions/detail/$id'
|
||||
| '/_app/subscriptions/edit/$id'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
@@ -741,8 +737,8 @@ export const routeTree = rootRoute
|
||||
"children": [
|
||||
"/_app/subscriptions/create",
|
||||
"/_app/subscriptions/manage",
|
||||
"/_app/subscriptions/detail/$subscriptionId",
|
||||
"/_app/subscriptions/edit/$subscriptionId"
|
||||
"/_app/subscriptions/detail/$id",
|
||||
"/_app/subscriptions/edit/$id"
|
||||
]
|
||||
},
|
||||
"/auth/sign-in": {
|
||||
@@ -798,12 +794,12 @@ export const routeTree = rootRoute
|
||||
"filePath": "_app/credential3rd/edit.$id.tsx",
|
||||
"parent": "/_app/credential3rd"
|
||||
},
|
||||
"/_app/subscriptions/detail/$subscriptionId": {
|
||||
"filePath": "_app/subscriptions/detail.$subscriptionId.tsx",
|
||||
"/_app/subscriptions/detail/$id": {
|
||||
"filePath": "_app/subscriptions/detail.$id.tsx",
|
||||
"parent": "/_app/subscriptions"
|
||||
},
|
||||
"/_app/subscriptions/edit/$subscriptionId": {
|
||||
"filePath": "_app/subscriptions/edit.$subscriptionId.tsx",
|
||||
"/_app/subscriptions/edit/$id": {
|
||||
"filePath": "_app/subscriptions/edit.$id.tsx",
|
||||
"parent": "/_app/subscriptions"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,13 +80,11 @@ function CredentialCreateRouteComponent() {
|
||||
...form.value,
|
||||
userAgent: form.value.userAgent || platformService.userAgent,
|
||||
};
|
||||
if (form.value.credentialType === Credential3rdTypeEnum.Mikan) {
|
||||
await insertCredential3rd({
|
||||
variables: {
|
||||
data: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
await insertCredential3rd({
|
||||
variables: {
|
||||
data: value,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -118,16 +116,7 @@ function CredentialCreateRouteComponent() {
|
||||
}}
|
||||
className="space-y-6"
|
||||
>
|
||||
<form.Field
|
||||
name="credentialType"
|
||||
validators={{
|
||||
onChange: ({ value }) => {
|
||||
if (!value) {
|
||||
return 'Please select the credential type';
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<form.Field name="credentialType">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Credential type *</Label>
|
||||
@@ -147,7 +136,11 @@ function CredentialCreateRouteComponent() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{field.state.meta.errors && (
|
||||
<FormFieldErrors errors={field.state.meta.errors} />
|
||||
<FormFieldErrors
|
||||
errors={field.state.meta.errors}
|
||||
isDirty={field.state.meta.isDirty}
|
||||
submissionAttempts={form.state.submissionAttempts}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -166,7 +159,11 @@ function CredentialCreateRouteComponent() {
|
||||
autoComplete="off"
|
||||
/>
|
||||
{field.state.meta.errors && (
|
||||
<FormFieldErrors errors={field.state.meta.errors} />
|
||||
<FormFieldErrors
|
||||
errors={field.state.meta.errors}
|
||||
isDirty={field.state.meta.isDirty}
|
||||
submissionAttempts={form.state.submissionAttempts}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -188,7 +185,11 @@ function CredentialCreateRouteComponent() {
|
||||
autoComplete="off"
|
||||
/>
|
||||
{field.state.meta.errors && (
|
||||
<FormFieldErrors errors={field.state.meta.errors} />
|
||||
<FormFieldErrors
|
||||
errors={field.state.meta.errors}
|
||||
isDirty={field.state.meta.isDirty}
|
||||
submissionAttempts={form.state.submissionAttempts}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -215,7 +216,11 @@ function CredentialCreateRouteComponent() {
|
||||
Current default user agent: {platformService.userAgent}
|
||||
</p>
|
||||
{field.state.meta.errors && (
|
||||
<FormFieldErrors errors={field.state.meta.errors} />
|
||||
<FormFieldErrors
|
||||
errors={field.state.meta.errors}
|
||||
isDirty={field.state.meta.isDirty}
|
||||
submissionAttempts={form.state.submissionAttempts}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -91,7 +91,7 @@ function Credential3rdDetailRouteComponent() {
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
View and manage credential #{credential.id}
|
||||
View credential #{credential.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,9 +117,9 @@ function FormView({
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential detail</h1>
|
||||
<h1 className="font-bold text-2xl">Credential edit</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
View and manage credential #{credential.id}
|
||||
Edit credential #{credential.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { GetCredential3rdQuery } from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { useEvent } from '@/presentation/hooks/use-event';
|
||||
import { cn } from '@/presentation/utils';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
@@ -332,14 +333,24 @@ function CredentialManageRouteComponent() {
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const isPinned = cell.column.getIsPinned();
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={cn({
|
||||
'sticky z-1 bg-background shadow-xs': isPinned,
|
||||
'right-0': isPinned === 'right',
|
||||
'left-0': isPinned === 'left',
|
||||
})}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SelectContent, SelectItem } from '@/components/ui/select';
|
||||
import { GET_CREDENTIAL_3RD } from '@/domains/recorder/schema/credential3rd';
|
||||
import {
|
||||
type Credential3rdTypeEnum,
|
||||
type GetCredential3rdQuery,
|
||||
type GetCredential3rdQueryVariables,
|
||||
OrderByEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { AlertCircle, Loader2, RefreshCw } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
export interface Credential3rdSelectContentProps
|
||||
extends ComponentProps<typeof SelectContent> {
|
||||
credentialType: Credential3rdTypeEnum;
|
||||
}
|
||||
|
||||
export function Credential3rdSelectContent({
|
||||
credentialType,
|
||||
...props
|
||||
}: Credential3rdSelectContentProps) {
|
||||
const { data, loading, error, refetch } = useQuery<
|
||||
GetCredential3rdQuery,
|
||||
GetCredential3rdQueryVariables
|
||||
>(GET_CREDENTIAL_3RD, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
nextFetchPolicy: 'cache-and-network',
|
||||
variables: {
|
||||
filters: {
|
||||
credentialType: {
|
||||
eq: credentialType,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: OrderByEnum.Desc,
|
||||
},
|
||||
pagination: {
|
||||
page: {
|
||||
page: 0,
|
||||
limit: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const credentials = data?.credential3rd?.nodes ?? [];
|
||||
|
||||
return (
|
||||
<SelectContent {...props}>
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="ml-2 text-muted-foreground text-sm">Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="flex flex-col items-center gap-2 py-6">
|
||||
<div className="flex items-center text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span className="ml-2 text-sm">Failed to load credentials</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetch()}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
!error &&
|
||||
(credentials.length === 0 ? (
|
||||
<div className="py-6 text-center">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
No credentials found
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
credentials.map((credential) => (
|
||||
<SelectItem key={credential.id} value={credential.id.toString()}>
|
||||
{credential.username}
|
||||
</SelectItem>
|
||||
))
|
||||
))}
|
||||
</SelectContent>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
import { useAuth } from '@/app/auth/hooks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -26,13 +17,28 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useAppForm } from '@/components/ui/tanstack-form';
|
||||
import { MikanSeasonEnum } from '@/domains/recorder/schema/mikan';
|
||||
import {
|
||||
INSERT_SUBSCRIPTION,
|
||||
type SubscriptionInsertForm,
|
||||
SubscriptionInsertFormSchema,
|
||||
} from '@/domains/recorder/schema/subscriptions';
|
||||
import { SubscriptionService } from '@/domains/recorder/services/subscription.service';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
import {
|
||||
Credential3rdTypeEnum,
|
||||
type InsertSubscriptionMutation,
|
||||
type InsertSubscriptionMutationVariables,
|
||||
SubscriptionCategoryEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { gql, useMutation } from '@apollo/client';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Loader2, Save } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Credential3rdSelectContent } from './-credential3rd-select';
|
||||
|
||||
export const Route = createFileRoute('/_app/subscriptions/create')({
|
||||
component: SubscriptionCreateRouteComponent,
|
||||
@@ -41,194 +47,312 @@ export const Route = createFileRoute('/_app/subscriptions/create')({
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
type SubscriptionFormValues = {
|
||||
displayName: string;
|
||||
sourceUrl: string;
|
||||
category: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
const CREATE_SUBSCRIPTION_MUTATION = gql`
|
||||
mutation CreateSubscription($input: SubscriptionsInsertInput!) {
|
||||
subscriptionsCreateOne(data: $input) {
|
||||
id
|
||||
displayName
|
||||
sourceUrl
|
||||
enabled
|
||||
category
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function SubscriptionCreateRouteComponent() {
|
||||
const { authData } = useAuth();
|
||||
console.log(JSON.stringify(authData, null, 2));
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const form = useForm<SubscriptionFormValues>({
|
||||
defaultValues: {
|
||||
displayName: '',
|
||||
sourceUrl: '',
|
||||
category: 'mikan',
|
||||
enabled: true,
|
||||
const subscriptionService = useInject(SubscriptionService);
|
||||
|
||||
const [insertSubscription, { loading }] = useMutation<
|
||||
InsertSubscriptionMutation['subscriptionsCreateOne'],
|
||||
InsertSubscriptionMutationVariables
|
||||
>(INSERT_SUBSCRIPTION, {
|
||||
onCompleted(data) {
|
||||
toast.success('Subscription created');
|
||||
navigate({
|
||||
to: '/subscriptions/detail/$id',
|
||||
params: { id: `${data.id}` },
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toast.error('Failed to create subscription', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [createSubscription] = useMutation(CREATE_SUBSCRIPTION_MUTATION);
|
||||
|
||||
const onSubmit = async (data: SubscriptionFormValues) => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const response = await createSubscription({
|
||||
const form = useAppForm({
|
||||
defaultValues: {
|
||||
displayName: '',
|
||||
category: undefined,
|
||||
enabled: true,
|
||||
sourceUrl: '',
|
||||
credentialId: '',
|
||||
year: undefined,
|
||||
seasonStr: '',
|
||||
} as unknown as SubscriptionInsertForm,
|
||||
validators: {
|
||||
onBlur: SubscriptionInsertFormSchema,
|
||||
onSubmit: SubscriptionInsertFormSchema,
|
||||
},
|
||||
onSubmit: async (form) => {
|
||||
const input = subscriptionService.transformInsertFormToInput(form.value);
|
||||
await insertSubscription({
|
||||
variables: {
|
||||
input: {
|
||||
category: data.category,
|
||||
displayName: data.displayName,
|
||||
sourceUrl: data.sourceUrl,
|
||||
enabled: data.enabled,
|
||||
},
|
||||
data: input,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.errors) {
|
||||
throw new Error(
|
||||
response.errors[0]?.message || 'Failed to create subscription'
|
||||
);
|
||||
}
|
||||
|
||||
toast.success('Subscription created successfully');
|
||||
navigate({ to: '/subscriptions/manage' });
|
||||
} catch (error) {
|
||||
console.error('Failed to create subscription:', error);
|
||||
toast.error(
|
||||
`Subscription creation failed: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create Bangumi Subscription</CardTitle>
|
||||
<CardDescription>Add a new bangumi subscription source</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="category"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Source Type</FormLabel>
|
||||
<div className="container mx-auto max-w-2xl py-6">
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Create Bangumi Subscription</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Add a new bangumi subscription source
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Subscription information</CardTitle>
|
||||
<CardDescription className="mt-2">
|
||||
Please fill in the information of the bangumi subscription source.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
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">
|
||||
<Label htmlFor={field.name}>Category *</Label>
|
||||
<Select
|
||||
disabled
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue="mikan"
|
||||
value={field.state.value}
|
||||
onValueChange={(value) =>
|
||||
field.handleChange(
|
||||
value as SubscriptionInsertForm['category']
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select source type" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select subscription category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mikan">mikan</SelectItem>
|
||||
<SelectItem value={SubscriptionCategoryEnum.MikanBangumi}>
|
||||
Mikan Bangumi Subscription
|
||||
</SelectItem>
|
||||
<SelectItem value={SubscriptionCategoryEnum.MikanSeason}>
|
||||
Mikan Season Subscription
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value={SubscriptionCategoryEnum.MikanSubscriber}
|
||||
>
|
||||
Mikan Subscriber Subscription
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Currently only mikan source is supported
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="displayName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Display Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter subscription display name"
|
||||
{...field}
|
||||
{field.state.meta.errors && (
|
||||
<FormFieldErrors
|
||||
errors={field.state.meta.errors}
|
||||
isDirty={field.state.meta.isDirty}
|
||||
submissionAttempts={form.state.submissionAttempts}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set an easily recognizable name for this subscription
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sourceUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Source URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter subscription source URL"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Copy the RSS subscription link from the source website, e.g.
|
||||
https://mikanani.me/RSS/Bangumi?bangumiId=3141&subgroupid=370
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
</form.Field>
|
||||
<form.Subscribe selector={(state) => state.values.category}>
|
||||
{(category) => {
|
||||
if (category === SubscriptionCategoryEnum.MikanSeason) {
|
||||
return (
|
||||
<>
|
||||
<form.Field name="credentialId">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Credential ID *</Label>
|
||||
<Select
|
||||
value={field.state.value.toString()}
|
||||
onValueChange={(value) =>
|
||||
field.handleChange(Number.parseInt(value))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select credential" />
|
||||
</SelectTrigger>
|
||||
<Credential3rdSelectContent
|
||||
credentialType={Credential3rdTypeEnum.Mikan}
|
||||
/>
|
||||
</Select>
|
||||
{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="year">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Year *</Label>
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
type="number"
|
||||
min={1970}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) =>
|
||||
field.handleChange(
|
||||
Number.parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
placeholder="Please enter full year (e.g. 2025)"
|
||||
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="seasonStr">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Season *</Label>
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select season" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={MikanSeasonEnum.Spring}>
|
||||
Spring
|
||||
</SelectItem>
|
||||
<SelectItem value={MikanSeasonEnum.Summer}>
|
||||
Summer
|
||||
</SelectItem>
|
||||
<SelectItem value={MikanSeasonEnum.Autumn}>
|
||||
Autumn
|
||||
</SelectItem>
|
||||
<SelectItem value={MikanSeasonEnum.Winter}>
|
||||
Winter
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{field.state.meta.errors && (
|
||||
<FormFieldErrors
|
||||
errors={field.state.meta.errors}
|
||||
isDirty={field.state.meta.isDirty}
|
||||
submissionAttempts={
|
||||
form.state.submissionAttempts
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form.Field>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<form.Field name="sourceUrl">
|
||||
{(field) => (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={field.name}>Source URL *</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 source URL"
|
||||
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.Subscribe>
|
||||
<form.Field name="enabled">
|
||||
{(field) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
Enable Subscription
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Enable this subscription immediately after creation
|
||||
</FormDescription>
|
||||
<Label htmlFor={field.name}>Enabled</Label>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Enable this subscription
|
||||
</div>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<Switch
|
||||
id={field.name}
|
||||
checked={field.state.value}
|
||||
onCheckedChange={(checked) => field.handleChange(checked)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</form.Field>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<form.Subscribe selector={(state) => [state.isSubmitting]}>
|
||||
{([isSubmitting]) => (
|
||||
<Button type="submit" disabled={loading} className="flex-1">
|
||||
{loading || isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Create subscription
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</form.Subscribe>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate({ to: '/subscriptions/manage' })}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Creating...' : 'Create Subscription'}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,19 +3,17 @@ import { useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { GET_SUBSCRIPTION_DETAIL } from '../../../../domains/recorder/schema/subscriptions.js';
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/_app/subscriptions/detail/$subscriptionId'
|
||||
)({
|
||||
export const Route = createFileRoute('/_app/subscriptions/detail/$id')({
|
||||
component: DetailRouteComponent,
|
||||
});
|
||||
|
||||
function DetailRouteComponent() {
|
||||
const { subscriptionId } = Route.useParams();
|
||||
const { id } = Route.useParams();
|
||||
const { data, loading, error } = useQuery<GetSubscriptionDetailQuery>(
|
||||
GET_SUBSCRIPTION_DETAIL,
|
||||
{
|
||||
variables: {
|
||||
id: Number.parseInt(subscriptionId),
|
||||
id: Number.parseInt(id),
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/_app/subscriptions/edit/$subscriptionId'
|
||||
)({
|
||||
export const Route = createFileRoute('/_app/subscriptions/edit/$id')({
|
||||
component: RouteComponent,
|
||||
staticData: {
|
||||
breadcrumb: { label: 'Edit' },
|
||||
@@ -11,5 +9,6 @@ export const Route = createFileRoute(
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/subscriptions/edit/$subscription-id"!</div>;
|
||||
const { id } = Route.useParams();
|
||||
return <div>Hello "/subscriptions/edit/$id"!</div>;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { useEvent } from '@/presentation/hooks/use-event';
|
||||
import { cn } from '@/presentation/utils';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { format } from 'date-fns';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -54,7 +56,10 @@ export const Route = createFileRoute('/_app/subscriptions/manage')({
|
||||
function SubscriptionManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
createdAt: false,
|
||||
updatedAt: false,
|
||||
});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
@@ -181,6 +186,30 @@ function SubscriptionManageRouteComponent() {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Created At',
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.original.createdAt;
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{format(new Date(createdAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Updated At',
|
||||
accessorKey: 'updatedAt',
|
||||
cell: ({ row }) => {
|
||||
const updatedAt = row.original.updatedAt;
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{format(new Date(updatedAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => (
|
||||
@@ -192,14 +221,14 @@ function SubscriptionManageRouteComponent() {
|
||||
showDelete
|
||||
onDetail={() => {
|
||||
navigate({
|
||||
to: '/subscriptions/detail/$subscriptionId',
|
||||
params: { subscriptionId: `${row.original.id}` },
|
||||
to: '/subscriptions/detail/$id',
|
||||
params: { id: `${row.original.id}` },
|
||||
});
|
||||
}}
|
||||
onEdit={() => {
|
||||
navigate({
|
||||
to: '/subscriptions/edit/$subscriptionId',
|
||||
params: { subscriptionId: `${row.original.id}` },
|
||||
to: '/subscriptions/edit/$id',
|
||||
params: { id: `${row.original.id}` },
|
||||
});
|
||||
}}
|
||||
onDelete={handleDeleteRecord(row)}
|
||||
@@ -220,11 +249,17 @@ function SubscriptionManageRouteComponent() {
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
pageCount: subscriptions?.paginationInfo?.pages,
|
||||
rowCount: subscriptions?.paginationInfo?.total,
|
||||
enableColumnPinning: true,
|
||||
state: {
|
||||
pagination,
|
||||
sorting,
|
||||
columnVisibility,
|
||||
},
|
||||
initialState: {
|
||||
columnPinning: {
|
||||
right: ['actions'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
@@ -284,14 +319,24 @@ function SubscriptionManageRouteComponent() {
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const isPinned = cell.column.getIsPinned();
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={cn({
|
||||
'sticky z-1 bg-background shadow-xs': isPinned,
|
||||
'right-0': isPinned === 'right',
|
||||
'left-0': isPinned === 'left',
|
||||
})}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user