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(web,novui): initial implementation of var autocomplete in controls #6097

Merged
merged 46 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c7f67c1
feat(web,novui): initial implementation of var autocomplete
ainouzgali Jul 17, 2024
dfb900e
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
antonjoel82 Jul 17, 2024
c5fafde
chore: Keep mantine package versions in sync
antonjoel82 Jul 17, 2024
75baf54
test: Expand sanitize util test cases
rifont Jul 18, 2024
6f2dea5
test(sanitize): add test for camelCase attribute conversion
rifont Jul 18, 2024
506f7c1
test: remove redundant client initialization
rifont Jul 18, 2024
3002b97
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
ainouzgali Jul 18, 2024
9fd199b
Merge remote-tracking branch 'origin/permissive-html-sanitization' in…
ainouzgali Jul 18, 2024
8aa698f
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
antonjoel82 Jul 18, 2024
2577fe7
feat(novui): experiment with closing mention list portal on escape
ainouzgali Jul 18, 2024
2c7914f
feat(novui): experiment with closing mention list portal on escape
ainouzgali Jul 18, 2024
bc1de1b
Merge remote-tracking branch 'origin/nv-4097-controls-auto-complete-i…
ainouzgali Jul 18, 2024
f54815b
Merge branch 'next' into permissive-html-sanitization
ainouzgali Jul 18, 2024
af5f717
Merge branch 'permissive-html-sanitization' into nv-4097-controls-aut…
ainouzgali Jul 18, 2024
51440b7
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
antonjoel82 Jul 19, 2024
de5f506
fix: Fix some studio colors
antonjoel82 Jul 20, 2024
d325612
refactor: Use ref for menu renderer
antonjoel82 Jul 20, 2024
22e1790
feat: Close menu on click elsewhere
antonjoel82 Jul 20, 2024
4266a34
feat(novui,web): auto suggestion in control panel
ainouzgali Jul 22, 2024
f2b05fa
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
ainouzgali Jul 22, 2024
eb67ffa
feat: update lock file
ainouzgali Jul 22, 2024
5b5a52a
style(novui): extract styles to files
ainouzgali Jul 22, 2024
ec774c0
test(framework): adjust tests to use payload namespace
ainouzgali Jul 22, 2024
7e37745
feat: Add variables for autocomplete to storybook
antonjoel82 Jul 22, 2024
5721b30
style(novui): create recipe and semantic sizes
ainouzgali Jul 23, 2024
76f9a40
style(novui): variable semantic color
ainouzgali Jul 23, 2024
f94c695
style(novui): recipe for input editor widget
ainouzgali Jul 23, 2024
9c1f0b3
fix: Hover selector
antonjoel82 Jul 23, 2024
477e219
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
antonjoel82 Jul 23, 2024
f9e93ab
chore: Move input styles
antonjoel82 Jul 23, 2024
b39cbf3
style(novui): remove size tokens and fix recipe for input editor
ainouzgali Jul 24, 2024
21a7f8f
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
ainouzgali Jul 24, 2024
8aeea70
test(api): fix test to use payload prefix for variable
ainouzgali Jul 24, 2024
092bcab
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
ainouzgali Jul 25, 2024
714d683
feat: Change how styles are aggregated, exported, and imported
antonjoel82 Jul 25, 2024
1f039e5
fix: Use static css generation, use layers
antonjoel82 Jul 25, 2024
ecae780
feat: Extend Input recipe
antonjoel82 Jul 25, 2024
70a0898
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
antonjoel82 Jul 25, 2024
ffe1835
fix: Input bg
antonjoel82 Jul 25, 2024
390cf3d
refactor: Extract constants
antonjoel82 Jul 25, 2024
3b391b6
Merge branch 'nv-4097-controls-auto-complete-in-the-dashboard' of htt…
ainouzgali Jul 28, 2024
fcf37b0
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
ainouzgali Jul 28, 2024
00cdda0
Merge branch 'nv-4097-controls-auto-complete-in-the-dashboard' of htt…
ainouzgali Jul 29, 2024
3cddea1
Merge branch 'next' into nv-4097-controls-auto-complete-in-the-dashboard
ainouzgali Jul 29, 2024
046ae4c
feat: update source
ainouzgali Jul 29, 2024
e4adee6
feat(novui): add comments
ainouzgali Jul 31, 2024
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
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"bcast",
"behaviour",
"bestguess",
"tiptap",
"binipdisplay",
"bitauth",
"bitjson",
Expand Down
2 changes: 1 addition & 1 deletion .source
ainouzgali marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion apps/api/src/app/events/e2e/bridge-trigger.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ contexts.forEach((context: Context) => {
controlSchema: {
type: 'object',
properties: {
name: { type: 'string', default: 'Hello {{name}}' },
name: { type: 'string', default: 'Hello {{payload.name}}' },
},
} as const,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const LocalStudioHeader: FC = () => {
borderBottom: 'none !important',
zIndex: 'sticky',
padding: '50',
// TODO: because this component is directly from mantine, it doesn't respect layer styles
bgColor: 'surface.page !important',
})}
>
<HStack justifyContent="space-between" width="full" display="flex">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LocalizedMessage, Text } from '@novu/novui';
import { Flex, Stack } from '@novu/novui/jsx';
import { FC } from 'react';
import { css } from '@novu/novui/css';
import { Popover, Tooltip, useColorScheme } from '@novu/design-system';
import { Popover, useColorScheme } from '@novu/design-system';
import { useDisclosure } from '@mantine/hooks';

