port changes from rest multivalue
This commit is contained in:
Родитель
38b099961b
Коммит
401d7cbd2f
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
32
package.json
32
package.json
|
@ -3,33 +3,39 @@
|
|||
"scripts": {
|
||||
"clean": "rimraf dist *.vsix vss-extension-release.json src/*js libs",
|
||||
"dev": "webpack-dev-server --hot --progress --colors --content-base ./src --https --port 8888",
|
||||
"dev:http": "webpack-dev-server -d --hot --progress --colors --content-base ./src --port 8888",
|
||||
"dev:http": "webpack-dev-server --hot --progress --colors --content-base ./src --http --port 8888",
|
||||
"package:dev": "node ./scripts/packageDev",
|
||||
"package:dev:http": "node ./scripts/packageDevHttp",
|
||||
"package:release": "node ./scripts/packageRelease",
|
||||
"package:beta": "node ./scripts/packageBeta",
|
||||
"publish:dev": "npm run package:dev && node ./scripts/publishDev",
|
||||
"build:dev": "npm run clean && mkdir dist && webpack --progress --colors --output-path ./dist",
|
||||
"build:release": "npm run clean && mkdir dist && webpack --progress --colors --output-path ./dist -p",
|
||||
"publish:release": "npm run build:release && node ./scripts/publishRelease",
|
||||
"test": "karma start --single-run",
|
||||
"postinstall": "typings install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.0.9",
|
||||
"@types/react-dom": "^16.0.0",
|
||||
"@types/applicationinsights-js": "^1.0.5",
|
||||
"@types/jquery": "^2.0.41",
|
||||
"@types/q": "^1.0.0",
|
||||
"webpack": "^2.3.3",
|
||||
"webpack-dev-server": "^2.4.2",
|
||||
"@types/jsonpath": "^0.2.0",
|
||||
"@types/react": "^16.4.8",
|
||||
"@types/react-dom": "^16.0.7",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"css-loader": "^0.28.0",
|
||||
"jsonpath": "^1.0.0",
|
||||
"office-ui-fabric-react": "^6.47.1",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2",
|
||||
"rimraf": "^2.6.1",
|
||||
"style-loader": "^0.16.1",
|
||||
"css-loader": "^0.28.0",
|
||||
"ts-loader": "^2.0.3",
|
||||
"tfx-cli": "^0.4.5",
|
||||
"typescript": "2.7.2",
|
||||
"typings": "^2.1.0",
|
||||
"uglifyjs-webpack-plugin": "^0.4.2",
|
||||
"copy-webpack-plugin": "^4.0.1"
|
||||
"ts-loader": "^4.4.2",
|
||||
"typescript": "^3.0.1",
|
||||
"typings": "^2.1.1",
|
||||
"vss-web-extension-sdk": "^2.117.0",
|
||||
"webpack": "^4.16.5",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-dev-server": "^2.4.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"vss-web-extension-sdk": "5.127.0"
|
||||
|
|
|
@ -6,6 +6,10 @@ var extensionId = manifest.id;
|
|||
|
||||
// Package extension
|
||||
var command = `tfx extension create --overrides-file configs/dev.json --manifest-globs vss-extension.json --extension-id ${extensionId}-dev --no-prompt --rev-version`;
|
||||
exec(command, function() {
|
||||
console.log("Package created");
|
||||
exec(command, function(err, stdout, stderr) {
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
|
@ -4,14 +4,10 @@ var exec = require("child_process").exec;
|
|||
|
||||
// Package extension
|
||||
var command = `tfx extension create --overrides-file configs/release.json --manifest-globs vss-extension.json --no-prompt --json`;
|
||||
exec(command, (error, stdout) => {
|
||||
if (error) {
|
||||
console.error(`Could not create package: '${error}'`);
|
||||
return;
|
||||
exec(command, function(err, stdout, stderr) {
|
||||
console.log(stderr);
|
||||
console.log(stdout);
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
let output = JSON.parse(stdout);
|
||||
|
||||
console.log(`Package created ${output.path}`);
|
||||
}
|
||||
);
|
||||
});
|
|
@ -1,163 +0,0 @@
|
|||
import Q = require("q");
|
||||
import * as WitService from "TFS/WorkItemTracking/Services";
|
||||
import * as VSSUtilsCore from "VSS/Utils/Core";
|
||||
|
||||
export class BaseMultiValueControl {
|
||||
/**
|
||||
* Field name input for the control
|
||||
*/
|
||||
public fieldName: string;
|
||||
|
||||
/**
|
||||
* The container to hold the control
|
||||
*/
|
||||
protected containerElement: JQuery;
|
||||
private _message: JQuery;
|
||||
|
||||
/**
|
||||
* The container for error message display
|
||||
*/
|
||||
private _errorPane: JQuery;
|
||||
|
||||
private _flushing: boolean;
|
||||
private _bodyElement: HTMLBodyElement;
|
||||
|
||||
/* Inherits from initalConfig to control if always show field border
|
||||
*/
|
||||
private _showFieldBorder: boolean;
|
||||
|
||||
/**
|
||||
* Store the last recorded window width to know
|
||||
* when we have been shrunk and should resize
|
||||
*/
|
||||
private _windowWidth: number;
|
||||
private _minWindowWidthDelta: number = 10; // Minum change in window width to react to
|
||||
private _windowResizeThrottleDelegate: () => void;
|
||||
|
||||
constructor() {
|
||||
const initialConfig = VSS.getConfiguration();
|
||||
this._showFieldBorder = !!initialConfig.fieldBorder;
|
||||
|
||||
this.containerElement = $(".container");
|
||||
if (this._showFieldBorder) {
|
||||
this.containerElement.addClass("fieldBorder");
|
||||
}
|
||||
|
||||
this._errorPane = $("<div>").addClass("errorPane").appendTo(this.containerElement);
|
||||
this._message = $("<div>").addClass("message").appendTo(this.containerElement);
|
||||
|
||||
const inputs: IDictionaryStringTo<string> = initialConfig.witInputs;
|
||||
|
||||
this.fieldName = inputs.FieldName;
|
||||
if (!this.fieldName) {
|
||||
this.showError("FieldName input has not been specified");
|
||||
}
|
||||
|
||||
this._windowResizeThrottleDelegate = VSSUtilsCore.throttledDelegate(this, 50, () => {
|
||||
this._windowWidth = window.innerWidth;
|
||||
this.resize();
|
||||
});
|
||||
|
||||
this._windowWidth = window.innerWidth;
|
||||
$(window).resize(() => {
|
||||
if (Math.abs(this._windowWidth - window.innerWidth) > this._minWindowWidthDelta) {
|
||||
this._windowResizeThrottleDelegate.call(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new instance of Control
|
||||
*/
|
||||
public initialize(): void {
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the control's value
|
||||
*/
|
||||
public invalidate(): void {
|
||||
if (!this._flushing) {
|
||||
this.setMessage("getting current value");
|
||||
this._getCurrentFieldValue().then(
|
||||
(value: string) => {
|
||||
this.setMessage("");
|
||||
this.setValue(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.resize();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the control's value to the field
|
||||
*/
|
||||
protected flush(): void {
|
||||
this._flushing = true;
|
||||
WitService.WorkItemFormService.getService().then(
|
||||
(service: WitService.IWorkItemFormService) => {
|
||||
service.setFieldValue(this.fieldName, this.getValue()).then(
|
||||
(values) => {
|
||||
this._flushing = false;
|
||||
},
|
||||
() => {
|
||||
this._flushing = false;
|
||||
this.showError("Error storing the field value");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
protected getValue(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected setValue(value: string): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
protected showError(error: string): void {
|
||||
this._errorPane.text(error);
|
||||
this._errorPane.show();
|
||||
}
|
||||
|
||||
protected clearError() {
|
||||
this._errorPane.text("");
|
||||
this._errorPane.hide();
|
||||
}
|
||||
|
||||
protected setMessage(message: string) {
|
||||
this._message.text(message);
|
||||
}
|
||||
|
||||
private _getCurrentFieldValue(): IPromise<string> {
|
||||
const defer = Q.defer<string>();
|
||||
WitService.WorkItemFormService.getService().then(
|
||||
(service) => {
|
||||
service.getFieldValues([this.fieldName]).then(
|
||||
(values) => {
|
||||
defer.resolve(values[this.fieldName] as string);
|
||||
},
|
||||
() => {
|
||||
this.showError("Error loading values for field: " + this.fieldName);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
protected resize() {
|
||||
this._bodyElement = document.getElementsByTagName("body").item(0) as HTMLBodyElement;
|
||||
|
||||
// Cast as any until declarations are updated
|
||||
VSS.resize(null, this._bodyElement.offsetHeight);
|
||||
}
|
||||
}
|
|
@ -1,345 +0,0 @@
|
|||
import Q = require("q");
|
||||
import * as WitService from "TFS/WorkItemTracking/Services";
|
||||
import * as Utils_Array from "VSS/Utils/Array";
|
||||
import * as VSSUtilsCore from "VSS/Utils/Core";
|
||||
import * as Utils_String from "VSS/Utils/String";
|
||||
import {BaseMultiValueControl} from "./BaseMultiValueControl";
|
||||
|
||||
export class MultiValueCombo extends BaseMultiValueControl {
|
||||
/*
|
||||
* UI elements for the control.
|
||||
*/
|
||||
private _selectedValuesWrapper: JQuery;
|
||||
private _selectedValuesContainer: JQuery;
|
||||
private _checkboxValuesContainer: JQuery;
|
||||
private _chevron: JQuery;
|
||||
|
||||
private _suggestedValues: string[];
|
||||
private _valueToCheckboxMap: IDictionaryStringTo<JQuery>;
|
||||
private _valueToLabelMap: IDictionaryStringTo<JQuery>;
|
||||
|
||||
private _maxSelectedToShow = 100;
|
||||
private _chevronDownClass = "bowtie-chevron-down-light";
|
||||
private _chevronUpClass = "bowtie-chevron-up-light";
|
||||
private _windowFocussed = false;
|
||||
|
||||
private _toggleThrottleDelegate: () => void;
|
||||
/**
|
||||
* Initialize a new instance of MultiValueControl
|
||||
*/
|
||||
public initialize(): void {
|
||||
this._selectedValuesWrapper = $("<div>").addClass("selectedValuesWrapper").appendTo(this.containerElement);
|
||||
this._selectedValuesContainer = $("<div>").addClass("selectedValuesContainer").attr("tabindex", "-1").appendTo(this._selectedValuesWrapper);
|
||||
this._chevron = $("<span />").addClass("bowtie-icon " + this._chevronDownClass).appendTo(this._selectedValuesWrapper);
|
||||
this._checkboxValuesContainer = $("<div>").addClass("checkboxValuesContainer").appendTo(this.containerElement);
|
||||
|
||||
this._valueToCheckboxMap = {};
|
||||
this._valueToLabelMap = {};
|
||||
|
||||
this.setMessage("getting suggested values");
|
||||
|
||||
this._getSuggestedValues().then(
|
||||
(values: string[]) => {
|
||||
this._suggestedValues = values.filter((s: string): boolean => {
|
||||
return s.trim() !== "";
|
||||
});
|
||||
this.setMessage("populating check boxes");
|
||||
|
||||
this._populateCheckBoxes();
|
||||
this.setMessage("parent intializing");
|
||||
super.initialize();
|
||||
this.setMessage("");
|
||||
},
|
||||
);
|
||||
|
||||
this._toggleThrottleDelegate = VSSUtilsCore.throttledDelegate(this, 100, () => {
|
||||
this._toggleCheckBoxContainer();
|
||||
});
|
||||
|
||||
$(window).blur(() => {
|
||||
this._hideCheckBoxContainer();
|
||||
return false;
|
||||
});
|
||||
|
||||
$(window).focus((e) => {
|
||||
this._windowFocussed = true;
|
||||
setTimeout(() => {
|
||||
this._windowFocussed = false;
|
||||
}, 500);
|
||||
this._toggleThrottleDelegate.call(this);
|
||||
return false;
|
||||
});
|
||||
|
||||
this._selectedValuesWrapper.click(() => {
|
||||
if (!this._windowFocussed) {
|
||||
this._toggleThrottleDelegate.call(this);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
this._chevron.click(() => {
|
||||
this._toggleThrottleDelegate.call(this);
|
||||
return false;
|
||||
});
|
||||
|
||||
$(window).keydown((eventObject) => {
|
||||
if (eventObject.keyCode === 38 /* Up */) {
|
||||
const focusedCheckBox = $("input:focus", this._checkboxValuesContainer);
|
||||
if (focusedCheckBox.length <= 0) {
|
||||
// None selected, choose last
|
||||
$("input:last", this._checkboxValuesContainer).focus();
|
||||
} else {
|
||||
// One selected to choose previous if it exists
|
||||
focusedCheckBox.parent().prev().find("input").focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if (eventObject.keyCode === 40 /* Down */) {
|
||||
const focusedCheckBox = $("input:focus", this._checkboxValuesContainer);
|
||||
if (focusedCheckBox.length <= 0) {
|
||||
// None selected, choose first
|
||||
$("input:first", this._checkboxValuesContainer).focus();
|
||||
} else {
|
||||
// One selected to choose previous if it exists
|
||||
focusedCheckBox.parent().next().find("input").focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
const checkboxes: JQuery = $("input", this._checkboxValuesContainer);
|
||||
const labels: JQuery = $(".checkboxLabel", this._checkboxValuesContainer);
|
||||
checkboxes.prop("checked", false);
|
||||
checkboxes.removeClass("selectedCheckbox");
|
||||
this._selectedValuesContainer.empty();
|
||||
}
|
||||
|
||||
protected getValue(): string {
|
||||
const selectedCheckboxes: JQuery = $("input.valueOption:checked", this._checkboxValuesContainer);
|
||||
let selectedValues: string[] = [];
|
||||
|
||||
selectedCheckboxes.each((i: number, elem: HTMLElement) => {
|
||||
selectedValues.push($(elem).attr("value"));
|
||||
});
|
||||
|
||||
selectedValues = Utils_Array.uniqueSort(selectedValues, Utils_String.localeIgnoreCaseComparer);
|
||||
return selectedValues.join(";");
|
||||
}
|
||||
|
||||
protected setValue(value: string): void {
|
||||
this.clear();
|
||||
const selectedValues = value ? value.split(";") : [];
|
||||
|
||||
this._showValues(selectedValues);
|
||||
|
||||
$.each(selectedValues, (i, selectedValue) => {
|
||||
if (selectedValue) {
|
||||
// mark the checkbox as checked
|
||||
const checkbox = this._valueToCheckboxMap[selectedValue];
|
||||
const label = this._valueToLabelMap[selectedValue];
|
||||
if (checkbox) {
|
||||
checkbox.prop("checked", true);
|
||||
checkbox.addClass("selectedCheckbox");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleCheckBoxContainer() {
|
||||
if (this._checkboxValuesContainer.is(":visible")) {
|
||||
this._hideCheckBoxContainer();
|
||||
} else {
|
||||
this._showCheckBoxContainer();
|
||||
}
|
||||
}
|
||||
|
||||
private _showCheckBoxContainer() {
|
||||
this._chevron.removeClass(this._chevronDownClass).addClass(this._chevronUpClass);
|
||||
this.containerElement.addClass("expanded").removeClass("collapsed");
|
||||
this._checkboxValuesContainer.show();
|
||||
this.resize();
|
||||
}
|
||||
|
||||
private _hideCheckBoxContainer() {
|
||||
this._chevron.removeClass(this._chevronUpClass).addClass(this._chevronDownClass);
|
||||
this.containerElement.removeClass("expanded").addClass("collapsed");
|
||||
this._checkboxValuesContainer.hide();
|
||||
this.resize();
|
||||
}
|
||||
|
||||
private _showValues(values: string[]) {
|
||||
if (values.length <= 0) {
|
||||
this._selectedValuesContainer.append("<div class='noSelection'>No selection made</div>");
|
||||
} else {
|
||||
$.each(values, (i, value) => {
|
||||
let control;
|
||||
// only show first N selections and the rest as more.
|
||||
if (i < this._maxSelectedToShow) {
|
||||
control = this._createSelectedValueControl(value);
|
||||
} else {
|
||||
control = this._createSelectedValueControl(values.length - i + " more");
|
||||
control.attr("title", values.slice(i).join(";"));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._updateSelectAllControlState();
|
||||
|
||||
this.resize();
|
||||
}
|
||||
|
||||
private _refreshValues() {
|
||||
const rawValue = this.getValue();
|
||||
const values = rawValue ? rawValue.split(";") : [];
|
||||
this._selectedValuesContainer.empty();
|
||||
this._showValues(values);
|
||||
}
|
||||
|
||||
private _createSelectedValueControl(value: string): JQuery {
|
||||
const control = $("<div />");
|
||||
if (value) {
|
||||
control.text(value);
|
||||
control.attr("title", value);
|
||||
control.addClass("selected");
|
||||
|
||||
this._selectedValuesContainer.append(control);
|
||||
}
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the UI with the list of checkboxes to choose the value from.
|
||||
*/
|
||||
private _populateCheckBoxes(): void {
|
||||
if (!this._suggestedValues || this._suggestedValues.length === 0) {
|
||||
this.showError("No values to select.");
|
||||
} else {
|
||||
// Add the select all method
|
||||
const selectAllBox = this._createSelectAllControl();
|
||||
|
||||
$.each(this._suggestedValues, (i, value) => {
|
||||
this._createCheckBoxControl(value, selectAllBox);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSelectAllControlState() {
|
||||
const selectAllBox = $("input.selectAllOption", this._checkboxValuesContainer);
|
||||
const allBoxes = $("input.valueOption", this._checkboxValuesContainer);
|
||||
const checkedBoxes = $("input.valueOption:checked", this._checkboxValuesContainer);
|
||||
if (allBoxes.length > checkedBoxes.length) {
|
||||
selectAllBox.prop("checked", false);
|
||||
} else {
|
||||
selectAllBox.prop("checked", true);
|
||||
}
|
||||
}
|
||||
|
||||
private _createSelectAllControl() {
|
||||
|
||||
const value = "Select All";
|
||||
const label = this._createValueLabel(value);
|
||||
const checkbox = this._createCheckBox(value, label, () => {
|
||||
|
||||
const checkBoxes = $("input.valueOption", this._checkboxValuesContainer);
|
||||
if (checkbox.prop("checked")) {
|
||||
checkBoxes.prop("checked", true);
|
||||
} else {
|
||||
checkBoxes.prop("checked", false);
|
||||
}
|
||||
|
||||
});
|
||||
const container = $("<div />").addClass("checkboxContainer selectAllControlContainer");
|
||||
checkbox.addClass("selectAllOption");
|
||||
this._valueToCheckboxMap[value] = checkbox;
|
||||
|
||||
this._valueToLabelMap[value] = label;
|
||||
|
||||
container.append(checkbox);
|
||||
container.append(label);
|
||||
|
||||
this._checkboxValuesContainer.append(container);
|
||||
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
private _createCheckBoxControl(value: string, selectAllBox: JQuery) {
|
||||
|
||||
const label = this._createValueLabel(value);
|
||||
const checkbox = this._createCheckBox(value, label);
|
||||
const container = $("<div />").addClass("checkboxContainer");
|
||||
checkbox.addClass("valueOption");
|
||||
this._valueToCheckboxMap[value] = checkbox;
|
||||
|
||||
this._valueToLabelMap[value] = label;
|
||||
|
||||
container.append(checkbox);
|
||||
container.append(label);
|
||||
|
||||
this._checkboxValuesContainer.append(container);
|
||||
}
|
||||
|
||||
private _createValueLabel(value: string) {
|
||||
const label = $("<label />");
|
||||
label.attr("for", "checkbox" + value);
|
||||
label.text(value);
|
||||
label.attr("title", value);
|
||||
label.addClass("checkboxLabel");
|
||||
return label;
|
||||
}
|
||||
|
||||
private _createCheckBox(value: string, label: JQuery, action?: () => void) {
|
||||
const checkbox = $("<input />");
|
||||
checkbox.attr("type", "checkbox");
|
||||
checkbox.attr("name", value);
|
||||
checkbox.attr("value", value);
|
||||
checkbox.attr("tabindex", -1);
|
||||
checkbox.attr("id", "checkbox" + value);
|
||||
|
||||
checkbox.change((e) => {
|
||||
|
||||
if (action) {
|
||||
action.call(this);
|
||||
}
|
||||
|
||||
this._refreshValues();
|
||||
this.flush();
|
||||
});
|
||||
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
private _getSuggestedValues(): Q.IPromise<string[]> {
|
||||
const defer = Q.defer<string[]>();
|
||||
const inputs: IDictionaryStringTo<string> = VSS.getConfiguration().witInputs;
|
||||
|
||||
const valuesString: string = inputs.Values;
|
||||
if (valuesString) {
|
||||
defer.resolve(valuesString.split(";"));
|
||||
} else {
|
||||
this.setMessage("getting form service");
|
||||
// if the values input were not specified as an input, get the suggested values for the field.
|
||||
WitService.WorkItemFormService.getService().then(
|
||||
(service: any) => {
|
||||
this.setMessage("getting allowed field values");
|
||||
service.getAllowedFieldValues(this.fieldName).then(
|
||||
(values: string[]) => {
|
||||
defer.resolve(values);
|
||||
},
|
||||
() => {
|
||||
this.showError("Could not load values for field " + this.fieldName);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
import { Checkbox } from "office-ui-fabric-react/lib/components/Checkbox";
|
||||
import { ITag, TagPicker } from "office-ui-fabric-react/lib/components/pickers";
|
||||
import { TextField } from "office-ui-fabric-react/lib/components/TextField";
|
||||
import { FocusZone, FocusZoneDirection } from "office-ui-fabric-react/lib/FocusZone";
|
||||
import * as React from "react";
|
||||
import { DelayedFunction } from "VSS/Utils/Core";
|
||||
import { BrowserCheckUtils } from "VSS/Utils/UI";
|
||||
|
||||
interface IMultiValueControlProps {
|
||||
selected?: string[];
|
||||
width?: number;
|
||||
readOnly?: boolean;
|
||||
placeholder?: string;
|
||||
noResultsFoundText?: string;
|
||||
searchingText?: string;
|
||||
onSelectionChanged?: (selection: string[]) => Promise<void>;
|
||||
forceValue?: boolean;
|
||||
options: string[];
|
||||
onBlurred?: () => void;
|
||||
onResize?: () => void;
|
||||
}
|
||||
|
||||
interface IMultiValueControlState {
|
||||
focused: boolean;
|
||||
filter: string;
|
||||
}
|
||||
|
||||
export class MultiValueControl extends React.Component<IMultiValueControlProps, IMultiValueControlState> {
|
||||
private readonly _unfocusedTimeout = BrowserCheckUtils.isSafari() ? 2000 : 1;
|
||||
private _setUnfocused = new DelayedFunction(null, this._unfocusedTimeout, "", () => {
|
||||
this.setState({focused: false, filter: ""});
|
||||
});
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = { focused: false, filter: "" };
|
||||
}
|
||||
public render() {
|
||||
const {focused} = this.state;
|
||||
return <div className={`multi-value-control ${focused ? "focused" : ""}`}>
|
||||
<TagPicker
|
||||
className="tag-picker"
|
||||
selectedItems={(this.props.selected || []).map((t) => ({ key: t, name: t }))}
|
||||
inputProps={{
|
||||
placeholder: this.props.placeholder,
|
||||
readOnly: this.props.readOnly,
|
||||
width: this.props.width || 200,
|
||||
onFocus: () => this.setState({ focused: true }),
|
||||
}}
|
||||
onChange={this._onTagsChanged}
|
||||
onResolveSuggestions={() => []}
|
||||
/>
|
||||
{focused ? this._getOptions() : null}
|
||||
</div>;
|
||||
}
|
||||
public componentDidUpdate() {
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize();
|
||||
}
|
||||
}
|
||||
private _getOptions() {
|
||||
const options = this.props.options;
|
||||
const selected = (this.props.selected || []).slice(0);
|
||||
const filteredOpts = this._filteredOptions();
|
||||
|
||||
return <div className="options">
|
||||
<TextField value={this.state.filter}
|
||||
autoFocus
|
||||
placeholder={"Filter values"}
|
||||
onKeyDown={this._onInputKeyDown}
|
||||
onBlur={this._onBlur}
|
||||
onFocus={this._onFocus}
|
||||
onChange={this._onInputChange}
|
||||
/>
|
||||
<FocusZone
|
||||
direction={FocusZoneDirection.vertical}
|
||||
className="checkboxes"
|
||||
>
|
||||
{this.state.filter ? null :
|
||||
<Checkbox
|
||||
label="Select All"
|
||||
checked={selected.join(";") === options.join(";")}
|
||||
onChange={this._toggleSelectAll}
|
||||
inputProps={{
|
||||
onBlur: this._onBlur,
|
||||
onFocus: this._onFocus,
|
||||
}}
|
||||
/>}
|
||||
{filteredOpts
|
||||
.map((o) => <Checkbox
|
||||
checked={selected.indexOf(o) >= 0}
|
||||
inputProps={{
|
||||
onBlur: this._onBlur,
|
||||
onFocus: this._onFocus,
|
||||
}}
|
||||
onChange={() => this._toggleOption(o)}
|
||||
label={o}
|
||||
/>)}
|
||||
</FocusZone>
|
||||
</div>;
|
||||
}
|
||||
private _onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.altKey || e.shiftKey || e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.keyCode === 13) {
|
||||
const filtered = this._filteredOptions();
|
||||
if (filtered.length !== 1) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleOption(filtered[0]);
|
||||
this.setState({filter: ""});
|
||||
}
|
||||
}
|
||||
private _toggleSelectAll = () => {
|
||||
const options = this.props.options;
|
||||
const selected = this.props.selected || [];
|
||||
if (selected.join(";") === options.join(";")) {
|
||||
this._setSelected([]);
|
||||
} else {
|
||||
this._setSelected(options);
|
||||
}
|
||||
this._ifSafariCloseDropdown();
|
||||
}
|
||||
private _filteredOptions = () => {
|
||||
const filter = this.state.filter.toLocaleLowerCase();
|
||||
const opts = this.props.options;
|
||||
return [
|
||||
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) === 0),
|
||||
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) > 0),
|
||||
];
|
||||
}
|
||||
private _onInputChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
this.setState({filter: newValue || ""});
|
||||
}
|
||||
private _onBlur = () => {
|
||||
this._setUnfocused.reset();
|
||||
}
|
||||
private _onFocus = () => {
|
||||
this._setUnfocused.cancel();
|
||||
}
|
||||
private _setSelected = async (selected: string[]): Promise<void> => {
|
||||
if (!this.props.onSelectionChanged) {
|
||||
return;
|
||||
}
|
||||
await this.props.onSelectionChanged(selected);
|
||||
}
|
||||
private _toggleOption = (option: string): boolean => {
|
||||
const selectedMap: {[k: string]: boolean} = {};
|
||||
for (const s of this.props.selected || []) {
|
||||
selectedMap[s] = true;
|
||||
}
|
||||
const change = option in selectedMap || this.props.options.indexOf(option) >= 0;
|
||||
selectedMap[option] = !selectedMap[option];
|
||||
const selected = this.props.options.filter((o) => selectedMap[o]);
|
||||
this._setSelected(selected);
|
||||
this._ifSafariCloseDropdown();
|
||||
return change;
|
||||
}
|
||||
private _ifSafariCloseDropdown() {
|
||||
if (BrowserCheckUtils.isSafari()) {
|
||||
this.setState({filter: "", focused: false});
|
||||
}
|
||||
}
|
||||
private _onTagsChanged = (tags: ITag[]) => {
|
||||
const values = tags.map(({name}) => name);
|
||||
if (this.props.onSelectionChanged) {
|
||||
this.props.onSelectionChanged(values);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { WorkItemFormService } from "TFS/WorkItemTracking/Services";
|
||||
import { getSuggestedValues } from "./getSuggestedValues";
|
||||
import { MultiValueControl } from "./MultiValueControl";
|
||||
|
||||
initializeIcons();
|
||||
export class MultiValueEvents {
|
||||
public readonly fieldName = VSS.getConfiguration().witInputs.FieldName;
|
||||
private readonly _container = document.getElementById("container") as HTMLElement;
|
||||
private _onRefreshed: () => void;
|
||||
/** Counter to avoid consuming own changed field events. */
|
||||
private _fired: number = 0;
|
||||
|
||||
public async refresh(selected?: string[]): Promise<void> {
|
||||
if (!selected) {
|
||||
if (this._fired) {
|
||||
this._fired--;
|
||||
return;
|
||||
}
|
||||
selected = await this._getSelected();
|
||||
}
|
||||
ReactDOM.render(<MultiValueControl
|
||||
selected={selected}
|
||||
options={await getSuggestedValues()}
|
||||
onSelectionChanged={this._setSelected}
|
||||
width={this._container.scrollWidth}
|
||||
placeholder={selected.length ? "Click to Add" : "No selection made"}
|
||||
onResize={this._resize}
|
||||
/>, this._container, () => {
|
||||
this._resize();
|
||||
if (this._onRefreshed) {
|
||||
this._onRefreshed();
|
||||
}
|
||||
});
|
||||
}
|
||||
private _resize = () => {
|
||||
VSS.resize(this._container.scrollWidth, this._container.scrollHeight);
|
||||
}
|
||||
private async _getSelected(): Promise<string[]> {
|
||||
const formService = await WorkItemFormService.getService();
|
||||
const value = await formService.getFieldValue(this.fieldName);
|
||||
if (typeof value !== "string") {
|
||||
return [];
|
||||
}
|
||||
return value.split(";").filter((v) => !!v);
|
||||
}
|
||||
private _setSelected = async (values: string[]): Promise<void> => {
|
||||
this.refresh(values);
|
||||
this._fired++;
|
||||
const formService = await WorkItemFormService.getService();
|
||||
formService.setFieldValue(this.fieldName, values.join(";"));
|
||||
return new Promise<void>((resolve) => {
|
||||
this._onRefreshed = resolve;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { WorkItemFormService } from "TFS/WorkItemTracking/Services";
|
||||
|
||||
export async function getSuggestedValues(): Promise<string[]> {
|
||||
const inputs: IDictionaryStringTo<string> = VSS.getConfiguration().witInputs;
|
||||
const valuesString: string = inputs.Values;
|
||||
if (valuesString) {
|
||||
return valuesString.split(";");
|
||||
}
|
||||
this.setMessage("getting form service");
|
||||
// if the values input were not specified as an input, get the suggested values for the field.
|
||||
const service = await WorkItemFormService.getService();
|
||||
return await service.getAllowedFieldValues(this.fieldName) as string[];
|
||||
}
|
|
@ -1,146 +1,15 @@
|
|||
.container {
|
||||
border: 1px transparent solid;
|
||||
margin-right: 4px;
|
||||
#container .multi-value-control .tag-picker [role=list]:not(:hover) {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
html:focus, body:focus, div:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input, label, .container {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container:hover, .container:active, .container.expanded, .container.fieldBorder {
|
||||
border: 1px #e6e6e6 solid;
|
||||
}
|
||||
|
||||
.container .selected {
|
||||
margin: 5px 5px 0 0;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
color: #4F4F4F;
|
||||
border: solid 1px #e5e5e5;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.container .selectedValuesContainer .noSelection {
|
||||
margin: 6px 6px 2px 4px;
|
||||
font-size: 14px;
|
||||
display:inline-block;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.container .checkboxLabel {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
input[type=checkbox]:checked + label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.container .selectedValuesContainer {
|
||||
padding-left:2px;
|
||||
}
|
||||
|
||||
.container .selectAllControlContainer {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.container .selectedValuesWrapper {
|
||||
padding-right: 20px;
|
||||
padding-bottom:2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container.expanded .selectedValuesWrapper {
|
||||
border-bottom: 1px #e6e6e6 solid;
|
||||
}
|
||||
|
||||
.container .selectedValuesWrapper .bowtie-icon {
|
||||
margin: -8px 2px 0 2px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
width: 20px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.container .selectedValuesWrapper:hover .bowtie-icon, .container .selectedValuesWrapper:active .bowtie-icon,
|
||||
.container.expanded .selectedValuesWrapper .bowtie-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.container .checkboxValuesContainer {
|
||||
overflow-y: auto;
|
||||
#container .multi-value-control.focused .tag-picker input {
|
||||
display: none;
|
||||
max-height: 290px; /* 10 items max in view */
|
||||
}
|
||||
|
||||
.container .errorPane {
|
||||
background-color: red;
|
||||
margin:5px;
|
||||
padding:5px;
|
||||
display:none;
|
||||
#container .multi-value-control .options button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container .checkboxContainer {
|
||||
margin-top:5px;
|
||||
margin-bottom:5px;
|
||||
font-size:12px;
|
||||
display:flex;
|
||||
#container .multi-value-control .options button:hover {
|
||||
background-color: aliceblue;
|
||||
}
|
||||
|
||||
.container .checkboxContainer:hover {
|
||||
background-color: #dce6f4;
|
||||
}
|
||||
|
||||
.container.identity-picker-container {
|
||||
border: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container .search-control-container {
|
||||
border: 1px #e6e6e6 solid;
|
||||
}
|
||||
|
||||
.container .identity-list-container {
|
||||
margin-top: 5px;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container .identity-list-container .identity-container {
|
||||
width: 150px;
|
||||
display: inline-block;
|
||||
background-color: #d7e6f3;
|
||||
margin: 1px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.container .identity-list-container .identity-container > div {
|
||||
width: 130px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.container .identity-list-container .identity-container .remove-identity {
|
||||
cursor: pointer;
|
||||
#container .multi-value-control .options .checkboxes {
|
||||
padding: 3px;
|
||||
}
|
|
@ -22,7 +22,7 @@
|
|||
VSS.notifyLoadSucceeded();
|
||||
});
|
||||
</script>
|
||||
<div class="container">
|
||||
<div id="container">
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as WitExtensionContracts from "TFS/WorkItemTracking/ExtensionContracts";
|
||||
import { WorkItemFormService } from "TFS/WorkItemTracking/Services";
|
||||
import {BaseMultiValueControl} from "./BaseMultiValueControl";
|
||||
import {MultiValueCombo} from "./MultiValueCombo";
|
||||
import { MultiValueEvents } from "./MultiValueEvents";
|
||||
|
||||
// save on ctr + s
|
||||
$(window).bind("keydown", (event: JQueryEventObject) => {
|
||||
|
@ -13,30 +12,30 @@ $(window).bind("keydown", (event: JQueryEventObject) => {
|
|||
}
|
||||
});
|
||||
|
||||
let control: BaseMultiValueControl;
|
||||
const provider = () => {
|
||||
let control: MultiValueEvents;
|
||||
|
||||
const provider = (): Partial<WitExtensionContracts.IWorkItemNotificationListener> => {
|
||||
const ensureControl = () => {
|
||||
if (!control) {
|
||||
control = new MultiValueCombo();
|
||||
control.initialize();
|
||||
control = new MultiValueEvents();
|
||||
}
|
||||
|
||||
control.invalidate();
|
||||
control.refresh();
|
||||
};
|
||||
|
||||
return {
|
||||
onLoaded: (args: WitExtensionContracts.IWorkItemLoadedArgs) => {
|
||||
ensureControl();
|
||||
},
|
||||
onUnloaded: (args: WitExtensionContracts.IWorkItemChangedArgs) => {
|
||||
if (control) {
|
||||
control.clear();
|
||||
}
|
||||
},
|
||||
// onUnloaded: (args: WitExtensionContracts.IWorkItemChangedArgs) => {
|
||||
// if (control) {
|
||||
// control.clear();
|
||||
// }
|
||||
// },
|
||||
onFieldChanged: (args: WitExtensionContracts.IWorkItemFieldChangedArgs) => {
|
||||
if (control && args.changedFields[control.fieldName] !== undefined && args.changedFields[control.fieldName] !== null) {
|
||||
control.invalidate();
|
||||
if (control && args.changedFields[control.fieldName] !== undefined &&
|
||||
args.changedFields[control.fieldName] !== null
|
||||
) {
|
||||
control.refresh();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "amd",
|
||||
"sourceMap": false,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"target": "es5",
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"jsx": "react",
|
||||
"types": [
|
||||
"q",
|
||||
"knockout",
|
||||
"requirejs",
|
||||
"jquery",
|
||||
"react",
|
||||
"react-dom"
|
||||
],
|
||||
"typeRoots": [
|
||||
"../node_modules/@types"
|
||||
"lib": [
|
||||
"es2015.promise",
|
||||
"dom",
|
||||
"es5"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"manifestVersion": 1.0,
|
||||
"id": "vsts-extensions-multivalue-control",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.24",
|
||||
"name": "Multivalue control",
|
||||
"description": "A work item form control which allows selection of multiple values.",
|
||||
"publisher": "ms-devlabs",
|
||||
|
|
|
@ -14,9 +14,6 @@ module.exports = {
|
|||
},
|
||||
externals: [
|
||||
{
|
||||
"q": true,
|
||||
"react": true,
|
||||
"react-dom": true
|
||||
},
|
||||
/^VSS\/.*/, /^TFS\/.*/, /^q$/
|
||||
],
|
||||
|
@ -25,26 +22,19 @@ module.exports = {
|
|||
moduleExtensions: ["-loader"],
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "ts-loader"
|
||||
use: "ts-loader"
|
||||
},
|
||||
{
|
||||
test: /\.s?css$/,
|
||||
loaders: ["style-loader", "css-loader"]
|
||||
use: ["style-loader", "css-loader"]
|
||||
}
|
||||
]
|
||||
},
|
||||
mode: "development",
|
||||
plugins: [
|
||||
new UglifyJSPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
},
|
||||
output: {
|
||||
comments: false
|
||||
}
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
{ from: "./node_modules/vss-web-extension-sdk/lib/VSS.SDK.min.js", to: "libs/VSS.SDK.min.js" },
|
||||
{ from: "./src/multivalue.html", to: "./" },
|
||||
|
|
Загрузка…
Ссылка в новой задаче