2020-05-26 03:30:55 +01:00
|
|
|
import * as ko from "knockout";
|
|
|
|
import Q from "q";
|
2020-09-15 19:31:30 +01:00
|
|
|
import DiscardIcon from "../../../images/discard.svg";
|
|
|
|
import SaveIcon from "../../../images/save-cosmos.svg";
|
|
|
|
import * as Constants from "../../Common/Constants";
|
|
|
|
import editable from "../../Common/EditableUtility";
|
2020-05-26 03:30:55 +01:00
|
|
|
import * as DataModels from "../../Contracts/DataModels";
|
|
|
|
import * as ViewModels from "../../Contracts/ViewModels";
|
2020-07-27 22:05:25 +01:00
|
|
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
2021-04-26 05:31:10 +01:00
|
|
|
import { loadMonaco, monaco } from "../LazyMonaco";
|
2020-09-15 19:31:30 +01:00
|
|
|
import TabsBase from "./TabsBase";
|
2020-05-26 03:30:55 +01:00
|
|
|
|
2020-07-27 22:05:25 +01:00
|
|
|
export default abstract class ScriptTabBase extends TabsBase implements ViewModels.WaitsForTemplate {
|
2020-05-26 03:30:55 +01:00
|
|
|
public ariaLabel: ko.Observable<string>;
|
|
|
|
public editorState: ko.Observable<ViewModels.ScriptEditorState>;
|
|
|
|
public id: ViewModels.Editable<string>;
|
|
|
|
public editorContent: ViewModels.Editable<string>;
|
|
|
|
public editorId: string;
|
|
|
|
public editor: ko.Observable<monaco.editor.IStandaloneCodeEditor>;
|
|
|
|
public executeButton: ViewModels.Button;
|
|
|
|
public saveButton: ViewModels.Button;
|
|
|
|
public updateButton: ViewModels.Button;
|
|
|
|
public discardButton: ViewModels.Button;
|
|
|
|
public deleteButton: ViewModels.Button;
|
|
|
|
public errors: ko.ObservableArray<ViewModels.QueryError>;
|
|
|
|
public statusMessge: ko.Observable<string>;
|
|
|
|
public statusIcon: ko.Observable<string>;
|
|
|
|
public formFields: ko.ObservableArray<ViewModels.Editable<any>>;
|
|
|
|
public formIsValid: ko.Computed<boolean>;
|
|
|
|
public formIsDirty: ko.Computed<boolean>;
|
|
|
|
public isNew: ko.Observable<boolean>;
|
2020-09-15 19:31:30 +01:00
|
|
|
// TODO: Remove any. The SDK types for all the script.body are slightly incorrect which makes this REALLY hard to type correct.
|
|
|
|
public resource: ko.Observable<any>;
|
2020-05-26 03:30:55 +01:00
|
|
|
public isTemplateReady: ko.Observable<boolean>;
|
|
|
|
protected _partitionKey: DataModels.PartitionKey;
|
|
|
|
|
|
|
|
constructor(options: ViewModels.ScriptTabOption) {
|
|
|
|
super(options);
|
|
|
|
this._partitionKey = options.partitionKey;
|
|
|
|
this.isNew = ko.observable(options.isNew);
|
|
|
|
this.resource = ko.observable(options.resource);
|
|
|
|
this.isTemplateReady = ko.observable<boolean>(false);
|
|
|
|
this.isTemplateReady.subscribe((isTemplateReady: boolean) => {
|
|
|
|
if (isTemplateReady) {
|
|
|
|
// setTimeout is needed as creating the edtior manipulates the dom directly and expects
|
|
|
|
// Knockout to have completed all of the initial bindings for the component
|
|
|
|
setTimeout(() => this._createBodyEditor(), Constants.ClientDefaults.waitForDOMElementMs);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.editorId = `editor_${this.tabId}`;
|
|
|
|
this.ariaLabel = ko.observable<string>();
|
|
|
|
if (this.isNew()) {
|
|
|
|
this.editorState = ko.observable(ViewModels.ScriptEditorState.newInvalid);
|
|
|
|
} else {
|
|
|
|
this.editorState = ko.observable(ViewModels.ScriptEditorState.exisitingNoEdits);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.id = editable.observable<string>();
|
|
|
|
this.id.validations([ScriptTabBase._isValidId]);
|
|
|
|
|
|
|
|
this.editorContent = editable.observable<string>();
|
|
|
|
this.editorContent.validations([ScriptTabBase._isNotEmpty]);
|
|
|
|
|
|
|
|
this.formFields = ko.observableArray([this.id, this.editorContent]);
|
|
|
|
|
|
|
|
this._setBaselines();
|
|
|
|
|
2021-01-20 15:15:01 +00:00
|
|
|
this.id.editableIsValid.subscribe((isValid) => {
|
2020-05-26 03:30:55 +01:00
|
|
|
const currentState = this.editorState();
|
|
|
|
switch (currentState) {
|
|
|
|
case ViewModels.ScriptEditorState.newValid:
|
|
|
|
case ViewModels.ScriptEditorState.newInvalid:
|
|
|
|
if (isValid) {
|
|
|
|
this.editorState(ViewModels.ScriptEditorState.newValid);
|
|
|
|
} else {
|
|
|
|
this.editorState(ViewModels.ScriptEditorState.newInvalid);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case ViewModels.ScriptEditorState.exisitingDirtyInvalid:
|
|
|
|
case ViewModels.ScriptEditorState.exisitingDirtyValid:
|
|
|
|
if (isValid) {
|
|
|
|
this.editorState(ViewModels.ScriptEditorState.exisitingDirtyValid);
|
|
|
|
} else {
|
|
|
|
this.editorState(ViewModels.ScriptEditorState.exisitingDirtyInvalid);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case ViewModels.ScriptEditorState.exisitingDirtyValid:
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.editor = ko.observable<monaco.editor.IStandaloneCodeEditor>();
|
|
|
|
|
|
|
|
this.formIsValid = ko.computed<boolean>(() => {
|
2021-01-20 15:15:01 +00:00
|
|
|
const formIsValid: boolean = this.formFields().every((field) => {
|
2020-05-26 03:30:55 +01:00
|
|
|
return field.editableIsValid();
|
|
|
|
});
|
|
|
|
|
|
|
|
return formIsValid;
|
|
|
|
});
|
|
|
|
|
|
|
|
this.formIsDirty = ko.computed<boolean>(() => {
|
2021-01-20 15:15:01 +00:00
|
|
|
const formIsDirty: boolean = this.formFields().some((field) => {
|
2020-05-26 03:30:55 +01:00
|
|
|
return field.editableIsDirty();
|
|
|
|
});
|
|
|
|
|
|
|
|
return formIsDirty;
|
|
|
|
});
|
|
|
|
|
|
|
|
this.saveButton = {
|
|
|
|
enabled: ko.computed<boolean>(() => {
|
|
|
|
if (!this.formIsValid()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.formIsDirty()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}),
|
|
|
|
|
|
|
|
visible: ko.computed<boolean>(() => {
|
|
|
|
return this.isNew();
|
2021-01-20 15:15:01 +00:00
|
|
|
}),
|
2020-05-26 03:30:55 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
this.updateButton = {
|
|
|
|
enabled: ko.computed<boolean>(() => {
|
|
|
|
if (!this.formIsValid()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.formIsDirty()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}),
|
|
|
|
|
|
|
|
visible: ko.computed<boolean>(() => {
|
|
|
|
return !this.isNew();
|
2021-01-20 15:15:01 +00:00
|
|
|
}),
|
2020-05-26 03:30:55 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
this.discardButton = {
|
|
|
|
enabled: ko.computed<boolean>(() => {
|
|
|
|
return this.formIsDirty();
|
|
|
|
}),
|
|
|
|
|
|
|
|
visible: ko.computed<boolean>(() => {
|
|
|
|
return true;
|
2021-01-20 15:15:01 +00:00
|
|
|
}),
|
2020-05-26 03:30:55 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
this.deleteButton = {
|
|
|
|
enabled: ko.computed<boolean>(() => {
|
|
|
|
return !this.isNew();
|
|
|
|
}),
|
|
|
|
|
|
|
|
visible: ko.computed<boolean>(() => {
|
|
|
|
return true;
|
2021-01-20 15:15:01 +00:00
|
|
|
}),
|
2020-05-26 03:30:55 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
this.executeButton = {
|
|
|
|
enabled: ko.computed<boolean>(() => {
|
|
|
|
return !this.isNew() && !this.formIsDirty() && this.formIsValid();
|
|
|
|
}),
|
|
|
|
|
|
|
|
visible: ko.computed<boolean>(() => {
|
|
|
|
return true;
|
2021-01-20 15:15:01 +00:00
|
|
|
}),
|
2020-05-26 03:30:55 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private _setBaselines() {
|
|
|
|
const resource = this.resource();
|
|
|
|
this.id.setBaseline(resource.id);
|
|
|
|
this.editorContent.setBaseline(resource.body);
|
|
|
|
}
|
|
|
|
|
|
|
|
public setBaselines() {
|
|
|
|
this._setBaselines();
|
|
|
|
}
|
|
|
|
|
2020-12-16 23:27:17 +00:00
|
|
|
public onTabClick(): void {
|
|
|
|
super.onTabClick();
|
|
|
|
if (this.isNew()) {
|
|
|
|
this.collection.selectedSubnodeKind(this.tabKind);
|
|
|
|
}
|
2020-05-26 03:30:55 +01:00
|
|
|
}
|
|
|
|
|
2021-05-21 02:34:29 +01:00
|
|
|
public abstract onSaveClick: () => void;
|
2020-09-15 19:31:30 +01:00
|
|
|
public abstract onUpdateClick: () => Promise<any>;
|
2020-05-26 03:30:55 +01:00
|
|
|
|
|
|
|
public onDiscard = (): Q.Promise<any> => {
|
|
|
|
this.setBaselines();
|
|
|
|
const original = this.editorContent.getEditableOriginalValue();
|
|
|
|
const editorModel = this.editor() && this.editor().getModel();
|
|
|
|
editorModel && editorModel.setValue(original);
|
|
|
|
|
|
|
|
return Q();
|
|
|
|
};
|
|
|
|
|
2020-07-27 22:05:25 +01:00
|
|
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
|
|
|
const buttons: CommandButtonComponentProps[] = [];
|
2020-05-26 03:30:55 +01:00
|
|
|
const label = "Save";
|
|
|
|
if (this.saveButton.visible()) {
|
|
|
|
buttons.push({
|
|
|
|
iconSrc: SaveIcon,
|
|
|
|
iconAlt: label,
|
|
|
|
onCommandClick: this.onSaveClick,
|
|
|
|
commandButtonLabel: label,
|
|
|
|
ariaLabel: label,
|
|
|
|
hasPopup: false,
|
2021-01-20 15:15:01 +00:00
|
|
|
disabled: !this.saveButton.enabled(),
|
2020-05-26 03:30:55 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.updateButton.visible()) {
|
|
|
|
const label = "Update";
|
|
|
|
buttons.push({
|
|
|
|
iconSrc: SaveIcon,
|
|
|
|
iconAlt: label,
|
|
|
|
onCommandClick: this.onUpdateClick,
|
|
|
|
commandButtonLabel: label,
|
|
|
|
ariaLabel: label,
|
|
|
|
hasPopup: false,
|
2021-01-20 15:15:01 +00:00
|
|
|
disabled: !this.updateButton.enabled(),
|
2020-05-26 03:30:55 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.discardButton.visible()) {
|
|
|
|
const label = "Discard";
|
|
|
|
buttons.push({
|
|
|
|
iconSrc: DiscardIcon,
|
|
|
|
iconAlt: label,
|
|
|
|
onCommandClick: this.onDiscard,
|
|
|
|
commandButtonLabel: label,
|
|
|
|
ariaLabel: label,
|
|
|
|
hasPopup: false,
|
2021-01-20 15:15:01 +00:00
|
|
|
disabled: !this.discardButton.enabled(),
|
2020-05-26 03:30:55 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return buttons;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected buildCommandBarOptions(): void {
|
|
|
|
ko.computed(() =>
|
|
|
|
ko.toJSON([
|
|
|
|
this.saveButton.visible,
|
|
|
|
this.saveButton.enabled,
|
|
|
|
this.updateButton.visible,
|
|
|
|
this.updateButton.enabled,
|
|
|
|
this.discardButton.visible,
|
2021-01-20 15:15:01 +00:00
|
|
|
this.discardButton.enabled,
|
2020-05-26 03:30:55 +01:00
|
|
|
])
|
|
|
|
).subscribe(() => this.updateNavbarWithTabsButtons());
|
|
|
|
this.updateNavbarWithTabsButtons();
|
|
|
|
}
|
|
|
|
|
|
|
|
private static _isValidId(id: string): boolean {
|
|
|
|
if (!id) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const invalidStartCharacters = /^[/?#\\]/;
|
|
|
|
if (invalidStartCharacters.test(id)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const invalidMiddleCharacters = /^.+[/?#\\]/;
|
|
|
|
if (invalidMiddleCharacters.test(id)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const invalidEndCharacters = /.*[/?#\\ ]$/;
|
|
|
|
if (invalidEndCharacters.test(id)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static _isNotEmpty(value: string): boolean {
|
|
|
|
return !!value;
|
|
|
|
}
|
|
|
|
|
2021-04-26 05:31:10 +01:00
|
|
|
protected async _createBodyEditor() {
|
2020-05-26 03:30:55 +01:00
|
|
|
const id = this.editorId;
|
|
|
|
const container = document.getElementById(id);
|
|
|
|
const options = {
|
|
|
|
value: this.editorContent(),
|
|
|
|
language: "javascript",
|
|
|
|
readOnly: false,
|
2021-01-20 15:15:01 +00:00
|
|
|
ariaLabel: this.ariaLabel(),
|
2020-05-26 03:30:55 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
container.innerHTML = "";
|
2021-04-26 05:31:10 +01:00
|
|
|
const monaco = await loadMonaco();
|
2020-05-26 03:30:55 +01:00
|
|
|
const editor = monaco.editor.create(container, options);
|
|
|
|
this.editor(editor);
|
|
|
|
|
|
|
|
const editorModel = editor.getModel();
|
|
|
|
editorModel.onDidChangeContent(this._onBodyContentChange.bind(this));
|
|
|
|
}
|
|
|
|
|
|
|
|
private _onBodyContentChange(e: monaco.editor.IModelContentChangedEvent) {
|
|
|
|
const editorModel = this.editor().getModel();
|
|
|
|
this.editorContent(editorModel.getValue());
|
|
|
|
}
|
|
|
|
}
|