type LocalStudioSidebarOrganizationDisplayProps = {
Expand All @@ -24,6 +24,9 @@ export const LocalStudioSidebarOrganizationDisplay: FC<LocalStudioSidebarOrganiz
offset={0}
withinPortal
title="Novu Local Studio"
classNames={{
dropdown: css({ bg: 'surface.popover !important', border: 'none !important', shadow: 'medium !important' }),
}}
target={
<Flex gap="50" py="75" px="100" onMouseEnter={open} onMouseLeave={close}>
<img
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import reportWebVitals from './reportWebVitals';
import { LAUNCH_DARKLY_CLIENT_SIDE_ID } from './config';

import './index.css';
import '@novu/novui/components.css';
import '@novu/novui/styles.css';

(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ function WorkflowFloatingMenuButton({ Icon, tooltipLabel, onClick }: IWorkflowFl
padding: '75 !important',
borderRadius: '100',
_hover: {
// TODO: this doesn't work due to all the !important in novui... need to fix layer styles
bg: 'legacy.B30 !important',
bg: 'select.option.surface.selected',
'& svg': {
color: 'typography.text.main !important',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { css } from '@novu/novui/css';
import { Container, Flex } from '@novu/novui/jsx';
import { useDebouncedCallback } from '@novu/novui';
import { useTelemetry } from '../../../../hooks/useNovuAPI';
import { getSuggestionVariables, subscriberVariables } from '../../../utils';

export type OnChangeType = 'step' | 'payload';

Expand Down Expand Up @@ -43,6 +44,16 @@ export const WorkflowStepEditorControlsPanel: FC<IWorkflowStepEditorControlsPane
);
}, [workflow?.payload?.schema, workflow?.options?.payloadSchema, workflow?.payloadSchema]);

const payloadProperties = useMemo(() => {
const payloadObject =
workflow?.payload?.schema?.properties ||
workflow?.options?.payloadSchema?.properties ||
workflow?.payloadSchema?.properties ||
{};

return getSuggestionVariables(payloadObject, 'payload');
}, [workflow?.payload?.schema, workflow?.options?.payloadSchema, workflow?.payloadSchema]);

const haveControlProperties = useMemo(() => {
return Object.keys(step?.controls?.schema?.properties || step?.inputs?.schema?.properties || {}).length > 0;
}, [step?.controls?.schema, step?.inputs?.schema]);
Expand Down Expand Up @@ -87,6 +98,7 @@ export const WorkflowStepEditorControlsPanel: FC<IWorkflowStepEditorControlsPane
onChange={(data, id) => handleOnChange('step', data, id)}
schema={step?.controls?.schema || step?.inputs?.schema || {}}
formData={defaultControls || {}}
variables={[...(subscriberVariables || []), ...(payloadProperties || [])]}
/>
</When>
<When truthy={!haveControlProperties}>
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/studio/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './routing';
export * from './variables';
14 changes: 14 additions & 0 deletions apps/web/src/studio/utils/variables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SystemVariablesWithTypes } from '@novu/shared';

export function getSuggestionVariables(schemaObject, namespace: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

question: Do we have more explicit types for schemaObject? Please add if so‏

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically its type should be the RJSFSchema that comes from the@rjsf/utils package that we don't import in web.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha -- then it sounds to me like this helper should live in novui!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about it, but it feels like we are already adding too much logic to novui, which we shouldn't do in a design system. No?

return Object.keys(schemaObject).flatMap((name) => {
const schemaItem = schemaObject[name];
if (schemaItem?.type === 'object') {
return getSuggestionVariables(schemaItem.properties, `${namespace}.${name}`);
}

return `${namespace}.${name}`;
});
}

export const subscriberVariables = getSuggestionVariables(SystemVariablesWithTypes.subscriber, 'subscriber');
17 changes: 12 additions & 5 deletions libs/novui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@
"require": "./styled-system/jsx/index.js",
"import": "./styled-system/jsx/index.js"
},
"./styles.css": "./styled-system/styles.css",
"./components.css": "./node_modules/@mantine/core/styles.layer.css"
"./styles.css": "./src/index.css"
},
"scripts": {
"dev": "pnpm build && pnpm storybook",
Expand Down Expand Up @@ -130,13 +129,21 @@
}
},
"dependencies": {
"@mantine/code-highlight": "^7.10.2",
"@mantine/core": "^7.10.0",
"@mantine/hooks": "^7.10.0",
"@mantine/code-highlight": "^7.11.2",
"@mantine/core": "^7.11.2",
"@mantine/hooks": "^7.11.2",
"@mantine/tiptap": "^7.11.2",
ainouzgali marked this conversation as resolved.
Show resolved Hide resolved
"@rjsf/core": "^5.17.1",
"@rjsf/utils": "^5.17.1",
"@rjsf/validator-ajv8": "^5.17.1",
"@tanstack/react-table": "^8.17.3",
"@tiptap/extension-document": "^2.5.0",
"@tiptap/extension-mention": "^2.5.0",
"@tiptap/extension-paragraph": "^2.5.0",
"@tiptap/extension-text": "^2.5.0",
"@tiptap/pm": "^3.0.0",
"@tiptap/react": "^3.0.0",
"@tiptap/suggestion": "^2.5.0",
"react-icons": "^5.0.1"
}
}
2 changes: 2 additions & 0 deletions libs/novui/src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@layer reset, base, mantine, tokens, recipes, utilities;

