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:
75
src/Explorer/Graph/NewVertexComponent/NewVertex.test.ts
Normal file
75
src/Explorer/Graph/NewVertexComponent/NewVertex.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import * as ko from "knockout";
|
||||
import { NewVertexComponent, NewVertexViewModel } from "./NewVertexComponent";
|
||||
|
||||
const component = NewVertexComponent;
|
||||
|
||||
describe("New Vertex Component", () => {
|
||||
let vm: NewVertexViewModel;
|
||||
let partitionKeyProperty: ko.Observable<string>;
|
||||
|
||||
beforeEach(async () => {
|
||||
document.body.innerHTML = component.template as any;
|
||||
partitionKeyProperty = ko.observable(null);
|
||||
vm = new component.viewModel({
|
||||
newVertexData: null,
|
||||
partitionKeyProperty
|
||||
});
|
||||
ko.applyBindings(vm);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ko.cleanNode(document);
|
||||
});
|
||||
|
||||
describe("Rendering", () => {
|
||||
it("should display property list with input and +Add Property", () => {
|
||||
expect(document.querySelector(".newVertexComponent .newVertexForm")).not.toBeNull();
|
||||
expect(document.querySelector(".newVertexComponent .edgeInput")).not.toBeNull();
|
||||
expect(document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should display partition key property if set", () => {
|
||||
partitionKeyProperty("testKey");
|
||||
expect(
|
||||
(document.querySelector(".newVertexComponent .newVertexForm .labelCol input") as HTMLInputElement).value
|
||||
).toEqual("testKey");
|
||||
});
|
||||
|
||||
it("should NOT display partition key property if NOT set", () => {
|
||||
expect(document.getElementsByClassName("valueCol").length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Behavior", () => {
|
||||
let clickSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
clickSpy = jasmine.createSpy("Command button click spy");
|
||||
});
|
||||
|
||||
it("should add new property row when +Add property button is pressed", () => {
|
||||
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
|
||||
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
|
||||
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
|
||||
expect(document.getElementsByClassName("valueCol").length).toBe(3);
|
||||
expect(document.getElementsByClassName("rightPaneTrashIcon").length).toBe(3);
|
||||
});
|
||||
|
||||
it("should remove property row when trash button is pressed", () => {
|
||||
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
|
||||
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
|
||||
|
||||
// Mark this one to delete
|
||||
const elts = document.querySelectorAll(".newVertexComponent .rightPaneTrashIconImg");
|
||||
elts[elts.length - 1].className += " deleteme";
|
||||
|
||||
document.querySelector(".newVertexComponent .rightPaneAddPropertyBtn").dispatchEvent(new Event("click"));
|
||||
document
|
||||
.querySelector(".newVertexComponent .rightPaneTrashIconImg.deleteme")
|
||||
.parentElement.dispatchEvent(new Event("click"));
|
||||
expect(document.getElementsByClassName("valueCol").length).toBe(2);
|
||||
expect(document.getElementsByClassName("rightPaneTrashIcon").length).toBe(2);
|
||||
expect(document.querySelectorAll(".newVertexComponent .rightPaneTrashIconImg.deleteme").length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
<div class="newVertexComponent" data-bind="setTemplateReady: true">
|
||||
<div class="newVertexForm">
|
||||
<div class="newVertexFormRow">
|
||||
<label for="VertexLabel" class="labelCol">Label</label>
|
||||
<input
|
||||
class="edgeInput"
|
||||
type="text"
|
||||
data-bind="textInput:$data.newVertexData().label, hasFocus: $data.firstFieldHasFocus"
|
||||
aria-label="Enter vertex label"
|
||||
role="textbox"
|
||||
tabindex="0"
|
||||
placeholder="Enter vertex label"
|
||||
autocomplete="off"
|
||||
id="VertexLabel"
|
||||
/>
|
||||
<div class="actionCol"></div>
|
||||
</div>
|
||||
|
||||
<!-- ko foreach:{ data:newVertexData().properties, as: 'property' } -->
|
||||
<div class="newVertexFormRow">
|
||||
<div class="labelCol">
|
||||
<input
|
||||
type="text"
|
||||
id="propertyKeyNewVertexPane"
|
||||
data-bind="textInput: property.key, attr: { 'aria-label': 'Enter key for property '+ ($index() + 1) }"
|
||||
placeholder="Key"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="valueCol">
|
||||
<input
|
||||
class="edgeInput"
|
||||
type="text"
|
||||
data-bind="textInput: property.values[0].value, , attr: { 'aria-label': 'Enter value for property '+ ($index() + 1) }"
|
||||
placeholder="Value"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
class="typeSelect"
|
||||
required
|
||||
data-bind="options:$parent.propertyTypes, value:property.values[0].type, attr: { 'aria-label': property.values[0].type + ': for property '+ ($index() + 1) }"
|
||||
></select>
|
||||
</div>
|
||||
<div class="actionCol">
|
||||
<div
|
||||
class="rightPaneTrashIcon rightPaneBtns"
|
||||
data-bind="click:$parent.removeNewVertexProperty.bind($parent, $index()), event: { keypress: $parent.removeNewVertexPropertyKeyPress.bind($parent, $index()) }, attr: { 'aria-label': 'Remove property '+ ($index() + 1) }"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
>
|
||||
<img class="refreshcol rightPaneTrashIconImg" src="/delete.svg" alt="Remove property" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<div class="newVertexFormRow">
|
||||
<span class="rightPaneAddPropertyBtnPadding">
|
||||
<span
|
||||
class="rightPaneAddPropertyBtn rightPaneBtns"
|
||||
id="addProperyNewVertexBtn"
|
||||
data-bind="click:onAddNewProperty, event: { keypress: onAddNewPropertyKeyPress }"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
>
|
||||
<img class="refreshcol rightPaneAddPropertyImg" src="/Add-property.svg" alt="Add property" /> Add
|
||||
Property</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
99
src/Explorer/Graph/NewVertexComponent/NewVertexComponent.ts
Normal file
99
src/Explorer/Graph/NewVertexComponent/NewVertexComponent.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as ko from "knockout";
|
||||
import { EditorNodePropertiesComponent } from "../GraphExplorerComponent/EditorNodePropertiesComponent";
|
||||
import { NewVertexData, InputProperty } from "../../../Contracts/ViewModels";
|
||||
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import template from "./NewVertexComponent.html";
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
export interface NewVertexParams {
|
||||
// Data to be edited by the component
|
||||
newVertexData: ko.Observable<NewVertexData>;
|
||||
partitionKeyProperty: ko.Observable<string>;
|
||||
firstFieldHasFocus?: ko.Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Callback triggered when the template is bound to the component (for testing purposes)
|
||||
*/
|
||||
onTemplateReady?: () => void;
|
||||
}
|
||||
|
||||
export class NewVertexViewModel extends WaitsForTemplateViewModel {
|
||||
private static readonly DEFAULT_PROPERTY_TYPE = "string";
|
||||
|
||||
private newVertexData: ko.Observable<NewVertexData>;
|
||||
private firstFieldHasFocus: ko.Observable<boolean>;
|
||||
private propertyTypes: string[];
|
||||
|
||||
public constructor(params: NewVertexParams) {
|
||||
super();
|
||||
super.onTemplateReady((isTemplateReady: boolean) => {
|
||||
if (isTemplateReady && params.onTemplateReady) {
|
||||
params.onTemplateReady();
|
||||
}
|
||||
});
|
||||
|
||||
this.newVertexData =
|
||||
params.newVertexData ||
|
||||
ko.observable({
|
||||
label: "",
|
||||
properties: <InputProperty[]>[]
|
||||
});
|
||||
this.firstFieldHasFocus = params.firstFieldHasFocus || ko.observable(false);
|
||||
this.propertyTypes = EditorNodePropertiesComponent.VERTEX_PROPERTY_TYPES;
|
||||
if (params.partitionKeyProperty) {
|
||||
params.partitionKeyProperty.subscribe((newKeyProp: string) => {
|
||||
if (!newKeyProp) {
|
||||
return;
|
||||
}
|
||||
this.addNewVertexProperty(newKeyProp);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onAddNewProperty() {
|
||||
this.addNewVertexProperty();
|
||||
document.getElementById("propertyKeyNewVertexPane").focus();
|
||||
}
|
||||
|
||||
public onAddNewPropertyKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
|
||||
this.onAddNewProperty();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public addNewVertexProperty(key?: string) {
|
||||
let ap = this.newVertexData().properties;
|
||||
ap.push({ key: key || "", values: [{ value: "", type: NewVertexViewModel.DEFAULT_PROPERTY_TYPE }] });
|
||||
this.newVertexData.valueHasMutated();
|
||||
}
|
||||
|
||||
public removeNewVertexProperty(index: number) {
|
||||
let ap = this.newVertexData().properties;
|
||||
ap.splice(index, 1);
|
||||
this.newVertexData.valueHasMutated();
|
||||
document.getElementById("addProperyNewVertexBtn").focus();
|
||||
}
|
||||
|
||||
public removeNewVertexPropertyKeyPress = (index: number, source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === Constants.KeyCodes.Enter || event.keyCode === Constants.KeyCodes.Space) {
|
||||
this.removeNewVertexProperty(index);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export const NewVertexComponent = {
|
||||
viewModel: NewVertexViewModel,
|
||||
template
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.newVertexComponent {
|
||||
padding: @LargeSpace 20px 20px 0px;
|
||||
width: 400px;
|
||||
|
||||
.newVertexForm {
|
||||
width: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
.newVertexFormRow {
|
||||
.flex-display();
|
||||
.flex-direction(@direction: row);
|
||||
padding: 4px 5px;
|
||||
|
||||
label {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.valueCol {
|
||||
flex-grow: 1;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.rightPaneAddPropertyBtnPadding {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.edgeLabel {
|
||||
padding-right: 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actionCol {
|
||||
min-width: 30px;
|
||||
padding: 0px 4px;
|
||||
}
|
||||
|
||||
.labelCol {
|
||||
width: 72px;
|
||||
min-width: 72px;
|
||||
|
||||
input {
|
||||
max-width: 65px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.edgeInput {
|
||||
width: 100%;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.typeSelect {
|
||||
height: 23px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.rightPaneTrashIcon {
|
||||
padding: 4px 1px 0px 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rightPaneTrashIconImg {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.rightPaneAddPropertyBtn {
|
||||
padding: 7px 7px 8px 8px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.rightPaneBtns {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: @BaseLow ;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: @AccentMediumLow;
|
||||
}
|
||||
}
|
||||
|
||||
.rightPaneAddPropertyImg {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.contentScroll {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user