mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 01:11:25 +00:00
Initial Move from Azure DevOps to GitHub
This commit is contained in:
385
src/Explorer/Tabs/ScriptTabBase.ts
Normal file
385
src/Explorer/Tabs/ScriptTabBase.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import TabsBase from "./TabsBase";
|
||||
import editable from "../../Common/EditableUtility";
|
||||
import * as monaco from "monaco-editor";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
|
||||
export default abstract class ScriptTabBase extends TabsBase
|
||||
implements ViewModels.ScriptTab, ViewModels.WaitsForTemplate {
|
||||
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>;
|
||||
public resource: ko.Observable<DataModels.Script>;
|
||||
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();
|
||||
|
||||
this.id.editableIsValid.subscribe(isValid => {
|
||||
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>(() => {
|
||||
const formIsValid: boolean = this.formFields().every(field => {
|
||||
return field.editableIsValid();
|
||||
});
|
||||
|
||||
return formIsValid;
|
||||
});
|
||||
|
||||
this.formIsDirty = ko.computed<boolean>(() => {
|
||||
const formIsDirty: boolean = this.formFields().some(field => {
|
||||
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();
|
||||
})
|
||||
};
|
||||
|
||||
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();
|
||||
})
|
||||
};
|
||||
|
||||
this.discardButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
return this.formIsDirty();
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
})
|
||||
};
|
||||
|
||||
this.deleteButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
return !this.isNew();
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
})
|
||||
};
|
||||
|
||||
this.executeButton = {
|
||||
enabled: ko.computed<boolean>(() => {
|
||||
return !this.isNew() && !this.formIsDirty() && this.formIsValid();
|
||||
}),
|
||||
|
||||
visible: ko.computed<boolean>(() => {
|
||||
return true;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private _setBaselines() {
|
||||
const resource = this.resource();
|
||||
this.id.setBaseline(resource.id);
|
||||
this.editorContent.setBaseline(resource.body);
|
||||
}
|
||||
|
||||
public setBaselines() {
|
||||
this._setBaselines();
|
||||
}
|
||||
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
if (this.isNew()) {
|
||||
this.collection.selectedSubnodeKind(this.tabKind);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public abstract onSaveClick: () => Q.Promise<any>;
|
||||
public abstract onUpdateClick: () => Q.Promise<any>;
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
public onSaveOrUpdateClick(): Q.Promise<any> {
|
||||
if (this.saveButton.visible()) {
|
||||
return this.onSaveClick();
|
||||
} else if (this.updateButton.visible()) {
|
||||
return this.onUpdateClick();
|
||||
}
|
||||
|
||||
return Q();
|
||||
}
|
||||
|
||||
protected getTabsButtons(): ViewModels.NavbarButtonConfig[] {
|
||||
const buttons: ViewModels.NavbarButtonConfig[] = [];
|
||||
const label = "Save";
|
||||
if (this.saveButton.visible()) {
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onSaveClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveButton.enabled()
|
||||
});
|
||||
}
|
||||
|
||||
if (this.updateButton.visible()) {
|
||||
const label = "Update";
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onUpdateClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.updateButton.enabled()
|
||||
});
|
||||
}
|
||||
|
||||
if (this.discardButton.visible()) {
|
||||
const label = "Discard";
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onDiscard,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.discardButton.enabled()
|
||||
});
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
|
||||
protected buildCommandBarOptions(): void {
|
||||
ko.computed(() =>
|
||||
ko.toJSON([
|
||||
this.saveButton.visible,
|
||||
this.saveButton.enabled,
|
||||
this.updateButton.visible,
|
||||
this.updateButton.enabled,
|
||||
this.discardButton.visible,
|
||||
this.discardButton.enabled
|
||||
])
|
||||
).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;
|
||||
}
|
||||
|
||||
private static _toSeverity(severity: string): monaco.MarkerSeverity {
|
||||
switch (severity.toLowerCase()) {
|
||||
case "error":
|
||||
return monaco.MarkerSeverity.Error;
|
||||
case "warning":
|
||||
return monaco.MarkerSeverity.Warning;
|
||||
case "info":
|
||||
return monaco.MarkerSeverity.Info;
|
||||
case "ignore":
|
||||
default:
|
||||
return monaco.MarkerSeverity.Hint;
|
||||
}
|
||||
}
|
||||
|
||||
private static _toEditorPosition(target: number, lines: string[]): ViewModels.EditorPosition {
|
||||
let cursor: number = 0;
|
||||
let previousCursor: number = 0;
|
||||
let i: number = 0;
|
||||
while (target > cursor + lines[i].length) {
|
||||
cursor += lines[i].length + 2;
|
||||
i++;
|
||||
}
|
||||
|
||||
const editorPosition: ViewModels.EditorPosition = {
|
||||
line: i + 1,
|
||||
column: target - cursor + 1
|
||||
};
|
||||
|
||||
return editorPosition;
|
||||
}
|
||||
|
||||
protected _createBodyEditor() {
|
||||
const id = this.editorId;
|
||||
const container = document.getElementById(id);
|
||||
const options = {
|
||||
value: this.editorContent(),
|
||||
language: "javascript",
|
||||
readOnly: false,
|
||||
ariaLabel: this.ariaLabel()
|
||||
};
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
private _setModelMarkers(errors: ViewModels.QueryError[]) {
|
||||
const markers: monaco.editor.IMarkerData[] = errors.map(e => this._toMarker(e));
|
||||
const editorModel = this.editor().getModel();
|
||||
monaco.editor.setModelMarkers(editorModel, this.tabId, markers);
|
||||
}
|
||||
|
||||
private _resetModelMarkers() {
|
||||
const queryEditorModel = this.editor().getModel();
|
||||
monaco.editor.setModelMarkers(queryEditorModel, this.tabId, []);
|
||||
}
|
||||
|
||||
private _toMarker(error: ViewModels.QueryError): monaco.editor.IMarkerData {
|
||||
const editorModel = this.editor().getModel();
|
||||
const lines: string[] = editorModel.getLinesContent();
|
||||
const start: ViewModels.EditorPosition = ScriptTabBase._toEditorPosition(Number(error.start), lines);
|
||||
const end: ViewModels.EditorPosition = ScriptTabBase._toEditorPosition(Number(error.end), lines);
|
||||
|
||||
return {
|
||||
severity: ScriptTabBase._toSeverity(error.severity),
|
||||
message: error.message,
|
||||
startLineNumber: start.line,
|
||||
startColumn: start.column,
|
||||
endLineNumber: end.line,
|
||||
endColumn: end.column,
|
||||
code: error.code
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user