2021-01-20 06:42:45 +00:00
|
|
|
import React from "react";
|
|
|
|
import { IStackTokens, PrimaryButton, Spinner, SpinnerSize, Stack } from "office-ui-fabric-react";
|
|
|
|
import {
|
|
|
|
ChoiceItem,
|
|
|
|
InputType,
|
|
|
|
InputTypeValue,
|
|
|
|
SmartUiComponent,
|
|
|
|
UiType,
|
|
|
|
SmartUiDescriptor,
|
2021-01-20 15:15:01 +00:00
|
|
|
Info,
|
2021-01-20 06:42:45 +00:00
|
|
|
} from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
|
|
|
|
|
|
|
export interface BaseInput {
|
|
|
|
label: (() => Promise<string>) | string;
|
|
|
|
dataFieldName: string;
|
|
|
|
type: InputTypeValue;
|
|
|
|
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
|
|
|
|
placeholder?: (() => Promise<string>) | string;
|
|
|
|
errorMessage?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface NumberInput extends BaseInput {
|
|
|
|
min: (() => Promise<number>) | number;
|
|
|
|
max: (() => Promise<number>) | number;
|
|
|
|
step: (() => Promise<number>) | number;
|
|
|
|
defaultValue?: number;
|
|
|
|
uiType: UiType;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface BooleanInput extends BaseInput {
|
|
|
|
trueLabel: (() => Promise<string>) | string;
|
|
|
|
falseLabel: (() => Promise<string>) | string;
|
|
|
|
defaultValue?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface StringInput extends BaseInput {
|
|
|
|
defaultValue?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ChoiceInput extends BaseInput {
|
|
|
|
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
|
|
|
defaultKey?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Node {
|
|
|
|
id: string;
|
|
|
|
info?: (() => Promise<Info>) | Info;
|
|
|
|
input?: AnyInput;
|
|
|
|
children?: Node[];
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface SelfServeDescriptor {
|
|
|
|
root: Node;
|
|
|
|
initialize?: () => Promise<Map<string, InputType>>;
|
|
|
|
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
|
|
|
|
inputNames?: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
export type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
|
|
|
|
|
|
|
|
export interface SelfServeComponentProps {
|
|
|
|
descriptor: SelfServeDescriptor;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface SelfServeComponentState {
|
|
|
|
root: SelfServeDescriptor;
|
|
|
|
currentValues: Map<string, InputType>;
|
|
|
|
baselineValues: Map<string, InputType>;
|
|
|
|
isRefreshing: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
|
|
|
componentDidMount(): void {
|
|
|
|
this.initializeSmartUiComponent();
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(props: SelfServeComponentProps) {
|
|
|
|
super(props);
|
|
|
|
this.state = {
|
|
|
|
root: this.props.descriptor,
|
|
|
|
currentValues: new Map(),
|
|
|
|
baselineValues: new Map(),
|
2021-01-20 15:15:01 +00:00
|
|
|
isRefreshing: false,
|
2021-01-20 06:42:45 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private initializeSmartUiComponent = async (): Promise<void> => {
|
|
|
|
this.setState({ isRefreshing: true });
|
|
|
|
await this.initializeSmartUiNode(this.props.descriptor.root);
|
|
|
|
await this.setDefaults();
|
|
|
|
this.setState({ isRefreshing: false });
|
|
|
|
};
|
|
|
|
|
|
|
|
private setDefaults = async (): Promise<void> => {
|
|
|
|
this.setState({ isRefreshing: true });
|
|
|
|
let { currentValues, baselineValues } = this.state;
|
|
|
|
|
|
|
|
const initialValues = await this.props.descriptor.initialize();
|
|
|
|
for (const key of initialValues.keys()) {
|
|
|
|
if (this.props.descriptor.inputNames.indexOf(key) === -1) {
|
|
|
|
this.setState({ isRefreshing: false });
|
|
|
|
throw new Error(`${key} is not an input property of this class.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
currentValues = currentValues.set(key, initialValues.get(key));
|
|
|
|
baselineValues = baselineValues.set(key, initialValues.get(key));
|
|
|
|
}
|
|
|
|
this.setState({ currentValues, baselineValues, isRefreshing: false });
|
|
|
|
};
|
|
|
|
|
|
|
|
public discard = (): void => {
|
|
|
|
let { currentValues } = this.state;
|
|
|
|
const { baselineValues } = this.state;
|
|
|
|
for (const key of baselineValues.keys()) {
|
|
|
|
currentValues = currentValues.set(key, baselineValues.get(key));
|
|
|
|
}
|
|
|
|
this.setState({ currentValues });
|
|
|
|
};
|
|
|
|
|
|
|
|
private initializeSmartUiNode = async (currentNode: Node): Promise<void> => {
|
|
|
|
currentNode.info = await this.getResolvedValue(currentNode.info);
|
|
|
|
|
|
|
|
if (currentNode.input) {
|
|
|
|
currentNode.input = await this.getResolvedInput(currentNode.input);
|
|
|
|
}
|
|
|
|
|
|
|
|
const promises = currentNode.children?.map(async (child: Node) => await this.initializeSmartUiNode(child));
|
|
|
|
if (promises) {
|
|
|
|
await Promise.all(promises);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private getResolvedInput = async (input: AnyInput): Promise<AnyInput> => {
|
|
|
|
input.label = await this.getResolvedValue(input.label);
|
|
|
|
input.placeholder = await this.getResolvedValue(input.placeholder);
|
|
|
|
|
|
|
|
switch (input.type) {
|
|
|
|
case "string": {
|
|
|
|
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);
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
private onInputChange = (input: AnyInput, newValue: InputType) => {
|
|
|
|
if (input.onChange) {
|
|
|
|
const newValues = input.onChange(this.state.currentValues, newValue);
|
|
|
|
this.setState({ currentValues: newValues });
|
|
|
|
} else {
|
|
|
|
const dataFieldName = input.dataFieldName;
|
|
|
|
const { currentValues } = this.state;
|
|
|
|
currentValues.set(dataFieldName, newValue);
|
|
|
|
this.setState({ currentValues });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
public render(): JSX.Element {
|
|
|
|
const containerStackTokens: IStackTokens = { childrenGap: 20 };
|
|
|
|
return !this.state.isRefreshing ? (
|
|
|
|
<div style={{ overflowX: "auto" }}>
|
|
|
|
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
|
|
|
|
<SmartUiComponent
|
|
|
|
descriptor={this.state.root as SmartUiDescriptor}
|
|
|
|
currentValues={this.state.currentValues}
|
|
|
|
onInputChange={this.onInputChange}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
|
|
|
<PrimaryButton
|
|
|
|
id="submitButton"
|
|
|
|
styles={{ root: { width: 100 } }}
|
|
|
|
text="submit"
|
|
|
|
onClick={async () => {
|
|
|
|
await this.props.descriptor.onSubmit(this.state.currentValues);
|
|
|
|
this.setDefaults();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<PrimaryButton
|
|
|
|
id="discardButton"
|
|
|
|
styles={{ root: { width: 100 } }}
|
|
|
|
text="discard"
|
|
|
|
onClick={() => this.discard()}
|
|
|
|
/>
|
|
|
|
</Stack>
|
|
|
|
</Stack>
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
<Spinner
|
|
|
|
size={SpinnerSize.large}
|
|
|
|
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|