@import '@mantine/core/styles.layer.css';
@import '@mantine/tiptap/styles.layer.css';
@import '../styled-system/styles.css';
13 changes: 12 additions & 1 deletion libs/novui/src/json-schema-components/JsonSchemaForm.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,18 @@ const Template: StoryFn<typeof JsonSchemaForm> = ({ colorPalette, ...args }) =>
Save
</Button>
</HStack>
<JsonSchemaForm {...args} />
<JsonSchemaForm
{...args}
variables={[
'fakeautocomplete.foo',
'fakeautocomplete.bar',
'fakeautocomplete.fizz',
'fakeautocomplete.buzz',
'fakeautocomplete.croissantia',
'fakeautocomplete.olympics',
'fakeautocomplete.reallylongstringthatshouldoverflow',
]}
/>
</form>
);
};
Expand Down
15 changes: 9 additions & 6 deletions libs/novui/src/json-schema-components/JsonSchemaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import { CoreProps } from '../types';
import { ArrayFieldItemTemplate, ArrayFieldTemplate, ArrayFieldTitleTemplate } from './templates/ArrayFieldTemplate';
import { AddButton, MoveDownButton, MoveUpButton, RemoveButton } from './templates/IconButton';
import { ObjectFieldTemplate } from './templates/ObjectFieldTemplate';
import { CheckboxWidget, InputWidget, SelectWidget, TextareaWidget } from './widgets';
import { CheckboxWidget, SelectWidget, InputEditorWidget } from './widgets';
import { JSON_SCHEMA_FORM_ID_DELIMITER } from './utils';

const WIDGETS: RegistryWidgetsType = {
CheckboxWidget: CheckboxWidget,
SelectWidget: SelectWidget,
TextWidget: TextareaWidget,
URLWidget: InputWidget,
EmailWidget: InputWidget,
TextWidget: InputEditorWidget,
URLWidget: InputEditorWidget,
EmailWidget: InputEditorWidget,
};

