From aaa8978f59d7e444bf1010455d54263717ab6c23 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 8 May 2026 17:06:44 +0800 Subject: [PATCH] tweaks --- .agents/skills/component-refactoring/SKILL.md | 1 - .../references/hook-extraction.md | 1 - .../skills/frontend-query-mutation/SKILL.md | 46 ----- .../agents/openai.yaml | 4 - .../references/contract-patterns.md | 129 ------------- .../references/runtime-rules.md | 172 ------------------ .../skills/how-to-write-component/SKILL.md | 38 +++- web/AGENTS.md | 4 - 8 files changed, 37 insertions(+), 358 deletions(-) delete mode 100644 .agents/skills/frontend-query-mutation/SKILL.md delete mode 100644 .agents/skills/frontend-query-mutation/agents/openai.yaml delete mode 100644 .agents/skills/frontend-query-mutation/references/contract-patterns.md delete mode 100644 .agents/skills/frontend-query-mutation/references/runtime-rules.md diff --git a/.agents/skills/component-refactoring/SKILL.md b/.agents/skills/component-refactoring/SKILL.md index 98a94592ab..0c0ea28aa9 100644 --- a/.agents/skills/component-refactoring/SKILL.md +++ b/.agents/skills/component-refactoring/SKILL.md @@ -190,7 +190,6 @@ const Template = useMemo(() => { **Dify Convention**: - This skill is for component decomposition, not query/mutation design. - When refactoring data fetching, follow `web/AGENTS.md`. -- Use `frontend-query-mutation` for contracts, query shape, data-fetching wrappers, query/mutation call-site patterns, conditional queries, invalidation, and mutation error handling. - Do not introduce deprecated `useInvalid` / `useReset`. - Do not add thin passthrough `useQuery` wrappers during refactoring; only extract a custom hook when it truly orchestrates multiple queries/mutations or shared derived state. diff --git a/.agents/skills/component-refactoring/references/hook-extraction.md b/.agents/skills/component-refactoring/references/hook-extraction.md index 0d567eb2a6..34d62a401f 100644 --- a/.agents/skills/component-refactoring/references/hook-extraction.md +++ b/.agents/skills/component-refactoring/references/hook-extraction.md @@ -160,7 +160,6 @@ const Configuration: FC = () => { When hook extraction touches query or mutation code, do not use this reference as the source of truth for data-layer patterns. - Follow `web/AGENTS.md` first. -- Use `frontend-query-mutation` for contracts, query shape, data-fetching wrappers, query/mutation call-site patterns, conditional queries, invalidation, and mutation error handling. - Do not introduce deprecated `useInvalid` / `useReset`. - Do not extract thin passthrough `useQuery` hooks; only extract orchestration hooks. diff --git a/.agents/skills/frontend-query-mutation/SKILL.md b/.agents/skills/frontend-query-mutation/SKILL.md deleted file mode 100644 index 10c49d222e..0000000000 --- a/.agents/skills/frontend-query-mutation/SKILL.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: frontend-query-mutation -description: Guide for implementing Dify frontend query and mutation patterns with TanStack Query and oRPC. Trigger when creating or updating contracts in web/contract, wiring router composition, consuming consoleQuery or marketplaceQuery in components or services, deciding whether to call queryOptions()/mutationOptions() directly or extract a helper or use-* hook, configuring oRPC experimental_defaults/default options, handling conditional queries, cache updates/invalidation, mutation error handling, or migrating legacy service calls to contract-first query and mutation helpers. ---- - -# Frontend Query & Mutation - -## Intent - -- Keep contract as the single source of truth in `web/contract/*`. -- Prefer contract-shaped `queryOptions()` and `mutationOptions()`. -- Keep default cache behavior with `consoleQuery`/`marketplaceQuery` setup, and keep business orchestration in feature vertical hooks when direct contract calls are not enough. -- Treat `web/service/use-*` query or mutation wrappers as legacy migration targets, not the preferred destination. -- Keep abstractions minimal to preserve TypeScript inference. - -## Workflow - -1. Identify the change surface. - - Read `references/contract-patterns.md` for contract files, router composition, client helpers, and query or mutation call-site shape. - - Read `references/runtime-rules.md` for conditional queries, default options, cache updates/invalidation, error handling, and legacy migrations. - - Read both references when a task spans contract shape and runtime behavior. -2. Implement the smallest abstraction that fits the task. - - Default to direct `useQuery(...)` or `useMutation(...)` calls with oRPC helpers at the call site. - - Extract a small shared query helper only when multiple call sites share the same extra options. - - Create or keep feature hooks only for real orchestration or shared domain behavior. - - When touching thin `web/service/use-*` wrappers, migrate them away when feasible. -3. Preserve Dify conventions. - - Keep contract inputs in `{ params, query?, body? }` shape. - - Bind default cache updates/invalidation in `createTanstackQueryUtils(...experimental_defaults...)`; use feature hooks only for workflows that cannot be expressed as default operation behavior. - - Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required. - -## Files Commonly Touched - -- `web/contract/console/*.ts` -- `web/contract/marketplace.ts` -- `web/contract/router.ts` -- `web/service/client.ts` -- legacy `web/service/use-*.ts` files when migrating wrappers away -- component and hook call sites using `consoleQuery` or `marketplaceQuery` - -## References - -- Use `references/contract-patterns.md` for contract shape, router registration, query and mutation helpers, and anti-patterns that degrade inference. -- Use `references/runtime-rules.md` for conditional queries, invalidation, `mutate` versus `mutateAsync`, and legacy migration rules. - -Treat this skill as the single query and mutation entry point for Dify frontend work. Keep detailed rules in the reference files instead of duplicating them in project docs. diff --git a/.agents/skills/frontend-query-mutation/agents/openai.yaml b/.agents/skills/frontend-query-mutation/agents/openai.yaml deleted file mode 100644 index 79e7e7d214..0000000000 --- a/.agents/skills/frontend-query-mutation/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Frontend Query & Mutation" - short_description: "Dify TanStack Query, oRPC, and default option patterns" - default_prompt: "Use this skill when implementing or reviewing Dify frontend contracts, query and mutation call sites, oRPC default options, conditional queries, cache updates/invalidation, or legacy query/mutation migrations." diff --git a/.agents/skills/frontend-query-mutation/references/contract-patterns.md b/.agents/skills/frontend-query-mutation/references/contract-patterns.md deleted file mode 100644 index 25ccfc81d7..0000000000 --- a/.agents/skills/frontend-query-mutation/references/contract-patterns.md +++ /dev/null @@ -1,129 +0,0 @@ -# Contract Patterns - -## Table of Contents - -- Intent -- Minimal structure -- Core workflow -- Query usage decision rule -- Mutation usage decision rule -- Thin hook decision rule -- Anti-patterns -- Contract rules -- Type export - -## Intent - -- Keep contract as the single source of truth in `web/contract/*`. -- Default query usage to call-site `useQuery(consoleQuery|marketplaceQuery.xxx.queryOptions(...))` when endpoint behavior maps 1:1 to the contract. -- Keep abstractions minimal and preserve TypeScript inference. - -## Minimal Structure - -```text -web/contract/ -├── base.ts -├── router.ts -├── marketplace.ts -└── console/ - ├── billing.ts - └── ...other domains -web/service/client.ts -``` - -## Core Workflow - -1. Define contract in `web/contract/console/{domain}.ts` or `web/contract/marketplace.ts`. - - Use `base.route({...}).output(type<...>())` as the baseline. - - Add `.input(type<...>())` only when the request has `params`, `query`, or `body`. - - For `GET` without input, omit `.input(...)`; do not use `.input(type())`. -2. Register contract in `web/contract/router.ts`. - - Import directly from domain files and nest by API prefix. -3. Consume from UI call sites via oRPC query utilities. - -```typescript -import { useQuery } from '@tanstack/react-query' -import { consoleQuery } from '@/service/client' - -const invoiceQuery = useQuery(consoleQuery.billing.invoices.queryOptions({ - staleTime: 5 * 60 * 1000, - throwOnError: true, - select: invoice => invoice.url, -})) -``` - -## Query Usage Decision Rule - -1. Default to direct `*.queryOptions(...)` usage at the call site. -2. If 3 or more call sites share the same extra options, extract a small query helper, not a `use-*` passthrough hook. -3. Create or keep feature hooks only for orchestration. - - Combine multiple queries or mutations. - - Share domain-level derived state or invalidation helpers. - - Prefer `web/features/{domain}/hooks/*` for feature-owned workflows. -4. Treat `web/service/use-{domain}.ts` as legacy. - - Do not create new thin service wrappers for oRPC contracts. - - When touching existing wrappers, inline direct `consoleQuery` or `marketplaceQuery` consumption when the wrapper is only a passthrough. - -```typescript -const invoicesBaseQueryOptions = () => - consoleQuery.billing.invoices.queryOptions({ retry: false }) - -const invoiceQuery = useQuery({ - ...invoicesBaseQueryOptions(), - throwOnError: true, -}) -``` - -## Mutation Usage Decision Rule - -1. Default to mutation helpers from `consoleQuery` or `marketplaceQuery`, for example `useMutation(consoleQuery.billing.bindPartnerStack.mutationOptions(...))`. -2. If the mutation flow is heavily custom, use oRPC clients as `mutationFn`, for example `consoleClient.xxx` or `marketplaceClient.xxx`, instead of handwritten non-oRPC mutation logic. - -```typescript -const createTagMutation = useMutation(consoleQuery.tags.create.mutationOptions()) -``` - -## Thin Hook Decision Rule - -Remove thin hooks when they only rename a single oRPC query or mutation helper. -Keep hooks when they orchestrate business behavior across multiple operations, own local workflow state, or normalize a feature-specific API. -Prefer feature vertical hooks for kept orchestration. Do not move new contract-first wrappers into `web/service/use-*`. - -Use: - -```typescript -const deleteTagMutation = useMutation(consoleQuery.tags.delete.mutationOptions()) -``` - -Keep: - -```typescript -const applyTagBindingsMutation = useApplyTagBindingsMutation() -``` - -`useApplyTagBindingsMutation` is acceptable because it coordinates bind and unbind requests, computes deltas, and exposes a feature-level workflow rather than a single endpoint passthrough. - -## Anti-Patterns - -- Do not wrap `useQuery` with `options?: Partial`. -- Do not split local `queryKey` and `queryFn` when oRPC `queryOptions` already exists and fits the use case. -- Do not create thin `use-*` passthrough hooks for a single endpoint. -- Do not create business-layer helpers whose only purpose is to call `consoleQuery.xxx.mutationOptions()` or `queryOptions()`. -- Do not introduce new `web/service/use-*` files for oRPC contract passthroughs. -- These patterns can degrade inference, especially around `throwOnError` and `select`, and add unnecessary indirection. - -## Contract Rules - -- Input structure: always use `{ params, query?, body? }`. -- No-input `GET`: omit `.input(...)`; do not use `.input(type())`. -- Path params: use `{paramName}` in the path and match it in the `params` object. -- Router nesting: group by API prefix, for example `/billing/*` becomes `billing: {}`. -- No barrel files: import directly from specific files. -- Types: import from `@/types/` and use the `type()` helper. -- Mutations: prefer `mutationOptions`; use explicit `mutationKey` mainly for defaults, filtering, and devtools. - -## Type Export - -```typescript -export type ConsoleInputs = InferContractRouterInputs -``` diff --git a/.agents/skills/frontend-query-mutation/references/runtime-rules.md b/.agents/skills/frontend-query-mutation/references/runtime-rules.md deleted file mode 100644 index 91b484d438..0000000000 --- a/.agents/skills/frontend-query-mutation/references/runtime-rules.md +++ /dev/null @@ -1,172 +0,0 @@ -# Runtime Rules - -## Table of Contents - -- Conditional queries -- oRPC default options -- Cache invalidation -- Key API guide -- `mutate` vs `mutateAsync` -- Legacy migration - -## Conditional Queries - -Prefer contract-shaped `queryOptions(...)`. -When required input is missing, prefer `input: skipToken` instead of placeholder params or non-null assertions. -Use `enabled` only for extra business gating after the input itself is already valid. - -```typescript -import { skipToken, useQuery } from '@tanstack/react-query' - -// Disable the query by skipping input construction. -function useAccessMode(appId: string | undefined) { - return useQuery(consoleQuery.accessControl.appAccessMode.queryOptions({ - input: appId - ? { params: { appId } } - : skipToken, - })) -} - -// Avoid runtime-only guards that bypass type checking. -function useBadAccessMode(appId: string | undefined) { - return useQuery(consoleQuery.accessControl.appAccessMode.queryOptions({ - input: { params: { appId: appId! } }, - enabled: !!appId, - })) -} -``` - -## oRPC Default Options - -Use `experimental_defaults` in `createTanstackQueryUtils` when a contract operation should always carry shared TanStack Query behavior, such as default stale time, mutation cache writes, or invalidation. - -Place defaults at the query utility creation point in `web/service/client.ts`: - -```typescript -export const consoleQuery = createTanstackQueryUtils(consoleClient, { - path: ['console'], - experimental_defaults: { - tags: { - create: { - mutationOptions: { - onSuccess: (tag, _variables, _result, context) => { - context.client.setQueryData( - consoleQuery.tags.list.queryKey({ - input: { - query: { - type: tag.type, - }, - }, - }), - (oldTags: Tag[] | undefined) => oldTags ? [tag, ...oldTags] : oldTags, - ) - }, - }, - }, - }, - }, -}) -``` - -Rules: - -- Keep defaults inline in the `consoleQuery` or `marketplaceQuery` initialization when they need sibling oRPC key builders. -- Do not create a wrapper function solely to host `createTanstackQueryUtils`. -- Do not split defaults into a vertical feature file if that forces handwritten operation paths such as `generateOperationKey(['console', ...])`. -- Keep feature-level orchestration in the feature vertical; keep query utility lifecycle defaults with the query utility. -- Prefer call-site callbacks for UI feedback only; shared cache behavior belongs in oRPC defaults when it is tied to a contract operation. - -## Cache Invalidation - -Bind shared invalidation in oRPC defaults when it is tied to a contract operation. -Use feature vertical hooks only for multi-operation workflows or domain orchestration that cannot live in a single operation default. -Components may add UI feedback in call-site callbacks, but they should not decide which queries to invalidate. - -Use: - -- `.key()` for namespace or prefix invalidation -- `.queryKey(...)` only for exact cache reads or writes such as `getQueryData` and `setQueryData` -- `queryClient.invalidateQueries(...)` in mutation `onSuccess` - -Do not use deprecated `useInvalid` from `use-base.ts`. - -```typescript -// Feature orchestration owns cache invalidation only when defaults are not enough. -export const useUpdateAccessMode = () => { - const queryClient = useQueryClient() - - return useMutation(consoleQuery.accessControl.updateAccessMode.mutationOptions({ - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: consoleQuery.accessControl.appWhitelistSubjects.key(), - }) - }, - })) -} - -// Component only adds UI behavior. -updateAccessMode({ appId, mode }, { - onSuccess: () => toast.success('...'), -}) - -// Avoid putting invalidation knowledge in the component. -mutate({ appId, mode }, { - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: consoleQuery.accessControl.appWhitelistSubjects.key(), - }) - }, -}) -``` - -## Key API Guide - -- `.key(...)` - - Use for partial matching operations. - - Prefer it for invalidation, refetch, and cancel patterns. - - Example: `queryClient.invalidateQueries({ queryKey: consoleQuery.billing.key() })` -- `.queryKey(...)` - - Use for a specific query's full key. - - Prefer it for exact cache addressing and direct reads or writes. -- `.mutationKey(...)` - - Use for a specific mutation's full key. - - Prefer it for mutation defaults registration, mutation-status filtering, and devtools grouping. - -## `mutate` vs `mutateAsync` - -Prefer `mutate` by default. -Use `mutateAsync` only when Promise semantics are truly required, such as parallel mutations or sequential steps with result dependencies. - -Rules: - -- Event handlers should usually call `mutate(...)` with `onSuccess` or `onError`. -- Every `await mutateAsync(...)` must be wrapped in `try/catch`. -- Do not use `mutateAsync` when callbacks already express the flow clearly. - -```typescript -// Default case. -mutation.mutate(data, { - onSuccess: result => router.push(result.url), -}) - -// Promise semantics are required. -try { - const order = await createOrder.mutateAsync(orderData) - await confirmPayment.mutateAsync({ orderId: order.id, token }) - router.push(`/orders/${order.id}`) -} -catch (error) { - toast.error(error instanceof Error ? error.message : 'Unknown error') -} -``` - -## Legacy Migration - -When touching old code, migrate it toward these rules: - -| Old pattern | New pattern | -|---|---| -| `useInvalid(key)` in service wrappers | oRPC defaults, or a feature vertical hook for real orchestration | -| component-triggered invalidation after mutation | move invalidation into oRPC defaults or a feature vertical hook | -| imperative fetch plus manual invalidation | wrap it in `useMutation(...mutationOptions(...))` | -| `await mutateAsync()` without `try/catch` | switch to `mutate(...)` or add `try/catch` | diff --git a/.agents/skills/how-to-write-component/SKILL.md b/.agents/skills/how-to-write-component/SKILL.md index e66ec8f851..628dd31bd5 100644 --- a/.agents/skills/how-to-write-component/SKILL.md +++ b/.agents/skills/how-to-write-component/SKILL.md @@ -1,6 +1,6 @@ --- name: how-to-write-component -description: React/TypeScript component style guide. Use when writing, refactoring, or reviewing React components, especially around props typing, state boundaries, shared local state with Jotai atoms, API types, navigation, memoization, wrappers, and empty-state handling. +description: React/TypeScript component style guide. Use when writing, refactoring, or reviewing React components, especially around props typing, state boundaries, shared local state with Jotai atoms, API types, query/mutation contracts, navigation, memoization, wrappers, and empty-state handling. --- # How To Write A Component @@ -29,6 +29,42 @@ Follow existing project patterns first. Use these rules to resolve unclear compo - For local UI state shared across siblings, distant children, or feature-local surfaces, use a colocated Jotai `atom`. Keep atoms feature-scoped and UI-owned; do not use them for server/cache state that belongs in query or API data flow. - Prefer uncontrolled components when DOM-owned state is enough. Expose style customization through CSS variables before adding controlled props only for visual changes. +## Query And Mutation Contracts + +- Keep `web/contract/*` as the single source of truth for API shape. +- Define contracts with `base.route({...}).output(type<...>())`. +- Add `.input(type<...>())` only when the request has `params`, `query`, or `body`. +- Keep contract input shaped as `{ params, query?, body? }`. +- For no-input `GET` routes, omit `.input(...)`; do not use `.input(type())`. +- Use `{paramName}` in route paths and match the same name in `params`. +- Register contracts in `web/contract/router.ts` by API prefix. +- Import contract modules directly from their domain files; do not add barrel files. +- Consume contract queries directly with `useQuery(consoleQuery.xxx.queryOptions(...))` or `useQuery(marketplaceQuery.xxx.queryOptions(...))`. +- Avoid wrapping `useQuery` and contract `queryOptions()` in pass-through hooks, such as `useAccessEnvironmentScope`. +- If three or more call sites share the same extra query options, extract a small `queryOptions` helper instead of a `use-*` hook. +- Keep feature hooks only when they orchestrate multiple operations, own workflow state, expose shared domain behavior, or normalize a feature-specific API. +- Treat `web/service/use-*` query and mutation wrappers as legacy migration targets. +- Do not create new thin `web/service/use-*` wrappers for oRPC contract calls. +- Inline existing wrappers when they only rename a single `queryOptions()` or `mutationOptions()` call. +- Avoid wrapper signatures such as `options?: Partial` because they degrade inference. +- Do not split `queryKey` and `queryFn` when oRPC `queryOptions()` already fits the use case. +- For missing required query input, pass `input: skipToken`. +- Use `enabled` only for extra business gating after the query input is already valid. +- Consume mutations with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))`. +- For heavily custom mutation flows, use oRPC clients as `mutationFn` instead of handwritten non-oRPC request logic. +- Put shared stale time, cache writes, and invalidation in `createTanstackQueryUtils(...experimental_defaults...)` when behavior belongs to a contract operation. +- Keep operation defaults in `web/service/client.ts` when they need sibling oRPC key builders. +- Keep multi-operation workflow orchestration in feature vertical hooks. +- Components may add UI feedback callbacks, but should not own shared cache invalidation decisions. +- Use `.key()` for namespace invalidation, refetch, and cancel patterns. +- Use `.queryKey(...)` for exact cache reads and writes. +- Use `.mutationKey(...)` for mutation defaults, mutation-status filtering, and devtools grouping. +- Invalidate with `queryClient.invalidateQueries(...)` in mutation `onSuccess`. +- Do not use deprecated `useInvalid` from `use-base.ts`. +- Prefer `mutate(...)` by default. +- Use `mutateAsync(...)` only when Promise semantics are required, such as sequential or parallel mutation flows. +- Wrap every `await mutateAsync(...)` in `try/catch`. + ## Component Boundaries - Separate first-render surfaces from secondary interactive surfaces. For dialogs, dropdowns, popovers, and similar branches, extract a small local component that owns the trigger, open state, and overlay/menu content when that branch obscures the parent flow. diff --git a/web/AGENTS.md b/web/AGENTS.md index dc72a293d1..bdf1304861 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -9,10 +9,6 @@ - In new or modified code, use only overlay primitives from `@langgenius/dify-ui/*`. - Do not introduce deprecated overlay imports from `@/app/components/base/*`; when touching legacy callers, prefer migrating them. -## Query & Mutation (Mandatory) - -- `frontend-query-mutation` is the source of truth for Dify frontend contracts, query and mutation call-site patterns, conditional queries, invalidation, and mutation error handling. - ## Design Token Mapping - When translating Figma designs to code, read `../packages/dify-ui/AGENTS.md` for the Figma `--radius/*` token to Tailwind `rounded-*` class mapping. The two scales are offset by one step.