Create new tag picker (#319)
* make-checkbox-values-ellipsis-revert (#283) * Update azure-pipelines reference to template repo. * create-new-tag-picker * create-new-tag-picker * tag style * dark theme * minor fixes --------- Co-authored-by: Mathias Olausson <mathias@olausson.net>
This commit is contained in:
Родитель
cb0d633ab9
Коммит
ea6ee99d1a
|
@ -24,7 +24,7 @@ resources:
|
|||
repositories:
|
||||
- repository: pipeline-templates
|
||||
type: git
|
||||
name: Sage/pipeline-templates
|
||||
name: DevLabs Extensions/pipeline-templates
|
||||
ref: main
|
||||
|
||||
stages:
|
||||
|
@ -87,4 +87,4 @@ stages:
|
|||
publisherId: $(publisherId)
|
||||
publicExtensionName: $(publicExtensionName)
|
||||
extensionVisibility: 'public'
|
||||
updateTaskVersion: true
|
||||
updateTaskVersion: true
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -21,6 +21,7 @@
|
|||
"@types/jsonpath": "^0.2.0",
|
||||
"@types/react": "^16.4.8",
|
||||
"@types/react-dom": "^16.0.7",
|
||||
"azure-devops-extension-sdk": "^4.0.2",
|
||||
"copy-webpack-plugin": "^9.0.1",
|
||||
"css-loader": "^6.7.1",
|
||||
"jsonpath": "^1.1.1",
|
||||
|
|
|
@ -1,228 +1,298 @@
|
|||
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 {
|
||||
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";
|
||||
import { initializeTheme } from "./theme"
|
||||
|
||||
interface IMultiValueControlProps {
|
||||
selected?: string[];
|
||||
width?: number;
|
||||
readOnly?: boolean;
|
||||
placeholder?: string;
|
||||
noResultsFoundText?: string;
|
||||
searchingText?: string;
|
||||
onSelectionChanged?: (selection: string[]) => Promise<void>;
|
||||
forceValue?: boolean;
|
||||
options: string[];
|
||||
error: JSX.Element;
|
||||
onBlurred?: () => void;
|
||||
onResize?: () => void;
|
||||
selected?: string[];
|
||||
width?: number;
|
||||
readOnly?: boolean;
|
||||
placeholder?: string;
|
||||
noResultsFoundText?: string;
|
||||
searchingText?: string;
|
||||
onSelectionChanged?: (selection: string[]) => Promise<void>;
|
||||
forceValue?: boolean;
|
||||
options: string[];
|
||||
error: JSX.Element;
|
||||
onBlurred?: () => void;
|
||||
onResize?: () => void;
|
||||
}
|
||||
|
||||
interface IMultiValueControlState {
|
||||
focused: boolean;
|
||||
filter: string;
|
||||
multiline: boolean;
|
||||
focused: boolean;
|
||||
filter: string;
|
||||
multiline: boolean;
|
||||
}
|
||||
|
||||
export class MultiValueControl extends React.Component<IMultiValueControlProps, IMultiValueControlState> {
|
||||
export class MultiValueControl extends React.Component<
|
||||
IMultiValueControlProps,
|
||||
IMultiValueControlState
|
||||
> {
|
||||
|
||||
private readonly _unfocusedTimeout = BrowserCheckUtils.isSafari() ? 2000 : 1;
|
||||
private readonly _allowCustom: boolean =
|
||||
VSS?.getConfiguration()?.witInputs?.AllowCustom || false;
|
||||
private readonly _labelDisplayLength: number =
|
||||
VSS?.getConfiguration()?.witInputs?.LabelDisplayLength || 35;
|
||||
private _setUnfocused = new DelayedFunction(
|
||||
null,
|
||||
this._unfocusedTimeout,
|
||||
"",
|
||||
() => {
|
||||
this.setState({ focused: false, filter: "" });
|
||||
}
|
||||
);
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = { focused: false, filter: "", multiline: false };
|
||||
}
|
||||
public render() {
|
||||
const { focused } = this.state;
|
||||
|
||||
|
||||
|
||||
|
||||
private readonly _unfocusedTimeout = BrowserCheckUtils.isSafari() ? 2000 : 1;
|
||||
private readonly _allowCustom: boolean = VSS.getConfiguration().witInputs.AllowCustom;
|
||||
private readonly _labelDisplayLength: number = VSS.getConfiguration().witInputs.LabelDisplayLength ? VSS.getConfiguration().witInputs.LabelDisplayLength : 35;
|
||||
private _setUnfocused = new DelayedFunction(null, this._unfocusedTimeout, "", () => {
|
||||
this.setState({focused: false, filter: ""});
|
||||
|
||||
|
||||
|
||||
const data = (this.props.selected || []).map((text) => {
|
||||
return text.length > Number(this._labelDisplayLength)
|
||||
? `${text.slice(0, Number(this._labelDisplayLength))}...`
|
||||
: text;
|
||||
});
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = { focused: false, filter: "", multiline: false };
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
width: "100%",
|
||||
margin: 3,
|
||||
}}
|
||||
>
|
||||
{data?.map((t, index) => {
|
||||
return (
|
||||
<div className="container">
|
||||
<div>{t.match(/.{1,50}/g)?.join("\n")}</div>
|
||||
<div
|
||||
className="close-icon"
|
||||
onClick={() => this.deleteTags(t, data)}
|
||||
>
|
||||
X
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={`multi-value-control ${focused ? "focused" : ""}`}>
|
||||
{this._getOptions()}
|
||||
<div className="error">{this.props.error}</div>
|
||||
</div>
|
||||
;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
public componentDidUpdate() {
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize();
|
||||
}
|
||||
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 className="error">{this.props.error}</div>
|
||||
</div>;
|
||||
}
|
||||
public componentDidUpdate() {
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize();
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
initializeTheme()
|
||||
}
|
||||
|
||||
private _getOptions() {
|
||||
const options = this.props.options;
|
||||
const selected = (this.props.selected || []).slice(0);
|
||||
const filteredOpts = this._filteredOptions();
|
||||
|
||||
|
||||
|
||||
|
||||
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}
|
||||
multiline={this.state.multiline}
|
||||
return (
|
||||
<div className="options">
|
||||
<TextField
|
||||
className="text"
|
||||
value={this.state.filter}
|
||||
autoFocus
|
||||
placeholder={"Filter values"}
|
||||
onKeyDown={this._onInputKeyDown}
|
||||
onBlur={this._onBlur}
|
||||
onFocus={this._onFocus}
|
||||
onChange={this._onInputChange}
|
||||
multiline={this.state.multiline}
|
||||
/>
|
||||
<FocusZone direction={FocusZoneDirection.vertical}>
|
||||
{this.state.filter ? null : (
|
||||
<Checkbox
|
||||
className="text"
|
||||
label="Select All"
|
||||
checked={selected.join(";") === options.join(";")}
|
||||
onChange={this._toggleSelectAll}
|
||||
inputProps={{
|
||||
onBlur: this._onBlur,
|
||||
onFocus: this._onFocus,
|
||||
}}
|
||||
/>
|
||||
<FocusZone
|
||||
direction={FocusZoneDirection.vertical}
|
||||
|
||||
>
|
||||
{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={this._wrapText(o.length > 30 ? `${o.slice(0, 30)}...` : o)}
|
||||
title={o}
|
||||
/>)}
|
||||
</FocusZone>
|
||||
</div>;
|
||||
)}
|
||||
{filteredOpts.map((o) => (
|
||||
<Checkbox
|
||||
className="text"
|
||||
checked={selected.indexOf(o) >= 0}
|
||||
inputProps={{
|
||||
onBlur: this._onBlur,
|
||||
onFocus: this._onFocus,
|
||||
}}
|
||||
onChange={() => this._toggleOption(o)}
|
||||
label={this._wrapText(o)}
|
||||
title={o}
|
||||
/>
|
||||
))}
|
||||
</FocusZone>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _wrapText(text: string) {
|
||||
return text.length > this._labelDisplayLength
|
||||
? `${text.slice(0, this._labelDisplayLength)}...`
|
||||
: text;
|
||||
}
|
||||
private _onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.altKey || e.shiftKey || e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _wrapText(text: string){
|
||||
return text.length > this._labelDisplayLength ? `${text.slice(0,this._labelDisplayLength)}...` : text;
|
||||
}
|
||||
private _onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.altKey || e.shiftKey || e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 13 /* enter */) {
|
||||
const filtered = this._filteredOptions();
|
||||
|
||||
if (e.keyCode === 13 /* enter */) {
|
||||
const filtered = this._filteredOptions();
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._toggleOption(filtered[0]);
|
||||
this.setState({filter: ""});
|
||||
}
|
||||
if (e.keyCode === 37 /* left arrow */) {
|
||||
const input: HTMLInputElement = e.currentTarget;
|
||||
if (input.selectionStart !== input.selectionEnd || input.selectionStart !== 0) {
|
||||
return;
|
||||
}
|
||||
const tags = document.querySelectorAll("#container .multi-value-control .tag-picker [data-selection-index]");
|
||||
if (tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
const lastTag = tags.item(tags.length - 1) as HTMLDivElement;
|
||||
lastTag.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
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();
|
||||
if (e.keyCode === 37 /* left arrow */) {
|
||||
const input: HTMLInputElement = e.currentTarget;
|
||||
if (
|
||||
input.selectionStart !== input.selectionEnd ||
|
||||
input.selectionStart !== 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const tags = document.querySelectorAll(
|
||||
"#container .multi-value-control .tag-picker [data-selection-index]"
|
||||
);
|
||||
if (tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
const lastTag = tags.item(tags.length - 1) as HTMLDivElement;
|
||||
lastTag.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
private _filteredOptions = (): string[] => {
|
||||
const filter = this.state.filter.toLocaleLowerCase();
|
||||
const opts = this._mergeStrArrays([this.props.options, this.props.selected || []]);
|
||||
|
||||
const filtered = [
|
||||
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) === 0),
|
||||
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) > 0),
|
||||
];
|
||||
};
|
||||
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 = (): string[] => {
|
||||
const filter = this.state.filter.toLocaleLowerCase();
|
||||
const opts = this._mergeStrArrays([
|
||||
this.props.options,
|
||||
this.props.selected || [],
|
||||
]);
|
||||
|
||||
const filterEmptyElement = this._allowCustom ? [this.state.filter, ...filtered] : filtered;
|
||||
const filtered = [
|
||||
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) === 0),
|
||||
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) > 0),
|
||||
];
|
||||
|
||||
return filterEmptyElement.filter(el => el !== "")
|
||||
const filterEmptyElement = this._allowCustom
|
||||
? [this.state.filter, ...filtered]
|
||||
: filtered;
|
||||
|
||||
return filterEmptyElement.filter((el) => el !== "");
|
||||
};
|
||||
private _onInputChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => {
|
||||
let isMultiline = this.state.multiline;
|
||||
if (newValue != undefined) {
|
||||
const newMultiline = newValue.length > 50;
|
||||
if (newMultiline !== this.state.multiline) {
|
||||
isMultiline = newMultiline;
|
||||
}
|
||||
}
|
||||
private _onInputChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
let isMultiline = this.state.multiline;
|
||||
if(newValue != undefined ){
|
||||
const newMultiline = newValue.length > 50;
|
||||
if (newMultiline !== this.state.multiline) {
|
||||
isMultiline = newMultiline
|
||||
}
|
||||
this.setState({ filter: newValue || "", multiline: isMultiline });
|
||||
};
|
||||
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 _mergeStrArrays = (arrs: string[][]): string[] => {
|
||||
const seen: { [str: string]: boolean } = {};
|
||||
const merged: string[] = [];
|
||||
for (const arr of arrs) {
|
||||
for (const ele of arr) {
|
||||
if (!seen[ele]) {
|
||||
seen[ele] = true;
|
||||
merged.push(ele);
|
||||
}
|
||||
this.setState({filter: newValue || "", multiline: isMultiline});
|
||||
}
|
||||
}
|
||||
private _onBlur = () => {
|
||||
this._setUnfocused.reset();
|
||||
return merged;
|
||||
};
|
||||
private _toggleOption = (option: string): boolean => {
|
||||
const selectedMap: { [k: string]: boolean } = {};
|
||||
for (const s of this.props.selected || []) {
|
||||
selectedMap[s] = true;
|
||||
}
|
||||
private _onFocus = () => {
|
||||
this._setUnfocused.cancel();
|
||||
const change =
|
||||
option in selectedMap || this.props.options.indexOf(option) >= 0;
|
||||
selectedMap[option] = !selectedMap[option];
|
||||
const selected = this._mergeStrArrays([
|
||||
this.props.options,
|
||||
this.props.selected || [],
|
||||
[option],
|
||||
]).filter((o) => selectedMap[o]);
|
||||
this._setSelected(selected);
|
||||
this._ifSafariCloseDropdown();
|
||||
return change;
|
||||
};
|
||||
private _ifSafariCloseDropdown() {
|
||||
if (BrowserCheckUtils.isSafari()) {
|
||||
this.setState({ filter: "", focused: false });
|
||||
}
|
||||
private _setSelected = async (selected: string[]): Promise<void> => {
|
||||
if (!this.props.onSelectionChanged) {
|
||||
return;
|
||||
}
|
||||
await this.props.onSelectionChanged(selected);
|
||||
}
|
||||
private _mergeStrArrays = (arrs: string[][]): string[] => {
|
||||
const seen: {[str: string]: boolean} = {};
|
||||
const merged: string[] = [];
|
||||
for (const arr of arrs) {
|
||||
for (const ele of arr) {
|
||||
if (!seen[ele]) {
|
||||
seen[ele] = true;
|
||||
merged.push(ele);
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
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._mergeStrArrays([this.props.options, this.props.selected || [], [option]]).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);
|
||||
}
|
||||
}
|
||||
|
||||
private deleteTags = (tag: string, data: string[]) => {
|
||||
const updatedTags = data.filter((t) => t !== tag);
|
||||
if (this.props.onSelectionChanged) {
|
||||
this.props.onSelectionChanged(updatedTags);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,7 +11,35 @@
|
|||
color: rgb($defaultR, $defaultG, $defaultB);
|
||||
color: rgb(var($varName, $defaultR, $defaultG, $defaultB));
|
||||
}
|
||||
.close-icon{
|
||||
cursor: pointer;
|
||||
|
||||
font-size:12px ;
|
||||
padding: 0px 5px;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
padding: 3px 5px;
|
||||
|
||||
background-color:#e1e1e1;
|
||||
color: black;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 20px; // Consolidated to one line to avoid redundancy
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tag-picker{
|
||||
visibility: hidden;
|
||||
}
|
||||
#container {
|
||||
padding-bottom: 3px;
|
||||
padding-right: 3px;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
// themeManager.ts
|
||||
import * as SDK from 'azure-devops-extension-sdk';
|
||||
|
||||
export const initializeTheme = () => {
|
||||
SDK.init().then(() => {
|
||||
// Assuming there's a global VSS object available
|
||||
VSS.require(["TFS/Dashboards/WidgetHelpers"], (WidgetHelpers) => {
|
||||
WidgetHelpers.ThemeService.getService().then((themeService) => {
|
||||
themeService.getTheme().then((theme) => {
|
||||
// Apply your theme logic here
|
||||
// This is a hypothetical example; actual implementation may vary
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
// theme.ts
|
||||
|
||||
import { createTheme, loadTheme } from 'office-ui-fabric-react';
|
||||
|
||||
// Define your dark and light themes here
|
||||
export const darkTheme = createTheme({
|
||||
palette: {
|
||||
themePrimary: '#1a1a1a',
|
||||
neutralPrimary: '#f4f4f4',
|
||||
neutralLighter: '#262626',
|
||||
neutralLight: '#333333',
|
||||
neutralQuaternary: '#444444',
|
||||
white: '#121212',
|
||||
neutralTertiaryAlt: '#e1e1e1'
|
||||
},
|
||||
// Add more theme settings if needed
|
||||
});
|
||||
|
||||
export const lightTheme = createTheme({
|
||||
palette: {
|
||||
themePrimary: '#0078d4',
|
||||
neutralPrimary: '#333333',
|
||||
neutralLighter: '#f4f4f4',
|
||||
neutralLight: '#eaeaea',
|
||||
neutralQuaternary: '#dcdcdc',
|
||||
white: '#ffffff',
|
||||
neutralTertiaryAlt: '#e1e1e1',
|
||||
},
|
||||
});
|
||||
|
||||
// Function to apply the theme
|
||||
export const applyTheme = (theme: string) => {
|
||||
if (theme === 'dark') {
|
||||
loadTheme(darkTheme);
|
||||
} else {
|
||||
loadTheme(lightTheme);
|
||||
}
|
||||
};
|
Загрузка…
Ссылка в новой задаче