diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 330915d..d1501fc 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -19,7 +19,7 @@ Lint and formatting are enforced by ESLint and Prettier in CI (`docker compose r - missing JSDoc on internal helpers - subjective style preferences not codified in a project rule -If a finding would be caught by `npm run lint` or `npm run format`, it's redundant. +If a finding would be caught by `npm run lint` or `npm run prettier` (check) / `npm run prettier:fix` (alias `npm run format`), it's redundant. ## Security (cross-cutting) diff --git a/.github/workflows/apply_pr_checks.yaml b/.github/workflows/apply_pr_checks.yaml index 3ff1118..96b6fe2 100644 --- a/.github/workflows/apply_pr_checks.yaml +++ b/.github/workflows/apply_pr_checks.yaml @@ -40,6 +40,27 @@ jobs: migrator: - 'migrator/**' + check_formatting: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check formatting + run: npm run prettier + check_generated_graphql: timeout-minutes: 4 needs: [changes] diff --git a/AGENTS.md b/AGENTS.md index 7adcdb3..ea82166 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,7 +102,7 @@ Lint / format / type-check (no Docker needed): ```bash npm run lint # lint all packages -npm run format # format all packages +npm run prettier:fix # format all packages (alias: npm run format) (cd server && npm run lint) (cd client && npm run lint) ``` @@ -111,7 +111,7 @@ If tests fail with database errors, check migration logs via `docker compose log ## CI -CI runs entirely via GitHub Actions (`.github/workflows/apply_pr_checks.yaml`). All PR checks are defined as `docker compose` services so you can reproduce any CI job locally. Run them in your shell (paste-as-is — each command's exit code matches the corresponding CI step's exit code): +CI runs entirely via GitHub Actions (`.github/workflows/apply_pr_checks.yaml`). Most PR checks are defined as `docker compose` services so you can reproduce any CI job locally; the formatting check runs directly via `actions/setup-node`. Run them in your shell (paste-as-is — each command's exit code matches the corresponding CI step's exit code): ```bash docker compose run --rm codegen-check @@ -120,12 +120,14 @@ docker compose run --rm backend npm run build docker compose run --rm client npm run lint docker compose run --rm client npm run build docker compose run --rm test +npm ci && npm run prettier ``` Individual checks: | CI job | Local command | | ---------------------------------------- | ----------------------------------------------- | +| `check_formatting` | `npm ci && npm run prettier` | | `check_generated_graphql` | `docker compose run --rm codegen-check` | | `check_api_server` (lint) | `docker compose run --rm backend npm run lint` | | `check_api_server` (build) | `docker compose run --rm backend npm run build` | @@ -159,7 +161,7 @@ Note: `check_migration_order` runs only in GitHub Actions — it's GitHub-specif ## Code style -- **TypeScript:** ESLint + Prettier (configs in `.eslintrc.cjs` and `.prettierrc` per package). Run `npm run lint` and `npm run format` from root. +- **TypeScript:** ESLint + Prettier (Prettier config at root `.prettierrc`; ESLint configs per package in `server/` and `client/`). Run `npm run lint` and `npm run prettier:fix` from root. - **Naming:** Use camelCase for variables/functions; PascalCase for components/classes; SCREAMING_SNAKE_CASE for constants. - **GraphQL:** Type-safe resolvers and queries via codegen; never hand-edit `generated.ts`. - **Imports:** Absolute imports configured via `tsconfig.json` paths; prefer `@/` prefix over relative paths where configured. diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs index 0d7e85b..e4993e9 100644 --- a/client/.eslintrc.cjs +++ b/client/.eslintrc.cjs @@ -2,8 +2,7 @@ const restrictedSyntax = [ { selector: ':matches(TSTypeAliasDeclaration[id.name=Props], TSInterfaceDeclaration[id.name=Props])', - message: - 'React types for props should be inlined', + message: 'React types for props should be inlined', }, { selector: @@ -33,7 +32,7 @@ module.exports = { 'tailwind.config.js', '*.stories.tsx', 'vite.config.ts', - 'vite-env.d.ts' + 'vite-env.d.ts', ], plugins: ['@typescript-eslint', 'custom-rules'], rules: { @@ -130,8 +129,7 @@ module.exports = { { object: 'window', property: 'open', - message: - 'Use an tag to open links, rather than window.open.', + message: 'Use an tag to open links, rather than window.open.', }, { object: '_', diff --git a/client/eslint/index.js b/client/eslint/index.js index 3335fab..e7b5bc7 100644 --- a/client/eslint/index.js +++ b/client/eslint/index.js @@ -1,15 +1,18 @@ -const fs = require("fs"); -const path = require("path"); +const fs = require('fs'); +const path = require('path'); -const ruleFiles = fs - .readdirSync(__dirname) - .filter((file) => { - const fullPath = path.join(__dirname, file); - return !fs.statSync(fullPath).isDirectory() && file.endsWith(".js") && file !== "index.js" && !file.endsWith("test.js"); - }); +const ruleFiles = fs.readdirSync(__dirname).filter((file) => { + const fullPath = path.join(__dirname, file); + return ( + !fs.statSync(fullPath).isDirectory() && + file.endsWith('.js') && + file !== 'index.js' && + !file.endsWith('test.js') + ); +}); const rules = Object.fromEntries( - ruleFiles.map((file) => [path.basename(file, ".js"), require("./" + file)]) + ruleFiles.map((file) => [path.basename(file, '.js'), require('./' + file)]), ); module.exports = { rules }; diff --git a/client/eslint/no-casting-in-getFieldValueForRole.js b/client/eslint/no-casting-in-getFieldValueForRole.js index 10d55ea..c8a8813 100644 --- a/client/eslint/no-casting-in-getFieldValueForRole.js +++ b/client/eslint/no-casting-in-getFieldValueForRole.js @@ -22,7 +22,7 @@ module.exports = { getFieldValueForRole< GQLSchemaFieldRoles, keyof GQLSchemaFieldRoles - >(reportedItem, 'displayName')` + >(reportedItem, 'displayName')`, }); } diff --git a/client/src/components/ActionParameterInputs.tsx b/client/src/components/ActionParameterInputs.tsx index 49da7a2..2b47b30 100644 --- a/client/src/components/ActionParameterInputs.tsx +++ b/client/src/components/ActionParameterInputs.tsx @@ -3,8 +3,8 @@ import { Input, InputNumber, Select, Switch, Tooltip } from 'antd'; import { useMemo } from 'react'; import { - type GQLActionParameter, GQLActionParameterType, + type GQLActionParameter, } from '../graphql/generated'; const { Option } = Select; @@ -260,7 +260,10 @@ export function findMissingRequiredParameters( (Array.isArray(value) && value.length === 0); // A `defaultValue` on the spec satisfies "required" since the server will // backfill on publish — keeps the UX consistent with server behavior. - if (isEmpty && (param.defaultValue === undefined || param.defaultValue === null)) { + if ( + isEmpty && + (param.defaultValue === undefined || param.defaultValue === null) + ) { missing.push(param.displayName); } } @@ -273,25 +276,22 @@ export function findMissingRequiredParameters( * replaced. Tiny but used in three call sites. */ export function useUpdateActionValues( - setMap: ( - next: Readonly>, - ) => void, + setMap: (next: Readonly>) => void, map: Readonly>, ) { return useMemo( - () => - (actionId: string, values: ActionParameterValues) => { - // Drop the entry entirely when the values map is empty, so the GQL - // input doesn't carry meaningless `{}` entries that confuse log - // readers. - if (Object.keys(values).length === 0) { - if (!(actionId in map)) return; - const { [actionId]: _omitted, ...rest } = map; - setMap(rest); - return; - } - setMap({ ...map, [actionId]: values }); - }, + () => (actionId: string, values: ActionParameterValues) => { + // Drop the entry entirely when the values map is empty, so the GQL + // input doesn't carry meaningless `{}` entries that confuse log + // readers. + if (Object.keys(values).length === 0) { + if (!(actionId in map)) return; + const { [actionId]: _omitted, ...rest } = map; + setMap(rest); + return; + } + setMap({ ...map, [actionId]: values }); + }, [map, setMap], ); } diff --git a/client/src/components/ActionParametersModal.tsx b/client/src/components/ActionParametersModal.tsx index 13b29c1..000e394 100644 --- a/client/src/components/ActionParametersModal.tsx +++ b/client/src/components/ActionParametersModal.tsx @@ -1,13 +1,13 @@ +import { type GQLActionParameter } from '@/graphql/generated'; import { Tooltip } from 'antd'; import { useEffect, useState } from 'react'; import CoopModal from '@/webpages/dashboard/components/CoopModal'; import { type CoopModalFooterButtonProps } from '@/webpages/dashboard/components/CoopModalFooter'; -import { type GQLActionParameter } from '@/graphql/generated'; import ActionParameterInputs, { - type ActionParameterValues, findMissingRequiredParameters, + type ActionParameterValues, } from './ActionParameterInputs'; type Props = { @@ -88,7 +88,11 @@ export default function ActionParametersModal({ {!canSave && (
- Fill in {missing.length === 1 ? 'the required field' : 'all required fields'} to continue. + Fill in{' '} + {missing.length === 1 + ? 'the required field' + : 'all required fields'}{' '} + to continue.
)} diff --git a/client/src/components/common/StepProgressIndicator.tsx b/client/src/components/common/StepProgressIndicator.tsx index 53d399e..bd6dbdc 100644 --- a/client/src/components/common/StepProgressIndicator.tsx +++ b/client/src/components/common/StepProgressIndicator.tsx @@ -73,8 +73,8 @@ export default function StepProgressIndicator(props: { index < currentStepIndex ? 'text-coop-blue' : index === currentStepIndex - ? 'text-gray-600' - : 'text-gray-400' + ? 'text-gray-600' + : 'text-gray-400' }`} > {step.name} diff --git a/client/src/coop-ui/Badge.tsx b/client/src/coop-ui/Badge.tsx index 4a9e787..a01ae5d 100644 --- a/client/src/coop-ui/Badge.tsx +++ b/client/src/coop-ui/Badge.tsx @@ -29,7 +29,8 @@ const badgeVariants = cva( ); export interface BadgeProps - extends React.HTMLAttributes, + extends + React.HTMLAttributes, VariantProps {} function Badge({ className, variant, size, ...props }: BadgeProps) { diff --git a/client/src/coop-ui/Button.tsx b/client/src/coop-ui/Button.tsx index 52af110..b0e0fff 100644 --- a/client/src/coop-ui/Button.tsx +++ b/client/src/coop-ui/Button.tsx @@ -36,12 +36,14 @@ const buttonVariants = cva( { variant: 'default', color: 'gray', - class: 'bg-gray-800 text-white hover:bg-gray-900 focus:bg-gray-900 dark:bg-white dark:text-neutral-800', + class: + 'bg-gray-800 text-white hover:bg-gray-900 focus:bg-gray-900 dark:bg-white dark:text-neutral-800', }, { variant: 'default', color: 'indigo', - class: 'bg-indigo-500 text-white hover:bg-indigo-700 focus:bg-indigo-700', + class: + 'bg-indigo-500 text-white hover:bg-indigo-700 focus:bg-indigo-700', }, { variant: 'default', @@ -51,7 +53,8 @@ const buttonVariants = cva( { variant: 'default', color: 'yellow', - class: 'bg-yellow-500 text-white hover:bg-yellow-700 focus:bg-yellow-700', + class: + 'bg-yellow-500 text-white hover:bg-yellow-700 focus:bg-yellow-700', }, { variant: 'default', @@ -63,81 +66,96 @@ const buttonVariants = cva( { variant: 'outline', color: 'gray', - class: 'border-gray-600 text-gray-600 hover:bg-gray-50 hover:border-gray-800 hover:text-gray-800 focus:border-gray-800 focus:text-gray-800 dark:border-gray-500 dark:text-gray-500 dark:hover:border-gray-700 dark:hover:text-gray-700 dark:focus:border-gray-700 dark:focus:text-gray-700', + class: + 'border-gray-600 text-gray-600 hover:bg-gray-50 hover:border-gray-800 hover:text-gray-800 focus:border-gray-800 focus:text-gray-800 dark:border-gray-500 dark:text-gray-500 dark:hover:border-gray-700 dark:hover:text-gray-700 dark:focus:border-gray-700 dark:focus:text-gray-700', }, { variant: 'outline', color: 'indigo', - class: 'border-indigo-600 text-indigo-600 hover:bg-indigo-50 hover:border-indigo-800 hover:text-indigo-800 focus:border-indigo-800 focus:text-indigo-800 dark:border-indigo-500 dark:text-indigo-500 dark:hover:border-indigo-700 dark:hover:text-indigo-700 dark:focus:border-indigo-700 dark:focus:text-indigo-700', + class: + 'border-indigo-600 text-indigo-600 hover:bg-indigo-50 hover:border-indigo-800 hover:text-indigo-800 focus:border-indigo-800 focus:text-indigo-800 dark:border-indigo-500 dark:text-indigo-500 dark:hover:border-indigo-700 dark:hover:text-indigo-700 dark:focus:border-indigo-700 dark:focus:text-indigo-700', }, { variant: 'outline', color: 'red', - class: 'border-red-600 text-red-600 hover:bg-red-50 hover:border-red-800 hover:text-red-800 focus:border-red-800 focus:text-red-800 dark:border-red-500 dark:text-red-500 dark:hover:border-red-700 dark:hover:text-red-700 dark:focus:border-red-700 dark:focus:text-red-700', + class: + 'border-red-600 text-red-600 hover:bg-red-50 hover:border-red-800 hover:text-red-800 focus:border-red-800 focus:text-red-800 dark:border-red-500 dark:text-red-500 dark:hover:border-red-700 dark:hover:text-red-700 dark:focus:border-red-700 dark:focus:text-red-700', }, { variant: 'outline', color: 'yellow', - class: 'border-yellow-600 text-yellow-600 hover:bg-yellow-50 hover:border-yellow-800 hover:text-yellow-800 focus:border-yellow-800 focus:text-yellow-800 dark:border-yellow-500 dark:text-yellow-500 dark:hover:border-yellow-700 dark:hover:text-yellow-700 dark:focus:border-yellow-700 dark:focus:text-yellow-700', + class: + 'border-yellow-600 text-yellow-600 hover:bg-yellow-50 hover:border-yellow-800 hover:text-yellow-800 focus:border-yellow-800 focus:text-yellow-800 dark:border-yellow-500 dark:text-yellow-500 dark:hover:border-yellow-700 dark:hover:text-yellow-700 dark:focus:border-yellow-700 dark:focus:text-yellow-700', }, { variant: 'outline', color: 'teal', - class: 'border-teal-600 text-teal-600 hover:bg-teal-50 hover:border-teal-800 hover:text-teal-800 focus:border-teal-800 focus:text-teal-800 dark:border-teal-500 dark:text-teal-500 dark:hover:border-teal-700 dark:hover:text-teal-700 dark:focus:border-teal-700 dark:focus:text-teal-700', + class: + 'border-teal-600 text-teal-600 hover:bg-teal-50 hover:border-teal-800 hover:text-teal-800 focus:border-teal-800 focus:text-teal-800 dark:border-teal-500 dark:text-teal-500 dark:hover:border-teal-700 dark:hover:text-teal-700 dark:focus:border-teal-700 dark:focus:text-teal-700', }, // Ghost variant compounds { variant: 'ghost', color: 'gray', - class: 'text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-gray-500 dark:hover:bg-gray-800/30 dark:hover:text-gray-400 dark:focus:bg-gray-800/30 dark:focus:text-gray-400', + class: + 'text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-gray-500 dark:hover:bg-gray-800/30 dark:hover:text-gray-400 dark:focus:bg-gray-800/30 dark:focus:text-gray-400', }, { variant: 'ghost', color: 'indigo', - class: 'text-indigo-600 hover:bg-indigo-100 hover:text-indigo-800 focus:bg-indigo-100 focus:text-indigo-800 dark:text-indigo-500 dark:hover:bg-indigo-800/30 dark:hover:text-indigo-400 dark:focus:bg-indigo-800/30 dark:focus:text-indigo-400', + class: + 'text-indigo-600 hover:bg-indigo-100 hover:text-indigo-800 focus:bg-indigo-100 focus:text-indigo-800 dark:text-indigo-500 dark:hover:bg-indigo-800/30 dark:hover:text-indigo-400 dark:focus:bg-indigo-800/30 dark:focus:text-indigo-400', }, { variant: 'ghost', color: 'red', - class: 'text-red-600 hover:bg-red-100 hover:text-red-800 focus:bg-red-100 focus:text-red-800 dark:text-red-500 dark:hover:bg-red-800/30 dark:hover:text-red-400 dark:focus:bg-red-800/30 dark:focus:text-red-400', + class: + 'text-red-600 hover:bg-red-100 hover:text-red-800 focus:bg-red-100 focus:text-red-800 dark:text-red-500 dark:hover:bg-red-800/30 dark:hover:text-red-400 dark:focus:bg-red-800/30 dark:focus:text-red-400', }, { variant: 'ghost', color: 'yellow', - class: 'text-yellow-600 hover:bg-yellow-100 hover:text-yellow-800 focus:bg-yellow-100 focus:text-yellow-800 dark:text-yellow-500 dark:hover:bg-yellow-800/30 dark:hover:text-yellow-400 dark:focus:bg-yellow-800/30 dark:focus:text-yellow-400', + class: + 'text-yellow-600 hover:bg-yellow-100 hover:text-yellow-800 focus:bg-yellow-100 focus:text-yellow-800 dark:text-yellow-500 dark:hover:bg-yellow-800/30 dark:hover:text-yellow-400 dark:focus:bg-yellow-800/30 dark:focus:text-yellow-400', }, { variant: 'ghost', color: 'teal', - class: 'text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-teal-500 dark:hover:bg-teal-800/30 dark:hover:text-teal-400 dark:focus:bg-teal-800/30 dark:focus:text-teal-400', + class: + 'text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-teal-500 dark:hover:bg-teal-800/30 dark:hover:text-teal-400 dark:focus:bg-teal-800/30 dark:focus:text-teal-400', }, // Soft variant compounds { variant: 'soft', color: 'gray', - class: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:bg-gray-200 dark:text-gray-400 dark:hover:bg-gray-900 dark:focus:bg-gray-900', + class: + 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:bg-gray-200 dark:text-gray-400 dark:hover:bg-gray-900 dark:focus:bg-gray-900', }, { variant: 'soft', color: 'indigo', - class: 'bg-indigo-100 text-indigo-900 hover:bg-indigo-200 focus:bg-indigo-200 dark:text-indigo-400 dark:hover:bg-indigo-900 dark:focus:bg-indigo-900', + class: + 'bg-indigo-100 text-indigo-900 hover:bg-indigo-200 focus:bg-indigo-200 dark:text-indigo-400 dark:hover:bg-indigo-900 dark:focus:bg-indigo-900', }, { variant: 'soft', color: 'red', - class: 'bg-red-100 text-red-900 hover:bg-red-200 focus:bg-red-200 dark:text-red-400 dark:hover:bg-red-900 dark:focus:bg-red-900', + class: + 'bg-red-100 text-red-900 hover:bg-red-200 focus:bg-red-200 dark:text-red-400 dark:hover:bg-red-900 dark:focus:bg-red-900', }, { variant: 'soft', color: 'yellow', - class: 'bg-yellow-100 text-yellow-900 hover:bg-yellow-200 focus:bg-yellow-200 dark:text-yellow-400 dark:hover:bg-yellow-900 dark:focus:bg-yellow-900', + class: + 'bg-yellow-100 text-yellow-900 hover:bg-yellow-200 focus:bg-yellow-200 dark:text-yellow-400 dark:hover:bg-yellow-900 dark:focus:bg-yellow-900', }, { variant: 'soft', color: 'teal', - class: 'bg-teal-100 text-teal-900 hover:bg-teal-200 focus:bg-teal-200 dark:text-teal-400 dark:hover:bg-teal-900 dark:focus:bg-teal-900', + class: + 'bg-teal-100 text-teal-900 hover:bg-teal-200 focus:bg-teal-200 dark:text-teal-400 dark:hover:bg-teal-900 dark:focus:bg-teal-900', }, // White variant compounds @@ -171,27 +189,32 @@ const buttonVariants = cva( { variant: 'link', color: 'gray', - class: 'text-gray-600 hover:text-gray-800 focus:text-gray-800 dark:text-gray-500 dark:hover:text-gray-400 dark:focus:text-gray-400', + class: + 'text-gray-600 hover:text-gray-800 focus:text-gray-800 dark:text-gray-500 dark:hover:text-gray-400 dark:focus:text-gray-400', }, { variant: 'link', color: 'indigo', - class: 'text-indigo-600 hover:text-indigo-800 focus:text-indigo-800 dark:text-indigo-500 dark:hover:text-indigo-400 dark:focus:text-indigo-400', + class: + 'text-indigo-600 hover:text-indigo-800 focus:text-indigo-800 dark:text-indigo-500 dark:hover:text-indigo-400 dark:focus:text-indigo-400', }, { variant: 'link', color: 'red', - class: 'text-red-600 hover:text-red-800 focus:text-red-800 dark:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400', + class: + 'text-red-600 hover:text-red-800 focus:text-red-800 dark:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400', }, { variant: 'link', color: 'yellow', - class: 'text-yellow-600 hover:text-yellow-800 focus:text-yellow-800 dark:text-yellow-500 dark:hover:text-yellow-400 dark:focus:text-yellow-400', + class: + 'text-yellow-600 hover:text-yellow-800 focus:text-yellow-800 dark:text-yellow-500 dark:hover:text-yellow-400 dark:focus:text-yellow-400', }, { variant: 'link', color: 'teal', - class: 'text-teal-600 hover:text-teal-800 focus:text-teal-800 dark:text-teal-500 dark:hover:text-teal-400 dark:focus:text-teal-400', + class: + 'text-teal-600 hover:text-teal-800 focus:text-teal-800 dark:text-teal-500 dark:hover:text-teal-400 dark:focus:text-teal-400', }, ], defaultVariants: { @@ -206,8 +229,10 @@ export type ButtonVariant = VariantProps['variant']; export type ButtonColor = VariantProps['color']; export type ButtonSize = VariantProps['size']; -export interface ButtonProps - extends Omit, 'color'> { +export interface ButtonProps extends Omit< + React.ButtonHTMLAttributes, + 'color' +> { variant?: ButtonVariant; color?: ButtonColor; size?: ButtonSize; @@ -236,7 +261,7 @@ const Button = React.forwardRef( const Comp = asChild ? Slot : 'button'; const finalColor = - variant === 'white' && !color ? 'gray' : color ?? 'indigo'; + variant === 'white' && !color ? 'gray' : (color ?? 'indigo'); const content = ( <> diff --git a/client/src/coop-ui/Calendar.stories.tsx b/client/src/coop-ui/Calendar.stories.tsx index 9eb45f3..e268044 100644 --- a/client/src/coop-ui/Calendar.stories.tsx +++ b/client/src/coop-ui/Calendar.stories.tsx @@ -1,7 +1,7 @@ import { Calendar, CalendarProps } from '@/coop-ui/Calendar'; -import { action } from 'storybook/actions'; import { Meta, StoryFn } from '@storybook/react'; import * as React from 'react'; +import { action } from 'storybook/actions'; export default { title: 'Components/Calendar', diff --git a/client/src/coop-ui/Checkbox.tsx b/client/src/coop-ui/Checkbox.tsx index ea6f5cd..ab29e11 100644 --- a/client/src/coop-ui/Checkbox.tsx +++ b/client/src/coop-ui/Checkbox.tsx @@ -6,11 +6,10 @@ import * as React from 'react'; // Note: indeterminate state can be added back once there is a need for it // it's been removed due to the need to cast the value to boolean // when using this component -interface CheckboxProps - extends Omit< - React.ComponentPropsWithoutRef, - 'onCheckedChange' - > { +interface CheckboxProps extends Omit< + React.ComponentPropsWithoutRef, + 'onCheckedChange' +> { onCheckedChange?: (checked: boolean) => void; } diff --git a/client/src/coop-ui/Input.tsx b/client/src/coop-ui/Input.tsx index 02a56b7..098eaa4 100644 --- a/client/src/coop-ui/Input.tsx +++ b/client/src/coop-ui/Input.tsx @@ -1,8 +1,7 @@ import { cn } from '@/lib/utils'; import * as React from 'react'; -export interface InputProps - extends React.InputHTMLAttributes { +export interface InputProps extends React.InputHTMLAttributes { startSlot?: React.ReactNode; endSlot?: React.ReactNode; } @@ -17,10 +16,10 @@ const Input = React.forwardRef( startSlot && endSlot ? 'rounded-none' : startSlot - ? 'rounded-r-lg rounded-l-none' - : endSlot - ? 'rounded-l-lg rounded-r-none' - : 'rounded-lg', + ? 'rounded-r-lg rounded-l-none' + : endSlot + ? 'rounded-l-lg rounded-r-none' + : 'rounded-lg', className, ); diff --git a/client/src/coop-ui/Select.stories.tsx b/client/src/coop-ui/Select.stories.tsx index d426067..e1696cb 100644 --- a/client/src/coop-ui/Select.stories.tsx +++ b/client/src/coop-ui/Select.stories.tsx @@ -166,7 +166,9 @@ export const Sizes: Story = { render: () => (
- Small: + + Small: + @@ -198,7 +202,9 @@ export const Sizes: Story = {
- Large: + + Large: + - + onChange({ min })} /> - + - onChange(options.filter((_, optionIndex) => optionIndex !== index)) + onChange( + options.filter((_, optionIndex) => optionIndex !== index), + ) } aria-label="Remove option" > @@ -756,7 +770,10 @@ export function validateDrafts( // widgets in `DefaultValueInput`, so only range/membership can drift. const dv = draft.defaultValue; if (dv !== undefined) { - if (draft.type === GQLActionParameterType.Number && typeof dv === 'number') { + if ( + draft.type === GQLActionParameterType.Number && + typeof dv === 'number' + ) { if (draft.min !== undefined && dv < draft.min) { return `${at}: default below min.`; } @@ -788,7 +805,10 @@ export function fromGraphQLParameters( readonly description?: string | null; readonly type: GQLActionParameterType; readonly required: boolean; - readonly options?: ReadonlyArray<{ readonly value: string; readonly label: string }> | null; + readonly options?: ReadonlyArray<{ + readonly value: string; + readonly label: string; + }> | null; readonly min?: number | null; readonly max?: number | null; readonly maxLength?: number | null; @@ -819,7 +839,8 @@ export function useParameterDraftsFromAction< T extends Parameters[0], >(parameters: T | undefined): ActionParameterDraft[] | undefined { return useMemo( - () => (parameters === undefined ? undefined : fromGraphQLParameters(parameters)), + () => + parameters === undefined ? undefined : fromGraphQLParameters(parameters), [parameters], ); } diff --git a/client/src/webpages/dashboard/banks/MatchingBanksDashboard.tsx b/client/src/webpages/dashboard/banks/MatchingBanksDashboard.tsx index 412dac1..a9de504 100644 --- a/client/src/webpages/dashboard/banks/MatchingBanksDashboard.tsx +++ b/client/src/webpages/dashboard/banks/MatchingBanksDashboard.tsx @@ -56,7 +56,6 @@ export default function MatchingBanksDashboard() { ? 'location' : 'hash' }`} - disabled={!canEditBanks} disabledTooltipTitle="To create Matching Banks, you need Admin permissions." disabledTooltipPlacement="bottomRight" diff --git a/client/src/webpages/dashboard/banks/hash/HashBankForm.tsx b/client/src/webpages/dashboard/banks/hash/HashBankForm.tsx index 8803af7..faa639e 100644 --- a/client/src/webpages/dashboard/banks/hash/HashBankForm.tsx +++ b/client/src/webpages/dashboard/banks/hash/HashBankForm.tsx @@ -1,32 +1,34 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Form, Input, Select, Slider, Switch, Tag } from 'antd'; -import { useNavigate, useParams } from 'react-router-dom'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet-async'; +import { useNavigate, useParams } from 'react-router-dom'; + +import FullScreenLoading from '../../../../components/common/FullScreenLoading'; +import CoopButton from '../../components/CoopButton'; +import CoopModal from '../../components/CoopModal'; +import FormHeader from '../../components/FormHeader'; +import FormSectionHeader from '../../components/FormSectionHeader'; +import NameDescriptionInput from '../../components/NameDescriptionInput'; + import { + GQLHashBankByIdDocument, + namedOperations, useGQLCreateHashBankMutation, - useGQLUpdateHashBankMutation, - useGQLUpdateExchangeCredentialsMutation, - useGQLHashBankByIdQuery, - useGQLExchangeApisQuery, useGQLExchangeApiSchemaLazyQuery, - namedOperations, - GQLHashBankByIdDocument, + useGQLExchangeApisQuery, + useGQLHashBankByIdQuery, + useGQLUpdateExchangeCredentialsMutation, + useGQLUpdateHashBankMutation, type GQLExchangeApiSchemaQuery, } from '../../../../graphql/generated'; -import CoopModal from '../../components/CoopModal'; -import FormHeader from '../../components/FormHeader'; -import FormSectionHeader from '../../components/FormSectionHeader'; -import NameDescriptionInput from '../../components/NameDescriptionInput'; -import CoopButton from '../../components/CoopButton'; -import FullScreenLoading from '../../../../components/common/FullScreenLoading'; type SchemaField = GQLExchangeApiSchemaQuery['exchangeApiSchema'] extends | infer S | null | undefined ? S extends { config_schema: { fields: ReadonlyArray } } - ? F - : never + ? F + : never : never; const EXCHANGE_DISPLAY_NAMES: Record = { @@ -93,7 +95,7 @@ function DynamicSchemaFields({ (fieldName: string, field: SchemaField, raw: string) => { onChange({ ...values, [fieldName]: coerceFieldValue(field, raw) }); }, - [values, onChange] + [values, onChange], ); if (fields.length === 0) return null; @@ -151,9 +153,7 @@ function DynamicSchemaFields({ ) : ( (null); - const [exchangeConfigValues, setExchangeConfigValues] = useState>({}); - const [exchangeCredValues, setExchangeCredValues] = useState>({}); + const [selectedExchangeApi, setSelectedExchangeApi] = useState( + null, + ); + const [exchangeConfigValues, setExchangeConfigValues] = useState< + Record + >({}); + const [exchangeCredValues, setExchangeCredValues] = useState< + Record + >({}); const isCreating = id == null; @@ -202,7 +208,7 @@ export default function HashBankForm() { const exchangeApisQuery = useGQLExchangeApisQuery({ skip: !isCreating }); const exchangeApis = useMemo( () => exchangeApisQuery.data?.exchangeApis ?? [], - [exchangeApisQuery.data?.exchangeApis] + [exchangeApisQuery.data?.exchangeApis], ); const [fetchSchema, schemaQuery] = useGQLExchangeApiSchemaLazyQuery(); @@ -211,7 +217,7 @@ export default function HashBankForm() { const selectedApiInfo = useMemo( () => exchangeApis.find((a) => a.name === selectedExchangeApi), - [exchangeApis, selectedExchangeApi] + [exchangeApis, selectedExchangeApi], ); useEffect(() => { @@ -222,7 +228,9 @@ export default function HashBankForm() { } }, [selectedExchangeApi, fetchSchema]); - const [editCredValues, setEditCredValues] = useState>({}); + const [editCredValues, setEditCredValues] = useState>( + {}, + ); const [showCredForm, setShowCredForm] = useState(false); useEffect(() => { @@ -381,15 +389,15 @@ export default function HashBankForm() { const exchangeInput = selectedExchangeApi && schema ? { - api_name: selectedExchangeApi, - config_json: JSON.stringify(exchangeConfigValues), - credentials_json: - schema.credentials_schema && + api_name: selectedExchangeApi, + config_json: JSON.stringify(exchangeConfigValues), + credentials_json: + schema.credentials_schema && selectedApiInfo && !selectedApiInfo.has_auth - ? JSON.stringify(exchangeCredValues) - : undefined, - } + ? JSON.stringify(exchangeCredValues) + : undefined, + } : undefined; createHashBank({ @@ -440,8 +448,10 @@ export default function HashBankForm() { hideModal(); if ( - (createMutationParams.data?.createHashBank && 'data' in createMutationParams.data.createHashBank) || - (updateMutationParams.data?.updateHashBank && 'data' in updateMutationParams.data.updateHashBank) + (createMutationParams.data?.createHashBank && + 'data' in createMutationParams.data.createHashBank) || + (updateMutationParams.data?.updateHashBank && + 'data' in updateMutationParams.data.updateHashBank) ) { navigate(-1); } @@ -453,7 +463,7 @@ export default function HashBankForm() { (f) => f.required && (exchangeConfigValues[f.name] == null || - exchangeConfigValues[f.name] === '') + exchangeConfigValues[f.name] === ''), ); const hasRequiredCredsMissing = @@ -465,10 +475,11 @@ export default function HashBankForm() { (f) => f.required && (exchangeCredValues[f.name] == null || - exchangeCredValues[f.name] === '') + exchangeCredValues[f.name] === ''), ); - const isExchangeIncomplete = Boolean(hasRequiredConfigMissing) || Boolean(hasRequiredCredsMissing); + const isExchangeIncomplete = + Boolean(hasRequiredConfigMissing) || Boolean(hasRequiredCredsMissing); const modal = ( - {bank.exchange.has_auth ? 'Credentials Set' : 'Credentials Missing'} + {bank.exchange.has_auth + ? 'Credentials Set' + : 'Credentials Missing'} {bank.exchange.last_fetch_succeeded === false && ( Fetch Failed @@ -540,7 +553,8 @@ export default function HashBankForm() {
{bank.exchange.last_fetch_time && ( - Last fetch: {new Date(bank.exchange.last_fetch_time).toLocaleString()} + Last fetch:{' '} + {new Date(bank.exchange.last_fetch_time).toLocaleString()} {bank.exchange.fetched_items != null && ( <> · {bank.exchange.fetched_items} items )} @@ -548,7 +562,9 @@ export default function HashBankForm() { )} {bank.exchange.last_fetch_succeeded === false && ( - The last fetch from this exchange failed. Check that credentials are correct and the exchange service is reachable. + The last fetch from this exchange failed. Check that + credentials are correct and the exchange service is + reachable. )}
@@ -581,15 +597,13 @@ export default function HashBankForm() { f.required) - .some( - (f) => - editCredValues[f.name] == null || - editCredValues[f.name] === '' - ) - } + disabled={schema.credentials_schema.fields + .filter((f) => f.required) + .some( + (f) => + editCredValues[f.name] == null || + editCredValues[f.name] === '', + )} disabledTooltipTitle="Please fill in all required credential fields" onClick={onUpdateCredentials} /> @@ -625,15 +639,18 @@ export default function HashBankForm() { marks={{ 0: 'Disabled', 0.5: '50%', - 1: 'Enabled' + 1: 'Enabled', }} tooltip={{ - formatter: (value) => `${Math.round((value ?? 0) * 100)}%` + formatter: (value) => `${Math.round((value ?? 0) * 100)}%`, }} - style={{ - '--ant-slider-track-background-color': getSliderColor(enabledRatio), - maxWidth: '100%', - } as React.CSSProperties} + style={ + { + '--ant-slider-track-background-color': + getSliderColor(enabledRatio), + maxWidth: '100%', + } as React.CSSProperties + } onChange={(value) => { setEnabledRatio(value); }} @@ -661,7 +678,9 @@ export default function HashBankForm() { loading={exchangeApisQuery.loading} className="max-w-md" options={exchangeApis.map((api) => ({ - label: EXCHANGE_DISPLAY_NAMES[api.name] ?? api.name.replace(/_/g, ' '), + label: + EXCHANGE_DISPLAY_NAMES[api.name] ?? + api.name.replace(/_/g, ' '), value: api.name, }))} /> @@ -702,12 +721,11 @@ export default function HashBankForm() { )} - {schema.credentials_schema && - selectedApiInfo?.has_auth && ( -
- Credentials for this exchange API are already configured. -
- )} + {schema.credentials_schema && selectedApiInfo?.has_auth && ( +
+ Credentials for this exchange API are already configured. +
+ )} )} diff --git a/client/src/webpages/dashboard/banks/hash/HashBanksDashboard.tsx b/client/src/webpages/dashboard/banks/hash/HashBanksDashboard.tsx index 001a88c..96e1084 100644 --- a/client/src/webpages/dashboard/banks/hash/HashBanksDashboard.tsx +++ b/client/src/webpages/dashboard/banks/hash/HashBanksDashboard.tsx @@ -1,13 +1,9 @@ -import React, { useMemo, useState, useCallback } from 'react'; import { Tag } from 'antd'; -import { MouseEvent } from 'react'; -import { useNavigate } from 'react-router-dom'; +import React, { MouseEvent, useCallback, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet-async'; -import { - useGQLHashBanksQuery, - useGQLDeleteHashBankMutation, - namedOperations -} from '../../../../graphql/generated'; +import { useNavigate } from 'react-router-dom'; + +import FullScreenLoading from '../../../../components/common/FullScreenLoading'; import CoopModal from '../../components/CoopModal'; import EmptyDashboard from '../../components/EmptyDashboard'; import RowMutations, { @@ -19,7 +15,12 @@ import { } from '../../components/table/filters'; import { stringSort } from '../../components/table/sort'; import CustomTable from '../../components/table/Table'; -import FullScreenLoading from '../../../../components/common/FullScreenLoading'; + +import { + namedOperations, + useGQLDeleteHashBankMutation, + useGQLHashBanksQuery, +} from '../../../../graphql/generated'; const getStatusColor = (enabled_ratio: number) => { if (enabled_ratio === 0) return 'red'; @@ -45,19 +46,25 @@ export default function HashBanksDashboard() { const navigate = useNavigate(); - const editBank = useCallback((id: string, event: MouseEvent) => { - // This ensures that the row's onClick isn't called because - // the row is the parent component - event.stopPropagation(); - navigate(`/dashboard/rules/banks/form/hash/${id}`); - }, [navigate]); + const editBank = useCallback( + (id: string, event: MouseEvent) => { + // This ensures that the row's onClick isn't called because + // the row is the parent component + event.stopPropagation(); + navigate(`/dashboard/rules/banks/form/hash/${id}`); + }, + [navigate], + ); - const onDeleteBank = useCallback((id: string) => { - deleteHashBank({ - variables: { id }, - refetchQueries: [namedOperations.Query.HashBanks], - }); - }, [deleteHashBank]); + const onDeleteBank = useCallback( + (id: string) => { + deleteHashBank({ + variables: { id }, + refetchQueries: [namedOperations.Query.HashBanks], + }); + }, + [deleteHashBank], + ); const showModal = useCallback((id: string, event: MouseEvent) => { // This ensures that the row's onClick isn't called because @@ -69,15 +76,18 @@ export default function HashBanksDashboard() { }); }, []); - const mutations = useCallback((id: string) => { - return ( - editBank(id, event)} - onDelete={(event: MouseEvent) => showModal(id, event)} - canDelete={true} - /> - ); - }, [editBank, showModal]); + const mutations = useCallback( + (id: string) => { + return ( + editBank(id, event)} + onDelete={(event: MouseEvent) => showModal(id, event)} + canDelete={true} + /> + ); + }, + [editBank, showModal], + ); const columns = useMemo( () => [ @@ -212,4 +222,4 @@ export default function HashBanksDashboard() { {deleteModal} ); -} \ No newline at end of file +} diff --git a/client/src/webpages/dashboard/bulk_actioning/BulkActioningDashboard.tsx b/client/src/webpages/dashboard/bulk_actioning/BulkActioningDashboard.tsx index df9993d..34f3873 100644 --- a/client/src/webpages/dashboard/bulk_actioning/BulkActioningDashboard.tsx +++ b/client/src/webpages/dashboard/bulk_actioning/BulkActioningDashboard.tsx @@ -7,8 +7,8 @@ import { Link, useNavigate } from 'react-router-dom'; import { type JsonObject } from 'type-fest'; import ActionParameterInputs, { - type ActionParameterValues, findMissingRequiredParameters, + type ActionParameterValues, } from '../../../components/ActionParameterInputs'; import FullScreenLoading from '../../../components/common/FullScreenLoading'; import { selectFilterByLabelOption } from '../components/antDesignUtils'; diff --git a/client/src/webpages/dashboard/components/PolicyDropdown.tsx b/client/src/webpages/dashboard/components/PolicyDropdown.tsx index ac85004..c0efd06 100644 --- a/client/src/webpages/dashboard/components/PolicyDropdown.tsx +++ b/client/src/webpages/dashboard/components/PolicyDropdown.tsx @@ -65,7 +65,7 @@ export default function PolicyDropdown(props: { treeLine={true} maxTagCount={maxTagCount} placeholder={ - placeholder ?? multiple ? 'Select Policies' : 'Select policy' + (placeholder ?? multiple) ? 'Select Policies' : 'Select policy' } dropdownMatchSelectWidth={false} value={selectedPolicyIds} diff --git a/client/src/webpages/dashboard/components/RoundedTag.tsx b/client/src/webpages/dashboard/components/RoundedTag.tsx index 8f48bb1..9b2cbe2 100644 --- a/client/src/webpages/dashboard/components/RoundedTag.tsx +++ b/client/src/webpages/dashboard/components/RoundedTag.tsx @@ -56,7 +56,7 @@ export default function RoundedTag(
- {title} + {title}
); } diff --git a/client/src/webpages/dashboard/components/TextToken.tsx b/client/src/webpages/dashboard/components/TextToken.tsx index 6d7f272..fae7ee6 100644 --- a/client/src/webpages/dashboard/components/TextToken.tsx +++ b/client/src/webpages/dashboard/components/TextToken.tsx @@ -8,8 +8,8 @@ export default function TextToken(props: { const { title, onDelete, disabled } = props; return (
- {title} - {Boolean(!disabled) ? : null} + {title} + {Boolean(!disabled) ? : null}
); } diff --git a/client/src/webpages/dashboard/components/table/sort.tsx b/client/src/webpages/dashboard/components/table/sort.tsx index d911514..665b97d 100644 --- a/client/src/webpages/dashboard/components/table/sort.tsx +++ b/client/src/webpages/dashboard/components/table/sort.tsx @@ -208,6 +208,9 @@ export function dateSort(dateKey: string) { if (!b) return -1; // Sort by timestamp (oldest first = smaller timestamp first) - return new Date(a as string | Date).getTime() - new Date(b as string | Date).getTime(); + return ( + new Date(a as string | Date).getTime() - + new Date(b as string | Date).getTime() + ); }; } diff --git a/client/src/webpages/dashboard/integrations/IntegrationCard.tsx b/client/src/webpages/dashboard/integrations/IntegrationCard.tsx index 1c48660..9ce3315 100644 --- a/client/src/webpages/dashboard/integrations/IntegrationCard.tsx +++ b/client/src/webpages/dashboard/integrations/IntegrationCard.tsx @@ -1,9 +1,9 @@ import { ReactNode, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import type { GQLIntegrationMetadata } from '../../../graphql/generated'; import CoopModal from '../components/CoopModal'; +import type { GQLIntegrationMetadata } from '../../../graphql/generated'; import { INTEGRATION_LOGO_FALLBACKS } from './integrationLogos'; export default function IntegrationCard(props: { @@ -14,9 +14,7 @@ export default function IntegrationCard(props: { const { name, title, docsUrl } = integration; // Integrations page uses only the plain logo (logoUrl from logoPath). Do not fall back to logoWithBackgroundUrl. const rawLogo = - integration.logoUrl ?? - INTEGRATION_LOGO_FALLBACKS[name]?.logo ?? - ''; + integration.logoUrl ?? INTEGRATION_LOGO_FALLBACKS[name]?.logo ?? ''; // Resolve relative API paths to absolute URL so img loads correctly (e.g. /api/v1/integration-logos/ID). const logo = typeof rawLogo === 'string' && rawLogo.startsWith('/') diff --git a/client/src/webpages/dashboard/integrations/IntegrationConfigApiCredentialsSection.tsx b/client/src/webpages/dashboard/integrations/IntegrationConfigApiCredentialsSection.tsx index c4441b3..df4da57 100644 --- a/client/src/webpages/dashboard/integrations/IntegrationConfigApiCredentialsSection.tsx +++ b/client/src/webpages/dashboard/integrations/IntegrationConfigApiCredentialsSection.tsx @@ -151,9 +151,10 @@ export default function IntegrationConfigApiCredentialsSection(props: { truePercentage: 'True percentage (0–100)', }; - const renderPluginCredential = ( - pluginCredential: { __typename: 'PluginIntegrationApiCredential'; credential: Record }, - ) => { + const renderPluginCredential = (pluginCredential: { + __typename: 'PluginIntegrationApiCredential'; + credential: Record; + }) => { const credential = pluginCredential.credential ?? {}; const entries = Object.entries(credential).filter( ([key]) => key !== 'name', @@ -166,16 +167,15 @@ export default function IntegrationConfigApiCredentialsSection(props: {
{fieldsToShow.map(([key, value]) => (
-
- {PLUGIN_FIELD_LABELS[key] ?? key} -
+
{PLUGIN_FIELD_LABELS[key] ?? key}
{ const next = { ...credential, [key]: event.target.value }; setApiCredential({ __typename: 'PluginIntegrationApiCredential', - credential: next as import('../../../graphql/generated').Scalars['JSONObject'], + credential: + next as import('../../../graphql/generated').Scalars['JSONObject'], }); }} /> diff --git a/client/src/webpages/dashboard/integrations/IntegrationConfigForm.tsx b/client/src/webpages/dashboard/integrations/IntegrationConfigForm.tsx index 2ec4fc7..cffd563 100644 --- a/client/src/webpages/dashboard/integrations/IntegrationConfigForm.tsx +++ b/client/src/webpages/dashboard/integrations/IntegrationConfigForm.tsx @@ -49,7 +49,9 @@ gql` } } - mutation SetPluginIntegrationConfig($input: SetPluginIntegrationConfigInput!) { + mutation SetPluginIntegrationConfig( + $input: SetPluginIntegrationConfigInput! + ) { setPluginIntegrationConfig(input: $input) { ... on SetIntegrationConfigSuccessResponse { config { @@ -136,9 +138,7 @@ gql` * This function returns an empty API credential config (type is * IntegrationConfigApiCredential), so the UI can display the proper empty inputs. */ -export function getNewEmptyApiKey( - name: string, -): GQLIntegrationApiCredential { +export function getNewEmptyApiKey(name: string): GQLIntegrationApiCredential { switch (name) { case 'GOOGLE_CONTENT_SAFETY_API': { return { @@ -258,8 +258,7 @@ export default function IntegrationConfigForm() { response?.__typename === 'IntegrationConfigSuccessResult' ? response.config : undefined; - const formattedName = - apiConfig?.title ?? integrationName.replace(/_/g, ' '); + const formattedName = apiConfig?.title ?? integrationName.replace(/_/g, ' '); const logo = apiConfig ? (apiConfig.logoUrl ?? INTEGRATION_LOGO_FALLBACKS[apiConfig.name]?.logo ?? @@ -290,7 +289,7 @@ export default function IntegrationConfigForm() { 'googleContentSafetyApi' in mappedApiCredential && !( mappedApiCredential[ - 'googleContentSafetyApi' + 'googleContentSafetyApi' ] as GQLGoogleContentSafetyApiIntegrationApiCredential ).apiKey ) { @@ -307,11 +306,8 @@ export default function IntegrationConfigForm() { if ( 'zentropi' in mappedApiCredential && - !( - mappedApiCredential[ - 'zentropi' - ] as GQLZentropiIntegrationApiCredential - ).apiKey + !(mappedApiCredential['zentropi'] as GQLZentropiIntegrationApiCredential) + .apiKey ) { return 'Please input the Zentropi API key'; } @@ -334,7 +330,7 @@ export default function IntegrationConfigForm() { if (isPluginIntegration) { const cred = apiCredential.__typename === 'PluginIntegrationApiCredential' - ? apiCredential.credential ?? {} + ? (apiCredential.credential ?? {}) : {}; await setPluginConfig({ variables: { @@ -361,15 +357,15 @@ export default function IntegrationConfigForm() { const [modalTitle, modalBody, modalButtonText] = mutationError == null ? [ - `${formattedName} Config Saved`, - `Your ${formattedName} Config was successfully saved!`, - 'Done', - ] + `${formattedName} Config Saved`, + `Your ${formattedName} Config was successfully saved!`, + 'Done', + ] : [ - `Error Saving ${formattedName} Config`, - `We encountered an error trying to save your ${formattedName} Config. Please try again.`, - 'OK', - ]; + `Error Saving ${formattedName} Config`, + `We encountered an error trying to save your ${formattedName} Config. Please try again.`, + 'OK', + ]; const onHideModal = () => { hideModal(); @@ -403,11 +399,11 @@ export default function IntegrationConfigForm() { case 'GOOGLE_CONTENT_SAFETY_API': return ( <> - The Content Safety API is an AI classifier which issues a Child - Safety prioritization recommendation on content sent to it. Content Safety API users - must conduct their own manual review in order to determine whether to take - action on the content, and comply with applicable local reporting - laws. Apply for API keys{' '} + The Content Safety API is an AI classifier which issues a Child + Safety prioritization recommendation on content sent to it. Content + Safety API users must conduct their own manual review in order to + determine whether to take action on the content, and comply with + applicable local reporting laws. Apply for API keys{' '}
here - - {' '} + {' '} and mention in your application that you are using the Coop moderation tool. Upon reviewing your application, Google will be - back in touch shortly to take the application forward if you qualify. + back in touch shortly to take the application forward if you + qualify. ); case 'OPEN_AI': @@ -441,11 +437,7 @@ export default function IntegrationConfigForm() {
- +
{`${formattedName} Integration`}
@@ -473,7 +465,9 @@ export default function IntegrationConfigForm() { Learn more about how to read model cards )} -
Configuration
+
+ Configuration +
Configure your integration settings below.
diff --git a/client/src/webpages/dashboard/integrations/IntegrationsDashboard.tsx b/client/src/webpages/dashboard/integrations/IntegrationsDashboard.tsx index 38d318f..97382ac 100644 --- a/client/src/webpages/dashboard/integrations/IntegrationsDashboard.tsx +++ b/client/src/webpages/dashboard/integrations/IntegrationsDashboard.tsx @@ -38,8 +38,11 @@ export default function IntegrationsDashboard() { useGQLAvailableIntegrationsQuery({ fetchPolicy: 'network-only', }); - const { loading: loadingMy, error, data: myData } = - useGQLMyIntegrationsQuery(); + const { + loading: loadingMy, + error, + data: myData, + } = useGQLMyIntegrationsQuery(); const loading = loadingCatalog || loadingMy; @@ -58,9 +61,9 @@ export default function IntegrationsDashboard() { const myIntegrations = allIntegrations.filter((it) => myIntegrationNames.includes(it.name), ); - const otherIntegrations = allIntegrations.filter( - (it) => !myIntegrationNames.includes(it.name), - ).sort((a, b) => a.name.localeCompare(b.name)); + const otherIntegrations = allIntegrations + .filter((it) => !myIntegrationNames.includes(it.name)) + .sort((a, b) => a.name.localeCompare(b.name)); return (
diff --git a/client/src/webpages/dashboard/integrations/ModelCardView.tsx b/client/src/webpages/dashboard/integrations/ModelCardView.tsx index 4c6ed43..109562d 100644 --- a/client/src/webpages/dashboard/integrations/ModelCardView.tsx +++ b/client/src/webpages/dashboard/integrations/ModelCardView.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; import { ChevronDown, ChevronRight } from 'lucide-react'; +import { useState } from 'react'; import type { GQLModelCard, diff --git a/client/src/webpages/dashboard/investigation/ItemInvestigationSummary.tsx b/client/src/webpages/dashboard/investigation/ItemInvestigationSummary.tsx index dd2ae4b..0e20b6a 100644 --- a/client/src/webpages/dashboard/investigation/ItemInvestigationSummary.tsx +++ b/client/src/webpages/dashboard/investigation/ItemInvestigationSummary.tsx @@ -13,6 +13,7 @@ import { type GQLContentSchemaFieldRoles, type GQLThreadItemType, } from '../../../graphql/generated'; +import { findFirstIframeUrl } from '../../../utils/contentUrlUtils'; import { getFieldValueForRole, getFieldValueOrValues, @@ -22,7 +23,6 @@ import type { ItemTypeFieldFieldData } from '../item_types/itemTypeUtils'; import ItemActionHistory from '../items/ItemActionHistory'; import IframeContentDisplayComponent from '../mrt/manual_review_job/IframeContentDisplayComponent'; import FieldsComponent from '../mrt/manual_review_job/v2/ManualReviewJobFieldsComponent'; -import { findFirstIframeUrl } from '../../../utils/contentUrlUtils'; export default function ItemInvestigationSummary(props: { item: { @@ -254,7 +254,9 @@ export default function ItemInvestigationSummary(props: { 'type' in firstIframeUrl && firstIframeUrl.type === 'URL' ? (
- +
) : null}
diff --git a/client/src/webpages/dashboard/mrt/ManualReviewDecisionsTable.tsx b/client/src/webpages/dashboard/mrt/ManualReviewDecisionsTable.tsx index 1d764a7..5b43665 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewDecisionsTable.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewDecisionsTable.tsx @@ -187,8 +187,8 @@ export default function ManualReviewDecisionsTable(props: { return a.Header === 'Name' ? -1 : b.Header === 'Name' - ? 1 - : a.Header.localeCompare(b.Header); + ? 1 + : a.Header.localeCompare(b.Header); }); const filledInData = columns diff --git a/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisionsFilter.tsx b/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisionsFilter.tsx index 22127ea..5b7db70 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisionsFilter.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisionsFilter.tsx @@ -1,6 +1,6 @@ +import { DateRangePicker } from '@/coop-ui/DateRangePicker'; import ChevronDown from '@/icons/lni/Direction/chevron-down.svg?react'; import ChevronUp from '@/icons/lni/Direction/chevron-up.svg?react'; -import { DateRangePicker } from '@/coop-ui/DateRangePicker'; import { Select } from 'antd'; import without from 'lodash/without'; import { useRef, useState } from 'react'; @@ -43,20 +43,20 @@ export type RecentDecisionsFilterInput = Omit< export type DecisionOrAction = | { - type: 'CUSTOM_ACTION'; - actionId: string; - } + type: 'CUSTOM_ACTION'; + actionId: string; + } | { - type: 'REJECT_APPEAL' | 'ACCEPT_APPEAL'; - appealId: string; - actionIds: string[]; - } + type: 'REJECT_APPEAL' | 'ACCEPT_APPEAL'; + appealId: string; + actionIds: string[]; + } | { - type: Exclude< - GQLManualReviewDecisionType, - 'CUSTOM_ACTION' | 'RELATED_ACTION' | 'REJECT_APPEAL' | 'ACCEPT_APPEAL' - >; - }; + type: Exclude< + GQLManualReviewDecisionType, + 'CUSTOM_ACTION' | 'RELATED_ACTION' | 'REJECT_APPEAL' | 'ACCEPT_APPEAL' + >; + }; const decisionFilterByColumns = [ 'decisions', @@ -167,9 +167,9 @@ export default function ManualReviewRecentDecisionsFilter(props: { it === 'RELATED_ACTION' || it === 'CUSTOM_ACTION' ? undefined : { - id: jsonStringify({ type: it }), - name: getReadableNameFromDecisionType(it), - }, + id: jsonStringify({ type: it }), + name: getReadableNameFromDecisionType(it), + }, ), ].flat(), ); @@ -254,12 +254,14 @@ export default function ManualReviewRecentDecisionsFilter(props: { something to do with dynamically choosing whether to render each icon because when we render both and just hide one of them, componentRef.current.contains() works. */}
{filterByMenuVisible && ( @@ -295,8 +297,9 @@ export default function ManualReviewRecentDecisionsFilter(props: { ); return (
{isExpanded && ( diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobContentBlurableVideo.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobContentBlurableVideo.tsx index 949fc92..9410760 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobContentBlurableVideo.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobContentBlurableVideo.tsx @@ -72,8 +72,8 @@ export default function ManualReviewJobContentBlurableVideo(props: { ? blurStrength ? BLUR_LEVELS[blurStrength] : !playing - ? 'blur-sm' - : 'blur-0' + ? 'blur-sm' + : 'blur-0' : 'blur-0' }`} > diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobCommentSection.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobCommentSection.tsx index a669b0e..1a5fc47 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobCommentSection.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobCommentSection.tsx @@ -99,7 +99,9 @@ function ManualReviewJobComment(props: { isBeingDeleted ? 'text-gray-300' : 'text-gray-500' }`} > - {formatDistanceToNow(new Date(comment.createdAt as string), { addSuffix: true })} + {formatDistanceToNow(new Date(comment.createdAt as string), { + addSuffix: true, + })}
{ - const { conditionSet, conditionSetIndex, signals, parentConditionSet } = opts; + const { conditionSet, conditionSetIndex, signals, parentConditionSet } = + opts; if (hasNestedConditionSets(conditionSet)) { const conditions = conditionSet.conditions; diff --git a/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRuleFormCondition.tsx b/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRuleFormCondition.tsx index 673f192..a096458 100644 --- a/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRuleFormCondition.tsx +++ b/client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRuleFormCondition.tsx @@ -1,7 +1,10 @@ import { DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons'; import { Button, Select, Tooltip } from 'antd'; -import { GQLConditionConjunction, GQLSignal } from '../../../../graphql/generated'; +import { + GQLConditionConjunction, + GQLSignal, +} from '../../../../graphql/generated'; import { hasNestedConditionSets, removeCondition, @@ -9,9 +12,9 @@ import { import { ConditionInput, ConditionLocation, + isConditionSet, RuleFormConditionSet, RuleFormLeafCondition, - isConditionSet, } from '../../rules/types'; import ManualReviewQueueRuleConditionComparator from './condition/comparator/ManualReviewQueueRuleConditionComparator'; import ManualReviewQueueRuleConditionInput from './condition/input/ManualReviewQueueRuleConditionInput'; @@ -226,7 +229,9 @@ export default function ManualReviewQueueRuleFormCondition(props: { ), ) } - allConditions={parentConditionSet.conditions.filter((c): c is RuleFormLeafCondition => !isConditionSet(c))} + allConditions={parentConditionSet.conditions.filter( + (c): c is RuleFormLeafCondition => !isConditionSet(c), + )} /> ; diff --git a/client/src/webpages/dashboard/mrt/queue_routing/condition/signal/ManualReviewQueueRuleConditionSignal.tsx b/client/src/webpages/dashboard/mrt/queue_routing/condition/signal/ManualReviewQueueRuleConditionSignal.tsx index 69a73b4..f765c37 100644 --- a/client/src/webpages/dashboard/mrt/queue_routing/condition/signal/ManualReviewQueueRuleConditionSignal.tsx +++ b/client/src/webpages/dashboard/mrt/queue_routing/condition/signal/ManualReviewQueueRuleConditionSignal.tsx @@ -1,8 +1,8 @@ +import { GQLSignal } from '@/graphql/generated'; import { DownOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import { useState } from 'react'; -import { GQLSignal } from '@/graphql/generated'; import RuleFormSignalModal from '../../../../rules/rule_form/signal_modal/RuleFormSignalModal'; import { ConditionLocation, diff --git a/client/src/webpages/dashboard/mrt/queue_routing/condition/threshold/ManualReviewQueueRuleConditionThreshold.tsx b/client/src/webpages/dashboard/mrt/queue_routing/condition/threshold/ManualReviewQueueRuleConditionThreshold.tsx index 533d122..6cd2b81 100644 --- a/client/src/webpages/dashboard/mrt/queue_routing/condition/threshold/ManualReviewQueueRuleConditionThreshold.tsx +++ b/client/src/webpages/dashboard/mrt/queue_routing/condition/threshold/ManualReviewQueueRuleConditionThreshold.tsx @@ -212,10 +212,10 @@ export default function ManualReviewQueueRuleConditionThreshold(props: { {renderBooleanThreshold ? 'Value' : renderPolicyThreshold - ? 'Policy' - : renderStringThreshold - ? 'Creation Source' - : 'Threshold'} + ? 'Policy' + : renderStringThreshold + ? 'Creation Source' + : 'Threshold'}
{!editing ? (
diff --git a/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsCard.tsx b/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsCard.tsx index 630e2be..e513241 100644 --- a/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsCard.tsx +++ b/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsCard.tsx @@ -1,4 +1,3 @@ -import { ChevronDown, ChevronUp } from 'lucide-react'; import { differenceInDays, differenceInHours, @@ -7,7 +6,7 @@ import { differenceInWeeks, differenceInYears, } from 'date-fns'; -import { ArrowRight } from 'lucide-react'; +import { ArrowRight, ChevronDown, ChevronUp } from 'lucide-react'; import React from 'react'; import { Link } from 'react-router-dom'; @@ -23,13 +22,11 @@ interface ManualReviewDashboardInsightsCardBaseProps { loading: boolean; } -interface ManualReviewDashboardInsightsCardWithChangeProps - extends ManualReviewDashboardInsightsCardBaseProps { +interface ManualReviewDashboardInsightsCardWithChangeProps extends ManualReviewDashboardInsightsCardBaseProps { change: number | undefined; } -interface ManualReviewDashboardInsightsCardWithLinkProps - extends ManualReviewDashboardInsightsCardBaseProps { +interface ManualReviewDashboardInsightsCardWithLinkProps extends ManualReviewDashboardInsightsCardBaseProps { link: string; linkTitle: string; } @@ -130,8 +127,8 @@ const ManualReviewDashboardInsightsCard = ( props.change === 0 ? 'text-slate-600 bg-slate-100' : props.change < 0 - ? 'text-red-600 bg-red-100' - : 'text-green-600 bg-green-100' + ? 'text-red-600 bg-red-100' + : 'text-green-600 bg-green-100' } p-1 rounded text-sm font-semibold flex items-center`} > {props.change === 0 ? null : props.change < 0 ? ( diff --git a/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsChart.tsx b/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsChart.tsx index c8c3fcd..df19679 100644 --- a/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsChart.tsx +++ b/client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsChart.tsx @@ -11,6 +11,7 @@ import { } from '@ant-design/icons'; import { gql } from '@apollo/client'; import { Tooltip as AntTooltip } from 'antd'; +import { format } from 'date-fns'; import flatten from 'lodash/flatten'; import keys from 'lodash/keys'; import map from 'lodash/map'; @@ -21,7 +22,6 @@ import sortBy from 'lodash/sortBy'; import sumBy from 'lodash/sumBy'; import union from 'lodash/union'; import without from 'lodash/without'; -import { format } from 'date-fns'; import React, { ReactNode, useCallback, @@ -698,7 +698,10 @@ export default function ManualReviewDashboardInsightsChart(props: { const formattedData = countsByDay?.map((it) => { const obj: { [key: string]: any } = { - ds: format(new Date(parseInt(it.time)), timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'), + ds: format( + new Date(parseInt(it.time)), + timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd', + ), }; obj[getLineNameFromCount(it)] = it.count; return obj; @@ -720,7 +723,9 @@ export default function ManualReviewDashboardInsightsChart(props: { ]; type ChartRow = Record; - const groupedData = formattedDataWithAllDates.reduce>((result, item) => { + const groupedData = formattedDataWithAllDates.reduce< + Record + >((result, item) => { const ds = item.ds; if (!(ds in result)) { diff --git a/client/src/webpages/dashboard/mrt/visualization/TimeToActionChart.tsx b/client/src/webpages/dashboard/mrt/visualization/TimeToActionChart.tsx index 49df570..056a64c 100644 --- a/client/src/webpages/dashboard/mrt/visualization/TimeToActionChart.tsx +++ b/client/src/webpages/dashboard/mrt/visualization/TimeToActionChart.tsx @@ -219,7 +219,9 @@ export default function TimeToActionByQueueChart({ const emptyChart = (
-
No data available for the selected time period.
+
+ No data available for the selected time period. +
getEmptyFilterState(timeWindow)} diff --git a/client/src/webpages/dashboard/overview/OverviewChart.tsx b/client/src/webpages/dashboard/overview/OverviewChart.tsx index 02f0d23..b324e33 100644 --- a/client/src/webpages/dashboard/overview/OverviewChart.tsx +++ b/client/src/webpages/dashboard/overview/OverviewChart.tsx @@ -8,6 +8,7 @@ import { truncateAndFormatLargeNumber } from '@/utils/number'; import { titleCaseEnumString } from '@/utils/string'; import { getDateRange } from '@/utils/time'; import { gql } from '@apollo/client'; +import { format } from 'date-fns'; import flatten from 'lodash/flatten'; import keys from 'lodash/keys'; import map from 'lodash/map'; @@ -16,7 +17,6 @@ import sortBy from 'lodash/sortBy'; import sum from 'lodash/sum'; import union from 'lodash/union'; import without from 'lodash/without'; -import { format } from 'date-fns'; import { useCallback, useEffect, @@ -325,7 +325,10 @@ export default function OverviewChart(props: { const formattedData = countsPerMetricPerTimeUnit?.map((it) => { const obj: { [key: string]: string | number } = { - ds: format(new Date(parseInt(it.time)), timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'), + ds: format( + new Date(parseInt(it.time)), + timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd', + ), }; obj[getLineNameFromCount(it)] = it.count; return obj; @@ -347,7 +350,9 @@ export default function OverviewChart(props: { ]; type ChartRow = Record; - const groupedData = formattedDataWithAllDates.reduce>((result, item) => { + const groupedData = formattedDataWithAllDates.reduce< + Record + >((result, item) => { const ds = item.ds; if (!(ds in result)) { @@ -461,7 +466,10 @@ export default function OverviewChart(props: {
- {!loading && (finalChartData.length === 0 || uniqueLines.length === 0 || !hasNonZeroData) ? ( + {!loading && + (finalChartData.length === 0 || + uniqueLines.length === 0 || + !hasNonZeroData) ? ( emptyChart ) : ( diff --git a/client/src/webpages/dashboard/overview/OverviewTable.tsx b/client/src/webpages/dashboard/overview/OverviewTable.tsx index c13f9a7..d7de844 100644 --- a/client/src/webpages/dashboard/overview/OverviewTable.tsx +++ b/client/src/webpages/dashboard/overview/OverviewTable.tsx @@ -97,7 +97,9 @@ export default function OverviewTable(props: { const emptyChart = (
-
No data available for the selected time period.
+
+ No data available for the selected time period. +
); diff --git a/client/src/webpages/dashboard/rules/dashboard/visualization/RulesDashboardInsights.tsx b/client/src/webpages/dashboard/rules/dashboard/visualization/RulesDashboardInsights.tsx index d9b5fa8..8366904 100644 --- a/client/src/webpages/dashboard/rules/dashboard/visualization/RulesDashboardInsights.tsx +++ b/client/src/webpages/dashboard/rules/dashboard/visualization/RulesDashboardInsights.tsx @@ -2,7 +2,6 @@ import './recharts.css'; import { DateRangePicker } from '@/coop-ui/DateRangePicker'; import { InvestmentFilled, PieChartAltFilled } from '@/icons'; -import { TriangleAlert } from 'lucide-react'; import { truncateAndFormatLargeNumber } from '@/utils/number'; import { BarChartOutlined, @@ -11,6 +10,7 @@ import { PieChartOutlined, } from '@ant-design/icons'; import { gql } from '@apollo/client'; +import { format } from 'date-fns'; import capitalize from 'lodash/capitalize'; import flatten from 'lodash/flatten'; import groupBy from 'lodash/groupBy'; @@ -26,7 +26,7 @@ import sum from 'lodash/sum'; import sumBy from 'lodash/sumBy'; import union from 'lodash/union'; import without from 'lodash/without'; -import { format } from 'date-fns'; +import { TriangleAlert } from 'lucide-react'; import React, { ReactNode, useCallback, useMemo, useState } from 'react'; import { Area, diff --git a/client/src/webpages/dashboard/rules/dashboard/visualization/rulesDashboardInsightsChart.tsx b/client/src/webpages/dashboard/rules/dashboard/visualization/rulesDashboardInsightsChart.tsx index 2750945..89750e3 100644 --- a/client/src/webpages/dashboard/rules/dashboard/visualization/rulesDashboardInsightsChart.tsx +++ b/client/src/webpages/dashboard/rules/dashboard/visualization/rulesDashboardInsightsChart.tsx @@ -10,6 +10,7 @@ import { TapFilled } from '@/icons'; import Download from '@/icons/lni/Web and Technology/download.svg?react'; import { truncateAndFormatLargeNumber } from '@/utils/number'; import type { TimeDivisionOptions } from '@/webpages/dashboard/overview/Overview'; +import { format } from 'date-fns'; import flatten from 'lodash/flatten'; import keys from 'lodash/keys'; import map from 'lodash/map'; @@ -18,7 +19,6 @@ import sortBy from 'lodash/sortBy'; import sumBy from 'lodash/sumBy'; import union from 'lodash/union'; import without from 'lodash/without'; -import { format } from 'date-fns'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { CSVLink } from 'react-csv'; import { @@ -197,7 +197,10 @@ export default function RuleDashboardInsightsChart(props: { const formattedData = countsByDay?.map((it) => { const obj: { [key: string]: any } = { - ds: format(new Date(parseInt(it.time)), timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'), + ds: format( + new Date(parseInt(it.time)), + timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd', + ), }; obj[getLineNameFromCount(it)] = it.count; return obj; @@ -219,7 +222,9 @@ export default function RuleDashboardInsightsChart(props: { ]; type ChartRow = Record; - const groupedData = formattedDataWithAllDates.reduce>((result, item) => { + const groupedData = formattedDataWithAllDates.reduce< + Record + >((result, item) => { const ds = item.ds; if (!(ds in result)) { @@ -539,7 +544,10 @@ export default function RuleDashboardInsightsChart(props: {
- {!loading && (finalChartData.length === 0 || uniqueLines.length === 0 || !hasNonZeroData) ? ( + {!loading && + (finalChartData.length === 0 || + uniqueLines.length === 0 || + !hasNonZeroData) ? ( emptyChart ) : ( diff --git a/client/src/webpages/dashboard/rules/info/insights/RuleInsightsActionsChart.tsx b/client/src/webpages/dashboard/rules/info/insights/RuleInsightsActionsChart.tsx index 83fdb45..71b6f91 100644 --- a/client/src/webpages/dashboard/rules/info/insights/RuleInsightsActionsChart.tsx +++ b/client/src/webpages/dashboard/rules/info/insights/RuleInsightsActionsChart.tsx @@ -1,14 +1,14 @@ import { DateRangePicker } from '@/coop-ui/DateRangePicker'; import { InvestmentFilled, PieChartAltFilled } from '@/icons'; -import { TriangleAlert } from 'lucide-react'; import { truncateAndFormatLargeNumber } from '@/utils/number'; import { BarChartOutlined, LineChartOutlined } from '@ant-design/icons'; import { gql } from '@apollo/client'; +import { format } from 'date-fns'; import last from 'lodash/last'; import orderBy from 'lodash/orderBy'; import sortBy from 'lodash/sortBy'; import sumBy from 'lodash/sumBy'; -import { format } from 'date-fns'; +import { TriangleAlert } from 'lucide-react'; import { ReactNode, useCallback, useMemo, useState } from 'react'; import { Bar, @@ -171,9 +171,7 @@ export default function RuleInsightsActionsChart(props: { ruleId: string }) { ); return (
-
- {label} -
+
{label}
{data.length > 1 && (
diff --git a/client/src/webpages/dashboard/rules/info/insights/sample_details/RuleInsightsSampleDetailMatchingValues.tsx b/client/src/webpages/dashboard/rules/info/insights/sample_details/RuleInsightsSampleDetailMatchingValues.tsx index cf4910a..336711b 100644 --- a/client/src/webpages/dashboard/rules/info/insights/sample_details/RuleInsightsSampleDetailMatchingValues.tsx +++ b/client/src/webpages/dashboard/rules/info/insights/sample_details/RuleInsightsSampleDetailMatchingValues.tsx @@ -49,9 +49,11 @@ export default function RuleInsightsSampleDetailMatchingValues(props: { const { loading, error, data } = useGQLMatchingBankNamesQuery({ skip: !type || - ![MatchingValueType.TEXT_BANK, MatchingValueType.LOCATION_BANK, MatchingValueType.IMAGE_BANK].includes( - type, - ), + ![ + MatchingValueType.TEXT_BANK, + MatchingValueType.LOCATION_BANK, + MatchingValueType.IMAGE_BANK, + ].includes(type), }); const { textBanks, locationBanks, hashBanks } = data?.myOrg?.banks ?? {}; @@ -132,7 +134,9 @@ export default function RuleInsightsSampleDetailMatchingValues(props: { case MatchingValueType.IMAGE_BANK: return staticValue({ text: matchingValues - .imageBankIds!.map((id) => hashBanks?.find((it) => it.id === id)?.name) + .imageBankIds!.map( + (id) => hashBanks?.find((it) => it.id === id)?.name, + ) .join(', '), outcome: condition.result?.outcome, matchedValue: condition.result?.matchedValue, diff --git a/client/src/webpages/dashboard/rules/rule_form/ReportingRuleForm.tsx b/client/src/webpages/dashboard/rules/rule_form/ReportingRuleForm.tsx index 8886a05..9e0cd63 100644 --- a/client/src/webpages/dashboard/rules/rule_form/ReportingRuleForm.tsx +++ b/client/src/webpages/dashboard/rules/rule_form/ReportingRuleForm.tsx @@ -23,13 +23,13 @@ import { GQLAction, GQLConditionConjunction, GQLReportingRuleStatus, + GQLSignal, GQLUserPermission, useGQLCreateReportingRuleMutation, useGQLDeleteReportingRuleMutation, useGQLReportingRuleFormOrgDataQuery, useGQLReportingRuleQuery, useGQLUpdateReportingRuleMutation, - GQLSignal, } from '../../../../graphql/generated'; import { userHasPermissions } from '../../../../routing/permissions'; import useRouteQueryParams from '../../../../routing/useRouteQueryParams'; diff --git a/client/src/webpages/dashboard/rules/rule_form/RuleForm.tsx b/client/src/webpages/dashboard/rules/rule_form/RuleForm.tsx index a41a6fc..af69578 100644 --- a/client/src/webpages/dashboard/rules/rule_form/RuleForm.tsx +++ b/client/src/webpages/dashboard/rules/rule_form/RuleForm.tsx @@ -1,5 +1,21 @@ import { Label } from '@/coop-ui/Label'; import { Switch } from '@/coop-ui/Switch'; +import { + GQLAction, + GQLConditionConjunction, + GQLRuleStatus, + GQLSignal, + GQLUserPermission, + namedOperations, + useGQLContentRuleFormConfigQuery, + useGQLCreateContentRuleMutation, + useGQLCreateUserRuleMutation, + useGQLDeleteRuleMutation, + useGQLMatchingBankIdsQuery, + useGQLRuleQuery, + useGQLUpdateContentRuleMutation, + useGQLUpdateUserRuleMutation, +} from '@/graphql/generated'; import CopyAlt from '@/icons/lni/Web and Technology/copy-alt.svg?react'; import TrashCan from '@/icons/lni/Web and Technology/trash-can.svg?react'; import { DownOutlined, PlusOutlined, UpOutlined } from '@ant-design/icons'; @@ -22,22 +38,6 @@ import NameDescriptionInput from '../../components/NameDescriptionInput'; import PolicyDropdown from '../../components/PolicyDropdown'; import SubmitButton from '../../components/SubmitButton'; -import { - GQLAction, - GQLConditionConjunction, - GQLRuleStatus, - GQLUserPermission, - namedOperations, - useGQLContentRuleFormConfigQuery, - useGQLCreateContentRuleMutation, - useGQLCreateUserRuleMutation, - useGQLDeleteRuleMutation, - useGQLMatchingBankIdsQuery, - useGQLRuleQuery, - useGQLUpdateContentRuleMutation, - useGQLUpdateUserRuleMutation, - GQLSignal, -} from '@/graphql/generated'; import { userHasPermissions } from '../../../../routing/permissions'; import useRouteQueryParams from '../../../../routing/useRouteQueryParams'; import { DAY, HOUR, MONTH, WEEK } from '../../../../utils/time'; diff --git a/client/src/webpages/dashboard/rules/rule_form/RuleFormCondition.tsx b/client/src/webpages/dashboard/rules/rule_form/RuleFormCondition.tsx index 496d314..e75bdf6 100644 --- a/client/src/webpages/dashboard/rules/rule_form/RuleFormCondition.tsx +++ b/client/src/webpages/dashboard/rules/rule_form/RuleFormCondition.tsx @@ -4,16 +4,16 @@ import { Button, Form, Select, Tooltip } from 'antd'; import { GQLConditionConjunction, GQLScalarType, - GQLValueComparator, GQLSignal, + GQLValueComparator, } from '../../../../graphql/generated'; import { CoopInput } from '../../types/enums'; import { ConditionInput, ConditionLocation, + isConditionSet, RuleFormConditionSet, RuleFormLeafCondition, - isConditionSet, } from '../types'; import RuleFormConditionComparator from './condition/comparator/RuleFormConditionComparator'; import { getDerivedFieldOutputType } from './condition/input/derivedField'; @@ -246,7 +246,9 @@ export default function RuleFormCondition(props: { location={location} inputScalarType={inputScalarType} onUpdateMatchingValues={onUpdateMatchingValues} - allConditions={parentConditionSet.conditions.filter((c): c is RuleFormLeafCondition => !isConditionSet(c))} + allConditions={parentConditionSet.conditions.filter( + (c): c is RuleFormLeafCondition => !isConditionSet(c), + )} /> - it.type === GQLSignalType.Custom, - ).length; + return eligibleSignals.filter((it) => it.type === GQLSignalType.Custom) + .length; }) .map((itemType) => ({ type: 'FULL_ITEM' as const, diff --git a/client/src/webpages/dashboard/rules/rule_form/RuleFormUtils.ts b/client/src/webpages/dashboard/rules/rule_form/RuleFormUtils.ts index 411037c..e730802 100644 --- a/client/src/webpages/dashboard/rules/rule_form/RuleFormUtils.ts +++ b/client/src/webpages/dashboard/rules/rule_form/RuleFormUtils.ts @@ -15,10 +15,10 @@ import { GQLDerivedFieldSpec, GQLLeafConditionFieldsFragment, GQLScalarType, + GQLSignal, GQLSignalType, GQLValueComparator, type GQLConditionInput, - GQLSignal, } from '../../../../graphql/generated'; import { taggedUnionToOneOfInput } from '../../../../graphql/inputHelpers'; import { locationAreaToLocationAreaInput } from '../../../../models/locationBank'; @@ -480,14 +480,14 @@ export function removeConditionSet( conditionSetIndex: number, ) { let newConditionSet = cloneDeep(conditionSet); - + if (hasNestedConditionSets(newConditionSet)) { const nestedConditionSets = newConditionSet.conditions; - + // Only allow deletion if there are multiple condition sets if (nestedConditionSets.length > 1) { nestedConditionSets.splice(conditionSetIndex, 1); - + // If, after removing this condition set, we now only have one ConditionSet // left, then we make it a top-level ConditionSet (rather than a ConditionSet // that just contains one ConditionSet within it). @@ -501,7 +501,7 @@ export function removeConditionSet( } } } - + return newConditionSet; } diff --git a/client/src/webpages/dashboard/rules/rule_form/condition/comparator/RuleFormConditionComparator.tsx b/client/src/webpages/dashboard/rules/rule_form/condition/comparator/RuleFormConditionComparator.tsx index 85dd69f..4e1e47b 100644 --- a/client/src/webpages/dashboard/rules/rule_form/condition/comparator/RuleFormConditionComparator.tsx +++ b/client/src/webpages/dashboard/rules/rule_form/condition/comparator/RuleFormConditionComparator.tsx @@ -30,22 +30,22 @@ export default function RuleFormConditionComparator(props: { const comparatorTypes = condition.signal?.outputType ? outputTypeToComparators(condition.signal.outputType) : inputScalarType === GQLScalarType.Number - ? [ - GQLValueComparator.Equals, - GQLValueComparator.NotEqualTo, - GQLValueComparator.GreaterThan, - GQLValueComparator.LessThan, - GQLValueComparator.GreaterThanOrEquals, - GQLValueComparator.LessThanOrEquals, - GQLValueComparator.IsUnavailable, - GQLValueComparator.IsNotProvided, - ] - : [ - GQLValueComparator.Equals, - GQLValueComparator.NotEqualTo, - GQLValueComparator.IsUnavailable, - GQLValueComparator.IsNotProvided, - ]; + ? [ + GQLValueComparator.Equals, + GQLValueComparator.NotEqualTo, + GQLValueComparator.GreaterThan, + GQLValueComparator.LessThan, + GQLValueComparator.GreaterThanOrEquals, + GQLValueComparator.LessThanOrEquals, + GQLValueComparator.IsUnavailable, + GQLValueComparator.IsNotProvided, + ] + : [ + GQLValueComparator.Equals, + GQLValueComparator.NotEqualTo, + GQLValueComparator.IsUnavailable, + GQLValueComparator.IsNotProvided, + ]; // If there is only one valid comparator to choose from, we should set // the condition.comparator value to that value by default diff --git a/client/src/webpages/dashboard/rules/rule_form/condition/input/RuleFormConditionInput.tsx b/client/src/webpages/dashboard/rules/rule_form/condition/input/RuleFormConditionInput.tsx index 85b2ba0..95c77b0 100644 --- a/client/src/webpages/dashboard/rules/rule_form/condition/input/RuleFormConditionInput.tsx +++ b/client/src/webpages/dashboard/rules/rule_form/condition/input/RuleFormConditionInput.tsx @@ -1,8 +1,8 @@ +import { GQLSignal } from '@/graphql/generated'; import { Form, Select } from 'antd'; import { selectFilterByLabelOption } from '@/webpages/dashboard/components/antDesignUtils'; -import { GQLSignal } from '@/graphql/generated'; import { safePick } from '../../../../../../utils/misc'; import { jsonParse, diff --git a/client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMatchingBankInput.tsx b/client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMatchingBankInput.tsx index f2be9eb..f7992d8 100644 --- a/client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMatchingBankInput.tsx +++ b/client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMatchingBankInput.tsx @@ -41,7 +41,8 @@ export default function RuleFormConditionMatchingBankInput(props: { const isRegexSignal = condition.signal?.type && receivesRegexInput(condition.signal.type); - const { textBankIds, locationBankIds, imageBankIds } = condition.matchingValues ?? {}; + const { textBankIds, locationBankIds, imageBankIds } = + condition.matchingValues ?? {}; const bankIds = textBankIds ?? locationBankIds ?? imageBankIds ?? []; const { loading, error, data } = useGQLMatchingBankIdsQuery(); diff --git a/client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMediaMatchingValues.tsx b/client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMediaMatchingValues.tsx index 3c56966..c638e02 100644 --- a/client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMediaMatchingValues.tsx +++ b/client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMediaMatchingValues.tsx @@ -2,6 +2,7 @@ import { Form, Select } from 'antd'; import ComponentLoading from '../../../../../../components/common/ComponentLoading'; import { selectFilterByLabelOption } from '@/webpages/dashboard/components/antDesignUtils'; + import { useGQLHashBanksQuery } from '../../../../../../graphql/generated'; import { ConditionLocation, RuleFormLeafCondition } from '../../../types'; @@ -15,7 +16,12 @@ export default function RuleFormConditionMediaMatchingValues(props: { ) => void; allConditions?: RuleFormLeafCondition[]; }) { - const { condition, location, onUpdateMatchingValues, allConditions = [] } = props; + const { + condition, + location, + onUpdateMatchingValues, + allConditions = [], + } = props; const { conditionSetIndex, conditionIndex } = location; const { loading, error, data } = useGQLHashBanksQuery(); @@ -25,7 +31,7 @@ export default function RuleFormConditionMediaMatchingValues(props: { const selectedBankIds = new Set( allConditions .filter((c) => c !== condition) // Exclude current condition by reference - .flatMap((c) => c.matchingValues?.imageBankIds ?? []) + .flatMap((c) => c.matchingValues?.imageBankIds ?? []), ); if (loading) { @@ -59,9 +65,9 @@ export default function RuleFormConditionMediaMatchingValues(props: { dropdownMatchSelectWidth={false} > {hashBanks.map((bank) => ( -
- + {newApiKey && (
- New API key generated successfully! Please copy and store it securely. + New API key generated successfully! Please copy and store it + securely. { />
)} - + {rotationError && (
@@ -246,15 +264,15 @@ const ApiAuthenticationSettings = () => {
)} - + { - {isNewKey ? 'Copy to clipboard' : 'Key is hidden for security'} + {isNewKey + ? 'Copy to clipboard' + : 'Key is hidden for security'}
} /> - + {isNewKey && (
- - + +
@@ -304,8 +332,9 @@ const ApiAuthenticationSettings = () => {

- This is the only time you'll see your API key in plain text. - We only store a hash value for security. Please copy and save this key securely. + This is the only time you'll see your API key in plain text. + We only store a hash value for security. Please copy and + save this key securely.

@@ -401,7 +430,7 @@ const ApiAuthenticationSettings = () => { } />
- + {/* API Key rotation confirmation dialog */} {showRotationDialog && (
@@ -410,8 +439,9 @@ const ApiAuthenticationSettings = () => { Rotate API Key - Are you sure you want to rotate your API key? This will generate a new key and - deactivate the current one. Make sure to update any applications using the current key. + Are you sure you want to rotate your API key? This will generate a + new key and deactivate the current one. Make sure to update any + applications using the current key.