From 946d4e8c2c02671815d8c29ac3b0e5eac78528d5 Mon Sep 17 00:00:00 2001 From: lonelyhentxi Date: Sat, 7 Jun 2025 02:50:14 +0800 Subject: [PATCH] feat: add subscription detail & edit page --- .../src/domains/recorder/schema/mikan.ts | 11 - .../domains/recorder/schema/subscriptions.ts | 1 + .../recorder/services/subscription.service.ts | 35 +- apps/webui/src/infra/graphql/gql/gql.ts | 6 +- apps/webui/src/infra/graphql/gql/graphql.ts | 4 +- .../routes/_app/credential3rd/create.tsx | 3 +- .../routes/_app/credential3rd/detail.$id.tsx | 8 +- .../routes/_app/subscriptions/detail.$id.tsx | 332 +++++++++++++- .../routes/_app/subscriptions/edit.$id.tsx | 410 +++++++++++++++++- .../routes/_app/subscriptions/manage.tsx | 2 +- 10 files changed, 771 insertions(+), 41 deletions(-) diff --git a/apps/webui/src/domains/recorder/schema/mikan.ts b/apps/webui/src/domains/recorder/schema/mikan.ts index 88443b1..4c9e982 100644 --- a/apps/webui/src/domains/recorder/schema/mikan.ts +++ b/apps/webui/src/domains/recorder/schema/mikan.ts @@ -179,14 +179,3 @@ export function extractMikanSubscriptionSubscriberSourceUrl( ), }); } - -export function extractMikanSubscriptionSourceUrl( - sourceUrl: string -): MikanSubscriptionSourceUrl | ArkErrors { - const u = new URL(sourceUrl); - return MikanSubscriptionSourceUrlSchema({ - category: SubscriptionCategoryEnum.MikanBangumi, - mikanBangumiId: u.searchParams.get(MIKAN_BANGUMI_ID_QUERY_KEY), - mikanFansubId: u.searchParams.get(MIKAN_FANSUB_ID_QUERY_KEY), - }); -} diff --git a/apps/webui/src/domains/recorder/schema/subscriptions.ts b/apps/webui/src/domains/recorder/schema/subscriptions.ts index a989771..85f7dee 100644 --- a/apps/webui/src/domains/recorder/schema/subscriptions.ts +++ b/apps/webui/src/domains/recorder/schema/subscriptions.ts @@ -96,6 +96,7 @@ query GetSubscriptionDetail ($id: Int!) { enabled credential3rd { id + username } bangumi { nodes { diff --git a/apps/webui/src/domains/recorder/services/subscription.service.ts b/apps/webui/src/domains/recorder/services/subscription.service.ts index f7e7e35..af9c16e 100644 --- a/apps/webui/src/domains/recorder/services/subscription.service.ts +++ b/apps/webui/src/domains/recorder/services/subscription.service.ts @@ -3,8 +3,17 @@ import { type SubscriptionsInsertInput, } from '@/infra/graphql/gql/graphql'; import { Injectable, inject } from '@outposts/injection-js'; +import { ArkErrors } from 'arktype'; import { omit } from 'lodash-es'; -import { buildMikanSubscriptionSeasonSourceUrl } from '../schema/mikan'; +import { + type MikanSubscriptionBangumiSourceUrl, + type MikanSubscriptionSeasonSourceUrl, + type MikanSubscriptionSubscriberSourceUrl, + buildMikanSubscriptionSeasonSourceUrl, + extractMikanSubscriptionBangumiSourceUrl, + extractMikanSubscriptionSeasonSourceUrl, + extractMikanSubscriptionSubscriberSourceUrl, +} from '../schema/mikan'; import type { SubscriptionInsertForm } from '../schema/subscriptions'; import { MikanService } from './mikan.service'; @@ -26,4 +35,28 @@ export class SubscriptionService { } return form; } + + extractSourceUrlMeta( + category: SubscriptionCategoryEnum, + sourceUrl: string + ): + | MikanSubscriptionSeasonSourceUrl + | MikanSubscriptionBangumiSourceUrl + | MikanSubscriptionSubscriberSourceUrl + | null { + let meta: + | MikanSubscriptionSeasonSourceUrl + | MikanSubscriptionBangumiSourceUrl + | MikanSubscriptionSubscriberSourceUrl + | null + | ArkErrors = null; + if (category === SubscriptionCategoryEnum.MikanSeason) { + meta = extractMikanSubscriptionSeasonSourceUrl(sourceUrl); + } else if (category === SubscriptionCategoryEnum.MikanBangumi) { + meta = extractMikanSubscriptionBangumiSourceUrl(sourceUrl); + } else if (category === SubscriptionCategoryEnum.MikanSubscriber) { + meta = extractMikanSubscriptionSubscriberSourceUrl(sourceUrl); + } + return meta instanceof ArkErrors ? null : meta; + } } diff --git a/apps/webui/src/infra/graphql/gql/gql.ts b/apps/webui/src/infra/graphql/gql/gql.ts index 5048a9d..f0e902a 100644 --- a/apps/webui/src/infra/graphql/gql/gql.ts +++ b/apps/webui/src/infra/graphql/gql/gql.ts @@ -23,7 +23,7 @@ type Documents = { "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument, "\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": typeof types.DeleteSubscriptionsDocument, - "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument, + "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument, }; const documents: Documents = { "\n query GetCredential3rd($filters: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filters: $filters, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetCredential3rdDocument, @@ -35,7 +35,7 @@ const documents: Documents = { "\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument, "\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filters: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filters\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument, "\n mutation DeleteSubscriptions($filters: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filters)\n }\n": types.DeleteSubscriptionsDocument, - "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument, + "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument, }; /** @@ -91,7 +91,7 @@ export function gql(source: "\n mutation DeleteSubscriptions($filters: Subscr /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"]; +export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filters: { id: {\n eq: $id\n } }) {\n nodes {\n id\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n rawName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n savePath\n homepage\n }\n }\n }\n }\n}\n"]; export function gql(source: string) { return (documents as any)[source] ?? {}; diff --git a/apps/webui/src/infra/graphql/gql/graphql.ts b/apps/webui/src/infra/graphql/gql/graphql.ts index 1078499..3922c3d 100644 --- a/apps/webui/src/infra/graphql/gql/graphql.ts +++ b/apps/webui/src/infra/graphql/gql/graphql.ts @@ -1714,7 +1714,7 @@ export type GetSubscriptionDetailQueryVariables = Exact<{ }>; -export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credential3rd?: { __typename?: 'Credential3rd', id: number } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, rawName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } }; +export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, rawName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, savePath?: string | null, homepage?: string | null }> } }> } }; export const GetCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdOrderInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode; @@ -1726,4 +1726,4 @@ export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credentialId"}}]}}]}}]} as unknown as DocumentNode; export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode; export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}}]}]}}]} as unknown as DocumentNode; -export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"rawName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"rawName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx index 68cc473..aab46a0 100644 --- a/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx +++ b/apps/webui/src/presentation/routes/_app/credential3rd/create.tsx @@ -104,7 +104,8 @@ function CredentialCreateRouteComponent() { userAgent: '', }, validators: { - onBlur: Credential3rdInsertSchema, + onChangeAsync: Credential3rdInsertSchema, + onChangeAsyncDebounceMs: 300, onSubmit: Credential3rdInsertSchema, }, onSubmit: async (form) => { diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx index e9749ac..6b1e644 100644 --- a/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx +++ b/apps/webui/src/presentation/routes/_app/credential3rd/detail.$id.tsx @@ -125,9 +125,7 @@ function Credential3rdDetailRouteComponent() { View credential detail - - {credential.credentialType} - + {credential.credentialType} @@ -143,9 +141,7 @@ function Credential3rdDetailRouteComponent() {
- - {credential.credentialType} - + {credential.credentialType}
diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx index a2e73a4..d857174 100644 --- a/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx +++ b/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx @@ -1,36 +1,346 @@ -import type { GetSubscriptionDetailQuery } from '@/infra/graphql/gql/graphql'; +import { DetailCardSkeleton } from '@/components/detail-card-skeleton'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +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_SUBSCRIPTION_DETAIL } from '@/domains/recorder/schema/subscriptions'; +import { SubscriptionService } from '@/domains/recorder/services/subscription.service'; +import { useInject } from '@/infra/di/inject'; +import { + type GetSubscriptionDetailQuery, + SubscriptionCategoryEnum, +} from '@/infra/graphql/gql/graphql'; import { useQuery } from '@apollo/client'; -import { createFileRoute } from '@tanstack/react-router'; -import { GET_SUBSCRIPTION_DETAIL } from '../../../../domains/recorder/schema/subscriptions.js'; +import { + createFileRoute, + useCanGoBack, + useNavigate, + useRouter, +} from '@tanstack/react-router'; +import { format } from 'date-fns'; +import { ArrowLeft, Edit, ExternalLink } from 'lucide-react'; +import { useMemo } from 'react'; export const Route = createFileRoute('/_app/subscriptions/detail/$id')({ - component: DetailRouteComponent, + component: SubscriptionDetailRouteComponent, }); -function DetailRouteComponent() { +function SubscriptionDetailRouteComponent() { const { id } = Route.useParams(); + const navigate = useNavigate(); + const router = useRouter(); + const canGoBack = useCanGoBack(); + const subscriptionService = useInject(SubscriptionService); + + const handleBack = () => { + if (canGoBack) { + router.history.back(); + } else { + navigate({ + to: '/subscriptions/manage', + }); + } + }; + const { data, loading, error } = useQuery( GET_SUBSCRIPTION_DETAIL, { variables: { id: Number.parseInt(id), }, + fetchPolicy: 'cache-and-network', } ); + const handleEnterEditMode = () => { + navigate({ + to: '/subscriptions/edit/$id', + params: { + id, + }, + }); + }; + const subscription = data?.subscriptions?.nodes?.[0]; + + const sourceUrlMeta = useMemo( + () => + subscription + ? subscriptionService.extractSourceUrlMeta( + subscription?.category, + subscription?.sourceUrl + ) + : null, + [ + subscription, + subscription?.category, + subscription?.sourceUrl, + subscriptionService.extractSourceUrlMeta, + ] + ); + if (loading) { - return
Loading...
; + return ; } if (error) { - return
Error: {error.message}
; + return ; } - const detail = data?.subscriptions?.nodes?.[0]; + if (!subscription) { + return ; + } return ( -
+
+
+
+ +
+

Subscription detail

+

+ View subscription #{subscription.id} +

+
+
+ +
+ +
+
+ + + +
+
+ Subscription information + + View subscription detail + +
+
+ + {subscription.enabled ? 'Enabled' : 'Disabled'} + + {subscription.category} +
+
+
+ +
+
+
+ +
+ {subscription.id} +
+
+ +
+ +
+ {subscription.category} +
+
+ +
+ +
+ + {subscription.displayName || 'Not set'} + +
+
+ +
+ +
+ + {subscription.enabled ? 'Enabled' : 'Disabled'} + +
+
+ + {subscription.credential3rd && ( + <> +
+ +
+ + {subscription.credential3rd.id} + +
+
+ +
+ +
+ + {subscription.credential3rd.username} + +
+
+ + )} +
+ +
+ +
+ + {subscription.sourceUrl || '-'} + + {subscription.sourceUrl && ( + + )} +
+
+
+ {sourceUrlMeta?.category === + SubscriptionCategoryEnum.MikanSeason && ( + <> +
+ +
+ {sourceUrlMeta.year} +
+
+
+ +
+ {sourceUrlMeta.seasonStr} +
+
+ + )} +
+ + +
+
+ +
+ + {format( + new Date(subscription.createdAt), + 'yyyy-MM-dd HH:mm:ss' + )} + +
+
+ +
+ +
+ + {format( + new Date(subscription.updatedAt), + 'yyyy-MM-dd HH:mm:ss' + )} + +
+
+
+ + {subscription.bangumi?.nodes && + subscription.bangumi.nodes.length > 0 && ( + <> + +
+ +
+ {subscription.bangumi.nodes.map((bangumi) => ( + +
+
+ +
+ {bangumi.displayName} +
+
+
+ +
+ {bangumi.season || '-'} +
+
+
+ +
+ {bangumi.fansub || '-'} +
+
+
+ +
+ {bangumi.savePath || '-'} +
+
+
+ {bangumi.homepage && ( +
+ +
+ )} +
+ ))} +
+
+ + )} +
+
+
+
); } diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/edit.$id.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/edit.$id.tsx index 543f3c4..4a0832c 100644 --- a/apps/webui/src/presentation/routes/_app/subscriptions/edit.$id.tsx +++ b/apps/webui/src/presentation/routes/_app/subscriptions/edit.$id.tsx @@ -1,14 +1,414 @@ +import { DetailCardSkeleton } from '@/components/detail-card-skeleton'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { DetailEmptyView } from '@/components/ui/detail-empty-view'; +import { FormFieldErrors } from '@/components/ui/form-field-errors'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { QueryErrorView } from '@/components/ui/query-error-view'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + 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 { + GET_SUBSCRIPTION_DETAIL, + type SubscriptionInsertForm, + SubscriptionInsertFormSchema, + UPDATE_SUBSCRIPTIONS, +} from '@/domains/recorder/schema/subscriptions'; +import { SubscriptionService } from '@/domains/recorder/services/subscription.service'; +import { useInject } from '@/infra/di/inject'; +import { + Credential3rdTypeEnum, + type GetSubscriptionDetailQuery, + SubscriptionCategoryEnum, + type UpdateSubscriptionsMutation, + type UpdateSubscriptionsMutationVariables, +} from '@/infra/graphql/gql/graphql'; import type { RouteStateDataOption } from '@/infra/routes/traits'; -import { createFileRoute } from '@tanstack/react-router'; +import { useMutation, useQuery } from '@apollo/client'; +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import { ArrowLeft, Save, X } 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')({ - component: RouteComponent, + component: SubscriptionEditRouteComponent, staticData: { breadcrumb: { label: 'Edit' }, } satisfies RouteStateDataOption, }); -function RouteComponent() { - const { id } = Route.useParams(); - return
Hello "/subscriptions/edit/$id"!
; +type SubscriptionDetailDto = NonNullable< + GetSubscriptionDetailQuery['subscriptions']['nodes'][0] +>; + +function FormView({ + subscription, + onCompleted, +}: { + subscription: SubscriptionDetailDto; + onCompleted: VoidFunction; +}) { + const navigate = useNavigate(); + const subscriptionService = useInject(SubscriptionService); + + const handleBack = () => { + navigate({ + to: '/subscriptions/detail/$id', + params: { id: subscription.id.toString() }, + }); + }; + + const [updateSubscription, { loading: updating }] = useMutation< + UpdateSubscriptionsMutation, + UpdateSubscriptionsMutationVariables + >(UPDATE_SUBSCRIPTIONS, { + onCompleted, + onError: (error) => { + toast.error('Update subscription failed', { + description: error.message, + }); + }, + }); + + // Extract source URL metadata for form initialization + const sourceUrlMeta = useMemo( + () => + subscriptionService.extractSourceUrlMeta( + subscription.category, + subscription.sourceUrl + ), + [subscription.category, subscription.sourceUrl, subscriptionService] + ); + + // Initialize form with current subscription data + const defaultValues = useMemo(() => { + const base = { + displayName: subscription.displayName, + category: subscription.category, + enabled: subscription.enabled, + sourceUrl: subscription.sourceUrl, + credentialId: subscription.credential3rd?.id || '', + }; + + if ( + subscription.category === SubscriptionCategoryEnum.MikanSeason && + sourceUrlMeta?.category === SubscriptionCategoryEnum.MikanSeason + ) { + return { + ...base, + year: sourceUrlMeta.year, + seasonStr: sourceUrlMeta.seasonStr, + }; + } + + return base; + }, [subscription, sourceUrlMeta]); + + const form = useAppForm({ + defaultValues: defaultValues as unknown as SubscriptionInsertForm, + validators: { + onChangeAsync: SubscriptionInsertFormSchema, + onChangeAsyncDebounceMs: 300, + onSubmit: SubscriptionInsertFormSchema, + }, + onSubmit: async (form) => { + const input = subscriptionService.transformInsertFormToInput(form.value); + + await updateSubscription({ + variables: { + data: input, + filters: { + id: { + eq: subscription.id, + }, + }, + }, + }); + }, + }); + + return ( +
+
+
+ +
+

Subscription edit

+

+ Edit subscription #{subscription.id} +

+
+
+ +
+ + +
+
+ + + +
+
+ Subscription information + + Edit subscription information + +
+ {subscription.category} +
+
+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-6" + > + + {(field) => ( +
+ + field.handleChange(e.target.value)} + placeholder="Please enter display name" + autoComplete="off" + /> + {field.state.meta.errors && ( + + )} +
+ )} +
+ + {/* Category is read-only in edit mode */} +
+ +
+ {subscription.category} +
+
+ + {/* Conditional fields based on category */} + {subscription.category === SubscriptionCategoryEnum.MikanSeason ? ( + <> + + {(field) => ( +
+ + + {field.state.meta.errors && ( + + )} +
+ )} +
+ + {(field) => ( +
+ + + field.handleChange(Number.parseInt(e.target.value)) + } + placeholder={`Please enter full year (e.g. ${new Date().getFullYear()})`} + autoComplete="off" + /> + {field.state.meta.errors && ( + + )} +
+ )} +
+ + {(field) => ( +
+ + + {field.state.meta.errors && ( + + )} +
+ )} +
+ + ) : ( + + {(field) => ( +
+ + field.handleChange(e.target.value)} + placeholder="Please enter source URL" + autoComplete="off" + /> + {field.state.meta.errors && ( + + )} +
+ )} +
+ )} + + + {(field) => ( +
+
+ +
+ Enable this subscription +
+
+ field.handleChange(checked)} + /> +
+ )} +
+
+
+
+
+ ); +} + +function SubscriptionEditRouteComponent() { + const { id } = Route.useParams(); + + const { loading, error, data, refetch } = + useQuery(GET_SUBSCRIPTION_DETAIL, { + variables: { + id: Number.parseInt(id), + }, + fetchPolicy: 'cache-and-network', + }); + + const subscription = data?.subscriptions?.nodes?.[0]; + + const onCompleted = useCallback(async () => { + const refetchResult = await refetch(); + if (refetchResult.errors) { + toast.error('Update subscription failed', { + description: refetchResult.errors[0].message, + }); + } else { + toast.success('Update subscription successfully'); + } + }, [refetch]); + + if (loading) { + return ; + } + + if (error) { + return ; + } + + if (!subscription) { + return ; + } + + return ; } diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/manage.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/manage.tsx index 3ff317c..c2985bf 100644 --- a/apps/webui/src/presentation/routes/_app/subscriptions/manage.tsx +++ b/apps/webui/src/presentation/routes/_app/subscriptions/manage.tsx @@ -72,7 +72,7 @@ function SubscriptionManageRouteComponent() { variables: { pagination: { page: { - page: pagination.pageIndex + 1, + page: pagination.pageIndex, limit: pagination.pageSize, }, },