2021-01-20 06:42:45 +00:00
|
|
|
import React from "react";
|
|
|
|
import {
|
2021-01-26 17:44:14 +00:00
|
|
|
CommandBar,
|
|
|
|
ICommandBarItemProps,
|
|
|
|
IStackTokens,
|
|
|
|
MessageBar,
|
|
|
|
MessageBarType,
|
|
|
|
Spinner,
|
|
|
|
SpinnerSize,
|
|
|
|
Stack,
|
|
|
|
} from "office-ui-fabric-react";
|
|
|
|
import {
|
|
|
|
AnyDisplay,
|
|
|
|
Node,
|
2021-01-20 06:42:45 +00:00
|
|
|
InputType,
|
2021-01-26 17:44:14 +00:00
|
|
|
RefreshResult,
|
|
|
|
SelfServeDescriptor,
|
|
|
|
SelfServeNotification,
|
|
|
|
SmartUiInput,
|
|
|
|
DescriptionDisplay,
|
|
|
|
StringInput,
|
|
|
|
NumberInput,
|
|
|
|
BooleanInput,
|
|
|
|
ChoiceInput,
|
|
|
|
SelfServeNotificationType,
|
|
|
|
} from "./SelfServeTypes";
|
|
|
|
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
|
|
|
import { getMessageBarType } from "./SelfServeUtils";
|
2021-01-20 06:42:45 +00:00
|
|
|
|
|
|
|
export interface SelfServeComponentProps {
|
|
|
|
descriptor: SelfServeDescriptor;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface SelfServeComponentState {
|
|
|
|
root: SelfServeDescriptor;
|
2021-01-26 17:44:14 +00:00
|
|
|
currentValues: Map<string, SmartUiInput>;
|
|
|
|
baselineValues: Map<string, SmartUiInput>;
|
|
|
|
isInitializing: boolean;
|
|
|
|
hasErrors: boolean;
|
|
|
|
compileErrorMessage: string;
|
|
|
|
notification: SelfServeNotification;
|
|
|
|
refreshResult: RefreshResult;
|
2021-01-20 06:42:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
|
|
|
componentDidMount(): void {
|
2021-01-26 17:44:14 +00:00
|
|
|
this.performRefresh();
|
2021-01-20 06:42:45 +00:00
|
|
|
this.initializeSmartUiComponent();
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props: SelfServeComponentProps) {
|
|
|
|
super(props);
|
|
|
|
this.state = {
|
|
|
|
root: this.props.descriptor,
|
|
|
|
currentValues: new Map(),
|
|
|
|
baselineValues: new Map(),
|
2021-01-26 17:44:14 +00:00
|
|
|
isInitializing: true,
|
|
|
|
hasErrors: false,
|
|
|
|
compileErrorMessage: undefined,
|
|
|
|
notification: undefined,
|
|
|
|
refreshResult: undefined,
|
2021-01-20 06:42:45 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-01-26 17:44:14 +00:00
|
|
|
private onError = (hasErrors: boolean): void => {
|
|
|
|
this.setState({ hasErrors });
|
|
|
|
};
|
|
|
|
|
2021-01-20 06:42:45 +00:00
|
|
|
private initializeSmartUiComponent = async (): Promise<void> => {
|
2021-01-26 17:44:14 +00:00
|
|
|
this.setState({ isInitializing: true });
|
2021-01-20 06:42:45 +00:00
|
|
|
await this.setDefaults();
|
2021-01-26 17:44:14 +00:00
|
|
|
const { currentValues, baselineValues } = this.state;
|
|
|
|
await this.initializeSmartUiNode(this.props.descriptor.root, currentValues, baselineValues);
|
|
|
|
this.setState({ isInitializing: false, currentValues, baselineValues });
|
2021-01-20 06:42:45 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
private setDefaults = async (): Promise<void> => {
|
|
|
|
let { currentValues, baselineValues } = this.state;
|
|
|
|
|
|
|
|
const initialValues = await this.props.descriptor.initialize();
|
2021-01-26 17:44:14 +00:00
|
|
|
this.props.descriptor.inputNames.map((inputName) => {
|
|
|
|
let initialValue = initialValues.get(inputName);
|
|
|
|
if (!initialValue) {
|
|
|
|
initialValue = { value: undefined, hidden: false };
|
|
|
|
}
|
|
|
|
currentValues = currentValues.set(inputName, initialValue);
|
|
|
|
baselineValues = baselineValues.set(inputName, initialValue);
|
|
|
|
initialValues.delete(inputName);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (initialValues.size > 0) {
|
|
|
|
const keys = [];
|
|
|
|
for (const key of initialValues.keys()) {
|
|
|
|
keys.push(key);
|
2021-01-20 06:42:45 +00:00
|
|
|
}
|
|
|
|
|
2021-01-26 17:44:14 +00:00
|
|
|
this.setState({
|
|
|
|
compileErrorMessage: `The following fields have default values set but are not input properties of this class: ${keys.join(
|
|
|
|
", "
|
|
|
|
)}`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.setState({ currentValues, baselineValues });
|
|
|
|
};
|
|
|
|
|
|
|
|
public resetBaselineValues = (): void => {
|
|
|
|
const currentValues = this.state.currentValues;
|
|
|
|
let baselineValues = this.state.baselineValues;
|
|
|
|
for (const key of currentValues.keys()) {
|
|
|
|
const currentValue = currentValues.get(key);
|
|
|
|
baselineValues = baselineValues.set(key, { ...currentValue });
|
2021-01-20 06:42:45 +00:00
|
|
|
}
|
2021-01-26 17:44:14 +00:00
|
|
|
this.setState({ baselineValues });
|
2021-01-20 06:42:45 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
public discard = (): void => {
|
|
|
|
let { currentValues } = this.state;
|
|
|
|
const { baselineValues } = this.state;
|
2021-01-26 17:44:14 +00:00
|
|
|
for (const key of currentValues.keys()) {
|
|
|
|
const baselineValue = baselineValues.get(key);
|
|
|
|
currentValues = currentValues.set(key, { ...baselineValue });
|
2021-01-20 06:42:45 +00:00
|
|
|
}
|
|
|
|
this.setState({ currentValues });
|
|
|
|
};
|
|
|
|
|
2021-01-26 17:44:14 +00:00
|
|
|
private initializeSmartUiNode = async (
|
|
|
|
currentNode: Node,
|
|
|
|
currentValues: Map<string, SmartUiInput>,
|
|
|
|
baselineValues: Map<string, SmartUiInput>
|
|
|
|
): Promise<void> => {
|
2021-01-20 06:42:45 +00:00
|
|
|
currentNode.info = await this.getResolvedValue(currentNode.info);
|
|
|
|
|
|
|
|
if (currentNode.input) {
|
2021-01-26 17:44:14 +00:00
|
|
|
currentNode.input = await this.getResolvedInput(currentNode.input, currentValues, baselineValues);
|
2021-01-20 06:42:45 +00:00
|
|
|
}
|
|
|
|
|
2021-01-26 17:44:14 +00:00
|
|
|
const promises = currentNode.children?.map(
|
|
|
|
async (child: Node) => await this.initializeSmartUiNode(child, currentValues, baselineValues)
|
|
|
|
);
|
2021-01-20 06:42:45 +00:00
|
|
|
if (promises) {
|
|
|
|
await Promise.all(promises);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-01-26 17:44:14 +00:00
|
|
|
private getResolvedInput = async (
|
|
|
|
input: AnyDisplay,
|
|
|
|
currentValues: Map<string, SmartUiInput>,
|
|
|
|
baselineValues: Map<string, SmartUiInput>
|
|
|
|
): Promise<AnyDisplay> => {
|
2021-01-20 06:42:45 +00:00
|
|
|
input.label = await this.getResolvedValue(input.label);
|
|
|
|
input.placeholder = await this.getResolvedValue(input.placeholder);
|
|
|
|
|
|
|
|
switch (input.type) {
|
|
|
|
case "string": {
|
2021-01-26 17:44:14 +00:00
|
|
|
if ("description" in input) {
|
|
|
|
const descriptionDisplay = input as DescriptionDisplay;
|
|
|
|
descriptionDisplay.description = await this.getResolvedValue(descriptionDisplay.description);
|
|
|
|
}
|
2021-01-20 06:42:45 +00:00
|
|
|
return input as StringInput;
|
|
|
|
}
|
|
|
|
case "number": {
|
|
|
|
const numberInput = input as NumberInput;
|
|
|
|
numberInput.min = await this.getResolvedValue(numberInput.min);
|
|
|
|
numberInput.max = await this.getResolvedValue(numberInput.max);
|
|
|
|
numberInput.step = await this.getResolvedValue(numberInput.step);
|
2021-01-26 17:44:14 +00:00
|
|
|
|
|
|
|
const dataFieldName = numberInput.dataFieldName;
|
|
|
|
const defaultValue = currentValues.get(dataFieldName)?.value;
|
|
|
|
|
|
|
|
if (!defaultValue) {
|
|
|
|
const newDefaultValue = { value: numberInput.min, hidden: currentValues.get(dataFieldName)?.hidden };
|
|
|
|
currentValues.set(dataFieldName, newDefaultValue);
|
|
|
|
baselineValues.set(dataFieldName, newDefaultValue);
|
|
|
|
}
|
|
|
|
|
2021-01-20 06:42:45 +00:00
|
|
|
return numberInput;
|
|
|
|
}
|
|
|
|
case "boolean": {
|
|
|
|
const booleanInput = input as BooleanInput;
|
|
|
|
booleanInput.trueLabel = await this.getResolvedValue(booleanInput.trueLabel);
|
|
|
|
booleanInput.falseLabel = await this.getResolvedValue(booleanInput.falseLabel);
|
|
|
|
return booleanInput;
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
const choiceInput = input as ChoiceInput;
|
|
|
|
choiceInput.choices = await this.getResolvedValue(choiceInput.choices);
|
|
|
|
return choiceInput;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
public async getResolvedValue<T>(value: T | (() => Promise<T>)): Promise<T> {
|
|
|
|
if (value instanceof Function) {
|
|
|
|
return value();
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2021-01-26 17:44:14 +00:00
|
|
|
private onInputChange = (input: AnyDisplay, newValue: InputType) => {
|
2021-01-20 06:42:45 +00:00
|
|
|
if (input.onChange) {
|
|
|
|
const newValues = input.onChange(this.state.currentValues, newValue);
|
|
|
|
this.setState({ currentValues: newValues });
|
|
|
|
} else {
|
|
|
|
const dataFieldName = input.dataFieldName;
|
|
|
|
const { currentValues } = this.state;
|
2021-01-26 17:44:14 +00:00
|
|
|
const currentInputValue = currentValues.get(dataFieldName);
|
|
|
|
currentValues.set(dataFieldName, { value: newValue, hidden: currentInputValue?.hidden });
|
2021-01-20 06:42:45 +00:00
|
|
|
this.setState({ currentValues });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-01-26 17:44:14 +00:00
|
|
|
public onSaveButtonClick = (): void => {
|
|
|
|
const onSavePromise = this.props.descriptor.onSave(this.state.currentValues);
|
|
|
|
onSavePromise.catch((error) => {
|
|
|
|
this.setState({
|
|
|
|
notification: {
|
|
|
|
message: `Error: ${error.message}`,
|
|
|
|
type: SelfServeNotificationType.error,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
onSavePromise.then((notification: SelfServeNotification) => {
|
|
|
|
this.setState({
|
|
|
|
notification: {
|
|
|
|
message: notification.message,
|
|
|
|
type: notification.type,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
this.resetBaselineValues();
|
|
|
|
this.onRefreshClicked();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
public isDiscardButtonDisabled = (): boolean => {
|
|
|
|
for (const key of this.state.currentValues.keys()) {
|
|
|
|
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
|
|
|
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
|
|
|
|
|
|
|
if (currentValue !== baselineValue) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
public isSaveButtonDisabled = (): boolean => {
|
|
|
|
if (this.state.hasErrors) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
for (const key of this.state.currentValues.keys()) {
|
|
|
|
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
|
|
|
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
|
|
|
|
|
|
|
if (currentValue !== baselineValue) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
private performRefresh = async (): Promise<RefreshResult> => {
|
|
|
|
const refreshResult = await this.props.descriptor.onRefresh();
|
|
|
|
this.setState({ refreshResult: { ...refreshResult } });
|
|
|
|
return refreshResult;
|
|
|
|
};
|
|
|
|
|
|
|
|
public onRefreshClicked = async (): Promise<void> => {
|
|
|
|
this.setState({ isInitializing: true });
|
|
|
|
const refreshResult = await this.performRefresh();
|
|
|
|
if (!refreshResult.isUpdateInProgress) {
|
|
|
|
this.initializeSmartUiComponent();
|
|
|
|
}
|
|
|
|
this.setState({ isInitializing: false });
|
|
|
|
};
|
|
|
|
|
|
|
|
private getCommandBarItems = (): ICommandBarItemProps[] => {
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
key: "save",
|
|
|
|
text: "Save",
|
|
|
|
iconProps: { iconName: "Save" },
|
|
|
|
split: true,
|
|
|
|
disabled: this.isSaveButtonDisabled(),
|
|
|
|
onClick: this.onSaveButtonClick,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "discard",
|
|
|
|
text: "Discard",
|
|
|
|
iconProps: { iconName: "Undo" },
|
|
|
|
split: true,
|
|
|
|
disabled: this.isDiscardButtonDisabled(),
|
|
|
|
onClick: () => {
|
|
|
|
this.discard();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
key: "refresh",
|
|
|
|
text: "Refresh",
|
|
|
|
disabled: this.state.isInitializing,
|
|
|
|
iconProps: { iconName: "Refresh" },
|
|
|
|
split: true,
|
|
|
|
onClick: () => {
|
|
|
|
this.onRefreshClicked();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
};
|
|
|
|
|
2021-01-20 06:42:45 +00:00
|
|
|
public render(): JSX.Element {
|
2021-01-26 17:44:14 +00:00
|
|
|
const containerStackTokens: IStackTokens = { childrenGap: 5 };
|
|
|
|
if (this.state.compileErrorMessage) {
|
|
|
|
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>;
|
|
|
|
}
|
|
|
|
return (
|
2021-01-20 06:42:45 +00:00
|
|
|
<div style={{ overflowX: "auto" }}>
|
2021-01-26 17:44:14 +00:00
|
|
|
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
|
|
|
|
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
|
|
|
|
{this.state.isInitializing ? (
|
|
|
|
<Spinner
|
|
|
|
size={SpinnerSize.large}
|
|
|
|
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
|
2021-01-20 06:42:45 +00:00
|
|
|
/>
|
2021-01-26 17:44:14 +00:00
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
{this.state.refreshResult?.isUpdateInProgress && (
|
|
|
|
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
|
|
|
|
{this.state.refreshResult.notificationMessage}
|
|
|
|
</MessageBar>
|
|
|
|
)}
|
|
|
|
{this.state.notification && (
|
|
|
|
<MessageBar
|
|
|
|
messageBarType={getMessageBarType(this.state.notification.type)}
|
|
|
|
styles={{ root: { width: 400 } }}
|
|
|
|
onDismiss={() => this.setState({ notification: undefined })}
|
|
|
|
>
|
|
|
|
{this.state.notification.message}
|
|
|
|
</MessageBar>
|
|
|
|
)}
|
|
|
|
<SmartUiComponent
|
|
|
|
disabled={this.state.refreshResult?.isUpdateInProgress}
|
|
|
|
descriptor={this.state.root as SmartUiDescriptor}
|
|
|
|
currentValues={this.state.currentValues}
|
|
|
|
onInputChange={this.onInputChange}
|
|
|
|
onError={this.onError}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
)}
|
2021-01-20 06:42:45 +00:00
|
|
|
</Stack>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|