-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Implement string template ast node on builder and workflow * chore: update translations * Fix PR feedbacks * remove 'datetime' value from ReturnValueType for ast-validation --------- Co-authored-by: Christopher DEBOVE <[email protected]>
- Loading branch information
1 parent
553f250
commit c9702e0
Showing
33 changed files
with
889 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
...Node/Operand/OperandEditor/OperandEditModal/StringTemplateEdit/StringTemplateEdit.hook.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { | ||
type AstNode, | ||
NewUndefinedAstNode, | ||
type StringTemplateAstNode, | ||
} from '@app-builder/models'; | ||
import { type AstNodeErrors } from '@app-builder/services/validation/ast-node-validation'; | ||
import { useMemo, useReducer } from 'react'; | ||
|
||
export const STRING_TEMPLATE_VARIABLE_REGEXP = /%([a-z0-9_]+)%/gim; | ||
export const STRING_TEMPLATE_VARIABLE_CAPTURE_REGEXP = /(%[a-z0-9_]+%)/gim; | ||
|
||
export const extractVariablesNamesFromTemplate = (template: string) => { | ||
const res = template.matchAll(STRING_TEMPLATE_VARIABLE_REGEXP).toArray(); | ||
|
||
return res.reduce((acc, match) => { | ||
return match[1] && !acc.includes(match[1]) ? [...acc, match[1]] : acc; | ||
}, [] as string[]); | ||
}; | ||
|
||
type EditStringTemplateState = { | ||
template: string; | ||
variables: Record<string, AstNode>; | ||
}; | ||
|
||
type EditStringTemplateAction = | ||
| { type: 'setTemplate'; payload: { template: string } } | ||
| { type: 'setVariable'; payload: { name: string; data: AstNode } }; | ||
|
||
const editStringTemplateReducer = ( | ||
prevState: EditStringTemplateState, | ||
action: EditStringTemplateAction, | ||
): EditStringTemplateState => { | ||
switch (action.type) { | ||
case 'setTemplate': { | ||
const nextState = { ...prevState, template: action.payload.template }; | ||
const variablesNames = extractVariablesNamesFromTemplate( | ||
action.payload.template, | ||
); | ||
const variables = { ...prevState.variables }; | ||
if (hasEmptyVariable(variables, variablesNames)) { | ||
for (const variableName of variablesNames) { | ||
if (variables[variableName] === undefined) { | ||
variables[variableName] = NewUndefinedAstNode(); | ||
} | ||
} | ||
return { ...nextState, variables }; | ||
} | ||
return nextState; | ||
} | ||
case 'setVariable': | ||
return { | ||
...prevState, | ||
variables: { | ||
...prevState.variables, | ||
[action.payload.name]: action.payload.data, | ||
}, | ||
}; | ||
} | ||
}; | ||
|
||
const hasEmptyVariable = ( | ||
variables: Record<string, AstNode>, | ||
variableNames: string[], | ||
) => { | ||
return ( | ||
variableNames.filter((variableName) => !variables[variableName]).length > 0 | ||
); | ||
}; | ||
|
||
const adaptStringTemplateEditState = ({ | ||
initialNode, | ||
initialErrors: _, | ||
}: { | ||
initialNode: StringTemplateAstNode; | ||
initialErrors: AstNodeErrors | undefined; | ||
}) => { | ||
return { | ||
template: initialNode.children[0]?.constant ?? '', | ||
variables: initialNode.namedChildren, | ||
}; | ||
}; | ||
|
||
export const useStringTemplateEditState = ( | ||
initialNode: StringTemplateAstNode, | ||
initialErrors: AstNodeErrors | undefined, | ||
) => { | ||
const [state, dispatch] = useReducer( | ||
editStringTemplateReducer, | ||
{ initialNode, initialErrors }, | ||
adaptStringTemplateEditState, | ||
); | ||
const variableNames = useMemo( | ||
() => extractVariablesNamesFromTemplate(state.template), | ||
[state.template], | ||
); | ||
|
||
return { | ||
template: state.template, | ||
setTemplate: (template: string) => { | ||
dispatch({ type: 'setTemplate', payload: { template } }); | ||
}, | ||
variableNames, | ||
variables: state.variables, | ||
setVariable: (name: string, data: AstNode) => { | ||
dispatch({ type: 'setVariable', payload: { name, data } }); | ||
}, | ||
}; | ||
}; |
81 changes: 81 additions & 0 deletions
81
...lderNode/Operand/OperandEditor/OperandEditModal/StringTemplateEdit/StringTemplateEdit.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { | ||
type AstNode, | ||
NewStringTemplateAstNode, | ||
type StringTemplateAstNode, | ||
} from '@app-builder/models'; | ||
import { | ||
type AstNodeErrors, | ||
computeLineErrors, | ||
} from '@app-builder/services/validation/ast-node-validation'; | ||
import { useEffect, useRef } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import * as R from 'remeda'; | ||
import { Button, ModalV2 } from 'ui-design-system'; | ||
|
||
import { | ||
extractVariablesNamesFromTemplate, | ||
useStringTemplateEditState, | ||
} from './StringTemplateEdit.hook'; | ||
import { StringTemplateEditForm } from './StringTemplateEditForm'; | ||
|
||
export type StringTemplateEditProps = { | ||
initialNode: StringTemplateAstNode; | ||
initialErrors?: AstNodeErrors; | ||
onSave: (node: AstNode) => void; | ||
onEdit?: (node: AstNode) => void; | ||
}; | ||
|
||
export const StringTemplateEdit = ({ | ||
initialNode, | ||
initialErrors, | ||
onSave, | ||
onEdit, | ||
}: StringTemplateEditProps) => { | ||
const { t } = useTranslation(['scenarios', 'common']); | ||
const state = useStringTemplateEditState(initialNode, initialErrors); | ||
const newNodeRef = useRef<AstNode>(initialNode); | ||
const hasErrors = initialErrors | ||
? computeLineErrors(newNodeRef.current, initialErrors).length > 0 | ||
: false; | ||
|
||
useEffect(() => { | ||
const template = state.template; | ||
const variableNames = extractVariablesNamesFromTemplate(template); | ||
const variables = R.pick(state.variables, variableNames); | ||
|
||
newNodeRef.current = NewStringTemplateAstNode(template, variables); | ||
|
||
onEdit?.(newNodeRef.current); | ||
}, [state.template, state.variables, onEdit]); | ||
|
||
const handleSave = () => { | ||
onSave(newNodeRef.current); | ||
}; | ||
|
||
return ( | ||
<> | ||
<ModalV2.Title>{t('scenarios:edit_string_template.title')}</ModalV2.Title> | ||
<div className="flex flex-col gap-9 p-6"> | ||
<StringTemplateEditForm {...state} errors={initialErrors} /> | ||
<div className="flex flex-1 flex-row gap-2"> | ||
<ModalV2.Close | ||
render={ | ||
<Button className="flex-1" variant="secondary" name="cancel" /> | ||
} | ||
> | ||
{t('common:cancel')} | ||
</ModalV2.Close> | ||
<Button | ||
disabled={hasErrors} | ||
className="flex-1" | ||
variant="primary" | ||
name="save" | ||
onClick={() => handleSave()} | ||
> | ||
{t('common:save')} | ||
</Button> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
}; |
84 changes: 84 additions & 0 deletions
84
...Node/Operand/OperandEditor/OperandEditModal/StringTemplateEdit/StringTemplateEditForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { EvaluationErrors } from '@app-builder/components/Scenario/ScenarioValidationError'; | ||
import { type AstNode, NewUndefinedAstNode } from '@app-builder/models'; | ||
import { | ||
adaptEvaluationErrorViewModels, | ||
useGetNodeEvaluationErrorMessage, | ||
} from '@app-builder/services/validation'; | ||
import { type AstNodeErrors } from '@app-builder/services/validation/ast-node-validation'; | ||
import { type ChangeEvent, Fragment } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Input } from 'ui-design-system'; | ||
|
||
import { VariableOperand } from './VariableOperand'; | ||
|
||
export type StringTemplateEditFormProps = { | ||
template: string; | ||
setTemplate: (template: string) => void; | ||
variables: Record<string, AstNode>; | ||
setVariable: (name: string, data: AstNode) => void; | ||
variableNames: string[]; | ||
errors?: AstNodeErrors; | ||
}; | ||
|
||
export const StringTemplateEditForm = ({ | ||
template, | ||
setTemplate, | ||
variables, | ||
setVariable, | ||
variableNames, | ||
errors, | ||
}: StringTemplateEditFormProps) => { | ||
const { t } = useTranslation(['scenarios', 'common']); | ||
const handleTemplateChange = (event: ChangeEvent<HTMLInputElement>) => { | ||
setTemplate(event.target.value); | ||
}; | ||
const getCommonError = useGetNodeEvaluationErrorMessage(); | ||
const templateErrors = adaptEvaluationErrorViewModels( | ||
errors?.errors.filter((e) => e.argumentIndex === 0) ?? [], | ||
); | ||
|
||
return ( | ||
<div className="flex flex-col gap-4"> | ||
<div className="flex flex-col gap-4"> | ||
{t('scenarios:edit_string_template.template_field.label')} | ||
<Input | ||
value={template} | ||
onChange={handleTemplateChange} | ||
placeholder={t( | ||
'scenarios:edit_string_template.template_field.placeholder', | ||
)} | ||
/> | ||
<EvaluationErrors errors={templateErrors.map(getCommonError)} /> | ||
</div> | ||
{variableNames.length > 0 ? ( | ||
<div className="flex flex-col gap-4"> | ||
{t('scenarios:edit_string_template.variables.label')} | ||
<div className="ml-8 grid grid-cols-[150px_1fr] gap-x-4 gap-y-2"> | ||
{variableNames.map((name) => ( | ||
<Fragment key={name}> | ||
<div className="text-s bg-grey-02 flex size-fit min-h-[40px] min-w-[40px] flex-wrap items-center justify-center gap-1 rounded p-2 font-semibold text-purple-100"> | ||
<span className="max-w-[140px] truncate" title={name}> | ||
{name} | ||
</span> | ||
</div> | ||
<div className="flex flex-col gap-2"> | ||
<VariableOperand | ||
astNode={variables[name] ?? NewUndefinedAstNode()} | ||
astNodeErrors={undefined} | ||
validationStatus="valid" | ||
onChange={(node) => setVariable(name, node)} | ||
/> | ||
<EvaluationErrors | ||
errors={adaptEvaluationErrorViewModels( | ||
errors?.namedChildren[name]?.errors ?? [], | ||
).map(getCommonError)} | ||
/> | ||
</div> | ||
</Fragment> | ||
))} | ||
</div> | ||
</div> | ||
) : null} | ||
</div> | ||
); | ||
}; |
54 changes: 54 additions & 0 deletions
54
...BuilderNode/Operand/OperandEditor/OperandEditModal/StringTemplateEdit/VariableOperand.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { type AstNode } from '@app-builder/models'; | ||
import { | ||
useGetAstNodeOption, | ||
useOperandOptions, | ||
} from '@app-builder/services/editor/options'; | ||
import { | ||
type AstNodeErrors, | ||
type ValidationStatus, | ||
} from '@app-builder/services/validation/ast-node-validation'; | ||
import { useMemo } from 'react'; | ||
|
||
import { Operand } from '../../../Operand'; | ||
|
||
export type VariableOperandProps = { | ||
astNode: AstNode; | ||
astNodeErrors: AstNodeErrors | undefined; | ||
validationStatus: ValidationStatus; | ||
onChange: (node: AstNode) => void; | ||
}; | ||
|
||
export const VariableOperand = ({ | ||
astNode, | ||
astNodeErrors, | ||
validationStatus, | ||
onChange, | ||
}: VariableOperandProps) => { | ||
const options = useOperandOptions([]); | ||
const leftOptions = useMemo( | ||
() => | ||
options.filter( | ||
(option) => | ||
option.dataType === 'String' || | ||
option.dataType === 'Int' || | ||
option.dataType === 'Float', | ||
), | ||
[options], | ||
); | ||
|
||
const getAstNodeOption = useGetAstNodeOption(); | ||
const operandProps = useMemo( | ||
() => getAstNodeOption(astNode), | ||
[astNode, getAstNodeOption], | ||
); | ||
|
||
return ( | ||
<Operand | ||
onSave={onChange} | ||
options={leftOptions} | ||
astNodeErrors={astNodeErrors} | ||
validationStatus={validationStatus} | ||
{...operandProps} | ||
/> | ||
); | ||
}; |
Oops, something went wrong.