Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WC-2741]: Combobox: Handle multi select selection order #1386

Merged
merged 13 commits into from
Feb 4, 2025
Merged
8 changes: 8 additions & 0 deletions packages/pluggableWidgets/combobox-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- We added a new "Selected items sorting" property to set the display order of the selected items. "Caption" based sorting display for selected items now possible for combobox with multiple selection.

### Changed

- Database multi selection will no longer display selected value sorted by the caption. The default behavior is to display them based on selection order, unless the new "Selected items sorting" configurations are set to "Caption".

## [2.1.4] - 2025-01-29

### Fixed
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/pluggableWidgets/combobox-web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mendix/combobox-web",
"widgetName": "Combobox",
"version": "2.1.4",
"version": "2.2.0",
"description": "Configurable Combo box widget with suggestions and autocomplete.",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function getProperties(values: ComboboxPreviewProps, defaultProperties: P
"selectionMethod",
"selectAllButton",
"selectAllButtonCaption",
"selectedItemsSorting",
...ASSOCIATION_SOURCE_CONFIG,
...LAZY_LOADING_CONFIG
]);
Expand Down Expand Up @@ -123,6 +124,8 @@ export function getProperties(values: ComboboxPreviewProps, defaultProperties: P
"optionsSourceDatabaseValueAttribute",
"databaseAttributeString"
]);
} else {
hidePropertiesIn(defaultProperties, values, ["selectedItemsSorting"]);
}
if (values.databaseAttributeString.length === 0) {
hidePropertiesIn(defaultProperties, values, ["optionsSourceDatabaseValueAttribute"]);
Expand Down
34 changes: 23 additions & 11 deletions packages/pluggableWidgets/combobox-web/src/Combobox.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@
<attributeType name="Enum" />
</attributeTypes>
</property>
<!-- END DATABASE / STRING -->
</propertyGroup>
<!-- END DATABASE / STRING -->
<propertyGroup caption="Attribute">
<!-- ASSOCIATION -->
<property key="attributeAssociation" type="association" selectableObjects="optionsSourceAssociationDataSource" required="true" setLabel="true">
Expand Down Expand Up @@ -188,16 +188,6 @@
<description />
</property>
<!-- MISC PROPS -->
<property key="filterType" type="enumeration" defaultValue="contains">
<caption>Filter type</caption>
<description />
<enumerationValues>
<enumerationValue key="contains">Contains (fuzzy)</enumerationValue>
<enumerationValue key="containsExact">Contains (exact)</enumerationValue>
<enumerationValue key="startsWith">Starts-with</enumerationValue>
<enumerationValue key="none">None</enumerationValue>
</enumerationValues>
</property>
<property key="noOptionsText" type="textTemplate" required="false">
<caption>No options text</caption>
<description />
Expand Down Expand Up @@ -393,6 +383,28 @@
</enumerationValues>
</property>
</propertyGroup>
<propertyGroup caption="Multiple-selection">
<property key="selectedItemsSorting" type="enumeration" defaultValue="none" required="true">
<caption>Selected items sorting</caption>
<description>How selected items should be sorted.</description>
<enumerationValues>
<enumerationValue key="caption">Caption</enumerationValue>
<enumerationValue key="none">Default</enumerationValue>
</enumerationValues>
</property>
</propertyGroup>
<propertyGroup caption="Filter">
<property key="filterType" type="enumeration" defaultValue="contains">
<caption>Filter type</caption>
<description />
<enumerationValues>
<enumerationValue key="contains">Contains (fuzzy)</enumerationValue>
<enumerationValue key="containsExact">Contains (exact)</enumerationValue>
<enumerationValue key="startsWith">Starts-with</enumerationValue>
<enumerationValue key="none">None</enumerationValue>
</enumerationValues>
</property>
</propertyGroup>
</propertyGroup>
</properties>
</widget>
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ describe("Combo box (Association)", () => {
staticDataSourceCustomContent: undefined,
staticDataSourceCaption: dynamic("caption2")
}
]
],
selectedItemsSorting: "none"
};
if (defaultProps.optionsSourceAssociationCaptionType === "expression") {
defaultProps.optionsSourceAssociationCaptionExpression!.get = i => dynamic(`${i.id}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ describe("Combo box (Association)", () => {
staticDataSourceCustomContent: undefined,
staticDataSourceCaption: dynamic("caption2")
}
]
],
selectedItemsSorting: "none"
};
if (defaultProps.optionsSourceAssociationCaptionType === "expression") {
defaultProps.optionsSourceAssociationCaptionExpression!.get = i => dynamic(`${i.id}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ describe("Combo box (Static values)", () => {
staticDataSourceCustomContent: undefined,
staticDataSourceCaption: dynamic("caption2")
}
]
],
selectedItemsSorting: "none"
};
if (defaultProps.optionsSourceAssociationCaptionType === "expression") {
defaultProps.optionsSourceAssociationCaptionExpression!.get = i => dynamic(`${i.id}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { ThreeStateCheckBoxEnum } from "@mendix/widget-plugin-component-kit/ThreeStateCheckBox";
import { ReferenceSetValue } from "mendix";
import { ComboboxContainerProps, SelectedItemsStyleEnum, SelectionMethodEnum } from "../../../typings/ComboboxProps";
import {
ComboboxContainerProps,
SelectedItemsSortingEnum,
SelectedItemsStyleEnum,
SelectionMethodEnum
} from "../../../typings/ComboboxProps";
import { MultiSelector } from "../types";
import { sortSelectedItems } from "../utils";
import { BaseAssociationSelector } from "./BaseAssociationSelector";
import { ThreeStateCheckBoxEnum } from "@mendix/widget-plugin-component-kit/ThreeStateCheckBox";

export class AssociationMultiSelector
extends BaseAssociationSelector<string[], ReferenceSetValue>
Expand All @@ -12,12 +18,19 @@ export class AssociationMultiSelector
selectedItemsStyle: SelectedItemsStyleEnum = "text";
selectionMethod: SelectionMethodEnum = "checkbox";
selectAllButton = false;
selectedItemsSorting: SelectedItemsSortingEnum = "none";

updateProps(props: ComboboxContainerProps): void {
super.updateProps(props);
this.selectedItemsStyle = props.selectedItemsStyle;
this.selectionMethod = props.selectionMethod;
this.selectAllButton = props.selectAllButton;
this.currentId = this._attr?.value?.map(v => v.id) ?? null;
this.selectedItemsSorting = props.selectedItemsSorting;

this.currentId = sortSelectedItems(this._attr?.value, this.selectedItemsSorting, this.options.sortOrder, id =>
this.caption.get(id)
);

if (this.selectionMethod === "rowclick" || this.customContentType === "yes") {
this.selectedItemsStyle = "boxes";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {
ComboboxContainerProps,
LoadingTypeEnum,
OptionsSourceAssociationCustomContentTypeEnum,
SelectedItemsSortingEnum,
SelectedItemsStyleEnum,
SelectionMethodEnum
} from "../../../typings/ComboboxProps";
import { LazyLoadProvider } from "../LazyLoadProvider";
import { MultiSelector, Status } from "../types";
import { sortSelectedItems } from "../utils";
import { DatabaseCaptionsProvider } from "./DatabaseCaptionsProvider";
import { DatabaseOptionsProvider } from "./DatabaseOptionsProvider";
import { extractDatabaseProps } from "./utils";
Expand All @@ -30,6 +32,7 @@ export class DatabaseMultiSelectionSelector implements MultiSelector {
type = "multi" as const;
protected lazyLoader: LazyLoadProvider = new LazyLoadProvider();
private _objectsMap: Map<string, ObjectItem> = new Map();
selectedItemsSorting: SelectedItemsSortingEnum = "none";

constructor() {
this.caption = new DatabaseCaptionsProvider(this._objectsMap);
Expand Down Expand Up @@ -119,7 +122,14 @@ export class DatabaseMultiSelectionSelector implements MultiSelector {
this.selectedItemsStyle = props.selectedItemsStyle;
this.selection = props.optionsSourceDatabaseItemSelection as SelectionMultiValue;
this.selectionMethod = props.selectionMethod;
this.currentId = this.selection?.selection.map(v => v.id) ?? null;
this.selectedItemsSorting = props.selectedItemsSorting;

this.currentId = sortSelectedItems(
this.selection?.selection,
this.selectedItemsSorting,
this.options.sortOrder,
id => this.caption.get(id)
);
}

setValue(value: string[] | null): void {
Expand Down
2 changes: 2 additions & 0 deletions packages/pluggableWidgets/combobox-web/src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
LoadingTypeEnum,
OptionsSourceAssociationCustomContentTypeEnum,
ReadOnlyStyleEnum,
SelectedItemsSortingEnum,
SelectedItemsStyleEnum,
SelectionMethodEnum
} from "../../typings/ComboboxProps";
Expand Down Expand Up @@ -85,6 +86,7 @@ export interface MultiSelector extends SelectorBase<"multi", string[]> {
selectedItemsStyle: SelectedItemsStyleEnum;
selectionMethod: SelectionMethodEnum;
selectAllButton: boolean;
selectedItemsSorting: SelectedItemsSortingEnum;
getOptions(): string[];
isOptionsSelected(): ThreeStateCheckBoxEnum;
}
Expand Down
45 changes: 40 additions & 5 deletions packages/pluggableWidgets/combobox-web/src/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Big } from "big.js";
import { MatchSorterOptions, matchSorter } from "match-sorter";
import { PropsWithChildren, ReactElement, createElement } from "react";
import { ComboboxPreviewProps, FilterTypeEnum } from "typings/ComboboxProps";
import { MultiSelector } from "./types";
import { ComboboxPreviewProps, FilterTypeEnum, SelectedItemsSortingEnum } from "typings/ComboboxProps";
import { MultiSelector, SortOrder } from "./types";
import { ObjectItem } from "mendix";

export const DEFAULT_LIMIT_SIZE = 100;

Expand All @@ -21,9 +22,9 @@ export function getSelectedCaptionsPlaceholder(selector: MultiSelector, selected
return "";
}

const selected = selectedItems.map(v => selector.caption.get(v)).sort();
const sortedSelected: string[] = selector.options.sortOrder === "desc" ? selected.reverse() : selected;
return sortedSelected.join(", ");
const selected = selectedItems.map(v => selector.caption.get(v));

return selected.join(", ");
}

export interface CaptionContentProps extends PropsWithChildren {
Expand Down Expand Up @@ -114,3 +115,37 @@ export function _valuesIsEqual(valueA: ValueType, valueB: ValueType): boolean {
}
return valueA === valueB;
}

export function sortSelectedItems(
values: ObjectItem[] | null | undefined,
sortingType: SelectedItemsSortingEnum,
sortOrder: SortOrder,
captionGetter: (id: string) => string | undefined
): string[] | null {
if (values) {
return sortSelections(
values.map(v => (v?.id as string) ?? null),
sortingType,
sortOrder,
captionGetter
);
} else {
return null;
}
}

function sortSelections(
newValueIds: string[],
sortingType: SelectedItemsSortingEnum,
sortOrder: SortOrder,
captionGetter: (id: string) => string | undefined
): string[] {
if (sortingType === "caption") {
return newValueIds.sort((a, b) => {
const captionA = captionGetter(a)?.toString() ?? "";
const captionB = captionGetter(b)?.toString() ?? "";
return sortOrder === "asc" ? captionA.localeCompare(captionB) : captionB.localeCompare(captionA);
});
}
return newValueIds;
}
2 changes: 1 addition & 1 deletion packages/pluggableWidgets/combobox-web/src/package.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<package xmlns="http://www.mendix.com/package/1.0/">
<clientModule name="Combobox" version="2.1.4" xmlns="http://www.mendix.com/clientModule/1.0/">
<clientModule name="Combobox" version="2.2.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<widgetFiles>
<widgetFile path="Combobox.xml" />
</widgetFiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export interface OptionsSourceStaticDataSourceType {
staticDataSourceCaption: DynamicValue<string>;
}

export type FilterTypeEnum = "contains" | "containsExact" | "startsWith" | "none";

export type OptionsSourceAssociationCustomContentTypeEnum = "yes" | "listItem" | "no";

export type OptionsSourceDatabaseCustomContentTypeEnum = "yes" | "listItem" | "no";
Expand All @@ -37,6 +35,10 @@ export type ReadOnlyStyleEnum = "bordered" | "text";

export type LoadingTypeEnum = "spinner" | "skeleton";

export type SelectedItemsSortingEnum = "caption" | "none";

export type FilterTypeEnum = "contains" | "containsExact" | "startsWith" | "none";

export interface OptionsSourceStaticDataSourcePreviewType {
staticDataSourceValue: string;
staticDataSourceCustomContent: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
Expand Down Expand Up @@ -66,7 +68,6 @@ export interface ComboboxContainerProps {
staticAttribute: EditableValue<string | Big | boolean | Date>;
optionsSourceStaticDataSource: OptionsSourceStaticDataSourceType[];
emptyOptionText?: DynamicValue<string>;
filterType: FilterTypeEnum;
noOptionsText?: DynamicValue<string>;
clearable: boolean;
optionsSourceAssociationCustomContentType: OptionsSourceAssociationCustomContentTypeEnum;
Expand All @@ -92,6 +93,8 @@ export interface ComboboxContainerProps {
a11yInstructions?: DynamicValue<string>;
lazyLoading: boolean;
loadingType: LoadingTypeEnum;
selectedItemsSorting: SelectedItemsSortingEnum;
filterType: FilterTypeEnum;
}

export interface ComboboxPreviewProps {
Expand All @@ -116,7 +119,6 @@ export interface ComboboxPreviewProps {
staticAttribute: string;
optionsSourceStaticDataSource: OptionsSourceStaticDataSourcePreviewType[];
emptyOptionText: string;
filterType: FilterTypeEnum;
noOptionsText: string;
clearable: boolean;
optionsSourceAssociationCustomContentType: OptionsSourceAssociationCustomContentTypeEnum;
Expand All @@ -143,4 +145,6 @@ export interface ComboboxPreviewProps {
a11yInstructions: string;
lazyLoading: boolean;
loadingType: LoadingTypeEnum;
selectedItemsSorting: SelectedItemsSortingEnum;
filterType: FilterTypeEnum;
}
Loading