Skip to content

Commit

Permalink
Save and load AST builder from and to backend (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
Roukii authored Jul 26, 2023
2 parents 912970b + 3d460c1 commit 64c8d3c
Show file tree
Hide file tree
Showing 27 changed files with 742 additions and 240 deletions.
3 changes: 2 additions & 1 deletion packages/app-builder/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"delete": "Delete",
"close": "Close",
"clipboard.copy": "Copied in clipboard: {{value}}",
"empty_scenario_iteration_list": "Reach out to Marble to create your first scenario draft."
"empty_scenario_iteration_list": "Reach out to Marble to create your first scenario draft.",
"success.save": "Saved successfully"
}
4 changes: 2 additions & 2 deletions packages/app-builder/public/locales/en/lists.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"other_scenarios_one": "+{{count}} other",
"other_scenarios_other": "+{{count}} others",
"show_less": "show less",
"empty_custom_lists_list": "Reach out to Marble to create your first list.",
"empty_custom_list_values_list": "Create your first list value to see it here.",
"empty_custom_lists_list": "You do not have any list. Add your first one to see it here.",
"empty_custom_list_values_list": "This list is empty. Add its first value to see it here.",
"create_list.title": "New List",
"create_list.name_placeholder": "Add a name to your list",
"create_list.description_placeholder": "Add a description",
Expand Down
5 changes: 3 additions & 2 deletions packages/app-builder/public/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
"auth.logout": "Se déconnecter",
"auth.login": "Connexion",
"cancel": "Annuler",
"clipboard.copy": "Copié dans le presse-papier : {{value}}",
"clipboard.copy": "Copié dans le presse-papier : {{value}}",
"close": "Fermer",
"errors.unknown": "Une erreur inconnue s'est produite",
"errors.edit.forbidden_not_draft": "Vous ne pouvez modifier qu'une version brouillon d'un scénario.",
"errors.list.duplicate_list_name": "Une liste avec ce nom existe déjà",
"search": "Recherche",
"empty_scenario_iteration_list": "Contactez Marble pour créer votre premier brouillon de scénario.",
"delete": "Supprimer",
"save": "Sauvegarder"
"save": "Sauvegarder",
"success.save": "Sauvegarde réussie"
}
8 changes: 4 additions & 4 deletions packages/app-builder/public/locales/fr/lists.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"other_scenarios_one": "{{count}} autre",
"other_scenarios_other": "{{count}} autres",
"show_less": "Montrer moins",
"used_in_scenarios": "Utilisé dans les scénarios suivants :",
"empty_custom_lists_list": "Contactez Marble pour créer votre première liste.",
"used_in_scenarios": "Utilisé dans les scénarios suivants :",
"empty_custom_lists_list": "Vous n'avez pas encore de liste. Créer votre première liste pour la voir s'afficher ici",
"empty_custom_list_values_list": "Cette liste est vide. Ajouter votre première valeur pour la voir s'afficher ici",
"create_value.title": "Nouvelle valeur",
"create_value.value_placeholder": "Ajouter une nouvelle valeur à votre liste",
"create_list.button_accept": "Creer une nouvelle liste",
Expand All @@ -21,6 +22,5 @@
"delete_value.no_return": "Cette action est irréversible, voulez vous continuer ?",
"create_list.title": "Nouvelle liste",
"create_list.name_placeholder": "Ajouter un nom à cette liste",
"create_list.description_placeholder": "Ajouter une description",
"empty_custom_list_values_list": "Créez votre première valeur de liste pour la voir ici."
"create_list.description_placeholder": "Ajouter une description"
}
123 changes: 38 additions & 85 deletions packages/app-builder/src/components/Edit/EditAstNode.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { type AstNode, NewAstNode } from '@app-builder/models';
import { useEditorIdentifiers } from '@app-builder/services/editor';
import {
adaptAstNodeToViewModelFromIdentifier,
type AstNode,
} from '@app-builder/models';
import {
useEditorIdentifiers,
useEditorOperators,
useGetIdentifierOptions,
useGetOperatorName,
useIsEditedOnce,
} from '@app-builder/services/editor';
import { Combobox, Select } from '@ui-design-system';
import { forwardRef, useCallback, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { forwardRef, useState } from 'react';

import { FormControl, FormField, FormItem } from '../Form';
import { useGetOperatorLabel } from '../Scenario/Formula/Operators';

export function EditAstNode({ name }: { name: string }) {
const { getFieldState, formState } = useFormContext();
const firstChildState = getFieldState(`${name}.children.0`, formState);
const nameState = getFieldState(`${name}.name`, formState);
const isFirstChildEditedOnce = useIsEditedOnce(`${name}.children.0`);
const isNameEditedOnce = useIsEditedOnce(`${name}.name`);

return (
<div className="flex flex-row gap-1">
Expand All @@ -26,9 +32,8 @@ export function EditAstNode({ name }: { name: string }) {
/>
<FormField
name={`${name}.name`}
rules={{ required: true }}
render={({ field }) => (
<FormItem className={firstChildState.isDirty ? '' : 'hidden'}>
<FormItem className={isFirstChildEditedOnce ? '' : 'hidden'}>
<FormControl>
<EditOperator {...field} />
</FormControl>
Expand All @@ -38,7 +43,7 @@ export function EditAstNode({ name }: { name: string }) {
<FormField
name={`${name}.children.1`}
render={({ field }) => (
<FormItem className={nameState.isDirty ? '' : 'hidden'}>
<FormItem className={isNameEditedOnce ? '' : 'hidden'}>
<FormControl>
<EditOperand {...field} />
</FormControl>
Expand All @@ -49,31 +54,33 @@ export function EditAstNode({ name }: { name: string }) {
);
}

//TODO: connect value to Combobox (we may need to save {label:string; node: AstNode} in the form to ease the process)
const EditOperand = forwardRef<
HTMLInputElement,
{
name: string;
value: string | null;
value: AstNode | null;
onChange: (value: AstNode | null) => void;
onBlur: () => void;
}
>(({ onChange, onBlur }, ref) => {
>(({ onChange, onBlur, value }, ref) => {
const editorIdentifier = useEditorIdentifiers();
const getIdentifierOptions = useGetIdentifierOptions();
const [inputValue, setInputValue] = useState('');
const [selectedItem, setSelectedItem] = useState<
ReturnType<typeof getIdentifierOptions>[number] | null
>(null);
const selectedItem = value
? adaptAstNodeToViewModelFromIdentifier(value, editorIdentifier)
: null;

const [inputValue, setInputValue] = useState(selectedItem?.label ?? '');

const items = getIdentifierOptions(inputValue);

const filteredItems = items.filter((item) => item.label.includes(inputValue));

return (
<Combobox.Root
<Combobox.Root<(typeof items)[0]>
value={selectedItem}
onChange={(value) => {
setSelectedItem(value);
onChange(value?.node ?? null);
setInputValue(value?.label ?? '');
onChange(value?.astNode ?? null);
}}
nullable
>
Expand Down Expand Up @@ -101,41 +108,6 @@ const EditOperand = forwardRef<
});
EditOperand.displayName = 'EditOperand';

function coerceToConstant(search: string) {
const parsedNumber = Number(search);
const isNumber = !isNaN(parsedNumber);

if (isNumber) {
return {
label: search,
node: NewAstNode({
name: 'CONSTANT_FLOAT',
constant: parsedNumber,
}),
};
}

return {
label: `"${search}"`,
node: NewAstNode({
name: 'CONSTANT_STRING',
constant: search,
}),
};
}

function useGetIdentifierOptions() {
const identifiers = useEditorIdentifiers();

return useCallback(
(search: string) => {
if (!search) return identifiers;
return [...identifiers, coerceToConstant(search)];
},
[identifiers]
);
}

const EditOperator = forwardRef<
HTMLButtonElement,
{
Expand All @@ -145,7 +117,8 @@ const EditOperator = forwardRef<
onBlur: () => void;
}
>(({ name, value, onChange, onBlur }, ref) => {
const getOperatorLabel = useGetOperatorLabel();
const operators = useEditorOperators();
const getOperatorName = useGetOperatorName();

return (
<Select.Root
Expand All @@ -164,21 +137,18 @@ const EditOperator = forwardRef<
</Select.Trigger>
<Select.Content className="max-h-60">
<Select.Viewport>
{mockedOperators.map((operator) => {
{operators.map((operator) => {
return (
<Select.Item
className="min-w-[110px]"
key={operator}
value={operator}
key={operator.name}
value={operator.name}
>
<p className="flex flex-col gap-1">
<Select.ItemText>
<span className="text-s text-grey-100 font-semibold">
{getOperatorLabel(operator)}
</span>
</Select.ItemText>
<span className="text-grey-50 text-xs">{operator}</span>
</p>
<Select.ItemText>
<span className="text-s text-grey-100 font-semibold">
{getOperatorName(operator.name)}
</span>
</Select.ItemText>
</Select.Item>
);
})}
Expand All @@ -188,20 +158,3 @@ const EditOperator = forwardRef<
);
});
EditOperator.displayName = 'EditOperator';

const mockedOperators = [
'EQUAL_BOOL',
'EQUAL_FLOAT',
'EQUAL_STRING',
'AND',
'PRODUCT_FLOAT',
'OR',
'SUM_FLOAT',
'SUBTRACT_FLOAT',
'DIVIDE_FLOAT',
'GREATER_FLOAT',
'GREATER_OR_EQUAL_FLOAT',
'LESSER_FLOAT',
'LESSER_OR_EQUAL_FLOAT',
'STRING_IS_IN_LIST',
] as const;
26 changes: 16 additions & 10 deletions packages/app-builder/src/components/Edit/RootOrWithAnd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import * as React from 'react';
import { useFieldArray } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import { LogicalOperator } from '../Scenario/LogicalOperator';
import { LogicalOperatorLabel } from '../Scenario/LogicalOperator';
import { RemoveButton } from './RemoveButton';

type RootOrWithAndFormFields = {
astNode: AstNode;
};

const AddLogicalOperator = React.forwardRef<
const AddLogicalOperatorButton = React.forwardRef<
HTMLButtonElement,
ButtonProps & {
operator: 'if' | 'and' | 'or' | 'where';
Expand All @@ -27,7 +27,11 @@ const AddLogicalOperator = React.forwardRef<
</Button>
);
});
AddLogicalOperator.displayName = 'AddLogicalOperator';
AddLogicalOperatorButton.displayName = 'AddLogicalOperatorButton';

function NewBinaryAstNode() {
return NewAstNode({ children: [NewAstNode(), NewAstNode()] });
}

export function RootOrOperator({
renderAstNode,
Expand All @@ -44,8 +48,8 @@ export function RootOrOperator({

function appendOrOperand() {
append({
name: 'AND',
children: [NewAstNode()],
name: 'And',
children: [NewBinaryAstNode()],
namedChildren: {},
constant: null,
});
Expand All @@ -60,7 +64,7 @@ export function RootOrOperator({
<React.Fragment key={operand.id}>
{!isFirstOperand && (
<div className="flex flex-row gap-1">
<LogicalOperator
<LogicalOperatorLabel
operator="or"
className="bg-grey-02 uppercase"
/>
Expand All @@ -77,7 +81,7 @@ export function RootOrOperator({
</React.Fragment>
);
})}
<AddLogicalOperator onClick={appendOrOperand} operator="or" />
<AddLogicalOperatorButton onClick={appendOrOperand} operator="or" />
</div>
);
}
Expand Down Expand Up @@ -105,7 +109,7 @@ function RootAndOperator({
}

function appendAndOperand() {
append(NewAstNode());
append(NewBinaryAstNode());
}

return (
Expand All @@ -125,12 +129,14 @@ function RootAndOperator({
<div className="peer-hover:border-grey-25 flex flex-1 flex-col rounded border border-transparent p-1 transition-colors duration-200 ease-in-out">
{renderAstNode({ name: `${name}.${operandIndex}` })}
</div>
<LogicalOperator operator={operandIndex === 0 ? 'if' : 'and'} />
<LogicalOperatorLabel
operator={operandIndex === 0 ? 'if' : 'and'}
/>
</div>
);
})}

<AddLogicalOperator
<AddLogicalOperatorButton
className="text-grey-25 h-fit w-fit text-xs"
variant="secondary"
onClick={appendAndOperand}
Expand Down
10 changes: 3 additions & 7 deletions packages/app-builder/src/components/Edit/WildEditAstNode.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type AstNode, NewAstNode, NoConstant } from '@app-builder/models';
import { type AstNode, NewAstNode } from '@app-builder/models';
import { Button, Input } from '@ui-design-system';
import { useFieldArray } from 'react-hook-form';

Expand Down Expand Up @@ -42,13 +42,9 @@ export function WildEditAstNode<TName extends string>({
<FormControl>
<Input
{...field}
value={
field.value === NoConstant || !field.value
? ''
: field.value.toString()
}
value={field.value?.toString() ?? ''}
onChange={(event) => {
field.onChange(event.target.value || NoConstant);
field.onChange(event.target.value ?? null);
}}
/>
</FormControl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { useTranslation } from 'react-i18next';
import { scenarioI18n } from './scenario-i18n';
import { ScenarioBox } from './ScenarioBox';

interface LogicalOperatorProps {
interface LogicalOperatorLabelProps {
operator: 'if' | 'and' | 'or' | 'where';
className?: string;
}

export function LogicalOperator({ operator, className }: LogicalOperatorProps) {
export function LogicalOperatorLabel({
operator,
className,
}: LogicalOperatorLabelProps) {
const { t } = useTranslation(scenarioI18n);

return (
Expand Down
Loading

0 comments on commit 64c8d3c

Please sign in to comment.