* 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:

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

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