= T extends { type: 'object'; properties: infer P }
12 | ? { [K in keyof P]: ExtractProperties }
13 | : T extends { type: 'array'; items: infer I }
14 | ? ExtractProperties[]
15 | : T extends { type: infer X }
16 | ? Convert
17 | : unknown;
18 |
19 | type MakePropertiesOptional = {
20 | [K in keyof T]?: T[K];
21 | };
22 |
23 | export type NodeDataProperties = MakePropertiesOptional>;
24 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/utils/unknown-renderer.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ControlProps,
3 | isControl,
4 | isLayout,
5 | JsonFormsRendererRegistryEntry,
6 | LayoutProps,
7 | or,
8 | rankWith,
9 | } from '@jsonforms/core';
10 | import { withJsonFormsControlProps } from '@jsonforms/react';
11 |
12 | function UnknownRenderer({ uischema: { type } }: ControlProps | LayoutProps) {
13 | return (
14 |
15 | No renderer provided for type: {type}
16 |
17 | );
18 | }
19 |
20 | const UnknownRendererTester = rankWith(0, or(isControl, isLayout));
21 |
22 | export const unknownRenderer: JsonFormsRendererRegistryEntry = {
23 | tester: UnknownRendererTester,
24 | renderer: withJsonFormsControlProps(UnknownRenderer),
25 | };
26 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/palette/components/footer/palette-footer.tsx:
--------------------------------------------------------------------------------
1 | import styles from './palette-footer.module.css';
2 | import { useTranslation } from 'react-i18next';
3 | import { Button } from '@synergycodes/overflow-ui';
4 | import { OptionalFooterContent } from '@/features/plugins-core/components/optional-footer-content';
5 |
6 | type Props = {
7 | onTemplateClick: () => void;
8 | };
9 |
10 | export function PaletteFooter({ onTemplateClick }: Props) {
11 | const { t } = useTranslation();
12 |
13 | return (
14 |
15 |
16 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Workflow Builder repository documentation
2 |
3 | You can check other documents in this directory, and there are also useful documents throughout the entire repository.
4 |
5 | If you look further, you will find that we have a `*.decision-log.md` file explaining our reasoning behind the choices we made.
6 |
7 | ## How do you integrate Workflow Builder with your data?
8 |
9 | The [@/feature/integration](../apps/frontend/src/app/features/integration/README.md) feature is responsible for it. You will find more information [here](../apps/frontend/src/app/features/integration/README.md).
10 |
11 | ## How do optional plugins work in Workflow Builder?
12 |
13 | How to add [@/plugins](../apps/frontend/src/app/plugins/README.md) and how they work is described [here](../apps/frontend/src/app/plugins/README.md).
14 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/plugins-core/README.md:
--------------------------------------------------------------------------------
1 | # How to use Optional content?
2 |
3 | ## Adding optional hook
4 |
5 | ```tsx
6 | const YourComponentWithACustomHook = () => {
7 | useYourCustomHook();
8 |
9 | return null;
10 | };
11 |
12 | registerComponentDecorator('OptionalHooks', {
13 | content: YourComponentWithAHook,
14 | });
15 | ```
16 |
17 | And import it in `apps/frontend/src/app/features/plugins-core/index.ts`.
18 |
19 | ## Adding button before
20 |
21 | ```tsx
22 | const YourComponentWithACustomControl = () => {
23 | return ;
24 | };
25 |
26 | registerComponentDecorator('OptionalAppBarTools', {
27 | content: YourComponentWithACustomControl,
28 | place: 'before',
29 | priority: 10,
30 | });
31 | ```
32 |
33 | And import it in `apps/frontend/src/app/features/plugins-core/index.ts`.
34 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/components/form/form-control-with-label/form-control-with-label.tsx:
--------------------------------------------------------------------------------
1 | import styles from './form-control-with-label.module.css';
2 | import clsx from 'clsx';
3 | import { Label } from '../label/label';
4 | import { PropsWithChildren } from 'react';
5 | import { ItemSize } from '@synergycodes/overflow-ui';
6 |
7 | type Props = {
8 | label: string;
9 | className?: string;
10 | required?: boolean;
11 | size?: ItemSize;
12 | };
13 |
14 | export function FormControlWithLabel({
15 | label,
16 | className,
17 | required,
18 | size = 'medium',
19 | children,
20 | }: PropsWithChildren) {
21 | return (
22 |
23 |
24 | {children}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/conditional/uischema.ts:
--------------------------------------------------------------------------------
1 | import { UISchema } from '@/features/json-form/types/uischema';
2 | import { getScope } from '@/features/json-form/utils/get-scope';
3 | import { ConditionalNodeSchema } from './schema';
4 |
5 | const scope = getScope;
6 |
7 | export const uischema: UISchema = {
8 | type: 'VerticalLayout',
9 | elements: [
10 | {
11 | label: 'Label',
12 | type: 'Text',
13 | scope: scope('properties.label'),
14 | },
15 | {
16 | label: 'Description',
17 | type: 'Text',
18 | scope: scope('properties.description'),
19 | placeholder: 'Type your description here...',
20 | },
21 | {
22 | label: 'Conditions',
23 | type: 'DynamicConditions',
24 | scope: scope('properties.conditionsArray'),
25 | },
26 | ],
27 | };
28 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/diagram/edges/label-edge/label-edge.module.css:
--------------------------------------------------------------------------------
1 | .label {
2 | composes: ax-public-p10 from global;
3 |
4 | position: absolute;
5 | pointer-events: auto;
6 | display: flex;
7 | padding: 0.125rem 0.75rem;
8 | justify-content: center;
9 | align-items: center;
10 |
11 | color: var(--wb-edge-label-color);
12 | background-color: var(--wb-edge-label-background);
13 | border-radius: 0.5rem;
14 | border: solid 1px var(--wb-edge-label-color);
15 |
16 | &.icon {
17 | padding: 0.25rem;
18 | border-radius: 50%;
19 | }
20 |
21 | &.hover {
22 | color: var(--wb-edge-label-color-hover);
23 | border-color: var(--wb-edge-label-color-hover);
24 | }
25 |
26 | &.selected {
27 | color: var(--wb-edge-label-color-select);
28 | border-color: var(--wb-edge-label-color-select);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/delay/conditional-validation.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable unicorn/no-thenable */
2 | import { IfThenElseSchema } from '@workflow-builder/types/node-validation-schema';
3 | import { delayTypeOptions } from './select-options';
4 |
5 | export const conditionalValidation = {
6 | allOf: [
7 | {
8 | if: {
9 | properties: {
10 | type: { const: delayTypeOptions.fixed.value },
11 | },
12 | },
13 | then: {
14 | properties: {
15 | duration: {
16 | type: 'object',
17 | required: ['delayAmount'],
18 | properties: {
19 | delayAmount: {
20 | type: 'number',
21 | minimum: 1,
22 | },
23 | },
24 | },
25 | },
26 | },
27 | },
28 | ] as IfThenElseSchema[],
29 | };
30 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/controls/switch-control/switch-control.tsx:
--------------------------------------------------------------------------------
1 | import { Switch } from '@synergycodes/overflow-ui';
2 | import { SwitchControlProps } from '../../types/controls';
3 | import { createControlRenderer } from '../../utils/rendering';
4 | import { ControlWrapper } from '../control-wrapper';
5 |
6 | function SwitchControl(props: SwitchControlProps) {
7 | const { data, handleChange, path, enabled } = props;
8 |
9 | function onChange(checked: boolean, _event: React.ChangeEvent) {
10 | handleChange(path, checked);
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | export const switchControlRenderer = createControlRenderer('Switch', SwitchControl);
21 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/diagram/nodes/ai-agent-node-template/components/tool-info/tool-info.tsx:
--------------------------------------------------------------------------------
1 | import styles from './tool-info.module.css';
2 |
3 | import { PropsWithChildren } from 'react';
4 | import { NodeInfoWrapper } from '../node-info-wrapper/node-wrapper-info';
5 | import { IconPlaceholder } from '../icon-placeholder/icon-placeholder';
6 | import { PlusCircle } from '@phosphor-icons/react';
7 |
8 | export function ToolInfo({ children }: PropsWithChildren) {
9 | return (
10 |
11 | {children}
12 |
13 |
14 | Add Tool
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/utils/validation/get-node-errors.mock.ts:
--------------------------------------------------------------------------------
1 | import { WorkflowBuilderNode } from '@workflow-builder/types/node-data';
2 |
3 | export const mockNodeDelay: WorkflowBuilderNode = {
4 | id: 'delay-1',
5 | type: 'node',
6 | position: {
7 | x: 0,
8 | y: 0,
9 | },
10 | data: {
11 | segments: [],
12 | properties: {
13 | label: 'Delay',
14 | description: 'Pause the workflow',
15 | status: 'draft',
16 | duration: {
17 | timeUnits: 'none',
18 | delayAmount: 3,
19 | maxWaitTime: '24',
20 | expression: 'order.processing_time * 2',
21 | },
22 | errors: [],
23 | type: 'fixedDelay',
24 | },
25 | type: 'delay',
26 | icon: 'Timer',
27 | },
28 | selected: false,
29 | measured: {
30 | width: 258,
31 | height: 63,
32 | },
33 | dragging: false,
34 | };
35 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "strict": true,
5 | "rootDir": ".",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "module": "preserve",
9 | "moduleResolution": "bundler",
10 | "emitDecoratorMetadata": true,
11 | "esModuleInterop": true,
12 | "experimentalDecorators": true,
13 | "target": "es2015",
14 | "lib": ["es2020", "dom", "dom.iterable"],
15 | "skipLibCheck": true,
16 | "skipDefaultLibCheck": true,
17 | "baseUrl": ".",
18 | "jsx": "react-jsx",
19 | "types": ["node", "../../apps/frontend/global.d.ts"],
20 | "paths": {
21 | "@/*": ["apps/frontend/src/app/*"],
22 | "@/assets/*": ["apps/frontend/src/assets/*"],
23 | "@workflow-builder/types/*": ["apps/types/src/*"]
24 | }
25 | },
26 | "exclude": ["node_modules", "tmp"]
27 | }
28 |
--------------------------------------------------------------------------------
/apps/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Workflow Builder
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/controls/date-picker-control/date-picker-control.tsx:
--------------------------------------------------------------------------------
1 | import { DatePicker, DatePickerProps } from '@synergycodes/overflow-ui';
2 | import { DatePickerControlProps } from '../../types/controls';
3 | import { ControlWrapper } from '../control-wrapper';
4 | import { createControlRenderer } from '../../utils/rendering';
5 |
6 | function DatePickerControl(props: DatePickerControlProps) {
7 | const { data, handleChange, path, enabled } = props;
8 |
9 | const onChange: DatePickerProps['onChange'] = (value) => {
10 | handleChange(path, value?.toString());
11 | };
12 |
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | export const datePickerControlRenderer = createControlRenderer('DatePicker', DatePickerControl);
21 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | env:
4 | APP_LOCATION: ""
5 |
6 | on:
7 | push:
8 | branches: ["main"]
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | name: Build
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 | with:
21 | submodules: true
22 |
23 | - name: Set up Node.js
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: 22
27 | registry-url: "https://registry.npmjs.org"
28 |
29 | - name: Enable Corepack
30 | run: npm i -g corepack@latest
31 |
32 | - name: Install pnpm
33 | run: corepack prepare
34 |
35 | - name: Install dependencies
36 | run: pnpm install --frozen-lockfile
37 |
38 | - name: Build application
39 | run: pnpm run build
40 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/layouts/group-layout/group-layout.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | import styles from './group-layout.module.css';
4 | import { LayoutWrapper } from '../layout-wrapper';
5 | import { GroupLayoutElement, LayoutProps } from '../../types/layouts';
6 | import { renderElements } from '../render-elements';
7 | import { createLayoutRenderer } from '../../utils/rendering';
8 |
9 | function GroupLayout(props: LayoutProps) {
10 | const { uischema } = props;
11 |
12 | return (
13 |
14 |
15 |
{uischema.label}
16 | {renderElements(props)}
17 |
18 |
19 | );
20 | }
21 |
22 | export const groupLayoutRenderer = createLayoutRenderer('Group', GroupLayout);
23 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/components/form/label/label.module.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --ax-public-form-label-color: var(--ax-txt-secondary-default);
3 | --ax-public-form-label-asterisk-color: var(--ax-txt-error-default);
4 | }
5 |
6 | @layer ui.component {
7 | .container {
8 | display: flex;
9 | gap: 0.25rem;
10 | color: var(--ax-public-form-label-color);
11 | align-items: center;
12 |
13 | .label {
14 | white-space: nowrap;
15 | overflow: hidden;
16 | text-overflow: ellipsis;
17 | width: 100%;
18 | }
19 |
20 | svg {
21 | width: 0.625rem;
22 | height: 0.625rem;
23 | min-width: 0.625rem;
24 |
25 | color: var(--ax-public-form-label-asterisk-color);
26 | }
27 | }
28 |
29 | .large {
30 | composes: ax-public-p10 from global;
31 | }
32 |
33 | .medium,
34 | .small {
35 | composes: ax-public-p11 from global;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/layouts/accordion-layout/accordion-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Accordion } from '@synergycodes/overflow-ui';
2 | import { AccordionLayoutElement, LayoutProps } from '../../types/layouts';
3 | import { LayoutWrapper } from '../layout-wrapper';
4 | import { createLayoutRenderer } from '../../utils/rendering';
5 | import { renderElements } from '../render-elements';
6 |
7 | import styles from './accordion-layout.module.css';
8 |
9 | function AccordionLayout(props: LayoutProps) {
10 | const { uischema } = props;
11 |
12 | return (
13 |
14 |
15 | {renderElements(props)}
16 |
17 |
18 | );
19 | }
20 |
21 | export const accordionLayoutRenderer = createLayoutRenderer('Accordion', AccordionLayout);
22 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/palette/components/header/palette-header.tsx:
--------------------------------------------------------------------------------
1 | import styles from './palette-header.module.css';
2 | import { NavButton } from '@synergycodes/overflow-ui';
3 | import { Icon } from '@workflow-builder/icons';
4 | import { useTranslation } from 'react-i18next';
5 |
6 | type PaletteHeaderProps = {
7 | onClick: () => void;
8 | isSidebarExpanded: boolean;
9 | };
10 |
11 | export function PaletteHeader({ onClick, isSidebarExpanded }: PaletteHeaderProps) {
12 | const { t } = useTranslation();
13 |
14 | return (
15 |
16 | {t('palette.nodesLibrary')}
17 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/integration/components/integration-variants/wrapper/integration-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, useEffect } from 'react';
2 |
3 | import { loadData } from '@/features/integration/stores/use-integration-store';
4 | import { IntegrationDataFormatOptional, OnSave } from '@/features/integration/types';
5 |
6 | import { IntegrationContextWrapper } from '../context/integration-context-wrapper';
7 |
8 | type Props = PropsWithChildren<
9 | IntegrationDataFormatOptional & {
10 | onSave: OnSave;
11 | }
12 | >;
13 |
14 | export function IntegrationWrapper({ children, name, layoutDirection, nodes, edges, onSave }: Props) {
15 | useEffect(() => {
16 | loadData({
17 | name,
18 | layoutDirection,
19 | nodes,
20 | edges,
21 | });
22 | }, [edges, layoutDirection, name, nodes]);
23 |
24 | return {children};
25 | }
26 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/layouts/horizontal-layout/use-has-child-error.ts:
--------------------------------------------------------------------------------
1 | import { useJsonForms } from '@jsonforms/react';
2 | import { UISchemaElement } from '../../types/uischema';
3 | import { Scopable, Scoped, toDataPath } from '@jsonforms/core';
4 |
5 | export function useHasChildError(childElements?: UISchemaElement[]) {
6 | const { core } = useJsonForms();
7 | if (!core?.errors || !childElements?.length) {
8 | return false;
9 | }
10 |
11 | const childPaths = childElements
12 | .filter((element) => (element as Scopable).scope)
13 | .map((element) => toDataPath((element as Scoped).scope).replace('.', '/'));
14 |
15 | const errorPaths = core.errors.map((error) =>
16 | error.keyword === 'required' ? `${error.instancePath}/${error.params['missingProperty']}` : error.instancePath,
17 | );
18 |
19 | return errorPaths.some((errorPath) => childPaths.some((childPath) => errorPath.includes(childPath)));
20 | }
21 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/modals/providers/modal-provider.tsx:
--------------------------------------------------------------------------------
1 | import { createPortal } from 'react-dom';
2 | import { Modal } from '@synergycodes/overflow-ui';
3 | import { closeModal, useModalStore } from '../stores/use-modal-store';
4 |
5 | export function ModalProvider() {
6 | const isOpen = useModalStore((state) => state.isOpen);
7 | const modal = useModalStore((state) => state.modal);
8 |
9 | if (!isOpen || !modal) {
10 | return null;
11 | }
12 |
13 | return (
14 | <>
15 | {createPortal(
16 |
25 | {modal.content}
26 | ,
27 | document.body,
28 | )}
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/templates.ts:
--------------------------------------------------------------------------------
1 | import { simpleFlow } from './templates/simple-flow';
2 | import { userRegistration } from './templates/user-registration';
3 | import { blackFriday } from './templates/black-friday';
4 | import { callFlow } from './templates/call-flow';
5 | import { TemplateModel } from '@workflow-builder/types/common';
6 | import { snapToGridIfNeeded } from '@/utils/position-utils';
7 |
8 | function snapTemplateToGrid(template: TemplateModel) {
9 | return {
10 | ...template,
11 | value: {
12 | ...template.value,
13 | diagram: {
14 | ...template.value.diagram,
15 | nodes: template.value.diagram.nodes.map((node) => ({
16 | ...node,
17 | ...(node.position ? { position: snapToGridIfNeeded(node.position) } : {}),
18 | })),
19 | },
20 | },
21 | };
22 | }
23 |
24 | export const templates: TemplateModel[] = [simpleFlow, userRegistration, blackFriday, callFlow].map(snapTemplateToGrid);
25 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/properties-bar/components/header/properties-bar-header.tsx:
--------------------------------------------------------------------------------
1 | import styles from './properties-bar-header.module.css';
2 |
3 | import { NavButton } from '@synergycodes/overflow-ui';
4 | import { Icon } from '@workflow-builder/icons';
5 |
6 | type Props = {
7 | header: string;
8 | name: string;
9 | isExpanded: boolean;
10 | onDotsClick?: () => void;
11 | };
12 |
13 | export function PropertiesBarHeader({ isExpanded, header, name, onDotsClick }: Props) {
14 | return (
15 |
16 |
17 |
{header}
18 | {isExpanded &&
{name}
}
19 |
20 | {onDotsClick && (
21 |
22 |
23 |
24 | )}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/apps/tools/src/tool-utils/create-script.ts:
--------------------------------------------------------------------------------
1 | import { Logger } from './logger';
2 |
3 | const logger = new Logger('Scripts');
4 |
5 | export function runScript(name: string, scriptFunction: ScriptFunction) {
6 | const script = createScript(name, scriptFunction);
7 |
8 | return script();
9 | }
10 |
11 | function createScript(name: string, scriptFunction: ScriptFunction) {
12 | return async () => {
13 | const startTime = performance.now();
14 |
15 | try {
16 | await scriptFunction({ logger });
17 | } catch (error) {
18 | if (!(error instanceof Error)) {
19 | return;
20 | }
21 |
22 | logger.taskFailure(name, error.message);
23 | }
24 |
25 | const endTime = performance.now();
26 | const elapsedTime = endTime - startTime;
27 | logger.taskSuccess(name, elapsedTime);
28 | };
29 | }
30 |
31 | type ScriptFunction = (context: ScriptContext) => Promise;
32 | type ScriptContext = {
33 | logger: Logger;
34 | };
35 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/diagram/nodes/ai-agent-node-template/components/setting-info/setting-info.tsx:
--------------------------------------------------------------------------------
1 | import styles from './setting-info.module.css';
2 |
3 | import { Plus } from '@phosphor-icons/react';
4 | import { IconPlaceholder } from '../icon-placeholder/icon-placeholder';
5 | import { NodeInfoWrapper } from '../node-info-wrapper/node-wrapper-info';
6 | import { Icon, WBIcon } from '@workflow-builder/icons';
7 |
8 | type SettingPlaceholderProps = {
9 | label: string;
10 | actionLabel?: string;
11 | icon?: WBIcon;
12 | className?: string;
13 | };
14 |
15 | export function SettingInfo({ label, actionLabel, icon, className }: SettingPlaceholderProps) {
16 | return (
17 |
18 |
19 |
{icon ? : }
20 | {actionLabel}
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/hooks/use-command-handler.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 | import { useOnSelectionChange, useStoreApi } from '@xyflow/react';
3 | import { WorkflowBuilderOnSelectionChangeParams } from '@workflow-builder/types/common';
4 |
5 | export type CommandHandler = {
6 | selectAll: () => void;
7 | };
8 |
9 | export function useCommandHandler(): CommandHandler {
10 | const [_, setSelection] = useState();
11 | const reactFlowStore = useStoreApi();
12 |
13 | useOnSelectionChange({
14 | onChange: (change) => setSelection(change as WorkflowBuilderOnSelectionChangeParams),
15 | });
16 |
17 | const selectAll = useCallback(() => {
18 | const state = reactFlowStore.getState();
19 | state.addSelectedNodes(state.nodes.map((node) => node.id));
20 | state.addSelectedEdges(state.edges.map((edge) => edge.id));
21 | }, [reactFlowStore]);
22 |
23 | return {
24 | selectAll,
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/diagram/nodes/ai-agent-node-template/ai-agent-node-template.module.css:
--------------------------------------------------------------------------------
1 | .icon {
2 | background: linear-gradient(158deg, #5360f9 8.25%, #852bec 91.09%), #151516;
3 |
4 | & svg path {
5 | stroke: white;
6 | }
7 | }
8 |
9 | .root {
10 | --ax-public-node-gap: 0;
11 | }
12 |
13 | .content {
14 | display: flex;
15 | flex-direction: column;
16 | overflow: hidden;
17 |
18 | & div svg {
19 | width: 18px;
20 | height: 18px;
21 | }
22 |
23 | .collapsible-content {
24 | padding-bottom: 0.5rem;
25 | display: flex;
26 | flex-direction: column;
27 | gap: 0.5rem;
28 | }
29 |
30 | .selected-model-icon {
31 | background-color: var(--ax-txt-primary-default);
32 | color: var(--ax-txt-primary-inverse);
33 | outline: none;
34 | }
35 |
36 | .selected-memory-icon {
37 | color: var(--ax-txt-primary-white);
38 | background-color: var(--ax-button-gray-bg-default);
39 | outline: none;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/modals/template-selector/template-selector.module.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --wb-template-title-width: 116px;
3 | --wb-template-title-padding: 12px;
4 | --wb-template-subtitle-color: var(--ax-colors-gray-500) /* missing token */;
5 | }
6 |
7 | .container {
8 | width: 100%;
9 | display: flex;
10 | flex-direction: column;
11 | gap: 32px;
12 |
13 | .sub-title {
14 | text-align: center;
15 | color: var(--wb-template-subtitle-color);
16 | width: 100%;
17 | }
18 |
19 | .header {
20 | display: flex;
21 | align-items: center;
22 | }
23 |
24 | .content {
25 | display: flex;
26 | flex-direction: column;
27 | align-items: center;
28 | gap: 16px;
29 |
30 | .templates {
31 | display: flex;
32 | gap: 6px;
33 | flex-wrap: wrap;
34 | justify-content: center;
35 | max-width: calc((var(--wb-template-title-width) + var(--wb-template-title-padding) * 2) * 3 + 16px);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/diagram/selectors.ts:
--------------------------------------------------------------------------------
1 | import { PaletteState } from '@/store/slices/palette/palette-slice';
2 | import { DiagramDataModificationState } from '@/store/slices/diagram-data-modification/diagram-data-modification-slice';
3 | import { DiagramState } from '@/store/slices/diagram-slice';
4 | import { DiagramSelectionState } from '@/store/slices/diagram-selection/diagram-selection-slice';
5 |
6 | export function diagramStateSelector({
7 | nodes,
8 | edges,
9 | onNodesChange,
10 | onEdgesChange,
11 | onConnect,
12 | onInit,
13 | isReadOnlyMode,
14 | onEdgeMouseEnter,
15 | onEdgeMouseLeave,
16 | onSelectionChange,
17 | }: DiagramState & PaletteState & DiagramDataModificationState & DiagramSelectionState) {
18 | return {
19 | nodes,
20 | edges,
21 | onNodesChange,
22 | onEdgesChange,
23 | onConnect,
24 | onInit,
25 | isReadOnlyMode,
26 | onEdgeMouseEnter,
27 | onEdgeMouseLeave,
28 | onSelectionChange,
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/plugins/help/functions/add-items-to-dots.tsx:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import { MenuItemProps } from '@synergycodes/overflow-ui';
3 | import { Icon } from '@workflow-builder/icons';
4 | import { openNoAccessModal } from './open-no-access-modal';
5 |
6 | export function addItemsToDots({ returnValue }: { returnValue: unknown }) {
7 | if (!Array.isArray(returnValue)) {
8 | return;
9 | }
10 |
11 | const items = returnValue as MenuItemProps[];
12 |
13 | const newItems: MenuItemProps[] = [
14 | {
15 | label: i18n.t('header.controls.saveAsImage'),
16 | icon: ,
17 | onClick: openNoAccessModal,
18 | },
19 | {
20 | type: 'separator',
21 | },
22 | {
23 | label: i18n.t('header.controls.archive'),
24 | icon: ,
25 | destructive: true,
26 | onClick: openNoAccessModal,
27 | },
28 | ];
29 |
30 | return { replacedReturn: [...items, ...newItems] };
31 | }
32 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/modals/template-selector/components/tile.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | import styles from './tile.module.css';
4 | import { Icon } from '@workflow-builder/icons';
5 | import { IconType } from '@workflow-builder/types/common';
6 |
7 | type TileProps = {
8 | icon: IconType;
9 | title: string;
10 | subTitle?: string;
11 | outlined?: boolean;
12 | onClick: () => void;
13 | };
14 |
15 | export function Tile({ icon, title, subTitle, outlined, onClick }: TileProps) {
16 | return (
17 | onClick()}
22 | >
23 |
24 |
25 | {title}
26 | {subTitle && {subTitle}}
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import { initReactI18next } from 'react-i18next';
3 | import LanguageDetector from 'i18next-browser-languagedetector';
4 | import { en } from './locales/en';
5 | import { pl } from './locales/pl';
6 |
7 | import { withOptionalComponentPluginsTranslation } from '@/features/plugins-core/adapters/adapter-i18n';
8 |
9 | const defaultNS = 'translation';
10 |
11 | const resources = {
12 | en: {
13 | translation: en,
14 | } as const,
15 | pl: {
16 | translation: pl,
17 | } as const,
18 | };
19 |
20 | i18n
21 | .use(LanguageDetector)
22 | .use(initReactI18next)
23 | .init({
24 | resources: withOptionalComponentPluginsTranslation(resources),
25 | defaultNS,
26 | fallbackLng: 'en',
27 | interpolation: {
28 | escapeValue: false,
29 | },
30 | returnNull: false,
31 | load: 'languageOnly',
32 | detection: {
33 | order: ['localStorage', 'navigator'],
34 | caches: ['localStorage'],
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/utils/validation/get-node-errors.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { getNodeErrors } from './get-node-errors';
3 | import { mockNodeDelay } from './get-node-errors.mock';
4 |
5 | describe('getNodeErrors', () => {
6 | it('should return an empty array for a valid node', () => {
7 | const errors = getNodeErrors(mockNodeDelay);
8 |
9 | expect(errors).toEqual([]);
10 | });
11 |
12 | it('should return an array with a title error for a node without a title', () => {
13 | const { description: _, ...properties } = mockNodeDelay.data.properties;
14 | const errors = getNodeErrors({
15 | ...mockNodeDelay,
16 | data: {
17 | ...mockNodeDelay.data,
18 | properties,
19 | },
20 | });
21 |
22 | expect(errors).toEqual([
23 | {
24 | instancePath: '',
25 | keyword: 'required',
26 | message: "must have required property 'description'",
27 | schemaPath: '#/required',
28 | },
29 | ]);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/properties-bar/components/properties-bar/properties-bar.types.ts:
--------------------------------------------------------------------------------
1 | import { SingleSelectedElement } from '@/features/properties-bar/use-single-selected-element';
2 |
3 | type PropertiesBarSelection = Omit & {
4 | selection: SingleSelectedElement;
5 | };
6 |
7 | type PropertiesBarTab = {
8 | label: string;
9 | value: string;
10 | components: PropertiesBarItem[];
11 | };
12 |
13 | type PropertiesBarBaseProps = {
14 | selection: SingleSelectedElement | null;
15 | selectedTab: string;
16 | };
17 |
18 | export type PropertiesBarItem = {
19 | when: (props: PropertiesBarSelection) => boolean;
20 | component: (props: PropertiesBarSelection) => React.ReactNode;
21 | };
22 |
23 | export type PropertiesBarProps = PropertiesBarBaseProps & {
24 | headerLabel: string;
25 | deleteNodeLabel: string;
26 | deleteEdgeLabel: string;
27 | tabs?: PropertiesBarTab[];
28 | onTabChange: (tab: string) => void;
29 | onMenuHeaderClick?: () => void;
30 | onDeleteClick: () => void;
31 | };
32 |
--------------------------------------------------------------------------------
/docs/overflow-ui.md:
--------------------------------------------------------------------------------
1 | # overflow-ui
2 |
3 | Overflow-ui is an open-to-the-public UI library developed by Synergy Codes:
4 |
5 | https://www.npmjs.com/package/@synergycodes/overflow-ui
6 |
7 | https://github.com/synergycodes/overflow-ui
8 |
9 | ## How can I work locally on both `workflow-builder` and `overflow-ui`?
10 |
11 | 1. Set up the overflow-ui repository next to this one.
12 | 2. Build the dist files in the Axiom tokens package with `pnpm token prepare`.
13 | 3. Build the dist files in the Axiom repository with `pnpm ui dev` (keep this process running to allow live updates).
14 | 4. Update `"@synergycodes/overflow-ui"` in `apps/frontend/package.json` to `"@synergycodes/overflow-ui": "link:../../../overflow-ui/packages/ui"`.
15 | 5. In `apps/frontend/src/global.css`, replace `@import '@synergycodes/overflow-ui/tokens.css';` with `@import '../../../../overflow-ui/packages/ui/dist/tokens.css';`.
16 | 6. If steps above are not enough you can try refreshing dependencies with `pnpm install`.
17 |
18 | Don't include changes from steps 4 - 6 in your commits.
19 |
--------------------------------------------------------------------------------
/apps/frontend/file-fallback-for-missing-plugins.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import fs from 'node:fs';
3 |
4 | export function fallbackForMissingPlugin(): {
5 | name: string;
6 | resolveId(source: string): Promise;
7 | } | null {
8 | return {
9 | name: 'fallback-for-missing-plugin',
10 | async resolveId(source) {
11 | const match = source.match(/app\/plugins\/(.*)?$/);
12 |
13 | if (match) {
14 | const realPath = path.resolve(import.meta.dirname, 'src', source);
15 |
16 | const doesFileExist = [`${realPath}.ts`, `${realPath}.tsx`].some((path) => fs.existsSync(path));
17 |
18 | if (doesFileExist) {
19 | // Skip stub and return real
20 | return null;
21 | } else {
22 | console.log(`Fallback used for missing plugin ${realPath.replace(import.meta.dirname, '')}`);
23 | return path.resolve(import.meta.dirname, 'src/app/features/plugins-core/utils/missing-plugin.stub.ts');
24 | }
25 | }
26 | return null;
27 | },
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/palette/components/dragged-item/dragged-item.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, forwardRef } from 'react';
2 | import { createPortal } from 'react-dom';
3 | import styles from './dragged-item.module.css';
4 | import { isChrome, isFirefox, isOpera, isSafari } from '@/utils/browser';
5 |
6 | type DraggedItemProps = {
7 | zoom: number;
8 | };
9 |
10 | export const DraggedItem = forwardRef>(
11 | ({ children, zoom }: PropsWithChildren, ref) => {
12 | function getStyles() {
13 | if (isSafari && !isOpera && !isChrome) {
14 | return zoom > 1 ? { zoom: `${zoom}` } : { scale: `${zoom}` };
15 | }
16 |
17 | if (isFirefox) {
18 | return { scale: `${zoom}` };
19 | }
20 |
21 | return { zoom: `${zoom}` };
22 | }
23 |
24 | return createPortal(
25 |
26 | {children}
27 |
,
28 | document.body,
29 | );
30 | },
31 | );
32 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/store/slices/diagram-selection/actions.ts:
--------------------------------------------------------------------------------
1 | // About actions: apps/frontend/src/app/store/README.md
2 | import { OnSelectionChangeParams } from '@xyflow/react';
3 | import useStore from '@/store/store';
4 | import { WorkflowBuilderEdge, WorkflowBuilderNode } from '@workflow-builder/types/node-data';
5 |
6 | export function getStoreSelection(): OnSelectionChangeParams {
7 | const state = useStore.getState();
8 |
9 | const selectedNodes = state.selectedNodesIds
10 | .map((nodeId) => state.nodes.find(({ id }) => id === nodeId))
11 | .filter((value): value is WorkflowBuilderNode => !!value);
12 |
13 | const selectedEdges = state.selectedEdgesIds
14 | .map((edgeId) => state.edges.find(({ id }) => id === edgeId))
15 | .filter((value): value is WorkflowBuilderEdge => !!value);
16 |
17 | return {
18 | nodes: selectedNodes,
19 | edges: selectedEdges,
20 | };
21 | }
22 |
23 | export function resetStoreSelection() {
24 | useStore.setState({
25 | selectedNodesIds: [],
26 | selectedEdgesIds: [],
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/utils/get-scope.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * A helper method to simplify defining JSON Forms scope paths.
3 | * Reference: https://jsonforms.io/docs/uischema/controls/#scope-string
4 | *
5 | * Usage:
6 | * getScope("properties.label")
7 | *
8 | * Returns:
9 | * '#/properties/label'
10 | */
11 | export function getScope(path: PropertyPath | ''): string {
12 | return `#/${path.split('.').join('/')}`;
13 | }
14 |
15 | // Get all possible paths in an object type, treating arrays as terminal properties
16 | type PropertyPath = T extends object
17 | ? {
18 | [K in keyof T]: K extends string
19 | ? T[K] extends Array
20 | ? // If property is an array, it's a terminal path
21 | K
22 | : T[K] extends object
23 | ? // If property is an object, allow deeper paths
24 | K | `${K}.${PropertyPath}`
25 | : // Otherwise it's a terminal path
26 | K
27 | : never;
28 | }[keyof T]
29 | : never;
30 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/integration/components/save-button/save-button.tsx:
--------------------------------------------------------------------------------
1 | import { NavButton } from '@synergycodes/overflow-ui';
2 | import { Icon } from '@workflow-builder/icons';
3 | import { useTranslation } from 'react-i18next';
4 | import { useContext } from 'react';
5 | import { IntegrationContext } from '../integration-variants/context/integration-context-wrapper';
6 | import { SavingStatus } from '../saving-status/saving-status';
7 | import { useAutoSave } from '../../hooks/use-auto-save';
8 | import { useAutoSaveOnClose } from '../../hooks/use-auto-save-on-close';
9 |
10 | export function SaveButton() {
11 | const { t } = useTranslation();
12 | const { onSave } = useContext(IntegrationContext);
13 |
14 | function handleSave() {
15 | onSave({ isAutoSave: false });
16 | }
17 |
18 | useAutoSave();
19 | useAutoSaveOnClose();
20 |
21 | return (
22 |
23 | <>
24 |
25 |
26 | >
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/integration/components/import-export/import-export-modal.module.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --wb-import-error-color: var(--ax-public-snackbar-title-color);
3 | --wb-import-error-background-color: var(--ax-public-snackbar-error-background);
4 | --wb-import-error-border: var(--ax-public-snackbar-border-size) solid var(--ax-public-snackbar-error-border);
5 | --wb-import-error-border-radius: var(--ax-public-snackbar-border-radius);
6 | }
7 |
8 | .container {
9 | display: flex;
10 | flex-flow: column;
11 | gap: 0.75rem;
12 | width: 100%;
13 | }
14 |
15 | .tip {
16 | color: var(--ax-txt-secondary-default);
17 | }
18 |
19 | .error {
20 | padding: var(--ax-public-snackbar-padding);
21 | color: var(--wb-import-error-color);
22 | background: var(--wb-import-error-background-color);
23 | border: var(--wb-import-error-border);
24 | border-radius: var(--wb-import-error-border-radius);
25 | /* padding: var(--ax-public-snackbar-padding) */
26 | }
27 |
28 | .actions {
29 | display: flex;
30 | gap: 0.5rem;
31 | justify-content: flex-end;
32 | }
33 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/palette/hooks/use-palette-drag-and-drop.tsx:
--------------------------------------------------------------------------------
1 | import useStore from '@/store/store';
2 | import { useRef, DragEvent } from 'react';
3 | import { dataFormat } from '@/utils/consts';
4 |
5 | export function usePaletteDragAndDrop(canDrag: boolean) {
6 | const setDraggedItem = useStore((state) => state.setDraggedItem);
7 | const draggedItem = useStore((state) => state.draggedItem);
8 | const zoom = useStore((state) => state.reactFlowInstance?.getZoom() || 1);
9 |
10 | const ref = useRef(null);
11 |
12 | function onMouseDown(type: string) {
13 | if (canDrag) {
14 | setDraggedItem({ type });
15 | }
16 | }
17 |
18 | function onDragStart(event: DragEvent) {
19 | if (!canDrag) {
20 | return event.preventDefault();
21 | }
22 | event.dataTransfer.setDragImage(ref.current as Element, 0, 0);
23 | event.dataTransfer.setData(dataFormat, JSON.stringify(draggedItem));
24 | }
25 |
26 | return {
27 | draggedItem,
28 | zoom,
29 | ref,
30 | onMouseDown,
31 | onDragStart,
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/app-bar/functions/get-controls-dots-items.tsx:
--------------------------------------------------------------------------------
1 | import i18n from 'i18next';
2 | import { withOptionalFunctionPlugins } from '@/features/plugins-core/adapters/adapter-functions';
3 |
4 | import { MenuItemProps } from '@synergycodes/overflow-ui';
5 | import { Icon } from '@workflow-builder/icons';
6 |
7 | import { openExportModal } from '@/features/integration/components/import-export/export-modal/open-export-modal';
8 | import { openImportModal } from '@/features/integration/components/import-export/import-modal/open-import-modal';
9 |
10 | function getControlsDotsItemsFunction(): MenuItemProps[] {
11 | return [
12 | {
13 | label: i18n.t('importExport.export'),
14 | icon: ,
15 | onClick: openExportModal,
16 | },
17 | {
18 | label: i18n.t('importExport.import'),
19 | icon: ,
20 | onClick: openImportModal,
21 | },
22 | ];
23 | }
24 |
25 | export const getControlsDotsItems = withOptionalFunctionPlugins(getControlsDotsItemsFunction, 'getControlsDotsItems');
26 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/app.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | position: absolute;
3 | display: flex;
4 | flex-direction: column;
5 | height: 100%;
6 | width: 100%;
7 | overflow: hidden;
8 |
9 | .header {
10 | display: flex;
11 | justify-content: space-between;
12 | box-sizing: border-box;
13 | width: 100%;
14 | padding: 1rem;
15 | z-index: 10;
16 | gap: 1rem;
17 | pointer-events: none;
18 | }
19 |
20 | .content {
21 | position: relative;
22 | width: 100%;
23 | height: 100%;
24 | padding: 0 1rem 1rem 1rem;
25 | box-sizing: border-box;
26 | display: flex;
27 | justify-content: space-between;
28 | overflow: hidden;
29 |
30 | .panel {
31 | height: 100%;
32 | display: flex;
33 | z-index: 1;
34 | pointer-events: none;
35 | }
36 | .right-panel {
37 | display: flex;
38 | flex-direction: column;
39 | gap: 1rem;
40 | align-items: end;
41 | justify-content: space-between;
42 | }
43 | }
44 |
45 | .diagram-container {
46 | position: absolute;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/delay/select-options.ts:
--------------------------------------------------------------------------------
1 | export const delayTypeOptions = {
2 | fixed: { label: 'Fixed Delay', value: 'fixedDelay', icon: 'Clock' },
3 | dynamic: {
4 | label: 'Dynamic Delay',
5 | value: 'dynamicDelay',
6 | icon: 'HourglassSimpleHigh',
7 | },
8 | conditional: {
9 | label: 'Conditional Delay',
10 | value: 'conditionalDelay',
11 | icon: 'ListChecks',
12 | },
13 | untilSpecific: {
14 | label: 'Until Specific Date/Time',
15 | value: 'untilSpecificDateTime',
16 | icon: 'CalendarDot',
17 | },
18 | } as const;
19 |
20 | export const timeUnitsOptions = {
21 | none: { label: 'None', value: 'none' },
22 | minutes: { label: 'Minutes', value: 'minutes' },
23 | hours: { label: 'Hours', value: 'hours' },
24 | } as const;
25 |
26 | export const maxWaitTimeOptions = {
27 | hours24: { label: '24 hours', value: '24' },
28 | hours12: { label: '12 hours', value: '12' },
29 | hours8: { label: '8 hours', value: '8' },
30 | hours4: { label: '4 hours', value: '4' },
31 | hours2: { label: '2 hours', value: '2' },
32 | } as const;
33 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/ai-agent/select-options.ts:
--------------------------------------------------------------------------------
1 | import { ItemOption } from '@workflow-builder/types/node-schema';
2 | export const toolOptions = {
3 | gmail: { label: 'Gmail', value: 'gmail', icon: 'GoogleLogo' },
4 | excel: { label: 'Excel', value: 'excel', icon: 'MicrosoftExcelLogo' },
5 | airtable: { label: 'Airtable', value: 'airtable', icon: 'AirtableLogo' },
6 | jira: { label: 'Jira', value: 'jira', icon: 'JiraLogo' },
7 | slack: { label: 'Slack', value: 'slack', icon: 'SlackLogo' },
8 | hubspot: { label: 'Hubspot', value: 'hubspot', icon: 'HubspotLogo' },
9 | } as Record;
10 |
11 | export const chatModelOptions = {
12 | gpt: { label: 'GPT-4', value: 'gpt40', icon: 'OpenAiLogo' },
13 | gemini: { label: 'Gemini 2.5 Pro', value: 'gemini2.5pro', icon: 'GeminiLogo' },
14 | claude: { label: 'Claude 3.7 Sonet', value: 'claude3.7sonet', icon: 'ClaudeLogo' },
15 | } as Record;
16 |
17 | export const memoryOptions = {
18 | system: { label: 'Windows System Memory', value: 'system', icon: 'Database' },
19 | } as Record;
20 |
--------------------------------------------------------------------------------
/apps/icons/assets/hubspot-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/decision/schema.ts:
--------------------------------------------------------------------------------
1 | import { statusOptions } from '../shared/general-information';
2 | import { sharedProperties } from '../shared/shared-properties';
3 | import { NodeSchema } from '@workflow-builder/types/node-schema';
4 |
5 | const conditions = {
6 | type: 'array',
7 | items: {
8 | type: 'object',
9 | properties: {
10 | x: { type: 'string' },
11 | comparisonOperator: { type: 'string' },
12 | y: { type: 'string' },
13 | logicalOperator: { type: 'string' },
14 | },
15 | },
16 | } as const;
17 |
18 | const decisionBranches = {
19 | type: 'array',
20 | items: {
21 | type: 'object',
22 | properties: {
23 | conditions,
24 | },
25 | },
26 | } as const;
27 |
28 | export const schema = {
29 | type: 'object',
30 | required: ['label'],
31 | properties: {
32 | ...sharedProperties,
33 | status: {
34 | type: 'string',
35 | options: Object.values(statusOptions),
36 | },
37 | decisionBranches,
38 | },
39 | } satisfies NodeSchema;
40 |
41 | export type DecisionNodeSchema = typeof schema;
42 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/components/loader/loader.module.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --wb-loader-background-opacity: 1;
3 | --wb-panel-background-color: var(--ax-ui-bg-primary-default);
4 | }
5 |
6 | .container {
7 | position: absolute;
8 | width: 100%;
9 | height: 100%;
10 | background-color: var(--wb-panel-background-color);
11 | z-index: 100;
12 | animation: fade-in 0.3s both;
13 |
14 | &.fade-out {
15 | animation: fade-out 0.3s both;
16 | }
17 |
18 | .loader {
19 | display: flex;
20 | align-content: center;
21 | justify-content: center;
22 | align-items: center;
23 | width: 100%;
24 | height: 100%;
25 | font-size: 1.25rem;
26 | }
27 | }
28 |
29 | @keyframes fade-in {
30 | from {
31 | display: block;
32 | visibility: visible;
33 | opacity: 0;
34 | }
35 |
36 | to {
37 | opacity: var(--wb-loader-background-opacity);
38 | }
39 | }
40 |
41 | @keyframes fade-out {
42 | from {
43 | opacity: var(--wb-loader-background-opacity);
44 | }
45 |
46 | to {
47 | opacity: 0;
48 | visibility: hidden;
49 | display: none;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/i18n/i18next.d.ts:
--------------------------------------------------------------------------------
1 | import 'i18next';
2 | import { defaultNS } from '.';
3 | import { en } from './locales/en';
4 |
5 | type PluginResources = {
6 | readonly translation: {
7 | readonly plugins: {
8 | readonly [pluginName: string]: {
9 | readonly [key: string]: string;
10 | };
11 | };
12 | };
13 | };
14 |
15 | type EnglishTranslationMap = typeof en;
16 | type DefaultResources = { translation: EnglishTranslationMap };
17 | type Resources = DefaultResources & PluginResources;
18 |
19 | declare module 'i18next' {
20 | interface CustomTypeOptions {
21 | defaultNS: typeof defaultNS;
22 | resources: Resources;
23 | returnNull: false;
24 | keySeparator: '.';
25 | nsSeparator: ':';
26 | strictKeyChecks: true;
27 | }
28 | }
29 |
30 | export type DefaultTranslationMap = DeepReplace;
31 | type DeepReplace = T extends object
32 | ? T extends readonly unknown[] | null
33 | ? LeafValue
34 | : {
35 | [K in keyof T]: DeepReplace;
36 | }
37 | : LeafValue;
38 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/integration/types.ts:
--------------------------------------------------------------------------------
1 | import { LayoutDirection } from '@workflow-builder/types/common';
2 | import { WorkflowBuilderEdge, WorkflowBuilderNode } from '@workflow-builder/types/node-data';
3 |
4 | export type IntegrationDataFormat = {
5 | name: string;
6 | layoutDirection: LayoutDirection;
7 | nodes: WorkflowBuilderNode[];
8 | edges: WorkflowBuilderEdge[];
9 | };
10 |
11 | export type IntegrationDataFormatOptional = Partial;
12 |
13 | export type OnSaveParams = { isAutoSave?: boolean };
14 |
15 | type DidSaveStatus = 'error' | 'success' | 'alreadyStarted';
16 |
17 | /*
18 | The OnSave function is used throughout the Workflow Builder application.
19 |
20 | You can call it from anywhere, and it will trigger the onSave action in the integration wrapper.
21 | */
22 | export type OnSave = (savingParams?: OnSaveParams) => Promise;
23 |
24 | // Only used in through props strategy (it calls callback onDataSave={onDataSave})
25 | export type OnSaveExternal = (data: IntegrationDataFormat, savingParams?: OnSaveParams) => Promise;
26 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-control.module.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --wb-conditions-form-border-color: var(--ax-ui-stroke-primary-default); /* missing token */
3 | --wb-conditions-form-border-radius: 0.75rem; /* missing token */
4 | --wb-conditions-form-header-color: var(--ax-public-form-label-color);
5 |
6 | --wb-conditions-form-tag-bg-color: var(--ax-chips-neutral-bg);
7 | }
8 |
9 | .container {
10 | padding: 0.5rem;
11 | border: 1px solid var(--wb-conditions-form-border-color);
12 | border-radius: var(--wb-conditions-form-border-radius);
13 | }
14 |
15 | .header {
16 | display: flex;
17 | justify-content: space-between;
18 | align-items: center;
19 | gap: 0.375rem;
20 | margin-bottom: 0.75rem;
21 | font-weight: 600;
22 | color: var(--wb-conditions-form-header-color);
23 | }
24 |
25 | .title {
26 | line-height: 1;
27 | }
28 |
29 | .tag {
30 | composes: ax-public-p10 from global;
31 |
32 | padding: 0.25rem 0.5rem;
33 | border-radius: 0.25rem;
34 | background-color: var(--wb-conditions-form-tag-bg-color);
35 | margin-right: auto;
36 | }
37 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/syntax-highlighter/components/syntax-highlighter-lazy.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { TextArea } from '@synergycodes/overflow-ui';
3 |
4 | import type { SyntaxHighlighterProps } from './syntax-highlighter';
5 |
6 | const SyntaxHighlighter = React.lazy(() =>
7 | import('./syntax-highlighter').then((module) => ({ default: module.SyntaxHighlighter })),
8 | );
9 |
10 | type SyntaxHighlighterLazyProps = SyntaxHighlighterProps;
11 |
12 | export function SyntaxHighlighterLazy(props: SyntaxHighlighterLazyProps) {
13 | const { value, onChange, isDisabled } = props;
14 |
15 | return (
16 | (onChange ? onChange(event.target.value) : undefined)}
21 | maxRows={10}
22 | disabled={isDisabled}
23 | />
24 | }
25 | >
26 | (onChange ? onChange(value || '') : undefined)}
29 | isDisabled={isDisabled}
30 | />
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/components/sidebar/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import styles from './sidebar.module.css';
3 | import './variables.css';
4 |
5 | import { Separator } from '@synergycodes/overflow-ui';
6 |
7 | type SidebarProps = React.HTMLAttributes & {
8 | isExpanded: boolean;
9 | children?: React.ReactNode;
10 | header?: React.ReactNode;
11 | footer?: React.ReactNode;
12 | contentClassName?: string;
13 | };
14 |
15 | export function Sidebar({ isExpanded, children, className, header, footer, contentClassName, ...props }: SidebarProps) {
16 | return (
17 |
18 |
{header}
19 | {isExpanded && (
20 | <>
21 |
22 |
{children}
23 | {footer && (
24 | <>
25 |
26 |
{footer}
27 | >
28 | )}
29 | >
30 | )}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/apps/icons/assets/workflow-builder-logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/controls/control-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import { FormControlWithLabel } from '@/components/form/form-control-with-label/form-control-with-label';
2 | import { BaseControlProps } from '../types/controls';
3 | import { IndicatorDot } from '../components/indicator-dot/indicator-dot';
4 |
5 | type Props = BaseControlProps & {
6 | children: React.ReactNode;
7 | };
8 |
9 | export function ControlWrapper({ children, uischema, errors, ...props }: Props) {
10 | const { required, visible } = props;
11 | const { label, errorIndicatorEnabled = true } = uischema;
12 |
13 | if (!visible) {
14 | return;
15 | }
16 |
17 | const hasLabel = typeof label === 'string';
18 | const showIndicatorDot = errors.length > 0 && errorIndicatorEnabled;
19 | const childrenControl = showIndicatorDot ? {children} : children;
20 |
21 | return (
22 | <>
23 | {hasLabel ? (
24 |
25 | {childrenControl}
26 |
27 | ) : (
28 | <>{childrenControl}>
29 | )}
30 | >
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/palette/components/items/palette-items.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import styles from './palette-items.module.css';
3 | import { NodePreviewContainer } from '../../node-preview-container';
4 | import { PaletteItem } from '@workflow-builder/types/common';
5 | import { DragEvent } from 'react';
6 |
7 | type PaletteItemsProps = {
8 | onDragStart: (event: DragEvent) => void;
9 | onMouseDown: (type: string) => void;
10 | items: PaletteItem[];
11 | isDisabled?: boolean;
12 | };
13 |
14 | export function PaletteItems({ items, onDragStart, onMouseDown, isDisabled = false }: PaletteItemsProps) {
15 | return (
16 |
17 | {items.map((item) => (
18 |
onMouseDown(item.type)}
25 | onDragStart={onDragStart}
26 | >
27 |
28 |
29 | ))}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/apps/frontend/src/assets/workflow-builder-logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/components/indicator-dot/indicator-dot.module.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --wb-indicator-dot-size: 0.75rem;
3 | --wb-indicator-dot-radius: calc(var(--wb-indicator-dot-size) / 2);
4 | --wb-indicator-dot-background-color: var(--ax-colors-orange-400); /* missing token */
5 | --wb-indicator-dot-pulse-radius-size: 0.625rem;
6 | }
7 |
8 | .with-indicator-dot {
9 | position: relative;
10 |
11 | &::before {
12 | content: '';
13 | position: absolute;
14 | background: var(--wb-indicator-dot-background-color);
15 | border-radius: 50%;
16 | height: var(--wb-indicator-dot-size);
17 | width: var(--wb-indicator-dot-size);
18 | animation: pulse 2s infinite;
19 | left: calc(-1 * var(--wb-sidebar-horizontal-padding) - var(--wb-indicator-dot-radius));
20 | top: calc(50% - var(--wb-indicator-dot-radius));
21 | }
22 | }
23 |
24 | @keyframes pulse {
25 | 0% {
26 | box-shadow: 0 0 0 0 var(--wb-indicator-dot-background-color);
27 | }
28 |
29 | 70% {
30 | box-shadow: 0 0 0 var(--wb-indicator-dot-pulse-radius-size) transparent;
31 | }
32 |
33 | 100% {
34 | box-shadow: 0 0 0 0 transparent;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/components/loader/loader.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, memo } from 'react';
2 | import { clsx } from 'clsx';
3 | import styles from './loader.module.css';
4 | import { useTranslation } from 'react-i18next';
5 |
6 | type LoaderType = {
7 | isLoading?: boolean;
8 | isSemiTransparent?: boolean;
9 | };
10 |
11 | interface CSSCustomProperties extends CSSProperties {
12 | '--wb-loader-background-opacity': number;
13 | }
14 |
15 | const semiTransparentOpacityVariable: CSSCustomProperties = {
16 | '--wb-loader-background-opacity': 0.8,
17 | };
18 |
19 | export const Loader = memo(({ isLoading, isSemiTransparent }: LoaderType) => {
20 | const { t } = useTranslation();
21 |
22 | const visibilityClassName = isLoading ? styles['fade-in'] : styles['fade-out'];
23 | const setLoaderBackgroundOpacityVariable = isSemiTransparent ? semiTransparentOpacityVariable : {};
24 |
25 | if (!isLoading) {
26 | return null;
27 | }
28 |
29 | return (
30 |
31 |
{t('loader.text')}
32 |
33 | );
34 | });
35 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/diagram/edges/label-edge/use-label-edge-hover.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import useStore from '@/store/store';
3 | import { EdgeState, useEdgeStyle } from '@synergycodes/overflow-ui';
4 |
5 | type UseLabelEdgeHoverParams = {
6 | id: string;
7 | isSelected?: boolean;
8 | };
9 |
10 | export function useLabelEdgeHover({ id, isSelected }: UseLabelEdgeHoverParams) {
11 | const draggedSegmentDestinationId = useStore((state) => state.draggedSegmentDestinationId);
12 | const [labelHovered, setLabelHovered] = useState(false);
13 | const edgeHovered = useStore((state) => state.hoveredElement === id);
14 | const hovered = (labelHovered || edgeHovered) && !draggedSegmentDestinationId;
15 |
16 | const edgeState: EdgeState = isSelected ? 'selected' : 'default';
17 | const style = useEdgeStyle({ state: edgeState, isHovered: hovered });
18 |
19 | function handleMouseEnter() {
20 | setLabelHovered(true);
21 | }
22 |
23 | function handleMouseLeave() {
24 | setLabelHovered(false);
25 | }
26 |
27 | return {
28 | style,
29 | hovered,
30 | onMouseEnter: handleMouseEnter,
31 | onMouseLeave: handleMouseLeave,
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/diagram/nodes/components/connectable-item/connectable-item.module.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --wb-connectable-item-horizontal-padding: 0.75rem;
3 | --wb-connectable-item-vertical-padding: 0.625rem;
4 | --wb-connectable-item-background: var(--ax-ui-bg-tertiary-default);
5 | --wb-connectable-item-border-width: 0.0625rem;
6 | --wb-connectable-item-border-color: var(--ax-input-stroke-primary-default);
7 | --wb-connectable-item-border-radius: 0.375rem;
8 | }
9 |
10 | .connectable-item {
11 | composes: ax-public-p11 from global;
12 |
13 | position: relative;
14 | display: flex;
15 | padding: var(--wb-connectable-item-vertical-padding) var(--wb-connectable-item-horizontal-padding);
16 | background-color: var(--wb-connectable-item-background);
17 | border: var(--wb-connectable-item-border-width) solid var(--wb-connectable-item-border-color);
18 | border-radius: var(--wb-connectable-item-border-radius);
19 | justify-content: space-between;
20 |
21 | .handle-container {
22 | position: relative;
23 |
24 | &.vertical {
25 | position: absolute;
26 | bottom: 0;
27 | left: 50%;
28 | transform: translateX(-50%);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/controls/dynamic-conditions-control/dynamic-conditions-form-field/conditions-form-field.module.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --wb-conditions-form-inputs-background: var(--ax-ui-bg-secondary-default); /* missing token */
3 | --wb-conditions-form-input-background: var(--ax-ui-bg-primary-default); /* missing token */
4 | --wb-conditions-form-input-background-destructive: var(--ax-colors-red-400-10); /* missing token */
5 | }
6 |
7 | .container {
8 | display: flex;
9 | align-items: center;
10 | gap: 0.125rem;
11 | padding: 0.25rem 0.375rem;
12 | border-radius: 0.5rem;
13 | background-color: var(--wb-conditions-form-inputs-background);
14 |
15 | &.container-error {
16 | background-color: var(--wb-conditions-form-input-background-destructive);
17 | }
18 | }
19 |
20 | .inputs-container {
21 | display: flex;
22 | flex-flow: column;
23 | gap: 0.125rem;
24 | width: 100%;
25 | }
26 |
27 | .input {
28 | background-color: var(--wb-conditions-form-input-background);
29 | }
30 |
31 | .segment-picker-container {
32 | display: flex;
33 | justify-content: center;
34 | align-items: center;
35 |
36 | .segment-picker {
37 | width: 150px;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/plugins-core/adapters/adapter-i18n.ts:
--------------------------------------------------------------------------------
1 | type Resource = {
2 | [lang: string]: {
3 | translation: {
4 | [key: string]: {
5 | [key: string]: string | { [key: string]: string };
6 | };
7 | };
8 | };
9 | };
10 |
11 | function mergePluginsTranslations(a: Resource, b: Resource): Resource {
12 | const langsB = Object.keys(b);
13 |
14 | return {
15 | ...a,
16 | ...langsB.reduce((stack: Resource, lang) => {
17 | stack[lang] = {
18 | ...a?.[lang],
19 | translation: {
20 | ...a[lang]?.translation,
21 | plugins: {
22 | ...a[lang]?.translation?.plugins,
23 | ...b[lang].translation.plugins,
24 | },
25 | },
26 | };
27 |
28 | return stack;
29 | }, {}),
30 | };
31 | }
32 |
33 | let pluginsResource: Resource = {};
34 |
35 | export function withOptionalComponentPluginsTranslation(i18nResource: Resource): Resource {
36 | return mergePluginsTranslations(i18nResource, pluginsResource);
37 | }
38 |
39 | export function registerPluginTranslation(pluginResourceToAdd: Resource) {
40 | pluginsResource = mergePluginsTranslations(pluginsResource, pluginResourceToAdd);
41 | }
42 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/shared/general-information.ts:
--------------------------------------------------------------------------------
1 | import type { UISchema } from '@/features/json-form/types/uischema';
2 |
3 | export const statusOptions = {
4 | active: { label: 'Active', value: 'active', icon: 'StatusActive' },
5 | draft: { label: 'Draft', value: 'draft', icon: 'StatusDraft' },
6 | disabled: { label: 'Disabled', value: 'disabled', icon: 'StatusDisabled' },
7 | } as const;
8 |
9 | export const generalInformation: UISchema = {
10 | type: 'Accordion',
11 | label: 'General Information',
12 | rule: {
13 | effect: 'SHOW',
14 | condition: {
15 | scope: '#',
16 | schema: {
17 | required: ['type'],
18 | },
19 | },
20 | },
21 | elements: [
22 | {
23 | type: 'Text',
24 | scope: '#/properties/label',
25 | label: 'Title',
26 | placeholder: 'Node Title...',
27 | },
28 | {
29 | type: 'Select',
30 | scope: '#/properties/status',
31 | options: Object.values(statusOptions),
32 | label: 'Status',
33 | },
34 | {
35 | type: 'Text',
36 | scope: '#/properties/description',
37 | label: 'Description',
38 | placeholder: 'Type your description here...',
39 | },
40 | ],
41 | };
42 |
--------------------------------------------------------------------------------
/apps/icons/README.md:
--------------------------------------------------------------------------------
1 | # @workflow-builder/icons
2 |
3 | To achieve type-safety, flexibility and small bundle size, we've implemented a lazy-loaded icons mechanism based on:
4 |
5 | - `generate-icons` script (to create types and icon chunks)
6 | - `icons.gen.ts` - the generated part that provides the API
7 | - `Icon` component that leverages the generated code and lazy-loads the chunks
8 |
9 | ## Usage
10 |
11 | To use an icon in your component, import it from `@workflow-builder/icons` and pass the `name` prop:
12 |
13 | ```typescriptreact
14 | import { Icon } from "@workflow-builder/icons";
15 |
16 | ```
17 |
18 | You may provide more props and extend their type directly in the `icon` file.
19 |
20 | ## Extending
21 |
22 | 1. Add new icon sources to `ICON_SOURCES` in `generate-icons` file:
23 |
24 | ```typescriptreact
25 | const ICON_SOURCES: IconSource[] = [
26 | //...
27 | {
28 | path: '../../../assets/icons/',
29 | },
30 | {
31 | path: '../../node_modules/some-cool-icons/dist/svg'
32 | }
33 | ];
34 | ```
35 |
36 | 2. Generate the icons using `pnpm i`. It will trigger the `prepare` script of `@workflow-builder/icons` package.
37 |
38 | 3. Commit the newly generated `icons.gen.ts` file
39 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/changes-tracker/stores/use-changes-tracker-store.ts:
--------------------------------------------------------------------------------
1 | import { withOptionalFunctionPlugins } from '@/features/plugins-core/adapters/adapter-functions';
2 | import { create } from 'zustand';
3 | import { devtools } from 'zustand/middleware';
4 |
5 | const initTimestamp = Date.now();
6 |
7 | type ChangesTrackerStore = {
8 | lastChangeName: string;
9 | lastChangeParams: object;
10 | lastChangeTimestamp: number;
11 | };
12 |
13 | const emptyStore: ChangesTrackerStore = {
14 | lastChangeName: '',
15 | lastChangeParams: {},
16 | lastChangeTimestamp: initTimestamp,
17 | };
18 |
19 | export const useChangesTrackerStore = create()(
20 | devtools(
21 | () =>
22 | ({
23 | ...emptyStore,
24 | }) satisfies ChangesTrackerStore,
25 | { name: 'changesTrackerStore' },
26 | ),
27 | );
28 |
29 | function trackFutureChangeFunction(changeName: string, params?: object) {
30 | useChangesTrackerStore.setState({
31 | lastChangeName: changeName,
32 | lastChangeParams: params || {},
33 | lastChangeTimestamp: Date.now(),
34 | });
35 | }
36 |
37 | export const trackFutureChange = withOptionalFunctionPlugins(trackFutureChangeFunction, 'trackFutureChange');
38 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/layouts/horizontal-layout/horizontal-layout.tsx:
--------------------------------------------------------------------------------
1 | import styles from './horizontal-layout.module.css';
2 | import { LayoutWrapper } from '../layout-wrapper';
3 | import { HorizontalLayoutElement, LayoutProps } from '../../types/layouts';
4 | import { renderElements } from '../render-elements';
5 | import { createLayoutRenderer } from '../../utils/rendering';
6 | import { CSSProperties, useMemo } from 'react';
7 | import { useHasChildError } from './use-has-child-error';
8 |
9 | function HorizontalLayout(props: LayoutProps) {
10 | const hasErrors = useHasChildError(props.uischema.elements);
11 |
12 | const { uischema } = props;
13 | const { layoutColumns } = uischema;
14 |
15 | const style: CSSProperties = useMemo(
16 | () => (layoutColumns ? { gridAutoColumns: layoutColumns } : {}),
17 | [layoutColumns],
18 | );
19 |
20 | return (
21 |
22 |
23 | {renderElements(props)}
24 |
25 |
26 | );
27 | }
28 |
29 | export const horizontalLayoutRenderer = createLayoutRenderer('HorizontalLayout', HorizontalLayout);
30 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/diagram/nodes/components/connectable-item/connectable-item.tsx:
--------------------------------------------------------------------------------
1 | import { Handle, HandleType, Position } from '@xyflow/react';
2 | import styles from './connectable-item.module.css';
3 | import clsx from 'clsx';
4 | import { getHandleId } from '@/features/diagram/handles/get-handle-id';
5 | import useStore from '@/store/store';
6 |
7 | type Props = {
8 | nodeId: string;
9 | innerId: string;
10 | handleType: HandleType;
11 | label: string;
12 | canHaveBottomHandle?: boolean;
13 | };
14 |
15 | export function ConnectableItem({ label, nodeId, innerId, handleType, canHaveBottomHandle = true }: Props) {
16 | const layoutDirection = useStore(({ layoutDirection }) => layoutDirection);
17 | const isVertical = layoutDirection === 'DOWN' && canHaveBottomHandle;
18 | const position = isVertical ? Position.Bottom : Position.Right;
19 |
20 | const handleId = getHandleId({ nodeId, innerId, handleType });
21 |
22 | return (
23 |
24 | {label}
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/utils/validation/get-node-errors.ts:
--------------------------------------------------------------------------------
1 | import Ajv from 'ajv';
2 | import { WorkflowBuilderNode } from '@workflow-builder/types/node-data';
3 | import { flatErrors } from './flat-errors';
4 | import { getNodeDefinition } from './get-node-definition';
5 |
6 | export function getNodeErrors(node?: WorkflowBuilderNode) {
7 | const definition = getNodeDefinition(node);
8 |
9 | if (!node || !definition) {
10 | return [];
11 | }
12 |
13 | const { schema } = definition;
14 | // jsonforms uses Ajv but doesn't call strick
15 | const ajv = new Ajv({ allErrors: true, strict: false });
16 |
17 | const validate = ajv.compile(schema);
18 | const isValid = validate(node.data.properties);
19 |
20 | if (isValid) {
21 | return [];
22 | }
23 |
24 | const flattenErrors = flatErrors(validate.errors);
25 |
26 | return flattenErrors;
27 | }
28 |
29 | export function getNodeWithErrors(node: WorkflowBuilderNode) {
30 | const errors = getNodeErrors(node);
31 |
32 | if (errors.length === 0) {
33 | return node;
34 | }
35 |
36 | return {
37 | ...node,
38 | data: {
39 | ...node.data,
40 | properties: {
41 | ...node.data.properties,
42 | errors,
43 | },
44 | },
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/decision/uischema.ts:
--------------------------------------------------------------------------------
1 | import { UISchema } from '@/features/json-form/types/uischema';
2 | import { getScope } from '@/features/json-form/utils/get-scope';
3 | import { DecisionNodeSchema } from './schema';
4 |
5 | const scope = getScope;
6 |
7 | const generalInformation: UISchema = {
8 | type: 'Accordion',
9 | label: 'General Settings',
10 | elements: [
11 | {
12 | type: 'Text',
13 | scope: scope('properties.label'),
14 | label: 'Title',
15 | placeholder: 'Node Title...',
16 | },
17 | {
18 | type: 'Text',
19 | scope: scope('properties.description'),
20 | label: 'Description',
21 | placeholder: 'Type your description here...',
22 | },
23 | {
24 | type: 'Select',
25 | scope: scope('properties.status'),
26 | label: 'Status',
27 | },
28 | ],
29 | } as const;
30 |
31 | const decisionSettings: UISchema = {
32 | type: 'Accordion',
33 | label: 'Decision Settings',
34 | elements: [
35 | {
36 | type: 'DecisionBranches',
37 | scope: scope('properties.decisionBranches'),
38 | },
39 | ],
40 | } as const;
41 |
42 | export const uischema: UISchema = {
43 | type: 'VerticalLayout',
44 | elements: [generalInformation, decisionSettings],
45 | };
46 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/action/default-properties-data.ts:
--------------------------------------------------------------------------------
1 | import { NodeDataProperties } from '@/features/json-form/types/default-properties';
2 | import { ActionNodeSchema } from './schema';
3 |
4 | export const defaultPropertiesData: NodeDataProperties = {
5 | label: 'node.action.label',
6 | description: 'node.action.description',
7 | status: 'active',
8 | sendEmail: {
9 | priority: 'normal',
10 | retryOnFailure: false,
11 | retries: 3,
12 | },
13 | updateRecord: {
14 | dataSource: 'crmSystem',
15 | objectType: 'order',
16 | recordId: '{{order.id}}',
17 | includeData: false,
18 | },
19 | makeAPICall: {
20 | apiUrl: 'https://api.example.com/update_status',
21 | httpMethod: 'get',
22 | responseFormat: 'json',
23 | storeResponse: 'orderUpdateResponse',
24 | retryOnFailure: false,
25 | },
26 | createRecord: {
27 | dataSource: 'hubspot',
28 | objectType: 'lead',
29 | assign: 'salesTeam',
30 | },
31 | executeScript: {
32 | scriptLanguage: 'javaScript',
33 | scriptStoring: 'orderUpdateResponse',
34 | passWorkflow: false,
35 | },
36 | createDocument: {
37 | template: 'invoiceTemplate',
38 | outputFormat: 'pdf',
39 | saveLocation: 'googleDrive',
40 | sendDocument: false,
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/utils/ensure-bounds.ts:
--------------------------------------------------------------------------------
1 | import { NodeChange, Node } from '@xyflow/react';
2 | import {
3 | addNodeChangedListener,
4 | removeNodeChangedListener,
5 | } from '../features/diagram/listeners/node-changed-listeners';
6 |
7 | const FALLBACK_TIMEOUT = 1000;
8 |
9 | export function ensureBounds(nodes: Node[], finishCallback?: () => void) {
10 | if (nodes.length === 0) {
11 | return;
12 | }
13 |
14 | const idSet = new Map(nodes.map((node) => [node.id, false]));
15 |
16 | return new Promise((resolve) => {
17 | const fallbackRejectId = setTimeout(() => {
18 | removeNodeChangedListener(callback);
19 | finishCallback?.();
20 | resolve();
21 | }, FALLBACK_TIMEOUT);
22 |
23 | function callback(changes: NodeChange[]) {
24 | for (const change of changes) {
25 | if (change.type !== 'dimensions') {
26 | continue;
27 | }
28 |
29 | if (idSet.has(change.id)) {
30 | idSet.set(change.id, true);
31 | }
32 | }
33 |
34 | if ([...idSet.values()].every((value) => !!value)) {
35 | clearTimeout(fallbackRejectId);
36 | removeNodeChangedListener(callback);
37 | finishCallback?.();
38 | resolve();
39 | }
40 | }
41 |
42 | addNodeChangedListener(callback);
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/apps/types/src/node-data.ts:
--------------------------------------------------------------------------------
1 | import type { IconType } from './common';
2 | import type { Edge, Node } from '@xyflow/react';
3 | import type { UISchema } from '../../frontend/src/app/features/json-form/types/uischema';
4 | import type { NodeDataProperties } from '../../frontend/src/app/features/json-form/types/default-properties';
5 | import type { BaseNodeProperties, NodeSchema } from './node-schema';
6 | import { NodeType } from './node-types';
7 |
8 | export type WorkflowBuilderNode = Node;
9 | export type WorkflowBuilderEdge = Edge;
10 |
11 | export type NodeDefinition = {
12 | schema: T;
13 | /** default values of schema based properties */
14 | defaultPropertiesData: NodeDataProperties;
15 | /** describes how the form looks like and to which fields data properties should be mapped */
16 | uischema?: UISchema;
17 | } & Required &
18 | Pick;
19 |
20 | export type NodeData> = {
21 | segments?: []; // TODO: Add segments back, it's a placeholder suggestion where to hold segments data
22 | templateType?: NodeType;
23 | properties: T;
24 | icon: IconType;
25 | type: string;
26 | };
27 |
28 | export type EdgeData = {
29 | label?: string;
30 | icon?: IconType;
31 | };
32 |
--------------------------------------------------------------------------------
/apps/types/src/node-validation-schema.ts:
--------------------------------------------------------------------------------
1 | /*
2 | https://json-schema.org/understanding-json-schema/reference/conditionals
3 | */
4 |
5 | export type ObjectFieldRequiredValidationSchema = {
6 | type?: 'object';
7 | required?: string[];
8 | };
9 |
10 | export type ObjectFieldValidationSchema = ObjectFieldRequiredValidationSchema & {
11 | properties: Record;
12 | };
13 |
14 | export type StringFieldValidationSchema = {
15 | type: 'string';
16 | minLength?: number;
17 | maxLength?: number;
18 | pattern?: string;
19 | format?: string;
20 | };
21 |
22 | export type NumberFieldValidationSchema = {
23 | type: 'number';
24 | minimum?: number;
25 | maximum?: number;
26 | exclusiveMinimum?: number;
27 | exclusiveMaximum?: number;
28 | multipleOf?: number;
29 | };
30 |
31 | export type FieldValidationSchema =
32 | | ObjectFieldValidationSchema
33 | | StringFieldValidationSchema
34 | | NumberFieldValidationSchema;
35 |
36 | export type IfThenElseSchema = {
37 | if: SchemaCondition;
38 | then?: ConditionalSchema;
39 | else?: ConditionalSchema;
40 | };
41 |
42 | export type SchemaCondition = {
43 | properties: Record;
44 | };
45 |
46 | export type ConditionalSchema = {
47 | properties: Record;
48 | };
49 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/integration/components/saving-status/saving-status.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { Icon } from '@workflow-builder/icons';
3 | import styles from './saving-status.module.css';
4 | import { useIntegrationStore } from '../../stores/use-integration-store';
5 |
6 | export function SavingStatus() {
7 | // lastSaveAttemptTimestamp is used here as a key to reset the animation on each save
8 | const lastSaveAttemptTimestamp = useIntegrationStore((state) => state.lastSaveAttemptTimestamp);
9 | const savingStatus = useIntegrationStore((state) => state.savingStatus);
10 |
11 | if (savingStatus === 'saving') {
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 |
19 | if (savingStatus === 'saved') {
20 | return (
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | if (savingStatus === 'notSaved') {
28 | return (
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | return null;
36 | }
37 |
--------------------------------------------------------------------------------
/apps/icons/assets/airtable-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/controls/text-area-control/text-area-control.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { TextAreaControlProps } from '../../types/controls';
3 | import { createControlRenderer } from '../../utils/rendering';
4 | import { ControlWrapper } from '../control-wrapper';
5 | import { TextArea } from '@synergycodes/overflow-ui';
6 |
7 | function TextAreaControl(props: TextAreaControlProps) {
8 | const { data, handleChange, path, enabled, uischema } = props;
9 | const { placeholder, minRows } = uischema;
10 |
11 | const [inputValue, setInputValue] = useState(data);
12 |
13 | function onChange(event: React.ChangeEvent) {
14 | setInputValue(event.target.value);
15 | }
16 |
17 | function onBlur() {
18 | handleChange(path, inputValue);
19 | }
20 |
21 | useEffect(() => {
22 | setInputValue(data);
23 | }, [data]);
24 |
25 | return (
26 |
27 |
36 |
37 | );
38 | }
39 |
40 | export const textAreaControlRenderer = createControlRenderer('TextArea', TextAreaControl);
41 |
--------------------------------------------------------------------------------
/apps/frontend/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import baseEslintConfig from '../../eslint.config.mjs';
2 | import pluginReact from 'eslint-plugin-react';
3 | import pluginHooks from 'eslint-plugin-react-hooks';
4 |
5 | const rules = {
6 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
7 | 'react/display-name': 'off',
8 | 'react/prop-types': 'off',
9 | 'react/function-component-definition': [
10 | 'error',
11 | {
12 | namedComponents: 'function-declaration',
13 | },
14 | ],
15 | 'no-restricted-imports': [
16 | 'error',
17 | {
18 | patterns: [
19 | {
20 | group: ['@/plugins/*'],
21 | message: 'Importing from plugins is restricted (plugin can be removed). Please use @/feature/plugins',
22 | },
23 | ],
24 | },
25 | ],
26 | };
27 |
28 | /** @type {import('eslint').Linter.Config[]} */
29 | export default [
30 | ...baseEslintConfig,
31 | { files: ['**/*.{ts,tsx}'] },
32 | pluginReact.configs.flat.recommended,
33 | pluginReact.configs.flat['jsx-runtime'],
34 | {
35 | plugins: {
36 | 'react-hooks': pluginHooks,
37 | },
38 | rules: {
39 | ...pluginHooks.configs.recommended.rules,
40 | ...rules,
41 | },
42 | settings: {
43 | react: {
44 | version: 'detect',
45 | },
46 | },
47 | },
48 | ];
49 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/properties-bar/properties-bar-container.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useState } from 'react';
2 | import { useRemoveElements } from '@/hooks/use-remove-elements';
3 | import { useTranslation } from 'react-i18next';
4 | import { useSingleSelectedElement } from '@/features/properties-bar/use-single-selected-element';
5 | import { PropertiesBar } from './components/properties-bar/properties-bar';
6 |
7 | export function PropertiesBarContainer() {
8 | const { removeElements } = useRemoveElements();
9 | const { t } = useTranslation();
10 |
11 | const [selectedTab, setSelectedTab] = useState('properties');
12 |
13 | const selection = useSingleSelectedElement();
14 | const selectionId = useMemo(() => selection?.node?.id, [selection]);
15 |
16 | useEffect(() => {
17 | setSelectedTab('properties');
18 | }, [selectionId]);
19 |
20 | function handleDeleteClick() {
21 | if (selection) {
22 | removeElements(selection);
23 | }
24 | }
25 |
26 | return (
27 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/apps/frontend/README.md:
--------------------------------------------------------------------------------
1 | # `@workflow-builder/frontend`
2 |
3 | ## Features
4 |
5 | - [Dynamic form generation for properties sidebar](./apps/frontend/src/app/components/json-form/form-generation.md)
6 |
7 | ## Short overview of Workflow Builder–specific code in `src/app`
8 |
9 | - `data\nodes` - Includes definitions of the nodes used by the application and passed to the palette.
10 | - `data\template` - List of pre-made, ready-to-use templates (you can select them when you enter the site for the first time or from the bottom button in the palette).
11 | - `features` - Most of Workflow Builder’s core functionalities are in that folder
12 | - `features\diagram` - The logic responsible for displaying the diagram using ReactFlow is located there
13 | - `features\json-form` - The code responsible for rendering and validating items in the properties sidebars is located there. If you want to add a new control, you can do it there.
14 | - `features\plugins-core` - Logic implementing the functionality of optional plugins (not the plugins themselves—they are in the plugins directory next to features). Here plugins added to the project are imported.
15 | - `plugins` - Plugins are optional features that can be removed from the project without breaking it, as they use adapters and stub imports for any removed files.
16 | - `store` - Directory defining the main Zustand store of the workflow builder app
17 |
--------------------------------------------------------------------------------
/apps/frontend/file-replacement-plugin.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-ts-comment */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | import path from 'node:path';
4 |
5 | export function replaceFiles(replacements: FileReplacement[]): {
6 | name: string;
7 | enforce: 'pre' | 'post' | undefined;
8 | resolveId(source: any, importer: any, options: any): Promise;
9 | } | null {
10 | if (!replacements?.length) {
11 | return null;
12 | }
13 |
14 | return {
15 | name: 'rollup-plugin-replace-files',
16 | enforce: 'pre',
17 | async resolveId(source, importer, options) {
18 | // @ts-expect-error
19 | const resolved = await this.resolve(source, importer, {
20 | ...options,
21 | });
22 |
23 | const foundReplace = replacements.find((replacement) => resolved?.id?.endsWith(replacement.replace));
24 |
25 | if (foundReplace) {
26 | console.info(`Replace "${foundReplace.replace}" with "${foundReplace.with}"`);
27 |
28 | try {
29 | // return new file content
30 | return path.resolve(foundReplace.with);
31 | } catch (error) {
32 | console.error(error);
33 | return null;
34 | }
35 | }
36 | return null;
37 | },
38 | };
39 | }
40 |
41 | export type FileReplacement = {
42 | replace: string;
43 | with: string;
44 | };
45 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/controls/select-control/select-control.tsx:
--------------------------------------------------------------------------------
1 | import { SelectControlProps } from '../../types/controls';
2 |
3 | import { createControlRenderer } from '../../utils/rendering';
4 | import { Select, SelectBaseProps } from '@synergycodes/overflow-ui';
5 | import { ControlWrapper } from '../control-wrapper';
6 | import { PrimitiveFieldSchema } from '@workflow-builder/types/node-schema';
7 | import { Icon } from '@workflow-builder/icons';
8 |
9 | function SelectControl(props: SelectControlProps) {
10 | const { data, handleChange, path, enabled, schema } = props;
11 |
12 | const items = (schema as PrimitiveFieldSchema).options?.map((option) =>
13 | option.type === 'separator' || !option.icon
14 | ? option
15 | : {
16 | ...option,
17 | icon: ,
18 | },
19 | );
20 |
21 | const onChange: SelectBaseProps['onChange'] = (_event, value) => {
22 | handleChange(path, value);
23 | };
24 |
25 | return (
26 |
27 |
34 |
35 | );
36 | }
37 |
38 | export const selectControlRenderer = createControlRenderer('Select', SelectControl);
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workflow-builder",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "pnpm --filter frontend dev",
7 | "build": "pnpm -r build",
8 | "preview-build": "pnpm --filter frontend preview-build",
9 | "typecheck": "pnpm -r typecheck",
10 | "lint": "pnpm -r lint",
11 | "lint:fix": "pnpm -r lint:fix",
12 | "build:frontend": "pnpm --filter frontend build",
13 | "format": "prettier --write --log-level silent \"**/*.+(css|ts|tsx|json)\"",
14 | "test": "pnpm --filter frontend test",
15 | "check": "pnpm lint && pnpm typecheck && pnpm format",
16 | "pre-commit": "lint-staged",
17 | "pre-push": "pnpm format"
18 | },
19 | "devDependencies": {
20 | "@eslint/js": "^9.18.0",
21 | "@types/node": "^22.12.0",
22 | "@types/react-dom": "^19.1.0",
23 | "@types/react-gtm-module": "^2.0.4",
24 | "concurrently": "^9.1.2",
25 | "eslint": "^9.18.0",
26 | "eslint-config-prettier": "^9.1.0",
27 | "eslint-plugin-unicorn": "^59.0.1",
28 | "globals": "^15.14.0",
29 | "husky": "^9.1.7",
30 | "jsdom": "^26.0.0",
31 | "knip": "^5.56.0",
32 | "lint-staged": "^16.0.0",
33 | "prettier": "^3.3.3",
34 | "tsx": "^4.19.3",
35 | "typescript": "~5.6.3",
36 | "typescript-eslint": "^8.22.0"
37 | },
38 | "engines": {
39 | "node": "22.12.0",
40 | "pnpm": "10.9.0"
41 | },
42 | "packageManager": "pnpm@10.9.0"
43 | }
44 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/delay/schema.ts:
--------------------------------------------------------------------------------
1 | import { sharedProperties } from '../shared/shared-properties';
2 | import { statusOptions } from '../shared/general-information';
3 | import { NodeSchema } from '@workflow-builder/types/node-schema';
4 | import { delayTypeOptions, timeUnitsOptions, maxWaitTimeOptions } from './select-options';
5 | import { conditionalValidation } from './conditional-validation';
6 |
7 | export const schema = {
8 | required: ['label', 'description', 'type', 'status'],
9 | type: 'object',
10 | properties: {
11 | ...sharedProperties,
12 | type: {
13 | type: 'string',
14 | placeholder: 'Select Delay Type...',
15 | options: Object.values(delayTypeOptions),
16 | },
17 | status: {
18 | type: 'string',
19 | options: Object.values(statusOptions),
20 | },
21 | duration: {
22 | type: 'object',
23 | properties: {
24 | timeUnits: {
25 | type: 'string',
26 | options: Object.values(timeUnitsOptions),
27 | },
28 | delayAmount: {
29 | type: 'number',
30 | },
31 | expression: {
32 | type: 'string',
33 | },
34 | maxWaitTime: {
35 | type: 'string',
36 | options: Object.values(maxWaitTimeOptions),
37 | },
38 | },
39 | },
40 | },
41 | ...conditionalValidation,
42 | } satisfies NodeSchema;
43 |
44 | export type DelayNodeSchema = typeof schema;
45 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/app-bar/components/controls/controls.tsx:
--------------------------------------------------------------------------------
1 | import styles from '../../app-bar.module.css';
2 | import { useTranslation } from 'react-i18next';
3 | import { NavButton, Menu, MenuItemProps } from '@synergycodes/overflow-ui';
4 | import { DotsThreeVertical } from '@phosphor-icons/react';
5 | import { useMemo } from 'react';
6 | import { getControlsDotsItems } from '../../functions/get-controls-dots-items';
7 | import { OptionalAppBarControls } from '@/features/plugins-core/components/optional-app-bar-controls';
8 | import { ToggleReadyOnlyMode } from '../toggle-read-only-mode/toggle-read-only-mode';
9 | import { ToggleDarkMode } from '../toggle-dark-mode/toggle-dark-mode';
10 |
11 | export function Controls() {
12 | const { t } = useTranslation();
13 |
14 | // eslint-disable-next-line react-hooks/exhaustive-deps
15 | const items: MenuItemProps[] = useMemo(() => getControlsDotsItems(), [t]);
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | {items.length > 0 && (
24 |
25 |
30 |
31 | )}
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/utils/rendering.ts:
--------------------------------------------------------------------------------
1 | import { ControlProps, JsonFormsRendererRegistryEntry, rankWith, uiTypeIs } from '@jsonforms/core';
2 | import { UISchemaControlElementType, UISchemaElementType, UISchemaLayoutElementType } from '../types/uischema';
3 | import { BaseControlProps } from '../types/controls';
4 | import { withJsonFormsControlProps, withJsonFormsLayoutProps } from '@jsonforms/react';
5 | import { ComponentType } from 'react';
6 | import { BaseLayoutElement, LayoutProps } from '../types/layouts';
7 |
8 | const JSON_FORM_DEFAULT_RANK = 1;
9 |
10 | export function createTester(type: UISchemaElementType) {
11 | return rankWith(JSON_FORM_DEFAULT_RANK, uiTypeIs(type));
12 | }
13 |
14 | export function createControlRenderer(
15 | type: UISchemaControlElementType,
16 | renderer: React.ComponentType,
17 | ): JsonFormsRendererRegistryEntry {
18 | return {
19 | tester: createTester(type),
20 | renderer: withJsonFormsControlProps(renderer as unknown as ComponentType),
21 | };
22 | }
23 |
24 | export function createLayoutRenderer>(
25 | type: UISchemaLayoutElementType,
26 | renderer: React.ComponentType,
27 | ): JsonFormsRendererRegistryEntry {
28 | return {
29 | tester: createTester(type),
30 | renderer: withJsonFormsLayoutProps(renderer as Parameters[0]),
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/i18n/components/language-selector/language-selector.tsx:
--------------------------------------------------------------------------------
1 | import styles from './language-selector.module.css';
2 | import { useMemo } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 | import { CaretDown } from '@phosphor-icons/react';
5 | import { MenuItemProps, Menu, NavButton } from '@synergycodes/overflow-ui';
6 | import { Icon } from '@workflow-builder/icons';
7 |
8 | type Language = {
9 | code: string;
10 | label: string;
11 | };
12 |
13 | const languages: Language[] = [
14 | { code: 'en', label: 'English' },
15 | { code: 'pl', label: 'Polski' },
16 | ];
17 |
18 | export function LanguageSelector() {
19 | const { t, i18n } = useTranslation();
20 |
21 | const currentLanguage = languages.find((lang) => lang.code === i18n.language) || languages[0];
22 |
23 | const languageItems: MenuItemProps[] = useMemo(
24 | () =>
25 | languages.map(({ code, label }) => ({
26 | label,
27 | icon: ,
28 | onClick: () => i18n.changeLanguage(code),
29 | })),
30 | [i18n],
31 | );
32 |
33 | return (
34 | <>
35 |
43 | >
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/data/nodes/ai-agent/schema.ts:
--------------------------------------------------------------------------------
1 | import { NodeSchema } from '@workflow-builder/types/node-schema';
2 | import { sharedProperties } from '../shared/shared-properties';
3 | import { statusOptions } from '../shared/general-information';
4 | import { chatModelOptions, memoryOptions } from './select-options';
5 |
6 | export const schema = {
7 | required: ['label', 'chatModel', 'memory'],
8 | type: 'object',
9 | properties: {
10 | ...sharedProperties,
11 | status: {
12 | type: 'string',
13 | options: Object.values(statusOptions),
14 | },
15 | chatModel: {
16 | type: 'string',
17 | options: Object.values(chatModelOptions),
18 | placeholder: 'Add Chat Model',
19 | },
20 | tools: {
21 | type: 'array',
22 | items: {
23 | type: 'object',
24 | properties: {
25 | id: {
26 | type: 'string',
27 | },
28 | tool: {
29 | type: 'string',
30 | },
31 | description: {
32 | type: 'string',
33 | },
34 | apiKey: {
35 | type: 'string',
36 | },
37 | },
38 | },
39 | },
40 | memory: {
41 | type: 'string',
42 | options: Object.values(memoryOptions),
43 | placeholder: 'Add memory',
44 | },
45 | systemPrompt: {
46 | type: 'string',
47 | },
48 | },
49 | } satisfies NodeSchema;
50 |
51 | export type AiAgentNodeSchema = typeof schema;
52 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/store/slices/palette/palette-slice.ts:
--------------------------------------------------------------------------------
1 | import { DraggingItem, PaletteItem, StatusType } from '@workflow-builder/types/common';
2 | import { GetDiagramState, SetDiagramState } from '@/store/store';
3 | import { paletteData } from '@/data/palette';
4 |
5 | export type PaletteState = {
6 | isSidebarExpanded: boolean;
7 | data: PaletteItem[];
8 | fetchDataStatus: StatusType;
9 | draggedItem: DraggingItem | null;
10 | toggleSidebar: (value?: boolean) => void;
11 | fetchData: () => void;
12 | setDraggedItem: (item: DraggingItem | null) => void;
13 | getNodeDefinition: (nodeType: string) => PaletteItem | undefined;
14 | };
15 |
16 | export function usePaletteSlice(set: SetDiagramState, get: GetDiagramState): PaletteState {
17 | return {
18 | isSidebarExpanded: false,
19 | data: [],
20 | fetchDataStatus: StatusType.Idle,
21 | draggedItem: null,
22 | setDraggedItem: (item) => {
23 | set({ draggedItem: item });
24 | },
25 | toggleSidebar: (value) => {
26 | set({
27 | isSidebarExpanded: value ?? !get().isSidebarExpanded,
28 | });
29 | },
30 | fetchData: () => {
31 | set({ fetchDataStatus: StatusType.Loading });
32 |
33 | set({
34 | data: paletteData,
35 | fetchDataStatus: StatusType.Success,
36 | });
37 | },
38 | getNodeDefinition: (nodeType) => {
39 | const { data } = get();
40 |
41 | return data.find(({ type }) => type === nodeType);
42 | },
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/utils/show-snackbar.tsx:
--------------------------------------------------------------------------------
1 | import { closeSnackbar, enqueueSnackbar } from 'notistack';
2 | import i18n from 'i18next';
3 | import { Snackbar, SnackbarProps } from '@synergycodes/overflow-ui';
4 | import { DefaultTranslationMap } from '@/features/i18n/i18next';
5 |
6 | const AUTO_HIDE_DURATION_TIME = 3000;
7 |
8 | const SNACKBAR_PREFIX = `snackbar` as const;
9 | type SnackbarKey = keyof DefaultTranslationMap[typeof SNACKBAR_PREFIX];
10 |
11 | type ShowSnackbarProps = Omit & {
12 | title: SnackbarKey;
13 | autoHideDuration?: number;
14 | preventDuplicate?: boolean;
15 | };
16 |
17 | export function showSnackbar({
18 | title,
19 | variant,
20 | subtitle,
21 | buttonLabel,
22 | onButtonClick,
23 | close = true,
24 | autoHideDuration = AUTO_HIDE_DURATION_TIME,
25 | preventDuplicate = true,
26 | }: ShowSnackbarProps) {
27 | enqueueSnackbar(variant, {
28 | content: (key) => (
29 | {
35 | onButtonClick?.();
36 | closeSnackbar(key);
37 | }}
38 | close={close}
39 | onClose={() => closeSnackbar(key)}
40 | />
41 | ),
42 | autoHideDuration,
43 | preventDuplicate,
44 | anchorOrigin: { horizontal: 'center', vertical: 'bottom' },
45 | });
46 | return { showSnackbar };
47 | }
48 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/types/layouts.ts:
--------------------------------------------------------------------------------
1 | import type { Layout, LayoutProps as JSONFormsLayoutProps } from '@jsonforms/core';
2 | import type { Override } from './utils';
3 | import type { UISchemaElement } from './uischema';
4 | import { UISchemaRule } from './rules';
5 |
6 | export type AccordionLayoutElement = Override<
7 | BaseLayoutElement,
8 | {
9 | label: string;
10 | type: 'Accordion';
11 | }
12 | >;
13 |
14 | export type GroupLayoutElement = Override<
15 | BaseLayoutElement,
16 | {
17 | label: string;
18 | type: 'Group';
19 | }
20 | >;
21 |
22 | export type VerticalLayoutElement = Override<
23 | BaseLayoutElement,
24 | {
25 | type: 'VerticalLayout';
26 | }
27 | >;
28 |
29 | export type HorizontalLayoutElement = Override<
30 | BaseLayoutElement,
31 | {
32 | type: 'HorizontalLayout';
33 | } & {
34 | /**
35 | * Defines the `grid-auto-columns` CSS property.
36 | * Can be any valid CSS value for the `grid-auto-columns` property:
37 | * - Length (e.g., '100px 1fr')
38 | * - Percentage (e.g., '50%')
39 | * - 'auto'
40 | */
41 | layoutColumns?: string;
42 | }
43 | >;
44 | export type LayoutProps = Override<
45 | JSONFormsLayoutProps,
46 | {
47 | children: React.ReactNode;
48 | uischema: T;
49 | }
50 | >;
51 |
52 | export type BaseLayoutElement = Override<
53 | Layout,
54 | {
55 | rule?: UISchemaRule;
56 | elements: UISchemaElement[];
57 | }
58 | >;
59 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/types/uischema.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | AccordionLayoutElement,
3 | GroupLayoutElement,
4 | HorizontalLayoutElement,
5 | VerticalLayoutElement,
6 | } from './layouts';
7 | import {
8 | DatePickerControlElement,
9 | SelectControlElement,
10 | SwitchControlElement,
11 | TextAreaControlElement,
12 | DynamicConditionsControlElement,
13 | TextControlElement,
14 | AiToolsControlElement,
15 | DecisionBranchesControlElement,
16 | } from './controls';
17 | import { LabelElement } from './label';
18 |
19 | export type UISchemaControlElement = (
20 | | TextControlElement
21 | | SwitchControlElement
22 | | SelectControlElement
23 | | DatePickerControlElement
24 | | TextAreaControlElement
25 | | DynamicConditionsControlElement
26 | | AiToolsControlElement
27 | | DecisionBranchesControlElement
28 | ) & { scope: T; errorIndicatorEnabled?: boolean };
29 | export type UISchemaControlElementType = UISchemaControlElement['type'];
30 |
31 | type UISchemaLayoutElement =
32 | | GroupLayoutElement
33 | | AccordionLayoutElement
34 | | VerticalLayoutElement
35 | | HorizontalLayoutElement;
36 | export type UISchemaLayoutElementType = UISchemaLayoutElement['type'];
37 |
38 | export type UISchemaElement =
39 | | UISchemaControlElement
40 | | UISchemaLayoutElement
41 | | LabelElement;
42 | export type UISchemaElementType = UISchemaElement['type'];
43 |
44 | export type UISchema = UISchemaElement;
45 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/json-form/utils/get-scope.spec.ts:
--------------------------------------------------------------------------------
1 | import { getScope } from './get-scope';
2 | import { describe, expect, it } from 'vitest';
3 |
4 | describe('getScope', () => {
5 | const _schema = {
6 | properties: {
7 | label: { type: 'string' },
8 | description: { type: 'string' },
9 | metadata: {
10 | properties: {
11 | createdBy: { type: 'string' },
12 | updatedAt: { type: 'string' },
13 | },
14 | },
15 | },
16 | } as const;
17 |
18 | it('should generate scope for a top-level property', () => {
19 | expect(getScope('properties.label')).toBe('#/properties/label');
20 | });
21 |
22 | it('should generate scope for another top-level property', () => {
23 | expect(getScope('properties.description')).toBe('#/properties/description');
24 | });
25 |
26 | it('should generate scope for a nested property', () => {
27 | expect(getScope('properties.metadata.properties.createdBy')).toBe(
28 | '#/properties/metadata/properties/createdBy',
29 | );
30 | });
31 |
32 | it('should generate scope for another nested property', () => {
33 | expect(getScope('properties.metadata.properties.updatedAt')).toBe(
34 | '#/properties/metadata/properties/updatedAt',
35 | );
36 | });
37 |
38 | it('should return root scope when path function does nothing', () => {
39 | expect(getScope('')).toBe('#/');
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/integration/hooks/use-auto-save-on-close.ts:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect, useRef } from 'react';
2 | import { IntegrationContext } from '../components/integration-variants/context/integration-context-wrapper';
3 | import { useChangesTrackerStore } from '@/features/changes-tracker/stores/use-changes-tracker-store';
4 | import { useIntegrationStore } from '../stores/use-integration-store';
5 |
6 | export function useAutoSaveOnClose() {
7 | const onSaveRef = useRef void)>(null);
8 |
9 | const { onSave } = useContext(IntegrationContext);
10 |
11 | useEffect(() => {
12 | if (onSaveRef.current) {
13 | window.removeEventListener('beforeunload', onSaveRef.current);
14 | }
15 |
16 | onSaveRef.current = () => {
17 | /*
18 | Don't use zustand .getState directly in react components body,
19 | but they are fine in callbacks like this.
20 | */
21 | const lastChangeTimestamp = useChangesTrackerStore.getState().lastChangeTimestamp;
22 | const lastSaveAttemptTimestamp = useIntegrationStore.getState().lastSaveAttemptTimestamp;
23 |
24 | if (lastChangeTimestamp > lastSaveAttemptTimestamp) {
25 | onSave({ isAutoSave: true });
26 | }
27 | };
28 |
29 | window.removeEventListener('beforeunload', onSaveRef.current);
30 |
31 | return () => {
32 | if (onSaveRef.current) {
33 | window.removeEventListener('beforeunload', onSaveRef.current);
34 | }
35 | };
36 | }, [onSave]);
37 | }
38 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/features/modals/stores/use-modal-store.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { devtools } from 'zustand/middleware';
3 | import { ComponentProps } from 'react';
4 | import { FooterVariant, Modal } from '@synergycodes/overflow-ui';
5 |
6 | type ModalProps = {
7 | content: ComponentProps['children'];
8 | icon?: ComponentProps['icon'];
9 | title: string;
10 | footer?: ComponentProps['footer'];
11 | isCloseButtonVisible?: boolean;
12 | footerVariant?: FooterVariant;
13 | onModalClosed?: () => void;
14 | };
15 |
16 | type ModalStore = {
17 | isOpen: boolean;
18 | modal: ModalProps | null;
19 | };
20 |
21 | const emptyStore: ModalStore = {
22 | isOpen: false,
23 | modal: null,
24 | };
25 |
26 | export const useModalStore = create()(
27 | devtools(
28 | () =>
29 | ({
30 | ...emptyStore,
31 | }) satisfies ModalStore,
32 | { name: 'modalStore' },
33 | ),
34 | );
35 |
36 | export function openModal({ isCloseButtonVisible = true, footerVariant = 'integrated', ...restProps }: ModalProps) {
37 | useModalStore.setState({
38 | isOpen: true,
39 | modal: {
40 | ...restProps,
41 | isCloseButtonVisible,
42 | footerVariant,
43 | },
44 | });
45 | }
46 |
47 | export function closeModal() {
48 | const modal = useModalStore.getState().modal;
49 |
50 | modal?.onModalClosed?.();
51 | useModalStore.setState({
52 | isOpen: false,
53 | modal: null,
54 | });
55 | }
56 |
--------------------------------------------------------------------------------
/apps/frontend/src/app/components/sidebar/sidebar.module.css:
--------------------------------------------------------------------------------
1 | @layer ui.component {
2 | .sidebar {
3 | height: min-content;
4 | width: auto;
5 | position: relative;
6 | align-items: center;
7 | justify-content: space-between;
8 | pointer-events: all;
9 | border-radius: var(--wb-sidebar-border-radius);
10 | background: var(--wb-sidebar-background);
11 | color: var(--wb-sidebar-text-color);
12 | display: flex;
13 | flex-direction: column;
14 | padding: var(--wb-sidebar-vertical-padding) 0;
15 | border: 1px solid var(--wb-sidebar-border-color);
16 |
17 | .header,
18 | .footer,
19 | .content {
20 | box-sizing: border-box;
21 | width: 100%;
22 | display: flex;
23 | flex-direction: column;
24 | padding: 0 var(--wb-sidebar-horizontal-padding);
25 | }
26 |
27 | .header {
28 | display: flex;
29 | gap: 1rem;
30 | margin-bottom: var(--wb-sidebar-content-gap);
31 |
32 | &:last-child {
33 | margin-bottom: 0;
34 | }
35 | }
36 |
37 | .content {
38 | flex: 1;
39 | overflow-y: auto;
40 | overflow-x: hidden;
41 | padding-top: var(--wb-sidebar-content-gap);
42 | padding-bottom: var(--wb-sidebar-content-gap);
43 | }
44 |
45 | .footer {
46 | margin-top: var(--wb-sidebar-content-gap);
47 | }
48 |
49 | &.expanded {
50 | height: 100%;
51 | width: var(--wb-sidebar-expanded-width);
52 | box-sizing: border-box;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------