Skip to content

Commit be02794

Browse files
committed
refactor custom-user-selector to use DMultiSelect
1 parent f331ad2 commit be02794

File tree

5 files changed

+260
-144
lines changed

5 files changed

+260
-144
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import Component from "@glimmer/component";
2+
import { tracked } from "@glimmer/tracking";
3+
import { array } from "@ember/helper";
4+
import { action } from "@ember/object";
5+
import DMultiSelect from "discourse/components/d-multi-select";
6+
import avatar from "discourse/helpers/avatar";
7+
import icon from "discourse/helpers/d-icon";
8+
import userSearch from "discourse/lib/user-search";
9+
import { i18n } from "discourse-i18n";
10+
11+
/**
12+
* Custom user selector component using DMultiSelect
13+
*
14+
* @component CustomUserSelector
15+
* @param {string} @placeholderKey - i18n key for placeholder text
16+
* @param {string} @usernames - Comma-separated string of selected usernames (read-only)
17+
* @param {boolean} @single - Whether to allow only single selection
18+
* @param {boolean} @allowAny - Whether to allow any input
19+
* @param {boolean} @disabled - Whether the component is disabled
20+
* @param {boolean} @includeGroups - Whether to include groups in search
21+
* @param {boolean} @includeMentionableGroups - Whether to include mentionable groups
22+
* @param {boolean} @includeMessageableGroups - Whether to include messageable groups
23+
* @param {boolean} @allowedUsers - Whether to restrict to allowed users only
24+
* @param {string} @topicId - Topic ID for context-aware search
25+
* @param {function} @onChangeCallback - Legacy callback for backward compatibility
26+
*/
27+
export default class CustomUserSelector extends Component {
28+
@tracked selectedUsers = [];
29+
@tracked hasGroups = false;
30+
@tracked usernames = "";
31+
32+
constructor(owner, args) {
33+
super(owner, args);
34+
this.parseInitialUsernames();
35+
}
36+
37+
get placeholder() {
38+
return this.args.placeholderKey ? i18n(this.args.placeholderKey) : "";
39+
}
40+
41+
get includeMentionableGroups() {
42+
return this.args.includeMentionableGroups === "true";
43+
}
44+
45+
get includeMessageableGroups() {
46+
return this.args.includeMessageableGroups === "true";
47+
}
48+
49+
get includeGroups() {
50+
return this.args.includeGroups === "true";
51+
}
52+
53+
get allowedUsers() {
54+
return this.args.allowedUsers === "true";
55+
}
56+
57+
get single() {
58+
return this.args.single;
59+
}
60+
61+
parseInitialUsernames() {
62+
if (!this.args.usernames) {
63+
this.selectedUsers = [];
64+
return;
65+
}
66+
67+
const usernames = this.args.usernames.split(",").filter(Boolean);
68+
this.selectedUsers = usernames.map((username) => {
69+
const trimmedUsername = username.trim();
70+
// Create user object similar to what reverseTransform did in original
71+
return {
72+
username: trimmedUsername,
73+
name: trimmedUsername,
74+
id: trimmedUsername,
75+
isUser: true
76+
};
77+
});
78+
}
79+
80+
@action
81+
async loadUsers(searchTerm) {
82+
const termRegex = /[^a-zA-Z0-9_\-\.@\+]/;
83+
const cleanTerm = searchTerm ? searchTerm.replace(termRegex, "") : "";
84+
85+
// Get currently selected usernames for exclusion
86+
const excludedUsernames = this.single
87+
? []
88+
: this.selectedUsers.map((u) => u.username);
89+
90+
try {
91+
const results = await userSearch({
92+
term: cleanTerm,
93+
topicId: this.args.topicId,
94+
exclude: excludedUsernames,
95+
includeGroups: this.includeGroups,
96+
allowedUsers: this.allowedUsers,
97+
includeMentionableGroups: this.includeMentionableGroups,
98+
includeMessageableGroups: this.includeMessageableGroups
99+
});
100+
101+
// Transform results to include both users and groups
102+
const transformedResults = [];
103+
104+
if (results.users) {
105+
transformedResults.push(
106+
...results.users.map((user) => ({
107+
...user,
108+
isUser: true,
109+
id: user.username // Use username as ID for comparison
110+
}))
111+
);
112+
}
113+
114+
if (results.groups) {
115+
transformedResults.push(
116+
...results.groups.map((group) => ({
117+
...group,
118+
isGroup: true,
119+
name: group.name, // Groups use name as username
120+
id: group.name // Use name as ID for comparison
121+
}))
122+
);
123+
}
124+
125+
return transformedResults;
126+
} catch {
127+
return [];
128+
}
129+
}
130+
131+
@action
132+
onSelectionChange(newSelection) {
133+
let selectedUsers = newSelection || [];
134+
135+
if (this.single && selectedUsers.length > 1) {
136+
selectedUsers = [selectedUsers[selectedUsers.length - 1]];
137+
}
138+
139+
this.selectedUsers = selectedUsers;
140+
this.hasGroups = this.selectedUsers.some((item) => item.isGroup);
141+
142+
this.usernames = this.selectedUsers
143+
.map((item) => item.username || item.name)
144+
.join(",");
145+
146+
if (this.args.onChangeCallback) {
147+
this.args.onChangeCallback();
148+
}
149+
}
150+
151+
@action
152+
compareUsers(a, b) {
153+
return (a.username || a.name) === (b.username || b.name);
154+
}
155+
156+
<template>
157+
<DMultiSelect
158+
@loadFn={{this.loadUsers}}
159+
@selection={{this.selectedUsers}}
160+
@onChange={{this.onSelectionChange}}
161+
@compareFn={{this.compareUsers}}
162+
@label={{this.placeholder}}
163+
class="custom-user-selector"
164+
id="custom-member-selector"
165+
@placement="bottom-start"
166+
@allowedPlacements={{array "top-start" "bottom-start"}}
167+
@matchTriggerWidth={{true}}
168+
@matchTriggerMinWidth={{true}}
169+
disabled={{@disabled}}
170+
style="--d-multi-select-pill-max-width: 100%"
171+
>
172+
<:selection as |user|>
173+
{{#if user.isGroup}}
174+
{{user.name}}
175+
{{else}}
176+
{{user.username}}
177+
{{/if}}
178+
</:selection>
179+
180+
<:result as |user|>
181+
{{#if user.isGroup}}
182+
<div class="group-result">
183+
{{icon "users" class="group-icon"}}
184+
<span class="username">{{user.name}}</span>
185+
</div>
186+
{{else}}
187+
<div class="user-result">
188+
{{avatar user imageSize="tiny"}}
189+
<span class="username">{{user.username}}</span>
190+
{{#if user.name}}
191+
<span class="name">{{user.name}}</span>
192+
{{/if}}
193+
</div>
194+
{{/if}}
195+
</:result>
196+
197+
</DMultiSelect>
198+
</template>
199+
}

assets/javascripts/discourse/components/custom-user-selector.js

Lines changed: 0 additions & 144 deletions
This file was deleted.

assets/javascripts/discourse/components/custom-wizard-field-user-selector.hbs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,12 @@
22
usernames=this.field.value
33
placeholderKey=this.field.placeholder
44
tabindex=this.field.tabindex
5+
includeGroups=this._includeGroups
6+
includeMentionableGroups=this._includeMentionableGroups
7+
includeMessageableGroups=this._includeMessageableGroups
8+
allowedUsers=this._allowedUsers
9+
single=this._single
10+
topicId=this._topicId
11+
disabled=this._disabled
12+
onChangeCallback=this._onChangeCallback
513
}}

0 commit comments

Comments
 (0)