mirror of
https://github.com/portainer/portainer.git
synced 2025-08-08 15:25:22 +02:00
refactor(custom-templates): render template variables [EE-2602] (#6937)
This commit is contained in:
parent
71c0e8e661
commit
1ccdb64938
32 changed files with 829 additions and 47 deletions
|
@ -74,7 +74,7 @@ function SelectAndInputItem({
|
|||
onChange: (value: ListWithSelectItem) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={item.value}
|
||||
|
|
|
@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|||
|
||||
import { AddButton, Button } from '@/portainer/components/Button';
|
||||
import { Tooltip } from '@/portainer/components/Tip/Tooltip';
|
||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||
|
||||
import { Input } from '../Input';
|
||||
import { FormError } from '../FormError';
|
||||
|
@ -32,17 +33,26 @@ type OnChangeEvent<T> =
|
|||
to: number;
|
||||
};
|
||||
|
||||
type RenderItemFunction<T> = (
|
||||
item: T,
|
||||
onChange: (value: T) => void,
|
||||
error?: InputListError<T>
|
||||
) => React.ReactNode;
|
||||
|
||||
interface Props<T> {
|
||||
label: string;
|
||||
value: T[];
|
||||
onChange(value: T[], e: OnChangeEvent<T>): void;
|
||||
itemBuilder?(): T;
|
||||
renderItem?: RenderItemFunction<T>;
|
||||
item?: ComponentType<ItemProps<T>>;
|
||||
tooltip?: string;
|
||||
addLabel?: string;
|
||||
itemKeyGetter?(item: T, index: number): Key;
|
||||
movable?: boolean;
|
||||
errors?: InputListError<T>[] | string;
|
||||
textTip?: string;
|
||||
isAddButtonHidden?: boolean;
|
||||
}
|
||||
|
||||
export function InputList<T = DefaultType>({
|
||||
|
@ -50,15 +60,16 @@ export function InputList<T = DefaultType>({
|
|||
value,
|
||||
onChange,
|
||||
itemBuilder = defaultItemBuilder as unknown as () => T,
|
||||
item = DefaultItem as unknown as ComponentType<ItemProps<T>>,
|
||||
renderItem = renderDefaultItem as unknown as RenderItemFunction<T>,
|
||||
item: Item,
|
||||
tooltip,
|
||||
addLabel = 'Add item',
|
||||
itemKeyGetter = (item: T, index: number) => index,
|
||||
movable,
|
||||
errors,
|
||||
textTip,
|
||||
isAddButtonHidden = false,
|
||||
}: Props<T>) {
|
||||
const Item = item;
|
||||
|
||||
return (
|
||||
<div className={clsx('form-group', styles.root)}>
|
||||
<div className={clsx('col-sm-12', styles.header)}>
|
||||
|
@ -66,14 +77,22 @@ export function InputList<T = DefaultType>({
|
|||
{label}
|
||||
{tooltip && <Tooltip message={tooltip} />}
|
||||
</div>
|
||||
<AddButton
|
||||
label={addLabel}
|
||||
className="space-left"
|
||||
onClick={handleAdd}
|
||||
/>
|
||||
{!isAddButtonHidden && (
|
||||
<AddButton
|
||||
label={addLabel}
|
||||
className="space-left"
|
||||
onClick={handleAdd}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={clsx('col-sm-12 form-inline', styles.items)}>
|
||||
{textTip && (
|
||||
<div className="col-sm-12 my-5">
|
||||
<TextTip color="blue">{textTip}</TextTip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx('col-sm-12', styles.items, 'space-y-4')}>
|
||||
{value.map((item, index) => {
|
||||
const key = itemKeyGetter(item, index);
|
||||
const error = typeof errors === 'object' ? errors[index] : undefined;
|
||||
|
@ -83,12 +102,20 @@ export function InputList<T = DefaultType>({
|
|||
key={key}
|
||||
className={clsx(styles.itemLine, { [styles.hasError]: !!error })}
|
||||
>
|
||||
<Item
|
||||
item={item}
|
||||
onChange={(value: T) => handleChangeItem(key, value)}
|
||||
error={error}
|
||||
/>
|
||||
<div className={styles.itemActions}>
|
||||
{Item ? (
|
||||
<Item
|
||||
item={item}
|
||||
onChange={(value: T) => handleChangeItem(key, value)}
|
||||
error={error}
|
||||
/>
|
||||
) : (
|
||||
renderItem(
|
||||
item,
|
||||
(value: T) => handleChangeItem(key, value),
|
||||
error
|
||||
)
|
||||
)}
|
||||
<div className={clsx(styles.itemActions, 'items-start')}>
|
||||
{movable && (
|
||||
<>
|
||||
<Button
|
||||
|
@ -191,7 +218,15 @@ function DefaultItem({ item, onChange, error }: ItemProps<DefaultType>) {
|
|||
onChange={(e) => onChange({ value: e.target.value })}
|
||||
className={styles.defaultItem}
|
||||
/>
|
||||
<FormError>{error}</FormError>
|
||||
{error && <FormError>{error}</FormError>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDefaultItem(
|
||||
item: DefaultType,
|
||||
onChange: (value: DefaultType) => void,
|
||||
error?: InputListError<DefaultType>
|
||||
) {
|
||||
return <DefaultItem item={item} onChange={onChange} error={error} />;
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.name || !$ctrl.state.deployable"
|
||||
ng-disabled="$ctrl.state.actionInProgress || !$ctrl.formValues.name || !$ctrl.state.deployable || stackTemplateForm.$invalid"
|
||||
ng-click="$ctrl.createTemplate()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
|
|
24
app/portainer/react/components/custom-templates/index.ts
Normal file
24
app/portainer/react/components/custom-templates/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { CustomTemplatesVariablesDefinitionField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField';
|
||||
import { CustomTemplatesVariablesField } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesField';
|
||||
|
||||
import { VariablesFieldAngular } from './variables-field';
|
||||
|
||||
export const customTemplatesModule = angular
|
||||
.module('portainer.app.react.components.custom-templates', [])
|
||||
.component(
|
||||
'customTemplatesVariablesFieldReact',
|
||||
r2a(CustomTemplatesVariablesField, ['value', 'onChange', 'definitions'])
|
||||
)
|
||||
.component('customTemplatesVariablesField', VariablesFieldAngular)
|
||||
.component(
|
||||
'customTemplatesVariablesDefinitionField',
|
||||
r2a(CustomTemplatesVariablesDefinitionField, [
|
||||
'onChange',
|
||||
'value',
|
||||
'errors',
|
||||
'isVariablesNamesFromParent',
|
||||
])
|
||||
).name;
|
|
@ -0,0 +1,71 @@
|
|||
import {
|
||||
IComponentOptions,
|
||||
IComponentController,
|
||||
IFormController,
|
||||
IScope,
|
||||
IOnChangesObject,
|
||||
} from 'angular';
|
||||
|
||||
import { VariableDefinition } from '@/react/portainer/custom-templates/components/CustomTemplatesVariablesDefinitionField/CustomTemplatesVariablesDefinitionField';
|
||||
|
||||
class VariablesFieldController implements IComponentController {
|
||||
formCtrl!: IFormController;
|
||||
|
||||
value!: Record<string, string>;
|
||||
|
||||
definitions!: VariableDefinition[];
|
||||
|
||||
onChange!: (value: Record<string, string>) => void;
|
||||
|
||||
$scope: IScope;
|
||||
|
||||
/* @ngInject */
|
||||
constructor($scope: IScope) {
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
|
||||
this.$scope = $scope;
|
||||
}
|
||||
|
||||
handleChange(value: Record<string, string>) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.onChange(value);
|
||||
});
|
||||
}
|
||||
|
||||
$onChanges({ value }: IOnChangesObject) {
|
||||
if (value.currentValue) {
|
||||
this.checkValidity(value.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
checkValidity(value: Record<string, string>) {
|
||||
this.formCtrl.$setValidity(
|
||||
'templateVariables',
|
||||
Object.entries(value).every(
|
||||
([name, value]) =>
|
||||
!!value ||
|
||||
this.definitions.some(
|
||||
(def) => def.name === name && !!def.defaultValue
|
||||
)
|
||||
),
|
||||
this.formCtrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const VariablesFieldAngular: IComponentOptions = {
|
||||
template: `<custom-templates-variables-field-react
|
||||
value="$ctrl.value"
|
||||
on-change="$ctrl.handleChange"
|
||||
definitions="$ctrl.definitions"
|
||||
></custom-templates-variables-field-react>`,
|
||||
bindings: {
|
||||
value: '<',
|
||||
definitions: '<',
|
||||
onChange: '<',
|
||||
},
|
||||
require: {
|
||||
formCtrl: '^form',
|
||||
},
|
||||
controller: VariablesFieldController,
|
||||
};
|
|
@ -3,8 +3,10 @@ import angular from 'angular';
|
|||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { TagSelector } from '@/react/components/TagSelector';
|
||||
|
||||
import { customTemplatesModule } from './custom-templates';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.app.react.components', [])
|
||||
.module('portainer.app.react.components', [customTemplatesModule])
|
||||
.component(
|
||||
'tagSelector',
|
||||
r2a(TagSelector, ['allowCreate', 'onChange', 'value'])
|
||||
|
|
|
@ -96,7 +96,23 @@
|
|||
</div>
|
||||
<!-- !upload -->
|
||||
<!-- repository -->
|
||||
<git-form ng-if="$ctrl.state.Method === 'repository'" model="$ctrl.formValues" on-change="($ctrl.onChangeFormValues)"></git-form>
|
||||
<git-form ng-if="$ctrl.state.Method === 'repository'" model="$ctrl.formValues" on-change="($ctrl.handleChange)"></git-form>
|
||||
|
||||
<div class="form-group" ng-if="!$ctrl.state.isTemplateValid">
|
||||
<div class="col-sm-12">
|
||||
<div class="small text-warning">
|
||||
<i class="fa fa-exclamation-triangle space-right" aria-hidden="true"></i>
|
||||
Template is invalid.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<custom-templates-variables-definition-field
|
||||
value="$ctrl.formValues.Variables"
|
||||
on-change="($ctrl.onVariablesChange)"
|
||||
is-variables-names-from-parent="$ctrl.state.Method === 'editor'"
|
||||
></custom-templates-variables-definition-field>
|
||||
|
||||
<!-- !repository -->
|
||||
<por-access-control-form form-data="$ctrl.formValues.AccessControlData"></por-access-control-form>
|
||||
|
||||
|
@ -111,7 +127,8 @@
|
|||
|| ($ctrl.state.Method === 'editor' && !$ctrl.formValues.FileContent)
|
||||
|| ($ctrl.state.Method === 'upload' && !$ctrl.formValues.File)
|
||||
|| ($ctrl.state.Method === 'repository' && ((!$ctrl.formValues.RepositoryURL || !$ctrl.formValues.ComposeFilePathInRepository) || ($ctrl.formValues.RepositoryAuthentication && (!$ctrl.formValues.RepositoryUsername || !$ctrl.formValues.RepositoryPassword))))
|
||||
|| !$ctrl.formValues.Title"
|
||||
|| !$ctrl.formValues.Title
|
||||
|| !$ctrl.state.isTemplateValid"
|
||||
ng-click="$ctrl.createCustomTemplate()"
|
||||
button-spinner="$ctrl.state.actionInProgress"
|
||||
>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||
|
||||
class CreateCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
|
@ -35,6 +36,7 @@ class CreateCustomTemplateViewController {
|
|||
Platform: 1,
|
||||
Type: 1,
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
Variables: [],
|
||||
};
|
||||
|
||||
this.state = {
|
||||
|
@ -45,6 +47,7 @@ class CreateCustomTemplateViewController {
|
|||
loading: true,
|
||||
isEditorDirty: false,
|
||||
templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
isTemplateValid: true,
|
||||
};
|
||||
|
||||
this.templates = [];
|
||||
|
@ -58,7 +61,21 @@ class CreateCustomTemplateViewController {
|
|||
this.createCustomTemplateFromGitRepository = this.createCustomTemplateFromGitRepository.bind(this);
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.onChangeMethod = this.onChangeMethod.bind(this);
|
||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||
this.onVariablesChange = this.onVariablesChange.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
onVariablesChange(value) {
|
||||
this.handleChange({ Variables: value });
|
||||
}
|
||||
|
||||
handleChange(values) {
|
||||
return this.$async(async () => {
|
||||
this.formValues = {
|
||||
...this.formValues,
|
||||
...values,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
createCustomTemplate() {
|
||||
|
@ -67,6 +84,7 @@ class CreateCustomTemplateViewController {
|
|||
|
||||
onChangeMethod() {
|
||||
this.formValues.FileContent = '';
|
||||
this.formValues.Variables = [];
|
||||
this.selectedTemplate = null;
|
||||
}
|
||||
|
||||
|
@ -151,12 +169,22 @@ class CreateCustomTemplateViewController {
|
|||
}
|
||||
|
||||
editorUpdate(cm) {
|
||||
this.formValues.FileContent = cm.getValue();
|
||||
const value = cm.getValue();
|
||||
this.formValues.FileContent = value;
|
||||
this.state.isEditorDirty = true;
|
||||
this.parseTemplate(value);
|
||||
}
|
||||
|
||||
onChangeFormValues(newValues) {
|
||||
this.formValues = newValues;
|
||||
parseTemplate(templateStr) {
|
||||
const variables = getTemplateVariables(templateStr);
|
||||
|
||||
const isValid = !!variables;
|
||||
|
||||
this.state.isTemplateValid = isValid;
|
||||
|
||||
if (isValid) {
|
||||
this.onVariablesChange(intersectVariables(this.formValues.Variables, variables));
|
||||
}
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
|
|
|
@ -17,6 +17,12 @@
|
|||
unselect-template="$ctrl.unselectTemplate"
|
||||
>
|
||||
<advanced-form>
|
||||
<custom-templates-variables-field
|
||||
definitions="$ctrl.state.selectedTemplate.Variables"
|
||||
value="$ctrl.formValues.variables"
|
||||
on-change="($ctrl.onChangeTemplateVariables)"
|
||||
></custom-templates-variables-field>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<a class="small interactive" ng-show="!$ctrl.state.showAdvancedOptions" ng-click="$ctrl.state.showAdvancedOptions = true;">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import _ from 'lodash-es';
|
||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
||||
|
||||
class CustomTemplatesViewController {
|
||||
/* @ngInject */
|
||||
|
@ -44,6 +45,7 @@ class CustomTemplatesViewController {
|
|||
isEditorVisible: false,
|
||||
deployable: false,
|
||||
templateNameRegex: TEMPLATE_NAME_VALIDATION_REGEX,
|
||||
templateContent: '',
|
||||
};
|
||||
|
||||
this.currentUser = {
|
||||
|
@ -56,6 +58,7 @@ class CustomTemplatesViewController {
|
|||
name: '',
|
||||
fileContent: '',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
variables: [],
|
||||
};
|
||||
|
||||
this.getTemplates = this.getTemplates.bind(this);
|
||||
|
@ -75,6 +78,8 @@ class CustomTemplatesViewController {
|
|||
this.confirmDeleteAsync = this.confirmDeleteAsync.bind(this);
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.isEditAllowed = this.isEditAllowed.bind(this);
|
||||
this.onChangeFormValues = this.onChangeFormValues.bind(this);
|
||||
this.onChangeTemplateVariables = this.onChangeTemplateVariables.bind(this);
|
||||
}
|
||||
|
||||
isEditAllowed(template) {
|
||||
|
@ -107,6 +112,24 @@ class CustomTemplatesViewController {
|
|||
}
|
||||
}
|
||||
|
||||
onChangeTemplateVariables(variables) {
|
||||
this.onChangeFormValues({ variables });
|
||||
|
||||
this.renderTemplate();
|
||||
}
|
||||
|
||||
renderTemplate() {
|
||||
const fileContent = renderTemplate(this.state.templateContent, this.formValues.variables, this.state.selectedTemplate.Variables);
|
||||
this.onChangeFormValues({ fileContent });
|
||||
}
|
||||
|
||||
onChangeFormValues(values) {
|
||||
this.formValues = {
|
||||
...this.formValues,
|
||||
...values,
|
||||
};
|
||||
}
|
||||
|
||||
validateForm(accessControlData, isAdmin) {
|
||||
this.state.formValidationError = '';
|
||||
const error = this.FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||
|
@ -161,6 +184,7 @@ class CustomTemplatesViewController {
|
|||
name: '',
|
||||
fileContent: '',
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
variables: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -184,7 +208,13 @@ class CustomTemplatesViewController {
|
|||
const applicationState = this.StateManager.getState();
|
||||
this.state.deployable = this.isDeployable(applicationState.endpoint, template.Type);
|
||||
const file = await this.CustomTemplateService.customTemplateFile(template.Id);
|
||||
this.state.templateContent = file;
|
||||
this.formValues.fileContent = file;
|
||||
|
||||
if (template.Variables && template.Variables.length > 0) {
|
||||
const variables = Object.fromEntries(template.Variables.map((variable) => [variable.name, '']));
|
||||
this.onChangeTemplateVariables(variables);
|
||||
}
|
||||
}
|
||||
|
||||
getNetworks(provider, apiVersion) {
|
||||
|
|
|
@ -36,6 +36,21 @@
|
|||
</div>
|
||||
<!-- !web-editor -->
|
||||
|
||||
<div class="form-group" ng-if="!$ctrl.state.isTemplateValid">
|
||||
<div class="col-sm-12">
|
||||
<div class="small text-warning">
|
||||
<i class="fa fa-exclamation-triangle space-right" aria-hidden="true"></i>
|
||||
Template is invalid.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<custom-templates-variables-definition-field
|
||||
value="$ctrl.formValues.Variables"
|
||||
on-change="($ctrl.onVariablesChange)"
|
||||
is-variables-names-from-parent="true"
|
||||
></custom-templates-variables-definition-field>
|
||||
|
||||
<por-access-control-form form-data="$ctrl.formValues.AccessControlData" resource-control="$ctrl.formValues.ResourceControl"></por-access-control-form>
|
||||
|
||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
||||
|
@ -46,7 +61,9 @@
|
|||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.actionInProgress || customTemplateForm.$invalid
|
||||
|| !$ctrl.formValues.Title
|
||||
|| !$ctrl.formValues.FileContent"
|
||||
|| !$ctrl.formValues.FileContent
|
||||
|| !$ctrl.state.isTemplateValid
|
||||
"
|
||||
ng-click="$ctrl.submitAction()"
|
||||
button-spinner="$ctrl.actionInProgress"
|
||||
>
|
||||
|
|
|
@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
||||
|
||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||
|
||||
class EditCustomTemplateViewController {
|
||||
/* @ngInject */
|
||||
|
@ -12,6 +13,7 @@ class EditCustomTemplateViewController {
|
|||
this.state = {
|
||||
formValidationError: '',
|
||||
isEditorDirty: false,
|
||||
isTemplateValid: true,
|
||||
};
|
||||
this.templates = [];
|
||||
|
||||
|
@ -20,6 +22,8 @@ class EditCustomTemplateViewController {
|
|||
this.submitAction = this.submitAction.bind(this);
|
||||
this.submitActionAsync = this.submitActionAsync.bind(this);
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.onVariablesChange = this.onVariablesChange.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
getTemplate() {
|
||||
|
@ -32,15 +36,33 @@ class EditCustomTemplateViewController {
|
|||
this.CustomTemplateService.customTemplateFile(this.$state.params.id),
|
||||
]);
|
||||
template.FileContent = file;
|
||||
template.Variables = template.Variables || [];
|
||||
this.formValues = template;
|
||||
this.parseTemplate(template.FileContent);
|
||||
|
||||
this.oldFileContent = this.formValues.FileContent;
|
||||
this.formValues.ResourceControl = new ResourceControlViewModel(template.ResourceControl);
|
||||
if (template.ResourceControl) {
|
||||
this.formValues.ResourceControl = new ResourceControlViewModel(template.ResourceControl);
|
||||
}
|
||||
this.formValues.AccessControlData = new AccessControlFormData();
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve custom template data');
|
||||
}
|
||||
}
|
||||
|
||||
onVariablesChange(value) {
|
||||
this.handleChange({ Variables: value });
|
||||
}
|
||||
|
||||
handleChange(values) {
|
||||
return this.$async(async () => {
|
||||
this.formValues = {
|
||||
...this.formValues,
|
||||
...values,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
this.state.formValidationError = '';
|
||||
|
||||
|
@ -96,12 +118,26 @@ class EditCustomTemplateViewController {
|
|||
}
|
||||
|
||||
editorUpdate(cm) {
|
||||
if (this.formValues.FileContent.replace(/(\r\n|\n|\r)/gm, '') !== cm.getValue().replace(/(\r\n|\n|\r)/gm, '')) {
|
||||
this.formValues.FileContent = cm.getValue();
|
||||
const value = cm.getValue();
|
||||
if (this.formValues.FileContent.replace(/(\r\n|\n|\r)/gm, '') !== value.replace(/(\r\n|\n|\r)/gm, '')) {
|
||||
this.formValues.FileContent = value;
|
||||
this.parseTemplate(value);
|
||||
this.state.isEditorDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
parseTemplate(templateStr) {
|
||||
const variables = getTemplateVariables(templateStr);
|
||||
|
||||
const isValid = !!variables;
|
||||
|
||||
this.state.isTemplateValid = isValid;
|
||||
|
||||
if (isValid) {
|
||||
this.onVariablesChange(intersectVariables(this.formValues.Variables, variables));
|
||||
}
|
||||
}
|
||||
|
||||
async uiCanExit() {
|
||||
if (this.formValues.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
|
||||
return this.ModalService.confirmWebEditorDiscard();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue