diff --git a/.husky/pre-commit b/.husky/pre-commit index 43f88f01f..fb24d7c1a 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,3 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - # Icons guard: when an icon SVG is added, modified or deleted, ensure the # generated components are in sync. Same check as CI (see # .github/workflows/icons-check.yml). diff --git a/packages/bootstrap/package.json b/packages/bootstrap/package.json index 1e208f101..0819c4ea7 100644 --- a/packages/bootstrap/package.json +++ b/packages/bootstrap/package.json @@ -31,7 +31,7 @@ "format": "prettier --check src/**/*.scss", "format:write": "prettier --write src/**/*.scss", "generate-commit-version": "node scripts/version.cjs", - "lint": "stylelint src/**/*.scss", + "lint": "stylelint src/**/*.scss --max-warnings 0", "watch": "sass --watch --load-path=node_modules/ --style=compressed --quiet-deps --silence-deprecation=import src/index.scss dist/index.css" }, "dependencies": { diff --git a/packages/client/package.json b/packages/client/package.json index 1e3359e79..2fd1f2763 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -39,7 +39,7 @@ "fix": "eslint --fix --report-unused-disable-directives --max-warnings 0", "format": "prettier --check \"src/ts/**/*.ts\"", "format:write": "prettier --write \"src/ts/**/*.ts\"", - "lint": "eslint", + "lint": "eslint --max-warnings 0", "test": "vitest", "test:coverage": "vitest run --coverage" }, @@ -61,4 +61,4 @@ "vite": "catalog:", "vite-plugin-dts": "catalog:" } -} +} \ No newline at end of file diff --git a/packages/extensions/package.json b/packages/extensions/package.json index 15e30774a..3960e0302 100644 --- a/packages/extensions/package.json +++ b/packages/extensions/package.json @@ -126,7 +126,7 @@ "fix": "eslint --fix --report-unused-disable-directives --max-warnings 0", "format": "prettier --check \"src/**/*.ts\"", "format:write": "prettier --write \"src/**/*.ts\"", - "lint": "eslint" + "lint": "eslint --max-warnings 0" }, "dependencies": { "@edifice.io/utilities": "workspace:*", diff --git a/packages/extensions/src/image/image.ts b/packages/extensions/src/image/image.ts index d3194d60e..78ce3a266 100644 --- a/packages/extensions/src/image/image.ts +++ b/packages/extensions/src/image/image.ts @@ -114,7 +114,7 @@ export const Image = TiptapImage.extend({ } : {}; }, - parseHTML: (element) => { + parseHTML: () => { return null; }, }, diff --git a/packages/react/package.json b/packages/react/package.json index 55d1b33e3..fc90e31bd 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -84,7 +84,7 @@ "fix": "eslint --fix --report-unused-disable-directives --max-warnings 0", "format": "prettier --check \"src/**/*.{ts,tsx}\"", "format:write": "prettier --write \"src/**/*.{ts,tsx}\"", - "lint": "eslint", + "lint": "eslint --max-warnings 0", "test": "vitest", "test:watch": "vitest --watch", "test:coverage": "vitest run --coverage" diff --git a/packages/react/src/components/Layout/hooks/useHelp.tsx b/packages/react/src/components/Layout/hooks/useHelp.tsx index 6c4973c65..23616bbd6 100644 --- a/packages/react/src/components/Layout/hooks/useHelp.tsx +++ b/packages/react/src/components/Layout/hooks/useHelp.tsx @@ -18,9 +18,12 @@ export function useHelp( const [isModalOpen, setIsModalOpen] = useState(false); const [error, setError] = useState(false); const helpPath = theme?.is1d ? '/help-1d' : '/help-2d'; + // Only the truthiness matters here, so derive a stable primitive to avoid + // re-running the effect when an object prop is recreated on each render. + const isHelpEnabled = !!hasOldHelpEnableWorkflow; useEffect(() => { - if (!hasOldHelpEnableWorkflow) { + if (!isHelpEnabled) { return; } @@ -61,7 +64,7 @@ export function useHelp( console.error(error); } })(); - }, [appCode, helpPath]); + }, [appCode, helpPath, isHelpEnabled]); const options = { replace: (domNode: any) => { diff --git a/packages/react/src/components/PageLayout/components/PageLayoutOverlay.tsx b/packages/react/src/components/PageLayout/components/PageLayoutOverlay.tsx index 76cd9a33d..2d249dbe0 100644 --- a/packages/react/src/components/PageLayout/components/PageLayoutOverlay.tsx +++ b/packages/react/src/components/PageLayout/components/PageLayoutOverlay.tsx @@ -1,6 +1,7 @@ import { type ComponentPropsWithoutRef, type ReactNode, + useCallback, useEffect, } from 'react'; @@ -30,10 +31,10 @@ const PageLayoutOverlay = ({ const { t } = useTranslation(); const { isOverlayOpen, updateOverlayOpen } = useOverlay(); - const handleClose = () => { + const handleClose = useCallback(() => { updateOverlayOpen(false); onClose?.(); - }; + }, [onClose, updateOverlayOpen]); useEffect(() => { if (!isOverlayOpen) return; @@ -60,7 +61,6 @@ const PageLayoutOverlay = ({ className, )} aria-hidden={!isOverlayOpen} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore — inert is not yet in React's HTMLAttributes but is valid HTML inert={!isOverlayOpen ? '' : undefined} {...props} diff --git a/packages/react/src/hooks/useCheckable/useCheckable.ts b/packages/react/src/hooks/useCheckable/useCheckable.ts index e89c5fbc1..01892df1b 100644 --- a/packages/react/src/hooks/useCheckable/useCheckable.ts +++ b/packages/react/src/hooks/useCheckable/useCheckable.ts @@ -11,12 +11,14 @@ export const useCheckable = ( useEffect(() => { // If data changes, check if selected items are still valid if (data) { - const validSelectedItems = selectedItems.filter((id) => - data.some((item) => item._id === id), - ); - if (validSelectedItems.length !== selectedItems.length) { - setSelectedItems(validSelectedItems); - } + setSelectedItems((currentSelection) => { + const validSelectedItems = currentSelection.filter((id) => + data.some((item) => item._id === id), + ); + return validSelectedItems.length !== currentSelection.length + ? validSelectedItems + : currentSelection; + }); } }, [data]); diff --git a/packages/react/src/hooks/useDate/useDate.ts b/packages/react/src/hooks/useDate/useDate.ts index b6945e777..218c20269 100644 --- a/packages/react/src/hooks/useDate/useDate.ts +++ b/packages/react/src/hooks/useDate/useDate.ts @@ -138,7 +138,7 @@ export default function useDate() { // format D MMM YYYY return computedDate.format(t('date.format.previousYear')); }, - [currentLanguage, parseDate], + [t, toComputedDate], ); /** Compute a user-friendly elapsed duration, between now and a date. */ @@ -147,7 +147,7 @@ export default function useDate() { const computedDate = toComputedDate(date); return computedDate?.isValid() ? computedDate.fromNow() : ''; }, - [currentLanguage, parseDate], + [toComputedDate], ); /** @@ -181,7 +181,7 @@ export default function useDate() { ? computedDate.locale(currentLanguage as string).format(dayjsFormat) : ''; }, - [currentLanguage, parseDate], + [currentLanguage, toComputedDate], ); /** Check if two dates are the same, according to the specified unit. See https://day.js.org/docs/en/query/is-same for more details. @@ -196,7 +196,7 @@ export default function useDate() { const computedDate2 = toComputedDate(date2); return computedDate?.isSame(computedDate2, unit) ?? false; }, - [currentLanguage, parseDate], + [toComputedDate], ); /** Check if a date is same or after another date. See https://day.js.org/docs/en/query/is-same-or-after for more details. @@ -211,7 +211,7 @@ export default function useDate() { const computedDate2 = toComputedDate(date2); return computedDate?.isSameOrAfter(computedDate2, unit) ?? false; }, - [currentLanguage, parseDate], + [toComputedDate], ); /** Check if a date is today. See https://day.js.org/docs/en/plugin/is-today for more details. @@ -223,7 +223,7 @@ export default function useDate() { const computedDate = toComputedDate(date); return computedDate?.isToday() ?? false; }, - [currentLanguage, parseDate], + [toComputedDate], ); return { diff --git a/packages/react/src/hooks/useHttpErrorToast/useHttpErrorToast.ts b/packages/react/src/hooks/useHttpErrorToast/useHttpErrorToast.ts index 8ece8c740..10cca2ac0 100644 --- a/packages/react/src/hooks/useHttpErrorToast/useHttpErrorToast.ts +++ b/packages/react/src/hooks/useHttpErrorToast/useHttpErrorToast.ts @@ -16,6 +16,11 @@ export const useHttpErrorToast = ({ const toast = useToast(); const { t } = useTranslation(); + // Keep the latest options in a ref so the subscription always uses fresh + // values without re-subscribing on every render (options is a new object each time). + const optionsRef = useRef(options); + optionsRef.current = options; + useEffect(() => { if (active) { // Subscribe to HTTP error events @@ -39,7 +44,7 @@ export const useHttpErrorToast = ({ message.current = t(i18nKey); toast.error( React.createElement('div', { children: [message.current] }), - options, + optionsRef.current, ); }); diff --git a/packages/react/src/hooks/useUploadFiles/useUploadFiles.ts b/packages/react/src/hooks/useUploadFiles/useUploadFiles.ts index d34353c38..7ea70697b 100644 --- a/packages/react/src/hooks/useUploadFiles/useUploadFiles.ts +++ b/packages/react/src/hooks/useUploadFiles/useUploadFiles.ts @@ -67,7 +67,7 @@ const useUploadFiles = ({ } }); }, - [uploadAlternateFile, uploadFile, replaceFileAt], + [uploadAlternateFile, uploadFile, replaceFileAt, resetInputValue], ); /* Try to upload more files when diff --git a/packages/react/src/hooks/useWorkspaceFolders/useWorkspaceFoldersTree.ts b/packages/react/src/hooks/useWorkspaceFolders/useWorkspaceFoldersTree.ts index 885a694bd..3980b3b9c 100644 --- a/packages/react/src/hooks/useWorkspaceFolders/useWorkspaceFoldersTree.ts +++ b/packages/react/src/hooks/useWorkspaceFolders/useWorkspaceFoldersTree.ts @@ -1,7 +1,6 @@ import { WorkspaceElement } from '@edifice.io/client'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useEdificeClient } from '../../providers/EdificeClientProvider/EdificeClientProvider.hook'; interface FolderTreeNode { id: string; @@ -14,7 +13,6 @@ export const WORKSPACE_SHARED_FOLDER_ID = 'workspace-shared-folder-id'; function useWorkspaceFoldersTree(folders?: WorkspaceElement[]) { const { t } = useTranslation(); - const { user } = useEdificeClient(); const [searchQuery, setSearchQuery] = useState(''); @@ -28,7 +26,7 @@ function useWorkspaceFoldersTree(folders?: WorkspaceElement[]) { ); return searchQuery ? filterTree(foldersTree, searchQuery) : foldersTree; - }, [folders, searchQuery, user]); + }, [folders, searchQuery, t]); return { foldersTree: filtredFoldersTree, diff --git a/packages/react/src/modules/editor/components/Editor/CantooAdaptTextBoxView.tsx b/packages/react/src/modules/editor/components/Editor/CantooAdaptTextBoxView.tsx index 93528bdea..a15118afc 100644 --- a/packages/react/src/modules/editor/components/Editor/CantooAdaptTextBoxView.tsx +++ b/packages/react/src/modules/editor/components/Editor/CantooAdaptTextBoxView.tsx @@ -22,6 +22,8 @@ const CantooAdaptTextBoxView = ({ inline: 'nearest', }); } + // Run only on mount: scroll the box into view once when it opens. + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( diff --git a/packages/react/src/modules/editor/components/EditorToolbar/EditorToolbar.tsx b/packages/react/src/modules/editor/components/EditorToolbar/EditorToolbar.tsx index 44ed1bf08..e496249a7 100644 --- a/packages/react/src/modules/editor/components/EditorToolbar/EditorToolbar.tsx +++ b/packages/react/src/modules/editor/components/EditorToolbar/EditorToolbar.tsx @@ -438,6 +438,7 @@ export const EditorToolbar = ({ }, [ alignmentOptions, canRecognizeSpeech, + cantooEditor, editor, listOptions, mediaLibraryRef, diff --git a/packages/react/src/modules/modals/OnboardingModal/useOnboardingModal.ts b/packages/react/src/modules/modals/OnboardingModal/useOnboardingModal.ts index 68d24dfbc..cc3c0dcfc 100644 --- a/packages/react/src/modules/modals/OnboardingModal/useOnboardingModal.ts +++ b/packages/react/src/modules/modals/OnboardingModal/useOnboardingModal.ts @@ -36,6 +36,9 @@ export const useOnboardingModal = ( state.current = undefined; } })(); + // Run only on mount: read the stored preference once to decide whether to + // start the onboarding. Re-running on prop changes would re-open the modal. + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleSavePreference = useCallback(async () => { diff --git a/packages/react/src/modules/multimedia/WorkspaceFolders/WorkspaceFolders.tsx b/packages/react/src/modules/multimedia/WorkspaceFolders/WorkspaceFolders.tsx index 631cecbc9..1282b122b 100644 --- a/packages/react/src/modules/multimedia/WorkspaceFolders/WorkspaceFolders.tsx +++ b/packages/react/src/modules/multimedia/WorkspaceFolders/WorkspaceFolders.tsx @@ -48,6 +48,9 @@ export default function WorkspaceFolders({ onFolderSelected(selectedFolderIdForAPI, canCopyFileInto); } + // Intentionally react only to a change of the selected folder. The callback + // and helper below are merely invoked here, not meant to re-trigger it. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedFolderId]); const handleSearchChange = (e: ChangeEvent) => { diff --git a/packages/rest-client-base/package.json b/packages/rest-client-base/package.json index d4f4d88ff..4ad7e082f 100644 --- a/packages/rest-client-base/package.json +++ b/packages/rest-client-base/package.json @@ -37,7 +37,7 @@ "clean": "rm -rf dist node_modules", "format": "prettier --check src", "format:write": "prettier --write src", - "lint": "eslint src", + "lint": "eslint src --max-warnings 0", "lint:fix": "eslint src --fix", "build": "pnpm run build:node && pnpm run build:browser && pnpm run build:react-native", "build:node": "vite build --config vite.config.node.ts && tsc src/bin/cli.ts --outDir dist/node --module commonjs --target ES2020 && mv dist/node/cli.js dist/node/cli.cjs", diff --git a/packages/utilities/package.json b/packages/utilities/package.json index 877648ebc..c635c5780 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -38,7 +38,7 @@ "fix": "eslint --fix --report-unused-disable-directives --max-warnings 0", "format": "prettier --check \"src/**/*.ts\"", "format:write": "prettier --write \"src/**/*.ts\"", - "lint": "eslint" + "lint": "eslint --max-warnings 0" }, "devDependencies": { "@types/node": "catalog:",