Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2bbd99c
extend icons with information from theseaurus
nielslyngsoe Apr 10, 2026
c167993
implement new icon search logic
nielslyngsoe Apr 10, 2026
fbc5d5c
clean-up data
nielslyngsoe Apr 11, 2026
5b926af
sorting with a backup of the name
nielslyngsoe Apr 13, 2026
4eb7330
refactor into a controller
nielslyngsoe Apr 13, 2026
ddd9269
Merge branch 'main' into v17/feature/extend-icon-data
nielslyngsoe Apr 13, 2026
b477e12
improve multi word group search
nielslyngsoe Apr 13, 2026
4fda3c9
embed lucide data
nielslyngsoe Apr 13, 2026
7f4ca19
rename tech into technology
nielslyngsoe Apr 13, 2026
15d0b0e
remove paper from dollar
nielslyngsoe Apr 13, 2026
d006f3a
implement fuzzy search for property editor UIs
nielslyngsoe Apr 14, 2026
4c5728d
minor style update
nielslyngsoe Apr 14, 2026
27e14d4
improve property editor UI search
nielslyngsoe Apr 14, 2026
c356564
improve search
nielslyngsoe Apr 14, 2026
250e6fe
Merge branch 'v17/feature/extend-icon-data' into v17/improvement/prop…
nielslyngsoe Apr 14, 2026
64922d5
improve search data for Property Editor UIs
nielslyngsoe Apr 14, 2026
b6b3ade
remove alias search from property editor ui search
nielslyngsoe Apr 14, 2026
a607694
add usage keywords
nielslyngsoe Apr 15, 2026
124e403
Property editor Suggestions based on Property Label
nielslyngsoe Apr 15, 2026
22bdb20
related should not show up in search
nielslyngsoe Apr 15, 2026
b496b8a
rename to suggestionQuery
nielslyngsoe Apr 15, 2026
ead0a7c
update threshold
nielslyngsoe Apr 15, 2026
602c020
Merge branch 'main' into v17/feature/extend-icon-data
nielslyngsoe Apr 16, 2026
cb40398
separate name words
nielslyngsoe Apr 16, 2026
bf30984
also consider full icon name match
nielslyngsoe Apr 16, 2026
e5c5130
better comment
nielslyngsoe Apr 16, 2026
c134a15
other approach for full name matches
nielslyngsoe Apr 16, 2026
253cc0e
full icon name search if query contains a -
nielslyngsoe Apr 16, 2026
b983ddc
Merge branch 'main' into v17/feature/extend-icon-data
nielslyngsoe Apr 16, 2026
e412a28
Merge branch 'main' into v17/feature/extend-icon-data
nielslyngsoe Apr 17, 2026
96f6df2
fix test
nielslyngsoe Apr 17, 2026
2740b47
Merge branch 'v17/feature/extend-icon-data' into v17/improvement/prop…
nielslyngsoe Apr 17, 2026
c9dd593
cache all tokens as well
nielslyngsoe Apr 24, 2026
976196b
catch rejection
nielslyngsoe Apr 24, 2026
354253e
resolve feedback
nielslyngsoe Apr 24, 2026
df32ab3
handle rejected promise
nielslyngsoe Apr 24, 2026
4a086ef
Merge branch 'main' into v17/improvement/property-editor-ui-search
nielslyngsoe Apr 24, 2026
6fbfbd2
cancel debounce on disconnect
nielslyngsoe Apr 24, 2026
3cdf79c
declare voids
nielslyngsoe Apr 24, 2026
d52b557
corrections
nielslyngsoe Apr 24, 2026
bd6ca39
back out if no tokens
nielslyngsoe Apr 24, 2026
b15f8e1
rename to suggestionQuery
nielslyngsoe Apr 24, 2026
b80c8ef
extend keywords
nielslyngsoe Apr 24, 2026
ecb5a4d
more keywords for toggle
nielslyngsoe Apr 24, 2026
1ecd28c
more toggle keywords
nielslyngsoe Apr 24, 2026
d38c448
append more keywords
nielslyngsoe Apr 24, 2026
a2ca5a2
Merge branch 'main' into v17/improvement/property-editor-ui-search
madsrasmussen May 4, 2026
1cafca0
remove duplicate
madsrasmussen May 4, 2026
3a9fdf3
Fix toggle manifest status keywords
madsrasmussen May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Umbraco.Web.UI.Client/src/assets/lang/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1758,6 +1758,7 @@ export default {
editorSettings: 'Input indstillinger',
searchResultSettings: 'Tilgængelige indstillinger',
searchResultEditors: 'Opret ny indstilling',
suggestedEditors: 'Forslag',
configuration: 'Konfiguration',
yesDelete: 'Ja, slet',
movedUnderneath: 'blev flyttet til',
Expand Down
1 change: 1 addition & 0 deletions src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,7 @@ export default {
editorSettings: 'Editor settings',
searchResultSettings: 'Available configurations',
searchResultEditors: 'Create a new configuration',
suggestedEditors: 'Suggestions',
configuration: 'Configuration',
yesDelete: 'Yes, delete',
movedUnderneath: 'was moved underneath',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const propertyEditorUi: UmbExtensionManifest = {
propertyEditorSchemaAlias: UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS,
icon: 'icon-layout',
group: 'richContent',
keywords: ['component', 'layout', 'grid', 'modules', 'widgets', 'page', 'builder', 'canvas'],
supportsReadOnly: true,
settings: {
properties: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const propertyEditorUi: UmbExtensionManifest = {
propertyEditorSchemaAlias: UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS,
icon: 'icon-thumbnail-list',
group: 'richContent',
keywords: ['component', 'list', 'items', 'blocks', 'cards', 'faq', 'testimonials', 'features', 'services'],
supportsReadOnly: true,
settings: {
properties: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const manifests: Array<UmbExtensionManifest> = [
propertyEditorSchemaAlias: UMB_BLOCK_SINGLE_PROPERTY_EDITOR_SCHEMA_ALIAS,
icon: 'icon-shape-square',
group: 'richContent',
keywords: ['component', 'widget', 'banner', 'hero', 'cta', 'promo', 'cta', 'callout', 'spotlight', 'feature'],
supportsReadOnly: true,
settings: {
properties: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ export const manifest: ManifestPropertyEditorUi = {
propertyEditorSchemaAlias: 'Umbraco.Plain.String',
icon: 'icon-brackets',
group: 'richContent',
keywords: [
'code',
'script',
'embed',
'snippet',
'json',
'html',
'css',
'javascript',
'js',
'xml',
'yaml',
'markup',
],
settings: {
properties: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i
slot="editor"
id="data-type-input"
.value=${this._data?.dataType?.unique ?? ''}
.suggestionQuery=${this._data?.name}
@change=${this.#onDataTypeIdChange}
required
${umbBindToValidation(this, '$.dataType.unique')}></umb-data-type-flow-input>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker

#searchController = new UmbIconSearchController(this);

#debouncedFilterIcons = debounce(() => this.#filterIcons(), 250);
#debouncedFilterIcons = debounce(() => {
void this.#filterIcons().catch((error) => {
if ((error as DOMException)?.name !== 'AbortError') {
console.error(error);
}
});
}, 250);

@query('#search')
private _searchInput?: HTMLInputElement;
Comment thread
nielslyngsoe marked this conversation as resolved.
Expand All @@ -45,7 +51,11 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
this.observe(context?.approvedIcons, (icons) => {
this.#icons = icons;
this.#searchController.setIcons(icons ?? []);
this.#filterIcons();
this.#filterIcons().catch((error) => {
if ((error as DOMException)?.name !== 'AbortError') {
console.error(error);
}
});
});
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export interface MetaPropertyEditorUi {
*/
forDataSourceTypes: string[];
};
/**
* A list of keywords that can be used to search for this property editor UI in the property editor picker.
* If not specified, the property editor UI will not have any keywords.
* @example ["text", "input", "string"]
*/
keywords?: string[];
}

// Model
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './property-editor-ui-picker-modal.token.js';
export * from './property-editor-ui-search.controller.js';
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import type {
UmbPropertyEditorUIPickerModalData,
UmbPropertyEditorUIPickerModalValue,
} from './property-editor-ui-picker-modal.token.js';
import { UmbPropertyEditorUISearchController } from './property-editor-ui-search.controller.js';
import { css, customElement, html, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { fromCamelCase } from '@umbraco-cms/backoffice/utils';
import { debounce, fromCamelCase } from '@umbraco-cms/backoffice/utils';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { umbFocus } from '@umbraco-cms/backoffice/lit-element';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
Expand All @@ -18,23 +19,32 @@ export class UmbPropertyEditorUIPickerModalElement extends UmbModalBaseElement<
@state()
private _groupedPropertyEditorUIs: Array<{ key: string; items: Array<ManifestPropertyEditorUi> }> = [];

@state()
private _propertyEditorUIs: Array<ManifestPropertyEditorUi> = [];
#propertyEditorUIs: Array<ManifestPropertyEditorUi> = [];

#searchController = new UmbPropertyEditorUISearchController(this);

#currentFilterQuery = '';

override connectedCallback(): void {
super.connectedCallback();

this.#usePropertyEditorUIs();
}

override disconnectedCallback(): void {
super.disconnectedCallback();
this.#debouncedFilter.cancel();
}

#usePropertyEditorUIs() {
this.observe(umbExtensionsRegistry.byType('propertyEditorUi'), (propertyEditorUIs) => {
// Only include Property Editor UIs which has Property Editor Schema Alias
this._propertyEditorUIs = propertyEditorUIs
this.#propertyEditorUIs = propertyEditorUIs
.filter((propertyEditorUi) => !!propertyEditorUi.meta.propertyEditorSchemaAlias)
.sort((a, b) => a.meta.label.localeCompare(b.meta.label));

this.#groupPropertyEditorUIs(this._propertyEditorUIs);
this.#searchController.setPropertyEditorUIs(this.#propertyEditorUIs);
this.#performFiltering();
});
}

Expand All @@ -44,16 +54,31 @@ export class UmbPropertyEditorUIPickerModalElement extends UmbModalBaseElement<
}

#handleFilterInput(event: UUIInputEvent) {
const query = ((event.target.value as string) || '').toLowerCase();
this.#currentFilterQuery = (event.target.value as string) || '';
this.#debouncedFilter();
}

#debouncedFilter = debounce(() => {
void this.#performFiltering().catch((error) => {
if ((error as DOMException)?.name !== 'AbortError') {
console.error(error);
}
});
}, 250);

