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

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

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

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

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

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