Skip to content

Commit e6e6286

Browse files
committed
DEV: refactor custom-user-selector to use DMultiSelect
1 parent f331ad2 commit e6e6286

File tree

7 files changed

+269
-150
lines changed

7 files changed

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

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

Lines changed: 0 additions & 144 deletions
This file was deleted.
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{{custom-user-selector
22
usernames=this.field.value
3-
placeholderKey=this.field.placeholder
4-
tabindex=this.field.tabindex
3+
includeGroups=this._includeGroups
4+
includeMentionableGroups=this._includeMentionableGroups
5+
includeMessageableGroups=this._includeMessageableGroups
6+
allowedUsers=this._allowedUsers
7+
single=this._single
8+
topicId=this._topicId
9+
disabled=this._disabled
10+
onChangeCallback=(action "updateFieldValue")
511
}}

0 commit comments

Comments
 (0)