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:
|
repositories:
|
||||||
- repository: pipeline-templates
|
- repository: pipeline-templates
|
||||||
type: git
|
type: git
|
||||||
name: Sage/pipeline-templates
|
name: DevLabs Extensions/pipeline-templates
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -21,6 +21,7 @@
|
||||||
"@types/jsonpath": "^0.2.0",
|
"@types/jsonpath": "^0.2.0",
|
||||||
"@types/react": "^16.4.8",
|
"@types/react": "^16.4.8",
|
||||||
"@types/react-dom": "^16.0.7",
|
"@types/react-dom": "^16.0.7",
|
||||||
|
"azure-devops-extension-sdk": "^4.0.2",
|
||||||
"copy-webpack-plugin": "^9.0.1",
|
"copy-webpack-plugin": "^9.0.1",
|
||||||
"css-loader": "^6.7.1",
|
"css-loader": "^6.7.1",
|
||||||
"jsonpath": "^1.1.1",
|
"jsonpath": "^1.1.1",
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { Checkbox } from "office-ui-fabric-react/lib/components/Checkbox";
|
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 { 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 * as React from "react";
|
||||||
|
|
||||||
import { DelayedFunction } from "VSS/Utils/Core";
|
import { DelayedFunction } from "VSS/Utils/Core";
|
||||||
import { BrowserCheckUtils } from "VSS/Utils/UI";
|
import { BrowserCheckUtils } from "VSS/Utils/UI";
|
||||||
|
import { initializeTheme } from "./theme"
|
||||||
|
|
||||||
interface IMultiValueControlProps {
|
interface IMultiValueControlProps {
|
||||||
selected?: string[];
|
selected?: string[];
|
||||||
|
@ -27,14 +32,24 @@ interface IMultiValueControlState {
|
||||||
multiline: boolean;
|
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 _unfocusedTimeout = BrowserCheckUtils.isSafari() ? 2000 : 1;
|
||||||
private readonly _allowCustom: boolean = VSS.getConfiguration().witInputs.AllowCustom;
|
private readonly _allowCustom: boolean =
|
||||||
private readonly _labelDisplayLength: number = VSS.getConfiguration().witInputs.LabelDisplayLength ? VSS.getConfiguration().witInputs.LabelDisplayLength : 35;
|
VSS?.getConfiguration()?.witInputs?.AllowCustom || false;
|
||||||
private _setUnfocused = new DelayedFunction(null, this._unfocusedTimeout, "", () => {
|
private readonly _labelDisplayLength: number =
|
||||||
|
VSS?.getConfiguration()?.witInputs?.LabelDisplayLength || 35;
|
||||||
|
private _setUnfocused = new DelayedFunction(
|
||||||
|
null,
|
||||||
|
this._unfocusedTimeout,
|
||||||
|
"",
|
||||||
|
() => {
|
||||||
this.setState({ focused: false, filter: "" });
|
this.setState({ focused: false, filter: "" });
|
||||||
});
|
}
|
||||||
|
);
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = { focused: false, filter: "", multiline: false };
|
this.state = { focused: false, filter: "", multiline: false };
|
||||||
|
@ -42,22 +57,51 @@ export class MultiValueControl extends React.Component<IMultiValueControlProps,
|
||||||
public render() {
|
public render() {
|
||||||
const { focused } = this.state;
|
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,
|
const data = (this.props.selected || []).map((text) => {
|
||||||
width: this.props.width || 200,
|
return text.length > Number(this._labelDisplayLength)
|
||||||
onFocus: () => this.setState({ focused: true }),
|
? `${text.slice(0, Number(this._labelDisplayLength))}...`
|
||||||
|
: text;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
width: "100%",
|
||||||
|
margin: 3,
|
||||||
}}
|
}}
|
||||||
onChange={this._onTagsChanged}
|
>
|
||||||
onResolveSuggestions={() => []}
|
{data?.map((t, index) => {
|
||||||
/>
|
return (
|
||||||
{focused ? this._getOptions() : null}
|
<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 className="error">{this.props.error}</div>
|
||||||
</div>;
|
</div>
|
||||||
|
;
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
public componentDidUpdate() {
|
public componentDidUpdate() {
|
||||||
if (this.props.onResize) {
|
if (this.props.onResize) {
|
||||||
|
@ -65,17 +109,20 @@ export class MultiValueControl extends React.Component<IMultiValueControlProps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
initializeTheme()
|
||||||
|
}
|
||||||
|
|
||||||
private _getOptions() {
|
private _getOptions() {
|
||||||
const options = this.props.options;
|
const options = this.props.options;
|
||||||
const selected = (this.props.selected || []).slice(0);
|
const selected = (this.props.selected || []).slice(0);
|
||||||
const filteredOpts = this._filteredOptions();
|
const filteredOpts = this._filteredOptions();
|
||||||
|
|
||||||
return <div className="options">
|
return (
|
||||||
<TextField value={this.state.filter}
|
<div className="options">
|
||||||
|
<TextField
|
||||||
|
className="text"
|
||||||
|
value={this.state.filter}
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder={"Filter values"}
|
placeholder={"Filter values"}
|
||||||
onKeyDown={this._onInputKeyDown}
|
onKeyDown={this._onInputKeyDown}
|
||||||
|
@ -84,12 +131,10 @@ export class MultiValueControl extends React.Component<IMultiValueControlProps,
|
||||||
onChange={this._onInputChange}
|
onChange={this._onInputChange}
|
||||||
multiline={this.state.multiline}
|
multiline={this.state.multiline}
|
||||||
/>
|
/>
|
||||||
<FocusZone
|
<FocusZone direction={FocusZoneDirection.vertical}>
|
||||||
direction={FocusZoneDirection.vertical}
|
{this.state.filter ? null : (
|
||||||
|
|
||||||
>
|
|
||||||
{this.state.filter ? null :
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
className="text"
|
||||||
label="Select All"
|
label="Select All"
|
||||||
checked={selected.join(";") === options.join(";")}
|
checked={selected.join(";") === options.join(";")}
|
||||||
onChange={this._toggleSelectAll}
|
onChange={this._toggleSelectAll}
|
||||||
|
@ -97,24 +142,30 @@ export class MultiValueControl extends React.Component<IMultiValueControlProps,
|
||||||
onBlur: this._onBlur,
|
onBlur: this._onBlur,
|
||||||
onFocus: this._onFocus,
|
onFocus: this._onFocus,
|
||||||
}}
|
}}
|
||||||
/>}
|
/>
|
||||||
{filteredOpts
|
)}
|
||||||
.map((o) => <Checkbox
|
{filteredOpts.map((o) => (
|
||||||
|
<Checkbox
|
||||||
|
className="text"
|
||||||
checked={selected.indexOf(o) >= 0}
|
checked={selected.indexOf(o) >= 0}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
onBlur: this._onBlur,
|
onBlur: this._onBlur,
|
||||||
onFocus: this._onFocus,
|
onFocus: this._onFocus,
|
||||||
}}
|
}}
|
||||||
onChange={() => this._toggleOption(o)}
|
onChange={() => this._toggleOption(o)}
|
||||||
label={this._wrapText(o.length > 30 ? `${o.slice(0, 30)}...` : o)}
|
label={this._wrapText(o)}
|
||||||
title={o}
|
title={o}
|
||||||
/>)}
|
/>
|
||||||
|
))}
|
||||||
</FocusZone>
|
</FocusZone>
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _wrapText(text: string) {
|
private _wrapText(text: string) {
|
||||||
return text.length > this._labelDisplayLength ? `${text.slice(0,this._labelDisplayLength)}...` : text;
|
return text.length > this._labelDisplayLength
|
||||||
|
? `${text.slice(0, this._labelDisplayLength)}...`
|
||||||
|
: text;
|
||||||
}
|
}
|
||||||
private _onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
private _onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.altKey || e.shiftKey || e.ctrlKey) {
|
if (e.altKey || e.shiftKey || e.ctrlKey) {
|
||||||
|
@ -131,10 +182,15 @@ export class MultiValueControl extends React.Component<IMultiValueControlProps,
|
||||||
}
|
}
|
||||||
if (e.keyCode === 37 /* left arrow */) {
|
if (e.keyCode === 37 /* left arrow */) {
|
||||||
const input: HTMLInputElement = e.currentTarget;
|
const input: HTMLInputElement = e.currentTarget;
|
||||||
if (input.selectionStart !== input.selectionEnd || input.selectionStart !== 0) {
|
if (
|
||||||
|
input.selectionStart !== input.selectionEnd ||
|
||||||
|
input.selectionStart !== 0
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tags = document.querySelectorAll("#container .multi-value-control .tag-picker [data-selection-index]");
|
const tags = document.querySelectorAll(
|
||||||
|
"#container .multi-value-control .tag-picker [data-selection-index]"
|
||||||
|
);
|
||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -143,7 +199,7 @@ export class MultiValueControl extends React.Component<IMultiValueControlProps,
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
private _toggleSelectAll = () => {
|
private _toggleSelectAll = () => {
|
||||||
const options = this.props.options;
|
const options = this.props.options;
|
||||||
const selected = this.props.selected || [];
|
const selected = this.props.selected || [];
|
||||||
|
@ -153,42 +209,50 @@ export class MultiValueControl extends React.Component<IMultiValueControlProps,
|
||||||
this._setSelected(options);
|
this._setSelected(options);
|
||||||
}
|
}
|
||||||
this._ifSafariCloseDropdown();
|
this._ifSafariCloseDropdown();
|
||||||
}
|
};
|
||||||
private _filteredOptions = (): string[] => {
|
private _filteredOptions = (): string[] => {
|
||||||
const filter = this.state.filter.toLocaleLowerCase();
|
const filter = this.state.filter.toLocaleLowerCase();
|
||||||
const opts = this._mergeStrArrays([this.props.options, this.props.selected || []]);
|
const opts = this._mergeStrArrays([
|
||||||
|
this.props.options,
|
||||||
|
this.props.selected || [],
|
||||||
|
]);
|
||||||
|
|
||||||
const filtered = [
|
const filtered = [
|
||||||
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) === 0),
|
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) === 0),
|
||||||
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) > 0),
|
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) > 0),
|
||||||
];
|
];
|
||||||
|
|
||||||
const filterEmptyElement = this._allowCustom ? [this.state.filter, ...filtered] : filtered;
|
const filterEmptyElement = this._allowCustom
|
||||||
|
? [this.state.filter, ...filtered]
|
||||||
|
: filtered;
|
||||||
|
|
||||||
return filterEmptyElement.filter(el => el !== "")
|
return filterEmptyElement.filter((el) => el !== "");
|
||||||
}
|
};
|
||||||
private _onInputChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
private _onInputChange = (
|
||||||
|
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
|
newValue?: string
|
||||||
|
) => {
|
||||||
let isMultiline = this.state.multiline;
|
let isMultiline = this.state.multiline;
|
||||||
if (newValue != undefined) {
|
if (newValue != undefined) {
|
||||||
const newMultiline = newValue.length > 50;
|
const newMultiline = newValue.length > 50;
|
||||||
if (newMultiline !== this.state.multiline) {
|
if (newMultiline !== this.state.multiline) {
|
||||||
isMultiline = newMultiline
|
isMultiline = newMultiline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({ filter: newValue || "", multiline: isMultiline });
|
this.setState({ filter: newValue || "", multiline: isMultiline });
|
||||||
}
|
};
|
||||||
private _onBlur = () => {
|
private _onBlur = () => {
|
||||||
this._setUnfocused.reset();
|
this._setUnfocused.reset();
|
||||||
}
|
};
|
||||||
private _onFocus = () => {
|
private _onFocus = () => {
|
||||||
this._setUnfocused.cancel();
|
this._setUnfocused.cancel();
|
||||||
}
|
};
|
||||||
private _setSelected = async (selected: string[]): Promise<void> => {
|
private _setSelected = async (selected: string[]): Promise<void> => {
|
||||||
if (!this.props.onSelectionChanged) {
|
if (!this.props.onSelectionChanged) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.props.onSelectionChanged(selected);
|
await this.props.onSelectionChanged(selected);
|
||||||
}
|
};
|
||||||
private _mergeStrArrays = (arrs: string[][]): string[] => {
|
private _mergeStrArrays = (arrs: string[][]): string[] => {
|
||||||
const seen: { [str: string]: boolean } = {};
|
const seen: { [str: string]: boolean } = {};
|
||||||
const merged: string[] = [];
|
const merged: string[] = [];
|
||||||
|
@ -201,28 +265,34 @@ export class MultiValueControl extends React.Component<IMultiValueControlProps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
}
|
};
|
||||||
private _toggleOption = (option: string): boolean => {
|
private _toggleOption = (option: string): boolean => {
|
||||||
const selectedMap: { [k: string]: boolean } = {};
|
const selectedMap: { [k: string]: boolean } = {};
|
||||||
for (const s of this.props.selected || []) {
|
for (const s of this.props.selected || []) {
|
||||||
selectedMap[s] = true;
|
selectedMap[s] = true;
|
||||||
}
|
}
|
||||||
const change = option in selectedMap || this.props.options.indexOf(option) >= 0;
|
const change =
|
||||||
|
option in selectedMap || this.props.options.indexOf(option) >= 0;
|
||||||
selectedMap[option] = !selectedMap[option];
|
selectedMap[option] = !selectedMap[option];
|
||||||
const selected = this._mergeStrArrays([this.props.options, this.props.selected || [], [option]]).filter((o) => selectedMap[o]);
|
const selected = this._mergeStrArrays([
|
||||||
|
this.props.options,
|
||||||
|
this.props.selected || [],
|
||||||
|
[option],
|
||||||
|
]).filter((o) => selectedMap[o]);
|
||||||
this._setSelected(selected);
|
this._setSelected(selected);
|
||||||
this._ifSafariCloseDropdown();
|
this._ifSafariCloseDropdown();
|
||||||
return change;
|
return change;
|
||||||
}
|
};
|
||||||
private _ifSafariCloseDropdown() {
|
private _ifSafariCloseDropdown() {
|
||||||
if (BrowserCheckUtils.isSafari()) {
|
if (BrowserCheckUtils.isSafari()) {
|
||||||
this.setState({ filter: "", focused: false });
|
this.setState({ filter: "", focused: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private _onTagsChanged = (tags: ITag[]) => {
|
|
||||||
const values = tags.map(({name}) => name);
|
private deleteTags = (tag: string, data: string[]) => {
|
||||||
|
const updatedTags = data.filter((t) => t !== tag);
|
||||||
if (this.props.onSelectionChanged) {
|
if (this.props.onSelectionChanged) {
|
||||||
this.props.onSelectionChanged(values);
|
this.props.onSelectionChanged(updatedTags);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,35 @@
|
||||||
color: rgb($defaultR, $defaultG, $defaultB);
|
color: rgb($defaultR, $defaultG, $defaultB);
|
||||||
color: rgb(var($varName, $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 {
|
#container {
|
||||||
padding-bottom: 3px;
|
padding-bottom: 3px;
|
||||||
padding-right: 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);
|
||||||
|
}
|
||||||
|
};
|
Загрузка…
Ссылка в новой задаче