diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a3efc4e488..fe36e6cfa3 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -55,6 +55,21 @@ jobs: download-translations: 'false' - name: Lint run: yarn lint + knip: + needs: setup + runs-on: depot-ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Set up environment + uses: ./.github/actions/setup + with: + download-translations: 'false' + - name: Build plugins service + run: yarn build:plugins-service + - name: Check for unused files, dependencies and exports + run: yarn knip typecheck: needs: setup runs-on: depot-ubuntu-latest diff --git a/knip.json b/knip.json new file mode 100644 index 0000000000..c564652909 --- /dev/null +++ b/knip.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "rules": { + "exports": "off", + "types": "off", + "nsExports": "off", + "nsTypes": "off", + "duplicates": "off" + }, + "ignoreBinaries": ["electron-rebuild"], + "workspaces": { + ".": { + "entry": ["bin/*.mts"], + "ignore": ["lage.config.js", ".claude/**"] + }, + "packages/api": { + "entry": ["index.browser.ts", "browser-worker.ts", "e2e/**/*.{ts,mjs}"], + "ignore": ["typings.ts"], + "ignoreDependencies": [ + "@actual-app/crdt", + "better-sqlite3", + "uuid", + "@jlongster/sql.js" + ] + }, + "packages/ci-actions": { + "entry": ["bin/*.ts"], + "ignoreBinaries": ["bin/tsx"], + "ignoreDependencies": ["extensionless"] + }, + "packages/component-library": { + "entry": ["src/icons/index-template.ts", "src/icons/template.ts"], + "ignore": ["src/icons/.svgrrc.js", "src/icons/add-attribute.ts"], + "ignoreDependencies": ["@svgr/babel-plugin-add-jsx-attribute"] + }, + "packages/crdt": { + "ignore": ["src/proto/**"], + "ignoreDependencies": ["@bufbuild/protoc-gen-es"] + }, + "packages/desktop-client": { + "entry": [ + "bin/*.mjs", + "globals.ts", + "src/globals.ts", + "src/build-shims.js" + ], + "ignoreDependencies": ["plugins-service"] + }, + "packages/desktop-electron": { + "entry": [ + "preload.ts", + "server.ts", + "afterSignHook.ts", + "beforePackHook.ts" + ], + "ignoreBinaries": ["bin/update-client"], + "ignoreDependencies": ["better-sqlite3"] + }, + "packages/docs": { + "entry": [ + "src/theme/**/*.{ts,tsx,js,jsx}", + "src/components/**/*.{ts,tsx,js,jsx}", + "docs/**/*.{js,jsx}" + ], + "ignore": ["docusaurus.netlify-config.js"] + }, + "packages/loot-core": { + "entry": ["src/**/index-types.ts", "src/mocks/**"], + "ignoreDependencies": [ + "assert", + "browserify-zlib", + "buffer", + "crypto-browserify", + "path-browserify", + "stream-browserify", + "timers-browserify", + "util" + ] + }, + "packages/plugins-service": { + "entry": ["src/plugin-service-worker.ts"] + }, + "packages/sync-server": { + "entry": [ + "app.ts", + "src/app.ts", + "bin/**/*.js", + "src/scripts/**/*.js", + "src/app-*/app-*.{ts,js}" + ] + } + } +} diff --git a/package.json b/package.json index 58e3c898db..d885b3d86e 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "rebuild-node": "yarn workspace @actual-app/core rebuild", "lint": "oxfmt --check . && oxlint --type-aware --quiet", "lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet", + "knip": "knip", "install:server": "yarn workspaces focus @actual-app/sync-server --production", "constraints": "yarn constraints", "typecheck": "tsgo -p tsconfig.root.json --noEmit && lage typecheck", @@ -74,6 +75,7 @@ "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-typescript-paths": "^0.0.33", "husky": "^9.1.7", + "knip": "^6.17.1", "lage": "^2.15.13", "minimatch": "^10.2.5", "nano-staged": "^1.0.2", diff --git a/packages/component-library/package.json b/packages/component-library/package.json index 554c5ba034..0677fad66e 100644 --- a/packages/component-library/package.json +++ b/packages/component-library/package.json @@ -61,6 +61,8 @@ "@storybook/react-vite": "^10.4.4", "@svgr/babel-plugin-add-jsx-attribute": "^8.0.0", "@svgr/cli": "^8.1.0", + "@svgr/core": "^8.1.0", + "@testing-library/react": "16.3.2", "@types/react": "^19.2.17", "@typescript/native-preview": "beta", "@vitejs/plugin-react": "^6.0.2", diff --git a/packages/desktop-client/src/components/budget/goals/BudgetAutomation.tsx b/packages/desktop-client/src/components/budget/goals/BudgetAutomation.tsx deleted file mode 100644 index 4e99b72fc5..0000000000 --- a/packages/desktop-client/src/components/budget/goals/BudgetAutomation.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useMemo, useReducer, useRef, useState } from 'react'; - -import { SpaceBetween } from '@actual-app/components/space-between'; -import type { CSSProperties } from '@actual-app/components/styles'; -import { firstDayOfMonth } from '@actual-app/core/shared/months'; -import type { - CategoryGroupEntity, - ScheduleEntity, -} from '@actual-app/core/types/models'; -import type { Template } from '@actual-app/core/types/models/templates'; - -import { useEffectAfterMount } from '#hooks/useEffectAfterMount'; - -import { BudgetAutomationEditor } from './BudgetAutomationEditor'; -import { BudgetAutomationReadOnly } from './BudgetAutomationReadOnly'; -import type { DisplayTemplateType } from './constants'; -import { DEFAULT_PRIORITY, getInitialState, templateReducer } from './reducer'; - -type BudgetAutomationProps = { - categories: CategoryGroupEntity[]; - schedules: readonly ScheduleEntity[]; - template?: Template; - onSave?: (template: Template, displayType: DisplayTemplateType) => void; - onDelete?: () => void; - style?: CSSProperties; - readOnlyStyle?: CSSProperties; - inline?: boolean; - hasLimitAutomation?: boolean; - onAddLimitAutomation?: () => void; -}; - -const DEFAULT_TEMPLATE: Template = { - directive: 'template', - type: 'periodic', - amount: 0, - period: { - period: 'month', - amount: 1, - }, - starting: firstDayOfMonth(new Date()), - priority: DEFAULT_PRIORITY, -}; - -export const BudgetAutomation = ({ - onDelete, - onSave, - categories, - schedules, - readOnlyStyle, - style, - template, - inline = false, - hasLimitAutomation, - onAddLimitAutomation, -}: BudgetAutomationProps) => { - const [isEditing, setIsEditing] = useState(false); - - const [state, dispatch] = useReducer(templateReducer, null, () => - getInitialState(template ?? DEFAULT_TEMPLATE), - ); - - const onSaveRef = useRef(onSave); - onSaveRef.current = onSave; - useEffectAfterMount(() => { - onSaveRef.current?.(state.template, state.displayType); - }, [state]); - - const categoryNameMap = useMemo(() => { - return categories.reduce( - (acc, group) => { - for (const category of group.categories ?? []) { - acc[category.id] = category.name; - } - return acc; - }, - {} as Record, - ); - }, [categories]); - - return ( - - - {isEditing && ( - - )} - - ); -}; diff --git a/packages/desktop-client/src/components/budget/goals/BudgetAutomationEditor.tsx b/packages/desktop-client/src/components/budget/goals/BudgetAutomationEditor.tsx deleted file mode 100644 index a78955fc27..0000000000 --- a/packages/desktop-client/src/components/budget/goals/BudgetAutomationEditor.tsx +++ /dev/null @@ -1,229 +0,0 @@ -import type { ReactNode } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; - -import { InitialFocus } from '@actual-app/components/initial-focus'; -import { Select } from '@actual-app/components/select'; -import { SpaceBetween } from '@actual-app/components/space-between'; -import { styles } from '@actual-app/components/styles'; -import { Text } from '@actual-app/components/text'; -import { theme } from '@actual-app/components/theme'; -import type { - CategoryGroupEntity, - ScheduleEntity, -} from '@actual-app/core/types/models'; - -import { FormField, FormLabel, FormTextLabel } from '#components/forms'; - -import { setType } from './actions'; -import type { Action } from './actions'; -import type { ReducerState } from './constants'; -import { displayTemplateTypes } from './constants'; -import { getDisplayTemplateMeta } from './displayTemplateMeta'; -import { BySaveAutomation } from './editor/BySaveAutomation'; -import { FixedAutomation } from './editor/FixedAutomation'; -import { HistoricalAutomation } from './editor/HistoricalAutomation'; -import { LimitAutomation } from './editor/LimitAutomation'; -import { LongTermGoalAutomation } from './editor/LongTermGoalAutomation'; -import { PercentageAutomation } from './editor/PercentageAutomation'; -import { RefillAutomation } from './editor/RefillAutomation'; -import { RemainderAutomation } from './editor/RemainderAutomation'; -import { ScheduleAutomation } from './editor/ScheduleAutomation'; - -type BudgetAutomationEditorProps = { - inline: boolean; - state: ReducerState; - dispatch: (action: Action) => void; - schedules: readonly ScheduleEntity[]; - categories: CategoryGroupEntity[]; - hasLimitAutomation?: boolean; - onAddLimitAutomation?: () => void; -}; - -const displayTypeToDescription = { - limit: ( - - Set a cap for all budget contributions to this category across all - automations. The maximum can be set on a monthly, weekly, or daily basis. - For example, a $100 weekly cap would result in a $400 monthly cap ($500 - depending on the month). - - ), - refill: ( - - Refill the category up to the balance limit set by the balance limit - automation. - - ), - fixed: ( - - Add a fixed amount to this category for each week in the month. For - example, $100 per week would be $400 per month in a 4-week month. - - ), - schedule: ( - - Add enough to this category to cover the selected schedule. If the - schedule occurs multiple times in a month, an amount will be added for - each occurrence. You can choose to save up for the next occurrence over - time (e.g. save $100 each month for a $300 quarterly bill) or cover each - occurrence when it occurs (e.g. only add the $300 when the bill is due). - - ), - percentage: ( - - Add a fixed percentage of your income to this category each month. You can - choose to take the percentage from the current month or the previous - month. - - ), - historical: ( - - Add an amount to this category each month based on the values from - previous months. For example, you can copy the amount from a year ago to - budget for an annual expense, or budget the average of the last 3 months - to account for seasonal changes. - - ), - by: ( - - Spread a target amount across the months between now and a target date. - Useful for annual goals or saving toward a one-off expense. - - ), - remainder: ( - - Split any remaining To Budget across categories using this automation. - Higher weights take a larger share of the leftover funds. - - ), - goal: ( - - Set a long-term savings target. This changes the coloring of the balance - on the budget page to be based on progress towards the target rather than - the current month funding progress. - - ), -}; - -export function BudgetAutomationEditor({ - inline, - state, - dispatch, - schedules, - categories, - hasLimitAutomation = false, - onAddLimitAutomation, -}: BudgetAutomationEditorProps) { - const { t } = useTranslation(); - - let automationEditor: ReactNode; - switch (state.displayType) { - case 'limit': - automationEditor = ( - - ); - break; - case 'refill': - automationEditor = ( - - ); - break; - case 'fixed': - automationEditor = ( - - ); - break; - case 'schedule': - automationEditor = ( - - ); - break; - case 'percentage': - automationEditor = ( - - ); - break; - case 'historical': - automationEditor = ( - - ); - break; - case 'by': - automationEditor = ( - - ); - break; - case 'remainder': - automationEditor = ( - - ); - break; - case 'goal': - automationEditor = ( - - ); - break; - default: - state satisfies never; - automationEditor = ( - - Unrecognized automation type. - - ); - } - - return ( - - - - - -