* 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:
AminTi 2024-10-24 10:49:50 +02:00 коммит произвёл GitHub
Родитель cb0d633ab9
Коммит ea6ee99d1a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
7 изменённых файлов: 7664 добавлений и 221 удалений

Просмотреть файл

@ -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:
@ -87,4 +87,4 @@ stages:
publisherId: $(publisherId) publisherId: $(publisherId)
publicExtensionName: $(publicExtensionName) publicExtensionName: $(publicExtensionName)
extensionVisibility: 'public' extensionVisibility: 'public'
updateTaskVersion: true updateTaskVersion: true

7337
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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,228 +1,298 @@
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[];
width?: number; width?: number;
readOnly?: boolean; readOnly?: boolean;
placeholder?: string; placeholder?: string;
noResultsFoundText?: string; noResultsFoundText?: string;
searchingText?: string; searchingText?: string;
onSelectionChanged?: (selection: string[]) => Promise<void>; onSelectionChanged?: (selection: string[]) => Promise<void>;
forceValue?: boolean; forceValue?: boolean;
options: string[]; options: string[];
error: JSX.Element; error: JSX.Element;
onBlurred?: () => void; onBlurred?: () => void;
onResize?: () => void; onResize?: () => void;
} }
interface IMultiValueControlState { interface IMultiValueControlState {
focused: boolean; focused: boolean;
filter: string; filter: string;
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 _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, "", () => { const data = (this.props.selected || []).map((text) => {
this.setState({focused: false, filter: ""}); 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" : ""}`}> componentDidMount() {
<TagPicker initializeTheme()
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();
}
}
private _getOptions() {
const options = this.props.options;
const selected = (this.props.selected || []).slice(0);
const filteredOpts = this._filteredOptions();
return (
<div className="options">
<TextField
private _getOptions() { className="text"
const options = this.props.options; value={this.state.filter}
const selected = (this.props.selected || []).slice(0); autoFocus
const filteredOpts = this._filteredOptions(); placeholder={"Filter values"}
onKeyDown={this._onInputKeyDown}
return <div className="options"> onBlur={this._onBlur}
<TextField value={this.state.filter} onFocus={this._onFocus}
autoFocus onChange={this._onInputChange}
placeholder={"Filter values"} multiline={this.state.multiline}
onKeyDown={this._onInputKeyDown} />
onBlur={this._onBlur} <FocusZone direction={FocusZoneDirection.vertical}>
onFocus={this._onFocus} {this.state.filter ? null : (
onChange={this._onInputChange} <Checkbox
multiline={this.state.multiline} className="text"
label="Select All"
checked={selected.join(";") === options.join(";")}
onChange={this._toggleSelectAll}
inputProps={{
onBlur: this._onBlur,
onFocus: this._onFocus,
}}
/> />
<FocusZone )}
direction={FocusZoneDirection.vertical} {filteredOpts.map((o) => (
<Checkbox
> className="text"
{this.state.filter ? null : checked={selected.indexOf(o) >= 0}
<Checkbox inputProps={{
label="Select All" onBlur: this._onBlur,
checked={selected.join(";") === options.join(";")} onFocus: this._onFocus,
onChange={this._toggleSelectAll} }}
inputProps={{ onChange={() => this._toggleOption(o)}
onBlur: this._onBlur, label={this._wrapText(o)}
onFocus: this._onFocus, title={o}
}} />
/>} ))}
{filteredOpts </FocusZone>
.map((o) => <Checkbox </div>
checked={selected.indexOf(o) >= 0} );
inputProps={{ }
onBlur: this._onBlur,
onFocus: this._onFocus, private _wrapText(text: string) {
}} return text.length > this._labelDisplayLength
onChange={() => this._toggleOption(o)} ? `${text.slice(0, this._labelDisplayLength)}...`
label={this._wrapText(o.length > 30 ? `${o.slice(0, 30)}...` : o)} : text;
title={o} }
/>)} private _onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
</FocusZone> if (e.altKey || e.shiftKey || e.ctrlKey) {
</div>; return;
} }
private _wrapText(text: string){ if (e.keyCode === 13 /* enter */) {
return text.length > this._labelDisplayLength ? `${text.slice(0,this._labelDisplayLength)}...` : text; const filtered = this._filteredOptions();
}
private _onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.altKey || e.shiftKey || e.ctrlKey) {
return;
}
if (e.keyCode === 13 /* enter */) { e.preventDefault();
const filtered = this._filteredOptions(); e.stopPropagation();
this._toggleOption(filtered[0]);
e.preventDefault(); this.setState({ filter: "" });
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();
}
} }
private _toggleSelectAll = () => { if (e.keyCode === 37 /* left arrow */) {
const options = this.props.options; const input: HTMLInputElement = e.currentTarget;
const selected = this.props.selected || []; if (
if (selected.join(";") === options.join(";")) { input.selectionStart !== input.selectionEnd ||
this._setSelected([]); input.selectionStart !== 0
} else { ) {
this._setSelected(options); return;
} }
this._ifSafariCloseDropdown(); 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(); private _toggleSelectAll = () => {
const opts = this._mergeStrArrays([this.props.options, this.props.selected || []]); const options = this.props.options;
const selected = this.props.selected || [];
const filtered = [ if (selected.join(";") === options.join(";")) {
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) === 0), this._setSelected([]);
...opts.filter((o) => o.toLocaleLowerCase().indexOf(filter) > 0), } 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) => { this.setState({ filter: newValue || "", multiline: isMultiline });
let isMultiline = this.state.multiline; };
if(newValue != undefined ){ private _onBlur = () => {
const newMultiline = newValue.length > 50; this._setUnfocused.reset();
if (newMultiline !== this.state.multiline) { };
isMultiline = newMultiline 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 = () => { return merged;
this._setUnfocused.reset(); };
private _toggleOption = (option: string): boolean => {
const selectedMap: { [k: string]: boolean } = {};
for (const s of this.props.selected || []) {
selectedMap[s] = true;
} }
private _onFocus = () => { const change =
this._setUnfocused.cancel(); 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; private deleteTags = (tag: string, data: string[]) => {
} const updatedTags = data.filter((t) => t !== tag);
await this.props.onSelectionChanged(selected); if (this.props.onSelectionChanged) {
} this.props.onSelectionChanged(updatedTags);
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);
}
} }
};
} }

Просмотреть файл

@ -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;

17
src/theme.ts Normal file
Просмотреть файл

@ -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
});
});
});
});
};

38
src/themeManager.ts Normal file
Просмотреть файл

@ -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);
}
};