diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 44ebd16a..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "root": true, - "env": { - "browser": true, - "es2021": true - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module", - "ecmaFeatures": { - "jsx": true - } - }, - "settings": { - "react": { - "version": "detect" - }, - "import/resolver": { - "typescript": { - "alwaysTryTypes": true - }, - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"] - } - } - }, - "plugins": ["react", "jsx-a11y", "import", "@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:@typescript-eslint/recommended", - "plugin:jsx-a11y/recommended" - ], - "ignorePatterns": ["node_modules/", "dist/", "build/"], - "rules": { - "react/jsx-curly-brace-presence": "warn", - "react/react-in-jsx-scope": "off", - "@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports" }], - "@typescript-eslint/no-import-type-side-effects": "error", - "newline-before-return": "error", - "import/no-unresolved": 0, - "import/named": 0, - "import/namespace": 0, - "import/default": 0, - "import/export": 0, - "no-restricted-imports": [ - "error", - { - "patterns": [ - { - "group": ["../*"], - "message": "Use @ imports instead of relative parent imports (../)." - }, - { - "group": [ - "./**/hooks/*", - "./**/components/*", - "./**/routes/*", - "./**/utils/*", - "./**/assets/*", - "./**/types/*" - ], - "message": "Use @ imports (e.g., @hooks/, @components/, etc.) instead of relative imports." - } - ] - } - ] - } -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..b8808e00 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,128 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import jsxA11y from 'eslint-plugin-jsx-a11y'; +import importPlugin from 'eslint-plugin-import'; +import tanstackQuery from '@tanstack/eslint-plugin-query'; +import globals from 'globals'; + +export default tseslint.config( + // Global ignores + { + ignores: [ + 'node_modules/', + 'dist/', + 'build/', + 'scripts/', + '*.config.js', + '*.config.cjs', + '.prettierrc.js', + ], + }, + + // Base JS recommended rules + js.configs.recommended, + + // TypeScript recommended rules + ...tseslint.configs.recommended, + + // TanStack Query recommended rules + ...tanstackQuery.configs['flat/recommended'], + + // Main configuration for TS/TSX files + { + files: ['**/*.{ts,tsx}'], + plugins: { + react, + 'react-hooks': reactHooks, + 'jsx-a11y': jsxA11y, + import: importPlugin, + }, + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.es2021, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + settings: { + react: { + version: 'detect', + }, + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + }, + }, + rules: { + // React rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + 'react/react-in-jsx-scope': 'off', + 'react/jsx-curly-brace-presence': 'warn', + + // React Hooks rules + ...reactHooks.configs.recommended.rules, + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + + // JSX A11y rules + ...jsxA11y.configs.recommended.rules, + + // TypeScript rules + '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + + // Import rules + 'import/no-unresolved': 0, + 'import/named': 0, + 'import/namespace': 0, + 'import/default': 0, + 'import/export': 0, + + // General rules + 'newline-before-return': 'error', + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['../*'], + message: 'Use @ imports instead of relative parent imports (../).', + }, + { + group: [ + './**/hooks/*', + './**/components/*', + './**/routes/*', + './**/utils/*', + './**/assets/*', + './**/types/*', + ], + message: 'Use @ imports (e.g., @hooks/, @components/, etc.) instead of relative imports.', + }, + ], + }, + ], + }, + } +); diff --git a/package.json b/package.json index 70dcfc09..444f8f12 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "test": "vitest --run", "i18n:validate": "node ./scripts/i18n/validate-translation.js", "i18n:schema": "node ./scripts/i18n/generate-schema.js", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint . --report-unused-disable-directives --max-warnings 0", "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"" }, @@ -87,6 +87,7 @@ "@medusajs/admin-vite-plugin": "2.12.6", "@medusajs/types": "2.12.6", "@medusajs/ui-preset": "2.12.6", + "@tanstack/eslint-plugin-query": "^5.91.3", "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/lodash": "^4.17.23", "@types/node": "^25.0.10", @@ -104,7 +105,9 @@ "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-tailwindcss": "^3.18.2", + "globals": "^17.1.0", "postcss": "^8.5.6", "prettier": "^3.8.1", "prettier-plugin-classnames": "^0.9.0", @@ -112,6 +115,7 @@ "tailwindcss": "^3.4.1", "tsup": "^8.5.1", "typescript": "^5.9.3", + "typescript-eslint": "^8.53.1", "vite": "^7.3.1", "vite-plugin-inspect": "^11.3.3", "vitest": "^4.0.17" diff --git a/src/app.tsx b/src/app.tsx index 7f460557..c4856f01 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,32 +1,32 @@ -import { DashboardApp } from "./dashboard-app" -import { DashboardPlugin } from "./dashboard-app/types" +import displayModule from 'virtual:medusa/displays'; +import formModule from 'virtual:medusa/forms'; +import menuItemModule from 'virtual:medusa/menu-items'; +import routeModule from 'virtual:medusa/routes'; +import widgetModule from 'virtual:medusa/widgets'; -import displayModule from "virtual:medusa/displays" -import formModule from "virtual:medusa/forms" -import menuItemModule from "virtual:medusa/menu-items" -import routeModule from "virtual:medusa/routes" -import widgetModule from "virtual:medusa/widgets" +import { DashboardApp } from './dashboard-app'; +import type { DashboardPlugin } from './dashboard-app/types'; -import "./index.css" +import './index.css'; const localPlugin = { widgetModule, routeModule, displayModule, formModule, - menuItemModule, -} + menuItemModule +}; interface AppProps { - plugins?: DashboardPlugin[] + plugins?: DashboardPlugin[]; } function App({ plugins = [] }: AppProps) { const app = new DashboardApp({ - plugins: [localPlugin, ...plugins], - }) + plugins: [localPlugin, ...plugins] + }); - return
{app.render()}
+ return
{app.render()}
; } -export default App +export default App; diff --git a/src/components/authentication/protected-route/index.ts b/src/components/authentication/protected-route/index.ts index ca67fd59..4acbc303 100644 --- a/src/components/authentication/protected-route/index.ts +++ b/src/components/authentication/protected-route/index.ts @@ -1 +1 @@ -export * from "./protected-route" +export * from './protected-route'; diff --git a/src/components/authentication/protected-route/protected-route.tsx b/src/components/authentication/protected-route/protected-route.tsx index c4fae776..29f41e27 100644 --- a/src/components/authentication/protected-route/protected-route.tsx +++ b/src/components/authentication/protected-route/protected-route.tsx @@ -1,8 +1,8 @@ -import { useMe } from "@hooks/api"; -import { Spinner } from "@medusajs/icons"; -import { SearchProvider } from "@providers/search-provider"; -import { SidebarProvider } from "@providers/sidebar-provider"; -import { Navigate, Outlet, useLocation } from "react-router-dom"; +import { useMe } from '@hooks/api'; +import { Spinner } from '@medusajs/icons'; +import { SearchProvider } from '@providers/search-provider'; +import { SidebarProvider } from '@providers/sidebar-provider'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; export const ProtectedRoute = () => { const { user, isLoading, error } = useMe(); @@ -25,7 +25,7 @@ export const ProtectedRoute = () => { if (!user) { return ( diff --git a/src/components/common/action-menu/action-menu.tsx b/src/components/common/action-menu/action-menu.tsx index 59000072..3fee0a81 100644 --- a/src/components/common/action-menu/action-menu.tsx +++ b/src/components/common/action-menu/action-menu.tsx @@ -1,13 +1,10 @@ -import type { PropsWithChildren, ReactNode } from "react"; +import type { PropsWithChildren, ReactNode } from 'react'; -import { EllipsisHorizontal } from "@medusajs/icons"; -import { DropdownMenu, IconButton, clx } from "@medusajs/ui"; - -import { Link } from "react-router-dom"; - -import { ConditionalTooltip } from "@components/common/conditional-tooltip"; - -import { useDocumentDirection } from "@hooks/use-document-direction"; +import { ConditionalTooltip } from '@components/common/conditional-tooltip'; +import { useDocumentDirection } from '@hooks/use-document-direction'; +import { EllipsisHorizontal } from '@medusajs/icons'; +import { clx, DropdownMenu, IconButton } from '@medusajs/ui'; +import { Link } from 'react-router-dom'; export type Action = { icon: ReactNode; @@ -34,24 +31,31 @@ export type ActionGroup = { type ActionMenuProps = PropsWithChildren<{ groups: ActionGroup[]; - variant?: "transparent" | "primary"; + variant?: 'transparent' | 'primary'; }>; export const ActionMenu = ({ groups, - variant = "transparent", + variant = 'transparent', children, - "data-testid": dataTestId, -}: ActionMenuProps & { "data-testid"?: string }) => { + 'data-testid': dataTestId +}: ActionMenuProps & { 'data-testid'?: string }) => { const direction = useDocumentDirection(); const inner = children ?? ( - + ); return ( - + {inner} {groups.map((group, index) => { @@ -62,7 +66,10 @@ export const ActionMenu = ({ const isLast = index === groups.length - 1; return ( - + {group.actions.map((action, actionIndex) => { const Wrapper = action.disabledTooltip ? ({ children }: { children: ReactNode }) => ( @@ -74,24 +81,25 @@ export const ActionMenu = ({
{children}
) - : "div"; + : 'div'; if (action.onClick) { return ( { + onClick={e => { e.stopPropagation(); action.onClick(); }} - className={clx( - "flex items-center gap-x-2 [&_svg]:text-ui-fg-subtle", - { - "[&_svg]:text-ui-fg-disabled": action.disabled, - }, - )} - data-testid={dataTestId ? `${dataTestId}-action-${actionIndex}-${action.label.toLowerCase().replace(/\s+/g, "-")}` : undefined} + className={clx('flex items-center gap-x-2 [&_svg]:text-ui-fg-subtle', { + '[&_svg]:text-ui-fg-disabled': action.disabled + })} + data-testid={ + dataTestId + ? `${dataTestId}-action-${actionIndex}-${action.label.toLowerCase().replace(/\s+/g, '-')}` + : undefined + } > {action.icon} {action.label} @@ -103,17 +111,21 @@ export const ActionMenu = ({ return ( - e.stopPropagation()}> + e.stopPropagation()} + > {action.icon} {action.label} diff --git a/src/components/common/action-menu/index.ts b/src/components/common/action-menu/index.ts index a3a827d6..19707d5d 100644 --- a/src/components/common/action-menu/index.ts +++ b/src/components/common/action-menu/index.ts @@ -1 +1 @@ -export * from "./action-menu" +export * from './action-menu'; diff --git a/src/components/common/actions-button/actions-button.tsx b/src/components/common/actions-button/actions-button.tsx index 63b90f62..e758adab 100644 --- a/src/components/common/actions-button/actions-button.tsx +++ b/src/components/common/actions-button/actions-button.tsx @@ -1,18 +1,23 @@ -import { useState } from "react"; +import { useState } from 'react'; -import { EllipsisHorizontal } from "@medusajs/icons"; -import { Button, DropdownMenu } from "@medusajs/ui"; +import { EllipsisHorizontal } from '@medusajs/icons'; +import { Button, DropdownMenu } from '@medusajs/ui'; export const ActionsButton = ({ actions, - "data-testid": dataTestId, + 'data-testid': dataTestId }: { actions: { label: string; onClick: () => void; icon?: JSX.Element }[]; - "data-testid"?: string; + 'data-testid'?: string; }) => { const [open, setOpen] = useState(false); + return ( - + - + {actions.map(({ label, onClick, icon }, index) => ( diff --git a/src/components/common/actions-button/index.ts b/src/components/common/actions-button/index.ts index 27c5e1fa..bd8bd999 100644 --- a/src/components/common/actions-button/index.ts +++ b/src/components/common/actions-button/index.ts @@ -1 +1 @@ -export * from "./actions-button"; +export * from './actions-button'; diff --git a/src/components/common/badge-list-summary/badge-list-summary.tsx b/src/components/common/badge-list-summary/badge-list-summary.tsx index af4259d1..d883083c 100644 --- a/src/components/common/badge-list-summary/badge-list-summary.tsx +++ b/src/components/common/badge-list-summary/badge-list-summary.tsx @@ -1,58 +1,62 @@ -import { Badge, Tooltip, clx } from "@medusajs/ui" -import { useTranslation } from "react-i18next" +import { Badge, clx, Tooltip } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; type BadgeListSummaryProps = { /** * Number of initial items to display * @default 2 */ - n?: number + n?: number; /** * List of strings to display as abbreviated list */ - list: string[] + list: string[]; /** * Is the summary displayed inline. * Determines whether the center text is truncated if there is no space in the container */ - inline?: boolean + inline?: boolean; /** * Whether the badges should be rounded */ - rounded?: boolean - className?: string -} + rounded?: boolean; + className?: string; +}; export const BadgeListSummary = ({ list, className, inline, rounded = false, - n = 2, + n = 2 }: BadgeListSummaryProps) => { - const { t } = useTranslation() + const { t } = useTranslation(); - const title = t("general.plusCount", { - count: list.length - n, - }) + const title = t('general.plusCount', { + count: list.length - n + }); return (
- {list.slice(0, n).map((item) => { + {list.slice(0, n).map(item => { return ( - + {item} - ) + ); })} {list.length > n && ( @@ -60,14 +64,14 @@ export const BadgeListSummary = ({ - {list.slice(n).map((c) => ( + {list.slice(n).map(c => (
  • {c}
  • ))} } > @@ -77,5 +81,5 @@ export const BadgeListSummary = ({
    )} - ) -} + ); +}; diff --git a/src/components/common/badge-list-summary/index.ts b/src/components/common/badge-list-summary/index.ts index a156dab6..aef2528a 100644 --- a/src/components/common/badge-list-summary/index.ts +++ b/src/components/common/badge-list-summary/index.ts @@ -1 +1 @@ -export * from "./badge-list-summary" +export * from './badge-list-summary'; diff --git a/src/components/common/chip-group/chip-group.tsx b/src/components/common/chip-group/chip-group.tsx index 48ed2ef3..19313532 100644 --- a/src/components/common/chip-group/chip-group.tsx +++ b/src/components/common/chip-group/chip-group.tsx @@ -1,50 +1,51 @@ -import { XMarkMini } from "@medusajs/icons" -import { Button, clx } from "@medusajs/ui" -import { Children, PropsWithChildren, createContext, useContext } from "react" -import { useTranslation } from "react-i18next" +import { Children, createContext, useContext, type PropsWithChildren } from 'react'; -type ChipGroupVariant = "base" | "component" +import { XMarkMini } from '@medusajs/icons'; +import { Button, clx } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; + +type ChipGroupVariant = 'base' | 'component'; type ChipGroupProps = PropsWithChildren<{ - onClearAll?: () => void - onRemove?: (index: number) => void - variant?: ChipGroupVariant - className?: string -}> + onClearAll?: () => void; + onRemove?: (index: number) => void; + variant?: ChipGroupVariant; + className?: string; +}>; type GroupContextValue = { - onRemove?: (index: number) => void - variant: ChipGroupVariant -} + onRemove?: (index: number) => void; + variant: ChipGroupVariant; +}; -const GroupContext = createContext(null) +const GroupContext = createContext(null); const useGroupContext = () => { - const context = useContext(GroupContext) + const context = useContext(GroupContext); if (!context) { - throw new Error("useGroupContext must be used within a ChipGroup component") + throw new Error('useGroupContext must be used within a ChipGroup component'); } - return context -} + return context; +}; const Group = ({ onClearAll, onRemove, - variant = "component", + variant = 'component', className, - children, + children }: ChipGroupProps) => { - const { t } = useTranslation() + const { t } = useTranslation(); - const showClearAll = !!onClearAll && Children.count(children) > 0 + const showClearAll = !!onClearAll && Children.count(children) > 0; return (
      {children} {showClearAll && ( @@ -56,35 +57,35 @@ const Group = ({ onClick={onClearAll} className="text-ui-fg-muted active:text-ui-fg-subtle" > - {t("actions.clearAll")} + {t('actions.clearAll')} )}
    - ) -} + ); +}; type ChipProps = PropsWithChildren<{ - index: number - className?: string -}> + index: number; + className?: string; +}>; const Chip = ({ index, className, children }: ChipProps) => { - const { onRemove, variant } = useGroupContext() + const { onRemove, variant } = useGroupContext(); return (
  • - + {children} {!!onRemove && ( @@ -92,12 +93,11 @@ const Chip = ({ index, className, children }: ChipProps) => { onClick={() => onRemove(index)} type="button" className={clx( - "text-ui-fg-muted active:text-ui-fg-subtle transition-fg flex items-center justify-center p-1", + 'flex items-center justify-center p-1 text-ui-fg-muted transition-fg active:text-ui-fg-subtle', { - "hover:bg-ui-bg-component-hover active:bg-ui-bg-component-pressed": - variant === "component", - "hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed": - variant === "base", + 'hover:bg-ui-bg-component-hover active:bg-ui-bg-component-pressed': + variant === 'component', + 'hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed': variant === 'base' } )} > @@ -105,7 +105,7 @@ const Chip = ({ index, className, children }: ChipProps) => { )}
  • - ) -} + ); +}; -export const ChipGroup = Object.assign(Group, { Chip }) +export const ChipGroup = Object.assign(Group, { Chip }); diff --git a/src/components/common/chip-group/index.ts b/src/components/common/chip-group/index.ts index c919f6cc..e492c284 100644 --- a/src/components/common/chip-group/index.ts +++ b/src/components/common/chip-group/index.ts @@ -1 +1 @@ -export * from "./chip-group" +export * from './chip-group'; diff --git a/src/components/common/conditional-tooltip/conditional-tooltip.tsx b/src/components/common/conditional-tooltip/conditional-tooltip.tsx index 829da904..61d7a4e2 100644 --- a/src/components/common/conditional-tooltip/conditional-tooltip.tsx +++ b/src/components/common/conditional-tooltip/conditional-tooltip.tsx @@ -1,11 +1,12 @@ -import { Tooltip } from "@medusajs/ui" -import { ComponentPropsWithoutRef, PropsWithChildren } from "react" +import type { ComponentPropsWithoutRef, PropsWithChildren } from 'react'; + +import { Tooltip } from '@medusajs/ui'; type ConditionalTooltipProps = PropsWithChildren< ComponentPropsWithoutRef & { - showTooltip?: boolean + showTooltip?: boolean; } -> +>; export const ConditionalTooltip = ({ children, @@ -13,8 +14,8 @@ export const ConditionalTooltip = ({ ...props }: ConditionalTooltipProps) => { if (showTooltip) { - return {children} + return {children}; } - return children -} + return children; +}; diff --git a/src/components/common/conditional-tooltip/index.ts b/src/components/common/conditional-tooltip/index.ts index a5a9ef37..74eae441 100644 --- a/src/components/common/conditional-tooltip/index.ts +++ b/src/components/common/conditional-tooltip/index.ts @@ -1 +1 @@ -export * from "./conditional-tooltip" +export * from './conditional-tooltip'; diff --git a/src/components/common/customer-info/customer-info.tsx b/src/components/common/customer-info/customer-info.tsx index fbf5db32..36e082bc 100644 --- a/src/components/common/customer-info/customer-info.tsx +++ b/src/components/common/customer-info/customer-info.tsx @@ -2,7 +2,7 @@ import { Avatar, Copy, Text } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" import { HttpTypes } from "@medusajs/types" -import { getFormattedAddress, isSameAddress } from "../../../lib/addresses" +import { getFormattedAddress, isSameAddress } from '@lib/addresses' const ID = ({ data }: { data: HttpTypes.AdminOrder }) => { const { t } = useTranslation() @@ -107,9 +107,9 @@ const Contact = ({ data }: { data: HttpTypes.AdminOrder }) => { } const AddressPrint = ({ - address, - type, -}: { + address, + type, + }: { address: | HttpTypes.AdminOrder["shipping_address"] | HttpTypes.AdminOrder["billing_address"] @@ -193,9 +193,9 @@ export const CustomerInfo = Object.assign( const getOrderCustomer = (obj: HttpTypes.AdminOrder) => { const { first_name: sFirstName, last_name: sLastName } = - obj.shipping_address || {} + obj.shipping_address || {} const { first_name: bFirstName, last_name: bLastName } = - obj.billing_address || {} + obj.billing_address || {} const { first_name: cFirstName, last_name: cLastName } = obj.customer || {} const customerName = [cFirstName, cLastName].filter(Boolean).join(" ") diff --git a/src/components/common/customer-info/index.ts b/src/components/common/customer-info/index.ts index 892c7d76..a1db6a16 100644 --- a/src/components/common/customer-info/index.ts +++ b/src/components/common/customer-info/index.ts @@ -1 +1 @@ -export * from "./customer-info" +export * from './customer-info'; diff --git a/src/components/common/date-range-display/date-range-display.tsx b/src/components/common/date-range-display/date-range-display.tsx index 738c9d9f..9e2ef2f6 100644 --- a/src/components/common/date-range-display/date-range-display.tsx +++ b/src/components/common/date-range-display/date-range-display.tsx @@ -1,73 +1,81 @@ -import { Text, clx } from "@medusajs/ui" -import { useTranslation } from "react-i18next" -import { useDate } from "../../../hooks/use-date" +import { useDate } from '@hooks/use-date.tsx'; +import { clx, Text } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; type DateRangeDisplayProps = { - startsAt?: Date | string | null - endsAt?: Date | string | null - showTime?: boolean -} + startsAt?: Date | string | null; + endsAt?: Date | string | null; + showTime?: boolean; +}; -export const DateRangeDisplay = ({ - startsAt, - endsAt, - showTime = false, -}: DateRangeDisplayProps) => { - const startDate = startsAt ? new Date(startsAt) : null - const endDate = endsAt ? new Date(endsAt) : null +export const DateRangeDisplay = ({ startsAt, endsAt, showTime = false }: DateRangeDisplayProps) => { + const startDate = startsAt ? new Date(startsAt) : null; + const endDate = endsAt ? new Date(endsAt) : null; - const { t } = useTranslation() - const { getFullDate } = useDate() + const { t } = useTranslation(); + const { getFullDate } = useDate(); return (
    -
    +
    - - {t("fields.startDate")} + + {t('fields.startDate')} - + {startDate ? getFullDate({ date: startDate, - includeTime: showTime, + includeTime: showTime }) - : "-"} + : '-'}
    -
    +
    - - {t("fields.endDate")} + + {t('fields.endDate')} - + {endDate ? getFullDate({ date: endDate, - includeTime: showTime, + includeTime: showTime }) - : "-"} + : '-'}
    - ) -} + ); +}; const Bar = ({ date }: { date: Date | null }) => { - const now = new Date() + const now = new Date(); - const isDateInFuture = date && date > now + const isDateInFuture = date && date > now; return (
    - ) -} + ); +}; diff --git a/src/components/common/date-range-display/index.ts b/src/components/common/date-range-display/index.ts index f703c451..ab774d1d 100644 --- a/src/components/common/date-range-display/index.ts +++ b/src/components/common/date-range-display/index.ts @@ -1 +1 @@ -export * from "./date-range-display" +export * from './date-range-display'; diff --git a/src/components/common/display-id/display-id.tsx b/src/components/common/display-id/display-id.tsx index 844dc048..a5238eeb 100644 --- a/src/components/common/display-id/display-id.tsx +++ b/src/components/common/display-id/display-id.tsx @@ -1,30 +1,38 @@ -import { useTranslation } from "react-i18next" -import { useState } from "react" -import copy from "copy-to-clipboard" +import { useState } from 'react'; -import { clx, toast, Tooltip } from "@medusajs/ui" +import { clx, toast, Tooltip } from '@medusajs/ui'; +import copy from 'copy-to-clipboard'; +import { useTranslation } from 'react-i18next'; type DisplayIdProps = { - id: string - className?: string -} + id: string; + className?: string; +}; function DisplayId({ id, className }: DisplayIdProps) { - const { t } = useTranslation() - const [open, setOpen] = useState(false) + const { t } = useTranslation(); + const [open, setOpen] = useState(false); const onClick = () => { - copy(id) - toast.success(t("actions.idCopiedToClipboard")) - } + copy(id); + toast.success(t('actions.idCopiedToClipboard')); + }; return ( - - + + - ) + ); } -export default DisplayId +export default DisplayId; diff --git a/src/components/common/display-id/index.ts b/src/components/common/display-id/index.ts index c423966f..3820b1ae 100644 --- a/src/components/common/display-id/index.ts +++ b/src/components/common/display-id/index.ts @@ -1 +1 @@ -export * from "./display-id" +export * from './display-id'; diff --git a/src/components/common/empty-table-content/empty-table-content.tsx b/src/components/common/empty-table-content/empty-table-content.tsx index 8b93b849..be210533 100644 --- a/src/components/common/empty-table-content/empty-table-content.tsx +++ b/src/components/common/empty-table-content/empty-table-content.tsx @@ -1,10 +1,9 @@ -import React from "react"; +import type { ReactNode } from 'react'; -import { ExclamationCircle, MagnifyingGlass, PlusMini } from "@medusajs/icons"; -import { Button, Text, clx } from "@medusajs/ui"; - -import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; +import { ExclamationCircle, MagnifyingGlass, PlusMini } from '@medusajs/icons'; +import { Button, clx, Text } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; export type NoResultsProps = { title?: string; @@ -16,19 +15,21 @@ export const NoResults = ({ title, message, className }: NoResultsProps) => { const { t } = useTranslation(); return ( -
    +
    - - {title ?? t("general.noResultsTitle")} + + {title ?? t('general.noResultsTitle')} - - {message ?? t("general.noResultsMessage")} + + {message ?? t('general.noResultsMessage')}
    @@ -48,14 +49,14 @@ type NoRecordsProps = { message?: string; className?: string; buttonVariant?: string; - icon?: React.ReactNode; - "data-testid"?: string; + icon?: ReactNode; + 'data-testid'?: string; } & ActionProps; const DefaultButton = ({ action, - "data-testid": dataTestId, -}: ActionProps & { "data-testid"?: string }) => + 'data-testid': dataTestId +}: ActionProps & { 'data-testid'?: string }) => action && ( {action.label} @@ -75,8 +74,8 @@ const DefaultButton = ({ const TransparentIconLeftButton = ({ action, - "data-testid": dataTestId, -}: ActionProps & { "data-testid"?: string }) => + 'data-testid': dataTestId +}: ActionProps & { 'data-testid'?: string }) => action && ( {action.label} @@ -99,17 +96,17 @@ export const NoRecords = ({ message, action, className, - buttonVariant = "default", + buttonVariant = 'default', icon = , - "data-testid": dataTestId, + 'data-testid': dataTestId }: NoRecordsProps) => { const { t } = useTranslation(); return (
    @@ -117,9 +114,7 @@ export const NoRecords = ({ className="flex flex-col items-center gap-y-3" data-testid={dataTestId ? `${dataTestId}-content` : undefined} > -
    - {icon} -
    +
    {icon}
    - {title ?? t("general.noRecordsTitle")} + {title ?? t('general.noRecordsTitle')} - {message ?? t("general.noRecordsMessage")} + {message ?? t('general.noRecordsMessage')}
    - {buttonVariant === "default" && ( - + {buttonVariant === 'default' && ( + )} - {buttonVariant === "transparentIconLeft" && ( - + {buttonVariant === 'transparentIconLeft' && ( + )}
    ); diff --git a/src/components/common/empty-table-content/index.ts b/src/components/common/empty-table-content/index.ts index 405e5ca3..88ebe538 100644 --- a/src/components/common/empty-table-content/index.ts +++ b/src/components/common/empty-table-content/index.ts @@ -1 +1 @@ -export * from "./empty-table-content" +export * from './empty-table-content'; diff --git a/src/components/common/file-preview/file-preview.tsx b/src/components/common/file-preview/file-preview.tsx index e7a2fa83..0f462caf 100644 --- a/src/components/common/file-preview/file-preview.tsx +++ b/src/components/common/file-preview/file-preview.tsx @@ -1,6 +1,6 @@ -import { ArrowDownTray, Spinner } from "@medusajs/icons" -import { IconButton, Text } from "@medusajs/ui" -import { ActionGroup, ActionMenu } from "../action-menu" +import { ActionMenu, type ActionGroup } from '@components/common/action-menu'; +import { ArrowDownTray, Spinner } from '@medusajs/icons'; +import { IconButton, Text } from '@medusajs/ui'; export const FilePreview = ({ filename, @@ -8,54 +8,58 @@ export const FilePreview = ({ loading, activity, actions, - hideThumbnail, + hideThumbnail }: { - filename: string - url?: string - loading?: boolean - activity?: string - actions?: ActionGroup[] - hideThumbnail?: boolean -}) => { - return ( -
    -
    -
    - {!hideThumbnail && } -
    + filename: string; + url?: string; + loading?: boolean; + activity?: string; + actions?: ActionGroup[]; + hideThumbnail?: boolean; +}) => ( +
    +
    +
    + {!hideThumbnail && } +
    + + {filename} + + + {loading && !!activity && ( - {filename} + {activity} - - {loading && !!activity && ( - - {activity} - - )} -
    + )}
    - - {loading && } - {!loading && actions && } - {!loading && url && ( - - - - - - )}
    + + {loading && } + {!loading && actions && } + {!loading && url && ( + + + + + + )}
    - ) -} +
    +); const FileThumbnail = () => { return ( @@ -70,14 +74,14 @@ const FileThumbnail = () => { d="M20 31.75H4C1.92893 31.75 0.25 30.0711 0.25 28V4C0.25 1.92893 1.92893 0.25 4 0.25H15.9431C16.9377 0.25 17.8915 0.645088 18.5948 1.34835L22.6516 5.4052C23.3549 6.10847 23.75 7.06229 23.75 8.05685V28C23.75 30.0711 22.0711 31.75 20 31.75Z" fill="url(#paint0_linear_6594_388107)" stroke="url(#paint1_linear_6594_388107)" - stroke-width="0.5" + strokeWidth="0.5" /> { y2="32" gradientUnits="userSpaceOnUse" > - - + + { y2="32" gradientUnits="userSpaceOnUse" > - - + + { y2="21.25" gradientUnits="userSpaceOnUse" > - - + + { y2="21.25" gradientUnits="userSpaceOnUse" > - - + + - ) -} + ); +}; diff --git a/src/components/common/file-preview/index.ts b/src/components/common/file-preview/index.ts index 1a365200..cbac03dc 100644 --- a/src/components/common/file-preview/index.ts +++ b/src/components/common/file-preview/index.ts @@ -1 +1 @@ -export * from "./file-preview" +export * from "./file-preview"; diff --git a/src/components/common/file-upload/file-upload.tsx b/src/components/common/file-upload/file-upload.tsx index f05c45ac..213c6e9d 100644 --- a/src/components/common/file-upload/file-upload.tsx +++ b/src/components/common/file-upload/file-upload.tsx @@ -1,20 +1,22 @@ -import { ArrowDownTray } from "@medusajs/icons" -import { Text, clx } from "@medusajs/ui" -import { ChangeEvent, DragEvent, useRef, useState } from "react" +import type { ChangeEvent, DragEvent } from "react"; +import { useRef, useState } from "react"; + +import { ArrowDownTray } from "@medusajs/icons"; +import { Text, clx } from "@medusajs/ui"; export interface FileType { - id: string - url: string - file: File + id: string; + url: string; + file: File; } export interface FileUploadProps { - label: string - multiple?: boolean - hint?: string - hasError?: boolean - formats: string[] - onUploaded: (files: FileType[]) => void + label: string; + multiple?: boolean; + hint?: string; + hasError?: boolean; + formats: string[]; + onUploaded: (files: FileType[]) => void; } export const FileUpload = ({ @@ -25,72 +27,73 @@ export const FileUpload = ({ formats, onUploaded, }: FileUploadProps) => { - const [isDragOver, setIsDragOver] = useState(false) - const inputRef = useRef(null) - const dropZoneRef = useRef(null) + const [isDragOver, setIsDragOver] = useState(false); + const inputRef = useRef(null); + const dropZoneRef = useRef(null); const handleOpenFileSelector = () => { - inputRef.current?.click() - } + inputRef.current?.click(); + }; const handleDragEnter = (event: DragEvent) => { - event.preventDefault() - event.stopPropagation() + event.preventDefault(); + event.stopPropagation(); - const files = event.dataTransfer?.files + const files = event.dataTransfer?.files; if (!files) { - return + return; } - setIsDragOver(true) - } + setIsDragOver(true); + }; const handleDragLeave = (event: DragEvent) => { - event.preventDefault() - event.stopPropagation() + event.preventDefault(); + event.stopPropagation(); if ( !dropZoneRef.current || dropZoneRef.current.contains(event.relatedTarget as Node) ) { - return + return; } - setIsDragOver(false) - } + setIsDragOver(false); + }; const handleUploaded = (files: FileList | null) => { if (!files) { - return + return; } - const fileList = Array.from(files) + const fileList = Array.from(files); const fileObj = fileList.map((file) => { - const id = Math.random().toString(36).substring(7) + const id = Math.random().toString(36).substring(7); + + const previewUrl = URL.createObjectURL(file); - const previewUrl = URL.createObjectURL(file) return { id: id, url: previewUrl, file, - } - }) + }; + }); - onUploaded(fileObj) - } + onUploaded(fileObj); + }; const handleDrop = (event: DragEvent) => { - event.preventDefault() - event.stopPropagation() + event.preventDefault(); + event.stopPropagation(); - setIsDragOver(false) + setIsDragOver(false); - handleUploaded(event.dataTransfer?.files) - } + handleUploaded(event.dataTransfer?.files); + }; const handleFileChange = async (event: ChangeEvent) => { - handleUploaded(event.target.files) - } + handleUploaded(event.target.files); + }; return (
    @@ -103,16 +106,16 @@ export const FileUpload = ({ onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} className={clx( - "bg-ui-bg-component border-ui-border-strong transition-fg group flex w-full flex-col items-center gap-y-2 rounded-lg border border-dashed p-8", + "group flex w-full flex-col items-center gap-y-2 rounded-lg border border-dashed border-ui-border-strong bg-ui-bg-component p-8 transition-fg", "hover:border-ui-border-interactive focus:border-ui-border-interactive", - "focus:shadow-borders-focus outline-none focus:border-solid", + "outline-none focus:border-solid focus:shadow-borders-focus", { "!border-ui-border-error": hasError, "!border-ui-border-interactive": isDragOver, - } + }, )} > -
    +
    {label}
    @@ -135,5 +138,5 @@ export const FileUpload = ({ multiple={multiple} />
    - ) -} + ); +}; diff --git a/src/components/common/file-upload/index.ts b/src/components/common/file-upload/index.ts index 622ec7e6..b64999d1 100644 --- a/src/components/common/file-upload/index.ts +++ b/src/components/common/file-upload/index.ts @@ -1 +1 @@ -export * from "./file-upload" +export * from "./file-upload"; diff --git a/src/components/common/form/form.tsx b/src/components/common/form/form.tsx index 2e7fbf05..419c8983 100644 --- a/src/components/common/form/form.tsx +++ b/src/components/common/form/form.tsx @@ -1,46 +1,43 @@ -import { InformationCircleSolid } from "@medusajs/icons" +import type { ReactNode } from "react"; +import type React from "react"; +import { createContext, forwardRef, useContext, useId } from "react"; + +import { InformationCircleSolid } from "@medusajs/icons"; import { Hint as HintComponent, Label as LabelComponent, Text, Tooltip, clx, -} from "@medusajs/ui" -import { Label as RadixLabel, Slot } from "radix-ui" -import React, { - ReactNode, - createContext, - forwardRef, - useContext, - useId, -} from "react" +} from "@medusajs/ui"; + +import type { Label as RadixLabel } from "radix-ui"; +import { Slot } from "radix-ui"; +import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form"; import { Controller, - ControllerProps, - FieldPath, - FieldValues, FormProvider, useFormContext, useFormState, -} from "react-hook-form" -import { useTranslation } from "react-i18next" +} from "react-hook-form"; +import { useTranslation } from "react-i18next"; -const Provider = FormProvider +const Provider = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, > = { - name: TName -} + name: TName; +}; const FormFieldContext = createContext( - {} as FormFieldContextValue -) + {} as FormFieldContextValue, +); const Field = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, >({ ...props }: ControllerProps) => { @@ -48,30 +45,30 @@ const Field = < - ) -} + ); +}; type FormItemContextValue = { - id: string -} + id: string; +}; const FormItemContext = createContext( - {} as FormItemContextValue -) + {} as FormItemContextValue, +); const useFormField = () => { - const fieldContext = useContext(FormFieldContext) - const itemContext = useContext(FormItemContext) - const { getFieldState } = useFormContext() + const fieldContext = useContext(FormFieldContext); + const itemContext = useContext(FormItemContext); + const { getFieldState } = useFormContext(); - const formState = useFormState({ name: fieldContext.name }) - const fieldState = getFieldState(fieldContext.name, formState) + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { - throw new Error("useFormField should be used within a FormField") + throw new Error("useFormField should be used within a FormField"); } - const { id } = itemContext + const { id } = itemContext; return { id, @@ -81,12 +78,12 @@ const useFormField = () => { formDescriptionId: `${id}-form-item-description`, formErrorMessageId: `${id}-form-item-message`, ...fieldState, - } -} + }; +}; const Item = forwardRef>( ({ className, ...props }, ref) => { - const id = useId() + const id = useId(); return ( @@ -96,21 +93,21 @@ const Item = forwardRef>( {...props} /> - ) - } -) -Item.displayName = "Form.Item" + ); + }, +); +Item.displayName = "Form.Item"; const Label = forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - optional?: boolean - tooltip?: ReactNode - icon?: ReactNode + optional?: boolean; + tooltip?: ReactNode; + icon?: ReactNode; } >(({ className, optional = false, tooltip, icon, ...props }, ref) => { - const { formLabelId, formItemId } = useFormField() - const { t } = useTranslation() + const { formLabelId, formItemId } = useFormField(); + const { t } = useTranslation(); return (
    @@ -135,9 +132,9 @@ const Label = forwardRef< )}
    - ) -}) -Label.displayName = "Form.Label" + ); +}); +Label.displayName = "Form.Label"; const Control = forwardRef< React.ElementRef, @@ -149,7 +146,7 @@ const Control = forwardRef< formDescriptionId, formErrorMessageId, formLabelId, - } = useFormField() + } = useFormField(); return ( - ) -}) -Control.displayName = "Form.Control" + ); +}); +Control.displayName = "Form.Control"; const Hint = forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => { - const { formDescriptionId } = useFormField() + const { formDescriptionId } = useFormField(); return ( - ) -}) -Hint.displayName = "Form.Hint" + ); +}); +Hint.displayName = "Form.Hint"; const ErrorMessage = forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, children, ...props }, ref) => { - const { error, formErrorMessageId } = useFormField() - const msg = error ? String(error?.message) : children + const { error, formErrorMessageId } = useFormField(); + const msg = error ? String(error?.message) : children; if (!msg || msg === "undefined") { - return null + return null; } return ( @@ -206,9 +203,9 @@ const ErrorMessage = forwardRef< > {msg} - ) -}) -ErrorMessage.displayName = "Form.ErrorMessage" + ); +}); +ErrorMessage.displayName = "Form.ErrorMessage"; const Form = Object.assign(Provider, { Item, @@ -217,6 +214,6 @@ const Form = Object.assign(Provider, { Hint, ErrorMessage, Field, -}) +}); -export { Form } +export { Form }; diff --git a/src/components/common/icon-avatar/icon-avatar.tsx b/src/components/common/icon-avatar/icon-avatar.tsx index 65b3e3e2..f00895b0 100644 --- a/src/components/common/icon-avatar/icon-avatar.tsx +++ b/src/components/common/icon-avatar/icon-avatar.tsx @@ -1,11 +1,12 @@ -import { clx } from "@medusajs/ui" -import { PropsWithChildren } from "react" +import type { PropsWithChildren } from 'react'; + +import { clx } from '@medusajs/ui'; type IconAvatarProps = PropsWithChildren<{ - className?: string - size?: "small" | "large" | "xlarge" - "data-testid"?: string -}> + className?: string; + size?: 'small' | 'large' | 'xlarge'; + 'data-testid'?: string; +}>; /** * Use this component when a design calls for an avatar with an icon. @@ -13,23 +14,20 @@ type IconAvatarProps = PropsWithChildren<{ * The `` component from `@medusajs/ui` does not support passing an icon as a child. */ export const IconAvatar = ({ - size = "small", + size = 'small', children, className, - "data-testid": dataTestId, + 'data-testid': dataTestId }: IconAvatarProps) => { return (
    div]:bg-ui-bg-field [&>div]:text-ui-fg-subtle [&>div]:flex [&>div]:size-6 [&>div]:items-center [&>div]:justify-center", + 'flex size-7 items-center justify-center shadow-borders-base', + '[&>div]:flex [&>div]:size-6 [&>div]:items-center [&>div]:justify-center [&>div]:bg-ui-bg-field [&>div]:text-ui-fg-subtle', { - "size-7 rounded-md [&>div]:size-6 [&>div]:rounded-[4px]": - size === "small", - "size-10 rounded-lg [&>div]:size-9 [&>div]:rounded-[6px]": - size === "large", - "size-12 rounded-xl [&>div]:size-11 [&>div]:rounded-[10px]": - size === "xlarge", + 'size-7 rounded-md [&>div]:size-6 [&>div]:rounded-[4px]': size === 'small', + 'size-10 rounded-lg [&>div]:size-9 [&>div]:rounded-[6px]': size === 'large', + 'size-12 rounded-xl [&>div]:size-11 [&>div]:rounded-[10px]': size === 'xlarge' }, className )} @@ -37,5 +35,5 @@ export const IconAvatar = ({ >
    {children}
    - ) -} + ); +}; diff --git a/src/components/common/icon-avatar/index.ts b/src/components/common/icon-avatar/index.ts index ee6f4861..ab1c1be2 100644 --- a/src/components/common/icon-avatar/index.ts +++ b/src/components/common/icon-avatar/index.ts @@ -1 +1 @@ -export * from "./icon-avatar" +export * from "./icon-avatar"; diff --git a/src/components/common/infinite-list/index.ts b/src/components/common/infinite-list/index.ts index c60bfb6b..06265b70 100644 --- a/src/components/common/infinite-list/index.ts +++ b/src/components/common/infinite-list/index.ts @@ -1 +1 @@ -export * from "./infinite-list" +export * from "./infinite-list"; diff --git a/src/components/common/infinite-list/infinite-list.tsx b/src/components/common/infinite-list/infinite-list.tsx index c2a7cf07..da9351e3 100644 --- a/src/components/common/infinite-list/infinite-list.tsx +++ b/src/components/common/infinite-list/infinite-list.tsx @@ -1,17 +1,21 @@ -import { QueryKey, useInfiniteQuery } from "@tanstack/react-query" -import { ReactNode, useEffect, useMemo, useRef } from "react" -import { toast } from "@medusajs/ui" -import { Spinner } from "@medusajs/icons" +import type { ReactNode } from "react"; +import { useEffect, useMemo, useRef } from "react"; + +import { Spinner } from "@medusajs/icons"; +import { toast } from "@medusajs/ui"; + +import type { QueryKey } from "@tanstack/react-query"; +import { useInfiniteQuery } from "@tanstack/react-query"; type InfiniteListProps = { - queryKey: QueryKey - queryFn: (params: TParams) => Promise - queryOptions?: { enabled?: boolean } - renderItem: (item: TEntity) => ReactNode - renderEmpty: () => ReactNode - responseKey: keyof TResponse - pageSize?: number -} + queryKey: QueryKey; + queryFn: (params: TParams) => Promise; + queryOptions?: { enabled?: boolean }; + renderItem: (item: TEntity) => ReactNode; + renderEmpty: () => ReactNode; + responseKey: keyof TResponse; + pageSize?: number; +}; export const InfiniteList = < TResponse extends { count: number; offset: number; limit: number }, @@ -41,34 +45,36 @@ export const InfiniteList = < return await queryFn({ limit: pageSize, offset: pageParam, - } as TParams) + } as TParams); }, initialPageParam: 0, maxPages: 5, getNextPageParam: (lastPage) => { - const moreItemsExist = lastPage.count > lastPage.offset + lastPage.limit - return moreItemsExist ? lastPage.offset + lastPage.limit : undefined + const moreItemsExist = lastPage.count > lastPage.offset + lastPage.limit; + + return moreItemsExist ? lastPage.offset + lastPage.limit : undefined; }, getPreviousPageParam: (firstPage) => { - const moreItemsExist = firstPage.offset !== 0 + const moreItemsExist = firstPage.offset !== 0; + return moreItemsExist ? Math.max(firstPage.offset - firstPage.limit, 0) - : undefined + : undefined; }, ...queryOptions, - }) + }); const items = useMemo(() => { - return data?.pages.flatMap((p) => p[responseKey] as TEntity[]) ?? [] - }, [data, responseKey]) + return data?.pages.flatMap((p) => p[responseKey] as TEntity[]) ?? []; + }, [data, responseKey]); - const parentRef = useRef(null) - const startObserver = useRef() - const endObserver = useRef() + const parentRef = useRef(null); + const startObserver = useRef(); + const endObserver = useRef(); useEffect(() => { if (isPending) { - return + return; } // Define the new observers after we stop fetching @@ -77,41 +83,41 @@ export const InfiniteList = < startObserver.current = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasPreviousPage) { - startObserver.current?.disconnect() - fetchPreviousPage() + startObserver.current?.disconnect(); + fetchPreviousPage(); } }, { threshold: 0.5, - } - ) + }, + ); endObserver.current = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage) { - endObserver.current?.disconnect() - fetchNextPage() + endObserver.current?.disconnect(); + fetchNextPage(); } }, { threshold: 0.5, - } - ) + }, + ); // Register the new observers to observe the new first and last children if (parentRef.current?.firstChild) { - startObserver.current?.observe(parentRef.current.firstChild as Element) + startObserver.current?.observe(parentRef.current.firstChild as Element); } if (parentRef.current?.lastChild) { - endObserver.current?.observe(parentRef.current.lastChild as Element) + endObserver.current?.observe(parentRef.current.lastChild as Element); } } // Clear the old observers return () => { - startObserver.current?.disconnect() - endObserver.current?.disconnect() - } + startObserver.current?.disconnect(); + endObserver.current?.disconnect(); + }; }, [ fetchNextPage, fetchPreviousPage, @@ -119,20 +125,20 @@ export const InfiniteList = < hasPreviousPage, isFetching, isPending, - ]) + ]); useEffect(() => { if (error) { - toast.error(error.message) + toast.error(error.message); } - }, [error]) + }, [error]); if (isPending) { return (
    - ) + ); } return ( @@ -147,5 +153,5 @@ export const InfiniteList = <
    )}
    - ) -} + ); +}; diff --git a/src/components/common/json-view-section/index.ts b/src/components/common/json-view-section/index.ts index be52949a..51f597db 100644 --- a/src/components/common/json-view-section/index.ts +++ b/src/components/common/json-view-section/index.ts @@ -1 +1 @@ -export * from "./json-view-section" +export * from "./json-view-section"; diff --git a/src/components/common/json-view-section/json-view-section.tsx b/src/components/common/json-view-section/json-view-section.tsx index 65ee10a6..32f638d1 100644 --- a/src/components/common/json-view-section/json-view-section.tsx +++ b/src/components/common/json-view-section/json-view-section.tsx @@ -1,38 +1,47 @@ +import { Suspense, useState, type CSSProperties, type MouseEvent } from 'react'; + import { ArrowUpRightOnBox, Check, SquareTwoStack, TriangleDownMini, - XMarkMini, -} from "@medusajs/icons" -import { - Badge, - Container, - Drawer, - Heading, - IconButton, - Kbd, -} from "@medusajs/ui" -import Primitive from "@uiw/react-json-view" -import { CSSProperties, MouseEvent, Suspense, useState } from "react" -import { Trans, useTranslation } from "react-i18next" + XMarkMini +} from '@medusajs/icons'; +import { Badge, Container, Drawer, Heading, IconButton, Kbd } from '@medusajs/ui'; +import Primitive from '@uiw/react-json-view'; +import { Trans, useTranslation } from 'react-i18next'; type JsonViewSectionProps = { - data: object - title?: string -} + data: object; + title?: string; +}; export const JsonViewSection = ({ data }: JsonViewSectionProps) => { - const { t } = useTranslation() - const numberOfKeys = Object.keys(data).length + const { t } = useTranslation(); + const numberOfKeys = Object.keys(data).length; return ( - -
    - {t("json.header")} - - {t("json.numberOfKeys", { - count: numberOfKeys, + +
    + + {t('json.header')} + + + {t('json.numberOfKeys', { + count: numberOfKeys })}
    @@ -49,35 +58,53 @@ export const JsonViewSection = ({ data }: JsonViewSectionProps) => { -
    +
    - + , + ]} /> - - {t("json.drawer.description")} + + {t('json.drawer.description')}
    -
    - +
    + esc @@ -85,71 +112,74 @@ export const JsonViewSection = ({ data }: JsonViewSectionProps) => {
    - -
    -
    } - > + +
    +
    }> } /> ( - null - )} + render={() => null} /> ( - undefined - )} + render={() => undefined} /> { return ( - - {t("general.items", { - count: Object.keys(value as object).length, + + {t('general.items', { + count: Object.keys(value as object).length })} - ) + ); }} /> - + : { - return + return ( + + ); }} /> @@ -159,46 +189,49 @@ export const JsonViewSection = ({ data }: JsonViewSectionProps) => { - ) -} + ); +}; type CopiedProps = { - style?: CSSProperties - value: object | undefined -} + style?: CSSProperties; + value: object | undefined; +}; const Copied = ({ style, value }: CopiedProps) => { - const [copied, setCopied] = useState(false) + const [copied, setCopied] = useState(false); const handler = (e: MouseEvent) => { - e.stopPropagation() - setCopied(true) + e.stopPropagation(); + setCopied(true); - if (typeof value === "string") { - navigator.clipboard.writeText(value) + if (typeof value === 'string') { + navigator.clipboard.writeText(value); } else { - const json = JSON.stringify(value, null, 2) - navigator.clipboard.writeText(json) + const json = JSON.stringify(value, null, 2); + navigator.clipboard.writeText(json); } setTimeout(() => { - setCopied(false) - }, 2000) - } + setCopied(false); + }, 2000); + }; - const styl = { whiteSpace: "nowrap", width: "20px" } + const styl = { whiteSpace: 'nowrap', width: '20px' }; if (copied) { return ( - ) + ); } return ( - + - ) -} + ); +}; diff --git a/src/components/common/link-button/index.ts b/src/components/common/link-button/index.ts index b71efb56..bd302126 100644 --- a/src/components/common/link-button/index.ts +++ b/src/components/common/link-button/index.ts @@ -1 +1 @@ -export * from "./link-button" +export * from "./link-button"; diff --git a/src/components/common/link-button/link-button.tsx b/src/components/common/link-button/link-button.tsx index cd5ed5bf..875d93b3 100644 --- a/src/components/common/link-button/link-button.tsx +++ b/src/components/common/link-button/link-button.tsx @@ -1,29 +1,29 @@ -import { clx } from "@medusajs/ui" -import { ComponentPropsWithoutRef } from "react" -import { Link } from "react-router-dom" +import type { ComponentPropsWithoutRef } from "react"; + +import { clx } from "@medusajs/ui"; + +import { Link } from "react-router-dom"; interface LinkButtonProps extends ComponentPropsWithoutRef { - variant?: "primary" | "interactive" + variant?: "primary" | "interactive"; } export const LinkButton = ({ className, variant = "interactive", ...props -}: LinkButtonProps) => { - return ( - - ) -} +}: LinkButtonProps) => ( + +); diff --git a/src/components/common/list-summary/index.ts b/src/components/common/list-summary/index.ts index 0fbd1961..a4fd7f5f 100644 --- a/src/components/common/list-summary/index.ts +++ b/src/components/common/list-summary/index.ts @@ -1 +1 @@ -export { ListSummary } from "./list-summary" +export { ListSummary } from './list-summary'; diff --git a/src/components/common/list-summary/list-summary.tsx b/src/components/common/list-summary/list-summary.tsx index 27cd5f66..81e31698 100644 --- a/src/components/common/list-summary/list-summary.tsx +++ b/src/components/common/list-summary/list-summary.tsx @@ -1,61 +1,61 @@ -import { Tooltip, clx } from "@medusajs/ui" -import { useTranslation } from "react-i18next" +import { clx, Tooltip } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; type ListSummaryProps = { /** * Number of initial items to display * @default 2 */ - n?: number + n?: number; /** * List of strings to display as abbreviated list */ - list: string[] + list: string[]; /** * Is the summary displayed inline. * Determines whether the center text is truncated if there is no space in the container */ - inline?: boolean - variant?: "base" | "compact" + inline?: boolean; + variant?: 'base' | 'compact'; - className?: string -} + className?: string; +}; export const ListSummary = ({ list, className, - variant = "compact", + variant = 'compact', inline, - n = 2, + n = 2 }: ListSummaryProps) => { - const { t } = useTranslation() + const { t } = useTranslation(); - const title = t("general.plusCountMore", { - count: list.length - n, - }) + const title = t('general.plusCountMore', { + count: list.length - n + }); return (
    - {list.slice(0, n).join(", ")} + {list.slice(0, n).join(', ')}
    {list.length > n && (
    - {list.slice(n).map((c) => ( + {list.slice(n).map(c => (
  • {c}
  • ))} @@ -66,5 +66,5 @@ export const ListSummary = ({
    )}
    - ) -} + ); +}; diff --git a/src/components/common/listicle/index.ts b/src/components/common/listicle/index.ts index 49b9a3a3..65a929c7 100644 --- a/src/components/common/listicle/index.ts +++ b/src/components/common/listicle/index.ts @@ -1 +1 @@ -export * from "./listicle" +export * from "./listicle"; diff --git a/src/components/common/listicle/listicle.tsx b/src/components/common/listicle/listicle.tsx index 05b7836d..adb804b6 100644 --- a/src/components/common/listicle/listicle.tsx +++ b/src/components/common/listicle/listicle.tsx @@ -1,34 +1,35 @@ -import { Text } from "@medusajs/ui" -import { ReactNode } from "react" +import type { ReactNode } from 'react'; + +import { Text } from '@medusajs/ui'; export interface ListicleProps { - labelKey: string - descriptionKey: string - children?: ReactNode + labelKey: string; + descriptionKey: string; + children?: ReactNode; } -export const Listicle = ({ - labelKey, - descriptionKey, - children, -}: ListicleProps) => { - return ( -
    -
    -
    -
    - - {labelKey} - - - {descriptionKey} - -
    -
    - {children} -
    +export const Listicle = ({ labelKey, descriptionKey, children }: ListicleProps) => ( +
    +
    +
    +
    + + {labelKey} + + + {descriptionKey} +
    +
    {children}
    - ) -} +
    +); diff --git a/src/components/common/loading-spinner/loading-spinner.tsx b/src/components/common/loading-spinner/loading-spinner.tsx index cbc20f06..14271ec6 100644 --- a/src/components/common/loading-spinner/loading-spinner.tsx +++ b/src/components/common/loading-spinner/loading-spinner.tsx @@ -1,9 +1,7 @@ -import { Spinner } from "@medusajs/icons" +import { Spinner } from "@medusajs/icons"; -export const LoadingSpinner = () => { -return ( -
    - -
    - ) -} +export const LoadingSpinner = () => ( +
    + +
    +); diff --git a/src/components/common/logo-box/avatar-box.tsx b/src/components/common/logo-box/avatar-box.tsx index 725f099f..7e30d3e5 100644 --- a/src/components/common/logo-box/avatar-box.tsx +++ b/src/components/common/logo-box/avatar-box.tsx @@ -1,82 +1,86 @@ -import { motion } from "motion/react" +import { IconAvatar } from '@components/common/icon-avatar'; +import { motion } from 'motion/react'; -import { IconAvatar } from "@components/common/icon-avatar" - -export default function AvatarBox({ - checked, - size = 44, -}: { - checked?: boolean - size?: number -}) { - return ( - - {checked && ( - ( + + {checked && ( + + - - - - - )} - + + + )} + + - - - - + + + + + + + + - - - - - - - - - - ) -} + + + + +); + +export default AvatarBox; diff --git a/src/components/common/logo-box/index.ts b/src/components/common/logo-box/index.ts index 96e7eccd..d0b48aed 100644 --- a/src/components/common/logo-box/index.ts +++ b/src/components/common/logo-box/index.ts @@ -1,2 +1,2 @@ -export * from "./logo-box" -export * from "./avatar-box" +export * from './logo-box'; +export * from './avatar-box'; diff --git a/src/components/common/logo-box/logo-box.tsx b/src/components/common/logo-box/logo-box.tsx index 9df3cc54..9b52ad75 100644 --- a/src/components/common/logo-box/logo-box.tsx +++ b/src/components/common/logo-box/logo-box.tsx @@ -1,12 +1,12 @@ -import { clx } from "@medusajs/ui" -import { Transition, motion } from "motion/react" +import { clx } from '@medusajs/ui'; +import { motion, type Transition } from 'motion/react'; type LogoBoxProps = { - className?: string - checked?: boolean - containerTransition?: Transition - pathTransition?: Transition -} + className?: string; + checked?: boolean; + containerTransition?: Transition; + pathTransition?: Transition; +}; export const LogoBox = ({ className, @@ -14,61 +14,59 @@ export const LogoBox = ({ containerTransition = { duration: 0.8, delay: 0.5, - ease: [0, 0.71, 0.2, 1.01], + ease: [0, 0.71, 0.2, 1.01] }, pathTransition = { duration: 0.8, delay: 0.6, - ease: [0.1, 0.8, 0.2, 1.01], - }, -}: LogoBoxProps) => { - return ( -
    - {checked && ( - - - - - - )} - ( +
    + {checked && ( + - - -
    - ) -} + + + + + )} + + + +
    +); diff --git a/src/components/common/metadata-editor/index.tsx b/src/components/common/metadata-editor/index.tsx index 0a7101db..60355985 100644 --- a/src/components/common/metadata-editor/index.tsx +++ b/src/components/common/metadata-editor/index.tsx @@ -1,61 +1,80 @@ -import { Button, Input, Text, Heading, DropdownMenu } from "@medusajs/ui" -import { useFieldArray, UseFormReturn, FieldValues, Path, FieldError, FieldErrors, ArrayPath, FieldArray } from "react-hook-form" -import { EllipsisHorizontal, Trash } from "@medusajs/icons" +import { EllipsisHorizontal, Trash } from '@medusajs/icons'; +import { Button, DropdownMenu, Heading, Input, Text } from '@medusajs/ui'; +import { + useFieldArray, + type ArrayPath, + type FieldArray, + type FieldError, + type FieldErrors, + type FieldValues, + type Path, + type UseFormReturn +} from 'react-hook-form'; interface MetadataField { - key: string - value: string + key: string; + value: string; } interface MetadataEditorProps { - form: UseFormReturn - name?: ArrayPath - title?: string + form: UseFormReturn; + name?: ArrayPath; + title?: string; } -export const MetadataEditor = ({ - form, - name = "metadata" as ArrayPath, - title = "Metadata" +export const MetadataEditor = ({ + form, + name = 'metadata' as ArrayPath, + title = 'Metadata' }: MetadataEditorProps) => { const { fields, append, remove } = useFieldArray({ control: form.control, - name, - }) + name + }); const getErrorMessage = (error: FieldError | undefined) => { - return error?.message ? String(error.message) : "" - } + return error?.message ? String(error.message) : ''; + }; const getFieldErrors = (index: number) => { - const errors = form.formState.errors[name] as FieldErrors | undefined + const errors = form.formState.errors[name] as FieldErrors | undefined; + return { key: errors?.[index]?.key, value: errors?.[index]?.value - } - } + }; + }; return (
    - {title} -
    -
    + + {title} + +
    +
    Key Value
    {fields.map((field, index) => { - const fieldErrors = getFieldErrors(index) + const fieldErrors = getFieldErrors(index); + return ( -
    -
    +
    +
    )} /> {fieldErrors.key && ( - + {getErrorMessage(fieldErrors.key)} )} @@ -63,11 +82,11 @@ export const MetadataEditor = )} /> {fieldErrors.value && ( - + {getErrorMessage(fieldErrors.value)} )} @@ -75,12 +94,18 @@ export const MetadataEditor = - - remove(index)} className="gap-x-2"> + remove(index)} + className="gap-x-2" + > Remove @@ -88,14 +113,20 @@ export const MetadataEditor =
    - ) + ); })}
    -
    - ) -} \ No newline at end of file + ); +}; diff --git a/src/components/common/metadata-section/index.ts b/src/components/common/metadata-section/index.ts index 2281a512..527d968b 100644 --- a/src/components/common/metadata-section/index.ts +++ b/src/components/common/metadata-section/index.ts @@ -1 +1 @@ -export * from "./metadata-section" +export * from "./metadata-section"; diff --git a/src/components/common/metadata-section/metadata-section.tsx b/src/components/common/metadata-section/metadata-section.tsx index 181beac2..1bfa33c6 100644 --- a/src/components/common/metadata-section/metadata-section.tsx +++ b/src/components/common/metadata-section/metadata-section.tsx @@ -1,36 +1,51 @@ -import { ArrowUpRightOnBox } from "@medusajs/icons" -import { Badge, Container, Heading, IconButton } from "@medusajs/ui" -import { useTranslation } from "react-i18next" -import { Link } from "react-router-dom" +import { ArrowUpRightOnBox } from '@medusajs/icons'; +import { Badge, Container, Heading, IconButton } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; type MetadataSectionProps = { - data: TData - href?: string -} + data: TData; + href?: string; +}; export const MetadataSection = ({ data, - href = "metadata/edit", + href = 'metadata/edit' }: MetadataSectionProps) => { - const { t } = useTranslation() + const { t } = useTranslation(); if (!data) { - return null + return null; } - if (!("metadata" in data)) { - return null + if (!('metadata' in data)) { + return null; } - const numberOfKeys = data.metadata ? Object.keys(data.metadata).length : 0 + const numberOfKeys = data.metadata ? Object.keys(data.metadata).length : 0; return ( - -
    - {t("metadata.header")} - - {t("metadata.numberOfKeys", { - count: numberOfKeys, + +
    + + {t('metadata.header')} + + + {t('metadata.numberOfKeys', { + count: numberOfKeys })}
    @@ -41,10 +56,13 @@ export const MetadataSection = ({ asChild data-testid="metadata-edit-button" > - +
    - ) -} + ); +}; diff --git a/src/components/common/order-status-badge/index.ts b/src/components/common/order-status-badge/index.ts index 20614e02..148fbbf6 100644 --- a/src/components/common/order-status-badge/index.ts +++ b/src/components/common/order-status-badge/index.ts @@ -1 +1 @@ -export * from "./order-status-badge"; +export * from './order-status-badge'; diff --git a/src/components/common/order-status-badge/order-status-badge.tsx b/src/components/common/order-status-badge/order-status-badge.tsx index a1f1153a..70ee1c59 100644 --- a/src/components/common/order-status-badge/order-status-badge.tsx +++ b/src/components/common/order-status-badge/order-status-badge.tsx @@ -1,13 +1,13 @@ -import { StatusCell } from "@components/table/table-cells/common/status-cell"; +import { StatusCell } from '@components/table/table-cells/common/status-cell'; export const OrderStatusBadge = ({ status }: { status: string }) => { const formattedStatus = status.charAt(0).toUpperCase() + status.slice(1); switch (formattedStatus) { - case "Pending": + case 'Pending': return {formattedStatus}; - case "Completed": + case 'Completed': return {formattedStatus}; - case "Canceled": + case 'Canceled': return {formattedStatus}; default: return {formattedStatus}; diff --git a/src/components/common/payments-status-badge/index.ts b/src/components/common/payments-status-badge/index.ts index 911e888c..7370041e 100644 --- a/src/components/common/payments-status-badge/index.ts +++ b/src/components/common/payments-status-badge/index.ts @@ -1 +1 @@ -export * from "./payment-status-badge"; +export * from './payment-status-badge'; diff --git a/src/components/common/payments-status-badge/payment-status-badge.tsx b/src/components/common/payments-status-badge/payment-status-badge.tsx index 0d4daebe..1c9ae7e0 100644 --- a/src/components/common/payments-status-badge/payment-status-badge.tsx +++ b/src/components/common/payments-status-badge/payment-status-badge.tsx @@ -1,15 +1,15 @@ -import { StatusCell } from "@components/table/table-cells/common/status-cell"; +import { StatusCell } from '@components/table/table-cells/common/status-cell'; export const PaymentStatusBadge = ({ status }: { status: string }) => { const formattedStatus = status.charAt(0).toUpperCase() + status.slice(1); switch (formattedStatus) { - case "Pending": + case 'Pending': return {formattedStatus}; - case "Captured": + case 'Captured': return {formattedStatus}; - case "Completed": + case 'Completed': return {formattedStatus}; - case "Cancelled": + case 'Cancelled': return {formattedStatus}; default: return {formattedStatus}; diff --git a/src/components/common/product-status-badge/index.ts b/src/components/common/product-status-badge/index.ts index 4c87ebc8..ea1ebeb6 100644 --- a/src/components/common/product-status-badge/index.ts +++ b/src/components/common/product-status-badge/index.ts @@ -1 +1 @@ -export * from "./product-status-badge"; +export * from './product-status-badge'; diff --git a/src/components/common/product-status-badge/product-status-badge.tsx b/src/components/common/product-status-badge/product-status-badge.tsx index 27eea40b..f775676d 100644 --- a/src/components/common/product-status-badge/product-status-badge.tsx +++ b/src/components/common/product-status-badge/product-status-badge.tsx @@ -1,15 +1,15 @@ -import { StatusCell } from "@components/table/table-cells/common/status-cell"; +import { StatusCell } from '@components/table/table-cells/common/status-cell'; export const ProductStatusBadge = ({ status }: { status: string }) => { const formattedStatus = status.charAt(0).toUpperCase() + status.slice(1); switch (formattedStatus) { - case "": + case '': return {formattedStatus}; - case "Published": + case 'Published': return {formattedStatus}; - case "Canceled": + case 'Canceled': return {formattedStatus}; - case "Draft": + case 'Draft': return {formattedStatus}; default: return {formattedStatus}; diff --git a/src/components/common/progress-bar/index.ts b/src/components/common/progress-bar/index.ts index ad8afe82..d71d9b1b 100644 --- a/src/components/common/progress-bar/index.ts +++ b/src/components/common/progress-bar/index.ts @@ -1 +1 @@ -export * from "./progress-bar" +export * from './progress-bar'; diff --git a/src/components/common/progress-bar/progress-bar.tsx b/src/components/common/progress-bar/progress-bar.tsx index ebfec4c3..12736387 100644 --- a/src/components/common/progress-bar/progress-bar.tsx +++ b/src/components/common/progress-bar/progress-bar.tsx @@ -1,4 +1,4 @@ -import { motion } from "motion/react" +import { motion } from 'motion/react'; interface ProgressBarProps { /** @@ -6,28 +6,26 @@ interface ProgressBarProps { * * @default 2 */ - duration?: number + duration?: number; } -export const ProgressBar = ({ duration = 2 }: ProgressBarProps) => { - return ( - - ) -} +export const ProgressBar = ({ duration = 2 }: ProgressBarProps) => ( + +); diff --git a/src/components/common/section/index.ts b/src/components/common/section/index.ts index 88b24669..659f6a92 100644 --- a/src/components/common/section/index.ts +++ b/src/components/common/section/index.ts @@ -1 +1 @@ -export * from "./section-row" +export * from './section-row'; diff --git a/src/components/common/section/section-row.tsx b/src/components/common/section/section-row.tsx index 635c3868..310fca7f 100644 --- a/src/components/common/section/section-row.tsx +++ b/src/components/common/section/section-row.tsx @@ -1,29 +1,32 @@ -import { Text, clx } from "@medusajs/ui" -import type { ReactNode } from "react" +import type { ReactNode } from 'react'; + +import { clx, Text } from '@medusajs/ui'; export type SectionRowProps = { - title: string - value?: ReactNode | string | null - actions?: ReactNode - "data-testid"?: string -} + title: string; + value?: ReactNode | string | null; + actions?: ReactNode; + 'data-testid'?: string; +}; -export const SectionRow = ({ title, value, actions, "data-testid": dataTestId }: SectionRowProps) => { - const isValueString = typeof value === "string" || !value +export const SectionRow = ({ + title, + value, + actions, + 'data-testid': dataTestId +}: SectionRowProps) => { + const isValueString = typeof value === 'string' || !value; return (
    - @@ -37,10 +40,10 @@ export const SectionRow = ({ title, value, actions, "data-testid": dataTestId }: className="whitespace-pre-line text-pretty" data-testid={dataTestId ? `${dataTestId}-value` : undefined} > - {value ?? "-"} + {value ?? '-'} ) : ( -
    @@ -48,7 +51,9 @@ export const SectionRow = ({ title, value, actions, "data-testid": dataTestId }:
    )} - {actions &&
    {actions}
    } + {actions && ( +
    {actions}
    + )}
    - ) -} + ); +}; diff --git a/src/components/common/seller-status-badge/index.ts b/src/components/common/seller-status-badge/index.ts index c67f38f5..d0cc339d 100644 --- a/src/components/common/seller-status-badge/index.ts +++ b/src/components/common/seller-status-badge/index.ts @@ -1 +1 @@ -export * from "./seller-status-badge"; +export * from './seller-status-badge'; diff --git a/src/components/common/seller-status-badge/seller-status-badge.tsx b/src/components/common/seller-status-badge/seller-status-badge.tsx index 89b56a1f..cdf87526 100644 --- a/src/components/common/seller-status-badge/seller-status-badge.tsx +++ b/src/components/common/seller-status-badge/seller-status-badge.tsx @@ -1,20 +1,48 @@ -import { StatusCell } from "@components/table/table-cells/common/status-cell"; +import { StatusCell } from '@components/table/table-cells/common/status-cell'; -export const SellerStatusBadge = ({ - status, - "data-testid": dataTestId -}: { - status: string - "data-testid"?: string +export const SellerStatusBadge = ({ + status, + 'data-testid': dataTestId +}: { + status: string; + 'data-testid'?: string; }) => { switch (status) { - case "INACTIVE": - return {status}; - case "ACTIVE": - return {status}; - case "SUSPENDED": - return {status}; + case 'INACTIVE': + return ( + + {status} + + ); + case 'ACTIVE': + return ( + + {status} + + ); + case 'SUSPENDED': + return ( + + {status} + + ); default: - return {status}; + return ( + + {status} + + ); } }; diff --git a/src/components/common/sidebar-link/index.ts b/src/components/common/sidebar-link/index.ts new file mode 100644 index 00000000..2703e49f --- /dev/null +++ b/src/components/common/sidebar-link/index.ts @@ -0,0 +1 @@ +export * from './sidebar-link'; diff --git a/src/components/common/sidebar-link/indext.ts b/src/components/common/sidebar-link/indext.ts deleted file mode 100644 index e5d18112..00000000 --- a/src/components/common/sidebar-link/indext.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./sidebar-link" diff --git a/src/components/common/sidebar-link/sidebar-link.tsx b/src/components/common/sidebar-link/sidebar-link.tsx index 09b82300..363c187c 100644 --- a/src/components/common/sidebar-link/sidebar-link.tsx +++ b/src/components/common/sidebar-link/sidebar-link.tsx @@ -1,16 +1,16 @@ -import { ReactNode } from "react" -import { Link } from "react-router-dom" +import type { ReactNode } from 'react'; -import { TriangleRightMini } from "@medusajs/icons" -import { Text } from "@medusajs/ui" -import { IconAvatar } from "../icon-avatar" +import { IconAvatar } from '@components/common/icon-avatar'; +import { TriangleRightMini } from '@medusajs/icons'; +import { Text } from '@medusajs/ui'; +import { Link } from 'react-router-dom'; export interface SidebarLinkProps { - to: string - labelKey: string - descriptionKey: string - icon: ReactNode - "data-testid"?: string + to: string; + labelKey: string; + descriptionKey: string; + icon: ReactNode; + 'data-testid'?: string; } export const SidebarLink = ({ @@ -18,33 +18,54 @@ export const SidebarLink = ({ labelKey, descriptionKey, icon, - "data-testid": dataTestId, -}: SidebarLinkProps) => { - return ( - -
    -
    -
    - {icon} -
    - - {labelKey} - - - {descriptionKey} - -
    -
    - -
    + 'data-testid': dataTestId +}: SidebarLinkProps) => ( + +
    +
    +
    + + {icon} + +
    + + {labelKey} + + + {descriptionKey} + +
    +
    +
    - - ) -} +
    + +); diff --git a/src/components/common/skeleton/index.ts b/src/components/common/skeleton/index.ts index d889ad70..25c51ad0 100644 --- a/src/components/common/skeleton/index.ts +++ b/src/components/common/skeleton/index.ts @@ -1 +1 @@ -export * from "./skeleton" +export * from './skeleton'; diff --git a/src/components/common/skeleton/skeleton.tsx b/src/components/common/skeleton/skeleton.tsx index 90671af6..94038d8c 100644 --- a/src/components/common/skeleton/skeleton.tsx +++ b/src/components/common/skeleton/skeleton.tsx @@ -1,166 +1,164 @@ -import { Container, Heading, Text, clx } from "@medusajs/ui" -import { CSSProperties, ComponentPropsWithoutRef } from "react" +import type { ComponentPropsWithoutRef, CSSProperties } from 'react'; + +import { clx, Container, type Heading, type Text } from '@medusajs/ui'; type SkeletonProps = { - className?: string - style?: CSSProperties -} + className?: string; + style?: CSSProperties; +}; -export const Skeleton = ({ className, style }: SkeletonProps) => { - return ( -
    - ) -} +export const Skeleton = ({ className, style }: SkeletonProps) => ( +
    +); type TextSkeletonProps = { - size?: ComponentPropsWithoutRef["size"] - leading?: ComponentPropsWithoutRef["leading"] - characters?: number -} + size?: ComponentPropsWithoutRef['size']; + leading?: ComponentPropsWithoutRef['leading']; + characters?: number; +}; type HeadingSkeletonProps = { - level?: ComponentPropsWithoutRef["level"] - characters?: number -} + level?: ComponentPropsWithoutRef['level']; + characters?: number; +}; -export const HeadingSkeleton = ({ - level = "h1", - characters = 10, -}: HeadingSkeletonProps) => { - let charWidth = 9 +export const HeadingSkeleton = ({ level = 'h1', characters = 10 }: HeadingSkeletonProps) => { + let charWidth = 9; switch (level) { - case "h1": - charWidth = 11 - break - case "h2": - charWidth = 10 - break - case "h3": - charWidth = 9 - break + case 'h1': + charWidth = 11; + break; + case 'h2': + charWidth = 10; + break; + case 'h3': + charWidth = 9; + break; } return ( - ) -} + ); +}; export const TextSkeleton = ({ - size = "small", - leading = "compact", - characters = 10, + size = 'small', + leading = 'compact', + characters = 10 }: TextSkeletonProps) => { - let charWidth = 9 + let charWidth = 9; switch (size) { - case "xlarge": - charWidth = 13 - break - case "large": - charWidth = 11 - break - case "base": - charWidth = 10 - break - case "small": - charWidth = 9 - break - case "xsmall": - charWidth = 8 - break + case 'xlarge': + charWidth = 13; + break; + case 'large': + charWidth = 11; + break; + case 'base': + charWidth = 10; + break; + case 'small': + charWidth = 9; + break; + case 'xsmall': + charWidth = 8; + break; } return ( - ) -} + ); +}; -export const IconButtonSkeleton = () => { - return -} +export const IconButtonSkeleton = () => ; type GeneralSectionSkeletonProps = { - rowCount?: number -} + rowCount?: number; +}; -export const GeneralSectionSkeleton = ({ - rowCount, -}: GeneralSectionSkeletonProps) => { - const rows = Array.from({ length: rowCount ?? 0 }, (_, i) => i) +export const GeneralSectionSkeleton = ({ rowCount }: GeneralSectionSkeletonProps) => { + const rows = Array.from({ length: rowCount ?? 0 }, (_, i) => i); return ( - +
    - {rows.map((row) => ( + {rows.map(row => (
    - - + +
    ))}
    - ) -} + ); +}; -export const TableFooterSkeleton = ({ layout }: { layout: "fill" | "fit" }) => { - return ( -
    - -
    - - - -
    +export const TableFooterSkeleton = ({ layout }: { layout: 'fill' | 'fit' }) => ( +
    + +
    + + +
    - ) -} +
    +); type TableSkeletonProps = { - rowCount?: number - search?: boolean - filters?: boolean - orderBy?: boolean - pagination?: boolean - layout?: "fit" | "fill" -} + rowCount?: number; + search?: boolean; + filters?: boolean; + orderBy?: boolean; + pagination?: boolean; + layout?: 'fit' | 'fill'; +}; export const TableSkeleton = ({ rowCount = 10, @@ -168,19 +166,19 @@ export const TableSkeleton = ({ filters = true, orderBy = true, pagination = true, - layout = "fit", + layout = 'fit' }: TableSkeletonProps) => { // Row count + header row - const totalRowCount = rowCount + 1 + const totalRowCount = rowCount + 1; - const rows = Array.from({ length: totalRowCount }, (_, i) => i) - const hasToolbar = search || filters || orderBy + const rows = Array.from({ length: totalRowCount }, (_, i) => i); + const hasToolbar = search || filters || orderBy; return (
    {hasToolbar && ( @@ -195,133 +193,141 @@ export const TableSkeleton = ({
    )}
    - {rows.map((row) => ( - + {rows.map(row => ( + ))}
    {pagination && }
    - ) -} + ); +}; -export const TableSectionSkeleton = (props: TableSkeletonProps) => { - return ( - -
    - - -
    - -
    - ) -} +export const TableSectionSkeleton = (props: TableSkeletonProps) => ( + +
    + + +
    + +
    +); -export const JsonViewSectionSkeleton = () => { - return ( - -
    -
    - - -
    - +export const JsonViewSectionSkeleton = () => ( + +
    +
    + +
    - - ) -} + +
    +
    +); type SingleColumnPageSkeletonProps = { - sections?: number - showJSON?: boolean - showMetadata?: boolean -} + sections?: number; + showJSON?: boolean; + showMetadata?: boolean; +}; export const SingleColumnPageSkeleton = ({ sections = 2, showJSON = false, - showMetadata = false, -}: SingleColumnPageSkeletonProps) => { - return ( -
    - {Array.from({ length: sections }, (_, i) => i).map((section) => { - return ( - - ) - })} - {showMetadata && } - {showJSON && } -
    - ) -} + showMetadata = false +}: SingleColumnPageSkeletonProps) => ( +
    + {Array.from({ length: sections }, (_, i) => i).map(section => { + return ( + + ); + })} + {showMetadata && } + {showJSON && } +
    +); type TwoColumnPageSkeletonProps = { - mainSections?: number - sidebarSections?: number - showJSON?: boolean - showMetadata?: boolean -} + mainSections?: number; + sidebarSections?: number; + showJSON?: boolean; + showMetadata?: boolean; +}; export const TwoColumnPageSkeleton = ({ mainSections = 2, sidebarSections = 1, showJSON = false, - showMetadata = true, + showMetadata = true }: TwoColumnPageSkeletonProps) => { - const showExtraData = showJSON || showMetadata + const showExtraData = showJSON || showMetadata; return (
    - {Array.from({ length: mainSections }, (_, i) => i).map((section) => { - return ( - - ) - })} + {Array.from({ length: mainSections }, (_, i) => i).map(section => ( + + ))} {showExtraData && (
    - {showMetadata && ( - - )} + {showMetadata && } {showJSON && }
    )}
    - {Array.from({ length: sidebarSections }, (_, i) => i).map( - (section) => { - return ( - - ) - } - )} + {Array.from({ length: sidebarSections }, (_, i) => i).map(section => ( + + ))} {showExtraData && (
    - {showMetadata && ( - - )} + {showMetadata && } {showJSON && }
    )}
    - ) -} + ); +}; diff --git a/src/components/common/sortable-list/index.ts b/src/components/common/sortable-list/index.ts index c9eb55be..ea079c48 100644 --- a/src/components/common/sortable-list/index.ts +++ b/src/components/common/sortable-list/index.ts @@ -1 +1 @@ -export * from "./sortable-list" +export * from './sortable-list'; diff --git a/src/components/common/sortable-list/sortable-list.tsx b/src/components/common/sortable-list/sortable-list.tsx index e588fc8b..1180ffe5 100644 --- a/src/components/common/sortable-list/sortable-list.tsx +++ b/src/components/common/sortable-list/sortable-list.tsx @@ -1,90 +1,91 @@ import { - Active, + createContext, + Fragment, + useContext, + useMemo, + useState, + type CSSProperties, + type PropsWithChildren, + type ReactNode +} from 'react'; + +import { + defaultDropAnimationSideEffects, DndContext, - DragEndEvent, DragOverlay, - DragStartEvent, - DraggableSyntheticListeners, KeyboardSensor, PointerSensor, - defaultDropAnimationSideEffects, useSensor, useSensors, + type Active, + type DragEndEvent, + type DraggableSyntheticListeners, + type DragStartEvent, type DropAnimation, - type UniqueIdentifier, -} from "@dnd-kit/core" + type UniqueIdentifier +} from '@dnd-kit/core'; import { - SortableContext, arrayMove, + SortableContext, sortableKeyboardCoordinates, - useSortable, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { DotsSix } from "@medusajs/icons" -import { IconButton, clx } from "@medusajs/ui" -import { - CSSProperties, - Fragment, - PropsWithChildren, - ReactNode, - createContext, - useContext, - useMemo, - useState, -} from "react" + useSortable +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { DotsSix } from '@medusajs/icons'; +import { clx, IconButton } from '@medusajs/ui'; type SortableBaseItem = { - id: UniqueIdentifier -} + id: UniqueIdentifier; +}; interface SortableListProps { - items: TItem[] - onChange: (items: TItem[]) => void - renderItem: (item: TItem, index: number) => ReactNode + items: TItem[]; + onChange: (items: TItem[]) => void; + renderItem: (item: TItem, index: number) => ReactNode; } const List = ({ items, onChange, - renderItem, + renderItem }: SortableListProps) => { - const [active, setActive] = useState(null) + const [active, setActive] = useState(null); const [activeItem, activeIndex] = useMemo(() => { if (active === null) { - return [null, null] + return [null, null]; } - const index = items.findIndex(({ id }) => id === active.id) + const index = items.findIndex(({ id }) => id === active.id); - return [items[index], index] - }, [active, items]) + return [items[index], index]; + }, [active, items]); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, + coordinateGetter: sortableKeyboardCoordinates }) - ) + ); const handleDragStart = ({ active }: DragStartEvent) => { - setActive(active) - } + setActive(active); + }; const handleDragEnd = ({ active, over }: DragEndEvent) => { if (over && active.id !== over.id) { - const activeIndex = items.findIndex(({ id }) => id === active.id) - const overIndex = items.findIndex(({ id }) => id === over.id) + const activeIndex = items.findIndex(({ id }) => id === active.id); + const overIndex = items.findIndex(({ id }) => id === over.id); - onChange(arrayMove(items, activeIndex, overIndex)) + onChange(arrayMove(items, activeIndex, overIndex)); } - setActive(null) - } + setActive(null); + }; const handleDragCancel = () => { - setActive(null) - } + setActive(null); + }; return ( ({ onDragCancel={handleDragCancel} > - {activeItem && activeIndex !== null - ? renderItem(activeItem, activeIndex) - : null} + {activeItem && activeIndex !== null ? renderItem(activeItem, activeIndex) : null}
      ({
    - ) -} + ); +}; const dropAnimationConfig: DropAnimation = { sideEffects: defaultDropAnimationSideEffects({ styles: { active: { - opacity: "0.4", - }, - }, - }), -} + opacity: '0.4' + } + } + }) +}; -type SortableOverlayProps = PropsWithChildren +type SortableOverlayProps = PropsWithChildren; -const Overlay = ({ children }: SortableOverlayProps) => { - return ( - - {children} - - ) -} +const Overlay = ({ children }: SortableOverlayProps) => ( + + {children} + +); type SortableItemProps = PropsWithChildren<{ - id: TItem["id"] - className?: string -}> + id: TItem['id']; + className?: string; +}>; type SortableItemContextValue = { - attributes: Record - listeners: DraggableSyntheticListeners - ref: (node: HTMLElement | null) => void - isDragging: boolean -} + // @todo fix any type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + attributes: Record; + listeners: DraggableSyntheticListeners; + ref: (node: HTMLElement | null) => void; + isDragging: boolean; +}; -const SortableItemContext = createContext(null) +const SortableItemContext = createContext(null); const useSortableItemContext = () => { - const context = useContext(SortableItemContext) + const context = useContext(SortableItemContext); if (!context) { - throw new Error( - "useSortableItemContext must be used within a SortableItemContext" - ) + throw new Error('useSortableItemContext must be used within a SortableItemContext'); } - return context -} + return context; +}; const Item = ({ id, className, - children, + children }: SortableItemProps) => { const { attributes, @@ -173,40 +170,40 @@ const Item = ({ setNodeRef, setActivatorNodeRef, transform, - transition, - } = useSortable({ id }) + transition + } = useSortable({ id }); const context = useMemo( () => ({ attributes, listeners, ref: setActivatorNodeRef, - isDragging, + isDragging }), [attributes, listeners, setActivatorNodeRef, isDragging] - ) + ); const style: CSSProperties = { opacity: isDragging ? 0.4 : undefined, transform: CSS.Translate.toString(transform), - transition, - } + transition + }; return (
  • {children}
  • - ) -} + ); +}; const DragHandle = () => { - const { attributes, listeners, ref } = useSortableItemContext() + const { attributes, listeners, ref } = useSortableItemContext(); return ( { > - ) -} + ); +}; export const SortableList = Object.assign(List, { Item, - DragHandle, -}) + DragHandle +}); diff --git a/src/components/common/sortable-tree/index.ts b/src/components/common/sortable-tree/index.ts index 46f2ac3c..2315bd49 100644 --- a/src/components/common/sortable-tree/index.ts +++ b/src/components/common/sortable-tree/index.ts @@ -1 +1 @@ -export * from "./sortable-tree" +export * from './sortable-tree'; diff --git a/src/components/common/sortable-tree/keyboard-coordinates.ts b/src/components/common/sortable-tree/keyboard-coordinates.ts index a836cc5a..fdb3d1e2 100644 --- a/src/components/common/sortable-tree/keyboard-coordinates.ts +++ b/src/components/common/sortable-tree/keyboard-coordinates.ts @@ -1,22 +1,22 @@ import { - DroppableContainer, - KeyboardCode, - KeyboardCoordinateGetter, closestCorners, getFirstCollision, -} from "@dnd-kit/core" + KeyboardCode, + type DroppableContainer, + type KeyboardCoordinateGetter +} from '@dnd-kit/core'; -import type { SensorContext } from "./types" -import { getProjection } from "./utils" +import type { SensorContext } from './types'; +import { getProjection } from './utils'; const directions: string[] = [ KeyboardCode.Down, KeyboardCode.Right, KeyboardCode.Up, - KeyboardCode.Left, -] + KeyboardCode.Left +]; -const horizontal: string[] = [KeyboardCode.Left, KeyboardCode.Right] +const horizontal: string[] = [KeyboardCode.Left, KeyboardCode.Right]; export const sortableTreeKeyboardCoordinates: ( context: SensorContext, @@ -27,25 +27,19 @@ export const sortableTreeKeyboardCoordinates: ( event, { currentCoordinates, - context: { - active, - over, - collisionRect, - droppableRects, - droppableContainers, - }, + context: { active, over, collisionRect, droppableRects, droppableContainers } } ) => { if (directions.includes(event.code)) { if (!active || !collisionRect) { - return + return; } - event.preventDefault() + event.preventDefault(); const { - current: { items, offset }, - } = context + current: { items, offset } + } = context; if (horizontal.includes(event.code) && over?.id) { const { depth, maxDepth, minDepth } = getProjection( @@ -54,80 +48,80 @@ export const sortableTreeKeyboardCoordinates: ( over.id, offset, indentationWidth - ) + ); switch (event.code) { case KeyboardCode.Left: if (depth > minDepth) { return { ...currentCoordinates, - x: currentCoordinates.x - indentationWidth, - } + x: currentCoordinates.x - indentationWidth + }; } - break + break; case KeyboardCode.Right: if (depth < maxDepth) { return { ...currentCoordinates, - x: currentCoordinates.x + indentationWidth, - } + x: currentCoordinates.x + indentationWidth + }; } - break + break; } - return undefined + return undefined; } - const containers: DroppableContainer[] = [] + const containers: DroppableContainer[] = []; - droppableContainers.forEach((container) => { + droppableContainers.forEach(container => { if (container?.disabled || container.id === over?.id) { - return + return; } - const rect = droppableRects.get(container.id) + const rect = droppableRects.get(container.id); if (!rect) { - return + return; } switch (event.code) { case KeyboardCode.Down: if (collisionRect.top < rect.top) { - containers.push(container) + containers.push(container); } - break + break; case KeyboardCode.Up: if (collisionRect.top > rect.top) { - containers.push(container) + containers.push(container); } - break + break; } - }) + }); const collisions = closestCorners({ active, collisionRect, pointerCoordinates: null, droppableRects, - droppableContainers: containers, - }) - let closestId = getFirstCollision(collisions, "id") + droppableContainers: containers + }); + let closestId = getFirstCollision(collisions, 'id'); if (closestId === over?.id && collisions.length > 1) { - closestId = collisions[1].id + closestId = collisions[1].id; } if (closestId && over?.id) { - const activeRect = droppableRects.get(active.id) - const newRect = droppableRects.get(closestId) - const newDroppable = droppableContainers.get(closestId) + const activeRect = droppableRects.get(active.id); + const newRect = droppableRects.get(closestId); + const newDroppable = droppableContainers.get(closestId); if (activeRect && newRect && newDroppable) { - const newIndex = items.findIndex(({ id }) => id === closestId) - const newItem = items[newIndex] - const activeIndex = items.findIndex(({ id }) => id === active.id) - const activeItem = items[activeIndex] + const newIndex = items.findIndex(({ id }) => id === closestId); + const newItem = items[newIndex]; + const activeIndex = items.findIndex(({ id }) => id === active.id); + const activeItem = items[activeIndex]; if (newItem && activeItem) { const { depth } = getProjection( @@ -136,21 +130,19 @@ export const sortableTreeKeyboardCoordinates: ( closestId, (newItem.depth - activeItem.depth) * indentationWidth, indentationWidth - ) - const isBelow = newIndex > activeIndex - const modifier = isBelow ? 1 : -1 - const offset = 0 + ); + const isBelow = newIndex > activeIndex; + const modifier = isBelow ? 1 : -1; + const offset = 0; - const newCoordinates = { + return { x: newRect.left + depth * indentationWidth, - y: newRect.top + modifier * offset, - } - - return newCoordinates + y: newRect.top + modifier * offset + }; } } } } - return undefined - } + return undefined; + }; diff --git a/src/components/common/sortable-tree/sortable-tree-item.tsx b/src/components/common/sortable-tree/sortable-tree-item.tsx index d87821a9..0606b5bf 100644 --- a/src/components/common/sortable-tree/sortable-tree-item.tsx +++ b/src/components/common/sortable-tree/sortable-tree-item.tsx @@ -1,28 +1,21 @@ -import type { UniqueIdentifier } from "@dnd-kit/core" -import { AnimateLayoutChanges, useSortable } from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { CSSProperties } from "react" +import type { CSSProperties } from 'react'; -import { TreeItem, TreeItemProps } from "./tree-item" -import { iOS } from "./utils" +import type { UniqueIdentifier } from '@dnd-kit/core'; +import { useSortable, type AnimateLayoutChanges } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; + +import { TreeItem, type TreeItemProps } from './tree-item'; +import { iOS } from './utils'; interface SortableTreeItemProps extends TreeItemProps { - id: UniqueIdentifier + id: UniqueIdentifier; } -const animateLayoutChanges: AnimateLayoutChanges = ({ - isSorting, - wasDragging, -}) => { - return isSorting || wasDragging ? false : true -} +const animateLayoutChanges: AnimateLayoutChanges = ({ isSorting, wasDragging }) => { + return !(isSorting || wasDragging); +}; -export function SortableTreeItem({ - id, - depth, - disabled, - ...props -}: SortableTreeItemProps) { +export function SortableTreeItem({ id, depth, disabled, ...props }: SortableTreeItemProps) { const { attributes, isDragging, @@ -31,16 +24,16 @@ export function SortableTreeItem({ setDraggableNodeRef, setDroppableNodeRef, transform, - transition, + transition } = useSortable({ id, animateLayoutChanges, - disabled, - }) + disabled + }); const style: CSSProperties = { transform: CSS.Translate.toString(transform), - transition, - } + transition + }; return ( - ) + ); } diff --git a/src/components/common/sortable-tree/sortable-tree.tsx b/src/components/common/sortable-tree/sortable-tree.tsx index 9ddeb896..5ba245b0 100644 --- a/src/components/common/sortable-tree/sortable-tree.tsx +++ b/src/components/common/sortable-tree/sortable-tree.tsx @@ -1,46 +1,37 @@ +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; +import { createPortal } from 'react-dom'; + import { - Announcements, + closestCenter, + defaultDropAnimation, DndContext, - DragEndEvent, - DragMoveEvent, - DragOverEvent, DragOverlay, - DragStartEvent, - DropAnimation, KeyboardSensor, MeasuringStrategy, PointerSensor, - UniqueIdentifier, - closestCenter, - defaultDropAnimation, useSensor, useSensors, -} from "@dnd-kit/core" -import { - SortableContext, - arrayMove, - verticalListSortingStrategy, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { ReactNode, useEffect, useMemo, useRef, useState } from "react" -import { createPortal } from "react-dom" - -import { sortableTreeKeyboardCoordinates } from "./keyboard-coordinates" -import { SortableTreeItem } from "./sortable-tree-item" -import type { FlattenedItem, SensorContext, TreeItem } from "./types" -import { - buildTree, - flattenTree, - getChildCount, - getProjection, - removeChildrenOf, -} from "./utils" + type Announcements, + type DragEndEvent, + type DragMoveEvent, + type DragOverEvent, + type DragStartEvent, + type DropAnimation, + type UniqueIdentifier +} from '@dnd-kit/core'; +import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; + +import { sortableTreeKeyboardCoordinates } from './keyboard-coordinates'; +import { SortableTreeItem } from './sortable-tree-item'; +import type { FlattenedItem, SensorContext, TreeItem } from './types'; +import { buildTree, flattenTree, getChildCount, getProjection, removeChildrenOf } from './utils'; const measuring = { droppable: { - strategy: MeasuringStrategy.Always, - }, -} + strategy: MeasuringStrategy.Always + } +}; const dropAnimationConfig: DropAnimation = { keyframes({ transform }) { @@ -51,194 +42,174 @@ const dropAnimationConfig: DropAnimation = { transform: CSS.Transform.toString({ ...transform.final, x: transform.final.x + 5, - y: transform.final.y + 5, - }), - }, - ] + y: transform.final.y + 5 + }) + } + ]; }, - easing: "ease-out", + easing: 'ease-out', sideEffects({ active }) { active.node.animate([{ opacity: 0 }, { opacity: 1 }], { duration: defaultDropAnimation.duration, - easing: defaultDropAnimation.easing, - }) - }, -} + easing: defaultDropAnimation.easing + }); + } +}; interface Props { - collapsible?: boolean - childrenProp?: string - items: T[] - indentationWidth?: number + collapsible?: boolean; + childrenProp?: string; + items: T[]; + indentationWidth?: number; /** * Enable drag for all items or provide a function to enable drag for specific items. * @default true */ - enableDrag?: boolean | ((item: T) => boolean) + enableDrag?: boolean | ((item: T) => boolean); onChange: ( updatedItem: { - id: UniqueIdentifier - parentId: UniqueIdentifier | null - index: number + id: UniqueIdentifier; + parentId: UniqueIdentifier | null; + index: number; }, items: T[] - ) => void - renderValue: (item: T) => ReactNode + ) => void; + renderValue: (item: T) => ReactNode; } export function SortableTree({ collapsible = true, - childrenProp = "children", // "children" is the default children prop name + childrenProp = 'children', // "children" is the default children prop name enableDrag = true, items = [], indentationWidth = 40, onChange, - renderValue, + renderValue }: Props) { - const [collapsedState, setCollapsedState] = useState< - Record - >({}) + const [collapsedState, setCollapsedState] = useState>({}); - const [activeId, setActiveId] = useState(null) - const [overId, setOverId] = useState(null) - const [offsetLeft, setOffsetLeft] = useState(0) + const [activeId, setActiveId] = useState(null); + const [overId, setOverId] = useState(null); + const [offsetLeft, setOffsetLeft] = useState(0); const [currentPosition, setCurrentPosition] = useState<{ - parentId: UniqueIdentifier | null - overId: UniqueIdentifier - } | null>(null) + parentId: UniqueIdentifier | null; + overId: UniqueIdentifier; + } | null>(null); const flattenedItems = useMemo(() => { - const flattenedTree = flattenTree(items, childrenProp) - const collapsedItems = flattenedTree.reduce( - (acc, item) => { - const { id } = item - const children = (item[childrenProp] || []) as FlattenedItem[] - const collapsed = collapsedState[id] - - return collapsed && children.length ? [...acc, id] : acc - }, - [] - ) + const flattenedTree = flattenTree(items, childrenProp); + const collapsedItems = flattenedTree.reduce((acc, item) => { + const { id } = item; + const children = (item[childrenProp] || []) as FlattenedItem[]; + const collapsed = collapsedState[id]; + + return collapsed && children.length ? [...acc, id] : acc; + }, []); return removeChildrenOf( flattenedTree, activeId ? [activeId, ...collapsedItems] : collapsedItems, childrenProp - ) - }, [activeId, items, childrenProp, collapsedState]) + ); + }, [activeId, items, childrenProp, collapsedState]); const projected = activeId && overId - ? getProjection( - flattenedItems, - activeId, - overId, - offsetLeft, - indentationWidth - ) - : null + ? getProjection(flattenedItems, activeId, overId, offsetLeft, indentationWidth) + : null; const sensorContext: SensorContext = useRef({ items: flattenedItems, - offset: offsetLeft, - }) + offset: offsetLeft + }); const [coordinateGetter] = useState(() => sortableTreeKeyboardCoordinates(sensorContext, indentationWidth) - ) + ); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { - coordinateGetter, + coordinateGetter }) - ) + ); - const sortedIds = useMemo( - () => flattenedItems.map(({ id }) => id), - [flattenedItems] - ) + const sortedIds = useMemo(() => flattenedItems.map(({ id }) => id), [flattenedItems]); - const activeItem = activeId - ? flattenedItems.find(({ id }) => id === activeId) - : null + const activeItem = activeId ? flattenedItems.find(({ id }) => id === activeId) : null; useEffect(() => { sensorContext.current = { items: flattenedItems, - offset: offsetLeft, - } - }, [flattenedItems, offsetLeft]) + offset: offsetLeft + }; + }, [flattenedItems, offsetLeft]); function handleDragStart({ active: { id: activeId } }: DragStartEvent) { - setActiveId(activeId) - setOverId(activeId) + setActiveId(activeId); + setOverId(activeId); - const activeItem = flattenedItems.find(({ id }) => id === activeId) + const activeItem = flattenedItems.find(({ id }) => id === activeId); if (activeItem) { setCurrentPosition({ parentId: activeItem.parentId, - overId: activeId, - }) + overId: activeId + }); } - document.body.style.setProperty("cursor", "grabbing") + document.body.style.setProperty('cursor', 'grabbing'); } function handleDragMove({ delta }: DragMoveEvent) { - setOffsetLeft(delta.x) + setOffsetLeft(delta.x); } function handleDragOver({ over }: DragOverEvent) { - setOverId(over?.id ?? null) + setOverId(over?.id ?? null); } function handleDragEnd({ active, over }: DragEndEvent) { - resetState() + resetState(); if (projected && over) { - const { depth, parentId } = projected + const { depth, parentId } = projected; const clonedItems: FlattenedItem[] = JSON.parse( JSON.stringify(flattenTree(items, childrenProp)) - ) - const overIndex = clonedItems.findIndex(({ id }) => id === over.id) + ); + const overIndex = clonedItems.findIndex(({ id }) => id === over.id); - const activeIndex = clonedItems.findIndex(({ id }) => id === active.id) - const activeTreeItem = clonedItems[activeIndex] + const activeIndex = clonedItems.findIndex(({ id }) => id === active.id); + const activeTreeItem = clonedItems[activeIndex]; - clonedItems[activeIndex] = { ...activeTreeItem, depth, parentId } + clonedItems[activeIndex] = { ...activeTreeItem, depth, parentId }; - const sortedItems = arrayMove(clonedItems, activeIndex, overIndex) + const sortedItems = arrayMove(clonedItems, activeIndex, overIndex); - const { items: newItems, update } = buildTree( - sortedItems, - overIndex, - childrenProp - ) + const { items: newItems, update } = buildTree(sortedItems, overIndex, childrenProp); - onChange(update, newItems) + onChange(update, newItems); } } function handleDragCancel() { - resetState() + resetState(); } function resetState() { - setOverId(null) - setActiveId(null) - setOffsetLeft(0) - setCurrentPosition(null) + setOverId(null); + setActiveId(null); + setOffsetLeft(0); + setCurrentPosition(null); - document.body.style.setProperty("cursor", "") + document.body.style.setProperty('cursor', ''); } function handleCollapse(id: UniqueIdentifier) { - setCollapsedState((state) => ({ + setCollapsedState(state => ({ ...state, - [id]: state[id] ? false : true, - })) + [id]: !state[id] + })); } function getMovementAnnouncement( @@ -247,76 +218,76 @@ export function SortableTree({ overId?: UniqueIdentifier ) { if (overId && projected) { - if (eventName !== "onDragEnd") { + if (eventName !== 'onDragEnd') { if ( currentPosition && projected.parentId === currentPosition.parentId && overId === currentPosition.overId ) { - return + return; } else { setCurrentPosition({ parentId: projected.parentId, - overId, - }) + overId + }); } } const clonedItems: FlattenedItem[] = JSON.parse( JSON.stringify(flattenTree(items, childrenProp)) - ) - const overIndex = clonedItems.findIndex(({ id }) => id === overId) - const activeIndex = clonedItems.findIndex(({ id }) => id === activeId) - const sortedItems = arrayMove(clonedItems, activeIndex, overIndex) + ); + const overIndex = clonedItems.findIndex(({ id }) => id === overId); + const activeIndex = clonedItems.findIndex(({ id }) => id === activeId); + const sortedItems = arrayMove(clonedItems, activeIndex, overIndex); - const previousItem = sortedItems[overIndex - 1] + const previousItem = sortedItems[overIndex - 1]; - let announcement - const movedVerb = eventName === "onDragEnd" ? "dropped" : "moved" - const nestedVerb = eventName === "onDragEnd" ? "dropped" : "nested" + let announcement; + const movedVerb = eventName === 'onDragEnd' ? 'dropped' : 'moved'; + const nestedVerb = eventName === 'onDragEnd' ? 'dropped' : 'nested'; if (!previousItem) { - const nextItem = sortedItems[overIndex + 1] - announcement = `${activeId} was ${movedVerb} before ${nextItem.id}.` + const nextItem = sortedItems[overIndex + 1]; + announcement = `${activeId} was ${movedVerb} before ${nextItem.id}.`; } else { if (projected.depth > previousItem.depth) { - announcement = `${activeId} was ${nestedVerb} under ${previousItem.id}.` + announcement = `${activeId} was ${nestedVerb} under ${previousItem.id}.`; } else { - let previousSibling: FlattenedItem | undefined = previousItem + let previousSibling: FlattenedItem | undefined = previousItem; while (previousSibling && projected.depth < previousSibling.depth) { - const parentId: UniqueIdentifier | null = previousSibling.parentId - previousSibling = sortedItems.find(({ id }) => id === parentId) + const parentId: UniqueIdentifier | null = previousSibling.parentId; + previousSibling = sortedItems.find(({ id }) => id === parentId); } if (previousSibling) { - announcement = `${activeId} was ${movedVerb} after ${previousSibling.id}.` + announcement = `${activeId} was ${movedVerb} after ${previousSibling.id}.`; } } } - return announcement + return announcement; } - return + return; } const announcements: Announcements = { onDragStart({ active }) { - return `Picked up ${active.id}.` + return `Picked up ${active.id}.`; }, onDragMove({ active, over }) { - return getMovementAnnouncement("onDragMove", active.id, over?.id) + return getMovementAnnouncement('onDragMove', active.id, over?.id); }, onDragOver({ active, over }) { - return getMovementAnnouncement("onDragOver", active.id, over?.id) + return getMovementAnnouncement('onDragOver', active.id, over?.id); }, onDragEnd({ active, over }) { - return getMovementAnnouncement("onDragEnd", active.id, over?.id) + return getMovementAnnouncement('onDragEnd', active.id, over?.id); }, onDragCancel({ active }) { - return `Moving was cancelled. ${active.id} was dropped in its original position.` - }, - } + return `Moving was cancelled. ${active.id} was dropped in its original position.`; + } + }; return ( ({ onDragEnd={handleDragEnd} onDragCancel={handleDragCancel} > - - {flattenedItems.map((item) => { - const { id, depth } = item - const children = (item[childrenProp] || []) as FlattenedItem[] + + {flattenedItems.map(item => { + const { id, depth } = item; + const children = (item[childrenProp] || []) as FlattenedItem[]; const disabled = - typeof enableDrag === "function" - ? !enableDrag(item as unknown as T) - : !enableDrag + typeof enableDrag === 'function' ? !enableDrag(item as unknown as T) : !enableDrag; return ( ({ indentationWidth={indentationWidth} collapsed={Boolean(collapsedState[id] && children.length)} childCount={children.length} - onCollapse={ - collapsible && children.length - ? () => handleCollapse(id) - : undefined - } + onCollapse={collapsible && children.length ? () => handleCollapse(id) : undefined} /> - ) + ); })} {createPortal( @@ -375,5 +343,5 @@ export function SortableTree({ )} - ) + ); } diff --git a/src/components/common/sortable-tree/tree-item.tsx b/src/components/common/sortable-tree/tree-item.tsx index 1516e68a..a573ac75 100644 --- a/src/components/common/sortable-tree/tree-item.tsx +++ b/src/components/common/sortable-tree/tree-item.tsx @@ -1,30 +1,31 @@ -import React, { forwardRef, HTMLAttributes, ReactNode } from "react" +import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'; +import type React from 'react'; import { DotsSix, FolderIllustration, FolderOpenIllustration, TagIllustration, - TriangleRightMini, -} from "@medusajs/icons" -import { Badge, clx, IconButton } from "@medusajs/ui" -import { HandleProps } from "./types" - -export interface TreeItemProps - extends Omit, "id"> { - childCount?: number - clone?: boolean - collapsed?: boolean - depth: number - disableInteraction?: boolean - disableSelection?: boolean - ghost?: boolean - handleProps?: HandleProps - indentationWidth: number - value: ReactNode - disabled?: boolean - onCollapse?(): void - wrapperRef?(node: HTMLLIElement): void + TriangleRightMini +} from '@medusajs/icons'; +import { Badge, clx, IconButton } from '@medusajs/ui'; + +import type { HandleProps } from './types'; + +export interface TreeItemProps extends Omit, 'id'> { + childCount?: number; + clone?: boolean; + collapsed?: boolean; + depth: number; + disableInteraction?: boolean; + disableSelection?: boolean; + ghost?: boolean; + handleProps?: HandleProps; + indentationWidth: number; + value: ReactNode; + disabled?: boolean; + onCollapse?(): void; + wrapperRef?(node: HTMLLIElement): void; } export const TreeItem = forwardRef( @@ -47,85 +48,83 @@ export const TreeItem = forwardRef( ...props }, ref - ) => { - return ( -
  • ( +
  • div]:border-t-0': !clone + })} + {...props} + > +
    div]:border-t-0": !clone, - })} - {...props} + 'border-l': depth > 0, + 'w-fit rounded-lg border-none bg-ui-bg-base pr-6 opacity-80 shadow-elevation-flyout': + clone, + 'z-[1] bg-ui-bg-base-hover opacity-50': ghost, + 'bg-ui-bg-disabled': disabled + } + )} > -
    0, - "shadow-elevation-flyout bg-ui-bg-base w-fit rounded-lg border-none pr-6 opacity-80": - clone, - "bg-ui-bg-base-hover z-[1] opacity-50": ghost, - "bg-ui-bg-disabled": disabled, - } - )} - > - - - - - -
    -
  • - ) - } -) -TreeItem.displayName = "TreeItem" - -const Handle = ({ - listeners, - attributes, - disabled, -}: HandleProps & { disabled?: boolean }) => { - return ( - - - + + + + + +
    + ) -} +); +TreeItem.displayName = 'TreeItem'; + +const Handle = ({ listeners, attributes, disabled }: HandleProps & { disabled?: boolean }) => ( + + + +); type IconProps = { - childrenCount?: number - collapsed?: boolean - clone?: boolean -} + childrenCount?: number; + collapsed?: boolean; + clone?: boolean; +}; const Icon = ({ childrenCount, collapsed, clone }: IconProps) => { - const isBranch = clone ? childrenCount && childrenCount > 1 : childrenCount - const isOpen = clone ? false : !collapsed + const isBranch = clone ? childrenCount && childrenCount > 1 : childrenCount; + const isOpen = clone ? false : !collapsed; return (
    @@ -139,22 +138,27 @@ const Icon = ({ childrenCount, collapsed, clone }: IconProps) => { )}
    - ) -} + ); +}; type CollapseProps = { - collapsed?: boolean - onCollapse?: () => void - clone?: boolean -} + collapsed?: boolean; + onCollapse?: () => void; + clone?: boolean; +}; const Collapse = ({ collapsed, onCollapse, clone }: CollapseProps) => { if (clone) { - return null + return null; } if (!onCollapse) { - return
    + return ( +
    + ); } return ( @@ -165,43 +169,43 @@ const Collapse = ({ collapsed, onCollapse, clone }: CollapseProps) => { type="button" > - ) -} + ); +}; type ValueProps = { - value: ReactNode -} + value: ReactNode; +}; -const Value = ({ value }: ValueProps) => { - return ( -
    - {value} -
    - ) -} +const Value = ({ value }: ValueProps) => ( +
    {value}
    +); type ChildrenCountProps = { - clone?: boolean - childrenCount?: number -} + clone?: boolean; + childrenCount?: number; +}; const ChildrenCount = ({ clone, childrenCount }: ChildrenCountProps) => { if (!clone || !childrenCount) { - return null + return null; } if (clone && childrenCount <= 1) { - return null + return null; } return ( - + {childrenCount} - ) -} + ); +}; diff --git a/src/components/common/sortable-tree/types.ts b/src/components/common/sortable-tree/types.ts index 8b78d701..2559900a 100644 --- a/src/components/common/sortable-tree/types.ts +++ b/src/components/common/sortable-tree/types.ts @@ -1,23 +1,24 @@ -import type { DraggableAttributes, UniqueIdentifier } from "@dnd-kit/core" -import { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities" -import type { MutableRefObject } from "react" +import type { MutableRefObject } from 'react'; + +import type { DraggableAttributes, UniqueIdentifier } from '@dnd-kit/core'; +import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities'; export interface TreeItem extends Record { - id: UniqueIdentifier + id: UniqueIdentifier; } export interface FlattenedItem extends TreeItem { - parentId: UniqueIdentifier | null - depth: number - index: number + parentId: UniqueIdentifier | null; + depth: number; + index: number; } export type SensorContext = MutableRefObject<{ - items: FlattenedItem[] - offset: number -}> + items: FlattenedItem[]; + offset: number; +}>; export type HandleProps = { - attributes?: DraggableAttributes | undefined - listeners?: SyntheticListenerMap | undefined -} + attributes?: DraggableAttributes | undefined; + listeners?: SyntheticListenerMap | undefined; +}; diff --git a/src/components/common/sortable-tree/utils.ts b/src/components/common/sortable-tree/utils.ts index 064546dc..a90df665 100644 --- a/src/components/common/sortable-tree/utils.ts +++ b/src/components/common/sortable-tree/utils.ts @@ -1,12 +1,12 @@ -import type { UniqueIdentifier } from "@dnd-kit/core" -import { arrayMove } from "@dnd-kit/sortable" +import type { UniqueIdentifier } from '@dnd-kit/core'; +import { arrayMove } from '@dnd-kit/sortable'; -import type { FlattenedItem, TreeItem } from "./types" +import type { FlattenedItem, TreeItem } from './types'; -export const iOS = /iPad|iPhone|iPod/.test(navigator.platform) +export const iOS = /iPad|iPhone|iPod/.test(navigator.platform); function getDragDepth(offset: number, indentationWidth: number) { - return Math.round(offset / indentationWidth) + return Math.round(offset / indentationWidth); } export function getProjection( @@ -16,64 +16,64 @@ export function getProjection( dragOffset: number, indentationWidth: number ) { - const overItemIndex = items.findIndex(({ id }) => id === overId) - const activeItemIndex = items.findIndex(({ id }) => id === activeId) - const activeItem = items[activeItemIndex] - const newItems = arrayMove(items, activeItemIndex, overItemIndex) - const previousItem = newItems[overItemIndex - 1] - const nextItem = newItems[overItemIndex + 1] - const dragDepth = getDragDepth(dragOffset, indentationWidth) - const projectedDepth = activeItem.depth + dragDepth + const overItemIndex = items.findIndex(({ id }) => id === overId); + const activeItemIndex = items.findIndex(({ id }) => id === activeId); + const activeItem = items[activeItemIndex]; + const newItems = arrayMove(items, activeItemIndex, overItemIndex); + const previousItem = newItems[overItemIndex - 1]; + const nextItem = newItems[overItemIndex + 1]; + const dragDepth = getDragDepth(dragOffset, indentationWidth); + const projectedDepth = activeItem.depth + dragDepth; const maxDepth = getMaxDepth({ - previousItem, - }) - const minDepth = getMinDepth({ nextItem }) - let depth = projectedDepth + previousItem + }); + const minDepth = getMinDepth({ nextItem }); + let depth = projectedDepth; if (projectedDepth >= maxDepth) { - depth = maxDepth + depth = maxDepth; } else if (projectedDepth < minDepth) { - depth = minDepth + depth = minDepth; } - return { depth, maxDepth, minDepth, parentId: getParentId() } + return { depth, maxDepth, minDepth, parentId: getParentId() }; function getParentId() { if (depth === 0 || !previousItem) { - return null + return null; } if (depth === previousItem.depth) { - return previousItem.parentId + return previousItem.parentId; } if (depth > previousItem.depth) { - return previousItem.id + return previousItem.id; } const newParent = newItems .slice(0, overItemIndex) .reverse() - .find((item) => item.depth === depth)?.parentId + .find(item => item.depth === depth)?.parentId; - return newParent ?? null + return newParent ?? null; } } function getMaxDepth({ previousItem }: { previousItem: FlattenedItem }) { if (previousItem) { - return previousItem.depth + 1 + return previousItem.depth + 1; } - return 0 + return 0; } function getMinDepth({ nextItem }: { nextItem: FlattenedItem }) { if (nextItem) { - return nextItem.depth + return nextItem.depth; } - return 0 + return 0; } function flatten( @@ -83,97 +83,85 @@ function flatten( childrenProp: string ): FlattenedItem[] { return items.reduce((acc, item, index) => { - const children = (item[childrenProp] || []) as T[] + const children = (item[childrenProp] || []) as T[]; return [ ...acc, { ...item, parentId, depth, index }, - ...flatten(children, item.id, depth + 1, childrenProp), - ] - }, []) + ...flatten(children, item.id, depth + 1, childrenProp) + ]; + }, []); } -export function flattenTree( - items: T[], - childrenProp: string -): FlattenedItem[] { - return flatten(items, undefined, undefined, childrenProp) +export function flattenTree(items: T[], childrenProp: string): FlattenedItem[] { + return flatten(items, undefined, undefined, childrenProp); } type ItemUpdate = { - id: UniqueIdentifier - parentId: UniqueIdentifier | null - index: number -} + id: UniqueIdentifier; + parentId: UniqueIdentifier | null; + index: number; +}; export function buildTree( flattenedItems: FlattenedItem[], newIndex: number, childrenProp: string ): { items: T[]; update: ItemUpdate } { - const root = { id: "root", [childrenProp]: [] } as T - const nodes: Record = { [root.id]: root } - const items = flattenedItems.map((item) => ({ ...item, [childrenProp]: [] })) + const root = { id: 'root', [childrenProp]: [] } as T; + const nodes: Record = { [root.id]: root }; + const items = flattenedItems.map(item => ({ ...item, [childrenProp]: [] })); let update: { - id: UniqueIdentifier | null - parentId: UniqueIdentifier | null - index: number + id: UniqueIdentifier | null; + parentId: UniqueIdentifier | null; + index: number; } = { id: null, parentId: null, - index: 0, - } + index: 0 + }; items.forEach((item, index) => { - const { - id, - index: _index, - depth: _depth, - parentId: _parentId, - ...rest - } = item - const children = (item[childrenProp] || []) as T[] - - const parentId = _parentId ?? root.id - const parent = nodes[parentId] ?? findItem(items, parentId) - - nodes[id] = { id, [childrenProp]: children } as T - ;(parent[childrenProp] as T[]).push({ + const { id, index: _index, depth: _depth, parentId: _parentId, ...rest } = item; + const children = (item[childrenProp] || []) as T[]; + + const parentId = _parentId ?? root.id; + const parent = nodes[parentId] ?? findItem(items, parentId); + + nodes[id] = { id, [childrenProp]: children } as T; + (parent[childrenProp] as T[]).push({ id, ...rest, - [childrenProp]: children, - } as T) + [childrenProp]: children + } as T); /** * Get the information for them item that was moved to the `newIndex`. */ if (index === newIndex) { - const parentChildren = parent[childrenProp] as FlattenedItem[] + const parentChildren = parent[childrenProp] as FlattenedItem[]; update = { id: item.id, - parentId: parent.id === "root" ? null : parent.id, - index: parentChildren.length - 1, - } + parentId: parent.id === 'root' ? null : parent.id, + index: parentChildren.length - 1 + }; } - }) + }); if (!update.id) { - throw new Error("Could not find item") + throw new Error('Could not find item'); } return { items: root[childrenProp] as T[], - update: update as ItemUpdate, - } + update: update as ItemUpdate + }; } -export function findItem( - items: T[], - itemId: UniqueIdentifier -) { - return items.find(({ id }) => id === itemId) +export function findItem(items: T[], itemId: UniqueIdentifier) { + return items.find(({ id }) => id === itemId); } export function findItemDeep( @@ -182,23 +170,23 @@ export function findItemDeep( childrenProp: string ): TreeItem | undefined { for (const item of items) { - const { id } = item - const children = (item[childrenProp] || []) as T[] + const { id } = item; + const children = (item[childrenProp] || []) as T[]; if (id === itemId) { - return item + return item; } if (children.length) { - const child = findItemDeep(children, itemId, childrenProp) + const child = findItemDeep(children, itemId, childrenProp); if (child) { - return child + return child; } } } - return undefined + return undefined; } export function setProperty( @@ -208,47 +196,37 @@ export function setProperty( childrenProp: keyof TItem, // Make childrenProp a key of TItem setter: (value: TItem[T]) => TItem[T] ): TItem[] { - return items.map((item) => { + return items.map(item => { if (item.id === id) { return { ...item, - [property]: setter(item[property]), - } + [property]: setter(item[property]) + }; } - const children = item[childrenProp] as TItem[] | undefined + const children = item[childrenProp] as TItem[] | undefined; if (children && children.length) { return { ...item, - [childrenProp]: setProperty( - children, - id, - property, - childrenProp, - setter - ), - } as TItem // Explicitly cast to TItem + [childrenProp]: setProperty(children, id, property, childrenProp, setter) + } as TItem; // Explicitly cast to TItem } - return item - }) + return item; + }); } -function countChildren( - items: T[], - count = 0, - childrenProp: string -): number { +function countChildren(items: T[], count = 0, childrenProp: string): number { return items.reduce((acc, item) => { - const children = (item[childrenProp] || []) as T[] + const children = (item[childrenProp] || []) as T[]; if (children.length) { - return countChildren(children, acc + 1, childrenProp) + return countChildren(children, acc + 1, childrenProp); } - return acc + 1 - }, count) + return acc + 1; + }, count); } export function getChildCount( @@ -256,11 +234,11 @@ export function getChildCount( id: UniqueIdentifier, childrenProp: string ) { - const item = findItemDeep(items, id, childrenProp) + const item = findItemDeep(items, id, childrenProp); - const children = (item?.[childrenProp] || []) as T[] + const children = (item?.[childrenProp] || []) as T[]; - return item ? countChildren(children, 0, childrenProp) : 0 + return item ? countChildren(children, 0, childrenProp) : 0; } export function removeChildrenOf( @@ -268,32 +246,30 @@ export function removeChildrenOf( ids: UniqueIdentifier[], childrenProp: string ) { - const excludeParentIds = [...ids] + const excludeParentIds = [...ids]; - return items.filter((item) => { + return items.filter(item => { if (item.parentId && excludeParentIds.includes(item.parentId)) { - const children = (item[childrenProp] || []) as FlattenedItem[] + const children = (item[childrenProp] || []) as FlattenedItem[]; if (children.length) { - excludeParentIds.push(item.id) + excludeParentIds.push(item.id); } - return false + + return false; } - return true - }) + return true; + }); } -export function listItemsWithChildren( - items: T[], - childrenProp: string -): T[] { - return items.map((item) => { +export function listItemsWithChildren(items: T[], childrenProp: string): T[] { + return items.map(item => { return { ...item, [childrenProp]: item[childrenProp] ? listItemsWithChildren(item[childrenProp] as TreeItem[], childrenProp) - : [], - } - }) + : [] + }; + }); } diff --git a/src/components/common/switch-box/index.ts b/src/components/common/switch-box/index.ts index c245e8b7..fd12bcca 100644 --- a/src/components/common/switch-box/index.ts +++ b/src/components/common/switch-box/index.ts @@ -1 +1 @@ -export * from "./switch-box" +export * from './switch-box'; diff --git a/src/components/common/switch-box/switch-box.tsx b/src/components/common/switch-box/switch-box.tsx index c03bfd6e..b7535838 100644 --- a/src/components/common/switch-box/switch-box.tsx +++ b/src/components/common/switch-box/switch-box.tsx @@ -1,28 +1,28 @@ -import { Switch } from "@medusajs/ui" -import { ReactNode } from "react" -import { ControllerProps, FieldPath, FieldValues } from "react-hook-form" +import type { ReactNode } from 'react'; -import { Form } from "../../common/form" +import { Form } from '@components/common/form'; +import { Switch } from '@medusajs/ui'; +import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'; interface HeadlessControllerProps< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, -> extends Omit, "render"> {} + TName extends FieldPath = FieldPath +> extends Omit, 'render'> {} interface SwitchBoxProps< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, + TName extends FieldPath = FieldPath > extends HeadlessControllerProps { - label: string - description: string - optional?: boolean - tooltip?: ReactNode + label: string; + description: string; + optional?: boolean; + tooltip?: ReactNode; /** * Callback for performing additional actions when the checked state changes. * This does not intercept the form control, it is only used for injecting side-effects. */ - onCheckedChange?: (checked: boolean) => void - "data-testid"?: string + onCheckedChange?: (checked: boolean) => void; + 'data-testid'?: string; } /** @@ -33,14 +33,14 @@ interface SwitchBoxProps< */ export const SwitchBox = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath, + TName extends FieldPath = FieldPath >({ label, description, optional = false, tooltip, onCheckedChange, - "data-testid": dataTestId, + 'data-testid': dataTestId, ...props }: SwitchBoxProps) => { return ( @@ -49,31 +49,40 @@ export const SwitchBox = < render={({ field: { value, onChange, ...field } }) => { return ( -
    +
    { - onCheckedChange?.(e) - onChange(e) + onCheckedChange={e => { + onCheckedChange?.(e); + onChange(e); }} data-testid={dataTestId ? `${dataTestId}-switch` : undefined} />
    - + {label} - {description} + + {description} +
    - ) + ); }} /> - ) -} + ); +}; diff --git a/src/components/common/tax-badge/index.ts b/src/components/common/tax-badge/index.ts new file mode 100644 index 00000000..ccf8d975 --- /dev/null +++ b/src/components/common/tax-badge/index.ts @@ -0,0 +1 @@ +export * from './tax-badge'; diff --git a/src/components/common/tax-badge/tax-badge.tsx b/src/components/common/tax-badge/tax-badge.tsx index c8ca4295..0f7062fe 100644 --- a/src/components/common/tax-badge/tax-badge.tsx +++ b/src/components/common/tax-badge/tax-badge.tsx @@ -1,30 +1,24 @@ -import { TaxExclusive, TaxInclusive } from "@medusajs/icons" -import { Tooltip } from "@medusajs/ui" -import { useTranslation } from "react-i18next" +import { TaxExclusive, TaxInclusive } from '@medusajs/icons'; +import { Tooltip } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; type IncludesTaxTooltipProps = { - includesTax?: boolean -} + includesTax?: boolean; +}; -export const IncludesTaxTooltip = ({ - includesTax, -}: IncludesTaxTooltipProps) => { - const { t } = useTranslation() +export const IncludesTaxTooltip = ({ includesTax }: IncludesTaxTooltipProps) => { + const { t } = useTranslation(); return ( {includesTax ? ( - + ) : ( - + )} - ) -} + ); +}; diff --git a/src/components/common/thumbnail/index.ts b/src/components/common/thumbnail/index.ts index 2ce6a861..d4ab7a50 100644 --- a/src/components/common/thumbnail/index.ts +++ b/src/components/common/thumbnail/index.ts @@ -1 +1 @@ -export * from "./thumbnail"; +export * from './thumbnail'; diff --git a/src/components/common/thumbnail/thumbnail.tsx b/src/components/common/thumbnail/thumbnail.tsx index 5e32de1c..3337ace4 100644 --- a/src/components/common/thumbnail/thumbnail.tsx +++ b/src/components/common/thumbnail/thumbnail.tsx @@ -1,32 +1,30 @@ -import { Photo } from "@medusajs/icons" -import { clx } from "@medusajs/ui" +import { Photo } from '@medusajs/icons'; +import { clx } from '@medusajs/ui'; type ThumbnailProps = { - src?: string | null - alt?: string - size?: "small" | "base" -} + src?: string | null; + alt?: string; + size?: 'small' | 'base'; +}; -export const Thumbnail = ({ src, alt, size = "base" }: ThumbnailProps) => { - return ( -
    - {src ? ( - {alt} - ) : ( - - )} -
    - ) -} +export const Thumbnail = ({ src, alt, size = 'base' }: ThumbnailProps) => ( +
    + {src ? ( + {alt} + ) : ( + + )} +
    +); diff --git a/src/components/common/user-link/index.ts b/src/components/common/user-link/index.ts index 951235f8..3863b20d 100644 --- a/src/components/common/user-link/index.ts +++ b/src/components/common/user-link/index.ts @@ -1 +1 @@ -export * from "./user-link" +export * from './user-link'; diff --git a/src/components/common/user-link/user-link.tsx b/src/components/common/user-link/user-link.tsx index 239abb8b..01140131 100644 --- a/src/components/common/user-link/user-link.tsx +++ b/src/components/common/user-link/user-link.tsx @@ -1,45 +1,46 @@ -import { Avatar, Text } from "@medusajs/ui" -import { Link } from "react-router-dom" -import { useUser } from "../../../hooks/api/users" +import { useUser } from '@hooks/api'; +import { Avatar, Text } from '@medusajs/ui'; +import { Link } from 'react-router-dom'; type UserLinkProps = { - id: string - first_name?: string | null - last_name?: string | null - email: string - type?: "customer" | "user" -} + id: string; + first_name?: string | null; + last_name?: string | null; + email: string; + type?: 'customer' | 'user'; +}; -export const UserLink = ({ - id, - first_name, - last_name, - email, - type = "user", -}: UserLinkProps) => { - const name = [first_name, last_name].filter(Boolean).join(" ") - const fallback = name ? name.slice(0, 1) : email.slice(0, 1) - const link = type === "user" ? `/settings/users/${id}` : `/customers/${id}` +export const UserLink = ({ id, first_name, last_name, email, type = 'user' }: UserLinkProps) => { + const name = [first_name, last_name].filter(Boolean).join(' '); + const fallback = name ? name.slice(0, 1) : email.slice(0, 1); + const link = type === 'user' ? `/settings/users/${id}` : `/customers/${id}`; return ( - - + + {name || email} - ) -} + ); +}; export const By = ({ id }: { id: string }) => { - const { user } = useUser(id) // todo: extend to support customers + const { user } = useUser(id); // todo: extend to support customers if (!user) { - return null + return null; } - return -} + return ; +}; diff --git a/src/components/data-grid/components/data-grid-boolean-cell.tsx b/src/components/data-grid/components/data-grid-boolean-cell.tsx index f9dad4b9..d75af8c6 100644 --- a/src/components/data-grid/components/data-grid-boolean-cell.tsx +++ b/src/components/data-grid/components/data-grid-boolean-cell.tsx @@ -1,71 +1,72 @@ -import { Checkbox } from "@medusajs/ui" -import { Controller, ControllerRenderProps } from "react-hook-form" +import { useDataGridCell, useDataGridCellError } from '@components/data-grid/hooks'; +import type { DataGridCellProps, InputProps } from '@components/data-grid/types'; +import { useCombinedRefs } from '@hooks/use-combined-refs.tsx'; +import { Checkbox } from '@medusajs/ui'; +import { Controller, type ControllerRenderProps } from 'react-hook-form'; -import { useCombinedRefs } from "../../../hooks/use-combined-refs" -import { useDataGridCell, useDataGridCellError } from "../hooks" -import { DataGridCellProps, InputProps } from "../types" -import { DataGridCellContainer } from "./data-grid-cell-container" +import { DataGridCellContainer } from './data-grid-cell-container'; -export const DataGridBooleanCell = ({ +export const DataGridBooleanCell = ({ context, - disabled, + disabled }: DataGridCellProps & { disabled?: boolean }) => { const { field, control, renderProps } = useDataGridCell({ - context, - }) - const errorProps = useDataGridCellError({ context }) + context + }); + const errorProps = useDataGridCellError({ context }); - const { container, input } = renderProps + const { container, input } = renderProps; return ( { - return ( - - - - ) - }} + render={({ field }) => ( + + + + )} /> - ) -} + ); +}; const Inner = ({ field, inputProps, - disabled, + disabled }: { - field: ControllerRenderProps - inputProps: InputProps - disabled?: boolean + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + field: ControllerRenderProps; + inputProps: InputProps; + disabled?: boolean; }) => { - const { ref, value, onBlur, name, disabled: fieldDisabled } = field - const { - ref: inputRef, - onBlur: onInputBlur, - onChange, - onFocus, - ...attributes - } = inputProps + const { ref, value, onBlur, name, disabled: fieldDisabled } = field; + const { ref: inputRef, onBlur: onInputBlur, onChange, onFocus, ...attributes } = inputProps; - const combinedRefs = useCombinedRefs(ref, inputRef) + const combinedRefs = useCombinedRefs(ref, inputRef); return ( onChange(newValue === true, value)} + onCheckedChange={newValue => onChange(newValue === true, value)} onFocus={onFocus} onBlur={() => { - onBlur() - onInputBlur() + onBlur(); + onInputBlur(); }} ref={combinedRefs} tabIndex={-1} {...attributes} /> - ) -} + ); +}; diff --git a/src/components/data-grid/components/data-grid-cell-container.tsx b/src/components/data-grid/components/data-grid-cell-container.tsx index 6c67b101..68622367 100644 --- a/src/components/data-grid/components/data-grid-cell-container.tsx +++ b/src/components/data-grid/components/data-grid-cell-container.tsx @@ -1,12 +1,13 @@ -import { ErrorMessage } from "@hookform/error-message" -import { ExclamationCircle } from "@medusajs/icons" -import { Tooltip, clx } from "@medusajs/ui" -import { PropsWithChildren } from "react" -import { get } from "react-hook-form" +import type { PropsWithChildren } from 'react'; -import { DataGridCellContainerProps, DataGridErrorRenderProps } from "../types" -import { DataGridRowErrorIndicator } from "./data-grid-row-error-indicator" -import { useDataGridContext } from "../context" +import { ErrorMessage } from '@hookform/error-message'; +import { ExclamationCircle } from '@medusajs/icons'; +import { clx, Tooltip } from '@medusajs/ui'; +import { get } from 'react-hook-form'; + +import { useDataGridContext } from '../context'; +import type { DataGridCellContainerProps, DataGridErrorRenderProps } from '../types'; +import { DataGridRowErrorIndicator } from './data-grid-row-error-indicator'; export const DataGridCellContainer = ({ isAnchor, @@ -20,29 +21,29 @@ export const DataGridCellContainer = ({ children, errors, rowErrors, - outerComponent, + outerComponent }: DataGridCellContainerProps & DataGridErrorRenderProps) => { - const context = useDataGridContext() - const error = get(errors, field) - const hasError = !!error - - const fieldName = field.replace(/\./g, '-').replace(/\[|\]/g, '') - const dataTestId = context.dataTestId - ? `${context.dataTestId}-cell-${fieldName}` - : undefined + const context = useDataGridContext(); + const error = get(errors, field); + const hasError = !!error; + + const fieldName = field.replace(/\./g, '-').replace(/\[|\]/g, ''); + const dataTestId = context.dataTestId ? `${context.dataTestId}-cell-${fieldName}` : undefined; return ( -
    +
    { - return ( -
    - - - -
    - ) - }} + render={({ message }) => ( +
    + + + +
    + )} />
    - + {children}
    @@ -78,19 +83,17 @@ export const DataGridCellContainer = ({
    {outerComponent}
    - ) -} + ); +}; const RenderChildren = ({ isAnchor, placeholder, - children, -}: PropsWithChildren< - Pick ->) => { + children +}: PropsWithChildren>) => { if (!isAnchor && placeholder) { - return placeholder + return placeholder; } - return children -} + return children; +}; diff --git a/src/components/data-grid/components/data-grid-currency-cell.tsx b/src/components/data-grid/components/data-grid-currency-cell.tsx index 2c50b5df..7eb116a1 100644 --- a/src/components/data-grid/components/data-grid-currency-cell.tsx +++ b/src/components/data-grid/components/data-grid-currency-cell.tsx @@ -1,116 +1,115 @@ -import CurrencyInput, { - CurrencyInputProps, - formatValue, -} from "react-currency-input-field" -import { Controller, ControllerRenderProps } from "react-hook-form" - -import { useCallback, useEffect, useState } from "react" -import { useCombinedRefs } from "../../../hooks/use-combined-refs" -import { CurrencyInfo, currencies } from "../../../lib/data/currencies" -import { useDataGridCell, useDataGridCellError } from "../hooks" -import { DataGridCellProps, InputProps } from "../types" -import { DataGridCellContainer } from "./data-grid-cell-container" - -interface DataGridCurrencyCellProps - extends DataGridCellProps { - code: string +import { useCallback, useEffect, useState } from 'react'; + +import { useDataGridCell, useDataGridCellError } from '@components/data-grid/hooks'; +import type { DataGridCellProps, InputProps } from '@components/data-grid/types'; +import { useCombinedRefs } from '@hooks/use-combined-refs.tsx'; +import { currencies, type CurrencyInfo } from '@lib/data/currencies.ts'; +import CurrencyInput, { formatValue, type CurrencyInputProps } from 'react-currency-input-field'; +import { Controller, type ControllerRenderProps } from 'react-hook-form'; + +import { DataGridCellContainer } from './data-grid-cell-container'; + +//@todo fix type +// eslint-disable-next-line @typescript-eslint/no-explicit-any +interface DataGridCurrencyCellProps extends DataGridCellProps { + code: string; } +//@todo fix type +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const DataGridCurrencyCell = ({ context, - code, + code }: DataGridCurrencyCellProps) => { const { field, control, renderProps } = useDataGridCell({ - context, - }) - const errorProps = useDataGridCellError({ context }) + context + }); + const errorProps = useDataGridCellError({ context }); - const { container, input } = renderProps + const { container, input } = renderProps; - const currency = currencies[code.toUpperCase()] + const currency = currencies[code.toUpperCase()]; return ( { - return ( - - - - ) - }} + render={({ field }) => ( + + + + )} /> - ) -} + ); +}; const Inner = ({ field, inputProps, - currencyInfo, + currencyInfo }: { - field: ControllerRenderProps - inputProps: InputProps - currencyInfo: CurrencyInfo + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + field: ControllerRenderProps; + inputProps: InputProps; + currencyInfo: CurrencyInfo; }) => { - const { value, onChange: _, onBlur, ref, ...rest } = field - const { - ref: inputRef, - onBlur: onInputBlur, - onFocus, - onChange, - ...attributes - } = inputProps + const { value, onBlur, ref, ...rest } = field; + const { ref: inputRef, onBlur: onInputBlur, onFocus, onChange, ...attributes } = inputProps; const formatter = useCallback( (value?: string | number) => { - const ensuredValue = - typeof value === "number" ? value.toString() : value || "" + const ensuredValue = typeof value === 'number' ? value.toString() : value || ''; return formatValue({ value: ensuredValue, decimalScale: currencyInfo.decimal_digits, disableGroupSeparators: true, - decimalSeparator: ".", - }) + decimalSeparator: '.' + }); }, [currencyInfo] - ) + ); - const [localValue, setLocalValue] = useState(value || "") + const [localValue, setLocalValue] = useState(value || ''); - const handleValueChange: CurrencyInputProps["onValueChange"] = ( - value, - _name, - _values - ) => { + const handleValueChange: CurrencyInputProps['onValueChange'] = value => { if (!value) { - setLocalValue("") - return + setLocalValue(''); + + return; } - setLocalValue(value) - } + setLocalValue(value); + }; useEffect(() => { - let update = value + let update = value; // The component we use is a bit fidly when the value is updated externally // so we need to ensure a format that will result in the cell being formatted correctly // according to the users locale on the next render. if (!isNaN(Number(value))) { - update = formatter(update) + update = formatter(update); } - setLocalValue(update) - }, [value, formatter]) + // eslint-disable-next-line react-hooks/set-state-in-effect + setLocalValue(update); + }, [value, formatter]); - const combinedRed = useCombinedRefs(inputRef, ref) + const combinedRed = useCombinedRefs(inputRef, ref); return (
    {currencyInfo.symbol_native} @@ -124,10 +123,10 @@ const Inner = ({ onValueChange={handleValueChange} formatValueOnBlur onBlur={() => { - onBlur() - onInputBlur() + onBlur(); + onInputBlur(); - onChange(localValue, value) + onChange(localValue, value); }} onFocus={onFocus} decimalScale={currencyInfo.decimal_digits} @@ -136,5 +135,5 @@ const Inner = ({ tabIndex={-1} />
    - ) -} + ); +}; diff --git a/src/components/data-grid/components/data-grid-duplicate-cell.tsx b/src/components/data-grid/components/data-grid-duplicate-cell.tsx index 19e8218f..ddf3facc 100644 --- a/src/components/data-grid/components/data-grid-duplicate-cell.tsx +++ b/src/components/data-grid/components/data-grid-duplicate-cell.tsx @@ -1,21 +1,20 @@ -import { ReactNode } from "react" -import { useDataGridDuplicateCell } from "../hooks" +import type { ReactNode } from 'react'; + +import { useDataGridDuplicateCell } from '@components/data-grid/hooks'; interface DataGridDuplicateCellProps { - duplicateOf: string - children?: ReactNode | ((props: { value: TValue }) => ReactNode) + duplicateOf: string; + children?: ReactNode | ((props: { value: TValue }) => ReactNode); } export const DataGridDuplicateCell = ({ duplicateOf, - children, + children }: DataGridDuplicateCellProps) => { - const { watchedValue } = useDataGridDuplicateCell({ duplicateOf }) + const { watchedValue } = useDataGridDuplicateCell({ duplicateOf }); return ( -
    - {typeof children === "function" - ? children({ value: watchedValue }) - : children} +
    + {typeof children === 'function' ? children({ value: watchedValue }) : children}
    - ) -} + ); +}; diff --git a/src/components/data-grid/components/data-grid-keyboard-shortcut-modal.tsx b/src/components/data-grid/components/data-grid-keyboard-shortcut-modal.tsx index a8b5f123..12dea066 100644 --- a/src/components/data-grid/components/data-grid-keyboard-shortcut-modal.tsx +++ b/src/components/data-grid/components/data-grid-keyboard-shortcut-modal.tsx @@ -1,208 +1,208 @@ -import { XMark } from "@medusajs/icons" -import { - Button, - clx, - Heading, - IconButton, - Input, - Kbd, - Text, -} from "@medusajs/ui" -import { Dialog as RadixDialog } from "radix-ui" -import { useMemo, useState } from "react" -import { useTranslation } from "react-i18next" +import { useMemo, useState } from 'react'; + +import { XMark } from '@medusajs/icons'; +import { Button, clx, Heading, IconButton, Input, Kbd, Text } from '@medusajs/ui'; +import { Dialog as RadixDialog } from 'radix-ui'; +import { useTranslation } from 'react-i18next'; const useDataGridShortcuts = () => { - const { t } = useTranslation() + const { t } = useTranslation(); - const shortcuts = useMemo( + return useMemo( () => [ { - label: t("dataGrid.shortcuts.commands.undo"), + label: t('dataGrid.shortcuts.commands.undo'), keys: { - Mac: ["⌘", "Z"], - Windows: ["Ctrl", "Z"], - }, + Mac: ['⌘', 'Z'], + Windows: ['Ctrl', 'Z'] + } }, { - label: t("dataGrid.shortcuts.commands.redo"), + label: t('dataGrid.shortcuts.commands.redo'), keys: { - Mac: ["⇧", "⌘", "Z"], - Windows: ["Shift", "Ctrl", "Z"], - }, + Mac: ['⇧', '⌘', 'Z'], + Windows: ['Shift', 'Ctrl', 'Z'] + } }, { - label: t("dataGrid.shortcuts.commands.copy"), + label: t('dataGrid.shortcuts.commands.copy'), keys: { - Mac: ["⌘", "C"], - Windows: ["Ctrl", "C"], - }, + Mac: ['⌘', 'C'], + Windows: ['Ctrl', 'C'] + } }, { - label: t("dataGrid.shortcuts.commands.paste"), + label: t('dataGrid.shortcuts.commands.paste'), keys: { - Mac: ["⌘", "V"], - Windows: ["Ctrl", "V"], - }, + Mac: ['⌘', 'V'], + Windows: ['Ctrl', 'V'] + } }, { - label: t("dataGrid.shortcuts.commands.edit"), + label: t('dataGrid.shortcuts.commands.edit'), keys: { - Mac: ["↵"], - Windows: ["Enter"], - }, + Mac: ['↵'], + Windows: ['Enter'] + } }, { - label: t("dataGrid.shortcuts.commands.delete"), + label: t('dataGrid.shortcuts.commands.delete'), keys: { - Mac: ["⌫"], - Windows: ["Backspace"], - }, + Mac: ['⌫'], + Windows: ['Backspace'] + } }, { - label: t("dataGrid.shortcuts.commands.clear"), + label: t('dataGrid.shortcuts.commands.clear'), keys: { - Mac: ["Space"], - Windows: ["Space"], - }, + Mac: ['Space'], + Windows: ['Space'] + } }, { - label: t("dataGrid.shortcuts.commands.moveUp"), + label: t('dataGrid.shortcuts.commands.moveUp'), keys: { - Mac: ["↑"], - Windows: ["↑"], - }, + Mac: ['↑'], + Windows: ['↑'] + } }, { - label: t("dataGrid.shortcuts.commands.moveDown"), + label: t('dataGrid.shortcuts.commands.moveDown'), keys: { - Mac: ["↓"], - Windows: ["↓"], - }, + Mac: ['↓'], + Windows: ['↓'] + } }, { - label: t("dataGrid.shortcuts.commands.moveLeft"), + label: t('dataGrid.shortcuts.commands.moveLeft'), keys: { - Mac: ["←"], - Windows: ["←"], - }, + Mac: ['←'], + Windows: ['←'] + } }, { - label: t("dataGrid.shortcuts.commands.moveRight"), + label: t('dataGrid.shortcuts.commands.moveRight'), keys: { - Mac: ["→"], - Windows: ["→"], - }, + Mac: ['→'], + Windows: ['→'] + } }, { - label: t("dataGrid.shortcuts.commands.moveTop"), + label: t('dataGrid.shortcuts.commands.moveTop'), keys: { - Mac: ["⌘", "↑"], - Windows: ["Ctrl", "↑"], - }, + Mac: ['⌘', '↑'], + Windows: ['Ctrl', '↑'] + } }, { - label: t("dataGrid.shortcuts.commands.moveBottom"), + label: t('dataGrid.shortcuts.commands.moveBottom'), keys: { - Mac: ["⌘", "↓"], - Windows: ["Ctrl", "↓"], - }, + Mac: ['⌘', '↓'], + Windows: ['Ctrl', '↓'] + } }, { - label: t("dataGrid.shortcuts.commands.selectDown"), + label: t('dataGrid.shortcuts.commands.selectDown'), keys: { - Mac: ["⇧", "↓"], - Windows: ["Shift", "↓"], - }, + Mac: ['⇧', '↓'], + Windows: ['Shift', '↓'] + } }, { - label: t("dataGrid.shortcuts.commands.selectUp"), + label: t('dataGrid.shortcuts.commands.selectUp'), keys: { - Mac: ["⇧", "↑"], - Windows: ["Shift", "↑"], - }, + Mac: ['⇧', '↑'], + Windows: ['Shift', '↑'] + } }, { - label: t("dataGrid.shortcuts.commands.selectColumnDown"), + label: t('dataGrid.shortcuts.commands.selectColumnDown'), keys: { - Mac: ["⇧", "⌘", "↓"], - Windows: ["Shift", "Ctrl", "↓"], - }, + Mac: ['⇧', '⌘', '↓'], + Windows: ['Shift', 'Ctrl', '↓'] + } }, { - label: t("dataGrid.shortcuts.commands.selectColumnUp"), + label: t('dataGrid.shortcuts.commands.selectColumnUp'), keys: { - Mac: ["⇧", "⌘", "↑"], - Windows: ["Shift", "Ctrl", "↑"], - }, + Mac: ['⇧', '⌘', '↑'], + Windows: ['Shift', 'Ctrl', '↑'] + } }, { - label: t("dataGrid.shortcuts.commands.focusToolbar"), + label: t('dataGrid.shortcuts.commands.focusToolbar'), keys: { - Mac: ["⌃", "⌥", ","], - Windows: ["Ctrl", "Alt", ","], - }, + Mac: ['⌃', '⌥', ','], + Windows: ['Ctrl', 'Alt', ','] + } }, { - label: t("dataGrid.shortcuts.commands.focusCancel"), + label: t('dataGrid.shortcuts.commands.focusCancel'), keys: { - Mac: ["⌃", "⌥", "."], - Windows: ["Ctrl", "Alt", "."], - }, - }, + Mac: ['⌃', '⌥', '.'], + Windows: ['Ctrl', 'Alt', '.'] + } + } ], [t] - ) - - return shortcuts -} + ); +}; type DataGridKeyboardShortcutModalProps = { - open: boolean - onOpenChange: (open: boolean) => void -} + open: boolean; + onOpenChange: (open: boolean) => void; +}; export const DataGridKeyboardShortcutModal = ({ open, - onOpenChange, + onOpenChange }: DataGridKeyboardShortcutModalProps) => { - const { t } = useTranslation() - const [searchValue, onSearchValueChange] = useState("") - const shortcuts = useDataGridShortcuts() + const { t } = useTranslation(); + const [searchValue, onSearchValueChange] = useState(''); + const shortcuts = useDataGridShortcuts(); const searchResults = useMemo(() => { - return shortcuts.filter((shortcut) => + return shortcuts.filter(shortcut => shortcut.label.toLowerCase().includes(searchValue.toLowerCase()) - ) - }, [searchValue, shortcuts]) + ); + }, [searchValue, shortcuts]); return ( - + - - +
    - {t("app.menus.user.shortcuts")} + {t('app.menus.user.shortcuts')}
    esc - + @@ -213,7 +213,7 @@ export const DataGridKeyboardShortcutModal = ({ type="search" value={searchValue} autoFocus - onChange={(e) => onSearchValueChange(e.target.value)} + onChange={e => onSearchValueChange(e.target.value)} />
    @@ -222,24 +222,27 @@ export const DataGridKeyboardShortcutModal = ({ return (
    {shortcut.label}
    {shortcut.keys.Mac?.map((key, index) => { return ( -
    +
    {key}
    - ) + ); })}
    - ) + ); })}
    - ) -} + ); +}; diff --git a/src/components/data-grid/components/data-grid-number-cell.tsx b/src/components/data-grid/components/data-grid-number-cell.tsx index 86d1f6ab..41855d8a 100644 --- a/src/components/data-grid/components/data-grid-number-cell.tsx +++ b/src/components/data-grid/components/data-grid-number-cell.tsx @@ -1,88 +1,93 @@ -import { clx } from "@medusajs/ui" -import { useEffect, useState } from "react" -import { Controller, ControllerRenderProps } from "react-hook-form" -import { useCombinedRefs } from "../../../hooks/use-combined-refs" -import { useDataGridCell, useDataGridCellError } from "../hooks" -import { DataGridCellProps, InputProps } from "../types" -import { DataGridCellContainer } from "./data-grid-cell-container" +import { useEffect, useState } from 'react'; +import { useDataGridCell, useDataGridCellError } from '@components/data-grid/hooks'; +import type { DataGridCellProps, InputProps } from '@components/data-grid/types'; +import { useCombinedRefs } from '@hooks/use-combined-refs.tsx'; +import { clx } from '@medusajs/ui'; +import { Controller, type ControllerRenderProps } from 'react-hook-form'; + +import { DataGridCellContainer } from './data-grid-cell-container'; + +//@todo fix type +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const DataGridNumberCell = ({ context, ...rest }: DataGridCellProps & { - min?: number - max?: number - placeholder?: string + min?: number; + max?: number; + placeholder?: string; }) => { const { field, control, renderProps } = useDataGridCell({ - context, - }) - const errorProps = useDataGridCellError({ context }) + context + }); + const errorProps = useDataGridCellError({ context }); - const { container, input } = renderProps + const { container, input } = renderProps; return ( { - return ( - - - - ) - }} + render={({ field }) => ( + + + + )} /> - ) -} + ); +}; const Inner = ({ field, inputProps, ...props }: { - field: ControllerRenderProps - inputProps: InputProps - min?: number - max?: number - placeholder?: string + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + field: ControllerRenderProps; + inputProps: InputProps; + min?: number; + max?: number; + placeholder?: string; }) => { - const { ref, value, onChange: _, onBlur, ...fieldProps } = field - const { - ref: inputRef, - onChange, - onBlur: onInputBlur, - onFocus, - ...attributes - } = inputProps + const { ref, value, onChange: _, onBlur, ...fieldProps } = field; + const { ref: inputRef, onChange, onBlur: onInputBlur, onFocus, ...attributes } = inputProps; - const [localValue, setLocalValue] = useState(value) + const [localValue, setLocalValue] = useState(value); useEffect(() => { - setLocalValue(value) - }, [value]) + setLocalValue(value); + }, [value]); - const combinedRefs = useCombinedRefs(inputRef, ref) + const combinedRefs = useCombinedRefs(inputRef, ref); return (
    setLocalValue(e.target.value)} + onChange={e => setLocalValue(e.target.value)} onBlur={() => { - onBlur() - onInputBlur() + onBlur(); + onInputBlur(); // We propagate the change to the field only when the input is blurred - onChange(localValue, value) + onChange(localValue, value); }} onFocus={onFocus} type="number" inputMode="decimal" className={clx( - "txt-compact-small size-full bg-transparent outline-none", - "placeholder:text-ui-fg-muted" + 'txt-compact-small size-full bg-transparent outline-none', + 'placeholder:text-ui-fg-muted' )} tabIndex={-1} {...props} @@ -90,5 +95,5 @@ const Inner = ({ {...attributes} />
    - ) -} + ); +}; diff --git a/src/components/data-grid/components/data-grid-readonly-cell.tsx b/src/components/data-grid/components/data-grid-readonly-cell.tsx index cf842684..b5c004b6 100644 --- a/src/components/data-grid/components/data-grid-readonly-cell.tsx +++ b/src/components/data-grid/components/data-grid-readonly-cell.tsx @@ -1,33 +1,38 @@ -import { PropsWithChildren } from "react" +import type { PropsWithChildren } from 'react'; -import { clx } from "@medusajs/ui" -import { useDataGridCellError } from "../hooks" -import { DataGridCellProps } from "../types" -import { DataGridRowErrorIndicator } from "./data-grid-row-error-indicator" +import { useDataGridCellError } from '@components/data-grid/hooks'; +import type { DataGridCellProps } from '@components/data-grid/types'; +import { clx } from '@medusajs/ui'; +import { DataGridRowErrorIndicator } from './data-grid-row-error-indicator'; + +//@todo fix type +// eslint-disable-next-line @typescript-eslint/no-explicit-any type DataGridReadonlyCellProps = PropsWithChildren< DataGridCellProps > & { - color?: "muted" | "normal" -} + color?: 'muted' | 'normal'; +}; +//@todo fix type +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const DataGridReadonlyCell = ({ context, - color = "muted", - children, + color = 'muted', + children }: DataGridReadonlyCellProps) => { - const { rowErrors } = useDataGridCellError({ context }) + const { rowErrors } = useDataGridCellError({ context }); return (
    {children}
    - ) -} + ); +}; diff --git a/src/components/data-grid/components/data-grid-root.tsx b/src/components/data-grid/components/data-grid-root.tsx index e1c23e11..002797c7 100644 --- a/src/components/data-grid/components/data-grid-root.tsx +++ b/src/components/data-grid/components/data-grid-root.tsx @@ -1,32 +1,32 @@ import React, { - CSSProperties, - ReactNode, useCallback, useEffect, useMemo, useRef, - useState + useState, + type CSSProperties, + type ReactNode } from 'react'; +import { useCommandHistory } from '@hooks/use-command-history'; +import { useDocumentDirection } from '@hooks/use-document-direction'; import { Adjustments, AdjustmentsDone, ExclamationCircle } from '@medusajs/icons'; import { Button, clx, DropdownMenu } from '@medusajs/ui'; import { - Cell, - CellContext, - Column, - ColumnDef, flexRender, getCoreRowModel, - Row, useReactTable, - VisibilityState + type Cell, + type CellContext, + type Column, + type ColumnDef, + type Row, + type VisibilityState } from '@tanstack/react-table'; -import { useVirtualizer, VirtualItem } from '@tanstack/react-virtual'; -import { FieldValues, UseFormReturn } from 'react-hook-form'; +import { useVirtualizer, type VirtualItem } from '@tanstack/react-virtual'; +import type { FieldValues, UseFormReturn } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { useCommandHistory } from '../../../hooks/use-command-history'; -import { useDocumentDirection } from '../../../hooks/use-document-direction'; import { ConditionalTooltip } from '../../common/conditional-tooltip'; import { DataGridContext, useDataGridContext } from '../context'; import { @@ -43,7 +43,7 @@ import { useDataGridQueryTool } from '../hooks'; import { DataGridMatrix } from '../models'; -import { DataGridCoordinates, GridColumnOption } from '../types'; +import type { DataGridCoordinates, GridColumnOption } from '../types'; import { isCellMatch, isSpecialFocusKey } from '../utils'; import { DataGridKeyboardShortcutModal } from './data-grid-keyboard-shortcut-modal'; @@ -403,6 +403,7 @@ export const DataGridRoot = { if (isSpecialFocusKey(e)) { handleSpecialFocusKeys(e); + return; } }; @@ -676,6 +677,7 @@ const DataGridHeader = ({ onHeaderInteractionChange(value); setColumnsOpen(value); }; + return (
    diff --git a/src/components/data-grid/components/data-grid-row-error-indicator.tsx b/src/components/data-grid/components/data-grid-row-error-indicator.tsx index 05315110..b756f28a 100644 --- a/src/components/data-grid/components/data-grid-row-error-indicator.tsx +++ b/src/components/data-grid/components/data-grid-row-error-indicator.tsx @@ -1,18 +1,16 @@ -import { Badge, Tooltip } from "@medusajs/ui" -import { useTranslation } from "react-i18next" -import { DataGridRowError } from "../types" +import type { DataGridRowError } from '@components/data-grid/types'; +import { Badge, Tooltip } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; type DataGridRowErrorIndicatorProps = { - rowErrors: DataGridRowError[] -} + rowErrors: DataGridRowError[]; +}; -export const DataGridRowErrorIndicator = ({ - rowErrors, -}: DataGridRowErrorIndicatorProps) => { - const rowErrorCount = rowErrors ? rowErrors.length : 0 +export const DataGridRowErrorIndicator = ({ rowErrors }: DataGridRowErrorIndicatorProps) => { + const rowErrorCount = rowErrors ? rowErrors.length : 0; if (!rowErrors || rowErrorCount <= 0) { - return null + return null; } return ( @@ -20,25 +18,28 @@ export const DataGridRowErrorIndicator = ({ content={
      {rowErrors.map((error, index) => ( - + ))}
    } delayDuration={0} > - + {rowErrorCount} - ) -} + ); +}; -const DataGridRowErrorLine = ({ - error, -}: { - error: { message: string; to: () => void } -}) => { - const { t } = useTranslation() +const DataGridRowErrorLine = ({ error }: { error: { message: string; to: () => void } }) => { + const { t } = useTranslation(); return (
  • @@ -46,10 +47,10 @@ const DataGridRowErrorLine = ({
  • - ) -} + ); +}; diff --git a/src/components/data-grid/components/data-grid-skeleton.tsx b/src/components/data-grid/components/data-grid-skeleton.tsx index a54f203c..771303ba 100644 --- a/src/components/data-grid/components/data-grid-skeleton.tsx +++ b/src/components/data-grid/components/data-grid-skeleton.tsx @@ -1,41 +1,39 @@ -import { ColumnDef } from "@tanstack/react-table" -import { Skeleton } from "../../common/skeleton" +import { Skeleton } from '@components/common/skeleton'; +import type { ColumnDef } from '@tanstack/react-table'; type DataGridSkeletonProps = { - columns: ColumnDef[] - rows?: number -} + columns: ColumnDef[]; + rows?: number; +}; export const DataGridSkeleton = ({ columns, - rows: rowCount = 10, + rows: rowCount = 10 }: DataGridSkeletonProps) => { - const rows = Array.from({ length: rowCount }, (_, i) => i) + const rows = Array.from({ length: rowCount }, (_, i) => i); - const colCount = columns.length + const colCount = columns.length; return ( -
    -
    -
    +
    +
    +
    -
    +
    - {columns.map((_col, i) => { - return ( -
    - -
    - ) - })} + {columns.map((_col, i) => ( +
    + +
    + ))}
    {rows.map((_, j) => ( @@ -44,20 +42,18 @@ export const DataGridSkeleton = ({ style={{ gridTemplateColumns: `repeat(${colCount}, 1fr)` }} key={j} > - {columns.map((_col, k) => { - return ( -
    - -
    - ) - })} + {columns.map((_col, k) => ( +
    + +
    + ))}
    ))}
    - ) -} + ); +}; diff --git a/src/components/data-grid/components/data-grid-text-cell.tsx b/src/components/data-grid/components/data-grid-text-cell.tsx index 3b1fb279..d535f6e5 100644 --- a/src/components/data-grid/components/data-grid-text-cell.tsx +++ b/src/components/data-grid/components/data-grid-text-cell.tsx @@ -1,75 +1,83 @@ -import { clx } from "@medusajs/ui" -import { useEffect, useState } from "react" -import { Controller, ControllerRenderProps } from "react-hook-form" +import { useEffect, useState } from 'react'; -import { useCombinedRefs } from "../../../hooks/use-combined-refs" -import { useDataGridCell, useDataGridCellError } from "../hooks" -import { DataGridCellProps, InputProps } from "../types" -import { DataGridCellContainer } from "./data-grid-cell-container" +import { useDataGridCell, useDataGridCellError } from '@components/data-grid/hooks'; +import type { DataGridCellProps, InputProps } from '@components/data-grid/types'; +import { useCombinedRefs } from '@hooks/use-combined-refs.tsx'; +import { clx } from '@medusajs/ui'; +import { Controller, type ControllerRenderProps } from 'react-hook-form'; -export const DataGridTextCell = ({ - context, +import { DataGridCellContainer } from './data-grid-cell-container'; + +export const DataGridTextCell = ({ + context }: DataGridCellProps) => { const { field, control, renderProps } = useDataGridCell({ - context, - }) - const errorProps = useDataGridCellError({ context }) + context + }); + const errorProps = useDataGridCellError({ context }); - const { container, input } = renderProps + const { container, input } = renderProps; return ( { - return ( - - - - ) - }} + render={({ field }) => ( + + + + )} /> - ) -} + ); +}; const Inner = ({ field, - inputProps, + inputProps }: { - field: ControllerRenderProps - inputProps: InputProps + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + field: ControllerRenderProps; + inputProps: InputProps; }) => { - const { onChange: _, onBlur, ref, value, ...rest } = field - const { ref: inputRef, onBlur: onInputBlur, onChange, ...input } = inputProps + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { onChange: _, onBlur, ref, value, ...rest } = field; + const { ref: inputRef, onBlur: onInputBlur, onChange, ...input } = inputProps; - const [localValue, setLocalValue] = useState(value) + const [localValue, setLocalValue] = useState(value); useEffect(() => { - setLocalValue(value) - }, [value]) + setLocalValue(value); + }, [value]); - const combinedRefs = useCombinedRefs(inputRef, ref) + const combinedRefs = useCombinedRefs(inputRef, ref); return ( setLocalValue(e.target.value)} + onChange={e => setLocalValue(e.target.value)} ref={combinedRefs} onBlur={() => { - onBlur() - onInputBlur() + onBlur(); + onInputBlur(); // We propagate the change to the field only when the input is blurred - onChange(localValue, value) + onChange(localValue, value); }} {...input} {...rest} /> - ) -} + ); +}; diff --git a/src/components/data-grid/components/data-grid-toggleable-number-cell.tsx b/src/components/data-grid/components/data-grid-toggleable-number-cell.tsx index aac12fa3..c9930abf 100644 --- a/src/components/data-grid/components/data-grid-toggleable-number-cell.tsx +++ b/src/components/data-grid/components/data-grid-toggleable-number-cell.tsx @@ -1,103 +1,112 @@ -import { Switch } from "@medusajs/ui" -import { useEffect, useRef, useState } from "react" -import CurrencyInput, { CurrencyInputProps } from "react-currency-input-field" -import { Controller, ControllerRenderProps } from "react-hook-form" -import { useCombinedRefs } from "../../../hooks/use-combined-refs" -import { ConditionalTooltip } from "../../common/conditional-tooltip" -import { useDataGridCell, useDataGridCellError } from "../hooks" -import { DataGridCellProps, InputProps } from "../types" -import { DataGridCellContainer } from "./data-grid-cell-container" +import { useEffect, useRef, useState } from 'react'; +import { ConditionalTooltip } from '@components/common/conditional-tooltip'; +import { useDataGridCell, useDataGridCellError } from '@components/data-grid/hooks'; +import type { DataGridCellProps, InputProps } from '@components/data-grid/types'; +import { useCombinedRefs } from '@hooks/use-combined-refs.tsx'; +import { Switch } from '@medusajs/ui'; +import CurrencyInput, { type CurrencyInputProps } from 'react-currency-input-field'; +import { Controller, type ControllerRenderProps } from 'react-hook-form'; + +import { DataGridCellContainer } from './data-grid-cell-container'; + +//@todo fix type +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const DataGridTogglableNumberCell = ({ context, disabledToggleTooltip, ...rest }: DataGridCellProps & { - min?: number - max?: number - placeholder?: string - disabledToggleTooltip?: string + min?: number; + max?: number; + placeholder?: string; + disabledToggleTooltip?: string; }) => { const { field, control, renderProps } = useDataGridCell({ - context, - }) - const errorProps = useDataGridCellError({ context }) + context + }); + const errorProps = useDataGridCellError({ context }); - const { container, input } = renderProps + const { container, input } = renderProps; return ( { - return ( - - } - > - - - ) - }} + render={({ field }) => ( + + } + > + + + )} /> - ) -} + ); +}; const OuterComponent = ({ field, inputProps, isAnchor, - tooltip, + tooltip }: { - field: ControllerRenderProps - inputProps: InputProps - isAnchor: boolean - tooltip?: string + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + field: ControllerRenderProps; + inputProps: InputProps; + isAnchor: boolean; + tooltip?: string; }) => { - const buttonRef = useRef(null) - const { value } = field - const { onChange } = inputProps + const buttonRef = useRef(null); + const { value } = field; + const { onChange } = inputProps; - const [localValue, setLocalValue] = useState(value) + const [localValue, setLocalValue] = useState(value); useEffect(() => { - setLocalValue(value) - }, [value]) + setLocalValue(value); + }, [value]); const handleCheckedChange = (update: boolean) => { - const newValue = { ...localValue, checked: update } + const newValue = { ...localValue, checked: update }; if (!update && !newValue.disabledToggle) { - newValue.quantity = "" + newValue.quantity = ''; } - if (update && newValue.quantity === "") { - newValue.quantity = 0 + if (update && newValue.quantity === '') { + newValue.quantity = 0; } - setLocalValue(newValue) - onChange(newValue, value) - } + setLocalValue(newValue); + onChange(newValue, value); + }; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (isAnchor && e.key.toLowerCase() === "x") { - e.preventDefault() - buttonRef.current?.click() + if (isAnchor && e.key.toLowerCase() === 'x') { + e.preventDefault(); + buttonRef.current?.click(); } - } + }; + + document.addEventListener('keydown', handleKeyDown); - document.addEventListener("keydown", handleKeyDown) - return () => document.removeEventListener("keydown", handleKeyDown) - }, [isAnchor]) + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isAnchor]); return (
    - ) -} + ); +}; const Inner = ({ field, @@ -125,36 +134,29 @@ const Inner = ({ placeholder, ...props }: { - field: ControllerRenderProps - inputProps: InputProps - min?: number - max?: number - placeholder?: string + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + field: ControllerRenderProps; + inputProps: InputProps; + min?: number; + max?: number; + placeholder?: string; }) => { - const { ref, value, onChange: _, onBlur, ...fieldProps } = field - const { - ref: inputRef, - onChange, - onBlur: onInputBlur, - onFocus, - ...attributes - } = inputProps + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { ref, value, onChange: _, onBlur, ...fieldProps } = field; + const { ref: inputRef, onChange, onBlur: onInputBlur, onFocus, ...attributes } = inputProps; - const [localValue, setLocalValue] = useState(value) + const [localValue, setLocalValue] = useState(value); useEffect(() => { - setLocalValue(value) - }, [value]) + setLocalValue(value); + }, [value]); - const combinedRefs = useCombinedRefs(inputRef, ref) + const combinedRefs = useCombinedRefs(inputRef, ref); - const handleInputChange: CurrencyInputProps["onValueChange"] = ( - updatedValue, - _name, - _values - ) => { - const ensuredValue = updatedValue !== undefined ? updatedValue : "" - const newValue = { ...localValue, quantity: ensuredValue } + const handleInputChange: CurrencyInputProps['onValueChange'] = updatedValue => { + const ensuredValue = updatedValue !== undefined ? updatedValue : ''; + const newValue = { ...localValue, quantity: ensuredValue }; /** * If the value is not empty, then the location should be enabled. @@ -162,22 +164,24 @@ const Inner = ({ * Else, if the value is empty and the location is enabled, then the * location should be disabled, unless toggling the location is disabled. */ - if (ensuredValue !== "") { - newValue.checked = true + if (ensuredValue !== '') { + newValue.checked = true; } else if (newValue.checked && newValue.disabledToggle === false) { - newValue.checked = false + newValue.checked = false; } - setLocalValue(newValue) - } + setLocalValue(newValue); + }; const handleOnChange = () => { - if (localValue.disabledToggle && localValue.quantity === "") { - localValue.quantity = 0 + if (localValue.disabledToggle && localValue.quantity === '') { + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any,react-hooks/immutability + localValue.quantity = 0; } - onChange(localValue, value) - } + onChange(localValue, value); + }; return (
    @@ -191,9 +195,9 @@ const Inner = ({ onValueChange={handleInputChange} formatValueOnBlur onBlur={() => { - onBlur() - onInputBlur() - handleOnChange() + onBlur(); + onInputBlur(); + handleOnChange(); }} onFocus={onFocus} decimalsLimit={0} @@ -202,5 +206,5 @@ const Inner = ({ placeholder={!localValue.checked ? placeholder : undefined} />
    - ) -} + ); +}; diff --git a/src/components/data-grid/components/index.ts b/src/components/data-grid/components/index.ts index 3968df05..b520901e 100644 --- a/src/components/data-grid/components/index.ts +++ b/src/components/data-grid/components/index.ts @@ -1,7 +1,8 @@ -export { DataGridBooleanCell } from "./data-grid-boolean-cell" -export { DataGridCurrencyCell } from "./data-grid-currency-cell" -export { DataGridNumberCell } from "./data-grid-number-cell" -export { DataGridReadonlyCell as DataGridReadOnlyCell } from "./data-grid-readonly-cell" -export { DataGridRoot, type DataGridRootProps } from "./data-grid-root" -export { DataGridSkeleton } from "./data-grid-skeleton" -export { DataGridTextCell } from "./data-grid-text-cell" +export { DataGridBooleanCell } from './data-grid-boolean-cell'; +export { DataGridCurrencyCell } from './data-grid-currency-cell'; +export { DataGridNumberCell } from './data-grid-number-cell'; +export { DataGridReadonlyCell as DataGridReadOnlyCell } from './data-grid-readonly-cell'; +export { DataGridRoot, type DataGridRootProps } from './data-grid-root'; +export { DataGridSkeleton } from './data-grid-skeleton'; +export { DataGridTextCell } from './data-grid-text-cell'; +export { DataGridReadonlyCell } from './data-grid-readonly-cell'; diff --git a/src/components/data-grid/context/data-grid-context.tsx b/src/components/data-grid/context/data-grid-context.tsx index bdfc4192..da2b0575 100644 --- a/src/components/data-grid/context/data-grid-context.tsx +++ b/src/components/data-grid/context/data-grid-context.tsx @@ -1,46 +1,41 @@ -import { FocusEvent, MouseEvent, createContext } from "react" -import { - Control, - FieldErrors, - FieldValues, - Path, - UseFormRegister, -} from "react-hook-form" -import { CellErrorMetadata, CellMetadata, DataGridCoordinates } from "../types" +import { createContext, type FocusEvent, type MouseEvent } from 'react'; + +import type { + CellErrorMetadata, + CellMetadata, + DataGridCoordinates +} from '@components/data-grid/types.ts'; +import type { Control, FieldErrors, FieldValues, Path, UseFormRegister } from 'react-hook-form'; type DataGridContextType = { // Grid state - anchor: DataGridCoordinates | null - trapActive: boolean - setTrapActive: (value: boolean) => void - errors: FieldErrors + anchor: DataGridCoordinates | null; + trapActive: boolean; + setTrapActive: (value: boolean) => void; + errors: FieldErrors; // Cell handlers - getIsCellSelected: (coords: DataGridCoordinates) => boolean - getIsCellDragSelected: (coords: DataGridCoordinates) => boolean + getIsCellSelected: (coords: DataGridCoordinates) => boolean; + getIsCellDragSelected: (coords: DataGridCoordinates) => boolean; // Grid handlers - setIsEditing: (value: boolean) => void - setIsSelecting: (value: boolean) => void - setRangeEnd: (coords: DataGridCoordinates) => void - setSingleRange: (coords: DataGridCoordinates) => void + setIsEditing: (value: boolean) => void; + setIsSelecting: (value: boolean) => void; + setRangeEnd: (coords: DataGridCoordinates) => void; + setSingleRange: (coords: DataGridCoordinates) => void; // Form state and handlers - register: UseFormRegister - control: Control - getInputChangeHandler: ( - field: Path - ) => (next: any, prev: any) => void + register: UseFormRegister; + control: Control; + getInputChangeHandler: (field: Path) => (next: any, prev: any) => void; // Wrapper handlers getWrapperFocusHandler: ( coordinates: DataGridCoordinates - ) => (e: FocusEvent) => void + ) => (e: FocusEvent) => void; getWrapperMouseOverHandler: ( coordinates: DataGridCoordinates - ) => ((e: MouseEvent) => void) | undefined - getCellMetadata: (coords: DataGridCoordinates) => CellMetadata - getCellErrorMetadata: (coords: DataGridCoordinates) => CellErrorMetadata - navigateToField: (field: string) => void - dataTestId?: string -} + ) => ((e: MouseEvent) => void) | undefined; + getCellMetadata: (coords: DataGridCoordinates) => CellMetadata; + getCellErrorMetadata: (coords: DataGridCoordinates) => CellErrorMetadata; + navigateToField: (field: string) => void; + dataTestId?: string; +}; -export const DataGridContext = createContext | null>( - null -) +export const DataGridContext = createContext | null>(null); diff --git a/src/components/data-grid/context/index.ts b/src/components/data-grid/context/index.ts index dda988e4..2d09d9f2 100644 --- a/src/components/data-grid/context/index.ts +++ b/src/components/data-grid/context/index.ts @@ -1,2 +1,2 @@ -export * from "./data-grid-context" -export * from "./use-data-grid-context" +export * from './data-grid-context'; +export * from './use-data-grid-context'; diff --git a/src/components/data-grid/context/use-data-grid-context.tsx b/src/components/data-grid/context/use-data-grid-context.tsx index 3a0f8bfa..061fc383 100644 --- a/src/components/data-grid/context/use-data-grid-context.tsx +++ b/src/components/data-grid/context/use-data-grid-context.tsx @@ -1,14 +1,13 @@ -import { useContext } from "react" -import { DataGridContext } from "./data-grid-context" +import { useContext } from 'react'; + +import { DataGridContext } from './data-grid-context'; export const useDataGridContext = () => { - const context = useContext(DataGridContext) + const context = useContext(DataGridContext); if (!context) { - throw new Error( - "useDataGridContext must be used within a DataGridContextProvider" - ) + throw new Error('useDataGridContext must be used within a DataGridContextProvider'); } - return context -} + return context; +}; diff --git a/src/components/data-grid/data-grid.tsx b/src/components/data-grid/data-grid.tsx index 74a08383..aee6d165 100644 --- a/src/components/data-grid/data-grid.tsx +++ b/src/components/data-grid/data-grid.tsx @@ -1,5 +1,3 @@ -import { FieldValues } from "react-hook-form" - import { DataGridBooleanCell, DataGridCurrencyCell, @@ -8,34 +6,32 @@ import { DataGridRoot, DataGridSkeleton, DataGridTextCell, - type DataGridRootProps, -} from "./components" + type DataGridRootProps +} from '@components/data-grid/components'; +import type { FieldValues } from 'react-hook-form'; interface DataGridProps extends DataGridRootProps { - isLoading?: boolean + isLoading?: boolean; } const _DataGrid = ({ isLoading, ...props -}: DataGridProps) => { - return isLoading ? ( +}: DataGridProps) => + isLoading ? ( 0 ? props.data.length : 10 - } + rows={props.data?.length && props.data.length > 0 ? props.data.length : 10} /> ) : ( - ) -} + ); export const DataGrid = Object.assign(_DataGrid, { BooleanCell: DataGridBooleanCell, TextCell: DataGridTextCell, NumberCell: DataGridNumberCell, CurrencyCell: DataGridCurrencyCell, - ReadonlyCell: DataGridReadOnlyCell, -}) + ReadonlyCell: DataGridReadOnlyCell +}); diff --git a/src/components/data-grid/helpers/create-data-grid-column-helper.ts b/src/components/data-grid/helpers/create-data-grid-column-helper.ts index c47b9e80..1683c42c 100644 --- a/src/components/data-grid/helpers/create-data-grid-column-helper.ts +++ b/src/components/data-grid/helpers/create-data-grid-column-helper.ts @@ -1,54 +1,50 @@ +import type { DataGridColumnType, FieldFunction } from '@components/data-grid/types'; import { - CellContext, - ColumnDefTemplate, createColumnHelper, - HeaderContext, -} from "@tanstack/react-table" -import { FieldValues } from "react-hook-form" - -import { DataGridColumnType, FieldFunction } from "../types" + type CellContext, + type ColumnDefTemplate, + type HeaderContext +} from '@tanstack/react-table'; +import type { FieldValues } from 'react-hook-form'; type DataGridHelperColumnsProps = { /** * The id of the column. */ - id: string + id: string; /** * The name of the column, shown in the column visibility menu. */ - name?: string + name?: string; /** * The header template for the column. */ - header: ColumnDefTemplate> | undefined + header: ColumnDefTemplate> | undefined; /** * The cell template for the column. */ - cell: ColumnDefTemplate> | undefined + cell: ColumnDefTemplate> | undefined; /** * Callback to set the field path for each cell in the column. * If a callback is not provided, or returns null, the cell will not be editable. */ - field?: FieldFunction + field?: FieldFunction; /** * Whether the column cannot be hidden by the user. * * @default false */ - disableHiding?: boolean + disableHiding?: boolean; } & ( | { - field: FieldFunction - type: DataGridColumnType + field: FieldFunction; + type: DataGridColumnType; } | { field?: null | undefined; type?: never } -) +); -export function createDataGridHelper< - TData, - TFieldValues extends FieldValues ->() { - const columnHelper = createColumnHelper() +export function createDataGridHelper() { + const columnHelper = createColumnHelper(); return { column: ({ @@ -58,7 +54,7 @@ export function createDataGridHelper< cell, disableHiding = false, field, - type, + type }: DataGridHelperColumnsProps) => columnHelper.display({ id, @@ -68,8 +64,8 @@ export function createDataGridHelper< meta: { name, field, - type, - }, - }), - } + type + } + }) + }; } diff --git a/src/components/data-grid/helpers/create-data-grid-price-columns.tsx b/src/components/data-grid/helpers/create-data-grid-price-columns.tsx index 0c1fff4e..57b15a74 100644 --- a/src/components/data-grid/helpers/create-data-grid-price-columns.tsx +++ b/src/components/data-grid/helpers/create-data-grid-price-columns.tsx @@ -1,126 +1,124 @@ -import { HttpTypes } from "@medusajs/types" -import { ColumnDef } from "@tanstack/react-table" -import { TFunction } from "i18next" -import { FieldPath, FieldValues } from "react-hook-form" -import { IncludesTaxTooltip } from "../../common/tax-badge/tax-badge" -import { DataGridCurrencyCell } from "../components/data-grid-currency-cell" -import { DataGridReadonlyCell } from "../components/data-grid-readonly-cell" -import { FieldContext } from "../types" -import { createDataGridHelper } from "./create-data-grid-column-helper" +import { IncludesTaxTooltip } from '@components/common/tax-badge'; +import { createDataGridHelper } from '@components/data-grid'; +import { DataGridCurrencyCell, DataGridReadonlyCell } from '@components/data-grid/components'; +import type { FieldContext } from '@components/data-grid/types'; +import type { HttpTypes } from '@medusajs/types'; +import type { ColumnDef } from '@tanstack/react-table'; +import type { TFunction } from 'i18next'; +import type { FieldPath, FieldValues } from 'react-hook-form'; -type CreateDataGridPriceColumnsProps< - TData, - TFieldValues extends FieldValues -> = { - currencies?: string[] - regions?: HttpTypes.AdminRegion[] - pricePreferences?: HttpTypes.AdminPricePreference[] - isReadyOnly?: (context: FieldContext) => boolean - getFieldName: ( - context: FieldContext, - value: string - ) => FieldPath | null - t: TFunction -} +type CreateDataGridPriceColumnsProps = { + currencies?: string[]; + regions?: HttpTypes.AdminRegion[]; + pricePreferences?: HttpTypes.AdminPricePreference[]; + isReadyOnly?: (context: FieldContext) => boolean; + getFieldName: (context: FieldContext, value: string) => FieldPath | null; + t: TFunction; +}; -export const createDataGridPriceColumns = < - TData, - TFieldValues extends FieldValues ->({ +export const createDataGridPriceColumns = ({ currencies, regions, pricePreferences, isReadyOnly, getFieldName, - t, -}: CreateDataGridPriceColumnsProps): ColumnDef< - TData, - unknown ->[] => { - const columnHelper = createDataGridHelper() + t +}: CreateDataGridPriceColumnsProps): ColumnDef[] => { + const columnHelper = createDataGridHelper(); return [ - ...(currencies?.map((currency) => { + ...(currencies?.map(currency => { const preference = pricePreferences?.find( - (p) => p.attribute === "currency_code" && p.value === currency - ) + p => p.attribute === 'currency_code' && p.value === currency + ); - const translatedCurrencyName = t("fields.priceTemplate", { - regionOrCurrency: currency.toUpperCase(), - }) + const translatedCurrencyName = t('fields.priceTemplate', { + regionOrCurrency: currency.toUpperCase() + }); return columnHelper.column({ id: `currency_prices.${currency}`, - name: t("fields.priceTemplate", { - regionOrCurrency: currency.toUpperCase(), + name: t('fields.priceTemplate', { + regionOrCurrency: currency.toUpperCase() }), - field: (context) => { - const isReadyOnlyValue = isReadyOnly?.(context) + field: context => { + const isReadyOnlyValue = isReadyOnly?.(context); if (isReadyOnlyValue) { - return null + return null; } - return getFieldName(context, currency) + return getFieldName(context, currency); }, - type: "number", + type: 'number', header: () => (
    - + {translatedCurrencyName}
    ), - cell: (context) => { + cell: context => { if (isReadyOnly?.(context)) { - return + return ; } - return - }, - }) + return ( + + ); + } + }); }) ?? []), - ...(regions?.map((region) => { + ...(regions?.map(region => { const preference = pricePreferences?.find( - (p) => p.attribute === "region_id" && p.value === region.id - ) + p => p.attribute === 'region_id' && p.value === region.id + ); - const translatedRegionName = t("fields.priceTemplate", { - regionOrCurrency: region.name, - }) + const translatedRegionName = t('fields.priceTemplate', { + regionOrCurrency: region.name + }); return columnHelper.column({ id: `region_prices.${region.id}`, - name: t("fields.priceTemplate", { - regionOrCurrency: region.name, + name: t('fields.priceTemplate', { + regionOrCurrency: region.name }), - field: (context) => { - const isReadyOnlyValue = isReadyOnly?.(context) + field: context => { + const isReadyOnlyValue = isReadyOnly?.(context); if (isReadyOnlyValue) { - return null + return null; } - return getFieldName(context, region.id) + return getFieldName(context, region.id); }, - type: "number", + type: 'number', header: () => (
    - + {translatedRegionName}
    ), - cell: (context) => { + cell: context => { if (isReadyOnly?.(context)) { - return + return ; } - const currency = currencies?.find((c) => c === region.currency_code) + const currency = currencies?.find(c => c === region.currency_code); if (!currency) { - return null + return null; } return ( @@ -128,9 +126,9 @@ export const createDataGridPriceColumns = < code={region.currency_code} context={context} /> - ) - }, - }) - }) ?? []), - ] -} + ); + } + }); + }) ?? []) + ]; +}; diff --git a/src/components/data-grid/helpers/index.ts b/src/components/data-grid/helpers/index.ts index 5a490604..c7b349c0 100644 --- a/src/components/data-grid/helpers/index.ts +++ b/src/components/data-grid/helpers/index.ts @@ -1,2 +1,2 @@ -export * from "./create-data-grid-column-helper" -export * from "./create-data-grid-price-columns" +export * from './create-data-grid-column-helper'; +export * from './create-data-grid-price-columns'; diff --git a/src/components/data-grid/hooks/index.ts b/src/components/data-grid/hooks/index.ts index 2307c30c..54299b4c 100644 --- a/src/components/data-grid/hooks/index.ts +++ b/src/components/data-grid/hooks/index.ts @@ -1,14 +1,14 @@ -export * from "./use-data-grid-cell" -export * from "./use-data-grid-cell-error" -export * from "./use-data-grid-cell-handlers" -export * from "./use-data-grid-cell-metadata" -export * from "./use-data-grid-cell-snapshot" -export * from "./use-data-grid-clipboard-events" -export * from "./use-data-grid-column-visibility" -export * from "./use-data-grid-duplicate-cell" -export * from "./use-data-grid-error-highlighting" -export * from "./use-data-grid-form-handlers" -export * from "./use-data-grid-keydown-event" -export * from "./use-data-grid-mouse-up-event" -export * from "./use-data-grid-navigation" -export * from "./use-data-grid-query-tool" +export * from './use-data-grid-cell'; +export * from './use-data-grid-cell-error'; +export * from './use-data-grid-cell-handlers'; +export * from './use-data-grid-cell-metadata'; +export * from './use-data-grid-cell-snapshot'; +export * from './use-data-grid-clipboard-events'; +export * from './use-data-grid-column-visibility'; +export * from './use-data-grid-duplicate-cell'; +export * from './use-data-grid-error-highlighting'; +export * from './use-data-grid-form-handlers'; +export * from './use-data-grid-keydown-event'; +export * from './use-data-grid-mouse-up-event'; +export * from './use-data-grid-navigation'; +export * from './use-data-grid-query-tool'; diff --git a/src/components/data-grid/hooks/use-data-grid-cell-error.tsx b/src/components/data-grid/hooks/use-data-grid-cell-error.tsx index b54e3125..c0c3b4b2 100644 --- a/src/components/data-grid/hooks/use-data-grid-cell-error.tsx +++ b/src/components/data-grid/hooks/use-data-grid-cell-error.tsx @@ -1,78 +1,70 @@ -import { CellContext } from "@tanstack/react-table" -import { useMemo } from "react" -import { FieldError, FieldErrors, get } from "react-hook-form" +import { useMemo } from 'react'; -import { useDataGridContext } from "../context" -import { DataGridCellContext, DataGridRowError } from "../types" +import { useDataGridContext } from '@components/data-grid/context'; +import type { DataGridCellContext, DataGridRowError } from '@components/data-grid/types'; +import type { CellContext } from '@tanstack/react-table'; +import { get, type FieldError, type FieldErrors } from 'react-hook-form'; type UseDataGridCellErrorOptions = { - context: CellContext -} + context: CellContext; +}; export const useDataGridCellError = ({ - context, + context }: UseDataGridCellErrorOptions) => { - const { errors, getCellErrorMetadata, navigateToField } = useDataGridContext() + const { errors, getCellErrorMetadata, navigateToField } = useDataGridContext(); - const { rowIndex, columnIndex } = context as DataGridCellContext< - TextData, - TValue - > + const { rowIndex, columnIndex } = context as DataGridCellContext; const { accessor, field } = useMemo(() => { - return getCellErrorMetadata({ row: rowIndex, col: columnIndex }) - }, [rowIndex, columnIndex, getCellErrorMetadata]) + return getCellErrorMetadata({ row: rowIndex, col: columnIndex }); + }, [rowIndex, columnIndex, getCellErrorMetadata]); const rowErrorsObject: FieldErrors | undefined = - accessor && columnIndex === 0 ? get(errors, accessor) : undefined + accessor && columnIndex === 0 ? get(errors, accessor) : undefined; - const rowErrors: DataGridRowError[] = [] + const rowErrors: DataGridRowError[] = []; - function collectErrors( - errorObject: FieldErrors | FieldError | undefined, - baseAccessor: string - ) { + function collectErrors(errorObject: FieldErrors | FieldError | undefined, baseAccessor: string) { if (!errorObject) { - return + return; } if (isFieldError(errorObject)) { // Handle a single FieldError directly - const message = errorObject.message + const message = errorObject.message; - const to = () => navigateToField(baseAccessor) + const to = () => navigateToField(baseAccessor); if (message) { - rowErrors.push({ message, to }) + rowErrors.push({ message, to }); } } else { // Traverse nested objects - Object.keys(errorObject).forEach((key) => { - const nestedError = errorObject[key] - const fieldAccessor = `${baseAccessor}.${key}` + Object.keys(errorObject).forEach(key => { + const nestedError = errorObject[key]; + const fieldAccessor = `${baseAccessor}.${key}`; - if (nestedError && typeof nestedError === "object") { - collectErrors(nestedError, fieldAccessor) + if (nestedError && typeof nestedError === 'object') { + collectErrors(nestedError, fieldAccessor); } - }) + }); } } if (rowErrorsObject && accessor) { - collectErrors(rowErrorsObject, accessor) + collectErrors(rowErrorsObject, accessor); } - const cellError: FieldError | undefined = field - ? get(errors, field) - : undefined + const cellError: FieldError | undefined = field ? get(errors, field) : undefined; return { errors, rowErrors, - cellError, - } -} + cellError + }; +}; function isFieldError(errors: FieldErrors | FieldError): errors is FieldError { - return typeof errors === "object" && "message" in errors && "type" in errors + return typeof errors === 'object' && 'message' in errors && 'type' in errors; } diff --git a/src/components/data-grid/hooks/use-data-grid-cell-handlers.tsx b/src/components/data-grid/hooks/use-data-grid-cell-handlers.tsx index 1aee1dc2..f0c416af 100644 --- a/src/components/data-grid/hooks/use-data-grid-cell-handlers.tsx +++ b/src/components/data-grid/hooks/use-data-grid-cell-handlers.tsx @@ -1,29 +1,27 @@ -import { FocusEvent, MouseEvent, useCallback } from "react" -import { FieldValues, UseFormSetValue } from "react-hook-form" -import { DataGridMatrix, DataGridUpdateCommand } from "../models" -import { DataGridCoordinates } from "../types" +import { useCallback, type FocusEvent, type MouseEvent } from 'react'; + +import { DataGridUpdateCommand, type DataGridMatrix } from '@components/data-grid/models'; +import type { DataGridCoordinates } from '@components/data-grid/types'; +import type { FieldValues, UseFormSetValue } from 'react-hook-form'; type UseDataGridCellHandlersOptions = { - matrix: DataGridMatrix - anchor: DataGridCoordinates | null - rangeEnd: DataGridCoordinates | null - setRangeEnd: (coords: DataGridCoordinates | null) => void - isSelecting: boolean - setIsSelecting: (isSelecting: boolean) => void - isDragging: boolean - setIsDragging: (isDragging: boolean) => void - setSingleRange: (coords: DataGridCoordinates) => void - dragEnd: DataGridCoordinates | null - setDragEnd: (coords: DataGridCoordinates | null) => void - setValue: UseFormSetValue - execute: (command: DataGridUpdateCommand) => void - multiColumnSelection?: boolean -} - -export const useDataGridCellHandlers = < - TData, - TFieldValues extends FieldValues ->({ + matrix: DataGridMatrix; + anchor: DataGridCoordinates | null; + rangeEnd: DataGridCoordinates | null; + setRangeEnd: (coords: DataGridCoordinates | null) => void; + isSelecting: boolean; + setIsSelecting: (isSelecting: boolean) => void; + isDragging: boolean; + setIsDragging: (isDragging: boolean) => void; + setSingleRange: (coords: DataGridCoordinates) => void; + dragEnd: DataGridCoordinates | null; + setDragEnd: (coords: DataGridCoordinates | null) => void; + setValue: UseFormSetValue; + execute: (command: DataGridUpdateCommand) => void; + multiColumnSelection?: boolean; +}; + +export const useDataGridCellHandlers = ({ matrix, anchor, rangeEnd, @@ -37,40 +35,37 @@ export const useDataGridCellHandlers = < setDragEnd, setValue, execute, - multiColumnSelection, + multiColumnSelection }: UseDataGridCellHandlersOptions) => { const getWrapperFocusHandler = useCallback( - (coords: DataGridCoordinates) => { - return (_e: FocusEvent) => { - setSingleRange(coords) - } + (coords: DataGridCoordinates) => (_e: FocusEvent) => { + setSingleRange(coords); }, [setSingleRange] - ) + ); const getOverlayMouseDownHandler = useCallback( - (coords: DataGridCoordinates) => { - return (e: MouseEvent) => { - e.stopPropagation() - e.preventDefault() - - if (e.shiftKey) { - setRangeEnd(coords) - return - } + (coords: DataGridCoordinates) => (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); - setIsSelecting(true) + if (e.shiftKey) { + setRangeEnd(coords); - setSingleRange(coords) + return; } + + setIsSelecting(true); + + setSingleRange(coords); }, [setIsSelecting, setRangeEnd, setSingleRange] - ) + ); const getWrapperMouseOverHandler = useCallback( (coords: DataGridCoordinates) => { if (!isDragging && !isSelecting) { - return + return; } return (_e: MouseEvent) => { @@ -79,76 +74,67 @@ export const useDataGridCellHandlers = < * we don't want to select the cell. Unless multiColumnSelection is true. */ if (anchor?.col !== coords.col && !multiColumnSelection) { - return + return; } if (isSelecting) { - setRangeEnd(coords) + setRangeEnd(coords); } else { - setDragEnd(coords) + setDragEnd(coords); } - } + }; }, - [ - anchor?.col, - isDragging, - isSelecting, - setDragEnd, - setRangeEnd, - multiColumnSelection, - ] - ) + [anchor?.col, isDragging, isSelecting, setDragEnd, setRangeEnd, multiColumnSelection] + ); const getInputChangeHandler = useCallback( - // Using `any` here as the generic type of Path will - // not be inferred correctly. - (field: any) => { - return (next: any, prev: any) => { - const command = new DataGridUpdateCommand({ - next, - prev, - setter: (value) => { - setValue(field, value, { - shouldDirty: true, - shouldTouch: true, - }) - }, - }) - - execute(command) - } + // @todo fix any type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (field: any) => (next: any, prev: any) => { + const command = new DataGridUpdateCommand({ + next, + prev, + setter: value => { + setValue(field, value, { + shouldDirty: true, + shouldTouch: true + }); + } + }); + + execute(command); }, [setValue, execute] - ) + ); const onDragToFillStart = useCallback( (_e: MouseEvent) => { - setIsDragging(true) + setIsDragging(true); }, [setIsDragging] - ) + ); const getIsCellSelected = useCallback( (cell: DataGridCoordinates | null) => { if (!cell || !anchor || !rangeEnd) { - return false + return false; } - return matrix.getIsCellSelected(cell, anchor, rangeEnd) + return matrix.getIsCellSelected(cell, anchor, rangeEnd); }, [anchor, rangeEnd, matrix] - ) + ); const getIsCellDragSelected = useCallback( (cell: DataGridCoordinates | null) => { if (!cell || !anchor || !dragEnd) { - return false + return false; } - return matrix.getIsCellSelected(cell, anchor, dragEnd) + return matrix.getIsCellSelected(cell, anchor, dragEnd); }, [anchor, dragEnd, matrix] - ) + ); return { getWrapperFocusHandler, @@ -157,6 +143,6 @@ export const useDataGridCellHandlers = < getInputChangeHandler, getIsCellSelected, getIsCellDragSelected, - onDragToFillStart, - } -} + onDragToFillStart + }; +}; diff --git a/src/components/data-grid/hooks/use-data-grid-cell-metadata.tsx b/src/components/data-grid/hooks/use-data-grid-cell-metadata.tsx index 182c81d3..a76e3b08 100644 --- a/src/components/data-grid/hooks/use-data-grid-cell-metadata.tsx +++ b/src/components/data-grid/hooks/use-data-grid-cell-metadata.tsx @@ -1,55 +1,57 @@ -import { useCallback } from "react" -import { FieldValues } from "react-hook-form" -import { DataGridMatrix } from "../models" -import { CellErrorMetadata, CellMetadata, DataGridCoordinates } from "../types" -import { generateCellId } from "../utils" +import { useCallback } from 'react'; + +import type { DataGridMatrix } from '@components/data-grid/models'; +import type { + CellErrorMetadata, + CellMetadata, + DataGridCoordinates +} from '@components/data-grid/types'; +import { generateCellId } from '@components/data-grid/utils'; +import type { FieldValues } from 'react-hook-form'; type UseDataGridCellMetadataOptions = { - matrix: DataGridMatrix -} + matrix: DataGridMatrix; +}; -export const useDataGridCellMetadata = < - TData, - TFieldValues extends FieldValues ->({ - matrix, +export const useDataGridCellMetadata = ({ + matrix }: UseDataGridCellMetadataOptions) => { /** * Creates metadata for a cell. */ const getCellMetadata = useCallback( (coords: DataGridCoordinates): CellMetadata => { - const { row, col } = coords + const { row, col } = coords; - const id = generateCellId(coords) - const field = matrix.getCellField(coords) - const type = matrix.getCellType(coords) + const id = generateCellId(coords); + const field = matrix.getCellField(coords); + const type = matrix.getCellType(coords); if (!field || !type) { - throw new Error(`'field' or 'type' is null for cell ${id}`) + throw new Error(`'field' or 'type' is null for cell ${id}`); } const inputAttributes = { - "data-row": row, - "data-col": col, - "data-cell-id": id, - "data-field": field, - } + 'data-row': row, + 'data-col': col, + 'data-cell-id': id, + 'data-field': field + }; const innerAttributes = { - "data-container-id": id, - } + 'data-container-id': id + }; return { id, field, type, inputAttributes, - innerAttributes, - } + innerAttributes + }; }, [matrix] - ) + ); /** * Creates error metadata for a cell. This is used to display error messages @@ -57,19 +59,19 @@ export const useDataGridCellMetadata = < */ const getCellErrorMetadata = useCallback( (coords: DataGridCoordinates): CellErrorMetadata => { - const accessor = matrix.getRowAccessor(coords.row) - const field = matrix.getCellField(coords) + const accessor = matrix.getRowAccessor(coords.row); + const field = matrix.getCellField(coords); return { accessor, - field, - } + field + }; }, [matrix] - ) + ); return { getCellMetadata, - getCellErrorMetadata, - } -} + getCellErrorMetadata + }; +}; diff --git a/src/components/data-grid/hooks/use-data-grid-cell-snapshot.tsx b/src/components/data-grid/hooks/use-data-grid-cell-snapshot.tsx index 9f73327c..19330692 100644 --- a/src/components/data-grid/hooks/use-data-grid-cell-snapshot.tsx +++ b/src/components/data-grid/hooks/use-data-grid-cell-snapshot.tsx @@ -1,24 +1,21 @@ -import { useCallback, useState } from "react" -import { FieldValues, Path, UseFormReturn } from "react-hook-form" -import { DataGridMatrix } from "../models" -import { DataGridCellSnapshot, DataGridCoordinates } from "../types" +import { useCallback, useState } from 'react'; + +import type { DataGridMatrix } from '@components/data-grid/models'; +import type { DataGridCellSnapshot, DataGridCoordinates } from '@components/data-grid/types'; +import type { FieldValues, Path, UseFormReturn } from 'react-hook-form'; type UseDataGridCellSnapshotOptions = { - matrix: DataGridMatrix - form: UseFormReturn -} + matrix: DataGridMatrix; + form: UseFormReturn; +}; -export const useDataGridCellSnapshot = < - TData, - TFieldValues extends FieldValues ->({ +export const useDataGridCellSnapshot = ({ matrix, - form, + form }: UseDataGridCellSnapshotOptions) => { - const [snapshot, setSnapshot] = - useState | null>(null) + const [snapshot, setSnapshot] = useState | null>(null); - const { getValues, setValue } = form + const { getValues, setValue } = form; /** * Creates a snapshot of the current cell value. @@ -26,50 +23,50 @@ export const useDataGridCellSnapshot = < const createSnapshot = useCallback( (cell: DataGridCoordinates | null) => { if (!cell) { - return null + return null; } - const field = matrix.getCellField(cell) + const field = matrix.getCellField(cell); if (!field) { - return null + return null; } - const value = getValues(field as Path) + const value = getValues(field as Path); - setSnapshot((curr) => { + setSnapshot(curr => { /** * If there already exists a snapshot for this field, we don't want to create a new one. * A case where this happens is when the user presses the space key on a field. In that case * we create a snapshot of the value before its destroyed by the space key. */ if (curr?.field === field) { - return curr + return curr; } - return { field, value } - }) + return { field, value }; + }); }, [getValues, matrix] - ) + ); /** * Restores the cell value from the snapshot if it exists. */ const restoreSnapshot = useCallback(() => { if (!snapshot) { - return + return; } - const { field, value } = snapshot + const { field, value } = snapshot; requestAnimationFrame(() => { - setValue(field as Path, value) - }) - }, [setValue, snapshot]) + setValue(field as Path, value); + }); + }, [setValue, snapshot]); return { createSnapshot, - restoreSnapshot, - } -} + restoreSnapshot + }; +}; diff --git a/src/components/data-grid/hooks/use-data-grid-cell.tsx b/src/components/data-grid/hooks/use-data-grid-cell.tsx index 1fa693f2..945830b1 100644 --- a/src/components/data-grid/hooks/use-data-grid-cell.tsx +++ b/src/components/data-grid/hooks/use-data-grid-cell.tsx @@ -1,23 +1,24 @@ -import { CellContext } from "@tanstack/react-table" -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" +import type React from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useDataGridContext } from "../context" -import { +import { useDataGridContext } from '@components/data-grid/context'; +import type { DataGridCellContext, DataGridCellRenderProps, - DataGridCoordinates, -} from "../types" -import { isCellMatch, isSpecialFocusKey } from "../utils" + DataGridCoordinates +} from '@components/data-grid/types'; +import { isCellMatch, isSpecialFocusKey } from '@components/data-grid/utils'; +import type { CellContext } from '@tanstack/react-table'; type UseDataGridCellOptions = { - context: CellContext -} + context: CellContext; +}; -const textCharacterRegex = /^.$/u -const numberCharacterRegex = /^[0-9]$/u +const textCharacterRegex = /^.$/u; +const numberCharacterRegex = /^[0-9]$/u; export const useDataGridCell = ({ - context, + context }: UseDataGridCellOptions) => { const { register, @@ -32,40 +33,37 @@ export const useDataGridCell = ({ getInputChangeHandler, getIsCellSelected, getIsCellDragSelected, - getCellMetadata, - } = useDataGridContext() + getCellMetadata + } = useDataGridContext(); - const { rowIndex, columnIndex } = context as DataGridCellContext< - TData, - TValue - > + const { rowIndex, columnIndex } = context as DataGridCellContext; const coords: DataGridCoordinates = useMemo( () => ({ row: rowIndex, col: columnIndex }), [rowIndex, columnIndex] - ) + ); const { id, field, type, innerAttributes, inputAttributes } = useMemo(() => { - return getCellMetadata(coords) - }, [coords, getCellMetadata]) + return getCellMetadata(coords); + }, [coords, getCellMetadata]); - const [showOverlay, setShowOverlay] = useState(true) + const [showOverlay, setShowOverlay] = useState(true); - const containerRef = useRef(null) - const inputRef = useRef(null) + const containerRef = useRef(null); + const inputRef = useRef(null); const handleOverlayMouseDown = useCallback( (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() + e.preventDefault(); + e.stopPropagation(); if (e.detail === 2) { if (inputRef.current) { - setShowOverlay(false) + setShowOverlay(false); - inputRef.current.focus() + inputRef.current.focus(); - return + return; } } @@ -73,138 +71,141 @@ export const useDataGridCell = ({ // Only allow setting the rangeEnd if the column matches the anchor column. // If not we let the function continue and treat the click as if the shift key was not pressed. if (coords.col === anchor?.col) { - setRangeEnd(coords) - return + setRangeEnd(coords); + + return; } } if (containerRef.current) { - setSingleRange(coords) - setIsSelecting(true) - containerRef.current.focus() + setSingleRange(coords); + setIsSelecting(true); + containerRef.current.focus(); } }, [coords, anchor, setRangeEnd, setSingleRange, setIsSelecting] - ) + ); const handleBooleanInnerMouseDown = useCallback( (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() + e.preventDefault(); + e.stopPropagation(); if (e.detail === 2) { - inputRef.current?.focus() - return + inputRef.current?.focus(); + + return; } if (e.shiftKey) { - setRangeEnd(coords) - return + setRangeEnd(coords); + + return; } if (containerRef.current) { - setSingleRange(coords) - setIsSelecting(true) - containerRef.current.focus() + setSingleRange(coords); + setIsSelecting(true); + containerRef.current.focus(); } }, [setIsSelecting, setSingleRange, setRangeEnd, coords] - ) + ); const handleInputBlur = useCallback(() => { - setShowOverlay(true) - setIsEditing(false) - }, [setIsEditing]) + setShowOverlay(true); + setIsEditing(false); + }, [setIsEditing]); const handleInputFocus = useCallback(() => { - setShowOverlay(false) - setIsEditing(true) - }, [setIsEditing]) + setShowOverlay(false); + setIsEditing(true); + }, [setIsEditing]); const validateKeyStroke = useCallback( (key: string) => { switch (type) { - case "togglable-number": - case "number": - return numberCharacterRegex.test(key) - case "text": - return textCharacterRegex.test(key) + case 'togglable-number': + case 'number': + return numberCharacterRegex.test(key); + case 'text': + return textCharacterRegex.test(key); default: // KeyboardEvents should not be forwareded to other types of cells - return false + return false; } }, [type] - ) + ); const handleContainerKeyDown = useCallback( (e: React.KeyboardEvent) => { if (!inputRef.current || !validateKeyStroke(e.key) || !showOverlay) { - return + return; } // Allow the user to undo/redo - if (e.key.toLowerCase() === "z" && (e.ctrlKey || e.metaKey)) { - return + if (e.key.toLowerCase() === 'z' && (e.ctrlKey || e.metaKey)) { + return; } // Allow the user to copy - if (e.key.toLowerCase() === "c" && (e.ctrlKey || e.metaKey)) { - return + if (e.key.toLowerCase() === 'c' && (e.ctrlKey || e.metaKey)) { + return; } // Allow the user to paste - if (e.key.toLowerCase() === "v" && (e.ctrlKey || e.metaKey)) { - return + if (e.key.toLowerCase() === 'v' && (e.ctrlKey || e.metaKey)) { + return; } - if (e.key === "Enter") { - return + if (e.key === 'Enter') { + return; } if (isSpecialFocusKey(e.nativeEvent)) { - return + return; } - inputRef.current.focus() - setShowOverlay(false) + inputRef.current.focus(); + setShowOverlay(false); if (inputRef.current instanceof HTMLInputElement) { // Clear the current value - inputRef.current.value = "" + inputRef.current.value = ''; // Simulate typing the new key const nativeInputValueSetter = Object.getOwnPropertyDescriptor( window.HTMLInputElement.prototype, - "value" - )?.set - nativeInputValueSetter?.call(inputRef.current, e.key) + 'value' + )?.set; + nativeInputValueSetter?.call(inputRef.current, e.key); // Trigger input event to notify react-hook-form - const event = new Event("input", { bubbles: true }) - inputRef.current.dispatchEvent(event) + const event = new Event('input', { bubbles: true }); + inputRef.current.dispatchEvent(event); } // Prevent the original event from propagating - e.stopPropagation() - e.preventDefault() + e.stopPropagation(); + e.preventDefault(); }, [showOverlay, validateKeyStroke] - ) + ); const isAnchor = useMemo(() => { - return anchor ? isCellMatch(coords, anchor) : false - }, [anchor, coords]) + return anchor ? isCellMatch(coords, anchor) : false; + }, [anchor, coords]); const fieldWithoutOverlay = useMemo(() => { - return type === "boolean" - }, [type]) + return type === 'boolean'; + }, [type]); useEffect(() => { if (isAnchor && !containerRef.current?.contains(document.activeElement)) { - containerRef.current?.focus() + containerRef.current?.focus(); } - }, [isAnchor]) + }, [isAnchor]); const renderProps: DataGridCellRenderProps = { container: { @@ -216,30 +217,29 @@ export const useDataGridCell = ({ innerProps: { ref: containerRef, onMouseOver: getWrapperMouseOverHandler(coords), - onMouseDown: - type === "boolean" ? handleBooleanInnerMouseDown : undefined, + onMouseDown: type === 'boolean' ? handleBooleanInnerMouseDown : undefined, onKeyDown: handleContainerKeyDown, onFocus: getWrapperFocusHandler(coords), - ...innerAttributes, + ...innerAttributes }, overlayProps: { - onMouseDown: handleOverlayMouseDown, - }, + onMouseDown: handleOverlayMouseDown + } }, input: { ref: inputRef, onBlur: handleInputBlur, onFocus: handleInputFocus, onChange: getInputChangeHandler(field), - ...inputAttributes, - }, - } + ...inputAttributes + } + }; return { id, field, register, control, - renderProps, - } -} + renderProps + }; +}; diff --git a/src/components/data-grid/hooks/use-data-grid-clipboard-events.tsx b/src/components/data-grid/hooks/use-data-grid-clipboard-events.tsx index d8170b48..7fb310cf 100644 --- a/src/components/data-grid/hooks/use-data-grid-clipboard-events.tsx +++ b/src/components/data-grid/hooks/use-data-grid-clipboard-events.tsx @@ -1,105 +1,90 @@ -import { useCallback } from "react" -import { FieldValues, Path, PathValue } from "react-hook-form" - -import { DataGridBulkUpdateCommand, DataGridMatrix } from "../models" -import { DataGridCoordinates } from "../types" - -type UseDataGridClipboardEventsOptions< - TData, - TFieldValues extends FieldValues -> = { - matrix: DataGridMatrix - isEditing: boolean - anchor: DataGridCoordinates | null - rangeEnd: DataGridCoordinates | null - getSelectionValues: ( - fields: string[] - ) => PathValue>[] +import { useCallback } from 'react'; + +import { DataGridBulkUpdateCommand, type DataGridMatrix } from '@components/data-grid/models'; +import type { DataGridCoordinates } from '@components/data-grid/types'; +import type { FieldValues, Path, PathValue } from 'react-hook-form'; + +type UseDataGridClipboardEventsOptions = { + matrix: DataGridMatrix; + isEditing: boolean; + anchor: DataGridCoordinates | null; + rangeEnd: DataGridCoordinates | null; + getSelectionValues: (fields: string[]) => PathValue>[]; setSelectionValues: ( fields: string[], values: PathValue>[] - ) => void - execute: (command: DataGridBulkUpdateCommand) => void -} - -export const useDataGridClipboardEvents = < - TData, - TFieldValues extends FieldValues ->({ + ) => void; + execute: (command: DataGridBulkUpdateCommand) => void; +}; + +export const useDataGridClipboardEvents = ({ matrix, anchor, rangeEnd, isEditing, getSelectionValues, setSelectionValues, - execute, + execute }: UseDataGridClipboardEventsOptions) => { const handleCopyEvent = useCallback( (e: ClipboardEvent) => { if (isEditing || !anchor || !rangeEnd) { - return + return; } - e.preventDefault() + e.preventDefault(); - const fields = matrix.getFieldsInSelection(anchor, rangeEnd) - const values = getSelectionValues(fields) + const fields = matrix.getFieldsInSelection(anchor, rangeEnd); + const values = getSelectionValues(fields); const text = values - .map((value) => { - if (typeof value === "object" && value !== null) { - return JSON.stringify(value) + .map(value => { + if (typeof value === 'object' && value !== null) { + return JSON.stringify(value); } - return `${value}` ?? "" + + return `${value}` ?? ''; }) - .join("\t") + .join('\t'); - e.clipboardData?.setData("text/plain", text) + e.clipboardData?.setData('text/plain', text); }, [isEditing, anchor, rangeEnd, matrix, getSelectionValues] - ) + ); const handlePasteEvent = useCallback( (e: ClipboardEvent) => { if (isEditing || !anchor || !rangeEnd) { - return + return; } - e.preventDefault() + e.preventDefault(); - const text = e.clipboardData?.getData("text/plain") + const text = e.clipboardData?.getData('text/plain'); if (!text) { - return + return; } - const next = text.split("\t") + const next = text.split('\t'); - const fields = matrix.getFieldsInSelection(anchor, rangeEnd) - const prev = getSelectionValues(fields) + const fields = matrix.getFieldsInSelection(anchor, rangeEnd); + const prev = getSelectionValues(fields); const command = new DataGridBulkUpdateCommand({ fields, next, prev, - setter: setSelectionValues, - }) + setter: setSelectionValues + }); - execute(command) + execute(command); }, - [ - isEditing, - anchor, - rangeEnd, - matrix, - getSelectionValues, - setSelectionValues, - execute, - ] - ) + [isEditing, anchor, rangeEnd, matrix, getSelectionValues, setSelectionValues, execute] + ); return { handleCopyEvent, - handlePasteEvent, - } -} + handlePasteEvent + }; +}; diff --git a/src/components/data-grid/hooks/use-data-grid-column-visibility.tsx b/src/components/data-grid/hooks/use-data-grid-column-visibility.tsx index 27714bda..0e4091c1 100644 --- a/src/components/data-grid/hooks/use-data-grid-column-visibility.tsx +++ b/src/components/data-grid/hooks/use-data-grid-column-visibility.tsx @@ -1,68 +1,72 @@ -import type { Column, Table } from "@tanstack/react-table" -import { useCallback } from "react" -import type { FieldValues } from "react-hook-form" +import { useCallback } from 'react'; -import { DataGridMatrix } from "../models" -import { GridColumnOption } from "../types" +import type { DataGridMatrix } from '@components/data-grid/models'; +import type { GridColumnOption } from '@components/data-grid/types'; +import type { Column, Table } from '@tanstack/react-table'; +import type { FieldValues } from 'react-hook-form'; -export function useDataGridColumnVisibility< - TData, - TFieldValues extends FieldValues ->(grid: Table, matrix: DataGridMatrix) { - const columns = grid.getAllLeafColumns() +export function useDataGridColumnVisibility( + grid: Table, + matrix: DataGridMatrix +) { + const columns = grid.getAllLeafColumns(); - const columnOptions: GridColumnOption[] = columns.map((column) => ({ + const columnOptions: GridColumnOption[] = columns.map(column => ({ id: column.id, name: getColumnName(column), checked: column.getIsVisible(), - disabled: !column.getCanHide(), - })) + disabled: !column.getCanHide() + })); const handleToggleColumn = useCallback( + //@todo fix this + // eslint-disable-next-line react-hooks/preserve-manual-memoization (index: number) => (value: boolean) => { - const column = columns[index] + const column = columns[index]; if (!column.getCanHide()) { - return + return; } - matrix.toggleColumn(index, value) - column.toggleVisibility(value) + matrix.toggleColumn(index, value); + column.toggleVisibility(value); }, + //@todo fix this + // eslint-disable-next-line react-hooks/preserve-manual-memoization [columns, matrix] - ) + ); const handleResetColumns = useCallback(() => { - grid.setColumnVisibility({}) - }, [grid]) + grid.setColumnVisibility({}); + }, [grid]); - const optionCount = columnOptions.filter((c) => !c.disabled).length - const isDisabled = optionCount === 0 + const optionCount = columnOptions.filter(c => !c.disabled).length; + const isDisabled = optionCount === 0; return { columnOptions, handleToggleColumn, handleResetColumns, - isDisabled, - } + isDisabled + }; } function getColumnName(column: Column): string { - const id = column.columnDef.id - const enableHiding = column.columnDef.enableHiding - const meta = column?.columnDef.meta as { name?: string } | undefined + const id = column.columnDef.id; + const enableHiding = column.columnDef.enableHiding; + const meta = column?.columnDef.meta as { name?: string } | undefined; if (!id) { throw new Error( - "Column is missing an id, which is a required field. Please provide an id for the column." - ) + 'Column is missing an id, which is a required field. Please provide an id for the column.' + ); } - if (process.env.NODE_ENV === "development" && !meta?.name && enableHiding) { + if (process.env.NODE_ENV === 'development' && !meta?.name && enableHiding) { console.warn( `Column "${id}" does not have a name. You should add a name to the column definition. Falling back to the column id.` - ) + ); } - return meta?.name || id + return meta?.name || id; } diff --git a/src/components/data-grid/hooks/use-data-grid-duplicate-cell.tsx b/src/components/data-grid/hooks/use-data-grid-duplicate-cell.tsx index 4244466c..8cc989bc 100644 --- a/src/components/data-grid/hooks/use-data-grid-duplicate-cell.tsx +++ b/src/components/data-grid/hooks/use-data-grid-duplicate-cell.tsx @@ -1,18 +1,16 @@ -import { useWatch } from "react-hook-form" -import { useDataGridContext } from "../context" +import { useDataGridContext } from '@components/data-grid/context'; +import { useWatch } from 'react-hook-form'; interface UseDataGridDuplicateCellOptions { - duplicateOf: string + duplicateOf: string; } -export const useDataGridDuplicateCell = ({ - duplicateOf, -}: UseDataGridDuplicateCellOptions) => { - const { control } = useDataGridContext() +export const useDataGridDuplicateCell = ({ duplicateOf }: UseDataGridDuplicateCellOptions) => { + const { control } = useDataGridContext(); - const watchedValue = useWatch({ control, name: duplicateOf }) + const watchedValue = useWatch({ control, name: duplicateOf }); return { - watchedValue, - } -} + watchedValue + }; +}; diff --git a/src/components/data-grid/hooks/use-data-grid-error-highlighting.tsx b/src/components/data-grid/hooks/use-data-grid-error-highlighting.tsx index 393b7b67..096c6fce 100644 --- a/src/components/data-grid/hooks/use-data-grid-error-highlighting.tsx +++ b/src/components/data-grid/hooks/use-data-grid-error-highlighting.tsx @@ -1,54 +1,46 @@ -import { Table, VisibilityState } from "@tanstack/react-table" -import { useCallback, useMemo, useState } from "react" -import { FieldError, FieldErrors, FieldValues } from "react-hook-form" +import { useCallback, useMemo, useState } from 'react'; -import { DataGridMatrix } from "../models" -import { VisibilitySnapshot } from "../types" +import type { DataGridMatrix } from '@components/data-grid/models'; +import type { VisibilitySnapshot } from '@components/data-grid/types'; +import type { Table, VisibilityState } from '@tanstack/react-table'; +import type { FieldError, FieldErrors, FieldValues } from 'react-hook-form'; -export const useDataGridErrorHighlighting = < - TData, - TFieldValues extends FieldValues ->( +export const useDataGridErrorHighlighting = ( matrix: DataGridMatrix, grid: Table, errors: FieldErrors ) => { - const [isHighlighted, setIsHighlighted] = useState(false) - const [visibilitySnapshot, setVisibilitySnapshot] = - useState(null) + const [isHighlighted, setIsHighlighted] = useState(false); + const [visibilitySnapshot, setVisibilitySnapshot] = useState(null); - const { flatRows } = grid.getRowModel() - const flatColumns = grid.getAllFlatColumns() + const { flatRows } = grid.getRowModel(); + const flatColumns = grid.getAllFlatColumns(); - const errorPaths = findErrorPaths(errors) - const errorCount = errorPaths.length + const errorPaths = findErrorPaths(errors); + const errorCount = errorPaths.length; const { rowsWithErrors, columnsWithErrors } = useMemo(() => { - const rowsWithErrors = new Set() - const columnsWithErrors = new Set() + const rowsWithErrors = new Set(); + const columnsWithErrors = new Set(); - errorPaths.forEach((errorPath) => { + errorPaths.forEach(errorPath => { const rowIndex = matrix.rowAccessors.findIndex( - (accessor) => - accessor && - (errorPath === accessor || errorPath.startsWith(`${accessor}.`)) - ) + accessor => accessor && (errorPath === accessor || errorPath.startsWith(`${accessor}.`)) + ); if (rowIndex !== -1) { - rowsWithErrors.add(rowIndex) + rowsWithErrors.add(rowIndex); } const columnIndex = matrix.columnAccessors.findIndex( - (accessor) => - accessor && - (errorPath === accessor || errorPath.endsWith(`.${accessor}`)) - ) + accessor => accessor && (errorPath === accessor || errorPath.endsWith(`.${accessor}`)) + ); if (columnIndex !== -1) { - columnsWithErrors.add(columnIndex) + columnsWithErrors.add(columnIndex); } - }) + }); - return { rowsWithErrors, columnsWithErrors } - }, [errorPaths, matrix.rowAccessors, matrix.columnAccessors]) + return { rowsWithErrors, columnsWithErrors }; + }, [errorPaths, matrix.rowAccessors, matrix.columnAccessors]); const toggleErrorHighlighting = useCallback( ( @@ -60,74 +52,55 @@ export const useDataGridErrorHighlighting = < if (isHighlighted) { // Clear error highlights if (visibilitySnapshot) { - setRowVisibility(visibilitySnapshot.rows) - setColumnVisibility(visibilitySnapshot.columns) + setRowVisibility(visibilitySnapshot.rows); + setColumnVisibility(visibilitySnapshot.columns); } } else { // Highlight errors setVisibilitySnapshot({ rows: { ...currentRowVisibility }, - columns: { ...currentColumnVisibility }, - }) + columns: { ...currentColumnVisibility } + }); const rowsToHide = flatRows .map((_, index) => { - return !rowsWithErrors.has(index) ? index : undefined + return !rowsWithErrors.has(index) ? index : undefined; }) - .filter((index): index is number => index !== undefined) + .filter((index): index is number => index !== undefined); const columnsToHide = flatColumns .map((column, index) => { - return !columnsWithErrors.has(index) && index !== 0 - ? column.id - : undefined + return !columnsWithErrors.has(index) && index !== 0 ? column.id : undefined; }) - .filter((id): id is string => id !== undefined) + .filter((id): id is string => id !== undefined); - setRowVisibility( - rowsToHide.reduce((acc, row) => ({ ...acc, [row]: false }), {}) - ) + setRowVisibility(rowsToHide.reduce((acc, row) => ({ ...acc, [row]: false }), {})); setColumnVisibility( - columnsToHide.reduce( - (acc, column) => ({ ...acc, [column]: false }), - {} - ) - ) + columnsToHide.reduce((acc, column) => ({ ...acc, [column]: false }), {}) + ); } - setIsHighlighted((prev) => !prev) + setIsHighlighted(prev => !prev); }, - [ - isHighlighted, - visibilitySnapshot, - flatRows, - flatColumns, - rowsWithErrors, - columnsWithErrors, - ] - ) + [isHighlighted, visibilitySnapshot, flatRows, flatColumns, rowsWithErrors, columnsWithErrors] + ); return { errorCount, isHighlighted, - toggleErrorHighlighting, - } -} + toggleErrorHighlighting + }; +}; -function findErrorPaths( - obj: FieldErrors | FieldError, - path: string[] = [] -): string[] { - if (typeof obj !== "object" || obj === null) { - return [] +function findErrorPaths(obj: FieldErrors | FieldError, path: string[] = []): string[] { + if (typeof obj !== 'object' || obj === null) { + return []; } - if ("message" in obj && "type" in obj) { - return [path.join(".")] + if ('message' in obj && 'type' in obj) { + return [path.join('.')]; } - return Object.entries(obj).flatMap(([key, value]) => - findErrorPaths(value, [...path, key]) - ) + return Object.entries(obj).flatMap(([key, value]) => findErrorPaths(value, [...path, key])); } diff --git a/src/components/data-grid/hooks/use-data-grid-form-handlers.tsx b/src/components/data-grid/hooks/use-data-grid-form-handlers.tsx index cf603950..0eadd3d3 100644 --- a/src/components/data-grid/hooks/use-data-grid-form-handlers.tsx +++ b/src/components/data-grid/hooks/use-data-grid-form-handlers.tsx @@ -1,222 +1,231 @@ -import get from "lodash/get" -import set from "lodash/set" -import { useCallback } from "react" -import { FieldValues, Path, PathValue, UseFormReturn } from "react-hook-form" +import { useCallback } from 'react'; -import { DataGridMatrix } from "../models" -import { +import type { DataGridMatrix } from '@components/data-grid/models'; +import type { DataGridColumnType, DataGridCoordinates, - DataGridToggleableNumber, -} from "../types" + DataGridToggleableNumber +} from '@components/data-grid/types'; +import get from 'lodash/get'; +import set from 'lodash/set'; +import type { FieldValues, Path, PathValue, UseFormReturn } from 'react-hook-form'; type UseDataGridFormHandlersOptions = { - matrix: DataGridMatrix - form: UseFormReturn - anchor: DataGridCoordinates | null -} + matrix: DataGridMatrix; + form: UseFormReturn; + anchor: DataGridCoordinates | null; +}; -export const useDataGridFormHandlers = < - TData, - TFieldValues extends FieldValues ->({ +export const useDataGridFormHandlers = ({ matrix, form, - anchor, + anchor }: UseDataGridFormHandlersOptions) => { - const { getValues, reset } = form + const { getValues, reset } = form; const getSelectionValues = useCallback( (fields: string[]): PathValue>[] => { if (!fields.length) { - return [] + return []; } - const allValues = getValues() + const allValues = getValues(); - return fields.map((field) => { - return field.split(".").reduce((obj, key) => obj?.[key], allValues) - }) as PathValue>[] + return fields.map(field => { + return field.split('.').reduce((obj, key) => obj?.[key], allValues); + }) as PathValue>[]; }, [getValues] - ) + ); const setSelectionValues = useCallback( async (fields: string[], values: string[], isHistory?: boolean) => { if (!fields.length || !anchor) { - return + return; } - const type = matrix.getCellType(anchor) + const type = matrix.getCellType(anchor); if (!type) { - return + return; } - const convertedValues = convertArrayToPrimitive(values, type) - const currentValues = getValues() + const convertedValues = convertArrayToPrimitive(values, type); + const currentValues = getValues(); fields.forEach((field, index) => { if (!field) { - return + return; } - const valueIndex = index % values.length - const newValue = convertedValues[valueIndex] + const valueIndex = index % values.length; + const newValue = convertedValues[valueIndex]; - setValue(currentValues, field, newValue, type, isHistory) - }) + setValue(currentValues, field, newValue, type, isHistory); + }); reset(currentValues, { keepDirty: true, keepTouched: true, - keepDefaultValues: true, - }) + keepDefaultValues: true + }); }, [matrix, anchor, getValues, reset] - ) + ); return { getSelectionValues, - setSelectionValues, - } -} + setSelectionValues + }; +}; function convertToNumber(value: string | number): number { - if (typeof value === "number") { - return value + if (typeof value === 'number') { + return value; } - const converted = Number(value) + const converted = Number(value); if (isNaN(converted)) { - throw new Error(`String "${value}" cannot be converted to number.`) + throw new Error(`String "${value}" cannot be converted to number.`); } - return converted + return converted; } function convertToBoolean(value: string | boolean): boolean { - if (typeof value === "boolean") { - return value + if (typeof value === 'boolean') { + return value; } - if (typeof value === "undefined" || value === null) { - return false + if (typeof value === 'undefined' || value === null) { + return false; } - const lowerValue = value.toLowerCase() + const lowerValue = value.toLowerCase(); - if (lowerValue === "true" || lowerValue === "false") { - return lowerValue === "true" + if (lowerValue === 'true' || lowerValue === 'false') { + return lowerValue === 'true'; } - throw new Error(`String "${value}" cannot be converted to boolean.`) + throw new Error(`String "${value}" cannot be converted to boolean.`); } +// @todo fix type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function covertToString(value: any): string { - if (typeof value === "undefined" || value === null) { - return "" + if (typeof value === 'undefined' || value === null) { + return ''; } - return String(value) + return String(value); } - +// @todo fix type +// eslint-disable-next-line @typescript-eslint/no-explicit-any function convertToggleableNumber(value: any): { - quantity: number - checked: boolean - disabledToggle: boolean + quantity: number; + checked: boolean; + disabledToggle: boolean; } { - let obj = value + let obj = value; - if (typeof obj === "string") { + if (typeof obj === 'string') { try { - obj = JSON.parse(obj) - } catch (error) { - throw new Error(`String "${value}" cannot be converted to object.`) + obj = JSON.parse(obj); + } catch (_error) { + throw new Error(`String "${value}" cannot be converted to object.`); } } - return obj + return obj; } -function setValue< - T extends DataGridToggleableNumber = DataGridToggleableNumber ->( +function setValue( + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any currentValues: any, field: string, newValue: T, type: string, isHistory?: boolean ) { - if (type !== "togglable-number") { - set(currentValues, field, newValue) - return + if (type !== 'togglable-number') { + set(currentValues, field, newValue); + + return; } - setValueToggleableNumber(currentValues, field, newValue, isHistory) + setValueToggleableNumber(currentValues, field, newValue, isHistory); } function setValueToggleableNumber( + // @todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any currentValues: any, field: string, newValue: DataGridToggleableNumber, isHistory?: boolean ) { - const currentValue = get(currentValues, field) - const { disabledToggle } = currentValue + const currentValue = get(currentValues, field); + const { disabledToggle } = currentValue; const normalizeQuantity = (value: number | string | null | undefined) => { - if (disabledToggle && value === "") { - return 0 + if (disabledToggle && value === '') { + return 0; } - return value - } + + return value; + }; const determineChecked = (quantity: number | string | null | undefined) => { if (disabledToggle) { - return true + return true; } - return quantity !== "" && quantity != null - } - const quantity = normalizeQuantity(newValue.quantity) + return quantity !== '' && quantity != null; + }; + + const quantity = normalizeQuantity(newValue.quantity); const checked = isHistory ? disabledToggle ? true : newValue.checked - : determineChecked(quantity) + : determineChecked(quantity); set(currentValues, field, { ...currentValue, quantity, - checked, - }) + checked + }); } export function convertArrayToPrimitive( + // @todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any values: any[], type: DataGridColumnType + // @todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any[] { switch (type) { - case "number": - return values.map((v) => { - if (v === "") { - return v + case 'number': + return values.map(v => { + if (v === '') { + return v; } if (v == null) { - return "" + return ''; } - return convertToNumber(v) - }) - case "togglable-number": - return values.map(convertToggleableNumber) - case "boolean": - return values.map(convertToBoolean) - case "text": - return values.map(covertToString) + return convertToNumber(v); + }); + case 'togglable-number': + return values.map(convertToggleableNumber); + case 'boolean': + return values.map(convertToBoolean); + case 'text': + return values.map(covertToString); default: - throw new Error(`Unsupported target type "${type}".`) + throw new Error(`Unsupported target type "${type}".`); } } diff --git a/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx b/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx index 9980db9a..67aa27aa 100644 --- a/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx +++ b/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx @@ -1,54 +1,51 @@ -import React, { useCallback } from "react" +import type React from 'react'; +import { useCallback } from 'react'; + +import { + DataGridBulkUpdateCommand, + DataGridUpdateCommand, + type DataGridMatrix, + type DataGridQueryTool +} from '@components/data-grid/models'; +import type { DataGridCoordinates } from '@components/data-grid/types'; import type { FieldValues, Path, PathValue, UseFormGetValues, - UseFormSetValue, -} from "react-hook-form" -import { - DataGridBulkUpdateCommand, - DataGridMatrix, - DataGridQueryTool, - DataGridUpdateCommand, -} from "../models" -import { DataGridCoordinates } from "../types" + UseFormSetValue +} from 'react-hook-form'; type UseDataGridKeydownEventOptions = { - containerRef: React.RefObject - matrix: DataGridMatrix - anchor: DataGridCoordinates | null - rangeEnd: DataGridCoordinates | null - isEditing: boolean + containerRef: React.RefObject; + matrix: DataGridMatrix; + anchor: DataGridCoordinates | null; + rangeEnd: DataGridCoordinates | null; + isEditing: boolean; scrollToCoordinates: ( coords: DataGridCoordinates, - direction: "horizontal" | "vertical" | "both" - ) => void - setTrapActive: (active: boolean) => void - setSingleRange: (coordinates: DataGridCoordinates | null) => void - setRangeEnd: (coordinates: DataGridCoordinates | null) => void - onEditingChangeHandler: (value: boolean) => void - getValues: UseFormGetValues - setValue: UseFormSetValue - execute: (command: DataGridUpdateCommand | DataGridBulkUpdateCommand) => void - undo: () => void - redo: () => void - queryTool: DataGridQueryTool | null - getSelectionValues: ( - fields: string[] - ) => PathValue>[] - setSelectionValues: (fields: string[], values: string[]) => void - restoreSnapshot: () => void - createSnapshot: (coords: DataGridCoordinates) => void -} - -const ARROW_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"] -const VERTICAL_KEYS = ["ArrowUp", "ArrowDown"] - -export const useDataGridKeydownEvent = < - TData, - TFieldValues extends FieldValues ->({ + direction: 'horizontal' | 'vertical' | 'both' + ) => void; + setTrapActive: (active: boolean) => void; + setSingleRange: (coordinates: DataGridCoordinates | null) => void; + setRangeEnd: (coordinates: DataGridCoordinates | null) => void; + onEditingChangeHandler: (value: boolean) => void; + getValues: UseFormGetValues; + setValue: UseFormSetValue; + execute: (command: DataGridUpdateCommand | DataGridBulkUpdateCommand) => void; + undo: () => void; + redo: () => void; + queryTool: DataGridQueryTool | null; + getSelectionValues: (fields: string[]) => PathValue>[]; + setSelectionValues: (fields: string[], values: string[]) => void; + restoreSnapshot: () => void; + createSnapshot: (coords: DataGridCoordinates) => void; +}; + +const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; +const VERTICAL_KEYS = ['ArrowUp', 'ArrowDown']; + +export const useDataGridKeydownEvent = ({ containerRef, matrix, anchor, @@ -68,15 +65,15 @@ export const useDataGridKeydownEvent = < getSelectionValues, setSelectionValues, restoreSnapshot, - createSnapshot, + createSnapshot }: UseDataGridKeydownEventOptions) => { const handleKeyboardNavigation = useCallback( (e: KeyboardEvent) => { if (!anchor) { - return + return; } - const type = matrix.getCellType(anchor) + const type = matrix.getCellType(anchor); /** * If the user is currently editing a cell, we don't want to @@ -86,13 +83,11 @@ export const useDataGridKeydownEvent = < * keyboard navigation, as we want to allow the user to navigate * away from the cell directly, as you cannot "enter" a boolean cell. */ - if (isEditing && type !== "boolean") { - return + if (isEditing && type !== 'boolean') { + return; } - const direction = VERTICAL_KEYS.includes(e.key) - ? "vertical" - : "horizontal" + const direction = VERTICAL_KEYS.includes(e.key) ? 'vertical' : 'horizontal'; /** * If the user performs a horizontal navigation, we want to @@ -103,210 +98,188 @@ export const useDataGridKeydownEvent = < * to use the rangeEnd as the basis. If the user is not holding shift, * we want to use the anchor as the basis. */ - const basis = - direction === "horizontal" ? anchor : e.shiftKey ? rangeEnd : anchor + const basis = direction === 'horizontal' ? anchor : e.shiftKey ? rangeEnd : anchor; const updater = - direction === "horizontal" - ? setSingleRange - : e.shiftKey - ? setRangeEnd - : setSingleRange + direction === 'horizontal' ? setSingleRange : e.shiftKey ? setRangeEnd : setSingleRange; if (!basis) { - return + return; } - const { row, col } = basis + const { row, col } = basis; const handleNavigation = (coords: DataGridCoordinates) => { - e.preventDefault() - e.stopPropagation() + e.preventDefault(); + e.stopPropagation(); - scrollToCoordinates(coords, direction) - updater(coords) - } + scrollToCoordinates(coords, direction); + updater(coords); + }; - const next = matrix.getValidMovement( - row, - col, - e.key, - e.metaKey || e.ctrlKey - ) + const next = matrix.getValidMovement(row, col, e.key, e.metaKey || e.ctrlKey); - handleNavigation(next) + handleNavigation(next); }, - [ - isEditing, - anchor, - rangeEnd, - scrollToCoordinates, - setSingleRange, - setRangeEnd, - matrix, - ] - ) + [isEditing, anchor, rangeEnd, scrollToCoordinates, setSingleRange, setRangeEnd, matrix] + ); const handleTabKey = useCallback( (e: KeyboardEvent) => { if (!anchor) { - return + return; } - e.preventDefault() - e.stopPropagation() + e.preventDefault(); + e.stopPropagation(); - const { row, col } = anchor + const { row, col } = anchor; - const key = e.shiftKey ? "ArrowLeft" : "ArrowRight" - const direction = "horizontal" + const key = e.shiftKey ? 'ArrowLeft' : 'ArrowRight'; + const direction = 'horizontal'; - const next = matrix.getValidMovement( - row, - col, - key, - e.metaKey || e.ctrlKey - ) + const next = matrix.getValidMovement(row, col, key, e.metaKey || e.ctrlKey); - scrollToCoordinates(next, direction) - setSingleRange(next) + scrollToCoordinates(next, direction); + setSingleRange(next); }, [anchor, scrollToCoordinates, setSingleRange, matrix] - ) + ); const handleUndo = useCallback( (e: KeyboardEvent) => { - e.preventDefault() + e.preventDefault(); if (e.shiftKey) { - redo() - return + redo(); + + return; } - undo() + undo(); }, [redo, undo] - ) + ); const handleSpaceKeyBoolean = useCallback( (anchor: DataGridCoordinates) => { - const end = rangeEnd ?? anchor + const end = rangeEnd ?? anchor; - const fields = matrix.getFieldsInSelection(anchor, end) + const fields = matrix.getFieldsInSelection(anchor, end); - const prev = getSelectionValues(fields) as boolean[] + const prev = getSelectionValues(fields) as boolean[]; - const allChecked = prev.every((value) => value === true) - const next = Array.from({ length: prev.length }, () => !allChecked) + const allChecked = prev.every(value => value); + const next = Array.from({ length: prev.length }, () => !allChecked); const command = new DataGridBulkUpdateCommand({ fields, next, prev, - setter: setSelectionValues, - }) + setter: setSelectionValues + }); - execute(command) + execute(command); }, [rangeEnd, matrix, getSelectionValues, setSelectionValues, execute] - ) + ); const handleSpaceKeyTextOrNumber = useCallback( (anchor: DataGridCoordinates) => { - const field = matrix.getCellField(anchor) - const input = queryTool?.getInput(anchor) + const field = matrix.getCellField(anchor); + const input = queryTool?.getInput(anchor); if (!field || !input) { - return + return; } - createSnapshot(anchor) + createSnapshot(anchor); - const current = getValues(field as Path) - const next = "" + const current = getValues(field as Path); + const next = ''; const command = new DataGridUpdateCommand({ next, prev: current, - setter: (value) => { + setter: value => { setValue(field as Path, value, { shouldDirty: true, - shouldTouch: true, - }) - }, - }) + shouldTouch: true + }); + } + }); - execute(command) + execute(command); - input.focus() + input.focus(); }, [matrix, queryTool, getValues, execute, setValue, createSnapshot] - ) + ); const handleSpaceKeyTogglableNumber = useCallback( (anchor: DataGridCoordinates) => { - const field = matrix.getCellField(anchor) - const input = queryTool?.getInput(anchor) + const field = matrix.getCellField(anchor); + const input = queryTool?.getInput(anchor); if (!field || !input) { - return + return; } - createSnapshot(anchor) + createSnapshot(anchor); - const current = getValues(field as Path) - let checked = current.checked + const current = getValues(field as Path); + let checked = current.checked; // If the toggle is not disabled, then we want to uncheck the toggle. if (!current.disabledToggle) { - checked = false + checked = false; } - const next = { ...current, quantity: "", checked } + const next = { ...current, quantity: '', checked }; const command = new DataGridUpdateCommand({ next, prev: current, - setter: (value) => { + setter: value => { setValue(field as Path, value, { shouldDirty: true, - shouldTouch: true, - }) - }, - }) + shouldTouch: true + }); + } + }); - execute(command) + execute(command); - input.focus() + input.focus(); }, [matrix, queryTool, getValues, execute, setValue, createSnapshot] - ) + ); const handleSpaceKey = useCallback( (e: KeyboardEvent) => { if (!anchor || isEditing) { - return + return; } - e.preventDefault() + e.preventDefault(); - const type = matrix.getCellType(anchor) + const type = matrix.getCellType(anchor); if (!type) { - return + return; } switch (type) { - case "boolean": - handleSpaceKeyBoolean(anchor) - break - case "togglable-number": - handleSpaceKeyTogglableNumber(anchor) - break - case "number": - case "text": - handleSpaceKeyTextOrNumber(anchor) - break + case 'boolean': + handleSpaceKeyBoolean(anchor); + break; + case 'togglable-number': + handleSpaceKeyTogglableNumber(anchor); + break; + case 'number': + case 'text': + handleSpaceKeyTextOrNumber(anchor); + break; } }, [ @@ -315,55 +288,44 @@ export const useDataGridKeydownEvent = < matrix, handleSpaceKeyBoolean, handleSpaceKeyTextOrNumber, - handleSpaceKeyTogglableNumber, + handleSpaceKeyTogglableNumber ] - ) + ); const handleMoveOnEnter = useCallback( (e: KeyboardEvent, anchor: DataGridCoordinates) => { - const direction = e.shiftKey ? "ArrowUp" : "ArrowDown" + const direction = e.shiftKey ? 'ArrowUp' : 'ArrowDown'; - const pos = matrix.getValidMovement( - anchor.row, - anchor.col, - direction, - false - ) + const pos = matrix.getValidMovement(anchor.row, anchor.col, direction, false); if (anchor.row !== pos.row || anchor.col !== pos.col) { - setSingleRange(pos) - scrollToCoordinates(pos, "vertical") + setSingleRange(pos); + scrollToCoordinates(pos, 'vertical'); } else { // If the the user is at the last cell, we want to focus the container of the cell. - const container = queryTool?.getContainer(anchor) + const container = queryTool?.getContainer(anchor); - container?.focus() + container?.focus(); } - onEditingChangeHandler(false) + onEditingChangeHandler(false); }, - [ - queryTool, - matrix, - scrollToCoordinates, - setSingleRange, - onEditingChangeHandler, - ] - ) + [queryTool, matrix, scrollToCoordinates, setSingleRange, onEditingChangeHandler] + ); const handleEditOnEnter = useCallback( (anchor: DataGridCoordinates) => { - const input = queryTool?.getInput(anchor) + const input = queryTool?.getInput(anchor); if (!input) { - return + return; } - input.focus() - onEditingChangeHandler(true) + input.focus(); + onEditingChangeHandler(true); }, [queryTool, onEditingChangeHandler] - ) + ); /** * Handles the enter key for text and number cells. @@ -375,14 +337,15 @@ export const useDataGridKeydownEvent = < const handleEnterKeyTextOrNumber = useCallback( (e: KeyboardEvent, anchor: DataGridCoordinates) => { if (isEditing) { - handleMoveOnEnter(e, anchor) - return + handleMoveOnEnter(e, anchor); + + return; } - handleEditOnEnter(anchor) + handleEditOnEnter(anchor); }, [handleMoveOnEnter, handleEditOnEnter, isEditing] - ) + ); /** * Handles the enter key for boolean cells. @@ -394,147 +357,141 @@ export const useDataGridKeydownEvent = < */ const handleEnterKeyBoolean = useCallback( (e: KeyboardEvent, anchor: DataGridCoordinates) => { - const field = matrix.getCellField(anchor) + const field = matrix.getCellField(anchor); if (!field) { - return + return; } - const current = getValues(field as Path) - let next: boolean - - if (typeof current === "boolean") { - next = !current - } else { - next = true - } + const current = getValues(field as Path); + const next = true; const command = new DataGridUpdateCommand({ next, prev: current, - setter: (value) => { + setter: value => { setValue(field as Path, value, { shouldDirty: true, - shouldTouch: true, - }) - }, - }) + shouldTouch: true + }); + } + }); - execute(command) - handleMoveOnEnter(e, anchor) + execute(command); + handleMoveOnEnter(e, anchor); }, [execute, getValues, handleMoveOnEnter, matrix, setValue] - ) + ); const handleEnterKey = useCallback( (e: KeyboardEvent) => { if (!anchor) { - return + return; } - e.preventDefault() + e.preventDefault(); - const type = matrix.getCellType(anchor) + const type = matrix.getCellType(anchor); switch (type) { - case "togglable-number": - case "text": - case "number": - handleEnterKeyTextOrNumber(e, anchor) - break - case "boolean": { - handleEnterKeyBoolean(e, anchor) - break + case 'togglable-number': + case 'text': + case 'number': + handleEnterKeyTextOrNumber(e, anchor); + break; + case 'boolean': { + handleEnterKeyBoolean(e, anchor); + break; } } }, [anchor, matrix, handleEnterKeyTextOrNumber, handleEnterKeyBoolean] - ) + ); const handleDeleteKeyTogglableNumber = useCallback( (anchor: DataGridCoordinates, rangeEnd: DataGridCoordinates) => { - const fields = matrix.getFieldsInSelection(anchor, rangeEnd) - const prev = getSelectionValues(fields) + const fields = matrix.getFieldsInSelection(anchor, rangeEnd); + const prev = getSelectionValues(fields); - const next = prev.map((value) => ({ + const next = prev.map(value => ({ ...value, - quantity: "", - checked: value.disableToggle ? value.checked : false, - })) + quantity: '', + checked: value.disableToggle ? value.checked : false + })); const command = new DataGridBulkUpdateCommand({ fields, next, prev, - setter: setSelectionValues, - }) + setter: setSelectionValues + }); - execute(command) + execute(command); }, [matrix, getSelectionValues, setSelectionValues, execute] - ) + ); const handleDeleteKeyTextOrNumber = useCallback( (anchor: DataGridCoordinates, rangeEnd: DataGridCoordinates) => { - const fields = matrix.getFieldsInSelection(anchor, rangeEnd) - const prev = getSelectionValues(fields) - const next = Array.from({ length: prev.length }, () => "") + const fields = matrix.getFieldsInSelection(anchor, rangeEnd); + const prev = getSelectionValues(fields); + const next = Array.from({ length: prev.length }, () => ''); const command = new DataGridBulkUpdateCommand({ fields, next, prev, - setter: setSelectionValues, - }) + setter: setSelectionValues + }); - execute(command) + execute(command); }, [matrix, getSelectionValues, setSelectionValues, execute] - ) + ); const handleDeleteKeyBoolean = useCallback( (anchor: DataGridCoordinates, rangeEnd: DataGridCoordinates) => { - const fields = matrix.getFieldsInSelection(anchor, rangeEnd) - const prev = getSelectionValues(fields) - const next = Array.from({ length: prev.length }, () => false) + const fields = matrix.getFieldsInSelection(anchor, rangeEnd); + const prev = getSelectionValues(fields); + const next = Array.from({ length: prev.length }, () => false); const command = new DataGridBulkUpdateCommand({ fields, next, prev, - setter: setSelectionValues, - }) + setter: setSelectionValues + }); - execute(command) + execute(command); }, [execute, getSelectionValues, matrix, setSelectionValues] - ) + ); const handleDeleteKey = useCallback( (e: KeyboardEvent) => { if (!anchor || !rangeEnd || isEditing) { - return + return; } - e.preventDefault() + e.preventDefault(); - const type = matrix.getCellType(anchor) + const type = matrix.getCellType(anchor); if (!type) { - return + return; } switch (type) { - case "text": - case "number": - handleDeleteKeyTextOrNumber(anchor, rangeEnd) - break - case "boolean": - handleDeleteKeyBoolean(anchor, rangeEnd) - break - case "togglable-number": - handleDeleteKeyTogglableNumber(anchor, rangeEnd) - break + case 'text': + case 'number': + handleDeleteKeyTextOrNumber(anchor, rangeEnd); + break; + case 'boolean': + handleDeleteKeyBoolean(anchor, rangeEnd); + break; + case 'togglable-number': + handleDeleteKeyTogglableNumber(anchor, rangeEnd); + break; } }, [ @@ -544,93 +501,100 @@ export const useDataGridKeydownEvent = < matrix, handleDeleteKeyTextOrNumber, handleDeleteKeyBoolean, - handleDeleteKeyTogglableNumber, + handleDeleteKeyTogglableNumber ] - ) + ); const handleEscapeKey = useCallback( (e: KeyboardEvent) => { if (!anchor || !isEditing) { - return + return; } - e.preventDefault() - e.stopPropagation() + e.preventDefault(); + e.stopPropagation(); // try to restore the previous value - restoreSnapshot() + restoreSnapshot(); // Restore focus to the container element - const container = queryTool?.getContainer(anchor) - container?.focus() + const container = queryTool?.getContainer(anchor); + container?.focus(); }, [queryTool, isEditing, anchor, restoreSnapshot] - ) + ); const handleSpecialFocusKeys = useCallback( (e: KeyboardEvent) => { if (!containerRef || isEditing) { - return + return; } - const focusableElements = getFocusableElements(containerRef) + const focusableElements = getFocusableElements(containerRef); const focusElement = (element: HTMLElement | null) => { if (element) { - setTrapActive(false) - element.focus() + setTrapActive(false); + element.focus(); } - } + }; switch (e.key) { - case ".": - focusElement(focusableElements.cancel) - break - case ",": - focusElement(focusableElements.shortcuts) - break + case '.': + focusElement(focusableElements.cancel); + break; + case ',': + focusElement(focusableElements.shortcuts); + break; default: - break + break; } }, [isEditing, setTrapActive, containerRef] - ) + ); const handleKeyDownEvent = useCallback( (e: KeyboardEvent) => { if (ARROW_KEYS.includes(e.key)) { - handleKeyboardNavigation(e) - return + handleKeyboardNavigation(e); + + return; } - if (e.key === "z" && (e.metaKey || e.ctrlKey)) { - handleUndo(e) - return + if (e.key === 'z' && (e.metaKey || e.ctrlKey)) { + handleUndo(e); + + return; } - if (e.key === " ") { - handleSpaceKey(e) - return + if (e.key === ' ') { + handleSpaceKey(e); + + return; } - if (e.key === "Delete" || e.key === "Backspace") { - handleDeleteKey(e) - return + if (e.key === 'Delete' || e.key === 'Backspace') { + handleDeleteKey(e); + + return; } - if (e.key === "Enter") { - handleEnterKey(e) - return + if (e.key === 'Enter') { + handleEnterKey(e); + + return; } - if (e.key === "Escape") { - handleEscapeKey(e) - return + if (e.key === 'Escape') { + handleEscapeKey(e); + + return; } - if (e.key === "Tab") { - handleTabKey(e) - return + if (e.key === 'Tab') { + handleTabKey(e); + + return; } }, [ @@ -640,35 +604,32 @@ export const useDataGridKeydownEvent = < handleSpaceKey, handleEnterKey, handleDeleteKey, - handleTabKey, + handleTabKey ] - ) + ); return { handleKeyDownEvent, - handleSpecialFocusKeys, - } -} + handleSpecialFocusKeys + }; +}; function getFocusableElements(ref: React.RefObject) { const focusableElements = Array.from( - document.querySelectorAll( - "[tabindex], a, button, input, select, textarea" - ) - ) + document.querySelectorAll('[tabindex], a, button, input, select, textarea') + ); - const currentElementIndex = focusableElements.indexOf(ref.current!) + const currentElementIndex = focusableElements.indexOf(ref.current!); - const shortcuts = - currentElementIndex > 0 ? focusableElements[currentElementIndex - 1] : null + const shortcuts = currentElementIndex > 0 ? focusableElements[currentElementIndex - 1] : null; - let cancel = null + let cancel = null; for (let i = currentElementIndex + 1; i < focusableElements.length; i++) { if (!ref.current!.contains(focusableElements[i])) { - cancel = focusableElements[i] - break + cancel = focusableElements[i]; + break; } } - return { shortcuts, cancel } + return { shortcuts, cancel }; } diff --git a/src/components/data-grid/hooks/use-data-grid-mouse-up-event.tsx b/src/components/data-grid/hooks/use-data-grid-mouse-up-event.tsx index f936b193..dc25aecd 100644 --- a/src/components/data-grid/hooks/use-data-grid-mouse-up-event.tsx +++ b/src/components/data-grid/hooks/use-data-grid-mouse-up-event.tsx @@ -1,31 +1,27 @@ -import { useCallback } from "react" -import { FieldValues, Path, PathValue } from "react-hook-form" -import { DataGridBulkUpdateCommand, DataGridMatrix } from "../models" -import { DataGridCoordinates } from "../types" +import { useCallback } from 'react'; + +import { DataGridBulkUpdateCommand, type DataGridMatrix } from '@components/data-grid/models'; +import type { DataGridCoordinates } from '@components/data-grid/types'; +import type { FieldValues, Path, PathValue } from 'react-hook-form'; type UseDataGridMouseUpEventOptions = { - matrix: DataGridMatrix - anchor: DataGridCoordinates | null - dragEnd: DataGridCoordinates | null - setDragEnd: (coords: DataGridCoordinates | null) => void - setRangeEnd: (coords: DataGridCoordinates | null) => void - setIsSelecting: (isSelecting: boolean) => void - setIsDragging: (isDragging: boolean) => void - getSelectionValues: ( - fields: string[] - ) => PathValue>[] + matrix: DataGridMatrix; + anchor: DataGridCoordinates | null; + dragEnd: DataGridCoordinates | null; + setDragEnd: (coords: DataGridCoordinates | null) => void; + setRangeEnd: (coords: DataGridCoordinates | null) => void; + setIsSelecting: (isSelecting: boolean) => void; + setIsDragging: (isDragging: boolean) => void; + getSelectionValues: (fields: string[]) => PathValue>[]; setSelectionValues: ( fields: string[], values: PathValue>[] - ) => void - execute: (command: DataGridBulkUpdateCommand) => void - isDragging: boolean -} + ) => void; + execute: (command: DataGridBulkUpdateCommand) => void; + isDragging: boolean; +}; -export const useDataGridMouseUpEvent = < - TData, - TFieldValues extends FieldValues ->({ +export const useDataGridMouseUpEvent = ({ matrix, anchor, dragEnd, @@ -36,42 +32,42 @@ export const useDataGridMouseUpEvent = < setIsSelecting, getSelectionValues, setSelectionValues, - execute, + execute }: UseDataGridMouseUpEventOptions) => { const handleDragEnd = useCallback(() => { if (!isDragging) { - return + return; } if (!anchor || !dragEnd) { - return + return; } - const dragSelection = matrix.getFieldsInSelection(anchor, dragEnd) - const anchorField = matrix.getCellField(anchor) + const dragSelection = matrix.getFieldsInSelection(anchor, dragEnd); + const anchorField = matrix.getCellField(anchor); if (!anchorField || !dragSelection.length) { - return + return; } - const anchorValue = getSelectionValues([anchorField]) - const fields = dragSelection.filter((field) => field !== anchorField) + const anchorValue = getSelectionValues([anchorField]); + const fields = dragSelection.filter(field => field !== anchorField); - const prev = getSelectionValues(fields) - const next = Array.from({ length: prev.length }, () => anchorValue[0]) + const prev = getSelectionValues(fields); + const next = Array.from({ length: prev.length }, () => anchorValue[0]); const command = new DataGridBulkUpdateCommand({ fields, prev, next, - setter: setSelectionValues, - }) + setter: setSelectionValues + }); - execute(command) + execute(command); - setIsDragging(false) - setDragEnd(null) + setIsDragging(false); + setDragEnd(null); - setRangeEnd(dragEnd) + setRangeEnd(dragEnd); }, [ isDragging, anchor, @@ -82,15 +78,15 @@ export const useDataGridMouseUpEvent = < execute, setIsDragging, setDragEnd, - setRangeEnd, - ]) + setRangeEnd + ]); const handleMouseUpEvent = useCallback(() => { - handleDragEnd() - setIsSelecting(false) - }, [handleDragEnd, setIsSelecting]) + handleDragEnd(); + setIsSelecting(false); + }, [handleDragEnd, setIsSelecting]); return { - handleMouseUpEvent, - } -} + handleMouseUpEvent + }; +}; diff --git a/src/components/data-grid/hooks/use-data-grid-navigation.tsx b/src/components/data-grid/hooks/use-data-grid-navigation.tsx index 598ebe40..36724d2d 100644 --- a/src/components/data-grid/hooks/use-data-grid-navigation.tsx +++ b/src/components/data-grid/hooks/use-data-grid-navigation.tsx @@ -1,22 +1,23 @@ -import { Column, Row, VisibilityState } from "@tanstack/react-table" -import { ScrollToOptions, Virtualizer } from "@tanstack/react-virtual" -import { Dispatch, SetStateAction, useCallback } from "react" -import { FieldValues } from "react-hook-form" -import { DataGridMatrix, DataGridQueryTool } from "../models" -import { DataGridCoordinates } from "../types" +import { useCallback, type Dispatch, type SetStateAction } from 'react'; + +import type { DataGridMatrix, DataGridQueryTool } from '@components/data-grid/models'; +import type { DataGridCoordinates } from '@components/data-grid/types'; +import type { Column, Row, VisibilityState } from '@tanstack/react-table'; +import type { ScrollToOptions, Virtualizer } from '@tanstack/react-virtual'; +import type { FieldValues } from 'react-hook-form'; type UseDataGridNavigationOptions = { - matrix: DataGridMatrix - anchor: DataGridCoordinates | null - visibleRows: Row[] - visibleColumns: Column[] - rowVirtualizer: Virtualizer - columnVirtualizer: Virtualizer - setColumnVisibility: Dispatch> - flatColumns: Column[] - queryTool: DataGridQueryTool | null - setSingleRange: (coords: DataGridCoordinates | null) => void -} + matrix: DataGridMatrix; + anchor: DataGridCoordinates | null; + visibleRows: Row[]; + visibleColumns: Column[]; + rowVirtualizer: Virtualizer; + columnVirtualizer: Virtualizer; + setColumnVisibility: Dispatch>; + flatColumns: Column[]; + queryTool: DataGridQueryTool | null; + setSingleRange: (coords: DataGridCoordinates | null) => void; +}; export const useDataGridNavigation = ({ matrix, @@ -28,86 +29,82 @@ export const useDataGridNavigation = ({ setColumnVisibility, flatColumns, queryTool, - setSingleRange, + setSingleRange }: UseDataGridNavigationOptions) => { const scrollToCoordinates = useCallback( - (coords: DataGridCoordinates, direction: "horizontal" | "vertical" | "both") => { + (coords: DataGridCoordinates, direction: 'horizontal' | 'vertical' | 'both') => { if (!anchor) { - return + return; } - const { row, col } = coords - const { row: anchorRow, col: anchorCol } = anchor + const { row, col } = coords; + const { row: anchorRow, col: anchorCol } = anchor; - const rowDirection = row >= anchorRow ? "down" : "up" - const colDirection = col >= anchorCol ? "right" : "left" + const rowDirection = row >= anchorRow ? 'down' : 'up'; + const colDirection = col >= anchorCol ? 'right' : 'left'; - let toRow = rowDirection === "down" ? row + 1 : row - 1 + let toRow = rowDirection === 'down' ? row + 1 : row - 1; if (visibleRows[toRow] === undefined) { - toRow = row + toRow = row; } - let toCol = colDirection === "right" ? col + 1 : col - 1 + let toCol = colDirection === 'right' ? col + 1 : col - 1; if (visibleColumns[toCol] === undefined) { - toCol = col + toCol = col; } - const scrollOptions: ScrollToOptions = { align: "auto", behavior: "auto" } + const scrollOptions: ScrollToOptions = { + align: 'auto', + behavior: 'auto' + }; - if (direction === "horizontal" || direction === "both") { - columnVirtualizer.scrollToIndex(toCol, scrollOptions) + if (direction === 'horizontal' || direction === 'both') { + columnVirtualizer.scrollToIndex(toCol, scrollOptions); } - if (direction === "vertical" || direction === "both") { - rowVirtualizer.scrollToIndex(toRow, scrollOptions) + if (direction === 'vertical' || direction === 'both') { + rowVirtualizer.scrollToIndex(toRow, scrollOptions); } }, [anchor, columnVirtualizer, visibleRows, rowVirtualizer, visibleColumns] - ) + ); const navigateToField = useCallback( (field: string) => { - const coords = matrix.getCoordinatesByField(field) + const coords = matrix.getCoordinatesByField(field); if (!coords) { - return + return; } - const column = flatColumns[coords.col] + const column = flatColumns[coords.col]; // Ensure that the column is visible - setColumnVisibility((prev) => { + setColumnVisibility(prev => { return { ...prev, - [column.id]: true, - } - }) + [column.id]: true + }; + }); requestAnimationFrame(() => { - scrollToCoordinates(coords, "both") - setSingleRange(coords) - }) + scrollToCoordinates(coords, 'both'); + setSingleRange(coords); + }); requestAnimationFrame(() => { - const input = queryTool?.getInput(coords) + const input = queryTool?.getInput(coords); if (input) { - input.focus() + input.focus(); } - }) + }); }, - [ - matrix, - flatColumns, - setColumnVisibility, - scrollToCoordinates, - setSingleRange, - queryTool, - ] - ) + [matrix, flatColumns, setColumnVisibility, scrollToCoordinates, setSingleRange, queryTool] + ); return { scrollToCoordinates, - navigateToField, - } -} + navigateToField + }; +}; diff --git a/src/components/data-grid/hooks/use-data-grid-query-tool.tsx b/src/components/data-grid/hooks/use-data-grid-query-tool.tsx index 38470166..a2a03084 100644 --- a/src/components/data-grid/hooks/use-data-grid-query-tool.tsx +++ b/src/components/data-grid/hooks/use-data-grid-query-tool.tsx @@ -1,15 +1,15 @@ -import { RefObject, useEffect, useRef } from "react" +import { useEffect, useRef, type RefObject } from 'react'; -import { DataGridQueryTool } from "../models" +import { DataGridQueryTool } from '@components/data-grid/models'; export const useDataGridQueryTool = (containerRef: RefObject) => { - const queryToolRef = useRef(null) + const queryToolRef = useRef(null); useEffect(() => { if (containerRef.current) { - queryToolRef.current = new DataGridQueryTool(containerRef.current) + queryToolRef.current = new DataGridQueryTool(containerRef.current); } - }, [containerRef]) + }, [containerRef]); - return queryToolRef.current -} + return queryToolRef.current; +}; diff --git a/src/components/data-grid/index.ts b/src/components/data-grid/index.ts index fd72f65b..c7d278b2 100644 --- a/src/components/data-grid/index.ts +++ b/src/components/data-grid/index.ts @@ -1,2 +1,2 @@ -export * from "./data-grid" -export * from "./helpers" +export * from './data-grid'; +export * from './helpers'; diff --git a/src/components/data-grid/models/data-grid-bulk-update-command.ts b/src/components/data-grid/models/data-grid-bulk-update-command.ts index 426b7ea4..9c491c55 100644 --- a/src/components/data-grid/models/data-grid-bulk-update-command.ts +++ b/src/components/data-grid/models/data-grid-bulk-update-command.ts @@ -1,38 +1,36 @@ -import { Command } from "../../../hooks/use-command-history" +// @todo fix types +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Command } from '@hooks/use-command-history.tsx'; export type DataGridBulkUpdateCommandArgs = { - fields: string[] - next: any[] - prev: any[] - setter: (fields: string[], values: any[], isHistory?: boolean) => void -} + fields: string[]; + next: any[]; + prev: any[]; + setter: (fields: string[], values: any[], isHistory?: boolean) => void; +}; export class DataGridBulkUpdateCommand implements Command { - private _fields: string[] + private readonly _fields: string[]; - private _prev: any[] - private _next: any[] + private readonly _prev: any[]; + private readonly _next: any[]; - private _setter: ( - fields: string[], - values: any[], - isHistory?: boolean - ) => void + private readonly _setter: (fields: string[], values: any[], isHistory?: boolean) => void; constructor({ fields, prev, next, setter }: DataGridBulkUpdateCommandArgs) { - this._fields = fields - this._prev = prev - this._next = next - this._setter = setter + this._fields = fields; + this._prev = prev; + this._next = next; + this._setter = setter; } execute(redo = false): void { - this._setter(this._fields, this._next, redo) + this._setter(this._fields, this._next, redo); } undo(): void { - this._setter(this._fields, this._prev, true) + this._setter(this._fields, this._prev, true); } redo(): void { - this.execute(true) + this.execute(true); } } diff --git a/src/components/data-grid/models/data-grid-matrix.ts b/src/components/data-grid/models/data-grid-matrix.ts index bfaf31bd..e9bc9554 100644 --- a/src/components/data-grid/models/data-grid-matrix.ts +++ b/src/components/data-grid/models/data-grid-matrix.ts @@ -1,121 +1,115 @@ -import { ColumnDef, Row } from "@tanstack/react-table" -import { FieldValues } from "react-hook-form" -import { +import type { DataGridColumnType, DataGridCoordinates, Grid, GridCell, - InternalColumnMeta, -} from "../types" + InternalColumnMeta +} from '@components/data-grid/types'; +import type { ColumnDef, Row } from '@tanstack/react-table'; +import type { FieldValues } from 'react-hook-form'; export class DataGridMatrix { - private multiColumnSelection: boolean - private cells: Grid - public rowAccessors: (string | null)[] = [] - public columnAccessors: (string | null)[] = [] + private readonly multiColumnSelection: boolean; + private readonly cells: Grid; + public rowAccessors: (string | null)[] = []; + public columnAccessors: (string | null)[] = []; constructor( data: Row[], columns: ColumnDef[], multiColumnSelection: boolean = false ) { - this.multiColumnSelection = multiColumnSelection - this.cells = this._populateCells(data, columns) + this.multiColumnSelection = multiColumnSelection; + this.cells = this._populateCells(data, columns); - this.rowAccessors = this._computeRowAccessors() - this.columnAccessors = this._computeColumnAccessors() + this.rowAccessors = this._computeRowAccessors(); + this.columnAccessors = this._computeColumnAccessors(); } private _computeRowAccessors(): (string | null)[] { - return this.cells.map((_, rowIndex) => this.getRowAccessor(rowIndex)) + return this.cells.map((_, rowIndex) => this.getRowAccessor(rowIndex)); } private _computeColumnAccessors(): (string | null)[] { if (this.cells.length === 0) { - return [] + return []; } - return this.cells[0].map((_, colIndex) => this.getColumnAccessor(colIndex)) + return this.cells[0].map((_, colIndex) => this.getColumnAccessor(colIndex)); } getFirstNavigableCell(): DataGridCoordinates | null { for (let row = 0; row < this.cells.length; row++) { for (let col = 0; col < this.cells[0].length; col++) { if (this.cells[row][col] !== null) { - return { row, col } + return { row, col }; } } } - return null + return null; } getFieldsInRow(row: number): string[] { - const keys: string[] = [] + const keys: string[] = []; if (row < 0 || row >= this.cells.length) { - return keys + return keys; } - this.cells[row].forEach((cell) => { + this.cells[row].forEach(cell => { if (cell !== null) { - keys.push(cell.field) + keys.push(cell.field); } - }) + }); - return keys + return keys; } getFieldsInSelection( start: DataGridCoordinates | null, end: DataGridCoordinates | null ): string[] { - const keys: string[] = [] + const keys: string[] = []; if (!start || !end) { - return keys + return keys; } if (!this.multiColumnSelection && start.col !== end.col) { - throw new Error( - "Selection must be in the same column when multiColumnSelection is disabled" - ) + throw new Error('Selection must be in the same column when multiColumnSelection is disabled'); } - const startRow = Math.min(start.row, end.row) - const endRow = Math.max(start.row, end.row) - const startCol = this.multiColumnSelection - ? Math.min(start.col, end.col) - : start.col - const endCol = this.multiColumnSelection - ? Math.max(start.col, end.col) - : start.col + const startRow = Math.min(start.row, end.row); + const endRow = Math.max(start.row, end.row); + const startCol = this.multiColumnSelection ? Math.min(start.col, end.col) : start.col; + const endCol = this.multiColumnSelection ? Math.max(start.col, end.col) : start.col; for (let row = startRow; row <= endRow; row++) { for (let col = startCol; col <= endCol; col++) { if (this._isValidPosition(row, col) && this.cells[row][col] !== null) { - keys.push(this.cells[row][col]?.field as string) + keys.push(this.cells[row][col]?.field as string); } } } - return keys + return keys; } getCellField(cell: DataGridCoordinates): string | null { if (this._isValidPosition(cell.row, cell.col)) { - return this.cells[cell.row][cell.col]?.field || null + return this.cells[cell.row][cell.col]?.field || null; } - return null + return null; } getCellType(cell: DataGridCoordinates): DataGridColumnType | null { if (this._isValidPosition(cell.row, cell.col)) { - return this.cells[cell.row][cell.col]?.type || null + return this.cells[cell.row][cell.col]?.type || null; } - return null + return null; } getIsCellSelected( @@ -124,174 +118,161 @@ export class DataGridMatrix { end: DataGridCoordinates | null ): boolean { if (!cell || !start || !end) { - return false + return false; } if (!this.multiColumnSelection && start.col !== end.col) { - throw new Error( - "Selection must be in the same column when multiColumnSelection is disabled" - ) + throw new Error('Selection must be in the same column when multiColumnSelection is disabled'); } - const startRow = Math.min(start.row, end.row) - const endRow = Math.max(start.row, end.row) - const startCol = this.multiColumnSelection - ? Math.min(start.col, end.col) - : start.col - const endCol = this.multiColumnSelection - ? Math.max(start.col, end.col) - : start.col - - return ( - cell.row >= startRow && - cell.row <= endRow && - cell.col >= startCol && - cell.col <= endCol - ) + const startRow = Math.min(start.row, end.row); + const endRow = Math.max(start.row, end.row); + const startCol = this.multiColumnSelection ? Math.min(start.col, end.col) : start.col; + const endCol = this.multiColumnSelection ? Math.max(start.col, end.col) : start.col; + + return cell.row >= startRow && cell.row <= endRow && cell.col >= startCol && cell.col <= endCol; } toggleColumn(col: number, enabled: boolean) { if (col < 0 || col >= this.cells[0].length) { - return + return; } this.cells.forEach((row, index) => { - const cell = row[col] + const cell = row[col]; if (cell) { this.cells[index][col] = { ...cell, - enabled, - } + enabled + }; } - }) + }); } toggleRow(row: number, enabled: boolean) { if (row < 0 || row >= this.cells.length) { - return + return; } this.cells[row].forEach((cell, index) => { if (cell) { this.cells[row][index] = { ...cell, - enabled, - } + enabled + }; } - }) + }); } getCoordinatesByField(field: string): DataGridCoordinates | null { if (this.rowAccessors.length === 1) { - const col = this.columnAccessors.indexOf(field) + const col = this.columnAccessors.indexOf(field); if (col === -1) { - return null + return null; } - return { row: 0, col } + return { row: 0, col }; } for (let row = 0; row < this.rowAccessors.length; row++) { - const rowAccessor = this.rowAccessors[row] + const rowAccessor = this.rowAccessors[row]; if (rowAccessor === null) { - continue + continue; } if (!field.startsWith(rowAccessor)) { - continue + continue; } for (let column = 0; column < this.columnAccessors.length; column++) { - const columnAccessor = this.columnAccessors[column] + const columnAccessor = this.columnAccessors[column]; if (columnAccessor === null) { - continue + continue; } - const fullFieldPath = `${rowAccessor}.${columnAccessor}` + const fullFieldPath = `${rowAccessor}.${columnAccessor}`; if (fullFieldPath === field) { - return { row, col: column } + return { row, col: column }; } } } - return null + return null; } getRowAccessor(row: number): string | null { if (row < 0 || row >= this.cells.length) { - return null + return null; } - const cells = this.cells[row] + const cells = this.cells[row]; const nonNullFields = cells .filter((cell): cell is GridCell => cell !== null) - .map((cell) => cell.field.split(".")) + .map(cell => cell.field.split('.')); if (nonNullFields.length === 0) { - return null + return null; } - let commonParts = nonNullFields[0] + let commonParts = nonNullFields[0]; for (const segments of nonNullFields) { - commonParts = commonParts.filter( - (part, index) => segments[index] === part - ) + commonParts = commonParts.filter((part, index) => segments[index] === part); if (commonParts.length === 0) { - break + break; } } - const accessor = commonParts.join(".") + const accessor = commonParts.join('.'); if (!accessor) { - return null + return null; } - return accessor + return accessor; } public getColumnAccessor(column: number): string | null { if (column < 0 || column >= this.cells[0].length) { - return null + return null; } // Extract the unique part of the field name for each row in the specified column const uniqueParts = this.cells .map((row, rowIndex) => { - const cell = row[column] + const cell = row[column]; if (!cell) { - return null + return null; } // Get the row accessor for the current row - const rowAccessor = this.getRowAccessor(rowIndex) + const rowAccessor = this.getRowAccessor(rowIndex); // Remove the row accessor part from the field name - if (rowAccessor && cell.field.startsWith(rowAccessor + ".")) { - return cell.field.slice(rowAccessor.length + 1) // Extract the part after the row accessor + if (rowAccessor && cell.field.startsWith(rowAccessor + '.')) { + return cell.field.slice(rowAccessor.length + 1); // Extract the part after the row accessor } - return null + return null; }) - .filter((part) => part !== null) // Filter out null values + .filter(part => part !== null); // Filter out null values if (uniqueParts.length === 0) { - return null + return null; } // Ensure all unique parts are the same (this should be true for well-formed data) - const firstPart = uniqueParts[0] - const isConsistent = uniqueParts.every((part) => part === firstPart) + const firstPart = uniqueParts[0]; + const isConsistent = uniqueParts.every(part => part === firstPart); - return isConsistent ? firstPart : null + return isConsistent ? firstPart : null; } getValidMovement( @@ -300,53 +281,46 @@ export class DataGridMatrix { direction: string, metaKey: boolean = false ): DataGridCoordinates { - const [dRow, dCol] = this._getDirectionDeltas(direction) + const [dRow, dCol] = this._getDirectionDeltas(direction); if (metaKey) { - return this._getLastValidCellInDirection(row, col, dRow, dCol) + return this._getLastValidCellInDirection(row, col, dRow, dCol); } else { - let newRow = row + dRow - let newCol = col + dCol + let newRow = row + dRow; + let newCol = col + dCol; while (this._isValidPosition(newRow, newCol)) { - if ( - this.cells[newRow][newCol] !== null && - this.cells[newRow][newCol]?.enabled !== false - ) { - return { row: newRow, col: newCol } + if (this.cells[newRow][newCol] !== null && this.cells[newRow][newCol]?.enabled !== false) { + return { row: newRow, col: newCol }; } - newRow += dRow - newCol += dCol + newRow += dRow; + newCol += dCol; } - return { row, col } + return { row, col }; } } - private _isValidPosition( - row: number, - col: number, - cells?: Grid - ): boolean { + private _isValidPosition(row: number, col: number, cells?: Grid): boolean { if (!cells) { - cells = this.cells + cells = this.cells; } - return row >= 0 && row < cells.length && col >= 0 && col < cells[0].length + return row >= 0 && row < cells.length && col >= 0 && col < cells[0].length; } private _getDirectionDeltas(direction: string): [number, number] { switch (direction) { - case "ArrowUp": - return [-1, 0] - case "ArrowDown": - return [1, 0] - case "ArrowLeft": - return [0, -1] - case "ArrowRight": - return [0, 1] + case 'ArrowUp': + return [-1, 0]; + case 'ArrowDown': + return [1, 0]; + case 'ArrowLeft': + return [0, -1]; + case 'ArrowRight': + return [0, 1]; default: - return [0, 0] + return [0, 0]; } } @@ -356,35 +330,35 @@ export class DataGridMatrix { dRow: number, dCol: number ): DataGridCoordinates { - let newRow = row - let newCol = col - let lastValidRow = row - let lastValidCol = col + let newRow = row; + let newCol = col; + let lastValidRow = row; + let lastValidCol = col; while (this._isValidPosition(newRow + dRow, newCol + dCol)) { - newRow += dRow - newCol += dCol + newRow += dRow; + newCol += dCol; if (this.cells[newRow][newCol] !== null) { - lastValidRow = newRow - lastValidCol = newCol + lastValidRow = newRow; + lastValidCol = newCol; } } return { row: lastValidRow, - col: lastValidCol, - } + col: lastValidCol + }; } private _populateCells(rows: Row[], columns: ColumnDef[]) { const cells = Array.from({ length: rows.length }, () => Array(columns.length).fill(null) - ) as Grid + ) as Grid; rows.forEach((row, rowIndex) => { columns.forEach((column, colIndex) => { if (!this._isValidPosition(rowIndex, colIndex, cells)) { - return + return; } const { @@ -392,30 +366,30 @@ export class DataGridMatrix { field, type, ...rest - } = column.meta as InternalColumnMeta + } = column.meta as InternalColumnMeta; const context = { row, column: { ...column, - meta: rest, - }, - } + meta: rest + } + }; - const fieldValue = field ? field(context) : null + const fieldValue = field ? field(context) : null; if (!fieldValue || !type) { - return + return; } cells[rowIndex][colIndex] = { field: fieldValue, type, - enabled: true, - } - }) - }) + enabled: true + }; + }); + }); - return cells + return cells; } } diff --git a/src/components/data-grid/models/data-grid-query-tool.ts b/src/components/data-grid/models/data-grid-query-tool.ts index 61d50742..973eda74 100644 --- a/src/components/data-grid/models/data-grid-query-tool.ts +++ b/src/components/data-grid/models/data-grid-query-tool.ts @@ -1,74 +1,70 @@ -import { DataGridCoordinates } from "../types" -import { generateCellId } from "../utils" +import type { DataGridCoordinates } from '@components/data-grid/types'; +import { generateCellId } from '@components/data-grid/utils'; export class DataGridQueryTool { - private container: HTMLElement | null + private container: HTMLElement | null; constructor(container: HTMLElement | null) { - this.container = container + this.container = container; } getInput(cell: DataGridCoordinates) { - const id = this._getCellId(cell) + const id = this._getCellId(cell); - const input = this.container?.querySelector(`[data-cell-id="${id}"]`) + const input = this.container?.querySelector(`[data-cell-id="${id}"]`); if (!input) { - return null + return null; } - return input as HTMLElement + return input as HTMLElement; } getInputByField(field: string) { - const input = this.container?.querySelector(`[data-field="${field}"]`) + const input = this.container?.querySelector(`[data-field="${field}"]`); if (!input) { - return null + return null; } - return input as HTMLElement + return input as HTMLElement; } getCoordinatesByField(field: string): DataGridCoordinates | null { - const cell = this.container?.querySelector( - `[data-field="${field}"][data-cell-id]` - ) + const cell = this.container?.querySelector(`[data-field="${field}"][data-cell-id]`); if (!cell) { - return null + return null; } - const cellId = cell.getAttribute("data-cell-id") + const cellId = cell.getAttribute('data-cell-id'); if (!cellId) { - return null + return null; } - const [row, col] = cellId.split(":").map((n) => parseInt(n, 10)) + const [row, col] = cellId.split(':').map(n => parseInt(n, 10)); if (isNaN(row) || isNaN(col)) { - return null + return null; } - return { row, col } + return { row, col }; } getContainer(cell: DataGridCoordinates) { - const id = this._getCellId(cell) + const id = this._getCellId(cell); - const container = this.container?.querySelector( - `[data-container-id="${id}"]` - ) + const container = this.container?.querySelector(`[data-container-id="${id}"]`); if (!container) { - return null + return null; } - return container as HTMLElement + return container as HTMLElement; } private _getCellId(cell: DataGridCoordinates): string { - return generateCellId(cell) + return generateCellId(cell); } -} \ No newline at end of file +} diff --git a/src/components/data-grid/models/data-grid-update-command.ts b/src/components/data-grid/models/data-grid-update-command.ts index 30b0f4be..fe348415 100644 --- a/src/components/data-grid/models/data-grid-update-command.ts +++ b/src/components/data-grid/models/data-grid-update-command.ts @@ -1,33 +1,35 @@ -import { Command } from "../../../hooks/use-command-history" +// @todo fix types +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { Command } from '@hooks/use-command-history.tsx'; export type DataGridUpdateCommandArgs = { - prev: any - next: any - setter: (value: any) => void -} + prev: any; + next: any; + setter: (value: any) => void; +}; export class DataGridUpdateCommand implements Command { - private _prev: any - private _next: any + private _prev: any; + private _next: any; - private _setter: (value: any) => void + private _setter: (value: any) => void; constructor({ prev, next, setter }: DataGridUpdateCommandArgs) { - this._prev = prev - this._next = next + this._prev = prev; + this._next = next; - this._setter = setter + this._setter = setter; } execute(): void { - this._setter(this._next) + this._setter(this._next); } undo(): void { - this._setter(this._prev) + this._setter(this._prev); } redo(): void { - this.execute() + this.execute(); } -} \ No newline at end of file +} diff --git a/src/components/data-grid/models/index.ts b/src/components/data-grid/models/index.ts index 976bfad4..e4425f6e 100644 --- a/src/components/data-grid/models/index.ts +++ b/src/components/data-grid/models/index.ts @@ -1,5 +1,4 @@ -export * from "./data-grid-bulk-update-command" -export * from "./data-grid-matrix" -export * from "./data-grid-query-tool" -export * from "./data-grid-update-command" - +export * from './data-grid-bulk-update-command'; +export * from './data-grid-matrix'; +export * from './data-grid-query-tool'; +export * from './data-grid-update-command'; diff --git a/src/components/data-grid/types.ts b/src/components/data-grid/types.ts index 8ddc4a41..38c4c983 100644 --- a/src/components/data-grid/types.ts +++ b/src/components/data-grid/types.ts @@ -1,32 +1,26 @@ -import { +// @todo fix types +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { PropsWithChildren, ReactNode, RefObject } from 'react'; +import type React from 'react'; + +import type { CellContext, ColumnDef, ColumnMeta, Row, - VisibilityState, -} from "@tanstack/react-table" -import React, { PropsWithChildren, ReactNode, RefObject } from "react" -import { - FieldErrors, - FieldPath, - FieldValues, - Path, - PathValue, -} from "react-hook-form" - -export type DataGridColumnType = - | "text" - | "number" - | "boolean" - | "togglable-number" + VisibilityState +} from '@tanstack/react-table'; +import type { FieldErrors, FieldPath, FieldValues, Path, PathValue } from 'react-hook-form'; + +export type DataGridColumnType = 'text' | 'number' | 'boolean' | 'togglable-number'; export type DataGridCoordinates = { - row: number - col: number -} + row: number; + col: number; +}; export interface DataGridCellProps { - context: CellContext + context: CellContext; } export interface DataGridCellContext @@ -34,139 +28,136 @@ export interface DataGridCellContext /** * The index of the column in the grid. */ - columnIndex: number + columnIndex: number; /** * The index of the row in the grid. */ - rowIndex: number + rowIndex: number; } export type DataGridRowError = { - message: string - to: () => void -} + message: string; + to: () => void; +}; export type DataGridErrorRenderProps = { - errors: FieldErrors - rowErrors: DataGridRowError[] -} + errors: FieldErrors; + rowErrors: DataGridRowError[]; +}; export interface DataGridCellRenderProps { - container: DataGridCellContainerProps - input: InputProps + container: DataGridCellContainerProps; + input: InputProps; } type InputAttributes = { - "data-row": number - "data-col": number - "data-cell-id": string - "data-field": string -} + 'data-row': number; + 'data-col': number; + 'data-cell-id': string; + 'data-field': string; +}; export interface InputProps { - ref: RefObject - onBlur: () => void - onFocus: () => void - onChange: (next: any, prev: any) => void - "data-row": number - "data-col": number - "data-cell-id": string - "data-field": string + ref: RefObject; + onBlur: () => void; + onFocus: () => void; + onChange: (next: any, prev: any) => void; + 'data-row': number; + 'data-col': number; + 'data-cell-id': string; + 'data-field': string; } type InnerAttributes = { - "data-container-id": string -} + 'data-container-id': string; +}; interface InnerProps { - ref: RefObject - onMouseOver: ((e: React.MouseEvent) => void) | undefined - onMouseDown: ((e: React.MouseEvent) => void) | undefined - onKeyDown: (e: React.KeyboardEvent) => void - onFocus: (e: React.FocusEvent) => void - "data-container-id": string + ref: RefObject; + onMouseOver: ((e: React.MouseEvent) => void) | undefined; + onMouseDown: ((e: React.MouseEvent) => void) | undefined; + onKeyDown: (e: React.KeyboardEvent) => void; + onFocus: (e: React.FocusEvent) => void; + 'data-container-id': string; } interface OverlayProps { - onMouseDown: (e: React.MouseEvent) => void + onMouseDown: (e: React.MouseEvent) => void; } -export interface DataGridCellContainerProps extends PropsWithChildren<{}> { - field: string - innerProps: InnerProps - overlayProps: OverlayProps - isAnchor: boolean - isSelected: boolean - isDragSelected: boolean - placeholder?: ReactNode - showOverlay: boolean - outerComponent?: ReactNode +export interface DataGridCellContainerProps extends PropsWithChildren { + field: string; + innerProps: InnerProps; + overlayProps: OverlayProps; + isAnchor: boolean; + isSelected: boolean; + isDragSelected: boolean; + placeholder?: ReactNode; + showOverlay: boolean; + outerComponent?: ReactNode; } -export type DataGridCellSnapshot< - TFieldValues extends FieldValues = FieldValues, -> = { - field: string - value: PathValue> -} +export type DataGridCellSnapshot = { + field: string; + value: PathValue>; +}; export type FieldContext = { - row: Row - column: ColumnDef -} + row: Row; + column: ColumnDef; +}; export type FieldFunction = ( context: FieldContext -) => FieldPath | null +) => FieldPath | null; export type InternalColumnMeta = { - name: string - field?: FieldFunction + name: string; + field?: FieldFunction; } & ( | { - field: FieldFunction - type: DataGridColumnType + field: FieldFunction; + type: DataGridColumnType; } | { field?: null | undefined; type?: never } ) & - ColumnMeta + ColumnMeta; export type GridCell = { - field: FieldPath - type: DataGridColumnType - enabled: boolean -} + field: FieldPath; + type: DataGridColumnType; + enabled: boolean; +}; -export type Grid = - (GridCell | null)[][] +export type Grid = (GridCell | null)[][]; export type CellMetadata = { - id: string - field: string - type: DataGridColumnType - inputAttributes: InputAttributes - innerAttributes: InnerAttributes -} + id: string; + field: string; + type: DataGridColumnType; + inputAttributes: InputAttributes; + innerAttributes: InnerAttributes; +}; export type CellErrorMetadata = { - field: string | null - accessor: string | null -} + field: string | null; + accessor: string | null; +}; export type VisibilitySnapshot = { - rows: VisibilityState - columns: VisibilityState -} + rows: VisibilityState; + columns: VisibilityState; +}; export type GridColumnOption = { - id: string - name: string - checked: boolean - disabled: boolean -} + id: string; + name: string; + checked: boolean; + disabled: boolean; +}; export type DataGridToggleableNumber = { - quantity: number | string - checked: boolean - disabledToggle: boolean -} + quantity: number | string; + checked: boolean; + disabledToggle: boolean; +}; diff --git a/src/components/data-grid/utils.ts b/src/components/data-grid/utils.ts index a10c6338..2b2b3278 100644 --- a/src/components/data-grid/utils.ts +++ b/src/components/data-grid/utils.ts @@ -1,7 +1,7 @@ -import { DataGridCoordinates } from "./types" +import type { DataGridCoordinates } from '@components/data-grid/types'; export function generateCellId(coords: DataGridCoordinates) { - return `${coords.row}:${coords.col}` + return `${coords.row}:${coords.col}`; } /** @@ -10,19 +10,16 @@ export function generateCellId(coords: DataGridCoordinates) { * @param coords - The coords to compare * @returns Whether the cell is equal to the coords */ -export function isCellMatch( - cell: DataGridCoordinates, - coords?: DataGridCoordinates | null -) { +export function isCellMatch(cell: DataGridCoordinates, coords?: DataGridCoordinates | null) { if (!coords) { - return false + return false; } - return cell.row === coords.row && cell.col === coords.col + return cell.row === coords.row && cell.col === coords.col; } -const SPECIAL_FOCUS_KEYS = [".", ","] +const SPECIAL_FOCUS_KEYS = ['.', ',']; export function isSpecialFocusKey(event: KeyboardEvent) { - return SPECIAL_FOCUS_KEYS.includes(event.key) && event.ctrlKey && event.altKey -} \ No newline at end of file + return SPECIAL_FOCUS_KEYS.includes(event.key) && event.ctrlKey && event.altKey; +} diff --git a/src/components/data-table/components/data-table-status-cell.tsx b/src/components/data-table/components/data-table-status-cell.tsx new file mode 100644 index 00000000..3a67effe --- /dev/null +++ b/src/components/data-table/components/data-table-status-cell.tsx @@ -0,0 +1,28 @@ +import type { PropsWithChildren } from 'react'; + +import { clx } from '@medusajs/ui'; + +type DataTableStatusCellProps = PropsWithChildren<{ + color?: 'green' | 'red' | 'blue' | 'orange' | 'grey' | 'purple'; +}>; + +export const DataTableStatusCell = ({ color, children }: DataTableStatusCellProps) => ( +
    +
    +
    +
    + {children} +
    +); diff --git a/src/components/data-table/components/data-table-status-cell/data-table-status-cell.tsx b/src/components/data-table/components/data-table-status-cell/data-table-status-cell.tsx deleted file mode 100644 index 2bf7a2b8..00000000 --- a/src/components/data-table/components/data-table-status-cell/data-table-status-cell.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { clx } from "@medusajs/ui" -import { PropsWithChildren } from "react" - -type DataTableStatusCellProps = PropsWithChildren<{ - color?: "green" | "red" | "blue" | "orange" | "grey" | "purple" -}> - -export const DataTableStatusCell = ({ - color, - children, -}: DataTableStatusCellProps) => { - return ( -
    -
    -
    -
    - {children} -
    - ) -} diff --git a/src/components/data-table/components/data-table-table-with-test-ids/data-table-table-with-test-ids.tsx b/src/components/data-table/components/data-table-table-with-test-ids/data-table-table-with-test-ids.tsx index 57b48cd6..900bab9b 100644 --- a/src/components/data-table/components/data-table-table-with-test-ids/data-table-table-with-test-ids.tsx +++ b/src/components/data-table/components/data-table-table-with-test-ids/data-table-table-with-test-ids.tsx @@ -1,30 +1,27 @@ -import { Table, clx, Text, Skeleton } from "@medusajs/ui" -import { flexRender } from "@tanstack/react-table" -import * as React from "react" -import { DataTableEmptyStateProps } from "@medusajs/ui" +import * as React from 'react'; + +import { clx, Skeleton, Table, Text, type DataTableEmptyStateProps } from '@medusajs/ui'; +import { flexRender } from '@tanstack/react-table'; // Define the empty state enum to match @medusajs/ui enum DataTableEmptyState { - EMPTY = "EMPTY", - FILTERED_EMPTY = "FILTERED_EMPTY", - POPULATED = "POPULATED", + EMPTY = 'EMPTY', + FILTERED_EMPTY = 'FILTERED_EMPTY', + POPULATED = 'POPULATED' } interface DataTableTableWithTestIdsProps { instance: { - getHeaderGroups: () => any[] - getRowModel: () => { rows: any[] } - getAllColumns: () => any[] - onRowClick?: ( - event: React.MouseEvent, - row: TData - ) => void - emptyState: DataTableEmptyState - showSkeleton: boolean - pageSize: number - pageIndex: number - } - emptyState?: DataTableEmptyStateProps + getHeaderGroups: () => any[]; + getRowModel: () => { rows: any[] }; + getAllColumns: () => any[]; + onRowClick?: (event: React.MouseEvent, row: TData) => void; + emptyState: DataTableEmptyState; + showSkeleton: boolean; + pageSize: number; + pageIndex: number; + }; + emptyState?: DataTableEmptyStateProps; } /** @@ -33,31 +30,31 @@ interface DataTableTableWithTestIdsProps { */ export const DataTableTableWithTestIds = ({ instance, - emptyState, + emptyState }: DataTableTableWithTestIdsProps) => { - const hoveredRowId = React.useRef(null) - const [showStickyBorder, setShowStickyBorder] = React.useState(false) - const scrollableRef = React.useRef(null) + const hoveredRowId = React.useRef(null); + const [showStickyBorder, setShowStickyBorder] = React.useState(false); + const scrollableRef = React.useRef(null); - const columns = instance.getAllColumns() - const hasSelect = columns.find((c) => c.id === "select") - const hasActions = columns.find((c) => c.id === "action") + const columns = instance.getAllColumns(); + const hasSelect = columns.find(c => c.id === 'select'); + const hasActions = columns.find(c => c.id === 'action'); const handleHorizontalScroll = (e: React.UIEvent) => { - const scrollLeft = e.currentTarget.scrollLeft + const scrollLeft = e.currentTarget.scrollLeft; if (scrollLeft > 0) { - setShowStickyBorder(true) + setShowStickyBorder(true); } else { - setShowStickyBorder(false) + setShowStickyBorder(false); } - } + }; React.useEffect(() => { - scrollableRef.current?.scroll({ top: 0, left: 0 }) - }, [instance.pageIndex]) + scrollableRef.current?.scroll({ top: 0, left: 0 }); + }, [instance.pageIndex]); if (instance.showSkeleton) { - return + return ; } return ( @@ -70,61 +67,53 @@ export const DataTableTableWithTestIds = ({ > - {instance.getHeaderGroups().map((headerGroup) => ( + {instance.getHeaderGroups().map(headerGroup => ( {headerGroup.headers.map((header: any, idx: number) => { - const isActionHeader = header.id === "action" - const isSelectHeader = header.id === "select" - const isSpecialHeader = isActionHeader || isSelectHeader - const isFirstColumn = hasSelect ? idx === 1 : idx === 0 + const isActionHeader = header.id === 'action'; + const isSelectHeader = header.id === 'select'; + const isSpecialHeader = isActionHeader || isSelectHeader; + const isFirstColumn = hasSelect ? idx === 1 : idx === 0; return ( - {flexRender( - header.column.columnDef.header, - header.getContext() - )} + {flexRender(header.column.columnDef.header, header.getContext())} - ) + ); })} ))} @@ -137,59 +126,50 @@ export const DataTableTableWithTestIds = ({ data-testid={`data-table-row-${rowIndex}`} onMouseEnter={() => (hoveredRowId.current = row.id)} onMouseLeave={() => (hoveredRowId.current = null)} - onClick={(e) => instance.onRowClick?.(e, row)} - className={clx("group/row last-of-type:border-b-0", { - "cursor-pointer": !!instance.onRowClick, + onClick={e => instance.onRowClick?.(e, row)} + className={clx('group/row last-of-type:border-b-0', { + 'cursor-pointer': !!instance.onRowClick })} > {row.getVisibleCells().map((cell: any, cellIndex: number) => { - const isSelectCell = cell.column.id === "select" - const isActionCell = cell.column.id === "action" - const isSpecialCell = isSelectCell || isActionCell - const isFirstColumn = hasSelect ? cellIndex === 1 : cellIndex === 0 + const isSelectCell = cell.column.id === 'select'; + const isActionCell = cell.column.id === 'action'; + const isSpecialCell = isSelectCell || isActionCell; + const isFirstColumn = hasSelect ? cellIndex === 1 : cellIndex === 0; return ( - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} + {flexRender(cell.column.columnDef.cell, cell.getContext())} - ) + ); })} - ) + ); })}
    @@ -200,37 +180,39 @@ export const DataTableTableWithTestIds = ({ props={emptyState} />
    - ) -} + ); +}; const DefaultEmptyStateContent = ({ heading, - description, + description }: { - heading?: string - description?: string + heading?: string; + description?: string; }) => (
    - + {heading} {description}
    -) +); const DataTableEmptyStateDisplay = ({ state, - props, + props }: { - state: DataTableEmptyState - props?: DataTableEmptyStateProps + state: DataTableEmptyState; + props?: DataTableEmptyStateProps; }) => { if (state === DataTableEmptyState.POPULATED) { - return null + return null; } - const content = - state === DataTableEmptyState.EMPTY ? props?.empty : props?.filtered + const content = state === DataTableEmptyState.EMPTY ? props?.empty : props?.filtered; return (
    @@ -241,8 +223,8 @@ const DataTableEmptyStateDisplay = ({ /> )}
    - ) -} + ); +}; const DataTableTableSkeleton = ({ pageSize = 10 }: { pageSize?: number }) => { return ( @@ -250,12 +232,14 @@ const DataTableTableSkeleton = ({ pageSize = 10 }: { pageSize?: number }) => {
    - {Array.from({ length: pageSize }, (_, i) => i).map((row) => ( - + {Array.from({ length: pageSize }, (_, i) => i).map(row => ( + ))}
    - ) -} - + ); +}; diff --git a/src/components/data-table/components/index.ts b/src/components/data-table/components/index.ts new file mode 100644 index 00000000..73fe1f53 --- /dev/null +++ b/src/components/data-table/components/index.ts @@ -0,0 +1 @@ +export { DataTableStatusCell } from './data-table-status-cell'; diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx index 44b07b22..0f6f18d2 100644 --- a/src/components/data-table/data-table.tsx +++ b/src/components/data-table/data-table.tsx @@ -1,106 +1,108 @@ +import React, { useCallback, useMemo, type ReactNode } from 'react'; + +import { ActionMenu } from '@components/common/action-menu'; +import { DataTableTableWithTestIds } from '@components/data-table/components/data-table-table-with-test-ids'; +import { ViewPills } from '@components/table/view-selector'; +import { useQueryParams } from '@hooks/use-query-params'; import { - DataTable as UiDataTable, - useDataTable, - DataTableColumnDef, - DataTableCommand, - DataTableEmptyStateProps, - DataTableFilter, - DataTableRow, - DataTableRowSelectionState, + Button, Heading, Text, - Button, - DataTableFilteringState, - DataTablePaginationState, - DataTableSortingState, -} from "@medusajs/ui" -import React, { ReactNode, useCallback, useMemo } from "react" -import { useTranslation } from "react-i18next" -import { Link, useNavigate, useSearchParams } from "react-router-dom" - -import { useQueryParams } from "../../hooks/use-query-params" -import { ActionMenu } from "../common/action-menu" -import { ViewPills } from "../table/view-selector" -import { useFeatureFlag } from "../../providers/feature-flag-provider" -import { DataTableTableWithTestIds } from "./components/data-table-table-with-test-ids" + DataTable as UiDataTable, + useDataTable, + type DataTableColumnDef, + type DataTableCommand, + type DataTableEmptyStateProps, + type DataTableFilter, + type DataTableFilteringState, + type DataTablePaginationState, + type DataTableRow, + type DataTableRowSelectionState, + type DataTableSortingState +} from '@medusajs/ui'; +import { useFeatureFlag } from '@providers/feature-flag-provider'; +import { useTranslation } from 'react-i18next'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; // Types for column visibility and ordering -type VisibilityState = Record -type ColumnOrderState = string[] +type VisibilityState = Record; +type ColumnOrderState = string[]; type DataTableActionProps = { - label: string - disabled?: boolean + label: string; + disabled?: boolean; } & ( | { - to: string + to: string; } | { - onClick: () => void + onClick: () => void; } -) +); type DataTableActionMenuActionProps = { - label: string - icon: ReactNode - disabled?: boolean + label: string; + icon: ReactNode; + disabled?: boolean; } & ( | { - to: string + to: string; } | { - onClick: () => void + onClick: () => void; } -) +); type DataTableActionMenuGroupProps = { - actions: DataTableActionMenuActionProps[] -} + actions: DataTableActionMenuActionProps[]; +}; type DataTableActionMenuProps = { - groups: DataTableActionMenuGroupProps[] - "data-testid"?: string -} + groups: DataTableActionMenuGroupProps[]; + 'data-testid'?: string; +}; interface DataTableProps { - data?: TData[] - columns: DataTableColumnDef[] - filters?: DataTableFilter[] - commands?: DataTableCommand[] - action?: DataTableActionProps - actions?: DataTableActionProps[] - actionMenu?: DataTableActionMenuProps - rowCount?: number - getRowId: (row: TData) => string - enablePagination?: boolean - enableSearch?: boolean - autoFocusSearch?: boolean - enableFilterMenu?: boolean - rowHref?: (row: TData) => string - emptyState?: DataTableEmptyStateProps - heading?: string - subHeading?: string - prefix?: string - pageSize?: number - isLoading?: boolean + data?: TData[]; + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + columns: DataTableColumnDef[]; + filters?: DataTableFilter[]; + commands?: DataTableCommand[]; + action?: DataTableActionProps; + actions?: DataTableActionProps[]; + actionMenu?: DataTableActionMenuProps; + rowCount?: number; + getRowId: (row: TData) => string; + enablePagination?: boolean; + enableSearch?: boolean; + autoFocusSearch?: boolean; + enableFilterMenu?: boolean; + rowHref?: (row: TData) => string; + emptyState?: DataTableEmptyStateProps; + heading?: string; + subHeading?: string; + prefix?: string; + pageSize?: number; + isLoading?: boolean; rowSelection?: { - state: DataTableRowSelectionState - onRowSelectionChange: (value: DataTableRowSelectionState) => void - enableRowSelection?: boolean | ((row: DataTableRow) => boolean) - } - layout?: "fill" | "auto" - enableColumnVisibility?: boolean - initialColumnVisibility?: VisibilityState - onColumnVisibilityChange?: (visibility: VisibilityState) => void - columnOrder?: ColumnOrderState - onColumnOrderChange?: (order: ColumnOrderState) => void - enableViewSelector?: boolean - entity?: string + state: DataTableRowSelectionState; + onRowSelectionChange: (value: DataTableRowSelectionState) => void; + enableRowSelection?: boolean | ((row: DataTableRow) => boolean); + }; + layout?: 'fill' | 'auto'; + enableColumnVisibility?: boolean; + initialColumnVisibility?: VisibilityState; + onColumnVisibilityChange?: (visibility: VisibilityState) => void; + columnOrder?: ColumnOrderState; + onColumnOrderChange?: (order: ColumnOrderState) => void; + enableViewSelector?: boolean; + entity?: string; currentColumns?: { - visible: string[] - order: string[] - } - filterBarContent?: React.ReactNode + visible: string[]; + order: string[]; + }; + filterBarContent?: React.ReactNode; } export const DataTable = ({ @@ -125,7 +127,7 @@ export const DataTable = ({ emptyState, rowSelection, isLoading = false, - layout = "auto", + layout = 'auto', enableColumnVisibility = false, initialColumnVisibility = {}, onColumnVisibilityChange, @@ -134,190 +136,186 @@ export const DataTable = ({ enableViewSelector = false, entity, currentColumns, - filterBarContent, + filterBarContent }: DataTableProps) => { - const { t } = useTranslation() - const isViewConfigEnabled = useFeatureFlag("view_configurations") + const { t } = useTranslation(); + const isViewConfigEnabled = useFeatureFlag('view_configurations'); // If view config is disabled, don't use column visibility features - const effectiveEnableColumnVisibility = - isViewConfigEnabled && enableColumnVisibility - const effectiveEnableViewSelector = isViewConfigEnabled && enableViewSelector + const effectiveEnableColumnVisibility = isViewConfigEnabled && enableColumnVisibility; + const effectiveEnableViewSelector = isViewConfigEnabled && enableViewSelector; - const enableFiltering = filters && filters.length > 0 - const showFilterMenu = - enableFilterMenu !== undefined ? enableFilterMenu : enableFiltering - const enableCommands = commands && commands.length > 0 - const enableSorting = columns.some((column) => column.enableSorting) + const enableFiltering = filters && filters.length > 0; + const showFilterMenu = enableFilterMenu !== undefined ? enableFilterMenu : enableFiltering; + const enableCommands = commands && commands.length > 0; + const enableSorting = columns.some(column => column.enableSorting); const [columnVisibility, setColumnVisibility] = - React.useState(initialColumnVisibility) + React.useState(initialColumnVisibility); // Update column visibility when initial visibility changes React.useEffect(() => { // Deep compare to check if the visibility has actually changed - const currentKeys = Object.keys(columnVisibility).sort() - const newKeys = Object.keys(initialColumnVisibility).sort() + const currentKeys = Object.keys(columnVisibility).sort(); + const newKeys = Object.keys(initialColumnVisibility).sort(); const hasChanged = currentKeys.length !== newKeys.length || currentKeys.some((key, index) => key !== newKeys[index]) || Object.entries(initialColumnVisibility).some( ([key, value]) => columnVisibility[key] !== value - ) + ); if (hasChanged) { - setColumnVisibility(initialColumnVisibility) + setColumnVisibility(initialColumnVisibility); } - }, [initialColumnVisibility]) + }, [initialColumnVisibility]); // Wrapper function to handle column visibility changes const handleColumnVisibilityChange = React.useCallback( (visibility: VisibilityState) => { - setColumnVisibility(visibility) - onColumnVisibilityChange?.(visibility) + setColumnVisibility(visibility); + onColumnVisibilityChange?.(visibility); }, [onColumnVisibilityChange] - ) + ); // Extract filter IDs for query param management - const filterIds = useMemo(() => filters?.map((f) => f.id) ?? [], [filters]) - const prefixedFilterIds = filterIds.map((id) => getQueryParamKey(id, prefix)) + const filterIds = useMemo(() => filters?.map(f => f.id) ?? [], [filters]); + const prefixedFilterIds = filterIds.map(id => getQueryParamKey(id, prefix)); const { offset, order, q, ...filterParams } = useQueryParams( [ ...filterIds, - ...(enableSorting ? ["order"] : []), - ...(enableSearch ? ["q"] : []), - ...(enablePagination ? ["offset"] : []), + ...(enableSorting ? ['order'] : []), + ...(enableSearch ? ['q'] : []), + ...(enablePagination ? ['offset'] : []) ], prefix - ) - const [_, setSearchParams] = useSearchParams() + ); + const [_, setSearchParams] = useSearchParams(); const search = useMemo(() => { - return q ?? "" - }, [q]) + return q ?? ''; + }, [q]); const handleSearchChange = (value: string) => { - setSearchParams((prev) => { + setSearchParams(prev => { if (value) { - prev.set(getQueryParamKey("q", prefix), value) + prev.set(getQueryParamKey('q', prefix), value); } else { - prev.delete(getQueryParamKey("q", prefix)) + prev.delete(getQueryParamKey('q', prefix)); } - return prev - }) - } + return prev; + }); + }; const pagination: DataTablePaginationState = useMemo(() => { - return offset - ? parsePaginationState(offset, pageSize) - : { pageIndex: 0, pageSize } - }, [offset, pageSize]) + return offset ? parsePaginationState(offset, pageSize) : { pageIndex: 0, pageSize }; + }, [offset, pageSize]); const handlePaginationChange = (value: DataTablePaginationState) => { - setSearchParams((prev) => { + setSearchParams(prev => { if (value.pageIndex === 0) { - prev.delete(getQueryParamKey("offset", prefix)) + prev.delete(getQueryParamKey('offset', prefix)); } else { - prev.set( - getQueryParamKey("offset", prefix), - transformPaginationState(value).toString() - ) + prev.set(getQueryParamKey('offset', prefix), transformPaginationState(value).toString()); } - return prev - }) - } + + return prev; + }); + }; const filtering: DataTableFilteringState = useMemo( () => parseFilterState(filterIds, filterParams), [filterIds, filterParams] - ) + ); const handleFilteringChange = (value: DataTableFilteringState) => { - setSearchParams((prev) => { + setSearchParams(prev => { // Remove filters that are no longer in the state - Array.from(prev.keys()).forEach((key) => { + Array.from(prev.keys()).forEach(key => { if (prefixedFilterIds.includes(key)) { // Extract the unprefixed key - const unprefixedKey = prefix ? key.replace(`${prefix}_`, "") : key + const unprefixedKey = prefix ? key.replace(`${prefix}_`, '') : key; if (!(unprefixedKey in value)) { - prev.delete(key) + prev.delete(key); } } - }) + }); // Add or update filters in the state Object.entries(value).forEach(([key, filter]) => { - const prefixedKey = getQueryParamKey(key, prefix) + const prefixedKey = getQueryParamKey(key, prefix); if (filter !== undefined) { - prev.set(prefixedKey, JSON.stringify(filter)) + prev.set(prefixedKey, JSON.stringify(filter)); } else { - prev.delete(prefixedKey) + prev.delete(prefixedKey); } - }) + }); - return prev - }) - } + return prev; + }); + }; const sorting: DataTableSortingState | null = useMemo(() => { - return order ? parseSortingState(order) : null - }, [order]) + return order ? parseSortingState(order) : null; + }, [order]); // Memoize current configuration to prevent infinite loops const currentConfiguration = useMemo( () => ({ filters: filtering, sorting: sorting, - search: search, + search: search }), [filtering, sorting, search] - ) + ); const handleSortingChange = (value: DataTableSortingState) => { - setSearchParams((prev) => { + setSearchParams(prev => { if (value) { - const valueToStore = transformSortingState(value) + const valueToStore = transformSortingState(value); - prev.set(getQueryParamKey("order", prefix), valueToStore) + prev.set(getQueryParamKey('order', prefix), valueToStore); } else { - prev.delete(getQueryParamKey("order", prefix)) + prev.delete(getQueryParamKey('order', prefix)); } - return prev - }) - } + return prev; + }); + }; const { pagination: paginationTranslations, toolbar: toolbarTranslations } = - useDataTableTranslations() + useDataTableTranslations(); - const navigate = useNavigate() + const navigate = useNavigate(); const onRowClick = useCallback( (event: React.MouseEvent, row: TData) => { if (!rowHref) { - return + return; } - const href = rowHref(row) + const href = rowHref(row); if (event.metaKey || event.ctrlKey || event.button === 1) { - window.open(href, "_blank", "noreferrer") - return + window.open(href, '_blank', 'noreferrer'); + + return; } if (event.shiftKey) { - window.open(href, undefined, "noreferrer") - return + window.open(href, undefined, 'noreferrer'); + + return; } - navigate(href) + navigate(href); }, [navigate, rowHref] - ) + ); const instance = useDataTable({ data, @@ -330,25 +328,25 @@ export const DataTable = ({ pagination: enablePagination ? { state: pagination, - onPaginationChange: handlePaginationChange, + onPaginationChange: handlePaginationChange } : undefined, filtering: enableFiltering ? { state: filtering, - onFilteringChange: handleFilteringChange, + onFilteringChange: handleFilteringChange } : undefined, sorting: enableSorting ? { state: sorting, - onSortingChange: handleSortingChange, + onSortingChange: handleSortingChange } : undefined, search: enableSearch ? { state: search, - onSearchChange: handleSearchChange, + onSearchChange: handleSearchChange } : undefined, rowSelection, @@ -356,26 +354,24 @@ export const DataTable = ({ columnVisibility: effectiveEnableColumnVisibility ? { state: columnVisibility, - onColumnVisibilityChange: handleColumnVisibilityChange, + onColumnVisibilityChange: handleColumnVisibilityChange } : undefined, columnOrder: effectiveEnableColumnVisibility && columnOrder && onColumnOrderChange ? { state: columnOrder, - onColumnOrderChange: onColumnOrderChange, + onColumnOrderChange: onColumnOrderChange } - : undefined, - }) + : undefined + }); - const shouldRenderHeading = heading || subHeading + const shouldRenderHeading = heading || subHeading; return ( ({ filterBarContent={filterBarContent} data-testid="data-table-toolbar" > -
    -
    +
    +
    {shouldRenderHeading && (
    {heading && {heading}} {subHeading && ( - + {subHeading} )} @@ -405,18 +411,38 @@ export const DataTable = ({
    )}
    -
    - {showFilterMenu &&
    } - {enableSorting &&
    } +
    + {showFilterMenu && ( +
    + +
    + )} + {enableSorting && ( +
    + +
    + )} {enableSearch && ( -
    +
    )} - {actionMenu && } + {actionMenu && ( + + )} {actions && actions.length > 0 && (
    @@ -427,7 +453,10 @@ export const DataTable = ({
    - +
    {enablePagination && (
    @@ -436,120 +465,123 @@ export const DataTable = ({ )} {enableCommands && (
    - `${count} selected`} - /> + `${count} selected`} />
    )} - ) -} + ); +}; function transformSortingState(value: DataTableSortingState) { - return value.desc ? `-${value.id}` : value.id + return value.desc ? `-${value.id}` : value.id; } function parseSortingState(value: string) { - return value.startsWith("-") - ? { id: value.slice(1), desc: true } - : { id: value, desc: false } + return value.startsWith('-') ? { id: value.slice(1), desc: true } : { id: value, desc: false }; } function transformPaginationState(value: DataTablePaginationState) { - return value.pageIndex * value.pageSize + return value.pageIndex * value.pageSize; } function parsePaginationState(value: string, pageSize: number) { - const offset = parseInt(value) + const offset = parseInt(value); return { pageIndex: Math.floor(offset / pageSize), - pageSize, - } + pageSize + }; } -function parseFilterState( - filterIds: string[], - value: Record -) { +function parseFilterState(filterIds: string[], value: Record) { if (!value) { - return {} + return {}; } - const filters: DataTableFilteringState = {} + const filters: DataTableFilteringState = {}; for (const id of filterIds) { - const filterValue = value[id] + const filterValue = value[id]; if (filterValue !== undefined) { - filters[id] = JSON.parse(filterValue) + filters[id] = JSON.parse(filterValue); } } - return filters + return filters; } function getQueryParamKey(key: string, prefix?: string) { - return prefix ? `${prefix}_${key}` : key + return prefix ? `${prefix}_${key}` : key; } const useDataTableTranslations = () => { - const { t } = useTranslation() + const { t } = useTranslation(); const paginationTranslations = { - of: t("general.of"), - results: t("general.results"), - pages: t("general.pages"), - prev: t("general.prev"), - next: t("general.next"), - } + of: t('general.of'), + results: t('general.results'), + pages: t('general.pages'), + prev: t('general.prev'), + next: t('general.next') + }; const toolbarTranslations = { - clearAll: t("actions.clearAll"), - sort: t("filters.sortLabel"), - columns: "Columns", - } + clearAll: t('actions.clearAll'), + sort: t('filters.sortLabel'), + columns: 'Columns' + }; return { pagination: paginationTranslations, - toolbar: toolbarTranslations, - } -} + toolbar: toolbarTranslations + }; +}; -const DataTableAction = ({ - label, - disabled, - ...props -}: DataTableActionProps) => { +const DataTableAction = ({ label, disabled, ...props }: DataTableActionProps) => { const buttonProps = { - size: "small" as const, + size: 'small' as const, disabled: disabled ?? false, - type: "button" as const, - variant: "secondary" as const, - "data-testid": `data-table-action-${label.toLowerCase().replace(/\s+/g, "-")}`, - } + type: 'button' as const, + variant: 'secondary' as const, + 'data-testid': `data-table-action-${label.toLowerCase().replace(/\s+/g, '-')}` + }; - if ("to" in props) { + if ('to' in props) { return ( - - ) + ); } return ( - - ) -} + ); +}; const DataTableActions = ({ actions }: { actions: DataTableActionProps[] }) => { return (
    {actions.map((action, index) => ( - + ))}
    - ) -} + ); +}; diff --git a/src/components/data-table/helpers/general/index.ts b/src/components/data-table/helpers/general/index.ts new file mode 100644 index 00000000..acf3c011 --- /dev/null +++ b/src/components/data-table/helpers/general/index.ts @@ -0,0 +1,2 @@ +export * from './use-data-table-date-columns.tsx'; +export * from './use-data-table-date-filters.tsx'; diff --git a/src/components/data-table/helpers/general/use-data-table-date-columns.tsx b/src/components/data-table/helpers/general/use-data-table-date-columns.tsx index 5513f275..051eca45 100644 --- a/src/components/data-table/helpers/general/use-data-table-date-columns.tsx +++ b/src/components/data-table/helpers/general/use-data-table-date-columns.tsx @@ -1,61 +1,60 @@ -import { - createDataTableColumnHelper, - DataTableColumnDef, - Tooltip, -} from "@medusajs/ui" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" -import { useDate } from "../../../../hooks/use-date" +import { useMemo } from 'react'; + +import { useDate } from '@hooks/use-date.tsx'; +import { createDataTableColumnHelper, Tooltip, type DataTableColumnDef } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; type EntityWithDates = { - created_at: string - updated_at: string -} + created_at: string; + updated_at: string; +}; -const columnHelper = createDataTableColumnHelper() +const columnHelper = createDataTableColumnHelper(); export const useDataTableDateColumns = () => { - const { t } = useTranslation() - const { getFullDate } = useDate() + const { t } = useTranslation(); + const { getFullDate } = useDate(); - return useMemo(() => { - return [ - columnHelper.accessor("created_at", { - header: t("fields.createdAt"), - cell: ({ row }) => { - return ( - - {getFullDate({ date: row.original.created_at })} - - ) - }, - enableSorting: true, - sortAscLabel: t("filters.sorting.dateAsc"), - sortDescLabel: t("filters.sorting.dateDesc"), - }), - columnHelper.accessor("updated_at", { - header: t("fields.updatedAt"), - cell: ({ row }) => { - return ( - - {getFullDate({ date: row.original.updated_at })} - - ) - }, - enableSorting: true, - sortAscLabel: t("filters.sorting.dateAsc"), - sortDescLabel: t("filters.sorting.dateDesc"), - }), - ] as DataTableColumnDef[] - }, [t, getFullDate]) -} + return useMemo( + () => + [ + columnHelper.accessor('created_at', { + header: t('fields.createdAt'), + cell: ({ row }) => { + return ( + + {getFullDate({ date: row.original.created_at })} + + ); + }, + enableSorting: true, + sortAscLabel: t('filters.sorting.dateAsc'), + sortDescLabel: t('filters.sorting.dateDesc') + }), + columnHelper.accessor('updated_at', { + header: t('fields.updatedAt'), + cell: ({ row }) => { + return ( + + {getFullDate({ date: row.original.updated_at })} + + ); + }, + enableSorting: true, + sortAscLabel: t('filters.sorting.dateAsc'), + sortDescLabel: t('filters.sorting.dateDesc') + }) + ] as DataTableColumnDef[], + [t, getFullDate] + ); +}; diff --git a/src/components/data-table/helpers/general/use-data-table-date-filters.tsx b/src/components/data-table/helpers/general/use-data-table-date-filters.tsx index 83b1eacb..ef48b09c 100644 --- a/src/components/data-table/helpers/general/use-data-table-date-filters.tsx +++ b/src/components/data-table/helpers/general/use-data-table-date-filters.tsx @@ -1,95 +1,100 @@ -import { createDataTableFilterHelper } from "@medusajs/ui" -import { subDays, subMonths } from "date-fns" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" +import { useMemo } from 'react'; -import { useDate } from "../../../../hooks/use-date" +import { useDate } from '@hooks/use-date.tsx'; +import { createDataTableFilterHelper } from '@medusajs/ui'; +import { subDays, subMonths } from 'date-fns'; +import { useTranslation } from 'react-i18next'; -const filterHelper = createDataTableFilterHelper() +// @todo fix any type +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const filterHelper = createDataTableFilterHelper(); const useDateFilterOptions = () => { - const { t } = useTranslation() + const { t } = useTranslation(); const today = useMemo(() => { - const date = new Date() - date.setHours(0, 0, 0, 0) - return date - }, []) + const date = new Date(); + date.setHours(0, 0, 0, 0); - return useMemo(() => { - return [ + return date; + }, []); + + return useMemo( + () => [ { - label: t("filters.date.today"), + label: t('filters.date.today'), value: { - $gte: today.toISOString(), - }, + $gte: today.toISOString() + } }, { - label: t("filters.date.lastSevenDays"), + label: t('filters.date.lastSevenDays'), value: { - $gte: subDays(today, 7).toISOString(), // 7 days ago - }, + $gte: subDays(today, 7).toISOString() // 7 days ago + } }, { - label: t("filters.date.lastThirtyDays"), + label: t('filters.date.lastThirtyDays'), value: { - $gte: subDays(today, 30).toISOString(), // 30 days ago - }, + $gte: subDays(today, 30).toISOString() // 30 days ago + } }, { - label: t("filters.date.lastNinetyDays"), + label: t('filters.date.lastNinetyDays'), value: { - $gte: subDays(today, 90).toISOString(), // 90 days ago - }, + $gte: subDays(today, 90).toISOString() // 90 days ago + } }, { - label: t("filters.date.lastTwelveMonths"), + label: t('filters.date.lastTwelveMonths'), value: { - $gte: subMonths(today, 12).toISOString(), // 12 months ago - }, - }, - ] - }, [today, t]) -} + $gte: subMonths(today, 12).toISOString() // 12 months ago + } + } + ], + [today, t] + ); +}; export const useDataTableDateFilters = (disableRangeOption?: boolean) => { - const { t } = useTranslation() - const { getFullDate } = useDate() - const dateFilterOptions = useDateFilterOptions() + const { t } = useTranslation(); + const { getFullDate } = useDate(); + const dateFilterOptions = useDateFilterOptions(); const rangeOptions = useMemo(() => { if (disableRangeOption) { return { - disableRangeOption: true, - } + disableRangeOption: true + }; } return { - rangeOptionStartLabel: t("filters.date.starting"), - rangeOptionEndLabel: t("filters.date.ending"), - rangeOptionLabel: t("filters.date.custom"), - options: dateFilterOptions, - } - }, [disableRangeOption, t, dateFilterOptions]) + rangeOptionStartLabel: t('filters.date.starting'), + rangeOptionEndLabel: t('filters.date.ending'), + rangeOptionLabel: t('filters.date.custom'), + options: dateFilterOptions + }; + }, [disableRangeOption, t, dateFilterOptions]); - return useMemo(() => { - return [ - filterHelper.accessor("created_at", { - type: "date", - label: t("fields.createdAt"), - format: "date", - formatDateValue: (date) => getFullDate({ date }), + return useMemo( + () => [ + filterHelper.accessor('created_at', { + type: 'date', + label: t('fields.createdAt'), + format: 'date', + formatDateValue: date => getFullDate({ date }), options: dateFilterOptions, - ...rangeOptions, + ...rangeOptions }), - filterHelper.accessor("updated_at", { - type: "date", - label: t("fields.updatedAt"), - format: "date", - formatDateValue: (date) => getFullDate({ date }), + filterHelper.accessor('updated_at', { + type: 'date', + label: t('fields.updatedAt'), + format: 'date', + formatDateValue: date => getFullDate({ date }), options: dateFilterOptions, - ...rangeOptions, - }), - ] - }, [t, dateFilterOptions, getFullDate, rangeOptions]) -} + ...rangeOptions + }) + ], + [t, dateFilterOptions, getFullDate, rangeOptions] + ); +}; diff --git a/src/components/data-table/helpers/sales-channels/index.ts b/src/components/data-table/helpers/sales-channels/index.ts index 197fc601..f54dbd06 100644 --- a/src/components/data-table/helpers/sales-channels/index.ts +++ b/src/components/data-table/helpers/sales-channels/index.ts @@ -1,4 +1,4 @@ -export * from "./use-sales-channel-table-columns" -export * from "./use-sales-channel-table-empty-state" -export * from "./use-sales-channel-table-filters" -export * from "./use-sales-channel-table-query" +export * from './use-sales-channel-table-columns'; +export * from './use-sales-channel-table-empty-state'; +export * from './use-sales-channel-table-filters'; +export * from './use-sales-channel-table-query'; diff --git a/src/components/data-table/helpers/sales-channels/use-sales-channel-table-columns.tsx b/src/components/data-table/helpers/sales-channels/use-sales-channel-table-columns.tsx index 255ee68d..6c929f49 100644 --- a/src/components/data-table/helpers/sales-channels/use-sales-channel-table-columns.tsx +++ b/src/components/data-table/helpers/sales-channels/use-sales-channel-table-columns.tsx @@ -1,28 +1,28 @@ -import { HttpTypes } from "@medusajs/types" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" +import { useMemo } from 'react'; -import { createDataTableColumnHelper, Tooltip } from "@medusajs/ui" -import { DataTableStatusCell } from "../../components/data-table-status-cell/data-table-status-cell" -import { useDataTableDateColumns } from "../general/use-data-table-date-columns" +import { DataTableStatusCell } from '@components/data-table/components'; +import { useDataTableDateColumns } from '@components/data-table/helpers/general/use-data-table-date-columns.tsx'; +import type { HttpTypes } from '@medusajs/types'; +import { createDataTableColumnHelper, Tooltip } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; -const columnHelper = createDataTableColumnHelper() +const columnHelper = createDataTableColumnHelper(); export const useSalesChannelTableColumns = () => { - const { t } = useTranslation() - const dateColumns = useDataTableDateColumns() + const { t } = useTranslation(); + const dateColumns = useDataTableDateColumns(); return useMemo( () => [ - columnHelper.accessor("name", { - header: () => t("fields.name"), + columnHelper.accessor('name', { + header: () => t('fields.name'), enableSorting: true, - sortLabel: t("fields.name"), - sortAscLabel: t("filters.sorting.alphabeticallyAsc"), - sortDescLabel: t("filters.sorting.alphabeticallyDesc"), + sortLabel: t('fields.name'), + sortAscLabel: t('filters.sorting.alphabeticallyAsc'), + sortDescLabel: t('filters.sorting.alphabeticallyDesc') }), - columnHelper.accessor("description", { - header: () => t("fields.description"), + columnHelper.accessor('description', { + header: () => t('fields.description'), cell: ({ getValue }) => { return ( @@ -30,32 +30,33 @@ export const useSalesChannelTableColumns = () => { {getValue()}
    - ) + ); }, enableSorting: true, - sortLabel: t("fields.description"), - sortAscLabel: t("filters.sorting.alphabeticallyAsc"), - sortDescLabel: t("filters.sorting.alphabeticallyDesc"), + sortLabel: t('fields.description'), + sortAscLabel: t('filters.sorting.alphabeticallyAsc'), + sortDescLabel: t('filters.sorting.alphabeticallyDesc'), maxSize: 250, - minSize: 100, + minSize: 100 }), - columnHelper.accessor("is_disabled", { - header: () => t("fields.status"), + columnHelper.accessor('is_disabled', { + header: () => t('fields.status'), enableSorting: true, - sortLabel: t("fields.status"), - sortAscLabel: t("filters.sorting.alphabeticallyAsc"), - sortDescLabel: t("filters.sorting.alphabeticallyDesc"), + sortLabel: t('fields.status'), + sortAscLabel: t('filters.sorting.alphabeticallyAsc'), + sortDescLabel: t('filters.sorting.alphabeticallyDesc'), cell: ({ getValue }) => { - const value = getValue() + const value = getValue(); + return ( - - {value ? t("general.disabled") : t("general.enabled")} + + {value ? t('general.disabled') : t('general.enabled')} - ) - }, + ); + } }), - ...dateColumns, + ...dateColumns ], [t, dateColumns] - ) -} + ); +}; diff --git a/src/components/data-table/helpers/sales-channels/use-sales-channel-table-empty-state.tsx b/src/components/data-table/helpers/sales-channels/use-sales-channel-table-empty-state.tsx index f92daa9e..b3c94153 100644 --- a/src/components/data-table/helpers/sales-channels/use-sales-channel-table-empty-state.tsx +++ b/src/components/data-table/helpers/sales-channels/use-sales-channel-table-empty-state.tsx @@ -1,22 +1,23 @@ -import { DataTableEmptyStateProps } from "@medusajs/ui" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" +import { useMemo } from 'react'; + +import type { DataTableEmptyStateProps } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; export const useSalesChannelTableEmptyState = (): DataTableEmptyStateProps => { - const { t } = useTranslation() + const { t } = useTranslation(); return useMemo(() => { const content: DataTableEmptyStateProps = { empty: { - heading: t("salesChannels.list.empty.heading"), - description: t("salesChannels.list.empty.description"), + heading: t('salesChannels.list.empty.heading'), + description: t('salesChannels.list.empty.description') }, filtered: { - heading: t("salesChannels.list.filtered.heading"), - description: t("salesChannels.list.filtered.description"), - }, - } + heading: t('salesChannels.list.filtered.heading'), + description: t('salesChannels.list.filtered.description') + } + }; - return content - }, [t]) -} + return content; + }, [t]); +}; diff --git a/src/components/data-table/helpers/sales-channels/use-sales-channel-table-filters.tsx b/src/components/data-table/helpers/sales-channels/use-sales-channel-table-filters.tsx index ff8ad147..424ef26c 100644 --- a/src/components/data-table/helpers/sales-channels/use-sales-channel-table-filters.tsx +++ b/src/components/data-table/helpers/sales-channels/use-sales-channel-table-filters.tsx @@ -1,33 +1,34 @@ -import { HttpTypes } from "@medusajs/types" -import { createDataTableFilterHelper } from "@medusajs/ui" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" -import { useDataTableDateFilters } from "../general/use-data-table-date-filters" +import { useMemo } from 'react'; -const filterHelper = createDataTableFilterHelper() +import { useDataTableDateFilters } from '@components/data-table/helpers/general/use-data-table-date-filters'; +import type { HttpTypes } from '@medusajs/types'; +import { createDataTableFilterHelper } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; + +const filterHelper = createDataTableFilterHelper(); export const useSalesChannelTableFilters = () => { - const { t } = useTranslation() - const dateFilters = useDataTableDateFilters() + const { t } = useTranslation(); + const dateFilters = useDataTableDateFilters(); return useMemo( () => [ - filterHelper.accessor("is_disabled", { - label: t("fields.status"), - type: "radio", + filterHelper.accessor('is_disabled', { + label: t('fields.status'), + type: 'radio', options: [ { - label: t("general.enabled"), - value: "false", + label: t('general.enabled'), + value: 'false' }, { - label: t("general.disabled"), - value: "true", - }, - ], + label: t('general.disabled'), + value: 'true' + } + ] }), - ...dateFilters, + ...dateFilters ], [dateFilters, t] - ) -} + ); +}; diff --git a/src/components/data-table/helpers/sales-channels/use-sales-channel-table-query.tsx b/src/components/data-table/helpers/sales-channels/use-sales-channel-table-query.tsx index 2252691d..df7016c9 100644 --- a/src/components/data-table/helpers/sales-channels/use-sales-channel-table-query.tsx +++ b/src/components/data-table/helpers/sales-channels/use-sales-channel-table-query.tsx @@ -1,21 +1,21 @@ -import { HttpTypes } from "@medusajs/types" -import { useQueryParams } from "../../../../hooks/use-query-params" +import { useQueryParams } from '@hooks/use-query-params.tsx'; +import type { HttpTypes } from '@medusajs/types'; type UseSalesChannelTableQueryProps = { - prefix?: string - pageSize?: number -} + prefix?: string; + pageSize?: number; +}; export const useSalesChannelTableQuery = ({ prefix, - pageSize = 20, + pageSize = 20 }: UseSalesChannelTableQueryProps) => { const queryObject = useQueryParams( - ["offset", "q", "order", "created_at", "updated_at", "is_disabled"], + ['offset', 'q', 'order', 'created_at', 'updated_at', 'is_disabled'], prefix - ) + ); - const { offset, created_at, updated_at, is_disabled, ...rest } = queryObject + const { offset, created_at, updated_at, is_disabled, ...rest } = queryObject; const searchParams: HttpTypes.AdminSalesChannelListParams = { limit: pageSize, @@ -23,8 +23,8 @@ export const useSalesChannelTableQuery = ({ created_at: created_at ? JSON.parse(created_at) : undefined, updated_at: updated_at ? JSON.parse(updated_at) : undefined, is_disabled: is_disabled ? JSON.parse(is_disabled) : undefined, - ...rest, - } + ...rest + }; - return searchParams -} + return searchParams; +}; diff --git a/src/components/data-table/index.ts b/src/components/data-table/index.ts index 8e2d5f82..fb0ea7ea 100644 --- a/src/components/data-table/index.ts +++ b/src/components/data-table/index.ts @@ -1 +1 @@ -export * from "./data-table" +export * from './data-table'; diff --git a/src/components/filtering/filter-group/filter-group.tsx b/src/components/filtering/filter-group/filter-group.tsx index bba70491..058fa0c8 100644 --- a/src/components/filtering/filter-group/filter-group.tsx +++ b/src/components/filtering/filter-group/filter-group.tsx @@ -1,58 +1,64 @@ -import { Button, DropdownMenu } from "@medusajs/ui" -import { ReactNode } from "react" -import { useSearchParams } from "react-router-dom" -import { useDocumentDirection } from "../../../hooks/use-document-direction" +import type { ReactNode } from 'react'; + +import { useDocumentDirection } from '@hooks/use-document-direction'; +import { Button, DropdownMenu } from '@medusajs/ui'; +import { useSearchParams } from 'react-router-dom'; type FilterGroupProps = { filters: { - [key: string]: ReactNode - } -} + [key: string]: ReactNode; + }; +}; export const FilterGroup = ({ filters }: FilterGroupProps) => { - const [searchParams] = useSearchParams() - const filterKeys = Object.keys(filters) + const [searchParams] = useSearchParams(); + const filterKeys = Object.keys(filters); if (filterKeys.length === 0) { - return null + return null; } - const isClearable = filterKeys.some((key) => searchParams.get(key)) - const hasMore = !filterKeys.every((key) => searchParams.get(key)) - const availableKeys = filterKeys.filter((key) => !searchParams.get(key)) + const isClearable = filterKeys.some(key => searchParams.get(key)); + const hasMore = !filterKeys.every(key => searchParams.get(key)); + const availableKeys = filterKeys.filter(key => !searchParams.get(key)); return ( -
    +
    {hasMore && } {isClearable && ( - )}
    - ) -} + ); +}; type AddFilterMenuProps = { - availableKeys: string[] -} + availableKeys: string[]; +}; const AddFilterMenu = ({ availableKeys }: AddFilterMenuProps) => { - const direction = useDocumentDirection() + const direction = useDocumentDirection(); + return ( - + - - {availableKeys.map((key) => ( + {availableKeys.map(key => ( {key} ))} - ) -} + ); +}; diff --git a/src/components/filtering/filter-group/index.ts b/src/components/filtering/filter-group/index.ts index 03b21681..40e7d215 100644 --- a/src/components/filtering/filter-group/index.ts +++ b/src/components/filtering/filter-group/index.ts @@ -1 +1 @@ -export * from "./filter-group" +export * from './filter-group'; diff --git a/src/components/filtering/order-by/index.ts b/src/components/filtering/order-by/index.ts index d8200bb4..e1033eb2 100644 --- a/src/components/filtering/order-by/index.ts +++ b/src/components/filtering/order-by/index.ts @@ -1 +1 @@ -export * from "./order-by" +export * from './order-by'; diff --git a/src/components/filtering/order-by/order-by.tsx b/src/components/filtering/order-by/order-by.tsx index 20111880..a22e01fd 100644 --- a/src/components/filtering/order-by/order-by.tsx +++ b/src/components/filtering/order-by/order-by.tsx @@ -1,105 +1,107 @@ -import { ArrowUpDown } from "@medusajs/icons" -import { DropdownMenu, IconButton } from "@medusajs/ui" -import { useState } from "react" -import { useTranslation } from "react-i18next" -import { useSearchParams } from "react-router-dom" +import { useState } from 'react'; -import { useDocumentDirection } from "../../../hooks/use-document-direction" +import { useDocumentDirection } from '@hooks/use-document-direction'; +import { ArrowUpDown } from '@medusajs/icons'; +import { DropdownMenu, IconButton } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; type OrderByProps = { - keys: string[] -} + keys: string[]; +}; enum SortDirection { - ASC = "asc", - DESC = "desc", + ASC = 'asc', + DESC = 'desc' } type SortState = { - key?: string - dir: SortDirection -} + key?: string; + dir: SortDirection; +}; const initState = (params: URLSearchParams): SortState => { - const sortParam = params.get("order") + const sortParam = params.get('order'); if (!sortParam) { return { - dir: SortDirection.ASC, - } + dir: SortDirection.ASC + }; } - const dir = sortParam.startsWith("-") ? SortDirection.DESC : SortDirection.ASC - const key = sortParam.replace("-", "") + const dir = sortParam.startsWith('-') ? SortDirection.DESC : SortDirection.ASC; + const key = sortParam.replace('-', ''); return { key, - dir, - } -} + dir + }; +}; const formatKey = (key: string) => { - const words = key.split("_") + const words = key.split('_'); const formattedWords = words.map((word, index) => { if (index === 0) { - return word.charAt(0).toUpperCase() + word.slice(1) + return word.charAt(0).toUpperCase() + word.slice(1); } else { - return word + return word; } - }) - return formattedWords.join(" ") -} + }); + + return formattedWords.join(' '); +}; export const OrderBy = ({ keys }: OrderByProps) => { - const [searchParams, setSearchParams] = useSearchParams() + const [searchParams, setSearchParams] = useSearchParams(); const [state, setState] = useState<{ - key?: string - dir: SortDirection - }>(initState(searchParams)) + key?: string; + dir: SortDirection; + }>(initState(searchParams)); - const { t } = useTranslation() - const direction = useDocumentDirection() + const { t } = useTranslation(); + const direction = useDocumentDirection(); const handleDirChange = (dir: string) => { - setState((prev) => ({ + setState(prev => ({ ...prev, - dir: dir as SortDirection, - })) + dir: dir as SortDirection + })); updateOrderParam({ key: state.key, - dir: dir as SortDirection, - }) - } + dir: dir as SortDirection + }); + }; const handleKeyChange = (value: string) => { - setState((prev) => ({ + setState(prev => ({ ...prev, - key: value, - })) + key: value + })); updateOrderParam({ key: value, - dir: state.dir, - }) - } + dir: state.dir + }); + }; const updateOrderParam = (state: SortState) => { if (!state.key) { - setSearchParams((prev) => { - prev.delete("order") - return prev - }) + setSearchParams(prev => { + prev.delete('order'); + + return prev; + }); - return + return; } - const orderParam = - state.dir === SortDirection.ASC ? state.key : `-${state.key}` - setSearchParams((prev) => { - prev.set("order", orderParam) - return prev - }) - } + const orderParam = state.dir === SortDirection.ASC ? state.key : `-${state.key}`; + setSearchParams(prev => { + prev.set('order', orderParam); + + return prev; + }); + }; return ( @@ -113,11 +115,11 @@ export const OrderBy = ({ keys }: OrderByProps) => { value={state.key} onValueChange={handleKeyChange} > - {keys.map((key) => ( + {keys.map(key => ( event.preventDefault()} + onSelect={event => event.preventDefault()} > {formatKey(key)} @@ -131,21 +133,21 @@ export const OrderBy = ({ keys }: OrderByProps) => { event.preventDefault()} + onSelect={event => event.preventDefault()} > - {t("general.ascending")} + {t('general.ascending')} 1 - 30 event.preventDefault()} + onSelect={event => event.preventDefault()} > - {t("general.descending")} + {t('general.descending')} 30 - 1 - ) -} + ); +}; diff --git a/src/components/filtering/query/index.ts b/src/components/filtering/query/index.ts index 4ddfa841..a38910e4 100644 --- a/src/components/filtering/query/index.ts +++ b/src/components/filtering/query/index.ts @@ -1 +1 @@ -export * from "./query" +export * from './query'; diff --git a/src/components/filtering/query/query.tsx b/src/components/filtering/query/query.tsx index eb5bef49..5b2373a9 100644 --- a/src/components/filtering/query/query.tsx +++ b/src/components/filtering/query/query.tsx @@ -1,49 +1,52 @@ -import { Input } from "@medusajs/ui" -import { debounce } from "lodash" -import { ChangeEvent, useCallback, useEffect, useState } from "react" -import { useTranslation } from "react-i18next" -import { useSearchParams } from "react-router-dom" +import { useCallback, useEffect, useState, type ChangeEvent } from 'react'; + +import { Input } from '@medusajs/ui'; +import { debounce } from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; type QueryProps = { - placeholder?: string -} + placeholder?: string; +}; export const Query = ({ placeholder }: QueryProps) => { - const { t } = useTranslation() - const placeholderText = placeholder || t("general.search") + const { t } = useTranslation(); + const placeholderText = placeholder || t('general.search'); - const [searchParams, setSearchParams] = useSearchParams() - const [inputValue, setInputValue] = useState(searchParams.get("q") || "") + const [searchParams, setSearchParams] = useSearchParams(); + const [inputValue, setInputValue] = useState(searchParams.get('q') || ''); const updateSearchParams = (newValue: string) => { if (!newValue) { - setSearchParams((prev) => { - prev.delete("q") - return prev - }) + setSearchParams(prev => { + prev.delete('q'); - return - } + return prev; + }); - setSearchParams((prev) => ({ ...prev, q: newValue || "" })) - } + return; + } + setSearchParams(prev => ({ ...prev, q: newValue || '' })); + }; + //@todo fix this + // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedUpdate = useCallback( debounce((newValue: string) => updateSearchParams(newValue), 500), [] - ) + ); useEffect(() => { - debouncedUpdate(inputValue) + debouncedUpdate(inputValue); return () => { - debouncedUpdate.cancel() - } - }, [inputValue, debouncedUpdate]) + debouncedUpdate.cancel(); + }; + }, [inputValue, debouncedUpdate]); const handleInputChange = (event: ChangeEvent) => { - setInputValue(event.target.value) - } + setInputValue(event.target.value); + }; return ( { onChange={handleInputChange} placeholder={placeholderText} /> - ) -} + ); +}; diff --git a/src/components/forms/address-form/address-form.tsx b/src/components/forms/address-form/address-form.tsx index ef3dde48..48e70ea6 100644 --- a/src/components/forms/address-form/address-form.tsx +++ b/src/components/forms/address-form/address-form.tsx @@ -1,228 +1,207 @@ -import { Heading, Input, Select, clx } from "@medusajs/ui" -import { useTranslation } from "react-i18next" -import { z } from "zod" +import { Form } from '@components/common/form'; +import { CountrySelect } from '@components/inputs/country-select'; +import { useDocumentDirection } from '@hooks/use-document-direction'; +import type { AddressSchema } from '@lib/schemas'; +import type { HttpTypes } from '@medusajs/types'; +import { clx, Heading, Input, Select } from '@medusajs/ui'; +import type { Control } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { z } from 'zod'; -import { HttpTypes } from "@medusajs/types" -import { Control } from "react-hook-form" -import { AddressSchema } from "../../../lib/schemas" -import { Form } from "../../common/form" -import { CountrySelect } from "../../inputs/country-select" -import { useDocumentDirection } from "../../../hooks/use-document-direction" - -type AddressFieldValues = z.infer +type AddressFieldValues = z.infer; type AddressFormProps = { - control: Control - countries?: HttpTypes.AdminRegionCountry[] - layout: "grid" | "stack" -} + control: Control; + countries?: HttpTypes.AdminRegionCountry[]; + layout: 'grid' | 'stack'; +}; -export const AddressForm = ({ - control, - countries, - layout, -}: AddressFormProps) => { - const { t } = useTranslation() - const direction = useDocumentDirection() - const style = clx("gap-4", { - "flex flex-col": layout === "stack", - "grid grid-cols-2": layout === "grid", - }) +export const AddressForm = ({ control, countries, layout }: AddressFormProps) => { + const { t } = useTranslation(); + const direction = useDocumentDirection(); + const style = clx('gap-4', { + 'flex flex-col': layout === 'stack', + 'grid grid-cols-2': layout === 'grid' + }); return (
    - {t("addresses.contactHeading")} + {t('addresses.contactHeading')}
    { - return ( - - {t("fields.firstName")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.firstName')} + + + + + + )} /> { - return ( - - {t("fields.lastName")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.lastName')} + + + + + + )} /> { - return ( - - {t("fields.company")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.company')} + + + + + + )} /> { - return ( - - {t("fields.phone")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.phone')} + + + + + + )} />
    - {t("addresses.locationHeading")} + {t('addresses.locationHeading')}
    { - return ( - - {t("fields.address")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.address')} + + + + + + )} /> { - return ( - - {t("fields.address2")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.address2')} + + + + + + )} /> { - return ( - - {t("fields.city")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.city')} + + + + + + )} /> { - return ( - - {t("fields.postalCode")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.postalCode')} + + + + + + )} /> { - return ( - - {t("fields.province")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.province')} + + + + + + )} /> { - return ( - - {t("fields.country")} - - {countries ? ( - + + + + + {countries.map(country => { + /** + * If a country does not have an ISO 2 code, it is not + * a valid country and should not be selectable. + */ + if (!country.iso_2) { + return null; + } - return ( - - {country.display_name} - - ) - })} - - - ) : ( - // When no countries are provided, use the country select component that has a built-in list of all countries - )} - - - - ) - }} + return ( + + {country.display_name} + + ); + })} + + + ) : ( + // When no countries are provided, use the country select component that has a built-in list of all countries + )} + + + + )} />
    - ) -} + ); +}; diff --git a/src/components/forms/address-form/index.ts b/src/components/forms/address-form/index.ts index ea5dea61..25d6cb58 100644 --- a/src/components/forms/address-form/index.ts +++ b/src/components/forms/address-form/index.ts @@ -1 +1 @@ -export * from "./address-form" +export * from './address-form'; diff --git a/src/components/forms/email-form/email-form.tsx b/src/components/forms/email-form/email-form.tsx index b100334a..290ae568 100644 --- a/src/components/forms/email-form/email-form.tsx +++ b/src/components/forms/email-form/email-form.tsx @@ -1,42 +1,40 @@ -import { Input, clx } from "@medusajs/ui" -import { Control } from "react-hook-form" -import { useTranslation } from "react-i18next" -import { z } from "zod" -import { EmailSchema } from "../../../lib/schemas" -import { Form } from "../../common/form" +import { Form } from '@components/common/form'; +import type { EmailSchema } from '@lib/schemas'; +import { clx, Input } from '@medusajs/ui'; +import type { Control } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import type { z } from 'zod'; -type EmailFieldValues = z.infer +type EmailFieldValues = z.infer; type EmailFormProps = { - control: Control - layout?: "grid" | "stack" -} + control: Control; + layout?: 'grid' | 'stack'; +}; -export const EmailForm = ({ control, layout = "stack" }: EmailFormProps) => { - const { t } = useTranslation() +export const EmailForm = ({ control, layout = 'stack' }: EmailFormProps) => { + const { t } = useTranslation(); return (
    { - return ( - - {t("fields.email")} - - - - - - ) - }} + render={({ field }) => ( + + {t('fields.email')} + + + + + + )} />
    - ) -} + ); +}; diff --git a/src/components/forms/email-form/index.ts b/src/components/forms/email-form/index.ts index ba4619f6..413cbbd2 100644 --- a/src/components/forms/email-form/index.ts +++ b/src/components/forms/email-form/index.ts @@ -1 +1 @@ -export * from "./email-form" +export * from './email-form'; diff --git a/src/components/forms/metadata-form/index.ts b/src/components/forms/metadata-form/index.ts index 77c403fc..3f72088f 100644 --- a/src/components/forms/metadata-form/index.ts +++ b/src/components/forms/metadata-form/index.ts @@ -1 +1 @@ -export * from "./metadata-form" +export * from './metadata-form'; diff --git a/src/components/forms/metadata-form/metadata-form.tsx b/src/components/forms/metadata-form/metadata-form.tsx index 89c57d6b..eb6c0832 100644 --- a/src/components/forms/metadata-form/metadata-form.tsx +++ b/src/components/forms/metadata-form/metadata-form.tsx @@ -1,182 +1,191 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { - Button, - DropdownMenu, - Heading, - IconButton, - InlineTip, - clx, - toast, -} from "@medusajs/ui" -import { useFieldArray, useForm } from "react-hook-form" -import { useTranslation } from "react-i18next" -import { z } from "zod" - -import { - ArrowDownMini, - ArrowUpMini, - EllipsisVertical, - Trash, -} from "@medusajs/icons" -import { FetchError } from "@medusajs/js-sdk" -import { useDocumentDirection } from "@hooks/use-document-direction" -import { KeyboundForm } from "@components/utilities/keybound-form" -import { RouteDrawer, useRouteModal } from "@components/modals" -import { Skeleton } from "@components/common/skeleton" -import { Form } from "@components/common/form" -import { ConditionalTooltip } from "@components/common/conditional-tooltip" -import { type ComponentPropsWithoutRef, forwardRef } from "react" +import { forwardRef, type ComponentPropsWithoutRef } from 'react'; + +import { ConditionalTooltip } from '@components/common/conditional-tooltip'; +import { Form } from '@components/common/form'; +import { Skeleton } from '@components/common/skeleton'; +import { RouteDrawer, useRouteModal } from '@components/modals'; +import { KeyboundForm } from '@components/utilities/keybound-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useDocumentDirection } from '@hooks/use-document-direction'; +import { ArrowDownMini, ArrowUpMini, EllipsisVertical, Trash } from '@medusajs/icons'; +import { FetchError } from '@medusajs/js-sdk'; +import { Button, clx, DropdownMenu, Heading, IconButton, InlineTip, toast } from '@medusajs/ui'; +import { useFieldArray, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; type MetaDataSubmitHook = ( params: { metadata?: Record | null }, callbacks: { onSuccess?: () => void; onError?: (error: FetchError | string) => void } -) => Promise +) => Promise; type MetadataFormProps = { - metadata?: Record | null - hook: MetaDataSubmitHook - isPending: boolean - isMutating: boolean -} + metadata?: Record | null; + hook: MetaDataSubmitHook; + isPending: boolean; + isMutating: boolean; +}; const MetadataFieldSchema = z.object({ key: z.string(), disabled: z.boolean().optional(), - value: z.any(), -}) + value: z.any() +}); const MetadataSchema = z.object({ - metadata: z.array(MetadataFieldSchema), -}) + metadata: z.array(MetadataFieldSchema) +}); export const MetadataForm = (props: MetadataFormProps) => { - const { t } = useTranslation() - const { isPending, ...innerProps } = props + const { t } = useTranslation(); + const { isPending, ...innerProps } = props; return ( - - {t("metadata.edit.header")} + + {t('metadata.edit.header')} - - {t("metadata.edit.description")} + + {t('metadata.edit.description')} {isPending ? : } - ) -} + ); +}; -const METADATA_KEY_LABEL_ID = "metadata-form-key-label" -const METADATA_VALUE_LABEL_ID = "metadata-form-value-label" +const METADATA_KEY_LABEL_ID = 'metadata-form-key-label'; +const METADATA_VALUE_LABEL_ID = 'metadata-form-value-label'; const InnerForm = ({ metadata, hook, - isMutating, -}: Omit, "isPending">) => { - const { t } = useTranslation() - const { handleSuccess } = useRouteModal() - const direction = useDocumentDirection() - const hasUneditableRows = getHasUneditableRows(metadata) + isMutating +}: Omit, 'isPending'>) => { + const { t } = useTranslation(); + const { handleSuccess } = useRouteModal(); + const direction = useDocumentDirection(); + const hasUneditableRows = getHasUneditableRows(metadata); const form = useForm>({ defaultValues: { - metadata: getDefaultValues(metadata), + metadata: getDefaultValues(metadata) }, - resolver: zodResolver(MetadataSchema), - }) + resolver: zodResolver(MetadataSchema) + }); - const handleSubmit = form.handleSubmit(async (data) => { - const parsedData = parseValues(data, metadata) + const handleSubmit = form.handleSubmit(async data => { + const parsedData = parseValues(data, metadata); await hook( { - metadata: parsedData, + metadata: parsedData }, { onSuccess: () => { - toast.success(t("metadata.edit.successToast")) - handleSuccess() - }, - onError: (error) => { - toast.error(error instanceof FetchError ? error.message : error) + toast.success(t('metadata.edit.successToast')); + handleSuccess(); }, + onError: error => { + toast.error(error instanceof FetchError ? error.message : error); + } } - ) - }) + ); + }); const { fields, insert, remove } = useFieldArray({ control: form.control, - name: "metadata", - }) + name: 'metadata' + }); function deleteRow(index: number) { - remove(index) + remove(index); // If the last row is deleted, add a new blank row if (fields.length === 1) { insert(0, { - key: "", - value: "", - disabled: false, - }) + key: '', + value: '', + disabled: false + }); } } - function insertRow(index: number, position: "above" | "below") { - insert(index + (position === "above" ? 0 : 1), { - key: "", - value: "", - disabled: false, - }) + function insertRow(index: number, position: 'above' | 'below') { + insert(index + (position === 'above' ? 0 : 1), { + key: '', + value: '', + disabled: false + }); } return ( - + - -
    -
    -
    - + +
    +
    +
    +
    -
    - +
    +
    {fields.map((field, index) => { - const isDisabled = field.disabled || false - let placeholder = "-" + const isDisabled = field.disabled || false; + let placeholder = '-'; - if (typeof field.value === "object") { - placeholder = "{ ... }" + if (typeof field.value === 'object') { + placeholder = '{ ... }'; } if (Array.isArray(field.value)) { - placeholder = "[ ... ]" + placeholder = '[ ... ]'; } return ( -
    +
    @@ -196,7 +205,7 @@ const InnerForm = ({ /> - ) + ); }} /> ({ render={({ field: { value, ...field } }) => { return ( - + ({ /> - ) + ); }} />
    @@ -228,63 +239,76 @@ const InnerForm = ({ className={clx( "invisible absolute inset-y-0 -end-2.5 my-auto group-hover/table:visible data-[state='open']:visible", { - hidden: isDisabled, + hidden: isDisabled } )} disabled={isDisabled} asChild data-testid={`metadata-form-row-${index}-actions-menu-trigger`} > - + - + insertRow(index, "above")} + onClick={() => insertRow(index, 'above')} data-testid={`metadata-form-row-${index}-action-insert-above`} > - {t("metadata.edit.actions.insertRowAbove")} + {t('metadata.edit.actions.insertRowAbove')} insertRow(index, "below")} + onClick={() => insertRow(index, 'below')} data-testid={`metadata-form-row-${index}-action-insert-below`} > - {t("metadata.edit.actions.insertRowBelow")} + {t('metadata.edit.actions.insertRowBelow')} - + deleteRow(index)} data-testid={`metadata-form-row-${index}-action-delete`} > - {t("metadata.edit.actions.deleteRow")} + {t('metadata.edit.actions.deleteRow')}
    - ) + ); })}
    {hasUneditableRows && ( - {t("metadata.edit.complexRow.description")} + {t('metadata.edit.complexRow.description')} )} -
    - +
    + -
    - ) -} - -const GridInput = forwardRef< - HTMLInputElement, - ComponentPropsWithoutRef<"input"> ->(({ className, ...props }, ref) => { - return ( - - ) -}) -GridInput.displayName = "MetadataForm.GridInput" + ); +}; + +const GridInput = forwardRef>( + ({ className, ...props }, ref) => { + return ( + + ); + } +); +GridInput.displayName = 'MetadataForm.GridInput'; const PlaceholderInner = () => { return ( @@ -336,10 +364,10 @@ const PlaceholderInner = () => {
    - ) -} + ); +}; -const EDITABLE_TYPES = ["string", "number", "boolean"] +const EDITABLE_TYPES = ['string', 'number', 'boolean']; function getDefaultValues( metadata?: Record | null @@ -347,11 +375,11 @@ function getDefaultValues( if (!metadata || !Object.keys(metadata).length) { return [ { - key: "", - value: "", - disabled: false, - }, - ] + key: '', + value: '', + disabled: false + } + ]; } return Object.entries(metadata).map(([key, value]) => { @@ -359,83 +387,80 @@ function getDefaultValues( return { key, value: value, - disabled: true, - } + disabled: true + }; } - let stringValue = value + let stringValue = value; - if (typeof value !== "string") { - stringValue = JSON.stringify(value) + if (typeof value !== 'string') { + stringValue = JSON.stringify(value); } return { key, value: stringValue, - original_key: key, - } - }) + original_key: key + }; + }); } function parseValues( values: z.infer, - original?: Record | null + _original?: Record | null ): Record | null { - const metadata = values.metadata + const metadata = values.metadata; const isEmpty = - !metadata.length || - (metadata.length === 1 && !metadata[0].key && !metadata[0].value) + !metadata.length || (metadata.length === 1 && !metadata[0].key && !metadata[0].value); if (isEmpty) { - return null + return null; } - const update: Record = {} + const update: Record = {}; // Build payload from current form rows only - removed keys are omitted entirely - metadata.forEach((field) => { - let key = field.key - let value = field.value - const disabled = field.disabled + metadata.forEach(field => { + let key = field.key; + let value = field.value; + const disabled = field.disabled; if (!key) { - return + return; } if (disabled) { - update[key] = value - - return + update[key] = value; + + return; } - key = key.trim() - value = value?.trim() ?? "" + key = key.trim(); + value = value?.trim() ?? ''; // We try to cast the value to a boolean or number if possible - if (value === "true") { - update[key] = true - } else if (value === "false") { - update[key] = false + if (value === 'true') { + update[key] = true; + } else if (value === 'false') { + update[key] = false; } else { - const isNumeric = /^-?\d*\.?\d+$/.test(value) + const isNumeric = /^-?\d*\.?\d+$/.test(value); if (isNumeric) { - update[key] = parseFloat(value) + update[key] = parseFloat(value); } else { - update[key] = value + update[key] = value; } } - }) + }); - return update + return update; } function getHasUneditableRows(metadata?: Record | null) { if (!metadata) { - return false + return false; } - return Object.values(metadata).some( - (value) => !EDITABLE_TYPES.includes(typeof value) - ) + return Object.values(metadata).some(value => !EDITABLE_TYPES.includes(typeof value)); } diff --git a/src/components/inputs/chip-input/chip-input.tsx b/src/components/inputs/chip-input/chip-input.tsx index cc6f39ce..2f861d18 100644 --- a/src/components/inputs/chip-input/chip-input.tsx +++ b/src/components/inputs/chip-input/chip-input.tsx @@ -1,16 +1,15 @@ import { - FocusEvent, - KeyboardEvent, forwardRef, useImperativeHandle, useRef, useState, -} from "react"; + type FocusEvent, + type KeyboardEvent +} from 'react'; -import { XMarkMini } from "@medusajs/icons"; -import { Badge, clx } from "@medusajs/ui"; - -import { AnimatePresence, motion } from "motion/react"; +import { XMarkMini } from '@medusajs/icons'; +import { Badge, clx } from '@medusajs/ui'; +import { AnimatePresence, motion } from 'motion/react'; type ChipInputProps = { value?: string[]; @@ -20,10 +19,10 @@ type ChipInputProps = { disabled?: boolean; allowDuplicates?: boolean; showRemove?: boolean; - variant?: "base" | "contrast"; + variant?: 'base' | 'contrast'; placeholder?: string; className?: string; - "data-testid"?: string; + 'data-testid'?: string; }; export const ChipInput = forwardRef( @@ -35,24 +34,26 @@ export const ChipInput = forwardRef( disabled, name, showRemove = true, - variant = "base", + variant = 'base', allowDuplicates = false, placeholder, className, - "data-testid": dataTestId, + 'data-testid': dataTestId }, - ref, + ref ) => { const innerRef = useRef(null); - const isControlledRef = useRef(typeof value !== "undefined"); + const isControlledRef = useRef(typeof value !== 'undefined'); + //@todo fix this + // eslint-disable-next-line react-hooks/refs const isControlled = isControlledRef.current; const [uncontrolledValue, setUncontrolledValue] = useState([]); useImperativeHandle( ref, - () => innerRef.current, + () => innerRef.current ); const [duplicateIndex, setDuplicateIndex] = useState(null); @@ -84,10 +85,10 @@ export const ChipInput = forwardRef( }; const handleRemoveChip = (chip: string) => { - onChange?.(chips.filter((v) => v !== chip)); + onChange?.(chips.filter(v => v !== chip)); if (!isControlled) { - setUncontrolledValue(chips.filter((v) => v !== chip)); + setUncontrolledValue(chips.filter(v => v !== chip)); } }; @@ -96,24 +97,24 @@ export const ChipInput = forwardRef( if (e.target.value) { handleAddChip(e.target.value); - e.target.value = ""; + e.target.value = ''; } }; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === ",") { + if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); if (!innerRef.current?.value) { return; } - handleAddChip(innerRef.current?.value ?? ""); - innerRef.current.value = ""; + handleAddChip(innerRef.current?.value ?? ''); + innerRef.current.value = ''; innerRef.current?.focus(); } - if (e.key === "Backspace" && innerRef.current?.value === "") { + if (e.key === 'Backspace' && innerRef.current?.value === '') { handleRemoveChip(chips[chips.length - 1]); } }; @@ -121,21 +122,20 @@ export const ChipInput = forwardRef( // create a shake animation using framer motion const shake = { x: [0, -2, 2, -2, 2, 0], - transition: { duration: 0.3 }, + transition: { duration: 0.3 } }; return (
    innerRef.current?.focus()} @@ -146,24 +146,20 @@ export const ChipInput = forwardRef( - + {v} {showRemove && ( @@ -175,10 +171,10 @@ export const ChipInput = forwardRef( })} ( />
    ); - }, + } ); -ChipInput.displayName = "ChipInput"; +ChipInput.displayName = 'ChipInput'; diff --git a/src/components/inputs/chip-input/index.ts b/src/components/inputs/chip-input/index.ts index 30cc6cbe..f9ae5b64 100644 --- a/src/components/inputs/chip-input/index.ts +++ b/src/components/inputs/chip-input/index.ts @@ -1 +1 @@ -export * from "./chip-input" +export * from './chip-input'; diff --git a/src/components/inputs/combobox/combobox.tsx b/src/components/inputs/combobox/combobox.tsx index a8875443..985d6b87 100644 --- a/src/components/inputs/combobox/combobox.tsx +++ b/src/components/inputs/combobox/combobox.tsx @@ -1,28 +1,5 @@ import { - Combobox as PrimitiveCombobox, - ComboboxDisclosure as PrimitiveComboboxDisclosure, - ComboboxItem as PrimitiveComboboxItem, - ComboboxItemCheck as PrimitiveComboboxItemCheck, - ComboboxItemValue as PrimitiveComboboxItemValue, - ComboboxPopover as PrimitiveComboboxPopover, - ComboboxProvider as PrimitiveComboboxProvider, - Separator as PrimitiveSeparator, -} from "@ariakit/react" -import { - CheckMini, - EllipseMiniSolid, - PlusMini, - TrianglesMini, - XMarkMini, -} from "@medusajs/icons" -import { clx, Text } from "@medusajs/ui" -import { matchSorter } from "match-sorter" -import { - ComponentPropsWithoutRef, - CSSProperties, - ForwardedRef, Fragment, - ReactNode, useCallback, useDeferredValue, useImperativeHandle, @@ -30,35 +7,52 @@ import { useRef, useState, useTransition, -} from "react" -import { useTranslation } from "react-i18next" + type ComponentPropsWithoutRef, + type CSSProperties, + type ForwardedRef, + type ReactNode +} from 'react'; -import { genericForwardRef } from "../../utilities/generic-forward-ref" +import { + Combobox as PrimitiveCombobox, + ComboboxDisclosure as PrimitiveComboboxDisclosure, + ComboboxItem as PrimitiveComboboxItem, + ComboboxItemCheck as PrimitiveComboboxItemCheck, + ComboboxItemValue as PrimitiveComboboxItemValue, + ComboboxPopover as PrimitiveComboboxPopover, + ComboboxProvider as PrimitiveComboboxProvider, + Separator as PrimitiveSeparator +} from '@ariakit/react'; +import { genericForwardRef } from '@components/utilities/generic-forward-ref'; +import { CheckMini, EllipseMiniSolid, PlusMini, TrianglesMini, XMarkMini } from '@medusajs/icons'; +import { clx, Text } from '@medusajs/ui'; +import { matchSorter } from 'match-sorter'; +import { useTranslation } from 'react-i18next'; type ComboboxOption = { - value: string - label: string - disabled?: boolean -} + value: string; + label: string; + disabled?: boolean; +}; -type Value = string[] | string +type Value = string[] | string; -const TABLUAR_NUM_WIDTH = 8 -const TAG_BASE_WIDTH = 28 +const TABLUAR_NUM_WIDTH = 8; +const TAG_BASE_WIDTH = 28; interface ComboboxProps - extends Omit, "onChange" | "value"> { - value?: T - onChange?: (value?: T) => void - searchValue?: string - onSearchValueChange?: (value: string) => void - options: ComboboxOption[] - fetchNextPage?: () => void - isFetchingNextPage?: boolean - onCreateOption?: (value: string) => void - noResultsPlaceholder?: ReactNode - allowClear?: boolean - forceHideInput?: boolean // always hide input -> used for singe value select that don't have query/filter + extends Omit, 'onChange' | 'value'> { + value?: T; + onChange?: (value?: T) => void; + searchValue?: string; + onSearchValueChange?: (value: string) => void; + options: ComboboxOption[]; + fetchNextPage?: () => void; + isFetchingNextPage?: boolean; + onCreateOption?: (value: string) => void; + noResultsPlaceholder?: ReactNode; + allowClear?: boolean; + forceHideInput?: boolean; // always hide input -> used for singe value select that don't have query/filter } const ComboboxImpl = ( @@ -80,68 +74,67 @@ const ComboboxImpl = ( }: ComboboxProps, ref: ForwardedRef ) => { - const [open, setOpen] = useState(false) - const [isPending, startTransition] = useTransition() - const { t } = useTranslation() + const [open, setOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + const { t } = useTranslation(); - const comboboxRef = useRef(null) - const listboxRef = useRef(null) + const comboboxRef = useRef(null); + const listboxRef = useRef(null); - useImperativeHandle(ref, () => comboboxRef.current!) + useImperativeHandle(ref, () => comboboxRef.current!); - const isValueControlled = controlledValue !== undefined - const isSearchControlled = controlledSearchValue !== undefined + const isValueControlled = controlledValue !== undefined; + const isSearchControlled = controlledSearchValue !== undefined; - const isArrayValue = Array.isArray(controlledValue) - const emptyState = (isArrayValue ? [] : "") as T + const isArrayValue = Array.isArray(controlledValue); + const emptyState = (isArrayValue ? [] : '') as T; const [uncontrolledSearchValue, setUncontrolledSearchValue] = useState( - controlledSearchValue || "" - ) - const defferedSearchValue = useDeferredValue(uncontrolledSearchValue) + controlledSearchValue || '' + ); + const defferedSearchValue = useDeferredValue(uncontrolledSearchValue); - const [uncontrolledValue, setUncontrolledValue] = useState(emptyState) + const [uncontrolledValue, setUncontrolledValue] = useState(emptyState); - const searchValue = isSearchControlled - ? controlledSearchValue - : uncontrolledSearchValue - const selectedValues = isValueControlled ? controlledValue : uncontrolledValue + const searchValue = isSearchControlled ? controlledSearchValue : uncontrolledSearchValue; + const selectedValues = isValueControlled ? controlledValue : uncontrolledValue; const handleValueChange = (newValues?: T) => { // check if the value already exists in options const exists = options - .filter((o) => !o.disabled) - .find((o) => { + .filter(o => !o.disabled) + .find(o => { if (isArrayValue) { - return newValues?.includes(o.value) + return newValues?.includes(o.value); } - return o.value === newValues - }) + + return o.value === newValues; + }); // If the value does not exist in the options, and the component has a handler // for creating new options, call it. if (!exists && onCreateOption && newValues) { - onCreateOption(newValues as string) + onCreateOption(newValues as string); } if (!isValueControlled) { - setUncontrolledValue(newValues || emptyState) + setUncontrolledValue(newValues || emptyState); } if (onChange) { - onChange(newValues) + onChange(newValues); } - setUncontrolledSearchValue("") - } + setUncontrolledSearchValue(''); + }; const handleSearchChange = (query: string) => { - setUncontrolledSearchValue(query) + setUncontrolledSearchValue(query); if (onSearchValueChange) { - onSearchValueChange(query) + onSearchValueChange(query); } - } + }; /** * Filter and sort the options based on the search value, @@ -151,114 +144,114 @@ const ComboboxImpl = ( */ const matches = useMemo(() => { if (isSearchControlled) { - return [] + return []; } // do not use `matcher` if the input is hidden if (forceHideInput) { - return options + return options; } return matchSorter(options, defferedSearchValue, { - keys: ["label"], - }) - }, [options, defferedSearchValue, isSearchControlled, forceHideInput]) + keys: ['label'] + }); + }, [options, defferedSearchValue, isSearchControlled, forceHideInput]); const observer = useRef( new IntersectionObserver( - (entries) => { - const first = entries[0] + entries => { + const first = entries[0]; if (first.isIntersecting) { - fetchNextPage?.() + fetchNextPage?.(); } }, { threshold: 1 } ) - ) + ); const lastOptionRef = useCallback( (node: HTMLDivElement) => { if (isFetchingNextPage) { - return + return; } if (observer.current) { - observer.current.disconnect() + observer.current.disconnect(); } if (node) { - observer.current.observe(node) + observer.current.observe(node); } }, [isFetchingNextPage] - ) + ); const handleOpenChange = (open: boolean) => { if (!open) { - setUncontrolledSearchValue("") + setUncontrolledSearchValue(''); } - setOpen(open) - } + setOpen(open); + }; - const hasValue = selectedValues?.length > 0 + const hasValue = selectedValues?.length > 0; - const showTag = hasValue && isArrayValue - const showSelected = showTag && !searchValue && !open + const showTag = hasValue && isArrayValue; + const showSelected = showTag && !searchValue && !open; - const hideInput = forceHideInput || (!isArrayValue && hasValue && !open) - const selectedLabel = options.find((o) => o.value === selectedValues)?.label + const hideInput = forceHideInput || (!isArrayValue && hasValue && !open); + const selectedLabel = options.find(o => o.value === selectedValues)?.label; - const hidePlaceholder = showSelected || open + const hidePlaceholder = showSelected || open; const tagWidth = useMemo(() => { if (!Array.isArray(selectedValues)) { - return TAG_BASE_WIDTH + TABLUAR_NUM_WIDTH // There can only be a single digit + return TAG_BASE_WIDTH + TABLUAR_NUM_WIDTH; // There can only be a single digit } - const count = selectedValues.length - const digits = count.toString().length + const count = selectedValues.length; + const digits = count.toString().length; - return TAG_BASE_WIDTH + digits * TABLUAR_NUM_WIDTH - }, [selectedValues]) + return TAG_BASE_WIDTH + digits * TABLUAR_NUM_WIDTH; + }, [selectedValues]); const results = useMemo(() => { - return isSearchControlled ? options : matches - }, [matches, options, isSearchControlled]) + return isSearchControlled ? options : matches; + }, [matches, options, isSearchControlled]); return ( handleValueChange(value as T)} + setSelectedValue={value => handleValueChange(value as T)} value={uncontrolledSearchValue} - setValue={(query) => { - startTransition(() => handleSearchChange(query)) + setValue={query => { + startTransition(() => handleSearchChange(query)); }} >
    {showTag && ( )} { + render={props => { return ( - ) + ); }} />
    @@ -344,14 +341,14 @@ const ComboboxImpl = ( ref={listboxRef} role="listbox" className={clx( - "shadow-elevation-flyout bg-ui-bg-base z-50 rounded-[8px] p-1", - "max-h-[200px] overflow-y-auto", - "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", - "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", - "data-[side=bottom]:slide-in-from-top-2 data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2" + 'z-50 rounded-[8px] bg-ui-bg-base p-1 shadow-elevation-flyout', + 'max-h-[200px] overflow-y-auto', + 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95', + 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', + 'data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2' )} style={{ - pointerEvents: open ? "auto" : "none", + pointerEvents: open ? 'auto' : 'none' }} aria-busy={isPending} > @@ -363,10 +360,10 @@ const ComboboxImpl = ( setValueOnClick={false} disabled={disabled} className={clx( - "transition-fg bg-ui-bg-base data-[active-item=true]:bg-ui-bg-base-hover group flex cursor-pointer items-center gap-x-2 rounded-[4px] px-2 py-1", + 'group flex cursor-pointer items-center gap-x-2 rounded-[4px] bg-ui-bg-base px-2 py-1 transition-fg data-[active-item=true]:bg-ui-bg-base-hover', { - "text-ui-fg-disabled": disabled, - "bg-ui-bg-component": disabled, + 'text-ui-fg-disabled': disabled, + 'bg-ui-bg-component': disabled } )} > @@ -378,10 +375,15 @@ const ComboboxImpl = ( ))} - {!!fetchNextPage &&
    } + {!!fetchNextPage && ( +
    + )} {isFetchingNextPage && ( -
    -
    +
    +
    )} {!results.length && @@ -394,29 +396,32 @@ const ComboboxImpl = ( leading="compact" className="text-ui-fg-subtle" > - {t("general.noResultsTitle")} + {t('general.noResultsTitle')}
    ))} {!results.length && onCreateOption && ( - + - - {t("actions.create")} "{searchValue}" + + {t('actions.create')} "{searchValue}" )} - ) -} + ); +}; -export const Combobox = genericForwardRef(ComboboxImpl) +export const Combobox = genericForwardRef(ComboboxImpl); diff --git a/src/components/inputs/combobox/index.ts b/src/components/inputs/combobox/index.ts index abd73dd0..36dd8c5b 100644 --- a/src/components/inputs/combobox/index.ts +++ b/src/components/inputs/combobox/index.ts @@ -1 +1 @@ -export * from "./combobox" +export * from './combobox'; diff --git a/src/components/inputs/country-select/country-select.tsx b/src/components/inputs/country-select/country-select.tsx index 61728339..a1f3ce62 100644 --- a/src/components/inputs/country-select/country-select.tsx +++ b/src/components/inputs/country-select/country-select.tsx @@ -1,29 +1,28 @@ -import { - ComponentPropsWithoutRef, - forwardRef, - useImperativeHandle, - useRef, -} from "react" -import { useTranslation } from "react-i18next" -import { countries } from "../../../lib/data/countries" -import { Select } from "@medusajs/ui" +import { forwardRef, useImperativeHandle, useRef, type ComponentPropsWithoutRef } from 'react'; + +import { countries } from '@lib/data/countries'; +import { Select } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; export const CountrySelect = forwardRef< HTMLButtonElement, ComponentPropsWithoutRef & { - placeholder?: string - defaultValue?: string - onChange?: (value: string) => void - "data-testid"?: string + placeholder?: string; + defaultValue?: string; + onChange?: (value: string) => void; + 'data-testid'?: string; } ->(({ disabled, placeholder, defaultValue, onChange, "data-testid": dataTestId, ...field }, ref) => { - const { t } = useTranslation() - const innerRef = useRef(null) +>(({ disabled, placeholder, defaultValue, onChange, 'data-testid': dataTestId, ...field }, ref) => { + const { t } = useTranslation(); + const innerRef = useRef(null); + + useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement); - useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement) - return ( -
    +
    - ) -}) + ); +}); -CountrySelect.displayName = "CountrySelect" +CountrySelect.displayName = 'CountrySelect'; diff --git a/src/components/inputs/country-select/index.ts b/src/components/inputs/country-select/index.ts index 4e19e8a7..437d9a50 100644 --- a/src/components/inputs/country-select/index.ts +++ b/src/components/inputs/country-select/index.ts @@ -1 +1 @@ -export * from "./country-select" +export * from './country-select'; diff --git a/src/components/inputs/handle-input/handle-input.tsx b/src/components/inputs/handle-input/handle-input.tsx index 78f22f80..c1499d41 100644 --- a/src/components/inputs/handle-input/handle-input.tsx +++ b/src/components/inputs/handle-input/handle-input.tsx @@ -1,11 +1,9 @@ -import { Input, Text } from "@medusajs/ui" -import { ComponentProps, ElementRef, forwardRef } from "react" +import { forwardRef, type ComponentProps, type ElementRef } from 'react'; -export const HandleInput = forwardRef< - ElementRef, - ComponentProps ->((props, ref) => { - return ( +import { Input, Text } from '@medusajs/ui'; + +export const HandleInput = forwardRef, ComponentProps>( + (props, ref) => (
    - +
    ) -}) -HandleInput.displayName = "HandleInput" +); +HandleInput.displayName = 'HandleInput'; diff --git a/src/components/inputs/handle-input/index.ts b/src/components/inputs/handle-input/index.ts index ed461fed..dabafcd9 100644 --- a/src/components/inputs/handle-input/index.ts +++ b/src/components/inputs/handle-input/index.ts @@ -1 +1 @@ -export * from "./handle-input" +export * from './handle-input'; diff --git a/src/components/inputs/percentage-input/index.ts b/src/components/inputs/percentage-input/index.ts index 69f623aa..05a76db3 100644 --- a/src/components/inputs/percentage-input/index.ts +++ b/src/components/inputs/percentage-input/index.ts @@ -1 +1 @@ -export * from "./percentage-input" +export * from './percentage-input'; diff --git a/src/components/inputs/percentage-input/percentage-input.tsx b/src/components/inputs/percentage-input/percentage-input.tsx index aa7e544d..3310a972 100644 --- a/src/components/inputs/percentage-input/percentage-input.tsx +++ b/src/components/inputs/percentage-input/percentage-input.tsx @@ -1,7 +1,8 @@ -import { clx, Input, Text } from "@medusajs/ui"; -import { getNumberOfDecimalPlaces } from "../../../lib/number-helpers"; -import { ComponentProps, ElementRef, forwardRef } from "react"; -import Primitive from "react-currency-input-field"; +import { forwardRef, type ComponentProps, type ElementRef } from 'react'; + +import { getNumberOfDecimalPlaces } from '@lib/number-helpers'; +import { clx, Input, Text } from '@medusajs/ui'; +import Primitive from 'react-currency-input-field'; const MIN_DECIMAL_SCALE = 2; @@ -11,64 +12,48 @@ function resolveDecimalScale( if (value == null || Array.isArray(value)) { return MIN_DECIMAL_SCALE; } - return Math.max( - getNumberOfDecimalPlaces(parseFloat(value.toString())), - MIN_DECIMAL_SCALE - ); + + return Math.max(getNumberOfDecimalPlaces(parseFloat(value.toString())), MIN_DECIMAL_SCALE); } export const DeprecatedPercentageInput = forwardRef< ElementRef, - Omit, "type"> ->(({ min = 0, max = 100, step = 0.0001, ...props }, ref) => { - return ( -
    -
    - - % - -
    - + Omit, 'type'> +>(({ min = 0, max = 100, step = 0.0001, ...props }, ref) => ( +
    +
    + + % +
    - ); -}); -DeprecatedPercentageInput.displayName = "PercentageInput"; + +
    +)); +DeprecatedPercentageInput.displayName = 'PercentageInput'; -export const PercentageInput = forwardRef< - ElementRef<"input">, - ComponentProps ->( - ( - { - min = 0, - max = 100, - decimalScale, - decimalsLimit, - value, - className, - ...props - }, - ref - ) => { +export const PercentageInput = forwardRef, ComponentProps>( + ({ min = 0, max = 100, decimalScale, decimalsLimit, value, className, ...props }, ref) => { const resolvedDecimalScale = decimalScale ?? resolveDecimalScale(value); const resolvedDecimalsLimit = decimalsLimit ?? resolvedDecimalScale; return (
    @@ -102,4 +87,4 @@ export const PercentageInput = forwardRef< ); } ); -PercentageInput.displayName = "PercentageInput"; +PercentageInput.displayName = 'PercentageInput'; diff --git a/src/components/inputs/province-select/index.ts b/src/components/inputs/province-select/index.ts index f1baa16a..6dd594db 100644 --- a/src/components/inputs/province-select/index.ts +++ b/src/components/inputs/province-select/index.ts @@ -1 +1 @@ -export * from "./province-select" +export * from './province-select'; diff --git a/src/components/inputs/province-select/province-select.tsx b/src/components/inputs/province-select/province-select.tsx index d1c60d95..03df3759 100644 --- a/src/components/inputs/province-select/province-select.tsx +++ b/src/components/inputs/province-select/province-select.tsx @@ -1,62 +1,48 @@ -import { - ComponentPropsWithoutRef, - forwardRef, - useImperativeHandle, - useRef, -} from "react" -import { Select } from "@medusajs/ui" -import { useTranslation } from "react-i18next" -import { getCountryProvinceObjectByIso2 } from "../../../lib/data/country-states" +import { forwardRef, useImperativeHandle, useRef, type ComponentPropsWithoutRef } from 'react'; + +import { getCountryProvinceObjectByIso2 } from '@lib/data/country-states'; +import { Select } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; export const ProvinceSelect = forwardRef< HTMLButtonElement, ComponentPropsWithoutRef & { - placeholder?: string - defaultValue?: string - country_code: string - valueAs?: "iso_2" | "name" - onChange?: (value: string) => void + placeholder?: string; + defaultValue?: string; + country_code: string; + valueAs?: 'iso_2' | 'name'; + onChange?: (value: string) => void; } >( ( - { - disabled, - placeholder, - defaultValue, - country_code, - valueAs = "iso_2", - onChange, - ...field - }, + { disabled, placeholder, defaultValue, country_code, valueAs = 'iso_2', onChange, ...field }, ref ) => { - const { t } = useTranslation() - const innerRef = useRef(null) + const { t } = useTranslation(); + const innerRef = useRef(null); - useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement) + useImperativeHandle(ref, () => innerRef.current as HTMLButtonElement); - const provinceObject = getCountryProvinceObjectByIso2(country_code) + const provinceObject = getCountryProvinceObjectByIso2(country_code); if (!provinceObject) { - disabled = true + disabled = true; } - const options = Object.entries(provinceObject?.options ?? {}).map( - ([iso2, name]) => { - return ( - - {name} - - ) - } - ) + const options = Object.entries(provinceObject?.options ?? {}).map(([iso2, name]) => { + return ( + + {name} + + ); + }); const placeholderText = provinceObject ? t(`taxRegions.fields.sublevels.placeholders.${provinceObject.type}`) - : "" + : ''; return (
    @@ -64,14 +50,14 @@ export const ProvinceSelect = forwardRef< {...field} value={ field.value - ? valueAs === "iso_2" + ? valueAs === 'iso_2' ? field.value.toLowerCase() : field.value : undefined } defaultValue={ defaultValue - ? valueAs === "iso_2" + ? valueAs === 'iso_2' ? defaultValue.toLowerCase() : defaultValue : undefined @@ -79,13 +65,16 @@ export const ProvinceSelect = forwardRef< onValueChange={onChange} disabled={disabled} > - + {options}
    - ) + ); } -) -ProvinceSelect.displayName = "ProvinceSelect" +); +ProvinceSelect.displayName = 'ProvinceSelect'; diff --git a/src/components/layout/main-layout/index.ts b/src/components/layout/main-layout/index.ts index 035d09e6..7813c9d2 100644 --- a/src/components/layout/main-layout/index.ts +++ b/src/components/layout/main-layout/index.ts @@ -1 +1 @@ -export * from "./main-layout" +export * from './main-layout'; diff --git a/src/components/layout/main-layout/main-layout.tsx b/src/components/layout/main-layout/main-layout.tsx index e6b54b11..ad3b76ea 100644 --- a/src/components/layout/main-layout/main-layout.tsx +++ b/src/components/layout/main-layout/main-layout.tsx @@ -1,7 +1,14 @@ +import { NavItem, type INavItem } from '@components//layout/nav-item'; +import { Skeleton } from '@components/common/skeleton'; +import { Shell } from '@components/layout/shell'; +import { UserMenu } from '@components/layout/user-menu'; +import { useLogout, useStore } from '@hooks/api'; +import { useDocumentDirection } from '@hooks/use-document-direction.tsx'; +import { queryClient } from '@lib/query-client'; import { BottomToTop, - BuildingStorefront, Buildings, + BuildingStorefront, ChatBubble, ChevronDownMini, CogSixTooth, @@ -15,66 +22,52 @@ import { ShoppingCart, SquaresPlus, Tag, - Users, -} from "@medusajs/icons"; -import { Avatar, Divider, DropdownMenu, Text, clx } from "@medusajs/ui"; - -import { Collapsible as RadixCollapsible } from "radix-ui"; -import { useTranslation } from "react-i18next"; -import { Link, useLocation, useNavigate } from "react-router-dom"; + Users +} from '@medusajs/icons'; +import { Avatar, clx, Divider, DropdownMenu, Text } from '@medusajs/ui'; +import { useExtension } from '@providers/extension-provider'; +import { useSearch } from '@providers/search-provider'; +import { Collapsible as RadixCollapsible } from 'radix-ui'; +import { useTranslation } from 'react-i18next'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; -import { useLogout } from "../../../hooks/api"; -import { useStore } from "../../../hooks/api/store"; -import { useDocumentDirection } from "../../../hooks/use-document-direction"; -import { queryClient } from "../../../lib/query-client"; -import { useExtension } from "../../../providers/extension-provider"; -import { useSearch } from "../../../providers/search-provider"; -import { Skeleton } from "../../common/skeleton"; -import { INavItem, NavItem } from "../../layout/nav-item"; -import { Shell } from "../../layout/shell"; -import { UserMenu } from "../user-menu"; +export const MainLayout = () => ( + + + +); -export const MainLayout = () => { - return ( - - - - ); -}; - -const MainSidebar = () => { - return ( - +); const Logout = () => { const { t } = useTranslation(); @@ -89,16 +82,19 @@ const Logout = () => { * When the user logs out, we want to clear the query cache */ queryClient.clear(); - navigate("/login"); - }, + navigate('/login'); + } }); }; return ( - +
    - {t("app.menus.actions.logout")} + {t('app.menus.actions.logout')}
    ); @@ -118,24 +114,38 @@ const Header = () => { } return ( -
    - +
    + {fallback ? ( - + ) : ( )} -
    +
    {name ? ( { {isLoaded && ( - -
    - -
    + +
    + +
    { className="text-ui-fg-subtle" data-testid="sidebar-header-dropdown-store-label" > - {t("app.nav.main.store")} + {t('app.nav.main.store')}
    - + - {t("app.nav.main.storeSettings")} + {t('app.nav.main.storeSettings')} @@ -191,133 +219,133 @@ const Header = () => { ); }; -const useCoreRoutes = (): Omit[] => { +const useCoreRoutes = (): Omit[] => { const { t } = useTranslation(); return [ { icon: , - label: t("orders.domain"), - to: "/orders", + label: t('orders.domain'), + to: '/orders', items: [ // TODO: Enable when domin is introduced // { // label: t("draftOrders.domain"), // to: "/draft-orders", // }, - ], + ] }, { icon: , - label: t("products.domain"), - to: "/products", + label: t('products.domain'), + to: '/products', items: [ { - label: t("collections.domain"), - to: "/collections", + label: t('collections.domain'), + to: '/collections' }, { - label: t("categories.domain"), - to: "/categories", - }, + label: t('categories.domain'), + to: '/categories' + } // TODO: Enable when domin is introduced // { // label: t("giftCards.domain"), // to: "/gift-cards", // }, - ], + ] }, { icon: , - label: t("inventory.domain"), - to: "/inventory", + label: t('inventory.domain'), + to: '/inventory', items: [ { - label: t("reservations.domain"), - to: "/reservations", - }, - ], + label: t('reservations.domain'), + to: '/reservations' + } + ] }, { icon: , - label: t("customers.domain"), - to: "/customers", + label: t('customers.domain'), + to: '/customers', items: [ { - label: t("customerGroups.domain"), - to: "/customer-groups", - }, - ], + label: t('customerGroups.domain'), + to: '/customer-groups' + } + ] }, { icon: , - label: t("sellers.domain"), - to: "/sellers", + label: t('sellers.domain'), + to: '/sellers' }, { icon: , - label: t("promotions.domain"), - to: "/promotions", + label: t('promotions.domain'), + to: '/promotions', items: [ { - label: t("campaigns.domain"), - to: "/campaigns", - }, - ], + label: t('campaigns.domain'), + to: '/campaigns' + } + ] }, { icon: , - label: t("priceLists.domain"), - to: "/price-lists", + label: t('priceLists.domain'), + to: '/price-lists' }, { icon: , - label: t("requests.domain"), - to: "requests/seller", + label: t('requests.domain'), + to: 'requests/seller', items: [ { - label: t("requests.seller"), - to: "/requests/seller", + label: t('requests.seller'), + to: '/requests/seller' }, { - label: t("requests.product"), - to: "/requests/product/", + label: t('requests.product'), + to: '/requests/product/' }, { - label: t("requests.product-tag"), - to: "/requests/product-tag", + label: t('requests.product-tag'), + to: '/requests/product-tag' }, { - label: t("requests.product-type"), - to: "/requests/product-type", + label: t('requests.product-type'), + to: '/requests/product-type' }, { - label: t("requests.review-remove"), - to: "/requests/review-remove", + label: t('requests.review-remove'), + to: '/requests/review-remove' }, { - label: t("requests.product-update"), - to: "/requests/product-update", + label: t('requests.product-update'), + to: '/requests/product-update' }, { - label: t("requests.return"), - to: "/requests/return", + label: t('requests.return'), + to: '/requests/return' }, { - label: t("requests.product-category"), - to: "/requests/product-category", + label: t('requests.product-category'), + to: '/requests/product-category' }, { - label: t("requests.product-collection"), - to: "/requests/product-collection", - }, - ], + label: t('requests.product-collection'), + to: '/requests/product-collection' + } + ] }, { icon: , - label: t("messages.domain"), - to: "/messages", - }, + label: t('messages.domain'), + to: '/messages' + } ]; }; @@ -326,23 +354,34 @@ const Searchbar = () => { const { toggleSearch } = useSearch(); return ( -
    +
    @@ -355,11 +394,11 @@ const CoreRouteSection = () => { const { getMenu } = useExtension(); - const menuItems = getMenu("coreExtensions"); + const menuItems = getMenu('coreExtensions'); - menuItems.forEach((item) => { + menuItems.forEach(item => { if (item.nested) { - const route = coreRoutes.find((route) => route.to === item.nested); + const route = coreRoutes.find(route => route.to === item.nested); if (route) { route.items?.push(item); } @@ -372,8 +411,13 @@ const CoreRouteSection = () => { data-testid="sidebar-core-routes" > - {coreRoutes.map((route) => { - return ; + {coreRoutes.map(route => { + return ( + + ); })} ); @@ -383,7 +427,7 @@ const ExtensionRouteSection = () => { const { t } = useTranslation(); const { getMenu } = useExtension(); - const menuItems = getMenu("coreExtensions").filter((item) => !item.nested); + const menuItems = getMenu('coreExtensions').filter(item => !item.nested); if (!menuItems.length) { return null; @@ -397,10 +441,17 @@ const ExtensionRouteSection = () => {
    - + - ) + ); } if (isError) { - throw error + throw error; } return ( -
    +
    -
    +
    {fallback ? ( - + ) : ( )}
    -
    +
    {displayName ? ( {
    - ) -} + ); +}; const ThemeToggle = () => { - const { t } = useTranslation() - const { theme, setTheme } = useTheme() + const { t } = useTranslation(); + const { theme, setTheme } = useTheme(); return ( - - - {t("app.menus.user.theme.label")} + + + {t('app.menus.user.theme.label')} - + { - e.preventDefault() - setTheme("system") + onClick={e => { + e.preventDefault(); + setTheme('system'); }} data-testid="sidebar-user-menu-theme-system" > - {t("app.menus.user.theme.system")} + {t('app.menus.user.theme.system')} { - e.preventDefault() - setTheme("light") + onClick={e => { + e.preventDefault(); + setTheme('light'); }} data-testid="sidebar-user-menu-theme-light" > - {t("app.menus.user.theme.light")} + {t('app.menus.user.theme.light')} { - e.preventDefault() - setTheme("dark") + onClick={e => { + e.preventDefault(); + setTheme('dark'); }} data-testid="sidebar-user-menu-theme-dark" > - {t("app.menus.user.theme.dark")} + {t('app.menus.user.theme.dark')} - ) -} + ); +}; const Logout = () => { - const { t } = useTranslation() - const navigate = useNavigate() + const { t } = useTranslation(); + const navigate = useNavigate(); - const { mutateAsync: logoutMutation } = useLogout() + const { mutateAsync: logoutMutation } = useLogout(); const handleLogout = async () => { await logoutMutation(undefined, { @@ -206,54 +248,81 @@ const Logout = () => { /** * When the user logs out, we want to clear the query cache */ - queryClient.clear() - navigate("/login") - }, - }) - } + queryClient.clear(); + navigate('/login'); + } + }); + }; return ( - +
    - {t("app.menus.actions.logout")} + {t('app.menus.actions.logout')}
    - ) -} + ); +}; -const GlobalKeybindsModal = (props: { - open: boolean - onOpenChange: (open: boolean) => void -}) => { - const { t } = useTranslation() - const globalShortcuts = useGlobalShortcuts() +const GlobalKeybindsModal = (props: { open: boolean; onOpenChange: (open: boolean) => void }) => { + const { t } = useTranslation(); + const globalShortcuts = useGlobalShortcuts(); - const [searchValue, onSearchValueChange] = useState("") + const [searchValue, onSearchValueChange] = useState(''); const searchResults = searchValue - ? globalShortcuts.filter((shortcut) => { - return shortcut.label.toLowerCase().includes(searchValue?.toLowerCase()) + ? globalShortcuts.filter(shortcut => { + return shortcut.label.toLowerCase().includes(searchValue?.toLowerCase()); }) - : globalShortcuts + : globalShortcuts; return ( - + - - -
    -
    + + +
    +
    - {t("app.menus.user.shortcuts")} + + {t('app.menus.user.shortcuts')} + - +
    -
    +
    esc - + @@ -263,64 +332,89 @@ const GlobalKeybindsModal = (props: { onSearchValueChange(e.target.value)} + onChange={e => onSearchValueChange(e.target.value)} data-testid="shortcuts-modal-search" />
    -
    +
    {searchResults.map((shortcut, index) => { return (
    - {shortcut.label} -
    + + {shortcut.label} + +
    {shortcut.keys.Mac?.map((key, index) => { return ( -
    - {key} +
    + + {key} + {index < (shortcut.keys.Mac?.length || 0) - 1 && ( - - {t("app.keyboardShortcuts.then")} + + {t('app.keyboardShortcuts.then')} )}
    - ) + ); })}
    - ) + ); })}
    - ) -} + ); +}; const UserItem = () => { - const { user, isPending, isError, error } = useMe() + const { user, isPending, isError, error } = useMe(); - const loaded = !isPending && !!user + const loaded = !isPending && !!user; if (!loaded) { - return
    + return
    ; } - const name = [user.first_name, user.last_name].filter(Boolean).join(" ") - const email = user.email - const fallback = name ? name[0].toUpperCase() : email[0].toUpperCase() - const avatar = user.avatar_url + const name = [user.first_name, user.last_name].filter(Boolean).join(' '); + const email = user.email; + const fallback = name ? name[0].toUpperCase() : email[0].toUpperCase(); + const avatar = user.avatar_url; if (isError) { - throw error + throw error; } return ( -
    +
    { fallback={fallback} data-testid="sidebar-user-menu-item-avatar" /> -
    +
    { {email} @@ -350,5 +447,5 @@ const UserItem = () => { )}
    - ) -} + ); +}; diff --git a/src/components/localization/localized-table-pagination/index.ts b/src/components/localization/localized-table-pagination/index.ts index 40d1ccdf..541d9c9a 100644 --- a/src/components/localization/localized-table-pagination/index.ts +++ b/src/components/localization/localized-table-pagination/index.ts @@ -1 +1 @@ -export * from "./localized-table-pagination" +export * from './localized-table-pagination'; diff --git a/src/components/localization/localized-table-pagination/localized-table-pagination.tsx b/src/components/localization/localized-table-pagination/localized-table-pagination.tsx index 8ec8a469..88e06fe6 100644 --- a/src/components/localization/localized-table-pagination/localized-table-pagination.tsx +++ b/src/components/localization/localized-table-pagination/localized-table-pagination.tsx @@ -1,26 +1,33 @@ -import { Table } from "@medusajs/ui" -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react" -import { useTranslation } from "react-i18next" +import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from 'react'; + +import { Table } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; type LocalizedTablePaginationProps = Omit< ComponentPropsWithoutRef, - "translations" -> + 'translations' +>; export const LocalizedTablePagination = forwardRef< ElementRef, LocalizedTablePaginationProps >((props, ref) => { - const { t } = useTranslation() + const { t } = useTranslation(); const translations = { - of: t("general.of"), - results: t("general.results"), - pages: t("general.pages"), - prev: t("general.prev"), - next: t("general.next"), - } + of: t('general.of'), + results: t('general.results'), + pages: t('general.pages'), + prev: t('general.prev'), + next: t('general.next') + }; - return -}) -LocalizedTablePagination.displayName = "LocalizedTablePagination" + return ( + + ); +}); +LocalizedTablePagination.displayName = 'LocalizedTablePagination'; diff --git a/src/components/modals/hooks/use-state-aware-to.tsx b/src/components/modals/hooks/use-state-aware-to.tsx index 83818902..79285f1f 100644 --- a/src/components/modals/hooks/use-state-aware-to.tsx +++ b/src/components/modals/hooks/use-state-aware-to.tsx @@ -1,5 +1,6 @@ -import { useMemo } from "react" -import { Path, useLocation } from "react-router-dom" +import { useMemo } from 'react'; + +import { useLocation, type Path } from 'react-router-dom'; /** * Checks if the current location has a restore_params property. @@ -11,22 +12,20 @@ import { Path, useLocation } from "react-router-dom" * the params that were present when the modal was opened. */ export const useStateAwareTo = (prev: string | Partial) => { - const location = useLocation() + const location = useLocation(); - const to = useMemo(() => { - const params = location.state?.restore_params + return useMemo(() => { + const params = location.state?.restore_params; if (params) { - return `${prev}?${params.toString()}` + return `${prev}?${params.toString()}`; } // If no restore_params in state, check if the current URL has search params if (location.search) { - return `${prev}${location.search}` + return `${prev}${location.search}`; } - return prev - }, [location.state, location.search, prev]) - - return to -} + return prev; + }, [location.state, location.search, prev]); +}; diff --git a/src/components/modals/index.ts b/src/components/modals/index.ts index 566853b6..2b453143 100644 --- a/src/components/modals/index.ts +++ b/src/components/modals/index.ts @@ -1,7 +1,7 @@ -export { RouteDrawer } from "./route-drawer" -export { RouteFocusModal } from "./route-focus-modal" -export { useRouteModal } from "./route-modal-provider" +export { RouteDrawer } from './route-drawer'; +export { RouteFocusModal } from './route-focus-modal'; +export { useRouteModal } from './route-modal-provider'; -export { StackedDrawer } from "./stacked-drawer" -export { StackedFocusModal } from "./stacked-focus-modal" -export { useStackedModal } from "./stacked-modal-provider" +export { StackedDrawer } from './stacked-drawer'; +export { StackedFocusModal } from './stacked-focus-modal'; +export { useStackedModal } from './stacked-modal-provider'; diff --git a/src/components/modals/route-drawer/index.ts b/src/components/modals/route-drawer/index.ts index 82f4bb96..ce21021f 100644 --- a/src/components/modals/route-drawer/index.ts +++ b/src/components/modals/route-drawer/index.ts @@ -1 +1 @@ -export * from "./route-drawer" +export * from './route-drawer'; diff --git a/src/components/modals/route-drawer/route-drawer.tsx b/src/components/modals/route-drawer/route-drawer.tsx index 3de85342..17211471 100644 --- a/src/components/modals/route-drawer/route-drawer.tsx +++ b/src/components/modals/route-drawer/route-drawer.tsx @@ -1,53 +1,58 @@ -import { Drawer, clx } from "@medusajs/ui" -import { PropsWithChildren, useEffect, useState } from "react" -import { Path, useNavigate } from "react-router-dom" -import { useStateAwareTo } from "../hooks/use-state-aware-to" -import { RouteModalForm } from "../route-modal-form" -import { RouteModalProvider } from "../route-modal-provider/route-provider" -import { StackedModalProvider } from "../stacked-modal-provider" +import { useEffect, useState, type PropsWithChildren } from 'react'; + +import { useStateAwareTo } from '@components/modals/hooks/use-state-aware-to'; +import { RouteModalForm } from '@components/modals/route-modal-form'; +import { RouteModalProvider } from '@components/modals/route-modal-provider/route-provider'; +import { StackedModalProvider } from '@components/modals/stacked-modal-provider'; +import { clx, Drawer } from '@medusajs/ui'; +import { useNavigate, type Path } from 'react-router-dom'; type RouteDrawerProps = PropsWithChildren<{ - prev?: string | Partial -}> + prev?: string | Partial; +}>; -const Root = ({ prev = "..", children }: RouteDrawerProps) => { - const navigate = useNavigate() - const [open, setOpen] = useState(false) - const [stackedModalOpen, onStackedModalOpen] = useState(false) +const Root = ({ prev = '..', children }: RouteDrawerProps) => { + const navigate = useNavigate(); + const [open, setOpen] = useState(false); + const [stackedModalOpen, onStackedModalOpen] = useState(false); - const to = useStateAwareTo(prev) + const to = useStateAwareTo(prev); /** * Open the modal when the component mounts. This * ensures that the entry animation is played. */ useEffect(() => { - setOpen(true) + setOpen(true); return () => { - setOpen(false) - onStackedModalOpen(false) - } - }, []) + setOpen(false); + onStackedModalOpen(false); + }; + }, []); const handleOpenChange = (open: boolean) => { if (!open) { - document.body.style.pointerEvents = "auto" - navigate(to, { replace: true }) - return + document.body.style.pointerEvents = 'auto'; + navigate(to, { replace: true }); + + return; } - setOpen(open) - } + setOpen(open); + }; return ( - + {children} @@ -55,16 +60,16 @@ const Root = ({ prev = "..", children }: RouteDrawerProps) => { - ) -} + ); +}; -const Header = Drawer.Header -const Title = Drawer.Title -const Description = Drawer.Description -const Body = Drawer.Body -const Footer = Drawer.Footer -const Close = Drawer.Close -const Form = RouteModalForm +const Header = Drawer.Header; +const Title = Drawer.Title; +const Description = Drawer.Description; +const Body = Drawer.Body; +const Footer = Drawer.Footer; +const Close = Drawer.Close; +const Form = RouteModalForm; /** * Drawer that is used to render a form on a separate route. @@ -78,5 +83,5 @@ export const RouteDrawer = Object.assign(Root, { Description, Footer, Close, - Form, -}) + Form +}); diff --git a/src/components/modals/route-focus-modal/index.ts b/src/components/modals/route-focus-modal/index.ts index 764e6c80..b5d75d5d 100644 --- a/src/components/modals/route-focus-modal/index.ts +++ b/src/components/modals/route-focus-modal/index.ts @@ -1 +1 @@ -export * from "./route-focus-modal" +export * from './route-focus-modal'; diff --git a/src/components/modals/route-focus-modal/route-focus-modal.tsx b/src/components/modals/route-focus-modal/route-focus-modal.tsx index 4ee7b0e0..cc9bf31d 100644 --- a/src/components/modals/route-focus-modal/route-focus-modal.tsx +++ b/src/components/modals/route-focus-modal/route-focus-modal.tsx @@ -1,91 +1,96 @@ -import { FocusModal, clx } from "@medusajs/ui" -import { PropsWithChildren, useEffect, useState } from "react" -import { Path, useNavigate } from "react-router-dom" -import { useStateAwareTo } from "../hooks/use-state-aware-to" -import { RouteModalForm } from "../route-modal-form" -import { useRouteModal } from "../route-modal-provider" -import { RouteModalProvider } from "../route-modal-provider/route-provider" -import { StackedModalProvider } from "../stacked-modal-provider" +import { useEffect, useState, type PropsWithChildren } from 'react'; + +import { useStateAwareTo } from '@components/modals/hooks/use-state-aware-to'; +import { RouteModalForm } from '@components/modals/route-modal-form'; +import { useRouteModal } from '@components/modals/route-modal-provider'; +import { RouteModalProvider } from '@components/modals/route-modal-provider/route-provider'; +import { StackedModalProvider } from '@components/modals/stacked-modal-provider'; +import { clx, FocusModal } from '@medusajs/ui'; +import { useNavigate, type Path } from 'react-router-dom'; type RouteFocusModalProps = PropsWithChildren<{ - prev?: string | Partial -}> + prev?: string | Partial; +}>; -const Root = ({ prev = "..", children }: RouteFocusModalProps) => { - const navigate = useNavigate() - const [open, setOpen] = useState(false) - const [stackedModalOpen, onStackedModalOpen] = useState(false) +const Root = ({ prev = '..', children }: RouteFocusModalProps) => { + const navigate = useNavigate(); + const [open, setOpen] = useState(false); + const [stackedModalOpen, onStackedModalOpen] = useState(false); - const to = useStateAwareTo(prev) + const to = useStateAwareTo(prev); /** * Open the modal when the component mounts. This * ensures that the entry animation is played. */ useEffect(() => { - setOpen(true) + setOpen(true); return () => { - setOpen(false) - onStackedModalOpen(false) - } - }, []) + setOpen(false); + onStackedModalOpen(false); + }; + }, []); const handleOpenChange = (open: boolean) => { if (!open) { - document.body.style.pointerEvents = "auto" - navigate(to, { replace: true }) - return + document.body.style.pointerEvents = 'auto'; + navigate(to, { replace: true }); + + return; } - setOpen(open) - } + setOpen(open); + }; return ( - + {children} - ) -} + ); +}; type ContentProps = PropsWithChildren<{ - stackedModalOpen: boolean -}> + stackedModalOpen: boolean; +}>; const Content = ({ stackedModalOpen, children }: ContentProps) => { - const { __internal } = useRouteModal() + const { __internal } = useRouteModal(); - const shouldPreventClose = !__internal.closeOnEscape + const shouldPreventClose = !__internal.closeOnEscape; return ( { - e.preventDefault() + ? e => { + e.preventDefault(); } : undefined } className={clx({ - "!bg-ui-bg-disabled !inset-x-5 !inset-y-3": stackedModalOpen, + '!inset-x-5 !inset-y-3 !bg-ui-bg-disabled': stackedModalOpen })} > {children} - ) -} + ); +}; -const Header = FocusModal.Header -const Title = FocusModal.Title -const Description = FocusModal.Description -const Footer = FocusModal.Footer -const Body = FocusModal.Body -const Close = FocusModal.Close -const Form = RouteModalForm +const Header = FocusModal.Header; +const Title = FocusModal.Title; +const Description = FocusModal.Description; +const Footer = FocusModal.Footer; +const Body = FocusModal.Body; +const Close = FocusModal.Close; +const Form = RouteModalForm; /** * FocusModal that is used to render a form on a separate route. @@ -100,5 +105,5 @@ export const RouteFocusModal = Object.assign(Root, { Description, Footer, Close, - Form, -}) + Form +}); diff --git a/src/components/modals/route-modal-form/index.ts b/src/components/modals/route-modal-form/index.ts index 0e1450ed..112fc217 100644 --- a/src/components/modals/route-modal-form/index.ts +++ b/src/components/modals/route-modal-form/index.ts @@ -1 +1 @@ -export * from "./route-modal-form" +export * from './route-modal-form'; diff --git a/src/components/modals/route-modal-form/route-modal-form.tsx b/src/components/modals/route-modal-form/route-modal-form.tsx index 96e4eabe..1d17cca9 100644 --- a/src/components/modals/route-modal-form/route-modal-form.tsx +++ b/src/components/modals/route-modal-form/route-modal-form.tsx @@ -1,90 +1,103 @@ -import { Prompt } from "@medusajs/ui" -import { PropsWithChildren } from "react" -import { FieldValues, UseFormReturn } from "react-hook-form" -import { useTranslation } from "react-i18next" -import { useBlocker } from "react-router-dom" -import { Form } from "../../common/form" +import type { PropsWithChildren } from 'react'; + +import { Prompt } from '@medusajs/ui'; +import type { FieldValues, UseFormReturn } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useBlocker } from 'react-router-dom'; + +import { Form } from '../../common/form'; type RouteModalFormProps = PropsWithChildren<{ - form: UseFormReturn - blockSearchParams?: boolean - onClose?: (isSubmitSuccessful: boolean) => void - "data-testid"?: string -}> + form: UseFormReturn; + blockSearchParams?: boolean; + onClose?: (isSubmitSuccessful: boolean) => void; + 'data-testid'?: string; +}>; export const RouteModalForm = ({ form, blockSearchParams: blockSearch = false, children, onClose, - "data-testid": dataTestId, + 'data-testid': dataTestId }: RouteModalFormProps) => { - const { t } = useTranslation() + const { t } = useTranslation(); const { - formState: { isDirty }, - } = form + formState: { isDirty } + } = form; const blocker = useBlocker(({ currentLocation, nextLocation }) => { - const { isSubmitSuccessful } = nextLocation.state || {} + const { isSubmitSuccessful } = nextLocation.state || {}; if (isSubmitSuccessful) { - onClose?.(true) - return false + onClose?.(true); + + return false; } - const isPathChanged = currentLocation.pathname !== nextLocation.pathname - const isSearchChanged = currentLocation.search !== nextLocation.search + const isPathChanged = currentLocation.pathname !== nextLocation.pathname; + const isSearchChanged = currentLocation.search !== nextLocation.search; if (blockSearch) { - const shouldBlock = isDirty && (isPathChanged || isSearchChanged) + const shouldBlock = isDirty && (isPathChanged || isSearchChanged); if (isPathChanged) { - onClose?.(isSubmitSuccessful) + onClose?.(isSubmitSuccessful); } - return shouldBlock + return shouldBlock; } - const shouldBlock = isDirty && isPathChanged + const shouldBlock = isDirty && isPathChanged; if (isPathChanged) { - onClose?.(isSubmitSuccessful) + onClose?.(isSubmitSuccessful); } - return shouldBlock - }) + return shouldBlock; + }); const handleCancel = () => { - blocker?.reset?.() - } + blocker?.reset?.(); + }; const handleContinue = () => { - blocker?.proceed?.() - onClose?.(false) - } + blocker?.proceed?.(); + onClose?.(false); + }; return ( -
    + {children} - + - {t("general.unsavedChangesTitle")} - - {t("general.unsavedChangesDescription")} - + {t('general.unsavedChangesTitle')} + {t('general.unsavedChangesDescription')} - - {t("actions.cancel")} + + {t('actions.cancel')} - - {t("actions.continue")} + + {t('actions.continue')} - ) -} + ); +}; diff --git a/src/components/modals/route-modal-provider/index.ts b/src/components/modals/route-modal-provider/index.ts index f819a019..1469b57f 100644 --- a/src/components/modals/route-modal-provider/index.ts +++ b/src/components/modals/route-modal-provider/index.ts @@ -1,2 +1,2 @@ -export * from "./route-provider" -export * from "./use-route-modal" +export * from './route-provider'; +export * from './use-route-modal'; diff --git a/src/components/modals/route-modal-provider/route-modal-context.tsx b/src/components/modals/route-modal-provider/route-modal-context.tsx index 95a5aae5..176b017d 100644 --- a/src/components/modals/route-modal-provider/route-modal-context.tsx +++ b/src/components/modals/route-modal-provider/route-modal-context.tsx @@ -1,12 +1,11 @@ -import { createContext } from "react" +import { createContext } from 'react'; type RouteModalProviderState = { - handleSuccess: (path?: string) => void - setCloseOnEscape: (value: boolean) => void + handleSuccess: (path?: string) => void; + setCloseOnEscape: (value: boolean) => void; __internal: { - closeOnEscape: boolean - } -} + closeOnEscape: boolean; + }; +}; -export const RouteModalProviderContext = - createContext(null) +export const RouteModalProviderContext = createContext(null); diff --git a/src/components/modals/route-modal-provider/route-provider.tsx b/src/components/modals/route-modal-provider/route-provider.tsx index 44e51398..59a3292b 100644 --- a/src/components/modals/route-modal-provider/route-provider.tsx +++ b/src/components/modals/route-modal-provider/route-provider.tsx @@ -1,39 +1,38 @@ -import { PropsWithChildren, useCallback, useMemo, useState } from "react" -import { Path, useNavigate } from "react-router-dom" -import { RouteModalProviderContext } from "./route-modal-context" +import { useCallback, useMemo, useState, type PropsWithChildren } from 'react'; + +import { useNavigate, type Path } from 'react-router-dom'; + +import { RouteModalProviderContext } from './route-modal-context'; type RouteModalProviderProps = PropsWithChildren<{ - prev: string | Partial -}> + prev: string | Partial; +}>; -export const RouteModalProvider = ({ - prev, - children, -}: RouteModalProviderProps) => { - const navigate = useNavigate() +export const RouteModalProvider = ({ prev, children }: RouteModalProviderProps) => { + const navigate = useNavigate(); - const [closeOnEscape, setCloseOnEscape] = useState(true) + const [closeOnEscape, setCloseOnEscape] = useState(true); const handleSuccess = useCallback( (path?: string) => { - const to = path || prev - navigate(to, { replace: true, state: { isSubmitSuccessful: true } }) + const to = path || prev; + navigate(to, { replace: true, state: { isSubmitSuccessful: true } }); }, [navigate, prev] - ) + ); const value = useMemo( () => ({ handleSuccess, setCloseOnEscape, - __internal: { closeOnEscape }, + __internal: { closeOnEscape } }), [handleSuccess, setCloseOnEscape, closeOnEscape] - ) + ); return ( {children} - ) -} + ); +}; diff --git a/src/components/modals/route-modal-provider/use-route-modal.tsx b/src/components/modals/route-modal-provider/use-route-modal.tsx index 04dad573..9c1b0740 100644 --- a/src/components/modals/route-modal-provider/use-route-modal.tsx +++ b/src/components/modals/route-modal-provider/use-route-modal.tsx @@ -1,12 +1,13 @@ -import { useContext } from "react" -import { RouteModalProviderContext } from "./route-modal-context" +import { useContext } from 'react'; + +import { RouteModalProviderContext } from './route-modal-context'; export const useRouteModal = () => { - const context = useContext(RouteModalProviderContext) + const context = useContext(RouteModalProviderContext); if (!context) { - throw new Error("useRouteModal must be used within a RouteModalProvider") + throw new Error('useRouteModal must be used within a RouteModalProvider'); } - return context -} + return context; +}; diff --git a/src/components/modals/stacked-drawer/index.ts b/src/components/modals/stacked-drawer/index.ts index 2467d18d..08fd5a9d 100644 --- a/src/components/modals/stacked-drawer/index.ts +++ b/src/components/modals/stacked-drawer/index.ts @@ -1 +1 @@ -export * from "./stacked-drawer" +export * from './stacked-drawer'; diff --git a/src/components/modals/stacked-drawer/stacked-drawer.tsx b/src/components/modals/stacked-drawer/stacked-drawer.tsx index 9c3c2091..530633a1 100644 --- a/src/components/modals/stacked-drawer/stacked-drawer.tsx +++ b/src/components/modals/stacked-drawer/stacked-drawer.tsx @@ -1,77 +1,79 @@ -import { Drawer, clx } from "@medusajs/ui" import { - ComponentPropsWithoutRef, - PropsWithChildren, forwardRef, useEffect, -} from "react" -import { useStackedModal } from "../stacked-modal-provider" + type ComponentPropsWithoutRef, + type PropsWithChildren +} from 'react'; + +import { useStackedModal } from '@components/modals/stacked-modal-provider'; +import { clx, Drawer } from '@medusajs/ui'; type StackedDrawerProps = PropsWithChildren<{ /** * A unique identifier for the modal. This is used to differentiate stacked modals, * when multiple stacked modals are registered to the same parent modal. */ - id: string -}> + id: string; +}>; /** * A stacked modal that can be rendered above a parent modal. */ export const Root = ({ id, children }: StackedDrawerProps) => { - const { register, unregister, getIsOpen, setIsOpen } = useStackedModal() + const { register, unregister, getIsOpen, setIsOpen } = useStackedModal(); useEffect(() => { - register(id) + register(id); - return () => unregister(id) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + return () => unregister(id); + }, []); return ( - setIsOpen(id, open)}> + setIsOpen(id, open)} + > {children} - ) -} + ); +}; -const Close = Drawer.Close -Close.displayName = "StackedDrawer.Close" +const Close = Drawer.Close; +Close.displayName = 'StackedDrawer.Close'; -const Header = Drawer.Header -Header.displayName = "StackedDrawer.Header" +const Header = Drawer.Header; +Header.displayName = 'StackedDrawer.Header'; -const Body = Drawer.Body -Body.displayName = "StackedDrawer.Body" +const Body = Drawer.Body; +Body.displayName = 'StackedDrawer.Body'; -const Trigger = Drawer.Trigger -Trigger.displayName = "StackedDrawer.Trigger" +const Trigger = Drawer.Trigger; +Trigger.displayName = 'StackedDrawer.Trigger'; -const Footer = Drawer.Footer -Footer.displayName = "StackedDrawer.Footer" +const Footer = Drawer.Footer; +Footer.displayName = 'StackedDrawer.Footer'; -const Title = Drawer.Title -Title.displayName = "StackedDrawer.Title" +const Title = Drawer.Title; +Title.displayName = 'StackedDrawer.Title'; -const Description = Drawer.Description -Description.displayName = "StackedDrawer.Description" +const Description = Drawer.Description; +Description.displayName = 'StackedDrawer.Description'; -const Content = forwardRef< - HTMLDivElement, - ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - return ( - - ) -}) -Content.displayName = "StackedDrawer.Content" +const Content = forwardRef>( + ({ className, ...props }, ref) => { + return ( + + ); + } +); +Content.displayName = 'StackedDrawer.Content'; export const StackedDrawer = Object.assign(Root, { Close, @@ -81,5 +83,5 @@ export const StackedDrawer = Object.assign(Root, { Trigger, Footer, Description, - Title, -}) + Title +}); diff --git a/src/components/modals/stacked-focus-modal/index.ts b/src/components/modals/stacked-focus-modal/index.ts index e1c50817..e4a0494b 100644 --- a/src/components/modals/stacked-focus-modal/index.ts +++ b/src/components/modals/stacked-focus-modal/index.ts @@ -1 +1 @@ -export * from "./stacked-focus-modal" +export * from './stacked-focus-modal'; diff --git a/src/components/modals/stacked-focus-modal/stacked-focus-modal.tsx b/src/components/modals/stacked-focus-modal/stacked-focus-modal.tsx index 92764504..91528d4b 100644 --- a/src/components/modals/stacked-focus-modal/stacked-focus-modal.tsx +++ b/src/components/modals/stacked-focus-modal/stacked-focus-modal.tsx @@ -1,90 +1,88 @@ -import { FocusModal, clx } from "@medusajs/ui" import { - ComponentPropsWithoutRef, - PropsWithChildren, forwardRef, useEffect, -} from "react" -import { useStackedModal } from "../stacked-modal-provider" + type ComponentPropsWithoutRef, + type PropsWithChildren +} from 'react'; + +import { useStackedModal } from '@components/modals/stacked-modal-provider'; +import { clx, FocusModal } from '@medusajs/ui'; type StackedFocusModalProps = PropsWithChildren<{ /** * A unique identifier for the modal. This is used to differentiate stacked modals, * when multiple stacked modals are registered to the same parent modal. */ - id: string + id: string; /** * An optional callback that is called when the modal is opened or closed. */ - onOpenChangeCallback?: (open: boolean) => void -}> + onOpenChangeCallback?: (open: boolean) => void; +}>; /** * A stacked modal that can be rendered above a parent modal. */ -export const Root = ({ - id, - onOpenChangeCallback, - children, -}: StackedFocusModalProps) => { - const { register, unregister, getIsOpen, setIsOpen } = useStackedModal() +export const Root = ({ id, onOpenChangeCallback, children }: StackedFocusModalProps) => { + const { register, unregister, getIsOpen, setIsOpen } = useStackedModal(); useEffect(() => { - register(id) + register(id); - return () => unregister(id) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + return () => unregister(id); + }, []); const handleOpenChange = (open: boolean) => { - setIsOpen(id, open) - onOpenChangeCallback?.(open) - } + setIsOpen(id, open); + onOpenChangeCallback?.(open); + }; return ( - + {children} - ) -} + ); +}; -const Close = FocusModal.Close -Close.displayName = "StackedFocusModal.Close" +const Close = FocusModal.Close; +Close.displayName = 'StackedFocusModal.Close'; -const Header = FocusModal.Header -Header.displayName = "StackedFocusModal.Header" +const Header = FocusModal.Header; +Header.displayName = 'StackedFocusModal.Header'; -const Body = FocusModal.Body -Body.displayName = "StackedFocusModal.Body" +const Body = FocusModal.Body; +Body.displayName = 'StackedFocusModal.Body'; -const Trigger = FocusModal.Trigger -Trigger.displayName = "StackedFocusModal.Trigger" +const Trigger = FocusModal.Trigger; +Trigger.displayName = 'StackedFocusModal.Trigger'; -const Footer = FocusModal.Footer -Footer.displayName = "StackedFocusModal.Footer" +const Footer = FocusModal.Footer; +Footer.displayName = 'StackedFocusModal.Footer'; -const Title = FocusModal.Title -Title.displayName = "StackedFocusModal.Title" +const Title = FocusModal.Title; +Title.displayName = 'StackedFocusModal.Title'; -const Description = FocusModal.Description -Description.displayName = "StackedFocusModal.Description" +const Description = FocusModal.Description; +Description.displayName = 'StackedFocusModal.Description'; -const Content = forwardRef< - HTMLDivElement, - ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - return ( - - ) -}) -Content.displayName = "StackedFocusModal.Content" +const Content = forwardRef>( + ({ className, ...props }, ref) => { + return ( + + ); + } +); +Content.displayName = 'StackedFocusModal.Content'; export const StackedFocusModal = Object.assign(Root, { Close, @@ -94,5 +92,5 @@ export const StackedFocusModal = Object.assign(Root, { Trigger, Footer, Description, - Title, -}) + Title +}); diff --git a/src/components/modals/stacked-modal-provider/index.ts b/src/components/modals/stacked-modal-provider/index.ts index 998c3fba..fffa9a03 100644 --- a/src/components/modals/stacked-modal-provider/index.ts +++ b/src/components/modals/stacked-modal-provider/index.ts @@ -1,2 +1,2 @@ -export * from "./stacked-modal-provider" -export * from "./use-stacked-modal" +export * from './stacked-modal-provider'; +export * from './use-stacked-modal'; diff --git a/src/components/modals/stacked-modal-provider/stacked-modal-context.tsx b/src/components/modals/stacked-modal-provider/stacked-modal-context.tsx index ebd50944..4939ed20 100644 --- a/src/components/modals/stacked-modal-provider/stacked-modal-context.tsx +++ b/src/components/modals/stacked-modal-provider/stacked-modal-context.tsx @@ -1,10 +1,10 @@ -import { createContext } from "react" +import { createContext } from 'react'; type StackedModalState = { - getIsOpen: (id: string) => boolean - setIsOpen: (id: string, open: boolean) => void - register: (id: string) => void - unregister: (id: string) => void -} + getIsOpen: (id: string) => boolean; + setIsOpen: (id: string, open: boolean) => void; + register: (id: string) => void; + unregister: (id: string) => void; +}; -export const StackedModalContext = createContext(null) +export const StackedModalContext = createContext(null); diff --git a/src/components/modals/stacked-modal-provider/stacked-modal-provider.tsx b/src/components/modals/stacked-modal-provider/stacked-modal-provider.tsx index ca3799cc..c6d9128f 100644 --- a/src/components/modals/stacked-modal-provider/stacked-modal-provider.tsx +++ b/src/components/modals/stacked-modal-provider/stacked-modal-provider.tsx @@ -1,43 +1,42 @@ -import { PropsWithChildren, useState } from "react" -import { StackedModalContext } from "./stacked-modal-context" +import { useState, type PropsWithChildren } from 'react'; + +import { StackedModalContext } from './stacked-modal-context'; type StackedModalProviderProps = PropsWithChildren<{ - onOpenChange: (open: boolean) => void -}> + onOpenChange: (open: boolean) => void; +}>; -export const StackedModalProvider = ({ - children, - onOpenChange, -}: StackedModalProviderProps) => { - const [state, setState] = useState>({}) +export const StackedModalProvider = ({ children, onOpenChange }: StackedModalProviderProps) => { + const [state, setState] = useState>({}); const getIsOpen = (id: string) => { - return state[id] || false - } + return state[id] || false; + }; const setIsOpen = (id: string, open: boolean) => { - setState((prevState) => ({ + setState(prevState => ({ ...prevState, - [id]: open, - })) + [id]: open + })); - onOpenChange(open) - } + onOpenChange(open); + }; const register = (id: string) => { - setState((prevState) => ({ + setState(prevState => ({ ...prevState, - [id]: false, - })) - } + [id]: false + })); + }; const unregister = (id: string) => { - setState((prevState) => { - const newState = { ...prevState } - delete newState[id] - return newState - }) - } + setState(prevState => { + const newState = { ...prevState }; + delete newState[id]; + + return newState; + }); + }; return ( {children} - ) -} + ); +}; diff --git a/src/components/modals/stacked-modal-provider/use-stacked-modal.ts b/src/components/modals/stacked-modal-provider/use-stacked-modal.ts index f246c0d0..c5f59991 100644 --- a/src/components/modals/stacked-modal-provider/use-stacked-modal.ts +++ b/src/components/modals/stacked-modal-provider/use-stacked-modal.ts @@ -1,14 +1,13 @@ -import { useContext } from "react" -import { StackedModalContext } from "./stacked-modal-context" +import { useContext } from 'react'; + +import { StackedModalContext } from './stacked-modal-context'; export const useStackedModal = () => { - const context = useContext(StackedModalContext) + const context = useContext(StackedModalContext); if (!context) { - throw new Error( - "useStackedModal must be used within a StackedModalProvider" - ) + throw new Error('useStackedModal must be used within a StackedModalProvider'); } - return context -} + return context; +}; diff --git a/src/components/search/constants.ts b/src/components/search/constants.ts index 6bb9aab7..52b69235 100644 --- a/src/components/search/constants.ts +++ b/src/components/search/constants.ts @@ -1,30 +1,30 @@ export const SEARCH_AREAS = [ - "all", - "order", - "product", - "productVariant", - "collection", - "category", - "inventory", - "customer", - "customerGroup", - "promotion", - "campaign", - "priceList", - "user", - "region", - "taxRegion", - "returnReason", - "salesChannel", - "productType", - "productTag", - "location", - "shippingProfile", - "publishableApiKey", - "secretApiKey", - "command", - "navigation", -] as const + 'all', + 'order', + 'product', + 'productVariant', + 'collection', + 'category', + 'inventory', + 'customer', + 'customerGroup', + 'promotion', + 'campaign', + 'priceList', + 'user', + 'region', + 'taxRegion', + 'returnReason', + 'salesChannel', + 'productType', + 'productTag', + 'location', + 'shippingProfile', + 'publishableApiKey', + 'secretApiKey', + 'command', + 'navigation' +] as const; -export const DEFAULT_SEARCH_LIMIT = 3 -export const SEARCH_LIMIT_INCREMENT = 20 +export const DEFAULT_SEARCH_LIMIT = 3; +export const SEARCH_LIMIT_INCREMENT = 20; diff --git a/src/components/search/index.ts b/src/components/search/index.ts index c368ec91..161bbbb6 100644 --- a/src/components/search/index.ts +++ b/src/components/search/index.ts @@ -1 +1 @@ -export { Search } from "./search" +export { Search } from './search'; diff --git a/src/components/search/search.tsx b/src/components/search/search.tsx index 734f0095..15009402 100644 --- a/src/components/search/search.tsx +++ b/src/components/search/search.tsx @@ -1,18 +1,5 @@ -import { - Badge, - Button, - clx, - DropdownMenu, - IconButton, - Kbd, - Text, -} from "@medusajs/ui" -import { Command } from "cmdk" -import { Dialog as RadixDialog } from "radix-ui" import { Children, - ComponentPropsWithoutRef, - ElementRef, forwardRef, Fragment, useCallback, @@ -21,136 +8,137 @@ import { useMemo, useRef, useState, -} from "react" -import { useTranslation } from "react-i18next" -import { useLocation, useNavigate } from "react-router-dom" - -import { - ArrowUturnLeft, - MagnifyingGlass, - Plus, - Spinner, - TriangleDownMini, -} from "@medusajs/icons" -import { matchSorter } from "match-sorter" - -import { useSearch } from "../../providers/search-provider" -import { Skeleton } from "../common/skeleton" -import { Thumbnail } from "../common/thumbnail" -import { - DEFAULT_SEARCH_LIMIT, - SEARCH_AREAS, - SEARCH_LIMIT_INCREMENT, -} from "./constants" -import { SearchArea } from "./types" -import { useSearchResults } from "./use-search-results" -import { useDocumentDirection } from "../../hooks/use-document-direction" + type ComponentPropsWithoutRef, + type ElementRef +} from 'react'; + +import { Skeleton } from '@components/common/skeleton'; +import { Thumbnail } from '@components/common/thumbnail'; +import { useDocumentDirection } from '@hooks/use-document-direction'; +import { ArrowUturnLeft, MagnifyingGlass, Plus, Spinner, TriangleDownMini } from '@medusajs/icons'; +import { Badge, Button, clx, DropdownMenu, IconButton, Kbd, Text } from '@medusajs/ui'; +import { Command } from 'cmdk'; +import { matchSorter } from 'match-sorter'; +import { Dialog as RadixDialog } from 'radix-ui'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { useSearch } from '@/providers/search-provider'; + +import { DEFAULT_SEARCH_LIMIT, SEARCH_AREAS, SEARCH_LIMIT_INCREMENT } from './constants'; +import type { SearchArea } from './types'; +import { useSearchResults } from './use-search-results'; export const Search = () => { - const [area, setArea] = useState("all") - const [search, setSearch] = useState("") - const [limit, setLimit] = useState(DEFAULT_SEARCH_LIMIT) - const { open, onOpenChange } = useSearch() - const location = useLocation() - const { t } = useTranslation() - const navigate = useNavigate() + const [area, setArea] = useState('all'); + const [search, setSearch] = useState(''); + const [limit, setLimit] = useState(DEFAULT_SEARCH_LIMIT); + const { open, onOpenChange } = useSearch(); + const location = useLocation(); + const { t } = useTranslation(); + const navigate = useNavigate(); - - const inputRef = useRef(null) - const listRef = useRef(null) + const inputRef = useRef(null); + const listRef = useRef(null); const { staticResults, dynamicResults, isFetching } = useSearchResults({ area, limit, - q: search, - }) + q: search + }); const handleReset = useCallback(() => { - setArea("all") - setSearch("") - setLimit(DEFAULT_SEARCH_LIMIT) - }, [setLimit]) + setArea('all'); + setSearch(''); + setLimit(DEFAULT_SEARCH_LIMIT); + }, [setLimit]); const handleBack = () => { - handleReset() - inputRef.current?.focus() - } + handleReset(); + inputRef.current?.focus(); + }; const handleOpenChange = useCallback( (open: boolean) => { if (!open) { - handleReset() + handleReset(); } - onOpenChange(open) + onOpenChange(open); }, [onOpenChange, handleReset] - ) + ); useEffect(() => { - handleOpenChange(false) - }, [location.pathname, handleOpenChange]) + handleOpenChange(false); + }, [location.pathname, handleOpenChange]); const handleSelect = (item: { to?: string; callback?: () => void }) => { - handleOpenChange(false) + handleOpenChange(false); if (item.to) { - navigate(item.to) - return + navigate(item.to); + + return; } if (item.callback) { - item.callback() - return + item.callback(); + + return; } - } + }; const handleShowMore = (area: SearchArea) => { - if (area === "all") { - setLimit(DEFAULT_SEARCH_LIMIT) + if (area === 'all') { + setLimit(DEFAULT_SEARCH_LIMIT); } else { - setLimit(SEARCH_LIMIT_INCREMENT) + setLimit(SEARCH_LIMIT_INCREMENT); } - setArea(area) - inputRef.current?.focus() - } + setArea(area); + inputRef.current?.focus(); + }; const handleLoadMore = () => { - setLimit((l) => l + SEARCH_LIMIT_INCREMENT) - } + setLimit(l => l + SEARCH_LIMIT_INCREMENT); + }; const filteredStaticResults = useMemo(() => { - const filteredResults: typeof staticResults = [] + const filteredResults: typeof staticResults = []; - staticResults.forEach((group) => { + staticResults.forEach(group => { const filteredItems = matchSorter(group.items, search, { - keys: ["label"], - }) + keys: ['label'] + }); if (filteredItems.length === 0) { - return + return; } filteredResults.push({ ...group, - items: filteredItems, - }) - }) + items: filteredItems + }); + }); - return filteredResults - }, [staticResults, search]) + return filteredResults; + }, [staticResults, search]); const handleSearch = (q: string) => { - setSearch(q) - listRef.current?.scrollTo({ top: 0 }) - } + setSearch(q); + listRef.current?.scrollTo({ top: 0 }); + }; const showLoading = useMemo(() => { - return isFetching && !dynamicResults.length && !filteredStaticResults.length - }, [isFetching, dynamicResults, filteredStaticResults]) + return isFetching && !dynamicResults.length && !filteredStaticResults.length; + }, [isFetching, dynamicResults, filteredStaticResults]); return ( - + { setArea={setArea} value={search} onValueChange={handleSearch} - onBack={area !== "all" ? handleBack : undefined} - placeholder={t("app.search.placeholder")} + onBack={area !== 'all' ? handleBack : undefined} + placeholder={t('app.search.placeholder')} data-testid="search-input" /> - + {showLoading && } - {dynamicResults.map((group) => { + {dynamicResults.map(group => { return ( - - {group.items.map((item) => { + + {group.items.map(item => { return ( { /> )} {item.title} - {item.subtitle && ( - - {item.subtitle} - - )} + {item.subtitle && {item.subtitle}}
    - ) + ); })} - {group.hasMore && area === "all" && ( + {group.hasMore && area === 'all' && ( handleShowMore(group.area)} hidden={true} value={`${group.title}:show:more`} // Prevent the "Show more" buttons across groups from sharing the same value/state data-testid={`search-show-more-${group.area}`} > -
    +
    - - {t("app.search.showMore")} + + {t('app.search.showMore')}
    @@ -216,36 +211,37 @@ export const Search = () => { value={`${group.title}:load:more`} data-testid={`search-load-more-${group.area}`} > -
    +
    - - {t("app.search.loadMore", { - count: Math.min( - SEARCH_LIMIT_INCREMENT, - group.count - limit - ), + + {t('app.search.loadMore', { + count: Math.min(SEARCH_LIMIT_INCREMENT, group.count - limit) })}
    )} - ) + ); })} - {filteredStaticResults.map((group) => { + {filteredStaticResults.map(group => { return ( - {group.items.map((item) => { + {group.items.map(item => { return ( handleSelect(item)} className="flex items-center justify-between" - data-testid={`search-shortcut-${item.label.toLowerCase().replace(/\s+/g, "-")}`} + data-testid={`search-shortcut-${item.label.toLowerCase().replace(/\s+/g, '-')}`} > {item.label}
    @@ -258,24 +254,29 @@ export const Search = () => { {key} {index < (item.keys.Mac?.length || 0) - 1 && ( - {t("app.keyboardShortcuts.then")} + {t('app.keyboardShortcuts.then')} )}
    - ) + ); })}
    - ) + ); })} - ) + ); })} - {!showLoading && } + {!showLoading && ( + + )} - ) -} + ); +}; const CommandPalette = forwardRef< ElementRef, @@ -285,62 +286,81 @@ const CommandPalette = forwardRef< shouldFilter={false} ref={ref} className={clx( - "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", + 'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md', className )} {...props} /> -)) -CommandPalette.displayName = Command.displayName +)); +CommandPalette.displayName = Command.displayName; interface CommandDialogProps extends RadixDialog.DialogProps { - isLoading?: boolean + isLoading?: boolean; } const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - const { t } = useTranslation() + const { t } = useTranslation(); const preserveHeight = useMemo(() => { - return props.isLoading && Children.count(children) === 0 - }, [props.isLoading, children]) + return props.isLoading && Children.count(children) === 0; + }, [props.isLoading, children]); return ( - + - - {t("app.search.title")} - + {t('app.search.title')} - {t("app.search.description")} + {t('app.search.description')} - + {children} -
    +
    -
    - - {t("app.search.navigation")} +
    + + {t('app.search.navigation')}
    -
    -
    - - {t("app.search.openResult")} +
    +
    + + {t('app.search.openResult')}
    @@ -349,129 +369,136 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - ) -} + ); +}; const CommandInput = forwardRef< ElementRef, ComponentPropsWithoutRef & { - area: SearchArea - setArea: (area: SearchArea) => void - isFetching: boolean - onBack?: () => void + area: SearchArea; + setArea: (area: SearchArea) => void; + isFetching: boolean; + onBack?: () => void; } ->( - ( - { - className, - value, - onValueChange, - area, - setArea, - isFetching, - onBack, - ...props - }, - ref - ) => { - const { t } = useTranslation() - const innerRef = useRef(null) - const direction = useDocumentDirection() - useImperativeHandle( - ref, - () => innerRef.current - ) - - return ( -
    -
    - - - - {t(`app.search.groups.${area}`)} - - - - { - e.preventDefault() - innerRef.current?.focus() - }} - data-testid="search-area-content" +>(({ className, value, onValueChange, area, setArea, isFetching, onBack, ...props }, ref) => { + const { t } = useTranslation(); + const innerRef = useRef(null); + const direction = useDocumentDirection(); + useImperativeHandle( + ref, + () => innerRef.current + ); + + return ( +
    +
    + + + - setArea(v as SearchArea)} - data-testid="search-area-radio-group" - > - {SEARCH_AREAS.map((area) => ( - - {area === "command" && } - - {t(`app.search.groups.${area}`)} - - {area === "all" && } - - ))} - - - -
    -
    - {onBack && ( - + + + { + e.preventDefault(); + innerRef.current?.focus(); + }} + data-testid="search-area-content" + > + setArea(v as SearchArea)} + data-testid="search-area-radio-group" + > + {SEARCH_AREAS.map(area => ( + + {area === 'command' && } + + {t(`app.search.groups.${area}`)} + + {area === 'all' && } + + ))} + + + +
    +
    + {onBack && ( + + + + )} + +
    + {isFetching && ( + + )} + {value && ( + )} - -
    - {isFetching && ( - - )} - {value && ( - - )} -
    - ) - } -) +
    + ); +}); -CommandInput.displayName = Command.Input.displayName +CommandInput.displayName = Command.Input.displayName; const CommandList = forwardRef< ElementRef, @@ -479,46 +506,61 @@ const CommandList = forwardRef< >(({ className, ...props }, ref) => ( -)) +)); -CommandList.displayName = Command.List.displayName +CommandList.displayName = Command.List.displayName; const CommandEmpty = forwardRef< ElementRef, - Omit, "children"> & { - q?: string + Omit, 'children'> & { + q?: string; } >((props, ref) => { - const { t } = useTranslation() + const { t } = useTranslation(); return ( - -
    - -
    - - {props.q - ? t("app.search.noResultsTitle") - : t("app.search.emptySearchTitle")} + +
    + +
    + + {props.q ? t('app.search.noResultsTitle') : t('app.search.emptySearchTitle')} - - {props.q - ? t("app.search.noResultsMessage") - : t("app.search.emptySearchMessage")} + + {props.q ? t('app.search.noResultsMessage') : t('app.search.emptySearchMessage')}
    - ) -}) + ); +}); -CommandEmpty.displayName = Command.Empty.displayName +CommandEmpty.displayName = Command.Empty.displayName; const CommandLoading = forwardRef< ElementRef, @@ -528,20 +570,27 @@ const CommandLoading = forwardRef< -
    +
    {Array.from({ length: 7 }).map((_, index) => ( -
    +
    ))} - ) -}) -CommandLoading.displayName = Command.Loading.displayName + ); +}); +CommandLoading.displayName = Command.Loading.displayName; const CommandGroup = forwardRef< ElementRef, @@ -550,14 +599,14 @@ const CommandGroup = forwardRef< -)) +)); -CommandGroup.displayName = Command.Group.displayName +CommandGroup.displayName = Command.Group.displayName; const CommandSeparator = forwardRef< ElementRef, @@ -565,11 +614,11 @@ const CommandSeparator = forwardRef< >(({ className, ...props }, ref) => ( -)) -CommandSeparator.displayName = Command.Separator.displayName +)); +CommandSeparator.displayName = Command.Separator.displayName; const CommandItem = forwardRef< ElementRef, @@ -578,11 +627,11 @@ const CommandItem = forwardRef< svg]:text-ui-fg-subtle relative flex cursor-pointer select-none items-center gap-x-3 rounded-md p-2 outline-none data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50", + 'txt-compact-small relative flex cursor-pointer select-none items-center gap-x-3 rounded-md p-2 outline-none focus-visible:bg-ui-bg-base-hover aria-selected:bg-ui-bg-base-hover data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 [&>svg]:text-ui-fg-subtle', className )} {...props} /> -)) +)); -CommandItem.displayName = Command.Item.displayName +CommandItem.displayName = Command.Item.displayName; diff --git a/src/components/search/types.ts b/src/components/search/types.ts index e73a1360..d32e42fc 100644 --- a/src/components/search/types.ts +++ b/src/components/search/types.ts @@ -1,20 +1,20 @@ -import { SEARCH_AREAS } from "./constants" +import type { SEARCH_AREAS } from './constants'; -export type SearchArea = (typeof SEARCH_AREAS)[number] +export type SearchArea = (typeof SEARCH_AREAS)[number]; export type DynamicSearchResultItem = { - id: string - title: string - subtitle?: string - to: string - thumbnail?: string - value: string -} + id: string; + title: string; + subtitle?: string; + to: string; + thumbnail?: string; + value: string; +}; export type DynamicSearchResult = { - area: SearchArea - title: string - hasMore: boolean - count: number - items: DynamicSearchResultItem[] -} + area: SearchArea; + title: string; + hasMore: boolean; + count: number; + items: DynamicSearchResultItem[]; +}; diff --git a/src/components/search/use-search-results.tsx b/src/components/search/use-search-results.tsx index 41149d9b..9741324c 100644 --- a/src/components/search/use-search-results.tsx +++ b/src/components/search/use-search-results.tsx @@ -1,8 +1,5 @@ -import { HttpTypes } from "@medusajs/types" -import { keepPreviousData } from "@tanstack/react-query" -import { TFunction } from "i18next" -import { useCallback, useEffect, useMemo, useState } from "react" -import { useTranslation } from "react-i18next" +import { useCallback, useEffect, useMemo, useState } from 'react'; + import { useApiKeys, useCampaigns, @@ -23,350 +20,343 @@ import { useStockLocations, useTaxRegions, useUsers, - useVariants, -} from "../../hooks/api" -import { useReturnReasons } from "../../hooks/api/return-reasons" -import { Shortcut, ShortcutType } from "../../providers/keybind-provider" -import { useGlobalShortcuts } from "../../providers/keybind-provider/hooks" -import { DynamicSearchResult, SearchArea } from "./types" + useVariants +} from '@hooks/api'; +import { useReturnReasons } from '@hooks/api/return-reasons'; +import type { HttpTypes } from '@medusajs/types'; +import type { Shortcut, ShortcutType } from '@providers/keybind-provider'; +import { useGlobalShortcuts } from '@providers/keybind-provider/hooks'; +import { keepPreviousData } from '@tanstack/react-query'; +import type { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; + +import type { DynamicSearchResult, SearchArea } from './types'; type UseSearchProps = { - q?: string - limit: number - area?: SearchArea -} + q?: string; + limit: number; + area?: SearchArea; +}; -export const useSearchResults = ({ - q, - limit, - area = "all", -}: UseSearchProps) => { - const staticResults = useStaticSearchResults(area) - const { dynamicResults, isFetching } = useDynamicSearchResults(area, limit, q) +export const useSearchResults = ({ q, limit, area = 'all' }: UseSearchProps) => { + const staticResults = useStaticSearchResults(area); + const { dynamicResults, isFetching } = useDynamicSearchResults(area, limit, q); return { staticResults, dynamicResults, - isFetching, - } -} + isFetching + }; +}; const useStaticSearchResults = (currentArea: SearchArea) => { - const globalCommands = useGlobalShortcuts() + const globalCommands = useGlobalShortcuts(); - const results = useMemo(() => { - const groups = new Map() + return useMemo(() => { + const groups = new Map(); - globalCommands.forEach((command) => { - const group = groups.get(command.type) || [] - group.push(command) - groups.set(command.type, group) - }) + globalCommands.forEach(command => { + const group = groups.get(command.type) || []; + group.push(command); + groups.set(command.type, group); + }); - let filteredGroups: [ShortcutType, Shortcut[]][] + let filteredGroups: [ShortcutType, Shortcut[]][]; switch (currentArea) { - case "all": - filteredGroups = Array.from(groups) - break - case "navigation": - filteredGroups = Array.from(groups).filter( - ([type]) => type === "pageShortcut" || type === "settingShortcut" - ) - break - case "command": + case 'all': + filteredGroups = Array.from(groups); + break; + case 'navigation': filteredGroups = Array.from(groups).filter( - ([type]) => type === "commandShortcut" - ) - break + ([type]) => type === 'pageShortcut' || type === 'settingShortcut' + ); + break; + case 'command': + filteredGroups = Array.from(groups).filter(([type]) => type === 'commandShortcut'); + break; default: - filteredGroups = [] + filteredGroups = []; } return filteredGroups.map(([title, items]) => ({ title, - items, - })) - }, [globalCommands, currentArea]) - - return results -} + items + })); + }, [globalCommands, currentArea]); +}; -const useDynamicSearchResults = ( - currentArea: SearchArea, - limit: number, - q?: string -) => { - const { t } = useTranslation() +const useDynamicSearchResults = (currentArea: SearchArea, limit: number, q?: string) => { + const { t } = useTranslation(); - const debouncedSearch = useDebouncedSearch(q, 300) + const debouncedSearch = useDebouncedSearch(q, 300); const orderResponse = useOrders( { - q: debouncedSearch?.replace(/^#/, ""), // Since we display the ID with a # prefix, it's natural for the user to include it in the search. This will however cause no results to be returned, so we remove the # prefix from the search query. + q: debouncedSearch?.replace(/^#/, ''), // Since we display the ID with a # prefix, it's natural for the user to include it in the search. This will however cause no results to be returned, so we remove the # prefix from the search query. limit, - fields: "id,display_id,email", + fields: 'id,display_id,email' }, { - enabled: isAreaEnabled(currentArea, "order"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'order'), + placeholderData: keepPreviousData } - ) + ); const productResponse = useProducts( { q: debouncedSearch, limit, - fields: "id,title,thumbnail", + fields: 'id,title,thumbnail' }, { - enabled: isAreaEnabled(currentArea, "product"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'product'), + placeholderData: keepPreviousData } - ) + ); const productVariantResponse = useVariants( { q: debouncedSearch, limit, - fields: "id,title,sku,product_id", + fields: 'id,title,sku,product_id' }, { - enabled: isAreaEnabled(currentArea, "productVariant"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'productVariant'), + placeholderData: keepPreviousData } - ) + ); const categoryResponse = useProductCategories( { // TODO: Remove the OR condition once the list endpoint does not throw when q equals an empty string q: debouncedSearch || undefined, limit, - fields: "id,name", + fields: 'id,name' }, { - enabled: isAreaEnabled(currentArea, "category"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'category'), + placeholderData: keepPreviousData } - ) + ); const collectionResponse = useCollections( { q: debouncedSearch, limit, - fields: "id,title", + fields: 'id,title' }, { - enabled: isAreaEnabled(currentArea, "collection"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'collection'), + placeholderData: keepPreviousData } - ) + ); const customerResponse = useCustomers( { q: debouncedSearch, limit, - fields: "id,email,first_name,last_name", + fields: 'id,email,first_name,last_name' }, { - enabled: isAreaEnabled(currentArea, "customer"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'customer'), + placeholderData: keepPreviousData } - ) + ); const customerGroupResponse = useCustomerGroups( { q: debouncedSearch, limit, - fields: "id,name", + fields: 'id,name' }, { - enabled: isAreaEnabled(currentArea, "customerGroup"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'customerGroup'), + placeholderData: keepPreviousData } - ) + ); const inventoryResponse = useInventoryItems( { q: debouncedSearch, limit, - fields: "id,title,sku", + fields: 'id,title,sku' }, { - enabled: isAreaEnabled(currentArea, "inventory"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'inventory'), + placeholderData: keepPreviousData } - ) + ); const promotionResponse = usePromotions( { q: debouncedSearch, limit, - fields: "id,code,status", + fields: 'id,code,status' }, { - enabled: isAreaEnabled(currentArea, "promotion"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'promotion'), + placeholderData: keepPreviousData } - ) + ); const campaignResponse = useCampaigns( { q: debouncedSearch, limit, - fields: "id,name", + fields: 'id,name' }, { - enabled: isAreaEnabled(currentArea, "campaign"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'campaign'), + placeholderData: keepPreviousData } - ) + ); const priceListResponse = usePriceLists( { q: debouncedSearch, limit, - fields: "id,title", + fields: 'id,title' }, { - enabled: isAreaEnabled(currentArea, "priceList"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'priceList'), + placeholderData: keepPreviousData } - ) + ); const userResponse = useUsers( { q: debouncedSearch, limit, - fields: "id,email,first_name,last_name", + fields: 'id,email,first_name,last_name' }, { - enabled: isAreaEnabled(currentArea, "user"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'user'), + placeholderData: keepPreviousData } - ) + ); const regionResponse = useRegions( { q: debouncedSearch, limit, - fields: "id,name", + fields: 'id,name' }, { - enabled: isAreaEnabled(currentArea, "region"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'region'), + placeholderData: keepPreviousData } - ) + ); const taxRegionResponse = useTaxRegions( { q: debouncedSearch, limit, - fields: "id,country_code,province_code", + fields: 'id,country_code,province_code' }, { - enabled: isAreaEnabled(currentArea, "taxRegion"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'taxRegion'), + placeholderData: keepPreviousData } - ) + ); const returnReasonResponse = useReturnReasons( { q: debouncedSearch, limit, - fields: "id,label,value", + fields: 'id,label,value' }, { - enabled: isAreaEnabled(currentArea, "returnReason"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'returnReason'), + placeholderData: keepPreviousData } - ) + ); const salesChannelResponse = useSalesChannels( { q: debouncedSearch, limit, - fields: "id,name", + fields: 'id,name' }, { - enabled: isAreaEnabled(currentArea, "salesChannel"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'salesChannel'), + placeholderData: keepPreviousData } - ) + ); const productTypeResponse = useProductTypes( { q: debouncedSearch, limit, - fields: "id,value", + fields: 'id,value' }, { - enabled: isAreaEnabled(currentArea, "productType"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'productType'), + placeholderData: keepPreviousData } - ) + ); const productTagResponse = useProductTags( { q: debouncedSearch, limit, - fields: "id,value", + fields: 'id,value' }, { - enabled: isAreaEnabled(currentArea, "productTag"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'productTag'), + placeholderData: keepPreviousData } - ) + ); const locationResponse = useStockLocations( { q: debouncedSearch, limit, - fields: "id,name", + fields: 'id,name' }, { - enabled: isAreaEnabled(currentArea, "location"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'location'), + placeholderData: keepPreviousData } - ) + ); const shippingProfileResponse = useShippingProfiles( { q: debouncedSearch, limit, - fields: "id,name", + fields: 'id,name' }, { - enabled: isAreaEnabled(currentArea, "shippingProfile"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'shippingProfile'), + placeholderData: keepPreviousData } - ) + ); const publishableApiKeyResponse = useApiKeys( { q: debouncedSearch, limit, - fields: "id,title,redacted", - type: "publishable", + fields: 'id,title,redacted', + type: 'publishable' }, { - enabled: isAreaEnabled(currentArea, "publishableApiKey"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'publishableApiKey'), + placeholderData: keepPreviousData } - ) + ); const secretApiKeyResponse = useApiKeys( { q: debouncedSearch, limit, - fields: "id,title,redacted", - type: "secret", + fields: 'id,title,redacted', + type: 'secret' }, { - enabled: isAreaEnabled(currentArea, "secretApiKey"), - placeholderData: keepPreviousData, + enabled: isAreaEnabled(currentArea, 'secretApiKey'), + placeholderData: keepPreviousData } - ) + ); const responseMap = useMemo( () => ({ @@ -391,7 +381,7 @@ const useDynamicSearchResults = ( location: locationResponse, shippingProfile: shippingProfileResponse, publishableApiKey: publishableApiKeyResponse, - secretApiKey: secretApiKeyResponse, + secretApiKey: secretApiKeyResponse }), [ orderResponse, @@ -415,313 +405,307 @@ const useDynamicSearchResults = ( locationResponse, shippingProfileResponse, publishableApiKeyResponse, - secretApiKeyResponse, + secretApiKeyResponse ] - ) + ); const results = useMemo(() => { - const groups = Object.entries(responseMap) + // Remove null values + + return Object.entries(responseMap) .map(([key, response]) => { - const area = key as SearchArea - if (isAreaEnabled(currentArea, area) || currentArea === "all") { - return transformDynamicSearchResults(area, limit, t, response) + const area = key as SearchArea; + if (isAreaEnabled(currentArea, area) || currentArea === 'all') { + return transformDynamicSearchResults(area, limit, t, response); } - return null - }) - .filter(Boolean) // Remove null values - return groups - }, [responseMap, currentArea, limit, t]) + return null; + }) + .filter(Boolean); + }, [responseMap, currentArea, limit, t]); const isAreaFetching = useCallback( (area: SearchArea): boolean => { - if (area === "all") { - return Object.values(responseMap).some( - (response) => response.isFetching - ) + if (area === 'all') { + return Object.values(responseMap).some(response => response.isFetching); } return ( isAreaEnabled(currentArea, area) && responseMap[area as keyof typeof responseMap]?.isFetching - ) + ); }, [currentArea, responseMap] - ) + ); const isFetching = useMemo(() => { - return isAreaFetching(currentArea) - }, [currentArea, isAreaFetching]) + return isAreaFetching(currentArea); + }, [currentArea, isAreaFetching]); const dynamicResults = q - ? (results.filter( - (group) => !!group && group.items.length > 0 - ) as DynamicSearchResult[]) - : [] + ? (results.filter(group => !!group && group.items.length > 0) as DynamicSearchResult[]) + : []; return { dynamicResults, - isFetching, - } -} + isFetching + }; +}; const useDebouncedSearch = (value: string | undefined, delay: number) => { - const [debouncedValue, setDebouncedValue] = useState(value) + const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { - setDebouncedValue(value) - }, delay) + setDebouncedValue(value); + }, delay); return () => { - clearTimeout(handler) - } - }, [value, delay]) + clearTimeout(handler); + }; + }, [value, delay]); - return debouncedValue -} + return debouncedValue; +}; function isAreaEnabled(area: SearchArea, currentArea: SearchArea) { - if (area === "all") { - return true - } - if (area === currentArea) { - return true + if (area === 'all') { + return true; } - return false + + return area === currentArea; } type TransformMap = { [K in SearchArea]?: { - dataKey: string + dataKey: string; + // @todo fix any type + // eslint-disable-next-line @typescript-eslint/no-explicit-any transform: (item: any) => { - id: string - title: string - subtitle?: string - to: string - value: string - thumbnail?: string - } - } -} + id: string; + title: string; + subtitle?: string; + to: string; + value: string; + thumbnail?: string; + }; + }; +}; const transformMap: TransformMap = { order: { - dataKey: "orders", + dataKey: 'orders', transform: (order: HttpTypes.AdminOrder) => ({ id: order.id, title: `#${order.display_id}`, subtitle: order.email ?? undefined, to: `/orders/${order.id}`, - value: `order:${order.id}`, - }), + value: `order:${order.id}` + }) }, product: { - dataKey: "products", + dataKey: 'products', transform: (product: HttpTypes.AdminProduct) => ({ id: product.id, title: product.title, to: `/products/${product.id}`, thumbnail: product.thumbnail ?? undefined, - value: `product:${product.id}`, - }), + value: `product:${product.id}` + }) }, productVariant: { - dataKey: "variants", + dataKey: 'variants', transform: (variant: HttpTypes.AdminProductVariant) => ({ id: variant.id, title: variant.title!, subtitle: variant.sku ?? undefined, to: `/products/${variant.product_id}/variants/${variant.id}`, - value: `variant:${variant.id}`, - }), + value: `variant:${variant.id}` + }) }, category: { - dataKey: "product_categories", + dataKey: 'product_categories', transform: (category: HttpTypes.AdminProductCategory) => ({ id: category.id, title: category.name, to: `/categories/${category.id}`, - value: `category:${category.id}`, - }), + value: `category:${category.id}` + }) }, inventory: { - dataKey: "inventory_items", + dataKey: 'inventory_items', transform: (inventory: HttpTypes.AdminInventoryItem) => ({ id: inventory.id, - title: inventory.title ?? "", + title: inventory.title ?? '', subtitle: inventory.sku ?? undefined, to: `/inventory/${inventory.id}`, - value: `inventory:${inventory.id}`, - }), + value: `inventory:${inventory.id}` + }) }, customer: { - dataKey: "customers", + dataKey: 'customers', transform: (customer: HttpTypes.AdminCustomer) => { - const name = [customer.first_name, customer.last_name] - .filter(Boolean) - .join(" ") + const name = [customer.first_name, customer.last_name].filter(Boolean).join(' '); + return { id: customer.id, title: name || customer.email, subtitle: name ? customer.email : undefined, to: `/customers/${customer.id}`, - value: `customer:${customer.id}`, - } - }, + value: `customer:${customer.id}` + }; + } }, customerGroup: { - dataKey: "customer_groups", + dataKey: 'customer_groups', transform: (customerGroup: HttpTypes.AdminCustomerGroup) => ({ id: customerGroup.id, title: customerGroup.name!, to: `/customer-groups/${customerGroup.id}`, - value: `customerGroup:${customerGroup.id}`, - }), + value: `customerGroup:${customerGroup.id}` + }) }, collection: { - dataKey: "collections", + dataKey: 'collections', transform: (collection: HttpTypes.AdminCollection) => ({ id: collection.id, title: collection.title, to: `/collections/${collection.id}`, - value: `collection:${collection.id}`, - }), + value: `collection:${collection.id}` + }) }, promotion: { - dataKey: "promotions", + dataKey: 'promotions', transform: (promotion: HttpTypes.AdminPromotion) => ({ id: promotion.id, title: promotion.code!, to: `/promotions/${promotion.id}`, - value: `promotion:${promotion.id}`, - }), + value: `promotion:${promotion.id}` + }) }, campaign: { - dataKey: "campaigns", + dataKey: 'campaigns', transform: (campaign: HttpTypes.AdminCampaign) => ({ id: campaign.id, title: campaign.name, to: `/campaigns/${campaign.id}`, - value: `campaign:${campaign.id}`, - }), + value: `campaign:${campaign.id}` + }) }, priceList: { - dataKey: "price_lists", + dataKey: 'price_lists', transform: (priceList: HttpTypes.AdminPriceList) => ({ id: priceList.id, title: priceList.title, to: `/price-lists/${priceList.id}`, - value: `priceList:${priceList.id}`, - }), + value: `priceList:${priceList.id}` + }) }, user: { - dataKey: "users", + dataKey: 'users', transform: (user: HttpTypes.AdminUser) => ({ id: user.id, title: `${user.first_name} ${user.last_name}`, subtitle: user.email, to: `/users/${user.id}`, - value: `user:${user.id}`, - }), + value: `user:${user.id}` + }) }, region: { - dataKey: "regions", + dataKey: 'regions', transform: (region: HttpTypes.AdminRegion) => ({ id: region.id, title: region.name, to: `/regions/${region.id}`, - value: `region:${region.id}`, - }), + value: `region:${region.id}` + }) }, taxRegion: { - dataKey: "tax_regions", + dataKey: 'tax_regions', transform: (taxRegion: HttpTypes.AdminTaxRegion) => ({ id: taxRegion.id, - title: - taxRegion.province_code?.toUpperCase() ?? - taxRegion.country_code!.toUpperCase(), + title: taxRegion.province_code?.toUpperCase() ?? taxRegion.country_code!.toUpperCase(), subtitle: taxRegion.province_code ? taxRegion.country_code! : undefined, to: `/tax-regions/${taxRegion.id}`, - value: `taxRegion:${taxRegion.id}`, - }), + value: `taxRegion:${taxRegion.id}` + }) }, returnReason: { - dataKey: "return_reasons", + dataKey: 'return_reasons', transform: (returnReason: HttpTypes.AdminReturnReason) => ({ id: returnReason.id, title: returnReason.label, subtitle: returnReason.value, to: `/return-reasons/${returnReason.id}/edit`, - value: `returnReason:${returnReason.id}`, - }), + value: `returnReason:${returnReason.id}` + }) }, salesChannel: { - dataKey: "sales_channels", + dataKey: 'sales_channels', transform: (salesChannel: HttpTypes.AdminSalesChannel) => ({ id: salesChannel.id, title: salesChannel.name, to: `/sales-channels/${salesChannel.id}`, - value: `salesChannel:${salesChannel.id}`, - }), + value: `salesChannel:${salesChannel.id}` + }) }, productType: { - dataKey: "product_types", + dataKey: 'product_types', transform: (productType: HttpTypes.AdminProductType) => ({ id: productType.id, title: productType.value, to: `/product-types/${productType.id}`, - value: `productType:${productType.id}`, - }), + value: `productType:${productType.id}` + }) }, productTag: { - dataKey: "product_tags", + dataKey: 'product_tags', transform: (productTag: HttpTypes.AdminProductTag) => ({ id: productTag.id, title: productTag.value, to: `/product-tags/${productTag.id}`, - value: `productTag:${productTag.id}`, - }), + value: `productTag:${productTag.id}` + }) }, location: { - dataKey: "stock_locations", + dataKey: 'stock_locations', transform: (location: HttpTypes.AdminStockLocation) => ({ id: location.id, title: location.name, to: `/locations/${location.id}`, - value: `location:${location.id}`, - }), + value: `location:${location.id}` + }) }, shippingProfile: { - dataKey: "shipping_profiles", + dataKey: 'shipping_profiles', transform: (shippingProfile: HttpTypes.AdminShippingProfile) => ({ id: shippingProfile.id, title: shippingProfile.name, to: `/shipping-profiles/${shippingProfile.id}`, - value: `shippingProfile:${shippingProfile.id}`, - }), + value: `shippingProfile:${shippingProfile.id}` + }) }, publishableApiKey: { - dataKey: "api_keys", - transform: (apiKey: HttpTypes.AdminApiKeyResponse["api_key"]) => ({ + dataKey: 'api_keys', + transform: (apiKey: HttpTypes.AdminApiKeyResponse['api_key']) => ({ id: apiKey.id, title: apiKey.title, subtitle: apiKey.redacted, to: `/publishable-api-keys/${apiKey.id}`, - value: `publishableApiKey:${apiKey.id}`, - }), + value: `publishableApiKey:${apiKey.id}` + }) }, secretApiKey: { - dataKey: "api_keys", - transform: (apiKey: HttpTypes.AdminApiKeyResponse["api_key"]) => ({ + dataKey: 'api_keys', + transform: (apiKey: HttpTypes.AdminApiKeyResponse['api_key']) => ({ id: apiKey.id, title: apiKey.title, subtitle: apiKey.redacted, to: `/secret-api-keys/${apiKey.id}`, - value: `secretApiKey:${apiKey.id}`, - }), - }, -} + value: `secretApiKey:${apiKey.id}` + }) + } +}; function transformDynamicSearchResults( type: SearchArea, @@ -730,14 +714,14 @@ function transformDynamicSearchResults( response?: T ): DynamicSearchResult | undefined { if (!response || !transformMap[type]) { - return undefined + return undefined; } - const { dataKey, transform } = transformMap[type]! - const data = response[dataKey as keyof T] + const { dataKey, transform } = transformMap[type]!; + const data = response[dataKey as keyof T]; if (!data || !Array.isArray(data)) { - return undefined + return undefined; } return { @@ -745,6 +729,6 @@ function transformDynamicSearchResults( area: type, hasMore: response.count > limit, count: response.count, - items: data.map(transform), - } + items: data.map(transform) + }; } diff --git a/src/components/table/configurable-data-table/configurable-data-table.tsx b/src/components/table/configurable-data-table/configurable-data-table.tsx index 844bd02d..64be6187 100644 --- a/src/components/table/configurable-data-table/configurable-data-table.tsx +++ b/src/components/table/configurable-data-table/configurable-data-table.tsx @@ -1,35 +1,35 @@ -import { useState } from "react" -import { Container, Button } from "@medusajs/ui" -import { useTranslation } from "react-i18next" -import { DataTable } from "../../data-table" -import { SaveViewDialog } from "../save-view-dialog" -import { SaveViewDropdown } from "./save-view-dropdown" -import { useTableConfiguration } from "../../../hooks/table/use-table-configuration" -import { useConfigurableTableColumns } from "../../../hooks/table/columns/use-configurable-table-columns" -import { getEntityAdapter } from "../../../lib/table/entity-adapters" -import { TableAdapter } from "../../../lib/table/table-adapters" +import { useState } from 'react'; + +import { DataTable } from '@components/data-table'; +import { SaveViewDialog } from '@components/table/save-view-dialog'; +import { SaveViewDropdown } from '@components/table/save-view-dropdown'; +import { useConfigurableTableColumns } from '@hooks/table/columns/use-configurable-table-columns'; +import { useTableConfiguration } from '@hooks/table/use-table-configuration'; +import { getEntityAdapter } from '@lib/table/entity-adapters'; +import type { TableAdapter } from '@lib/table/table-adapters'; +import { Button, Container } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; type DataTableActionProps = { - label: string - disabled?: boolean + label: string; + disabled?: boolean; } & ( - | { - to: string + | { + to: string; } - | { - onClick: () => void + | { + onClick: () => void; } - ) - +); export interface ConfigurableDataTableProps { - adapter: TableAdapter - heading?: string - subHeading?: string - pageSize?: number - queryPrefix?: string - layout?: "fill" | "auto" - actions?: DataTableActionProps[] + adapter: TableAdapter; + heading?: string; + subHeading?: string; + pageSize?: number; + queryPrefix?: string; + layout?: 'fill' | 'auto'; + actions?: DataTableActionProps[]; } export function ConfigurableDataTable({ @@ -38,18 +38,20 @@ export function ConfigurableDataTable({ subHeading, pageSize: pageSizeProp, queryPrefix: queryPrefixProp, - layout = "fill", - actions, + layout = 'fill', + actions }: ConfigurableDataTableProps) { - const { t } = useTranslation() - const [saveDialogOpen, setSaveDialogOpen] = useState(false) - const [editingView, setEditingView] = useState(null) - - const entity = adapter.entity - const entityName = adapter.entityName - const filters = adapter.filters || [] - const pageSize = pageSizeProp || adapter.pageSize || 20 - const queryPrefix = queryPrefixProp || adapter.queryPrefix || "" + const { t } = useTranslation(); + const [saveDialogOpen, setSaveDialogOpen] = useState(false); + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [editingView, setEditingView] = useState(null); + + const entity = adapter.entity; + const entityName = adapter.entityName; + const filters = adapter.filters || []; + const pageSize = pageSizeProp || adapter.pageSize || 20; + const queryPrefix = queryPrefixProp || adapter.queryPrefix || ''; const { activeView, @@ -67,43 +69,42 @@ export function ConfigurableDataTable({ isLoadingColumns, apiColumns, requiredFields, - queryParams, + queryParams } = useTableConfiguration({ entity, pageSize, queryPrefix, - filters, - }) + filters + }); - const parsedQueryParams = { ...queryParams } + const parsedQueryParams = { ...queryParams }; filters.forEach(filter => { - const filterKey = filter.id + const filterKey = filter.id; if (parsedQueryParams[filterKey] !== undefined) { try { - parsedQueryParams[filterKey] = JSON.parse(parsedQueryParams[filterKey]) + parsedQueryParams[filterKey] = JSON.parse(parsedQueryParams[filterKey]); } catch { // If parsing fails, keep the original value } } - }) + }); const searchParams = { ...parsedQueryParams, fields: requiredFields, limit: pageSize, - offset: parsedQueryParams.offset ? Number(parsedQueryParams.offset) : 0, - } + offset: parsedQueryParams.offset ? Number(parsedQueryParams.offset) : 0 + }; - const fetchResult = adapter.useData(requiredFields, searchParams) + const fetchResult = adapter.useData(requiredFields, searchParams); - const columnAdapter = adapter.columnAdapter || getEntityAdapter(entity) - const generatedColumns = useConfigurableTableColumns(entity, apiColumns || [], columnAdapter) - const columns = (adapter.getColumns && apiColumns) - ? adapter.getColumns(apiColumns) - : generatedColumns + const columnAdapter = adapter.columnAdapter || getEntityAdapter(entity); + const generatedColumns = useConfigurableTableColumns(entity, apiColumns || [], columnAdapter); + const columns = + adapter.getColumns && apiColumns ? adapter.getColumns(apiColumns) : generatedColumns; if (fetchResult.isError) { - throw fetchResult.error + throw fetchResult.error; } const handleSaveAsDefault = async () => { @@ -116,12 +117,12 @@ export function ConfigurableDataTable({ column_order: currentColumns.order, filters: currentConfiguration.filters || {}, sorting: currentConfiguration.sorting || null, - search: currentConfiguration.search || "", + search: currentConfiguration.search || '' } - }) + }); } else { await createView.mutateAsync({ - name: "Default", + name: 'Default', is_system_default: true, set_active: true, configuration: { @@ -129,17 +130,17 @@ export function ConfigurableDataTable({ column_order: currentColumns.order, filters: currentConfiguration.filters || {}, sorting: currentConfiguration.sorting || null, - search: currentConfiguration.search || "", + search: currentConfiguration.search || '' } - }) + }); } } catch (_) { // Error is handled by the hook } - } + }; const handleUpdateExisting = async () => { - if (!activeView) return + if (!activeView) return; try { await updateView.mutateAsync({ @@ -149,18 +150,18 @@ export function ConfigurableDataTable({ column_order: currentColumns.order, filters: currentConfiguration.filters || {}, sorting: currentConfiguration.sorting || null, - search: currentConfiguration.search || "", + search: currentConfiguration.search || '' } - }) + }); } catch (_) { // Error is handled by the hook } - } + }; const handleSaveAsNew = () => { - setSaveDialogOpen(true) - setEditingView(null) - } + setSaveDialogOpen(true); + setEditingView(null); + }; // Filter bar content with save controls const filterBarContent = hasConfigurationChanged ? ( @@ -171,7 +172,7 @@ export function ConfigurableDataTable({ type="button" onClick={handleClearConfiguration} > - {t("actions.clear")} + {t('actions.clear')} ({ onSaveAsNew={handleSaveAsNew} /> - ) : null + ) : null; return ( @@ -189,6 +190,8 @@ export function ConfigurableDataTable({ data={fetchResult.data || []} columns={columns} filters={filters} + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any getRowId={adapter.getRowId || ((row: any) => row.id)} rowCount={fetchResult.count} enablePagination @@ -196,7 +199,9 @@ export function ConfigurableDataTable({ pageSize={pageSize} isLoading={fetchResult.isLoading || isLoadingColumns} layout={layout} - heading={heading || entityName || (entity ? t(`${entity}.domain` as any) : "")} + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + heading={heading || entityName || (entity ? t(`${entity}.domain` as any) : '')} subHeading={subHeading} enableColumnVisibility={isViewConfigEnabled} initialColumnVisibility={visibleColumns} @@ -207,12 +212,18 @@ export function ConfigurableDataTable({ entity={entity} currentColumns={currentColumns} filterBarContent={filterBarContent} + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any rowHref={adapter.getRowHref as ((row: any) => string) | undefined} - emptyState={adapter.emptyState || { - empty: { - heading: t(`${entity}.list.noRecordsMessage` as any), + emptyState={ + adapter.emptyState || { + empty: { + //@todo fix type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + heading: t(`${entity}.list.noRecordsMessage` as any) + } } - }} + } prefix={queryPrefix} actions={actions} enableFilterMenu={false} @@ -225,15 +236,15 @@ export function ConfigurableDataTable({ currentConfiguration={currentConfiguration} editingView={editingView} onClose={() => { - setSaveDialogOpen(false) - setEditingView(null) + setSaveDialogOpen(false); + setEditingView(null); }} onSaved={() => { - setSaveDialogOpen(false) - setEditingView(null) + setSaveDialogOpen(false); + setEditingView(null); }} /> )} - ) + ); } diff --git a/src/components/table/configurable-data-table/index.ts b/src/components/table/configurable-data-table/index.ts index 52187f40..73bf3d71 100644 --- a/src/components/table/configurable-data-table/index.ts +++ b/src/components/table/configurable-data-table/index.ts @@ -1,3 +1,3 @@ -export { ConfigurableDataTable } from "./configurable-data-table" -export type { ConfigurableDataTableProps } from "./configurable-data-table" -export { SaveViewDropdown } from "./save-view-dropdown" \ No newline at end of file +export { ConfigurableDataTable } from './configurable-data-table'; +export type { ConfigurableDataTableProps } from './configurable-data-table'; +export { SaveViewDropdown } from './save-view-dropdown'; diff --git a/src/components/table/configurable-data-table/save-view-dropdown.tsx b/src/components/table/configurable-data-table/save-view-dropdown.tsx index 07c0ad97..6627e9da 100644 --- a/src/components/table/configurable-data-table/save-view-dropdown.tsx +++ b/src/components/table/configurable-data-table/save-view-dropdown.tsx @@ -1,14 +1,15 @@ -import React from "react" -import { Button, DropdownMenu, usePrompt } from "@medusajs/ui" -import { ChevronDownMini } from "@medusajs/icons" -import { useTranslation } from "react-i18next" +import type React from 'react'; + +import { ChevronDownMini } from '@medusajs/icons'; +import { Button, DropdownMenu, usePrompt } from '@medusajs/ui'; +import { useTranslation } from 'react-i18next'; interface SaveViewDropdownProps { - isDefaultView: boolean - currentViewName?: string - onSaveAsDefault: () => void - onUpdateExisting: () => void - onSaveAsNew: () => void + isDefaultView: boolean; + currentViewName?: string; + onSaveAsDefault: () => void; + onUpdateExisting: () => void; + onSaveAsNew: () => void; } export const SaveViewDropdown: React.FC = ({ @@ -16,36 +17,36 @@ export const SaveViewDropdown: React.FC = ({ currentViewName, onSaveAsDefault, onUpdateExisting, - onSaveAsNew, + onSaveAsNew }) => { - const { t } = useTranslation() - const prompt = usePrompt() + const { t } = useTranslation(); + const prompt = usePrompt(); const handleSaveAsDefault = async () => { const result = await prompt({ - title: t("views.prompts.updateDefault.title"), - description: t("views.prompts.updateDefault.description"), - confirmText: t("views.prompts.updateDefault.confirmText"), - cancelText: t("views.prompts.updateDefault.cancelText"), - }) + title: t('views.prompts.updateDefault.title'), + description: t('views.prompts.updateDefault.description'), + confirmText: t('views.prompts.updateDefault.confirmText'), + cancelText: t('views.prompts.updateDefault.cancelText') + }); if (result) { - onSaveAsDefault() + onSaveAsDefault(); } - } + }; const handleUpdateExisting = async () => { const result = await prompt({ - title: t("views.prompts.updateView.title"), - description: t("views.prompts.updateView.description", { name: currentViewName }), - confirmText: t("views.prompts.updateView.confirmText"), - cancelText: t("views.prompts.updateView.cancelText"), - }) + title: t('views.prompts.updateView.title'), + description: t('views.prompts.updateView.description', { name: currentViewName }), + confirmText: t('views.prompts.updateView.confirmText'), + cancelText: t('views.prompts.updateView.cancelText') + }); if (result) { - onUpdateExisting() + onUpdateExisting(); } - } + }; return ( @@ -55,7 +56,7 @@ export const SaveViewDropdown: React.FC = ({ size="small" type="button" > - {t("views.save")} + {t('views.save')} @@ -63,23 +64,19 @@ export const SaveViewDropdown: React.FC = ({ {isDefaultView ? ( <> - {t("views.updateDefaultForEveryone")} - - - {t("views.saveAsNew")} + {t('views.updateDefaultForEveryone')} + {t('views.saveAsNew')} ) : ( <> - {t("views.updateViewName")} - - - {t("views.saveAsNew")} + {t('views.updateViewName')} + {t('views.saveAsNew')} )} - ) -} \ No newline at end of file + ); +}; diff --git a/src/components/table/data-table/data-table-filter/context.tsx b/src/components/table/data-table/data-table-filter/context.tsx index daacb414..d3361629 100644 --- a/src/components/table/data-table/data-table-filter/context.tsx +++ b/src/components/table/data-table/data-table-filter/context.tsx @@ -1,19 +1,19 @@ -import { createContext, useContext } from "react" +import { createContext, useContext } from 'react'; type DataTableFilterContextValue = { - removeFilter: (key: string) => void - removeAllFilters: () => void -} + removeFilter: (key: string) => void; + removeAllFilters: () => void; +}; -export const DataTableFilterContext = - createContext(null) +export const DataTableFilterContext = createContext(null); export const useDataTableFilterContext = () => { - const ctx = useContext(DataTableFilterContext) + const ctx = useContext(DataTableFilterContext); if (!ctx) { throw new Error( - "useDataTableFacetedFilterContext must be used within a DataTableFacetedFilter" - ) + 'useDataTableFacetedFilterContext must be used within a DataTableFacetedFilter' + ); } - return ctx -} + + return ctx; +}; diff --git a/src/components/table/data-table/data-table-filter/data-table-filter.tsx b/src/components/table/data-table/data-table-filter/data-table-filter.tsx index b01f2eb2..1fac17e3 100644 --- a/src/components/table/data-table/data-table-filter/data-table-filter.tsx +++ b/src/components/table/data-table/data-table-filter/data-table-filter.tsx @@ -1,132 +1,127 @@ -import { Button, clx } from "@medusajs/ui" -import { Popover as RadixPopover } from "radix-ui" -import { useCallback, useEffect, useMemo, useRef, useState } from "react" -import { useSearchParams } from "react-router-dom" +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useTranslation } from "react-i18next" -import { DataTableFilterContext, useDataTableFilterContext } from "./context" -import { DateFilter } from "./date-filter" -import { NumberFilter } from "./number-filter" -import { SelectFilter } from "./select-filter" -import { StringFilter } from "./string-filter" +import { Button, clx } from '@medusajs/ui'; +import { Popover as RadixPopover } from 'radix-ui'; +import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; + +import { DataTableFilterContext, useDataTableFilterContext } from './context'; +import { DateFilter } from './date-filter'; +import { NumberFilter } from './number-filter'; +import { SelectFilter } from './select-filter'; +import { StringFilter } from './string-filter'; type Option = { - label: string - value: unknown -} + label: string; + value: unknown; +}; export type Filter = { - key: string - label: string + key: string; + label: string; } & ( | { - type: "select" - options: Option[] - multiple?: boolean - searchable?: boolean + type: 'select'; + options: Option[]; + multiple?: boolean; + searchable?: boolean; } | { - type: "date" - options?: never + type: 'date'; + options?: never; } | { - type: "string" - options?: never + type: 'string'; + options?: never; } | { - type: "number" - options?: never + type: 'number'; + options?: never; } -) +); type DataTableFilterProps = { - filters: Filter[] - readonly?: boolean - prefix?: string -} + filters: Filter[]; + readonly?: boolean; + prefix?: string; +}; -export const DataTableFilter = ({ - filters, - readonly, - prefix, -}: DataTableFilterProps) => { - const { t } = useTranslation() - const [searchParams] = useSearchParams() - const [open, setOpen] = useState(false) +export const DataTableFilter = ({ filters, readonly, prefix }: DataTableFilterProps) => { + const { t } = useTranslation(); + const [searchParams] = useSearchParams(); + const [open, setOpen] = useState(false); const [activeFilters, setActiveFilters] = useState( getInitialFilters({ searchParams, filters, prefix }) - ) + ); - const availableFilters = filters.filter( - (f) => !activeFilters.find((af) => af.key === f.key) - ) + const availableFilters = filters.filter(f => !activeFilters.find(af => af.key === f.key)); /** * If there are any filters in the URL that are not in the active filters, * add them to the active filters. This ensures that we display the filters * if a user navigates to a page with filters in the URL. */ - const initialMount = useRef(true) + const initialMount = useRef(true); useEffect(() => { if (initialMount.current) { - const params = new URLSearchParams(searchParams) + const params = new URLSearchParams(searchParams); - filters.forEach((filter) => { - const key = prefix ? `${prefix}_${filter.key}` : filter.key - const value = params.get(key) - if (value && !activeFilters.find((af) => af.key === filter.key)) { - if (filter.type === "select") { - setActiveFilters((prev) => [ + filters.forEach(filter => { + const key = prefix ? `${prefix}_${filter.key}` : filter.key; + const value = params.get(key); + if (value && !activeFilters.find(af => af.key === filter.key)) { + if (filter.type === 'select') { + setActiveFilters(prev => [ ...prev, { ...filter, multiple: filter.multiple, options: filter.options, - openOnMount: false, - }, - ]) + openOnMount: false + } + ]); } else { - setActiveFilters((prev) => [ - ...prev, - { ...filter, openOnMount: false }, - ]) + setActiveFilters(prev => [...prev, { ...filter, openOnMount: false }]); } } - }) + }); } - initialMount.current = false - }, [activeFilters, filters, prefix, searchParams]) + initialMount.current = false; + }, [activeFilters, filters, prefix, searchParams]); const addFilter = (filter: Filter) => { - setOpen(false) - setActiveFilters((prev) => [...prev, { ...filter, openOnMount: true }]) - } + setOpen(false); + setActiveFilters(prev => [...prev, { ...filter, openOnMount: true }]); + }; const removeFilter = useCallback((key: string) => { - setActiveFilters((prev) => prev.filter((f) => f.key !== key)) - }, []) + setActiveFilters(prev => prev.filter(f => f.key !== key)); + }, []); const removeAllFilters = useCallback(() => { - setActiveFilters([]) - }, []) + setActiveFilters([]); + }, []); return ( ({ removeFilter, - removeAllFilters, + removeAllFilters }), [removeAllFilters, removeFilter] )} > -
    - {activeFilters.map((filter) => { +
    + {activeFilters.map(filter => { switch (filter.type) { - case "select": + case 'select': return ( - ) - case "date": + ); + case 'date': return ( - ) - case "string": + ); + case 'string': return ( - ) - case "number": + ); + case 'number': return ( - ) + ); default: - break + break; } })} {!readonly && availableFilters.length > 0 && ( - - - { - const hasOpenFilter = activeFilters.find( - (filter) => filter.openOnMount - ) + onCloseAutoFocus={e => { + const hasOpenFilter = activeFilters.find(filter => filter.openOnMount); if (hasOpenFilter) { - e.preventDefault() + e.preventDefault(); } }} > - {availableFilters.map((filter) => { + {availableFilters.map(filter => { return (
    { - addFilter(filter) + addFilter(filter); }} data-testid={`data-table-filter-option-${filter.key}`} > {filter.label}
    - ) + ); })}
    )} {!readonly && activeFilters.length > 0 && ( - + )}
    - ) -} + ); +}; type ClearAllFiltersProps = { - filters: Filter[] - prefix?: string -} + filters: Filter[]; + prefix?: string; +}; const ClearAllFilters = ({ filters, prefix }: ClearAllFiltersProps) => { - const { removeAllFilters } = useDataTableFilterContext() - const [_, setSearchParams] = useSearchParams() + const { removeAllFilters } = useDataTableFilterContext(); + const [_, setSearchParams] = useSearchParams(); const handleRemoveAll = () => { - setSearchParams((prev) => { - const newValues = new URLSearchParams(prev) + setSearchParams(prev => { + const newValues = new URLSearchParams(prev); - filters.forEach((filter) => { - newValues.delete(prefix ? `${prefix}_${filter.key}` : filter.key) - }) + filters.forEach(filter => { + newValues.delete(prefix ? `${prefix}_${filter.key}` : filter.key); + }); - return newValues - }) + return newValues; + }); - removeAllFilters() - } + removeAllFilters(); + }; return ( - ) -} + ); +}; const getInitialFilters = ({ searchParams, filters, - prefix, + prefix }: { - searchParams: URLSearchParams - filters: Filter[] - prefix?: string + searchParams: URLSearchParams; + filters: Filter[]; + prefix?: string; }) => { - const params = new URLSearchParams(searchParams) - const activeFilters: (Filter & { openOnMount: boolean })[] = [] + const params = new URLSearchParams(searchParams); + const activeFilters: (Filter & { openOnMount: boolean })[] = []; - filters.forEach((filter) => { - const key = prefix ? `${prefix}_${filter.key}` : filter.key - const value = params.get(key) + filters.forEach(filter => { + const key = prefix ? `${prefix}_${filter.key}` : filter.key; + const value = params.get(key); if (value) { - if (filter.type === "select") { + if (filter.type === 'select') { activeFilters.push({ ...filter, multiple: filter.multiple, options: filter.options, - openOnMount: false, - }) + openOnMount: false + }); } else { - activeFilters.push({ ...filter, openOnMount: false }) + activeFilters.push({ ...filter, openOnMount: false }); } } - }) + }); - return activeFilters -} + return activeFilters; +}; diff --git a/src/components/table/data-table/data-table-filter/date-filter.tsx b/src/components/table/data-table/data-table-filter/date-filter.tsx index 7d98d834..d1adc902 100644 --- a/src/components/table/data-table/data-table-filter/date-filter.tsx +++ b/src/components/table/data-table/data-table-filter/date-filter.tsx @@ -1,137 +1,136 @@ -import { EllipseMiniSolid } from "@medusajs/icons" -import { DatePicker, Text, clx } from "@medusajs/ui" -import isEqual from "lodash/isEqual" -import { Popover as RadixPopover } from "radix-ui" -import { useMemo, useState } from "react" - -import { t } from "i18next" -import { useTranslation } from "react-i18next" -import { useDate } from "../../../../hooks/use-date" -import { useSelectedParams } from "../hooks" -import { useDataTableFilterContext } from "./context" -import FilterChip from "./filter-chip" -import { IFilter } from "./types" - -type DateFilterProps = IFilter +import { useMemo, useState } from 'react'; + +import { useSelectedParams } from '@components/table/data-table/hooks'; +import { useDate } from '@hooks/use-date'; +import { EllipseMiniSolid } from '@medusajs/icons'; +import { clx, DatePicker, Text } from '@medusajs/ui'; +import { t } from 'i18next'; +import isEqual from 'lodash/isEqual'; +import { Popover as RadixPopover } from 'radix-ui'; +import { useTranslation } from 'react-i18next'; + +import { useDataTableFilterContext } from './context'; +import FilterChip from './filter-chip'; +import type { IFilter } from './types'; + +type DateFilterProps = IFilter; type DateComparisonOperator = { /** * The filtered date must be greater than or equal to this value. */ - $gte?: string + $gte?: string; /** * The filtered date must be less than or equal to this value. */ - $lte?: string + $lte?: string; /** * The filtered date must be less than this value. */ - $lt?: string + $lt?: string; /** * The filtered date must be greater than this value. */ - $gt?: string -} + $gt?: string; +}; -export const DateFilter = ({ - filter, - prefix, - readonly, - openOnMount, -}: DateFilterProps) => { - const [open, setOpen] = useState(openOnMount) - const [showCustom, setShowCustom] = useState(false) +export const DateFilter = ({ filter, prefix, readonly, openOnMount }: DateFilterProps) => { + const [open, setOpen] = useState(openOnMount); + const [showCustom, setShowCustom] = useState(false); - const { getFullDate } = useDate() + const { getFullDate } = useDate(); - const { key, label } = filter + const { key, label } = filter; - const { removeFilter } = useDataTableFilterContext() - const selectedParams = useSelectedParams({ param: key, prefix }) + const { removeFilter } = useDataTableFilterContext(); + const selectedParams = useSelectedParams({ param: key, prefix }); - const presets = usePresets() + const presets = usePresets(); const handleSelectPreset = (value: DateComparisonOperator) => { - selectedParams.add(JSON.stringify(value)) - setShowCustom(false) - } + selectedParams.add(JSON.stringify(value)); + setShowCustom(false); + }; const handleSelectCustom = () => { - selectedParams.delete() - setShowCustom((prev) => !prev) - } + selectedParams.delete(); + setShowCustom(prev => !prev); + }; - const currentValue = selectedParams.get() + const currentValue = selectedParams.get(); - const currentDateComparison = parseDateComparison(currentValue) - const customStartValue = getDateFromComparison(currentDateComparison, "$gte") - const customEndValue = getDateFromComparison(currentDateComparison, "$lte") + const currentDateComparison = parseDateComparison(currentValue); + const customStartValue = getDateFromComparison(currentDateComparison, '$gte'); + const customEndValue = getDateFromComparison(currentDateComparison, '$lte'); - const handleCustomDateChange = (value: Date | null, pos: "start" | "end") => { - const key = pos === "start" ? "$gte" : "$lte" + const handleCustomDateChange = (value: Date | null, pos: 'start' | 'end') => { + const key = pos === 'start' ? '$gte' : '$lte'; - let dateValue = value + let dateValue = value; // offset to the end of the day so the results include the selected end date - if (key === "$lte" && value) { - dateValue = new Date(value.getTime()) - dateValue.setHours(23, 59, 59, 999) + if (key === '$lte' && value) { + dateValue = new Date(value.getTime()); + dateValue.setHours(23, 59, 59, 999); } selectedParams.add( JSON.stringify({ ...(currentDateComparison || {}), - [key]: dateValue?.toISOString(), + [key]: dateValue?.toISOString() }) - ) - } + ); + }; const getDisplayValueFromPresets = () => { - const preset = presets.find((p) => isEqual(p.value, currentDateComparison)) - return preset?.label - } + const preset = presets.find(p => isEqual(p.value, currentDateComparison)); + + return preset?.label; + }; const formatCustomDate = (date: Date | undefined) => { - return date ? getFullDate({ date: date }) : undefined - } + return date ? getFullDate({ date: date }) : undefined; + }; const getCustomDisplayValue = () => { - const formattedDates = [customStartValue, customEndValue].map( - formatCustomDate - ) - return formattedDates.filter(Boolean).join(" - ") - } + const formattedDates = [customStartValue, customEndValue].map(formatCustomDate); + + return formattedDates.filter(Boolean).join(' - '); + }; - const displayValue = getDisplayValueFromPresets() || getCustomDisplayValue() + const displayValue = getDisplayValueFromPresets() || getCustomDisplayValue(); - const [previousValue, setPreviousValue] = useState( - displayValue - ) + const [previousValue, setPreviousValue] = useState(displayValue); const handleRemove = () => { - selectedParams.delete() - removeFilter(key) - } + selectedParams.delete(); + removeFilter(key); + }; - let timeoutId: ReturnType | null = null + let timeoutId: ReturnType | null = null; const handleOpenChange = (open: boolean) => { - setOpen(open) - setPreviousValue(displayValue) + setOpen(open); + setPreviousValue(displayValue); if (timeoutId) { - clearTimeout(timeoutId) + clearTimeout(timeoutId); } if (!open && !currentValue.length) { timeoutId = setTimeout(() => { - removeFilter(key) - }, 200) + removeFilter(key); + }, 200); } - } + }; return ( - + { + onInteractOutside={e => { if (e.target instanceof HTMLElement) { if ( - e.target.attributes.getNamedItem("data-name")?.value === - "filters_menu_content" + e.target.attributes.getNamedItem('data-name')?.value === 'filters_menu_content' ) { - e.preventDefault() + e.preventDefault(); } } }} > -
      - {presets.map((preset) => { - const isSelected = selectedParams - .get() - .includes(JSON.stringify(preset.value)) +
        + {presets.map(preset => { + const isSelected = selectedParams.get().includes(JSON.stringify(preset.value)); + return (
      • - ) + ); })}
      {showCustom && ( -
      +
      - - {t("filters.date.from")} + + {t('filters.date.from')}
      @@ -228,15 +230,20 @@ export const DateFilter = ({ modal maxValue={customEndValue} value={customStartValue} - onChange={(d) => handleCustomDateChange(d, "start")} + onChange={d => handleCustomDateChange(d, 'start')} data-testid={`data-table-date-filter-from-picker-${key}`} />
      - - {t("filters.date.to")} + + {t('filters.date.to')}
      @@ -244,8 +251,8 @@ export const DateFilter = ({ modal minValue={customStartValue} value={customEndValue || undefined} - onChange={(d) => { - handleCustomDateChange(d, "end") + onChange={d => { + handleCustomDateChange(d, 'end'); }} data-testid={`data-table-date-filter-to-picker-${key}`} /> @@ -257,80 +264,67 @@ export const DateFilter = ({ )} - ) -} + ); +}; -const today = new Date() -today.setHours(0, 0, 0, 0) +const today = new Date(); +today.setHours(0, 0, 0, 0); const usePresets = () => { - const { t } = useTranslation() + const { t } = useTranslation(); return useMemo( () => [ { - label: t("filters.date.today"), + label: t('filters.date.today'), value: { - $gte: today.toISOString(), - }, + $gte: today.toISOString() + } }, { - label: t("filters.date.lastSevenDays"), + label: t('filters.date.lastSevenDays'), value: { - $gte: new Date( - today.getTime() - 7 * 24 * 60 * 60 * 1000 - ).toISOString(), // 7 days ago - }, + $gte: new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString() // 7 days ago + } }, { - label: t("filters.date.lastThirtyDays"), + label: t('filters.date.lastThirtyDays'), value: { - $gte: new Date( - today.getTime() - 30 * 24 * 60 * 60 * 1000 - ).toISOString(), // 30 days ago - }, + $gte: new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString() // 30 days ago + } }, { - label: t("filters.date.lastNinetyDays"), + label: t('filters.date.lastNinetyDays'), value: { - $gte: new Date( - today.getTime() - 90 * 24 * 60 * 60 * 1000 - ).toISOString(), // 90 days ago - }, + $gte: new Date(today.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString() // 90 days ago + } }, { - label: t("filters.date.lastTwelveMonths"), + label: t('filters.date.lastTwelveMonths'), value: { - $gte: new Date( - today.getTime() - 365 * 24 * 60 * 60 * 1000 - ).toISOString(), // 365 days ago - }, - }, + $gte: new Date(today.getTime() - 365 * 24 * 60 * 60 * 1000).toISOString() // 365 days ago + } + } ], [t] - ) -} + ); +}; const parseDateComparison = (value: string[]) => { - return value?.length - ? (JSON.parse(value.join(",")) as DateComparisonOperator) - : null -} - -const getDateFromComparison = ( - comparison: DateComparisonOperator | null, - key: "$gte" | "$lte" -) => { + return value?.length ? (JSON.parse(value.join(',')) as DateComparisonOperator) : null; +}; + +const getDateFromComparison = (comparison: DateComparisonOperator | null, key: '$gte' | '$lte') => { if (!comparison?.[key]) { - return undefined + return undefined; } - const compareDate = new Date(comparison[key] as string) + const compareDate = new Date(comparison[key] as string); - if (key === "$lte") { + if (key === '$lte') { // offset back to the display date - compareDate.setHours(0, 0, 0, 0) + compareDate.setHours(0, 0, 0, 0); } - return compareDate -} + return compareDate; +}; diff --git a/src/components/table/data-table/data-table-filter/filter-chip.tsx b/src/components/table/data-table/data-table-filter/filter-chip.tsx index 4bcbdc22..e9fdf057 100644 --- a/src/components/table/data-table/data-table-filter/filter-chip.tsx +++ b/src/components/table/data-table/data-table-filter/filter-chip.tsx @@ -1,10 +1,9 @@ -import { MouseEvent } from "react"; +import type { MouseEvent } from 'react'; -import { XMarkMini } from "@medusajs/icons"; -import { Text, clx } from "@medusajs/ui"; - -import { Popover as RadixPopover } from "radix-ui"; -import { useTranslation } from "react-i18next"; +import { XMarkMini } from '@medusajs/icons'; +import { clx, Text } from '@medusajs/ui'; +import { Popover as RadixPopover } from 'radix-ui'; +import { useTranslation } from 'react-i18next'; export type FilterChipProps = { hadPreviousValue?: boolean; @@ -13,7 +12,7 @@ export type FilterChipProps = { readonly?: boolean; hasOperator?: boolean; onRemove: () => void; - "data-testid"?: string; + 'data-testid'?: string; }; const FilterChip = ({ @@ -23,7 +22,7 @@ const FilterChip = ({ readonly, hasOperator, onRemove, - "data-testid": dataTestId, + 'data-testid': dataTestId }: FilterChipProps) => { const { t } = useTranslation(); @@ -39,15 +38,16 @@ const FilterChip = ({ > {!hadPreviousValue && }
      - + {label}
      @@ -63,20 +63,17 @@ const FilterChip = ({ leading="compact" className="text-ui-fg-muted" > - {t("general.is")} + {t('general.is')}
      )} {!!(value || hadPreviousValue) && ( - {value || "\u00A0"} + {value || '\u00A0'} )} @@ -94,9 +91,9 @@ const FilterChip = ({