Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): Digest liquid helper and popover handler #7439

Merged
merged 69 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
997c1f8
fix(api): Allow arbitrary variables on the payload namespace
SokratisVidros Dec 23, 2024
feaf04c
wup:
scopsy Dec 23, 2024
163ad78
fix: items
scopsy Dec 23, 2024
b2111ec
Update email-subject.tsx
scopsy Dec 23, 2024
65733c8
fix: hello world
scopsy Dec 23, 2024
624383a
fix: re order
scopsy Dec 23, 2024
75d2c7d
fix: state
scopsy Dec 23, 2024
faa410b
fix: refactor field editor
scopsy Dec 23, 2024
96acb70
j
scopsy Dec 23, 2024
80ed1f3
fix: update other field
scopsy Dec 23, 2024
6ec81ab
fix: refactor
scopsy Dec 23, 2024
25141c8
fix: item
scopsy Dec 23, 2024
c32cd7e
fix:
scopsy Dec 23, 2024
2c3e6a9
fix: cursor
scopsy Dec 23, 2024
bf9324a
Update field-editor.tsx
scopsy Dec 23, 2024
fefc2cc
fix: a
scopsy Dec 24, 2024
96ffc1b
Merge branch 'next' into pills-for-all-inputs
scopsy Dec 25, 2024
ed3eaae
Merge branch 'next' into pills-for-all-inputs
scopsy Dec 27, 2024
c6be4ab
Merge branch 'next' into pills-for-all-inputs
scopsy Dec 30, 2024
ec924ed
fix: open prs
scopsy Dec 30, 2024
e717bc8
fix: design
scopsy Dec 30, 2024
e56a5c6
fix:
scopsy Dec 30, 2024
c83af5c
Update variable-popover.tsx
scopsy Dec 30, 2024
93c1d46
fix: it worked
scopsy Dec 30, 2024
50a25c9
fix: refactor
scopsy Dec 30, 2024
74b7ba4
fix: done
scopsy Dec 30, 2024
e78ee36
Merge branch 'next' into pills-for-all-inputs
scopsy Dec 30, 2024
b378aac
fix: add some personality
scopsy Dec 30, 2024
c0f0c3e
fix: view
scopsy Dec 30, 2024
05842f0
Merge branch 'pills-for-all-inputs' of https://github.com/novuhq/novu…
scopsy Dec 30, 2024
48b448c
fix: popover
scopsy Dec 30, 2024
62c48c2
fixes: asdas
scopsy Dec 30, 2024
ecc8a8c
fix: reusability
scopsy Dec 30, 2024
fb0901f
fix: refactor
scopsy Dec 30, 2024
95f249b
feat: add comments
scopsy Dec 30, 2024
92f915f
fix: types
scopsy Dec 30, 2024
f970981
fix: wip for digest liquid
scopsy Dec 30, 2024
f97c433
fix: remove unused
scopsy Dec 30, 2024
44c678a
Merge branch 'next' into pills-for-all-inputs
scopsy Jan 1, 2025
2bd08b0
Update field-editor.tsx
scopsy Jan 1, 2025
a20ee80
Merge branch 'next' into pills-for-all-inputs
scopsy Jan 2, 2025
7209172
fix: close on blur
scopsy Jan 2, 2025
64564ef
fix: refactor
scopsy Jan 3, 2025
d00d441
fix: working state
scopsy Jan 3, 2025
36da57b
fix: auto complete
scopsy Jan 3, 2025
c10c935
fix: liquid
scopsy Jan 3, 2025
d355467
fix: pr comments
scopsy Jan 3, 2025
a148965
fix: initial values
scopsy Jan 3, 2025
71d6d08
Update variable-pill-widget.ts
scopsy Jan 3, 2025
d1d512a
fix: items
scopsy Jan 3, 2025
17164ed
fix: popover
scopsy Jan 3, 2025
bda722a
fix: command refactor
scopsy Jan 3, 2025
cc3b33b
improve usememo
scopsy Jan 3, 2025
a8c1816
fix: revie
scopsy Jan 3, 2025
967be19
fix: minor issues
scopsy Jan 3, 2025
dc7d907
Merge branch 'next' into pills-for-all-inputs
scopsy Jan 3, 2025
3601b0d
Merge branch 'pills-for-all-inputs' into digest-liquid-helper
scopsy Jan 6, 2025
b76fbfa
fix:
scopsy Jan 6, 2025
a88ba03
Update liquid-parser.ts
scopsy Jan 6, 2025
076d365
fix
scopsy Jan 6, 2025
43bbbaa
fix: items
scopsy Jan 6, 2025
eea4a0e
Merge branch 'next' into digest-liquid-helper
scopsy Jan 16, 2025
a0b3f91
fix: remove
scopsy Jan 16, 2025
81d94b4
fix: review
scopsy Jan 17, 2025
a2b8f7a
Merge branch 'next' into digest-liquid-helper
scopsy Jan 19, 2025
e656ad4
Merge branch 'next' into digest-liquid-helper
scopsy Jan 26, 2025
4fdab39
Merge branch 'next' into digest-liquid-helper
scopsy Jan 27, 2025
1045e1e
Merge branch 'next' into digest-liquid-helper
scopsy Feb 3, 2025
650a5ca
Update utils.ts
scopsy Feb 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,7 @@
"tsconfig.json",
"unreadRead",
"websockets",
"apps/dashboard/src/components/header-navigation/customer-support-button.tsx"
"apps/dashboard/src/components/header-navigation/customer-support-button.tsx",
"apps/dashboard/src/components/primitives/field-editor/variable-popover/constants.ts"
]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ActionStepEnum, actionStepSchemas, ChannelStepEnum, channelStepSchemas } from '@novu/framework/internal';
import { JSONSchema } from 'json-schema-to-ts';
import { StepTypeEnum } from '@novu/shared';
import { JSONSchema } from 'json-schema-to-ts';

