mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 20:44:32 -05:00
Compare commits
5 Commits
react-quer
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74c27ea698 | ||
|
|
71d8b0be45 | ||
|
|
0c070e5703 | ||
|
|
c6c1fa686e | ||
|
|
791a2e63f8 |
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -50,6 +50,22 @@ jobs:
|
|||||||
name: actual-crdt
|
name: actual-crdt
|
||||||
path: packages/crdt/actual-crdt.tgz
|
path: packages/crdt/actual-crdt.tgz
|
||||||
|
|
||||||
|
plugins-core:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
- name: Build Plugins Core
|
||||||
|
run: yarn workspace @actual-app/plugins-core build
|
||||||
|
- name: Create package tgz
|
||||||
|
run: cd packages/plugins-core && yarn pack && mv package.tgz actual-plugins-core.tgz
|
||||||
|
- name: Upload Build
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: actual-plugins-core
|
||||||
|
path: packages/plugins-core/actual-plugins-core.tgz
|
||||||
|
|
||||||
web:
|
web:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ export default pluginTypescript.config(
|
|||||||
'packages/loot-core/**/lib-dist/*',
|
'packages/loot-core/**/lib-dist/*',
|
||||||
'packages/loot-core/**/proto/*',
|
'packages/loot-core/**/proto/*',
|
||||||
'packages/sync-server/build/',
|
'packages/sync-server/build/',
|
||||||
|
'packages/plugins-core/build/',
|
||||||
|
'packages/plugins-core/node_modules/',
|
||||||
'.yarn/*',
|
'.yarn/*',
|
||||||
'.github/*',
|
'.github/*',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -49,7 +49,8 @@
|
|||||||
"./toggle": "./src/Toggle.tsx",
|
"./toggle": "./src/Toggle.tsx",
|
||||||
"./tooltip": "./src/Tooltip.tsx",
|
"./tooltip": "./src/Tooltip.tsx",
|
||||||
"./view": "./src/View.tsx",
|
"./view": "./src/View.tsx",
|
||||||
"./color-picker": "./src/ColorPicker.tsx"
|
"./color-picker": "./src/ColorPicker.tsx",
|
||||||
|
"./props/*": "./src/props/*.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
||||||
|
|||||||
11
packages/component-library/src/props/modalProps.ts
Normal file
11
packages/component-library/src/props/modalProps.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { CSSProperties } from '../styles';
|
||||||
|
|
||||||
|
export type BasicModalProps = {
|
||||||
|
isLoading?: boolean;
|
||||||
|
noAnimation?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
|
onClose?: () => void;
|
||||||
|
containerProps?: {
|
||||||
|
style?: CSSProperties;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { q } from 'loot-core/shared/query';
|
|
||||||
import type { AccountEntity, CategoryEntity } from 'loot-core/types/models';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parametrizedField,
|
parametrizedField,
|
||||||
type SheetFields,
|
type SheetFields,
|
||||||
type Binding,
|
type Binding,
|
||||||
type SheetNames,
|
type SheetNames,
|
||||||
} from '.';
|
} from '@actual-app/plugins-core';
|
||||||
|
|
||||||
|
import { q } from 'loot-core/shared/query';
|
||||||
|
import type { AccountEntity, CategoryEntity } from 'loot-core/types/models';
|
||||||
|
|
||||||
import { uncategorizedTransactions } from '@desktop-client/queries';
|
import { uncategorizedTransactions } from '@desktop-client/queries';
|
||||||
|
|
||||||
|
|||||||
67
packages/plugins-core/package.json
Normal file
67
packages/plugins-core/package.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"name": "@actual-app/plugins-core",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Core plugin system for Actual Budget",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint src/ tests/ --ext .ts,.tsx",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@actual-app/components": "workspace:^",
|
||||||
|
"@types/react": "^19.1.4",
|
||||||
|
"@types/react-dom": "^19.1.4",
|
||||||
|
"i18next": "^25.2.1",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-aria-components": "^1.7.1",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
|
"typescript": "^5.5.4",
|
||||||
|
"typescript-eslint": "^8.18.1",
|
||||||
|
"typescript-strict-plugin": "^2.4.4",
|
||||||
|
"vite": "^6.2.0",
|
||||||
|
"vite-plugin-dts": "^4.5.3"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
|
"import": "./build/index.js",
|
||||||
|
"require": "./build/index.cjs"
|
||||||
|
},
|
||||||
|
"./server": {
|
||||||
|
"types": "./src/server.ts",
|
||||||
|
"development": "./src/server.ts",
|
||||||
|
"import": "./build/server.js",
|
||||||
|
"require": "./build/server.cjs"
|
||||||
|
},
|
||||||
|
"./client": {
|
||||||
|
"types": "./src/client.ts",
|
||||||
|
"development": "./src/client.ts",
|
||||||
|
"import": "./build/client.js",
|
||||||
|
"require": "./build/client.cjs"
|
||||||
|
},
|
||||||
|
"./BasicModalComponents": {
|
||||||
|
"types": "./src/BasicModalComponents.tsx",
|
||||||
|
"import": "./build/BasicModalComponents.js",
|
||||||
|
"development": "./src/BasicModalComponents.tsx"
|
||||||
|
},
|
||||||
|
"./types/*": {
|
||||||
|
"types": "./src/types/*.ts",
|
||||||
|
"development": "./src/types/*.ts"
|
||||||
|
},
|
||||||
|
"./query": {
|
||||||
|
"types": "./src/query/index.ts",
|
||||||
|
"development": "./src/query/index.ts"
|
||||||
|
},
|
||||||
|
"./src/*": "./src/*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
257
packages/plugins-core/src/BasicModalComponents.tsx
Normal file
257
packages/plugins-core/src/BasicModalComponents.tsx
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@actual-app/components/button';
|
||||||
|
import { SvgLogo } from '@actual-app/components/icons/logo';
|
||||||
|
import { SvgDelete } from '@actual-app/components/icons/v0';
|
||||||
|
import { Input } from '@actual-app/components/input';
|
||||||
|
import { CSSProperties, styles } from '@actual-app/components/styles';
|
||||||
|
import { View } from '@actual-app/components/view';
|
||||||
|
|
||||||
|
type ModalButtonsProps = {
|
||||||
|
style?: CSSProperties;
|
||||||
|
leftContent?: ReactNode;
|
||||||
|
focusButton?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModalButtons = ({
|
||||||
|
style,
|
||||||
|
leftContent,
|
||||||
|
focusButton = false,
|
||||||
|
children,
|
||||||
|
}: ModalButtonsProps) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusButton && containerRef.current) {
|
||||||
|
const button = containerRef.current.querySelector<HTMLButtonElement>(
|
||||||
|
'button:not([data-hidden])',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [focusButton]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
innerRef={containerRef}
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginTop: 30,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{leftContent}
|
||||||
|
<View style={{ flex: 1 }} />
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModalHeaderProps = {
|
||||||
|
leftContent?: ReactNode;
|
||||||
|
showLogo?: boolean;
|
||||||
|
title?: ReactNode;
|
||||||
|
rightContent?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header used inside modals that centers an optional title and/or logo with
|
||||||
|
* optional left and right content pinned to the edges.
|
||||||
|
*
|
||||||
|
* The center area shows either a logo (when `showLogo` is true), a title (string/number
|
||||||
|
* rendered via ModalTitle), or a custom React node. `leftContent` and `rightContent`
|
||||||
|
* are rendered in absolutely-positioned regions at the left and right edges respectively.
|
||||||
|
*
|
||||||
|
* @param leftContent - Content rendered at the left edge of the header (optional).
|
||||||
|
* @param showLogo - When true, renders the app logo in the centered area.
|
||||||
|
* @param title - Title to display in the center. If a string or number, it's rendered with ModalTitle; otherwise the node is rendered as-is.
|
||||||
|
* @param rightContent - Content rendered at the right edge of the header (optional).
|
||||||
|
* @returns A JSX element representing the modal header.
|
||||||
|
*/
|
||||||
|
export function ModalHeader({
|
||||||
|
leftContent,
|
||||||
|
showLogo,
|
||||||
|
title,
|
||||||
|
rightContent,
|
||||||
|
}: ModalHeaderProps) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
role="heading"
|
||||||
|
style={{
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
height: 60,
|
||||||
|
flex: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{leftContent}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{(title || showLogo) && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
// We need to force a width for the text-overflow
|
||||||
|
// ellipses to work because we are aligning center.
|
||||||
|
width: 'calc(100% - 60px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showLogo && (
|
||||||
|
<SvgLogo
|
||||||
|
width={30}
|
||||||
|
height={30}
|
||||||
|
style={{ justifyContent: 'center', alignSelf: 'center' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{title &&
|
||||||
|
(typeof title === 'string' || typeof title === 'number' ? (
|
||||||
|
<ModalTitle title={`${title}`} />
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{rightContent && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rightContent}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModalTitleProps = {
|
||||||
|
title: string;
|
||||||
|
isEditable?: boolean;
|
||||||
|
getStyle?: (isEditing: boolean) => CSSProperties;
|
||||||
|
onEdit?: (isEditing: boolean) => void;
|
||||||
|
onTitleUpdate?: (newName: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a modal title that can be edited inline when enabled.
|
||||||
|
*
|
||||||
|
* Renders a centered, bold title. If `isEditable` is true, clicking the title switches it to an input field
|
||||||
|
* so the user can edit the text. Pressing Enter or completing the input will call `onTitleUpdate` with the
|
||||||
|
* new value only if it differs from the original, then exit edit mode. `getStyle` can supply additional
|
||||||
|
* style overrides based on whether the title is currently being edited.
|
||||||
|
*
|
||||||
|
* @param title - The current title text to display.
|
||||||
|
* @param isEditable - If true, the title becomes clickable and editable.
|
||||||
|
* @param getStyle - Optional function that receives `isEditing` and returns CSS overrides merged into the title/input.
|
||||||
|
* @param onTitleUpdate - Optional callback invoked with the new title when the user commits a change (only called if the value changed).
|
||||||
|
* @returns A JSX element: an Input when editing, otherwise a centered span showing the title.
|
||||||
|
*/
|
||||||
|
export function ModalTitle({
|
||||||
|
title,
|
||||||
|
isEditable,
|
||||||
|
getStyle,
|
||||||
|
onTitleUpdate,
|
||||||
|
}: ModalTitleProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
const onTitleClick = () => {
|
||||||
|
if (isEditable) {
|
||||||
|
setIsEditing(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _onTitleUpdate = (newTitle: string) => {
|
||||||
|
if (newTitle !== title) {
|
||||||
|
onTitleUpdate?.(newTitle);
|
||||||
|
}
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing) {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.scrollLeft = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isEditing]);
|
||||||
|
|
||||||
|
const style = getStyle?.(isEditing);
|
||||||
|
|
||||||
|
return isEditing ? (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
style={{
|
||||||
|
fontSize: 25,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: 'center',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
defaultValue={title}
|
||||||
|
onUpdate={_onTitleUpdate}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
_onTitleUpdate?.(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
onClick={onTitleClick}
|
||||||
|
style={{
|
||||||
|
fontSize: 25,
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: 'center',
|
||||||
|
...(isEditable && styles.underlinedText),
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModalCloseButtonProps = {
|
||||||
|
onPress?: () => void;
|
||||||
|
style?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A compact "close" button that renders a small delete icon inside a bare-styled Button.
|
||||||
|
*
|
||||||
|
* The `onPress` handler is forwarded to the Button. The `style` prop customizes the SVG icon's
|
||||||
|
* appearance (it is applied to the `SvgDelete`), not the Button itself. The Button uses fixed
|
||||||
|
* internal padding and the icon is rendered at a width of 10px.
|
||||||
|
*
|
||||||
|
* @param onPress - Optional callback invoked when the button is pressed.
|
||||||
|
* @param style - Optional CSS properties applied to the delete icon SVG.
|
||||||
|
* @returns A JSX element for use in modal headers or other compact UI areas.
|
||||||
|
*/
|
||||||
|
export function ModalCloseButton({ onPress, style }: ModalCloseButtonProps) {
|
||||||
|
return (
|
||||||
|
<Button variant="bare" onPress={onPress} style={{ padding: '10px 10px' }}>
|
||||||
|
<SvgDelete width={10} style={style} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
packages/plugins-core/src/client.ts
Normal file
74
packages/plugins-core/src/client.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// React Components (client-side only)
|
||||||
|
export { AlignedText } from '@actual-app/components/aligned-text';
|
||||||
|
export { Block } from '@actual-app/components/block';
|
||||||
|
export { Button, ButtonWithLoading } from '@actual-app/components/button';
|
||||||
|
export { Card } from '@actual-app/components/card';
|
||||||
|
export { FormError } from '@actual-app/components/form-error';
|
||||||
|
export { InitialFocus } from '@actual-app/components/initial-focus';
|
||||||
|
export { InlineField } from '@actual-app/components/inline-field';
|
||||||
|
export { Input } from '@actual-app/components/input';
|
||||||
|
export { Label } from '@actual-app/components/label';
|
||||||
|
export { Menu } from '@actual-app/components/menu';
|
||||||
|
export { Paragraph } from '@actual-app/components/paragraph';
|
||||||
|
export { Popover } from '@actual-app/components/popover';
|
||||||
|
export { Select } from '@actual-app/components/select';
|
||||||
|
export { SpaceBetween } from '@actual-app/components/space-between';
|
||||||
|
export { Stack } from '@actual-app/components/stack';
|
||||||
|
export { Text } from '@actual-app/components/text';
|
||||||
|
export { TextOneLine } from '@actual-app/components/text-one-line';
|
||||||
|
export { Toggle } from '@actual-app/components/toggle';
|
||||||
|
export { Tooltip } from '@actual-app/components/tooltip';
|
||||||
|
export { View } from '@actual-app/components/view';
|
||||||
|
|
||||||
|
// Modal Components (client-side only)
|
||||||
|
export {
|
||||||
|
ModalTitle,
|
||||||
|
ModalButtons,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
} from './BasicModalComponents';
|
||||||
|
|
||||||
|
// Client-side middleware
|
||||||
|
export { initializePlugin } from './middleware';
|
||||||
|
|
||||||
|
// Icons, styles, theme (client-side only)
|
||||||
|
export * from '@actual-app/components/icons/v2';
|
||||||
|
export * from '@actual-app/components/styles';
|
||||||
|
export * from '@actual-app/components/theme';
|
||||||
|
|
||||||
|
// Client-side hooks (React hooks)
|
||||||
|
export { useReport } from './utils';
|
||||||
|
|
||||||
|
// Query System (also needed on client-side for components)
|
||||||
|
export {
|
||||||
|
Query,
|
||||||
|
q,
|
||||||
|
getPrimaryOrderBy,
|
||||||
|
type QueryState,
|
||||||
|
type QueryBuilder,
|
||||||
|
type ObjectExpression,
|
||||||
|
} from './query';
|
||||||
|
|
||||||
|
// Spreadsheet types and utilities (client-side only)
|
||||||
|
export {
|
||||||
|
parametrizedField,
|
||||||
|
type SheetFields,
|
||||||
|
type Binding,
|
||||||
|
type SheetNames,
|
||||||
|
type Spreadsheets,
|
||||||
|
type BindingObject,
|
||||||
|
} from './spreadsheet';
|
||||||
|
|
||||||
|
// Client-side plugin types
|
||||||
|
export type {
|
||||||
|
ActualPlugin,
|
||||||
|
ActualPluginInitialized,
|
||||||
|
ThemeColorOverrides,
|
||||||
|
HostContext,
|
||||||
|
} from './types/actualPlugin';
|
||||||
|
|
||||||
|
export type { BasicModalProps } from '@actual-app/components/props/modalProps';
|
||||||
|
export type {
|
||||||
|
ActualPluginToolkit,
|
||||||
|
ActualPluginToolkitFunctions,
|
||||||
|
} from './types/toolkit';
|
||||||
13
packages/plugins-core/src/index.ts
Normal file
13
packages/plugins-core/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Main Entry Point for @actual-app/plugins-core
|
||||||
|
*
|
||||||
|
* Re-exports everything from both server and client exports.
|
||||||
|
* `server` must be used in `loot-core`
|
||||||
|
* `client` must be used in `desktop-client`
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Re-export all server-safe exports
|
||||||
|
export * from './server';
|
||||||
|
|
||||||
|
// Re-export all client-only exports
|
||||||
|
export * from './client';
|
||||||
152
packages/plugins-core/src/middleware.ts
Normal file
152
packages/plugins-core/src/middleware.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import { BasicModalProps } from '@actual-app/components/props/modalProps';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActualPlugin,
|
||||||
|
ActualPluginInitialized,
|
||||||
|
SidebarLocations,
|
||||||
|
} from './types/actualPlugin';
|
||||||
|
|
||||||
|
const containerRoots = new WeakMap<HTMLElement, Map<string, ReactDOM.Root>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a short random plugin identifier string.
|
||||||
|
*
|
||||||
|
* The returned value has the form `plugin-<random>` where `<random>` is
|
||||||
|
* a 10-character base-36 substring derived from Math.random().
|
||||||
|
*
|
||||||
|
* @returns A pseudo-random plugin id (not cryptographically unique).
|
||||||
|
*/
|
||||||
|
function generateRandomPluginId() {
|
||||||
|
return 'plugin-' + Math.random().toString(36).slice(2, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve or create a React root for a specific plugin within a host container.
|
||||||
|
*
|
||||||
|
* Returns a cached ReactDOM.Root associated with the given container and pluginId,
|
||||||
|
* creating and caching a new root if none exists. The mapping is stored in a
|
||||||
|
* per-container WeakMap so roots are reused for the same (container, pluginId)
|
||||||
|
* pair and can be garbage-collected with the container.
|
||||||
|
*
|
||||||
|
* @param container - Host DOM element that will host the React root.
|
||||||
|
* @param pluginId - Identifier for the plugin instance; roots are namespaced by this id.
|
||||||
|
* @returns The existing or newly created React root for the specified container and pluginId.
|
||||||
|
*/
|
||||||
|
function getOrCreateRoot(container: HTMLElement, pluginId: string) {
|
||||||
|
let pluginMap = containerRoots.get(container);
|
||||||
|
if (!pluginMap) {
|
||||||
|
pluginMap = new Map();
|
||||||
|
containerRoots.set(container, pluginMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = pluginMap.get(pluginId);
|
||||||
|
if (!root) {
|
||||||
|
root = ReactDOM.createRoot(container);
|
||||||
|
pluginMap.set(pluginId, root);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces an initialized plugin wrapper that mounts the plugin's UI into per-plugin React roots
|
||||||
|
* and forwards host resources.
|
||||||
|
*
|
||||||
|
* The returned plugin is a shallow copy of `plugin` with `initialized: true` and an `activate`
|
||||||
|
* implementation that:
|
||||||
|
* - installs `initReactI18next` into the plugin's i18n instance,
|
||||||
|
* - wraps the host activation context to forward host-provided resources (`db`, `q`, theme and utility
|
||||||
|
* helpers) and to replace UI registration helpers so provided React elements are rendered into
|
||||||
|
* per-container, per-plugin React roots (menus, modals, routes, dashboard widgets),
|
||||||
|
* - preserves and then calls the original `plugin.activate` with the wrapped context.
|
||||||
|
*
|
||||||
|
* @param plugin - The plugin to initialize.
|
||||||
|
* @param providedPluginId - Optional plugin identifier to use for scoping per-container React roots;
|
||||||
|
* when omitted a random plugin id is generated.
|
||||||
|
* @returns A plugin object marked `initialized: true` whose `activate` is wrapped to provide the
|
||||||
|
* augmented context and per-plugin rendering behavior.
|
||||||
|
*/
|
||||||
|
export function initializePlugin(
|
||||||
|
plugin: ActualPlugin,
|
||||||
|
providedPluginId?: string,
|
||||||
|
): ActualPluginInitialized {
|
||||||
|
const pluginId = providedPluginId || generateRandomPluginId();
|
||||||
|
|
||||||
|
const originalActivate = plugin.activate;
|
||||||
|
|
||||||
|
const newPlugin: ActualPluginInitialized = {
|
||||||
|
...plugin,
|
||||||
|
initialized: true,
|
||||||
|
activate: context => {
|
||||||
|
context.i18nInstance.use(initReactI18next);
|
||||||
|
|
||||||
|
const wrappedContext = {
|
||||||
|
...context,
|
||||||
|
|
||||||
|
// Database provided by host
|
||||||
|
db: context.db,
|
||||||
|
|
||||||
|
// Query builder passed through directly
|
||||||
|
q: context.q,
|
||||||
|
|
||||||
|
registerMenu(position: SidebarLocations, element: ReactElement) {
|
||||||
|
return context.registerMenu(position, container => {
|
||||||
|
const root = getOrCreateRoot(container, pluginId);
|
||||||
|
root.render(element);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
pushModal(element: ReactElement, modalProps?: BasicModalProps) {
|
||||||
|
context.pushModal(container => {
|
||||||
|
const root = getOrCreateRoot(container, pluginId);
|
||||||
|
root.render(element);
|
||||||
|
}, modalProps);
|
||||||
|
},
|
||||||
|
|
||||||
|
registerRoute(path: string, element: ReactElement) {
|
||||||
|
return context.registerRoute(path, container => {
|
||||||
|
const root = getOrCreateRoot(container, pluginId);
|
||||||
|
root.render(element);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
registerDashboardWidget(
|
||||||
|
widgetType: string,
|
||||||
|
displayName: string,
|
||||||
|
element: ReactElement,
|
||||||
|
options?: {
|
||||||
|
defaultWidth?: number;
|
||||||
|
defaultHeight?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return context.registerDashboardWidget(
|
||||||
|
widgetType,
|
||||||
|
displayName,
|
||||||
|
container => {
|
||||||
|
const root = getOrCreateRoot(container, pluginId);
|
||||||
|
root.render(element);
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Theme methods - passed through from host context
|
||||||
|
addTheme: context.addTheme,
|
||||||
|
overrideTheme: context.overrideTheme,
|
||||||
|
|
||||||
|
// Report and spreadsheet utilities - passed through from host context
|
||||||
|
createSpreadsheet: context.createSpreadsheet,
|
||||||
|
makeFilters: context.makeFilters,
|
||||||
|
};
|
||||||
|
|
||||||
|
originalActivate(wrappedContext);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return newPlugin;
|
||||||
|
}
|
||||||
215
packages/plugins-core/src/query/index.ts
Normal file
215
packages/plugins-core/src/query/index.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Query System - Single Source of Truth
|
||||||
|
*
|
||||||
|
* This is the main query implementation used by both loot-core and plugins.
|
||||||
|
* No more conversion functions needed!
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WithRequired } from '../types/util';
|
||||||
|
|
||||||
|
export type ObjectExpression = {
|
||||||
|
[key: string]: ObjectExpression | unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryState = {
|
||||||
|
get table(): string;
|
||||||
|
get tableOptions(): Readonly<Record<string, unknown>>;
|
||||||
|
get filterExpressions(): ReadonlyArray<ObjectExpression>;
|
||||||
|
get selectExpressions(): ReadonlyArray<ObjectExpression | string | '*'>;
|
||||||
|
get groupExpressions(): ReadonlyArray<ObjectExpression | string>;
|
||||||
|
get orderExpressions(): ReadonlyArray<ObjectExpression | string>;
|
||||||
|
get calculation(): boolean;
|
||||||
|
get rawMode(): boolean;
|
||||||
|
get withDead(): boolean;
|
||||||
|
get validateRefs(): boolean;
|
||||||
|
get limit(): number | null;
|
||||||
|
get offset(): number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Query {
|
||||||
|
state: QueryState;
|
||||||
|
|
||||||
|
constructor(state: WithRequired<Partial<QueryState>, 'table'>) {
|
||||||
|
this.state = {
|
||||||
|
tableOptions: state.tableOptions || {},
|
||||||
|
filterExpressions: state.filterExpressions || [],
|
||||||
|
selectExpressions: state.selectExpressions || [],
|
||||||
|
groupExpressions: state.groupExpressions || [],
|
||||||
|
orderExpressions: state.orderExpressions || [],
|
||||||
|
calculation: false,
|
||||||
|
rawMode: false,
|
||||||
|
withDead: false,
|
||||||
|
validateRefs: true,
|
||||||
|
limit: null,
|
||||||
|
offset: null,
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
filter(expr: ObjectExpression) {
|
||||||
|
return new Query({
|
||||||
|
...this.state,
|
||||||
|
filterExpressions: [...this.state.filterExpressions, expr],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unfilter(exprs?: Array<keyof ObjectExpression>) {
|
||||||
|
// Remove all filters if no arguments are passed
|
||||||
|
if (!exprs) {
|
||||||
|
return new Query({
|
||||||
|
...this.state,
|
||||||
|
filterExpressions: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const exprSet = new Set(exprs);
|
||||||
|
return new Query({
|
||||||
|
...this.state,
|
||||||
|
filterExpressions: this.state.filterExpressions.filter(
|
||||||
|
expr => !exprSet.has(Object.keys(expr)[0]),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
select(
|
||||||
|
exprs:
|
||||||
|
| Array<ObjectExpression | string>
|
||||||
|
| ObjectExpression
|
||||||
|
| string
|
||||||
|
| '*'
|
||||||
|
| ['*'] = [],
|
||||||
|
) {
|
||||||
|
if (!Array.isArray(exprs)) {
|
||||||
|
exprs = [exprs];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Query({
|
||||||
|
...this.state,
|
||||||
|
selectExpressions: exprs,
|
||||||
|
calculation: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate(expr: ObjectExpression | string) {
|
||||||
|
return new Query({
|
||||||
|
...this.state,
|
||||||
|
selectExpressions: [{ result: expr }],
|
||||||
|
calculation: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
groupBy(exprs: ObjectExpression | string | Array<ObjectExpression | string>) {
|
||||||
|
if (!Array.isArray(exprs)) {
|
||||||
|
exprs = [exprs];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Query({
|
||||||
|
...this.state,
|
||||||
|
groupExpressions: [...this.state.groupExpressions, ...exprs],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
orderBy(exprs: ObjectExpression | string | Array<ObjectExpression | string>) {
|
||||||
|
if (!Array.isArray(exprs)) {
|
||||||
|
exprs = [exprs];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Query({
|
||||||
|
...this.state,
|
||||||
|
orderExpressions: [...this.state.orderExpressions, ...exprs],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
limit(num: number) {
|
||||||
|
return new Query({ ...this.state, limit: num });
|
||||||
|
}
|
||||||
|
|
||||||
|
offset(num: number) {
|
||||||
|
return new Query({ ...this.state, offset: num });
|
||||||
|
}
|
||||||
|
|
||||||
|
raw() {
|
||||||
|
return new Query({ ...this.state, rawMode: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
withDead() {
|
||||||
|
return new Query({ ...this.state, withDead: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
withoutValidatedRefs() {
|
||||||
|
return new Query({ ...this.state, validateRefs: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
options(opts: Record<string, unknown>) {
|
||||||
|
return new Query({ ...this.state, tableOptions: opts });
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
return q(this.state.table);
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeAsString() {
|
||||||
|
return JSON.stringify(this.serialize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new immutable Query preconfigured for the given table.
|
||||||
|
*
|
||||||
|
* @param table - The table name to build the query for.
|
||||||
|
* @returns A new Query instance whose state.table is set to `table`.
|
||||||
|
*/
|
||||||
|
export function q(table: QueryState['table']) {
|
||||||
|
return new Query({ table });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query builder type for use in contexts
|
||||||
|
*/
|
||||||
|
export interface QueryBuilder {
|
||||||
|
(table: string): Query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives the primary ORDER BY clause for a Query.
|
||||||
|
*
|
||||||
|
* If the query has any order expressions, the first one is normalized into a simple
|
||||||
|
* descriptor. If the first expression is a string it is treated as the field name
|
||||||
|
* with ascending order. If it is an object, the first key is used as the field
|
||||||
|
* and its value as the order.
|
||||||
|
*
|
||||||
|
* When the query has no order expressions and `defaultOrderBy` is provided, the
|
||||||
|
* function returns an object produced by merging `{ order: 'asc' }` with
|
||||||
|
* `defaultOrderBy` (so the default order is ascending unless overridden by the
|
||||||
|
* spread object). If there is no order information and no default, `null` is
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* @param query - The Query instance to inspect.
|
||||||
|
* @param defaultOrderBy - Fallback object expression to use when the query has no order expressions; may be `null`.
|
||||||
|
* @returns An object describing the primary order (commonly `{ field: string, order: 'asc' | 'desc' }`,
|
||||||
|
* or a merged object when `defaultOrderBy` is used), or `null` if no ordering is available.
|
||||||
|
*/
|
||||||
|
export function getPrimaryOrderBy(
|
||||||
|
query: Query,
|
||||||
|
defaultOrderBy: ObjectExpression | null,
|
||||||
|
) {
|
||||||
|
const orderExprs = query.serialize().orderExpressions;
|
||||||
|
if (orderExprs.length === 0) {
|
||||||
|
if (defaultOrderBy) {
|
||||||
|
return { order: 'asc', ...defaultOrderBy };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstOrder = orderExprs[0];
|
||||||
|
if (typeof firstOrder === 'string') {
|
||||||
|
return { field: firstOrder, order: 'asc' };
|
||||||
|
}
|
||||||
|
// Handle this form: { field: 'desc' }
|
||||||
|
const [field] = Object.keys(firstOrder);
|
||||||
|
return { field, order: firstOrder[field] };
|
||||||
|
}
|
||||||
53
packages/plugins-core/src/server.ts
Normal file
53
packages/plugins-core/src/server.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Server-Only Exports for @actual-app/plugins-core
|
||||||
|
*
|
||||||
|
* This file contains only types and utilities that can be used in server environments
|
||||||
|
* (Web Workers, Node.js) without any DOM dependencies or React components.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Database types
|
||||||
|
export type {
|
||||||
|
SqlParameter,
|
||||||
|
DatabaseQueryResult,
|
||||||
|
DatabaseRow,
|
||||||
|
DatabaseSelectResult,
|
||||||
|
DatabaseResult,
|
||||||
|
DatabaseOperation,
|
||||||
|
PluginMetadata,
|
||||||
|
} from './types/database';
|
||||||
|
|
||||||
|
// Plugin file types
|
||||||
|
export type { PluginFile, PluginFileCollection } from './types/plugin-files';
|
||||||
|
|
||||||
|
// AQL query result types
|
||||||
|
export type { AQLQueryResult, AQLQueryOptions } from './types/aql-result';
|
||||||
|
|
||||||
|
// Model types (server-safe)
|
||||||
|
export type * from './types/models';
|
||||||
|
|
||||||
|
// Plugin types (server-safe ones)
|
||||||
|
export type {
|
||||||
|
PluginDatabase,
|
||||||
|
PluginSpreadsheet,
|
||||||
|
PluginBinding,
|
||||||
|
PluginCellValue,
|
||||||
|
PluginFilterCondition,
|
||||||
|
PluginFilterResult,
|
||||||
|
PluginConditionValue,
|
||||||
|
PluginMigration,
|
||||||
|
PluginContext,
|
||||||
|
ContextEvent,
|
||||||
|
} from './types/actualPlugin';
|
||||||
|
|
||||||
|
export type { ActualPluginEntry } from './types/actualPluginEntry';
|
||||||
|
export type { ActualPluginManifest } from './types/actualPluginManifest';
|
||||||
|
|
||||||
|
// Query System (server-safe)
|
||||||
|
export {
|
||||||
|
Query,
|
||||||
|
q,
|
||||||
|
getPrimaryOrderBy,
|
||||||
|
type QueryState,
|
||||||
|
type QueryBuilder,
|
||||||
|
type ObjectExpression,
|
||||||
|
} from './query';
|
||||||
127
packages/plugins-core/src/spreadsheet.ts
Normal file
127
packages/plugins-core/src/spreadsheet.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Spreadsheet Types and Utilities
|
||||||
|
*
|
||||||
|
* Core spreadsheet schema definitions and binding utilities used across
|
||||||
|
* the application for managing financial data in sheet-like structures.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type Query } from './query';
|
||||||
|
|
||||||
|
export type Spreadsheets = {
|
||||||
|
account: {
|
||||||
|
// Common fields
|
||||||
|
'uncategorized-amount': number;
|
||||||
|
'uncategorized-balance': number;
|
||||||
|
|
||||||
|
// Account fields
|
||||||
|
balance: number;
|
||||||
|
[key: `balance-${string}-cleared`]: number | null;
|
||||||
|
'accounts-balance': number;
|
||||||
|
'onbudget-accounts-balance': number;
|
||||||
|
'offbudget-accounts-balance': number;
|
||||||
|
'closed-accounts-balance': number;
|
||||||
|
balanceCleared: number;
|
||||||
|
balanceUncleared: number;
|
||||||
|
lastReconciled: string | null;
|
||||||
|
};
|
||||||
|
category: {
|
||||||
|
// Common fields
|
||||||
|
'uncategorized-amount': number;
|
||||||
|
'uncategorized-balance': number;
|
||||||
|
|
||||||
|
balance: number;
|
||||||
|
balanceCleared: number;
|
||||||
|
balanceUncleared: number;
|
||||||
|
};
|
||||||
|
'envelope-budget': {
|
||||||
|
// Common fields
|
||||||
|
'uncategorized-amount': number;
|
||||||
|
'uncategorized-balance': number;
|
||||||
|
|
||||||
|
// Envelope budget fields
|
||||||
|
'available-funds': number;
|
||||||
|
'last-month-overspent': number;
|
||||||
|
buffered: number;
|
||||||
|
'buffered-auto': number;
|
||||||
|
'buffered-selected': number;
|
||||||
|
'to-budget': number | null;
|
||||||
|
'from-last-month': number;
|
||||||
|
'total-budgeted': number;
|
||||||
|
'total-income': number;
|
||||||
|
'total-spent': number;
|
||||||
|
'total-leftover': number;
|
||||||
|
'group-sum-amount': number;
|
||||||
|
'group-budget': number;
|
||||||
|
'group-leftover': number;
|
||||||
|
budget: number;
|
||||||
|
'sum-amount': number;
|
||||||
|
leftover: number;
|
||||||
|
carryover: number;
|
||||||
|
goal: number;
|
||||||
|
'long-goal': number;
|
||||||
|
};
|
||||||
|
'tracking-budget': {
|
||||||
|
// Common fields
|
||||||
|
'uncategorized-amount': number;
|
||||||
|
'uncategorized-balance': number;
|
||||||
|
|
||||||
|
// Tracking budget fields
|
||||||
|
'total-budgeted': number;
|
||||||
|
'total-budget-income': number;
|
||||||
|
'total-saved': number;
|
||||||
|
'total-income': number;
|
||||||
|
'total-spent': number;
|
||||||
|
'real-saved': number;
|
||||||
|
'total-leftover': number;
|
||||||
|
'group-sum-amount': number;
|
||||||
|
'group-budget': number;
|
||||||
|
'group-leftover': number;
|
||||||
|
budget: number;
|
||||||
|
'sum-amount': number;
|
||||||
|
leftover: number;
|
||||||
|
carryover: number;
|
||||||
|
goal: number;
|
||||||
|
'long-goal': number;
|
||||||
|
};
|
||||||
|
[`balance`]: {
|
||||||
|
// Common fields
|
||||||
|
'uncategorized-amount': number;
|
||||||
|
'uncategorized-balance': number;
|
||||||
|
|
||||||
|
// Balance fields
|
||||||
|
[key: `balance-query-${string}`]: number;
|
||||||
|
[key: `selected-transactions-${string}`]: Array<{ id: string }>;
|
||||||
|
[key: `selected-balance-${string}`]: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SheetNames = keyof Spreadsheets & string;
|
||||||
|
|
||||||
|
export type SheetFields<SheetName extends SheetNames> =
|
||||||
|
keyof Spreadsheets[SheetName] & string;
|
||||||
|
|
||||||
|
export type BindingObject<
|
||||||
|
SheetName extends SheetNames,
|
||||||
|
SheetFieldName extends SheetFields<SheetName>,
|
||||||
|
> = {
|
||||||
|
name: SheetFieldName;
|
||||||
|
value?: Spreadsheets[SheetName][SheetFieldName] | undefined;
|
||||||
|
query?: Query | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Binding<
|
||||||
|
SheetName extends SheetNames,
|
||||||
|
SheetFieldName extends SheetFields<SheetName>,
|
||||||
|
> =
|
||||||
|
| SheetFieldName
|
||||||
|
| {
|
||||||
|
name: SheetFieldName;
|
||||||
|
value?: Spreadsheets[SheetName][SheetFieldName] | undefined;
|
||||||
|
query?: Query | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parametrizedField =
|
||||||
|
<SheetName extends SheetNames>() =>
|
||||||
|
<SheetFieldName extends SheetFields<SheetName>>(field: SheetFieldName) =>
|
||||||
|
(id?: string): SheetFieldName =>
|
||||||
|
`${field}-${id}` as SheetFieldName;
|
||||||
517
packages/plugins-core/src/types/actualPlugin.ts
Normal file
517
packages/plugins-core/src/types/actualPlugin.ts
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
import { ReactElement } from 'react';
|
||||||
|
|
||||||
|
import type { BasicModalProps } from '@actual-app/components/props/modalProps';
|
||||||
|
import type { i18n } from 'i18next';
|
||||||
|
|
||||||
|
import { Query, QueryBuilder } from '../query';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
PayeeEntity,
|
||||||
|
CategoryEntity,
|
||||||
|
CategoryGroupEntity,
|
||||||
|
AccountEntity,
|
||||||
|
ScheduleEntity,
|
||||||
|
} from './models';
|
||||||
|
|
||||||
|
export type SidebarLocations =
|
||||||
|
| 'main-menu'
|
||||||
|
| 'more-menu'
|
||||||
|
| 'before-accounts'
|
||||||
|
| 'after-accounts'
|
||||||
|
| 'topbar';
|
||||||
|
|
||||||
|
// Define condition value types for filtering
|
||||||
|
export type PluginConditionValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| Array<string | number>
|
||||||
|
| { num1: number; num2: number };
|
||||||
|
|
||||||
|
export type PluginFilterCondition = {
|
||||||
|
field: string;
|
||||||
|
op: string;
|
||||||
|
value: PluginConditionValue;
|
||||||
|
type?: string;
|
||||||
|
customName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginFilterResult = {
|
||||||
|
filters: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simple color mapping type for theme methods
|
||||||
|
export type ThemeColorOverrides = {
|
||||||
|
// Page colors
|
||||||
|
pageBackground?: string;
|
||||||
|
pageBackgroundModalActive?: string;
|
||||||
|
pageBackgroundTopLeft?: string;
|
||||||
|
pageBackgroundBottomRight?: string;
|
||||||
|
pageBackgroundLineTop?: string;
|
||||||
|
pageBackgroundLineMid?: string;
|
||||||
|
pageBackgroundLineBottom?: string;
|
||||||
|
pageText?: string;
|
||||||
|
pageTextLight?: string;
|
||||||
|
pageTextSubdued?: string;
|
||||||
|
pageTextDark?: string;
|
||||||
|
pageTextPositive?: string;
|
||||||
|
pageTextLink?: string;
|
||||||
|
pageTextLinkLight?: string;
|
||||||
|
|
||||||
|
// Card colors
|
||||||
|
cardBackground?: string;
|
||||||
|
cardBorder?: string;
|
||||||
|
cardShadow?: string;
|
||||||
|
|
||||||
|
// Table colors
|
||||||
|
tableBackground?: string;
|
||||||
|
tableRowBackgroundHover?: string;
|
||||||
|
tableText?: string;
|
||||||
|
tableTextLight?: string;
|
||||||
|
tableTextSubdued?: string;
|
||||||
|
tableTextSelected?: string;
|
||||||
|
tableTextHover?: string;
|
||||||
|
tableTextInactive?: string;
|
||||||
|
tableHeaderText?: string;
|
||||||
|
tableHeaderBackground?: string;
|
||||||
|
tableBorder?: string;
|
||||||
|
tableBorderSelected?: string;
|
||||||
|
tableBorderHover?: string;
|
||||||
|
tableBorderSeparator?: string;
|
||||||
|
tableRowBackgroundHighlight?: string;
|
||||||
|
tableRowBackgroundHighlightText?: string;
|
||||||
|
tableRowHeaderBackground?: string;
|
||||||
|
tableRowHeaderText?: string;
|
||||||
|
|
||||||
|
// Sidebar colors
|
||||||
|
sidebarBackground?: string;
|
||||||
|
sidebarItemBackgroundPending?: string;
|
||||||
|
sidebarItemBackgroundPositive?: string;
|
||||||
|
sidebarItemBackgroundFailed?: string;
|
||||||
|
sidebarItemBackgroundHover?: string;
|
||||||
|
sidebarItemAccentSelected?: string;
|
||||||
|
sidebarItemText?: string;
|
||||||
|
sidebarItemTextSelected?: string;
|
||||||
|
|
||||||
|
// Menu colors
|
||||||
|
menuBackground?: string;
|
||||||
|
menuItemBackground?: string;
|
||||||
|
menuItemBackgroundHover?: string;
|
||||||
|
menuItemText?: string;
|
||||||
|
menuItemTextHover?: string;
|
||||||
|
menuItemTextSelected?: string;
|
||||||
|
menuItemTextHeader?: string;
|
||||||
|
menuBorder?: string;
|
||||||
|
menuBorderHover?: string;
|
||||||
|
menuKeybindingText?: string;
|
||||||
|
menuAutoCompleteBackground?: string;
|
||||||
|
menuAutoCompleteBackgroundHover?: string;
|
||||||
|
menuAutoCompleteText?: string;
|
||||||
|
menuAutoCompleteTextHover?: string;
|
||||||
|
menuAutoCompleteTextHeader?: string;
|
||||||
|
menuAutoCompleteItemTextHover?: string;
|
||||||
|
menuAutoCompleteItemText?: string;
|
||||||
|
|
||||||
|
// Modal colors
|
||||||
|
modalBackground?: string;
|
||||||
|
modalBorder?: string;
|
||||||
|
|
||||||
|
// Mobile colors
|
||||||
|
mobileHeaderBackground?: string;
|
||||||
|
mobileHeaderText?: string;
|
||||||
|
mobileHeaderTextSubdued?: string;
|
||||||
|
mobileHeaderTextHover?: string;
|
||||||
|
mobilePageBackground?: string;
|
||||||
|
mobileNavBackground?: string;
|
||||||
|
mobileNavItem?: string;
|
||||||
|
mobileNavItemSelected?: string;
|
||||||
|
mobileAccountShadow?: string;
|
||||||
|
mobileAccountText?: string;
|
||||||
|
mobileTransactionSelected?: string;
|
||||||
|
mobileViewTheme?: string;
|
||||||
|
mobileConfigServerViewTheme?: string;
|
||||||
|
|
||||||
|
// Markdown colors
|
||||||
|
markdownNormal?: string;
|
||||||
|
markdownDark?: string;
|
||||||
|
markdownLight?: string;
|
||||||
|
|
||||||
|
// Button colors - Menu buttons
|
||||||
|
buttonMenuText?: string;
|
||||||
|
buttonMenuTextHover?: string;
|
||||||
|
buttonMenuBackground?: string;
|
||||||
|
buttonMenuBackgroundHover?: string;
|
||||||
|
buttonMenuBorder?: string;
|
||||||
|
buttonMenuSelectedText?: string;
|
||||||
|
buttonMenuSelectedTextHover?: string;
|
||||||
|
buttonMenuSelectedBackground?: string;
|
||||||
|
buttonMenuSelectedBackgroundHover?: string;
|
||||||
|
buttonMenuSelectedBorder?: string;
|
||||||
|
|
||||||
|
// Button colors - Primary buttons
|
||||||
|
buttonPrimaryText?: string;
|
||||||
|
buttonPrimaryTextHover?: string;
|
||||||
|
buttonPrimaryBackground?: string;
|
||||||
|
buttonPrimaryBackgroundHover?: string;
|
||||||
|
buttonPrimaryBorder?: string;
|
||||||
|
buttonPrimaryShadow?: string;
|
||||||
|
buttonPrimaryDisabledText?: string;
|
||||||
|
buttonPrimaryDisabledBackground?: string;
|
||||||
|
buttonPrimaryDisabledBorder?: string;
|
||||||
|
|
||||||
|
// Button colors - Normal buttons
|
||||||
|
buttonNormalText?: string;
|
||||||
|
buttonNormalTextHover?: string;
|
||||||
|
buttonNormalBackground?: string;
|
||||||
|
buttonNormalBackgroundHover?: string;
|
||||||
|
buttonNormalBorder?: string;
|
||||||
|
buttonNormalShadow?: string;
|
||||||
|
buttonNormalSelectedText?: string;
|
||||||
|
buttonNormalSelectedBackground?: string;
|
||||||
|
buttonNormalDisabledText?: string;
|
||||||
|
buttonNormalDisabledBackground?: string;
|
||||||
|
buttonNormalDisabledBorder?: string;
|
||||||
|
|
||||||
|
// Button colors - Bare buttons
|
||||||
|
buttonBareText?: string;
|
||||||
|
buttonBareTextHover?: string;
|
||||||
|
buttonBareBackground?: string;
|
||||||
|
buttonBareBackgroundHover?: string;
|
||||||
|
buttonBareBackgroundActive?: string;
|
||||||
|
buttonBareDisabledText?: string;
|
||||||
|
buttonBareDisabledBackground?: string;
|
||||||
|
|
||||||
|
// Calendar colors
|
||||||
|
calendarText?: string;
|
||||||
|
calendarBackground?: string;
|
||||||
|
calendarItemText?: string;
|
||||||
|
calendarItemBackground?: string;
|
||||||
|
calendarSelectedBackground?: string;
|
||||||
|
calendarCellBackground?: string;
|
||||||
|
|
||||||
|
// Status colors - Notice
|
||||||
|
noticeBackground?: string;
|
||||||
|
noticeBackgroundLight?: string;
|
||||||
|
noticeBackgroundDark?: string;
|
||||||
|
noticeText?: string;
|
||||||
|
noticeTextLight?: string;
|
||||||
|
noticeTextDark?: string;
|
||||||
|
noticeTextMenu?: string;
|
||||||
|
noticeTextMenuHover?: string;
|
||||||
|
noticeBorder?: string;
|
||||||
|
|
||||||
|
// Status colors - Warning
|
||||||
|
warningBackground?: string;
|
||||||
|
warningText?: string;
|
||||||
|
warningTextLight?: string;
|
||||||
|
warningTextDark?: string;
|
||||||
|
warningBorder?: string;
|
||||||
|
|
||||||
|
// Status colors - Error
|
||||||
|
errorBackground?: string;
|
||||||
|
errorText?: string;
|
||||||
|
errorTextDark?: string;
|
||||||
|
errorTextDarker?: string;
|
||||||
|
errorTextMenu?: string;
|
||||||
|
errorBorder?: string;
|
||||||
|
|
||||||
|
// Status colors - Upcoming
|
||||||
|
upcomingBackground?: string;
|
||||||
|
upcomingText?: string;
|
||||||
|
upcomingBorder?: string;
|
||||||
|
|
||||||
|
// Form colors
|
||||||
|
formLabelText?: string;
|
||||||
|
formLabelBackground?: string;
|
||||||
|
formInputBackground?: string;
|
||||||
|
formInputBackgroundSelected?: string;
|
||||||
|
formInputBackgroundSelection?: string;
|
||||||
|
formInputBorder?: string;
|
||||||
|
formInputTextReadOnlySelection?: string;
|
||||||
|
formInputBorderSelected?: string;
|
||||||
|
formInputText?: string;
|
||||||
|
formInputTextSelected?: string;
|
||||||
|
formInputTextPlaceholder?: string;
|
||||||
|
formInputTextPlaceholderSelected?: string;
|
||||||
|
formInputTextSelection?: string;
|
||||||
|
formInputShadowSelected?: string;
|
||||||
|
formInputTextHighlight?: string;
|
||||||
|
|
||||||
|
// Checkbox colors
|
||||||
|
checkboxText?: string;
|
||||||
|
checkboxBackgroundSelected?: string;
|
||||||
|
checkboxBorderSelected?: string;
|
||||||
|
checkboxShadowSelected?: string;
|
||||||
|
checkboxToggleBackground?: string;
|
||||||
|
checkboxToggleBackgroundSelected?: string;
|
||||||
|
checkboxToggleDisabled?: string;
|
||||||
|
|
||||||
|
// Pill colors
|
||||||
|
pillBackground?: string;
|
||||||
|
pillBackgroundLight?: string;
|
||||||
|
pillText?: string;
|
||||||
|
pillTextHighlighted?: string;
|
||||||
|
pillBorder?: string;
|
||||||
|
pillBorderDark?: string;
|
||||||
|
pillBackgroundSelected?: string;
|
||||||
|
pillTextSelected?: string;
|
||||||
|
pillBorderSelected?: string;
|
||||||
|
pillTextSubdued?: string;
|
||||||
|
|
||||||
|
// Reports colors
|
||||||
|
reportsRed?: string;
|
||||||
|
reportsBlue?: string;
|
||||||
|
reportsGreen?: string;
|
||||||
|
reportsGray?: string;
|
||||||
|
reportsLabel?: string;
|
||||||
|
reportsInnerLabel?: string;
|
||||||
|
|
||||||
|
// Note tag colors
|
||||||
|
noteTagBackground?: string;
|
||||||
|
noteTagBackgroundHover?: string;
|
||||||
|
noteTagText?: string;
|
||||||
|
|
||||||
|
// Budget colors
|
||||||
|
budgetCurrentMonth?: string;
|
||||||
|
budgetOtherMonth?: string;
|
||||||
|
budgetHeaderCurrentMonth?: string;
|
||||||
|
budgetHeaderOtherMonth?: string;
|
||||||
|
|
||||||
|
// Floating action bar colors
|
||||||
|
floatingActionBarBackground?: string;
|
||||||
|
floatingActionBarBorder?: string;
|
||||||
|
floatingActionBarText?: string;
|
||||||
|
|
||||||
|
// Tooltip colors
|
||||||
|
tooltipText?: string;
|
||||||
|
tooltipBackground?: string;
|
||||||
|
tooltipBorder?: string;
|
||||||
|
|
||||||
|
// Custom colors (plugin-specific)
|
||||||
|
[customColor: `custom-${string}`]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PluginDatabase {
|
||||||
|
runQuery<T = unknown>(
|
||||||
|
sql: string,
|
||||||
|
params?: (string | number)[],
|
||||||
|
fetchAll?: boolean,
|
||||||
|
): Promise<T[] | { changes: number; insertId?: number }>;
|
||||||
|
|
||||||
|
execQuery(sql: string): void;
|
||||||
|
|
||||||
|
transaction(fn: () => void): void;
|
||||||
|
|
||||||
|
getMigrationState(): Promise<string[]>;
|
||||||
|
|
||||||
|
setMetadata(key: string, value: string): Promise<void>;
|
||||||
|
|
||||||
|
getMetadata(key: string): Promise<string | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an AQL (Actual Query Language) query.
|
||||||
|
* This provides a higher-level abstraction over SQL that's consistent with Actual Budget's query system.
|
||||||
|
*
|
||||||
|
* @param query - The AQL query (can be a PluginQuery object or serialized PluginQueryState)
|
||||||
|
* @param options - Optional parameters for the query
|
||||||
|
* @param options.target - Target database: 'plugin' for plugin tables, 'host' for main app tables. Defaults to 'plugin'
|
||||||
|
* @param options.params - Named parameters for the query
|
||||||
|
* @returns Promise that resolves to the query result with data and dependencies
|
||||||
|
*/
|
||||||
|
aql(
|
||||||
|
query: Query,
|
||||||
|
options?: {
|
||||||
|
target?: 'plugin' | 'host';
|
||||||
|
params?: Record<string, unknown>;
|
||||||
|
},
|
||||||
|
): Promise<{ data: unknown; dependencies: string[] }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginBinding = string | { name: string; query?: Query };
|
||||||
|
|
||||||
|
export type PluginCellValue = { name: string; value: unknown | null };
|
||||||
|
|
||||||
|
export interface PluginSpreadsheet {
|
||||||
|
/**
|
||||||
|
* Bind to a cell and observe changes
|
||||||
|
* @param sheetName - Name of the sheet (optional, defaults to global)
|
||||||
|
* @param binding - Cell binding (string name or object with name and optional query)
|
||||||
|
* @param callback - Function called when cell value changes
|
||||||
|
* @returns Cleanup function to stop observing
|
||||||
|
*/
|
||||||
|
bind(
|
||||||
|
sheetName: string | undefined,
|
||||||
|
binding: PluginBinding,
|
||||||
|
callback: (node: PluginCellValue) => void,
|
||||||
|
): () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a cell value directly
|
||||||
|
* @param sheetName - Name of the sheet
|
||||||
|
* @param name - Cell name
|
||||||
|
* @returns Promise that resolves to the cell value
|
||||||
|
*/
|
||||||
|
get(sheetName: string, name: string): Promise<PluginCellValue>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all cell names in a sheet
|
||||||
|
* @param sheetName - Name of the sheet
|
||||||
|
* @returns Promise that resolves to array of cell names
|
||||||
|
*/
|
||||||
|
getCellNames(sheetName: string): Promise<string[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a query in a sheet
|
||||||
|
* @param sheetName - Name of the sheet
|
||||||
|
* @param name - Query name
|
||||||
|
* @param query - The query to create
|
||||||
|
* @returns Promise that resolves when query is created
|
||||||
|
*/
|
||||||
|
createQuery(sheetName: string, name: string, query: Query): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginMigration = [
|
||||||
|
timestamp: number,
|
||||||
|
name: string,
|
||||||
|
upCommand: string,
|
||||||
|
downCommand: string,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Plugin context type for easier reuse
|
||||||
|
export type PluginContext = Omit<
|
||||||
|
HostContext,
|
||||||
|
'registerMenu' | 'pushModal' | 'registerRoute' | 'registerDashboardWidget'
|
||||||
|
> & {
|
||||||
|
registerMenu: (location: SidebarLocations, element: ReactElement) => string;
|
||||||
|
pushModal: (element: ReactElement, modalProps?: BasicModalProps) => void;
|
||||||
|
registerRoute: (path: string, routeElement: ReactElement) => string;
|
||||||
|
|
||||||
|
// Dashboard widget registration - wrapped for JSX elements
|
||||||
|
registerDashboardWidget: (
|
||||||
|
widgetType: string,
|
||||||
|
displayName: string,
|
||||||
|
element: ReactElement,
|
||||||
|
options?: {
|
||||||
|
defaultWidth?: number;
|
||||||
|
defaultHeight?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
},
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
// Theme methods - simple and direct
|
||||||
|
addTheme: (
|
||||||
|
themeId: string,
|
||||||
|
displayName: string,
|
||||||
|
colorOverrides: ThemeColorOverrides,
|
||||||
|
options?: {
|
||||||
|
baseTheme?: 'light' | 'dark' | 'midnight';
|
||||||
|
description?: string;
|
||||||
|
},
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
overrideTheme: (
|
||||||
|
themeId: 'light' | 'dark' | 'midnight' | string,
|
||||||
|
colorOverrides: ThemeColorOverrides,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
db?: PluginDatabase;
|
||||||
|
q: QueryBuilder;
|
||||||
|
|
||||||
|
// Report and spreadsheet utilities
|
||||||
|
createSpreadsheet: () => PluginSpreadsheet;
|
||||||
|
|
||||||
|
makeFilters: (
|
||||||
|
conditions: Array<PluginFilterCondition>,
|
||||||
|
) => Promise<PluginFilterResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ActualPlugin {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
uninstall: () => void;
|
||||||
|
migrations?: () => PluginMigration[];
|
||||||
|
activate: (context: PluginContext) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActualPluginInitialized = Omit<ActualPlugin, 'activate'> & {
|
||||||
|
initialized: true;
|
||||||
|
activate: (context: HostContext & { db: PluginDatabase }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ContextEvent {
|
||||||
|
payess: { payess: PayeeEntity[] };
|
||||||
|
categories: { categories: CategoryEntity[]; groups: CategoryGroupEntity[] };
|
||||||
|
accounts: { accounts: AccountEntity[] };
|
||||||
|
schedules: { schedules: ScheduleEntity[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HostContext {
|
||||||
|
navigate: (routePath: string) => void;
|
||||||
|
|
||||||
|
pushModal: (
|
||||||
|
parameter: (container: HTMLDivElement) => void,
|
||||||
|
modalProps?: BasicModalProps,
|
||||||
|
) => void;
|
||||||
|
popModal: () => void;
|
||||||
|
|
||||||
|
registerRoute: (
|
||||||
|
path: string,
|
||||||
|
routeElement: (container: HTMLDivElement) => void,
|
||||||
|
) => string;
|
||||||
|
unregisterRoute: (id: string) => void;
|
||||||
|
|
||||||
|
registerMenu: (
|
||||||
|
location: SidebarLocations,
|
||||||
|
parameter: (container: HTMLDivElement) => void,
|
||||||
|
) => string;
|
||||||
|
unregisterMenu: (id: string) => void;
|
||||||
|
|
||||||
|
on: <K extends keyof ContextEvent>(
|
||||||
|
eventType: K,
|
||||||
|
callback: (data: ContextEvent[K]) => void,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
// Dashboard widget methods
|
||||||
|
registerDashboardWidget: (
|
||||||
|
widgetType: string,
|
||||||
|
displayName: string,
|
||||||
|
renderWidget: (container: HTMLDivElement) => void,
|
||||||
|
options?: {
|
||||||
|
defaultWidth?: number;
|
||||||
|
defaultHeight?: number;
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
},
|
||||||
|
) => string;
|
||||||
|
unregisterDashboardWidget: (id: string) => void;
|
||||||
|
|
||||||
|
// Theme methods
|
||||||
|
addTheme: (
|
||||||
|
themeId: string,
|
||||||
|
displayName: string,
|
||||||
|
colorOverrides: ThemeColorOverrides,
|
||||||
|
options?: {
|
||||||
|
baseTheme?: 'light' | 'dark' | 'midnight';
|
||||||
|
description?: string;
|
||||||
|
},
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
overrideTheme: (
|
||||||
|
themeId: 'light' | 'dark' | 'midnight' | string,
|
||||||
|
colorOverrides: ThemeColorOverrides,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
// Query builder provided by host (loot-core's q function)
|
||||||
|
q: QueryBuilder;
|
||||||
|
|
||||||
|
// Report and spreadsheet utilities for dashboard widgets
|
||||||
|
createSpreadsheet: () => PluginSpreadsheet;
|
||||||
|
|
||||||
|
makeFilters: (
|
||||||
|
conditions: Array<PluginFilterCondition>,
|
||||||
|
) => Promise<PluginFilterResult>;
|
||||||
|
|
||||||
|
i18nInstance: i18n;
|
||||||
|
}
|
||||||
5
packages/plugins-core/src/types/actualPluginEntry.ts
Normal file
5
packages/plugins-core/src/types/actualPluginEntry.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ActualPluginInitialized } from './actualPlugin';
|
||||||
|
|
||||||
|
export type ActualPluginEntry = () => ActualPluginInitialized;
|
||||||
11
packages/plugins-core/src/types/actualPluginManifest.ts
Normal file
11
packages/plugins-core/src/types/actualPluginManifest.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface ActualPluginManifest {
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
description?: string;
|
||||||
|
pluginType: 'server' | 'client';
|
||||||
|
minimumActualVersion: string;
|
||||||
|
author: string;
|
||||||
|
plugin?: Blob;
|
||||||
|
}
|
||||||
27
packages/plugins-core/src/types/aql-result.ts
Normal file
27
packages/plugins-core/src/types/aql-result.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* AQL Query Result Types
|
||||||
|
*
|
||||||
|
* Types for AQL (Advanced Query Language) query results.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DatabaseRow } from './database';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of an AQL query operation
|
||||||
|
*/
|
||||||
|
export interface AQLQueryResult {
|
||||||
|
/** The actual data returned by the query */
|
||||||
|
data: DatabaseRow[] | DatabaseRow | null;
|
||||||
|
/** List of table/field dependencies detected during query execution */
|
||||||
|
dependencies: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for AQL query execution
|
||||||
|
*/
|
||||||
|
export interface AQLQueryOptions {
|
||||||
|
/** Target for the query - 'plugin' uses plugin DB, 'host' uses main DB */
|
||||||
|
target?: 'plugin' | 'host';
|
||||||
|
/** Parameters to be passed to the query */
|
||||||
|
params?: Record<string, string | number | boolean | null>;
|
||||||
|
}
|
||||||
51
packages/plugins-core/src/types/database.ts
Normal file
51
packages/plugins-core/src/types/database.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Plugin Database Types
|
||||||
|
*
|
||||||
|
* Shared types for database operations between plugins and the host application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters that can be passed to SQL queries
|
||||||
|
*/
|
||||||
|
export type SqlParameter = string | number | boolean | null | Buffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a database query operation (INSERT, UPDATE, DELETE)
|
||||||
|
*/
|
||||||
|
export interface DatabaseQueryResult {
|
||||||
|
changes: number;
|
||||||
|
insertId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Row returned from a database SELECT query
|
||||||
|
*/
|
||||||
|
export type DatabaseRow = Record<string, SqlParameter>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a database SELECT query
|
||||||
|
*/
|
||||||
|
export type DatabaseSelectResult = DatabaseRow[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union type for all possible database query results
|
||||||
|
*/
|
||||||
|
export type DatabaseResult = DatabaseQueryResult | DatabaseSelectResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database transaction operation
|
||||||
|
*/
|
||||||
|
export interface DatabaseOperation {
|
||||||
|
type: 'exec' | 'query';
|
||||||
|
sql: string;
|
||||||
|
params?: SqlParameter[];
|
||||||
|
fetchAll?: boolean; // Only used for query operations
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin database metadata key-value pair
|
||||||
|
*/
|
||||||
|
export interface PluginMetadata {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
25
packages/plugins-core/src/types/models/account.ts
Normal file
25
packages/plugins-core/src/types/models/account.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export type AccountEntity = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
offbudget: 0 | 1;
|
||||||
|
closed: 0 | 1;
|
||||||
|
sort_order: number;
|
||||||
|
last_reconciled: string | null;
|
||||||
|
tombstone: 0 | 1;
|
||||||
|
} & (_SyncFields<true> | _SyncFields<false>);
|
||||||
|
|
||||||
|
export type _SyncFields<T> = {
|
||||||
|
account_id: T extends true ? string : null;
|
||||||
|
bank: T extends true ? string : null;
|
||||||
|
bankName: T extends true ? string : null;
|
||||||
|
bankId: T extends true ? number : null;
|
||||||
|
mask: T extends true ? string : null; // end of bank account number
|
||||||
|
official_name: T extends true ? string : null;
|
||||||
|
balance_current: T extends true ? number : null;
|
||||||
|
balance_available: T extends true ? number : null;
|
||||||
|
balance_limit: T extends true ? number : null;
|
||||||
|
account_sync_source: T extends true ? AccountSyncSource : null;
|
||||||
|
last_sync: T extends true ? string : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AccountSyncSource = 'simpleFin' | 'goCardless' | 'pluggyai';
|
||||||
11
packages/plugins-core/src/types/models/category-group.ts
Normal file
11
packages/plugins-core/src/types/models/category-group.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { CategoryEntity } from './category';
|
||||||
|
|
||||||
|
export interface CategoryGroupEntity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
is_income?: boolean;
|
||||||
|
sort_order?: number;
|
||||||
|
tombstone?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
categories?: CategoryEntity[];
|
||||||
|
}
|
||||||
7
packages/plugins-core/src/types/models/category-views.ts
Normal file
7
packages/plugins-core/src/types/models/category-views.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { CategoryEntity } from './category';
|
||||||
|
import { CategoryGroupEntity } from './category-group';
|
||||||
|
|
||||||
|
export interface CategoryViews {
|
||||||
|
grouped: CategoryGroupEntity[];
|
||||||
|
list: CategoryEntity[];
|
||||||
|
}
|
||||||
13
packages/plugins-core/src/types/models/category.ts
Normal file
13
packages/plugins-core/src/types/models/category.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { CategoryGroupEntity } from './category-group';
|
||||||
|
|
||||||
|
export interface CategoryEntity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
is_income?: boolean;
|
||||||
|
group: CategoryGroupEntity['id'];
|
||||||
|
goal_def?: string;
|
||||||
|
template_settings?: { source: 'notes' | 'ui' };
|
||||||
|
sort_order?: number;
|
||||||
|
tombstone?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
7
packages/plugins-core/src/types/models/index.ts
Normal file
7
packages/plugins-core/src/types/models/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type * from './account';
|
||||||
|
export type * from './payee';
|
||||||
|
export type * from './category';
|
||||||
|
export type * from './category-group';
|
||||||
|
export type * from './category-views';
|
||||||
|
export type * from './rule';
|
||||||
|
export type * from './schedule';
|
||||||
10
packages/plugins-core/src/types/models/payee.ts
Normal file
10
packages/plugins-core/src/types/models/payee.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { AccountEntity } from './account';
|
||||||
|
|
||||||
|
export interface PayeeEntity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
transfer_acct?: AccountEntity['id'];
|
||||||
|
favorite?: boolean;
|
||||||
|
learn_categories?: boolean;
|
||||||
|
tombstone?: boolean;
|
||||||
|
}
|
||||||
179
packages/plugins-core/src/types/models/rule.ts
Normal file
179
packages/plugins-core/src/types/models/rule.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { type RecurConfig } from './schedule';
|
||||||
|
|
||||||
|
export interface NewRuleEntity {
|
||||||
|
stage: 'pre' | null | 'post';
|
||||||
|
conditionsOp: 'or' | 'and';
|
||||||
|
conditions: RuleConditionEntity[];
|
||||||
|
actions: RuleActionEntity[];
|
||||||
|
tombstone?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleEntity extends NewRuleEntity {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RuleConditionOp =
|
||||||
|
| 'is'
|
||||||
|
| 'isNot'
|
||||||
|
| 'oneOf'
|
||||||
|
| 'notOneOf'
|
||||||
|
| 'isapprox'
|
||||||
|
| 'isbetween'
|
||||||
|
| 'gt'
|
||||||
|
| 'gte'
|
||||||
|
| 'lt'
|
||||||
|
| 'lte'
|
||||||
|
| 'contains'
|
||||||
|
| 'doesNotContain'
|
||||||
|
| 'hasTags'
|
||||||
|
| 'and'
|
||||||
|
| 'matches'
|
||||||
|
| 'onBudget'
|
||||||
|
| 'offBudget';
|
||||||
|
|
||||||
|
export type FieldValueTypes = {
|
||||||
|
account: string;
|
||||||
|
amount: number;
|
||||||
|
category: string;
|
||||||
|
date: string | RecurConfig;
|
||||||
|
notes: string;
|
||||||
|
payee: string;
|
||||||
|
payee_name: string;
|
||||||
|
imported_payee: string;
|
||||||
|
saved: string;
|
||||||
|
transfer: boolean;
|
||||||
|
parent: boolean;
|
||||||
|
cleared: boolean;
|
||||||
|
reconciled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BaseConditionEntity<
|
||||||
|
Field extends keyof FieldValueTypes,
|
||||||
|
Op extends RuleConditionOp,
|
||||||
|
> = {
|
||||||
|
field: Field;
|
||||||
|
op: Op;
|
||||||
|
value: Op extends 'oneOf' | 'notOneOf'
|
||||||
|
? Array<FieldValueTypes[Field]>
|
||||||
|
: Op extends 'isbetween'
|
||||||
|
? { num1: number; num2: number }
|
||||||
|
: FieldValueTypes[Field];
|
||||||
|
options?: {
|
||||||
|
inflow?: boolean;
|
||||||
|
outflow?: boolean;
|
||||||
|
month?: boolean;
|
||||||
|
year?: boolean;
|
||||||
|
};
|
||||||
|
conditionsOp?: string;
|
||||||
|
type?: 'id' | 'boolean' | 'date' | 'number' | 'string';
|
||||||
|
customName?: string;
|
||||||
|
queryFilter?: Record<string, { $oneof: string[] }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RuleConditionEntity =
|
||||||
|
| BaseConditionEntity<
|
||||||
|
'account',
|
||||||
|
| 'is'
|
||||||
|
| 'isNot'
|
||||||
|
| 'oneOf'
|
||||||
|
| 'notOneOf'
|
||||||
|
| 'contains'
|
||||||
|
| 'doesNotContain'
|
||||||
|
| 'matches'
|
||||||
|
| 'onBudget'
|
||||||
|
| 'offBudget'
|
||||||
|
>
|
||||||
|
| BaseConditionEntity<
|
||||||
|
'category',
|
||||||
|
| 'is'
|
||||||
|
| 'isNot'
|
||||||
|
| 'oneOf'
|
||||||
|
| 'notOneOf'
|
||||||
|
| 'contains'
|
||||||
|
| 'doesNotContain'
|
||||||
|
| 'matches'
|
||||||
|
>
|
||||||
|
| BaseConditionEntity<
|
||||||
|
'amount',
|
||||||
|
'is' | 'isapprox' | 'isbetween' | 'gt' | 'gte' | 'lt' | 'lte'
|
||||||
|
>
|
||||||
|
| BaseConditionEntity<
|
||||||
|
'date',
|
||||||
|
'is' | 'isapprox' | 'isbetween' | 'gt' | 'gte' | 'lt' | 'lte'
|
||||||
|
>
|
||||||
|
| BaseConditionEntity<
|
||||||
|
'notes',
|
||||||
|
| 'is'
|
||||||
|
| 'isNot'
|
||||||
|
| 'oneOf'
|
||||||
|
| 'notOneOf'
|
||||||
|
| 'contains'
|
||||||
|
| 'doesNotContain'
|
||||||
|
| 'matches'
|
||||||
|
| 'hasTags'
|
||||||
|
>
|
||||||
|
| BaseConditionEntity<
|
||||||
|
'payee',
|
||||||
|
| 'is'
|
||||||
|
| 'isNot'
|
||||||
|
| 'oneOf'
|
||||||
|
| 'notOneOf'
|
||||||
|
| 'contains'
|
||||||
|
| 'doesNotContain'
|
||||||
|
| 'matches'
|
||||||
|
>
|
||||||
|
| BaseConditionEntity<
|
||||||
|
'imported_payee',
|
||||||
|
| 'is'
|
||||||
|
| 'isNot'
|
||||||
|
| 'oneOf'
|
||||||
|
| 'notOneOf'
|
||||||
|
| 'contains'
|
||||||
|
| 'doesNotContain'
|
||||||
|
| 'matches'
|
||||||
|
>
|
||||||
|
| BaseConditionEntity<'saved', 'is'>
|
||||||
|
| BaseConditionEntity<'cleared', 'is'>
|
||||||
|
| BaseConditionEntity<'reconciled', 'is'>;
|
||||||
|
|
||||||
|
export type RuleActionEntity =
|
||||||
|
| SetRuleActionEntity
|
||||||
|
| SetSplitAmountRuleActionEntity
|
||||||
|
| LinkScheduleRuleActionEntity
|
||||||
|
| PrependNoteRuleActionEntity
|
||||||
|
| AppendNoteRuleActionEntity;
|
||||||
|
|
||||||
|
export interface SetRuleActionEntity {
|
||||||
|
field: string;
|
||||||
|
op: 'set';
|
||||||
|
value: unknown;
|
||||||
|
options?: {
|
||||||
|
template?: string;
|
||||||
|
splitIndex?: number;
|
||||||
|
};
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetSplitAmountRuleActionEntity {
|
||||||
|
op: 'set-split-amount';
|
||||||
|
value: number;
|
||||||
|
options?: {
|
||||||
|
splitIndex?: number;
|
||||||
|
method: 'fixed-amount' | 'fixed-percent' | 'remainder';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkScheduleRuleActionEntity {
|
||||||
|
op: 'link-schedule';
|
||||||
|
value: unknown; // Changed from ScheduleEntity to avoid circular dependency
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrependNoteRuleActionEntity {
|
||||||
|
op: 'prepend-notes';
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppendNoteRuleActionEntity {
|
||||||
|
op: 'append-notes';
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
49
packages/plugins-core/src/types/models/schedule.ts
Normal file
49
packages/plugins-core/src/types/models/schedule.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { AccountEntity } from './account';
|
||||||
|
import type { PayeeEntity } from './payee';
|
||||||
|
import type { RuleConditionEntity, RuleEntity } from './rule';
|
||||||
|
|
||||||
|
export interface RecurPattern {
|
||||||
|
value: number;
|
||||||
|
type: 'SU' | 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'day';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecurConfig {
|
||||||
|
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||||
|
interval?: number;
|
||||||
|
patterns?: RecurPattern[];
|
||||||
|
skipWeekend?: boolean;
|
||||||
|
start: string;
|
||||||
|
endMode: 'never' | 'after_n_occurrences' | 'on_date';
|
||||||
|
endOccurrences?: number;
|
||||||
|
endDate?: string;
|
||||||
|
weekendSolveMode?: 'before' | 'after';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleEntity {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
rule: RuleEntity['id'];
|
||||||
|
next_date: string;
|
||||||
|
completed: boolean;
|
||||||
|
posts_transaction: boolean;
|
||||||
|
tombstone: boolean;
|
||||||
|
|
||||||
|
// These are special fields that are actually pulled from the
|
||||||
|
// underlying rule
|
||||||
|
_payee: PayeeEntity['id'];
|
||||||
|
_account: AccountEntity['id'];
|
||||||
|
_amount: number | { num1: number; num2: number };
|
||||||
|
_amountOp: string;
|
||||||
|
_date: RecurConfig;
|
||||||
|
_conditions: RuleConditionEntity[];
|
||||||
|
_actions: Array<{ op: unknown }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DiscoverScheduleEntity = {
|
||||||
|
id: ScheduleEntity['id'];
|
||||||
|
account: AccountEntity['id'];
|
||||||
|
payee: PayeeEntity['id'];
|
||||||
|
date: ScheduleEntity['_date'];
|
||||||
|
amount: ScheduleEntity['_amount'];
|
||||||
|
_conditions: ScheduleEntity['_conditions'];
|
||||||
|
};
|
||||||
18
packages/plugins-core/src/types/plugin-files.ts
Normal file
18
packages/plugins-core/src/types/plugin-files.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Plugin File Types
|
||||||
|
*
|
||||||
|
* Types for plugin file operations and storage.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single file within a plugin package
|
||||||
|
*/
|
||||||
|
export interface PluginFile {
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of files that make up a plugin
|
||||||
|
*/
|
||||||
|
export type PluginFileCollection = PluginFile[];
|
||||||
7
packages/plugins-core/src/types/toolkit.ts
Normal file
7
packages/plugins-core/src/types/toolkit.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type ActualPluginToolkitFunctions = {
|
||||||
|
pushModal: (modalName: string, options?: unknown) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActualPluginToolkit = {
|
||||||
|
functions: ActualPluginToolkitFunctions;
|
||||||
|
};
|
||||||
5
packages/plugins-core/src/types/util.ts
Normal file
5
packages/plugins-core/src/types/util.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Utility types for plugins-core
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
||||||
30
packages/plugins-core/src/utils.ts
Normal file
30
packages/plugins-core/src/utils.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { PluginSpreadsheet } from './types/actualPlugin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook that fetches and returns report data for a plugin spreadsheet.
|
||||||
|
*
|
||||||
|
* Calls the provided async `getData` function with the spreadsheet and a setter;
|
||||||
|
* the setter updates the returned results when data becomes available.
|
||||||
|
*
|
||||||
|
* @param sheetName - Identifier for the sheet (kept for API compatibility; not used by the hook).
|
||||||
|
* @param getData - Async function that receives the spreadsheet and a `setData` callback to supply results.
|
||||||
|
* @returns The most recent results of type `T`, or `null` if no results have been set yet.
|
||||||
|
*/
|
||||||
|
export function useReport<T>(
|
||||||
|
sheetName: string,
|
||||||
|
getData: (
|
||||||
|
spreadsheet: PluginSpreadsheet,
|
||||||
|
setData: (results: T) => void,
|
||||||
|
) => Promise<void>,
|
||||||
|
spreadsheet: PluginSpreadsheet,
|
||||||
|
): T | null {
|
||||||
|
const [results, setResults] = useState<T | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getData(spreadsheet, results => setResults(results));
|
||||||
|
}, [getData, spreadsheet]);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
13
packages/plugins-core/tsconfig.json
Normal file
13
packages/plugins-core/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
39
packages/plugins-core/vite.config.mts
Normal file
39
packages/plugins-core/vite.config.mts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: 'build',
|
||||||
|
lib: {
|
||||||
|
entry: {
|
||||||
|
index: 'src/index.ts',
|
||||||
|
server: 'src/server.ts',
|
||||||
|
client: 'src/client.ts',
|
||||||
|
},
|
||||||
|
name: '@actual-app/plugins-core',
|
||||||
|
fileName: (format, entryName) =>
|
||||||
|
format === 'es' ? `${entryName}.js` : `${entryName}.cjs`,
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['react', 'react-dom', 'i18next'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
react: 'React',
|
||||||
|
'react-dom': 'ReactDOM',
|
||||||
|
i18next: 'i18next',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
dts({
|
||||||
|
insertTypesEntry: true,
|
||||||
|
outDir: 'build',
|
||||||
|
include: ['src/**/*'],
|
||||||
|
exclude: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
|
||||||
|
rollupTypes: false,
|
||||||
|
copyDtsFiles: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -32,6 +32,15 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
// TEMPORARY: Until we can fix the "exports" in the loot-core package.json
|
// TEMPORARY: Until we can fix the "exports" in the loot-core package.json
|
||||||
"loot-core/*": ["./packages/loot-core/src/*"],
|
"loot-core/*": ["./packages/loot-core/src/*"],
|
||||||
|
"plugins-core/*": ["./packages/plugins-core/src/*"],
|
||||||
|
"@actual-app/plugins-core": ["./packages/plugins-core/src/index.ts"],
|
||||||
|
"@actual-app/plugins-core/server": [
|
||||||
|
"./packages/plugins-core/src/server.ts"
|
||||||
|
],
|
||||||
|
"@actual-app/plugins-core/client": [
|
||||||
|
"./packages/plugins-core/src/client.ts"
|
||||||
|
],
|
||||||
|
"@actual-app/plugins-core/*": ["./packages/plugins-core/src/*"],
|
||||||
"@desktop-client/*": ["./packages/desktop-client/src/*"],
|
"@desktop-client/*": ["./packages/desktop-client/src/*"],
|
||||||
"@desktop-client/e2e/*": ["./packages/desktop-client/e2e/*"]
|
"@desktop-client/e2e/*": ["./packages/desktop-client/e2e/*"]
|
||||||
},
|
},
|
||||||
@@ -52,7 +61,8 @@
|
|||||||
"**/dist/*",
|
"**/dist/*",
|
||||||
"**/lib-dist/*",
|
"**/lib-dist/*",
|
||||||
"**/test-results/*",
|
"**/test-results/*",
|
||||||
"**/playwright-report/*"
|
"**/playwright-report/*",
|
||||||
|
"packages/test-plugin/*"
|
||||||
],
|
],
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
|||||||
7
upcoming-release-notes/5786.md
Normal file
7
upcoming-release-notes/5786.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
category: Features
|
||||||
|
authors: [lelemm]
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for frontend plugins with a new core package for enhanced functionality.
|
||||||
|
|
||||||
Reference in New Issue
Block a user