Skip to main content

add-portal-screen

Add screens or components to the portal frontend. Use when the user asks to create a new portal page, screen, tab, or UI component.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
authgear/authgear-server
آخر نشاط في المصدر
١٤ أبريل ٢٠٢٦ في ٠٥:٥٧
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٢٬٠٥٨
التفرعات
١٢٥

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
add-portal-screen
description
Add screens or components to the portal frontend. Use when the user asks to create a new portal page, screen, tab, or UI component.
argument-hint
<feature name or description>
Follow this guide when adding screens or components to the portal. ## Step 1: Locate existing patterns Before writing any code, read at least one existing screen from `portal/src/screens/` that is similar to the feature being built. Reading the full file is preferred so you understand the complete pattern. **Good reference screens:** - Config-form screen with tabs: `portal/src/screens/fraud-protection/FraudProtectionConfigurationScreen.tsx` - Screen with sub-tab files: `portal/src/screens/api-resources/APIResourceDetailsScreen.tsx` Also read any components under `portal/src/components/` in the same feature area that exist already. --- ## Step 2: File structure ### Screens New screens go in: ``` portal/src/screens/<feature-name>/ <FeatureName>Screen.tsx <FeatureName>Screen.module.css ``` - Use **kebab-case** for the directory name (e.g., `fraud-protection`, `api-resources`). - Use **PascalCase** for file names (e.g., `FraudProtectionConfigurationScreen.tsx`). - Sub-tabs of a screen that are complex enough to deserve their own file live in the **same screen directory**, not in `components/`. ### Components Reusable or extracted sub-components go in: ``` portal/src/components/<feature-name>/ <ComponentName>.tsx <ComponentName>.module.css ``` - A component that is only used by one screen is still placed in `components/` when it is large enough to deserve its own file. - Shared low-level UI primitives (buttons, inputs, etc.) go in `portal/src/components/common/`. ### CSS modules Every component or screen that has its own styles gets its own `.module.css` file. Name it identically to the `.tsx` file (e.g., `FraudProtectionOverviewTab.module.css`). When two sibling components share many CSS classes (e.g., two card variants), they may both import from a single shared `.module.css` file. Name it after the dominant component (e.g., `OverviewMetricCard.module.css` shared by `OverviewMetricCard.tsx` and `OverviewEnforcementCard.tsx`). --- ## Step 3: Screen anatomy A screen file always has two components: ### Outer component (data shell) Handles routing params, loading/error states, and form setup. Renders nothing except loading spinners and error views until data is ready. Then renders `FormContainer` wrapping the inner content. ```tsx const MyFeatureScreen: React.VFC = function MyFeatureScreen() { const { appID } = useParams() as { appID: string }; // Data hooks const form = useAppConfigForm({ appID, constructFormState, constructConfig }); const featureConfig = useAppFeatureConfigQuery(appID); // Tab navigation (if the screen has tabs) const { selectedKey, onLinkClick, onChangeKey } = usePivotNavigation<MyTab>(["overview", "settings"]); // Loading / error guards — render nothing else until resolved if (form.isLoading || featureConfig.isLoading) { return <ShowLoading />; } if (form.loadError) { return <ShowError error={form.loadError} onRetry={form.reload} />; } if (featureConfig.loadError) { return <ShowError error={featureConfig.loadError} onRetry={featureConfig.reload} />; } return ( <FormContainer form={form} canSave={isModifiable} ...> <MyFeatureContent form={form} featureConfig={featureConfig.effectiveFeatureConfig?.my_feature} selectedKey={selectedKey} onLinkClick={onLinkClick} onChangeKey={onChangeKey} /> </FormContainer> ); }; ``` ### Inner content component Receives the form model and config as props. Handles user interactions and renders the full UI. Callbacks (e.g., `onChange` handlers) are defined here using `useCallback`. ```tsx interface MyFeatureContentProps { form: AppConfigFormModel<FormState>; featureConfig?: MyFeatureConfig; selectedKey: MyTab; onLinkClick: (item?: PivotItem) => void; onChangeKey: (key: MyTab) => void; } const MyFeatureContent: React.VFC<MyFeatureContentProps> = function MyFeatureContent(props) { const { form, featureConfig, selectedKey, onLinkClick, onChangeKey } = props; const { state, setState } = form; const onSomeFieldChange = useCallback( (_event, value?: string) => { setState((current) => ({ ...current, someField: value ?? "" })); }, [setState] ); return ( <ScreenContent layout="list"> <ScreenTitle className={styles.widget}>...</ScreenTitle> {/* ... */} </ScreenContent> ); }; ``` ### Form state helpers Define `constructFormState` and `constructConfig` as plain functions (not inside components) near the top of the file, after the `FormState` interface: ```tsx interface FormState { enabled: boolean; someField: string; } function constructFormState(config: PortalAPIAppConfig): FormState { return { enabled: config.my_feature?.enabled ?? false, someField: config.my_feature?.some_field ?? "", }; } function constructConfig( config: PortalAPIAppConfig, _initialState: FormState, currentState: FormState, _effectiveConfig: PortalAPIAppConfig ): PortalAPIAppConfig { return produce(config, (draft) => { draft.my_feature ??= {}; draft.my_feature.enabled = currentState.enabled; draft.my_feature.some_field = currentState.someField || undefined; clearEmptyObject(draft); }); } ``` --- ## Step 4: Register the screen in AppRoot After creating the screen file, register it as a lazy-loaded route in `portal/src/AppRoot.tsx`. ### Add the lazy import (near other similar imports) ```tsx const MyFeatureScreen = lazy( async () => import("./screens/my-feature/MyFeatureScreen") ); ``` ### Add the route (inside the JSX route tree) Find the appropriate `<Route>` section and add: ```tsx <Route path="my-feature" element={<MyFeatureScreen />} /> ``` --- ## Step 5: Breaking down large components When a component grows large, extract sub-components into `portal/src/components/<feature>/`. Good candidates for extraction: - **A tab's content** when the tab has its own state or data-fetching logic - **A repeated card/row pattern** rendered multiple times with different props - **A self-contained panel** (e.g., a sidebar widget with its own toggle state) ### Extraction rules 1. **Each extracted component gets its own `.tsx` and `.module.css`.** 2. **Do not pass raw `form` or `setState` into sub-components.** Instead, pass the specific values and typed callback functions they need. 3. **Keep state as close to its consumer as possible.** If state is only used inside a sub-component (e.g., `showAll` for a list toggle), define it inside that sub-component — do not lift it up. 4. **A sub-component that is only rendered conditionally** (e.g., only when a tab is active) does not need an `isActive` prop — it is simply not rendered when inactive, so queries inside it should use `skip: !enabled` (or similar) rather than checking tab state. ### Example: extracting a tab Before (all in one component): ```tsx {selectedKey === "overview" ? ( <section> {/* 200 lines of overview JSX */} </section> ) : null} ``` After: ```tsx // In components/my-feature/MyFeatureOverviewTab.tsx export interface MyFeatureOverviewTabProps { enabled: boolean; onChangeToSettings: () => void; } const MyFeatureOverviewTab: React.VFC<MyFeatureOverviewTabProps> = ... // In the screen: {selectedKey === "overview" ? ( <MyFeatureOverviewTab enabled={state.enabled} onChangeToSettings={() => onChangeKey("settings")} /> ) : null} ``` --- ## Step 6: GraphQL queries The portal has two separate GraphQL APIs with separate code-generation configs: | API | Schema | Query files | Generated output | Use for | |-----|--------|------------|-----------------|---------| | Admin API | `portal/src/graphql/adminapi/schema.graphql` | `portal/src/graphql/adminapi/query/*.graphql` | `*.generated.ts` next to the `.graphql` file | App-level data (users, audit logs, fraud protection, etc.) | | Portal API | `portal/src/graphql/portal/schema.graphql` | `portal/src/graphql/portal/query/*.graphql` | `*.generated.ts` next to the `.graphql` file | Portal-level data (subscriptions, feature config, app list, etc.) | ### Writing a query 1. Read the relevant schema file to understand available types and fields. 2. Create a `.graphql` file in the appropriate `query/` directory: ```graphql # portal/src/graphql/adminapi/query/myFeatureQuery.graphql query myFeatureQuery($appID: ID!, $rangeFrom: DateTime, $rangeTo: DateTime) { myFeature(appID: $appID, rangeFrom: $rangeFrom, rangeTo: $rangeTo) { totalCount someField nestedItems { id value } } } ``` 3. Run code generation from `portal/`: ``` npm run gentype ``` This produces a `myFeatureQuery.generated.ts` file next to the `.graphql` file containing typed hooks, variables types, and result types. ### Using the generated hook **Always import from the `.generated.ts` file, never inline `gql` in `.ts`/`.tsx` files.** ```tsx // ✅ Correct import { useMyFeatureQueryQuery } from "../../graphql/adminapi/query/myFeatureQuery.generated"; // ❌ Wrong — do not write inline gql documents const MY_QUERY = gql`query myFeatureQuery { ... }`; ``` The generated hook is a standard Apollo hook. Use Apollo options directly: ```tsx const { data, loading, error, refetch, } = useMyFeatureQueryQuery({ skip: !enabled, variables: { appID, rangeFrom, rangeTo }, }); const result = data?.myFeature ?? null; const onRetry = useCallback(() => { void refetch(); }, [refetch]); ``` ### When to use which API - **Admin API**: data tied to a specific app (audit logs, user data, app config overrides, fraud protection stats). The query variables typically include `appID`. - **Portal API**: portal-level data (subscription info, feature flags, app list). The existing hooks like `useAppFeatureConfigQuery` use this API. --- ## Step 7: CSS module conventions Use Tailwind utility classes via `@apply`. Never write raw CSS values when a Tailwind class exists. ```css /* ✅ Good */ .metricCard { @apply flex flex-col rounded-lg border border-[#edebe9] bg-white px-[14px] py-4; } /* ❌ Avoid raw CSS when Tailwind equivalent exists */ .metricCard { display: flex; flex-direction: column; border-radius: 0.5rem; padding: 1rem; } ``` For icon/badge variants that share a base style, group them in one selector then override per-variant: ```css /* Shared base */ .metricIcon, .metricIconSuccess, .metricIconWarning, .metricIconBlocked { @apply inline-flex h-8 w-8 flex-none items-center justify-center rounded-md text-[16px]; } /* Per-variant color */ .metricIcon { @apply bg-[#edf6ff] text-[#176df3]; } .metricIconSuccess { @apply bg-[#eef6ef] text-[#16a34a]; } .metricIconWarning { @apply bg-[#fff4ce] text-[#d97706]; } .metricIconBlocked { @apply bg-[#fef2f2] text-[#dc2626]; } ``` Map variants via a lookup object in the component rather than building class strings dynamically: ```tsx const iconVariantClass: Record<MetricIconVariant, string> = { default: styles.metricIcon, success: styles.metricIconSuccess, warning: styles.metricIconWarning, blocked: styles.metricIconBlocked, }; ``` --- ## Step 8: Quality checks After any changes to portal files, always run both checks and fix all errors before finishing: ``` cd portal npm run typecheck npm run eslint ``` Common issues to watch for: - **`@typescript-eslint/no-unnecessary-condition`**: A condition that is always true/false because TypeScript can prove it at the call site. Usually caused by checking a prop value that is already guaranteed by context (e.g., `isActive={selectedKey === "overview"}` inside a branch where `selectedKey === "overview"` is already true). - **Unused imports**: Remove any imports that are no longer referenced after refactoring. - **Missing `useCallback`/`useMemo`**: Inline arrow functions passed as props to components should be wrapped in `useCallback` to avoid unnecessary re-renders. --- ## Quick reference | Task | Command | |------|---------| | Generate TypeScript types from `.graphql` files | `cd portal && npm run gentype` | | Type-check all TypeScript | `cd portal && npm run typecheck` | | Lint all TypeScript/TSX | `cd portal && npm run eslint` | | Auto-fix lint issues | `cd portal && npm run eslint:format` | | Check CSS | `cd portal && npm run stylelint` |
عرض على GitHub