export function computeResultSchema(stepType: StepTypeEnum, payloadSchema?: JSONSchema) {
const mapStepTypeToResult: Record<ChannelStepEnum & ActionStepEnum, JSONSchema> = {
Expand Down Expand Up @@ -31,9 +31,13 @@ function buildDigestResult(payloadSchema?: JSONSchema) {
time: {
type: 'string',
},
payload: payloadSchema || {
type: 'object',
},
payload:
payloadSchema && typeof payloadSchema === 'object'
? { ...payloadSchema, additionalProperties: true }
: {
type: 'object',
additionalProperties: true,
},
},
required: ['id', 'time', 'payload'],
additionalProperties: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Template, Liquid, RenderError, LiquidError } from 'liquidjs';
import { isValidTemplate, extractLiquidExpressions } from './parser-utils';
import { Liquid, LiquidError, RenderError, Template } from 'liquidjs';
import { extractLiquidExpressions, isValidTemplate } from './parser-utils';

const LIQUID_CONFIG = {
strictVariables: true,
Expand Down Expand Up @@ -123,10 +123,14 @@ function processLiquidRawOutput(rawOutputs: string[]): TemplateVariables {
}

function parseByLiquid(rawOutput: string): TemplateVariables {
const parserEngine = new Liquid(LIQUID_CONFIG);

// Register digest filter for validation of digest transformers
parserEngine.registerFilter('digest', () => '');

Comment on lines +126 to +129
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we have 2 version of liquid one in the framework and one in the API, didn't do it in this PR but worth considering consolidating them somehow or exporting from the framework the parser

const validVariables: Variable[] = [];
const invalidVariables: Variable[] = [];
const engine = new Liquid(LIQUID_CONFIG);
const parsed = engine.parse(rawOutput) as unknown as Template[];
const parsed = parserEngine.parse(rawOutput) as unknown as Template[];

parsed.forEach((template: Template) => {
if (isOutputToken(template)) {
Expand Down
6 changes: 2 additions & 4 deletions apps/api/src/app/workflows-v2/util/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import difference from 'lodash/difference';
import flatMap from 'lodash/flatMap';
import isArray from 'lodash/isArray';
import isObject from 'lodash/isObject';
import reduce from 'lodash/reduce';
import set from 'lodash/set';
import values from 'lodash/values';
import isObject from 'lodash/isObject';
import isArray from 'lodash/isArray';

import { BadRequestException } from '@nestjs/common';

import { JSONSchemaDto } from '@novu/shared';

Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"flat": "^6.0.1",
"js-cookie": "^3.0.5",
"launchdarkly-react-client-sdk": "^3.3.2",
"liquidjs": "^10.20.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2",
Expand Down
20 changes: 20 additions & 0 deletions apps/dashboard/public/images/code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 8 additions & 8 deletions apps/dashboard/src/components/primitives/command.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from 'react';
import { type DialogProps } from '@radix-ui/react-dialog';
import { Command as CommandPrimitive } from 'cmdk';
import * as React from 'react';

import { cn } from '@/utils/ui';
import { Dialog, DialogContent } from '@/components/primitives/dialog';
import { InputField, inputVariants } from '@/components/primitives/input';
import { cn } from '@/utils/ui';

const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
Expand Down Expand Up @@ -37,9 +37,9 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {

const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<InputField>
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> & { inputFieldClassName?: string }
>(({ className, inputFieldClassName, ...props }, ref) => (
<InputField className={inputFieldClassName}>
<CommandPrimitive.Input ref={ref} className={cn(inputVariants(), className)} {...props} />
</InputField>
));
Expand Down Expand Up @@ -115,11 +115,11 @@ CommandShortcut.displayName = 'CommandShortcut';
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandShortcut,
CommandList,
CommandSeparator,
CommandShortcut,
};
115 changes: 115 additions & 0 deletions apps/dashboard/src/components/primitives/field-editor/field-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { autocompletion } from '@codemirror/autocomplete';
import { EditorView } from '@uiw/react-codemirror';

import { Editor } from '@/components/primitives/editor';
import { Popover, PopoverTrigger } from '@/components/primitives/popover';
import { createAutocompleteSource } from '@/utils/liquid-autocomplete';
import { LiquidVariable } from '@/utils/parseStepVariablesToLiquidVariables';
import { useCallback, useMemo, useRef } from 'react';
import { useVariables } from './hooks/use-variables';
import { createVariablePlugin } from './variable-plugin';
import { variablePillTheme } from './variable-plugin/variable-theme';
import { VariablePopover } from './variable-popover';

type CompletionRange = {
from: number;
to: number;
} | null;

type FieldEditorProps = {
value: string;
onChange: (value: string) => void;
variables: LiquidVariable[];
placeholder?: string;
autoFocus?: boolean;
size?: 'default' | 'lg';
id?: string;
singleLine?: boolean;
indentWithTab?: boolean;
};

const baseExtensions = [EditorView.lineWrapping, variablePillTheme];

export function FieldEditor({
value,
onChange,
variables,
placeholder,
autoFocus,
size = 'default',
id,
singleLine,
indentWithTab,
}: FieldEditorProps) {
const viewRef = useRef<EditorView | null>(null);
const lastCompletionRef = useRef<CompletionRange>(null);

const { selectedVariable, setSelectedVariable, handleVariableSelect, handleVariableUpdate } = useVariables(
viewRef,
onChange
);

const completionSource = useMemo(() => createAutocompleteSource(variables), [variables]);

const autocompletionExtension = useMemo(
() =>
autocompletion({
override: [completionSource],
closeOnBlur: true,
defaultKeymap: true,
activateOnTyping: true,
}),
[completionSource]
);

const variablePlugin = useMemo(
() =>
createVariablePlugin({
viewRef,
lastCompletionRef,
onSelect: handleVariableSelect,
}),
[handleVariableSelect]
);

const extensions = useMemo(
() => [...baseExtensions, autocompletionExtension, variablePlugin],
[autocompletionExtension, variablePlugin]
);

const handleOpenChange = useCallback(
(open: boolean) => {
if (!open) {
setTimeout(() => setSelectedVariable(null), 0);
}
},
[setSelectedVariable]
);

return (
<div className="relative">
<Editor
fontFamily="inherit"
singleLine={singleLine}
indentWithTab={indentWithTab}
size={size}
basicSetup={{
defaultKeymap: true,
}}
className="flex-1"
autoFocus={autoFocus}
placeholder={placeholder}
id={id}
extensions={extensions}
value={value}
onChange={onChange}
/>
<Popover open={!!selectedVariable} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<div />
</PopoverTrigger>
{selectedVariable && <VariablePopover variable={selectedVariable.value} onUpdate={handleVariableUpdate} />}
</Popover>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { EditorView } from '@uiw/react-codemirror';
import { useCallback, useRef, useState } from 'react';

type SelectedVariable = {
value: string;
from: number;
to: number;
} | null;

/**
* Manages variable selection and updates in the editor.
*
* This hook combines variable selection and update logic:
* 1. Tracks which variable is currently selected
* 2. Prevents recursive updates when variables are being modified
* 3. Handles proper Liquid syntax maintenance
* 4. Manages cursor position and editor state updates
*/
export function useVariables(viewRef: React.RefObject<EditorView>, onChange: (value: string) => void) {
const [selectedVariable, setSelectedVariable] = useState<SelectedVariable>(null);
const isUpdatingRef = useRef(false);

const handleVariableSelect = useCallback((value: string, from: number, to: number) => {
if (isUpdatingRef.current) return;
setSelectedVariable({ value, from, to });
}, []);

const handleVariableUpdate = useCallback(
(newValue: string) => {
if (!selectedVariable || !viewRef.current || isUpdatingRef.current) return;

try {
isUpdatingRef.current = true;
const { from, to } = selectedVariable;
const view = viewRef.current;

// Ensure the new value has proper liquid syntax
const hasLiquidSyntax = newValue.match(/^\{\{.*\}\}$/);
const newVariableText = hasLiquidSyntax ? newValue : `{{${newValue}}}`;

// Calculate the actual end position including closing brackets
const currentContent = view.state.doc.toString();
const afterCursor = currentContent.slice(to);
const closingBracketPos = afterCursor.indexOf('}}');
const actualEnd = closingBracketPos >= 0 ? to + closingBracketPos + 2 : to;

const changes = {
from,
to: actualEnd,
insert: newVariableText,
};

view.dispatch({
changes,
selection: { anchor: from + newVariableText.length },
});

onChange(view.state.doc.toString());

// Update the selected variable with new bounds
setSelectedVariable((prev: SelectedVariable) =>
prev ? { ...prev, value: newValue, to: from + newVariableText.length } : null
);
} finally {
isUpdatingRef.current = false;
}
},
[selectedVariable, onChange, viewRef]
);

return {
selectedVariable,
setSelectedVariable,
handleVariableSelect,
handleVariableUpdate,
isUpdatingRef,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FieldEditor } from './field-editor';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const VARIABLE_REGEX = /{{([^{}]+)}}/g;
export const VARIABLE_PILL_CLASS = 'cm-variable-pill';
export const MODIFIERS_CLASS = 'has-modifiers';
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { EditorView, ViewPlugin, Decoration } from '@uiw/react-codemirror';
import type { PluginState } from './types';
import { VariablePluginView } from './plugin-view';

export function createVariablePlugin({ viewRef, lastCompletionRef, onSelect }: PluginState) {
return ViewPlugin.fromClass(
class {
private view: VariablePluginView;

constructor(view: EditorView) {
this.view = new VariablePluginView(view, viewRef, lastCompletionRef, onSelect);
}

update(update: any) {
this.view.update(update);
}

get decorations() {
return this.view.decorations;
}
},
{
decorations: (v) => v.decorations,
provide: (plugin) =>
EditorView.atomicRanges.of((view) => {
return view.plugin(plugin)?.decorations || Decoration.none;
}),
}
);
}

export * from './types';
export * from './constants';
Loading
Loading