Comment thread
nielslyngsoe marked this conversation as resolved.
const result = !query
? this._propertyEditorUIs
: this._propertyEditorUIs.filter(
(propertyEditorUI) =>
propertyEditorUI.name.toLowerCase().includes(query) || propertyEditorUI.alias.toLowerCase().includes(query),
);
async #performFiltering() {
const query = this.#currentFilterQuery.trim();
if (!query) {
this.#groupPropertyEditorUIs(this.#propertyEditorUIs);
return;
}

this.#groupPropertyEditorUIs(result);
try {
const results = await this.#searchController.search(query);
this.#groupPropertyEditorUIs(results);
} catch (error) {
if ((error as DOMException)?.name !== 'AbortError') throw error;
}
}

#groupPropertyEditorUIs(items: Array<ManifestPropertyEditorUi>) {
Expand Down Expand Up @@ -220,6 +245,7 @@ export class UmbPropertyEditorUIPickerModalElement extends UmbModalBaseElement<
max-width: 100%;
display: -webkit-box;
overflow: hidden;
padding-bottom: 0.1em;
}
`,
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { expect } from '@open-wc/testing';
import { UmbPropertyEditorUISearchController } from './property-editor-ui-search.controller.js';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor';

@customElement('test-property-editor-ui-search-host')
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}

function mockUI(
overrides: Partial<ManifestPropertyEditorUi> & { alias: string; name: string },
): ManifestPropertyEditorUi {
const { meta, ...rest } = overrides;
return {
type: 'propertyEditorUi',
meta: {
label: overrides.name,
icon: 'icon-circle',
group: 'Common',
propertyEditorSchemaAlias: 'Umbraco.Plain',
...meta,
},
...rest,
} as ManifestPropertyEditorUi;
}

const mockUIs: Array<ManifestPropertyEditorUi> = [
mockUI({
alias: 'Umb.PropertyEditorUi.TextBox',
name: 'Text Box',
meta: { label: 'Text Box', icon: 'icon-edit', group: 'Common', propertyEditorSchemaAlias: 'Umbraco.TextBox' },
}),
mockUI({
alias: 'Umb.PropertyEditorUi.TextArea',
name: 'Text Area',
meta: { label: 'Text Area', icon: 'icon-edit', group: 'Common', propertyEditorSchemaAlias: 'Umbraco.TextArea' },
}),
mockUI({
alias: 'Umb.PropertyEditorUi.RichText',
name: 'Rich Text Editor',
meta: {
label: 'Rich Text Editor',
icon: 'icon-browser-window',
group: 'richContent',
propertyEditorSchemaAlias: 'Umbraco.RichText',
},
}),
mockUI({
alias: 'Umb.PropertyEditorUi.MediaPicker',
name: 'Media Picker',
meta: {
label: 'Media Picker',
icon: 'icon-picture',
group: 'Pickers',
propertyEditorSchemaAlias: 'Umbraco.MediaPicker3',
keywords: ['image', 'photo', 'picture'],
},
}),
mockUI({
alias: 'Umb.PropertyEditorUi.ColorPicker',
name: 'Color Picker',
meta: {
label: 'Color Picker',
icon: 'icon-palette',
group: 'Pickers',
propertyEditorSchemaAlias: 'Umbraco.ColorPicker',
},
}),
];

describe('UmbPropertyEditorUISearchController', () => {
let host: UmbTestControllerHostElement;
let controller: UmbPropertyEditorUISearchController;

beforeEach(() => {
host = new UmbTestControllerHostElement();
controller = new UmbPropertyEditorUISearchController(host);
controller.setPropertyEditorUIs(mockUIs);
});

it('should return empty array for empty query', async () => {
expect(await controller.search('')).to.deep.equal([]);
expect(await controller.search(' ')).to.deep.equal([]);
});

it('should match by label substring', async () => {
const results = await controller.search('Text Box');
expect(results[0].alias).to.equal('Umb.PropertyEditorUi.TextBox');
});

it('should match by group', async () => {
const results = await controller.search('pickers');
expect(results.some((r) => r.alias === 'Umb.PropertyEditorUi.MediaPicker')).to.be.true;
expect(results.some((r) => r.alias === 'Umb.PropertyEditorUi.ColorPicker')).to.be.true;
});

it('should include fuzzy matches for typos', async () => {
const results = await controller.search('texbx');
expect(results.some((r) => r.alias === 'Umb.PropertyEditorUi.TextBox')).to.be.true;
});

it('should include fuzzy matches for misspelled group', async () => {
const results = await controller.search('pickrs');
expect(results.some((r) => r.alias === 'Umb.PropertyEditorUi.MediaPicker')).to.be.true;
expect(results.some((r) => r.alias === 'Umb.PropertyEditorUi.ColorPicker')).to.be.true;
});

it('should return no results for unrelated query', async () => {
const results = await controller.search('xyznonexistent');
expect(results).to.have.length(0);
});

it('should match multiple tokens', async () => {
const results = await controller.search('color picker');
expect(results[0].alias).to.equal('Umb.PropertyEditorUi.ColorPicker');
});

it('should surface partial matches for multi-word queries where only one token matches a keyword', async () => {
// "hero" matches nothing, but "image" is an exact keyword on Media Picker.
// Without partial-token scoring this would return zero matches.
const results = await controller.search('hero image');
expect(results.some((r) => r.alias === 'Umb.PropertyEditorUi.MediaPicker')).to.be.true;
});

it('should abort a prior in-flight search when a new one starts', async () => {
const many: Array<ManifestPropertyEditorUi> = [];
for (let i = 0; i < 200; i++) {
many.push(
mockUI({
alias: `Umb.PropertyEditorUi.Bulk${i}`,
name: `Bulk ${i}`,
}),
);
}
many.push(...mockUIs);
controller.setPropertyEditorUIs(many);

const first = controller.search('Text Box');
const second = controller.search('Media Picker');

let firstError: unknown;
try {
await first;
} catch (err) {
firstError = err;
}
expect((firstError as DOMException)?.name).to.equal('AbortError');

const secondResults = await second;
expect(secondResults[0].alias).to.equal('Umb.PropertyEditorUi.MediaPicker');
});

it('should abort an in-flight search on destroy', async () => {
const many: Array<ManifestPropertyEditorUi> = [];
for (let i = 0; i < 200; i++) {
many.push(
mockUI({
alias: `Umb.PropertyEditorUi.Bulk${i}`,
name: `Bulk ${i}`,
}),
);
}
many.push(...mockUIs);
controller.setPropertyEditorUIs(many);

const pending = controller.search('Text Box');
controller.destroy();

let error: unknown;
try {
await pending;
} catch (err) {
error = err;
}
expect((error as DOMException)?.name).to.equal('AbortError');
});
});
Loading
Loading