const UI_SCHEMA: UiSchema = {
Expand All @@ -31,13 +31,15 @@ const UI_SCHEMA: UiSchema = {

export type JsonSchemaFormProps<TFormData = any> = JsxStyleProps &
CoreProps &
Pick<FormProps<TFormData>, 'onChange' | 'onSubmit' | 'onBlur' | 'schema' | 'formData' | 'tagName'>;
Pick<FormProps<TFormData>, 'onChange' | 'onSubmit' | 'onBlur' | 'schema' | 'formData' | 'tagName'> & {
variables?: string[];
};

/**
* Specialized form editor for data passed as JSON.
*/
export function JsonSchemaForm<TFormData = any>(props: JsonSchemaFormProps<TFormData>) {
const [cssProps, { className, ...formProps }] = splitCssProps(props);
const [cssProps, { className, variables, ...formProps }] = splitCssProps(props);

return (
<Form
Expand All @@ -60,6 +62,7 @@ export function JsonSchemaForm<TFormData = any>(props: JsonSchemaFormProps<TForm
widgets={WIDGETS}
validator={validator}
autoComplete={'false'}
formContext={{ variables }}
idSeparator={JSON_SCHEMA_FORM_ID_DELIMITER}
liveValidate
templates={{
Expand Down
134 changes: 134 additions & 0 deletions libs/novui/src/json-schema-components/widgets/InputEditorWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { useEffect, useMemo, useRef } from 'react';

import { WidgetProps } from '@rjsf/utils';

import { Input } from '@mantine/core';
import { RichTextEditor } from '@mantine/tiptap';

import { type Extensions, useEditor } from '@tiptap/react';
import Text from '@tiptap/extension-text';
import Paragraph from '@tiptap/extension-paragraph';
import { ReactRenderer } from '@tiptap/react';
import Document from '@tiptap/extension-document';
import Mention from '@tiptap/extension-mention';

import { css, cx } from '../../../styled-system/css';
import { input, inputEditorWidget } from '../../../styled-system/recipes';
import { splitCssProps } from '../../../styled-system/jsx';

import { VariableSuggestionList, SuggestionListRef, VariableItem } from './VariableSuggestionList';

const inputEditorClassNames = inputEditorWidget();

const AUTOCOMPLETE_OPEN_TAG = '{{';
const AUTOCOMPLETE_CLOSE_TAG = '}}';

const AUTOCOMPLETE_REGEX = new RegExp(`${AUTOCOMPLETE_OPEN_TAG}(.*?(.*?))${AUTOCOMPLETE_CLOSE_TAG}`, 'gm');

export const InputEditorWidget = (props: WidgetProps) => {
const { value, label, formContext, onChange, required, readonly, rawErrors, options, schema } = props;
const [variantProps, inputProps] = input.splitVariantProps({});
const [cssProps] = splitCssProps(inputProps);
const classNames = input(variantProps);

const { variables = [] } = formContext;
const reactRenderer = useRef<ReactRenderer<SuggestionListRef>>(null);

const variablesList = useMemo<VariableItem[]>(() => {
return variables?.map((variable: string) => {
return { label: variable, id: variable };
});
}, [variables]);

const baseExtensions: Extensions = [Document, Paragraph, Text];
ainouzgali marked this conversation as resolved.
Show resolved Hide resolved

if (variables.length) {
baseExtensions.push(
Mention.configure({
HTMLAttributes: {
class: 'suggestion',
},
renderHTML: ({ options, node }) => {
return [
'span',
options.HTMLAttributes,
`${options.suggestion.char}${node.attrs.label ?? node.attrs.id}${AUTOCOMPLETE_CLOSE_TAG}`,
];
},
renderText: ({ options, node }) => {
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}${AUTOCOMPLETE_CLOSE_TAG}`;
},
suggestion: {
items: ({ query }) => {
return variablesList?.filter((item) => item.label.toLowerCase().includes(query.toLowerCase()));
},
char: AUTOCOMPLETE_OPEN_TAG,
render() {
return {
onStart: (props) => {
reactRenderer.current = new ReactRenderer(VariableSuggestionList, {
props,
editor: props.editor,
});
},
onUpdate(props) {
reactRenderer.current?.updateProps(props);
},
onKeyDown(props) {
if (!reactRenderer.current?.ref) {
return false;
}

return reactRenderer.current?.ref.onKeyDown(props);
},
onExit() {
reactRenderer.current?.destroy();
},
};
},
},
})
);
}

const editor = useEditor({
extensions: baseExtensions,
content: '',
editable: !readonly,
onFocus: () => {
reactRenderer.current?.ref?.focus();
},
onUpdate: ({ editor }) => {
const content = editor.isEmpty ? undefined : editor.getText();
onChange(content);
},
});

useEffect(() => {
if (editor) {
const newValue = value
?.toString()
.replace(
AUTOCOMPLETE_REGEX,
'<span data-id="$1" contenteditable="false" class="suggestion" data-type="mention">$1</span>'
);

editor.commands.setContent(newValue);
}
}, []);
ainouzgali marked this conversation as resolved.
Show resolved Hide resolved

return (
<Input.Wrapper
classNames={classNames}
className={cx('group', css(cssProps))}
required={required}
label={label}
description={props.schema.description}
error={rawErrors?.length > 0 && rawErrors}
>
<RichTextEditor classNames={inputEditorClassNames} editor={editor}>
<RichTextEditor.Content />
</RichTextEditor>
</Input.Wrapper>
);
};
24 changes: 0 additions & 24 deletions libs/novui/src/json-schema-components/widgets/InputWidget.tsx

This file was deleted.

Loading
Loading