Skip to content

Commit f9f14dd

Browse files
committed
Improve searching by member number in new message form.
1 parent 6c0a2de commit f9f14dd

File tree

5 files changed

+168
-87
lines changed

5 files changed

+168
-87
lines changed
Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Message from "Models/Message";
12
import React, { useEffect, useState } from "react";
23
import Async from "react-select/async";
34
import { get } from "../gateway";
@@ -7,19 +8,40 @@ import Icon from "./icons";
78
import Textarea from "./Textarea";
89
import TextInput from "./TextInput";
910

10-
const groupOption = (d) => {
11-
const id = d[Group.model.id];
11+
type GroupOption = {
12+
id: number;
13+
type: "group";
14+
label: string;
15+
value: string;
16+
}
17+
18+
const groupOption = (d: Group): GroupOption => {
19+
const id = d.id;
1220
const type = "group";
1321
return {
14-
id,
22+
id: id!,
1523
type,
1624
label: `Grupp: ${d.title}`,
1725
value: type + id,
1826
};
1927
};
2028

21-
const memberOption = (d) => {
22-
const id = d[Member.model.id];
29+
type MemberOption = {
30+
id: number;
31+
type: "member";
32+
label: string;
33+
value: string;
34+
}
35+
36+
type CombinedOption = {
37+
type: "combined",
38+
label: string,
39+
value: string,
40+
inner: (MemberOption | GroupOption)[],
41+
}
42+
43+
const memberOption = (d: Member): MemberOption => {
44+
const id = d.member_id;
2345
const type = "member";
2446
const lastname = d.lastname || "";
2547
return {
@@ -30,15 +52,25 @@ const memberOption = (d) => {
3052
};
3153
};
3254

33-
const MessageForm = ({ message, onSave, recipientSelect }) => {
55+
const MessageForm = ({ message, onSave, recipientSelect }: { message: Message, onSave: ()=>void, recipientSelect: boolean }) => {
3456
const [sendDisabled, setSendDisabled] = useState(true);
35-
const [recipients, setRecipients] = useState([]);
57+
const [recipients, setRecipients] = useState<(MemberOption | GroupOption)[]>([]);
3658
const [bodyLength, setBodyLength] = useState(message.body.length);
59+
const memberCache = React.useRef(new Map<number, Member>()).current;
60+
61+
const getMember = async (id: number): Promise<Member> => {
62+
if (memberCache.has(id)) {
63+
return memberCache.get(id)!;
64+
}
65+
const { data } = await get({ url: `/membership/member/${id}` });
66+
memberCache.set(id, data);
67+
return data;
68+
};
3769

3870
useEffect(() => {
3971
const unsubscribe = message.subscribe(() => {
4072
setSendDisabled(!message.canSave());
41-
setRecipients(message.recipients);
73+
setRecipients(message.recipients as any as (MemberOption | GroupOption)[]);
4274
setBodyLength(message.body.length);
4375
});
4476

@@ -47,7 +79,29 @@ const MessageForm = ({ message, onSave, recipientSelect }) => {
4779
};
4880
}, [message]);
4981

50-
const loadOptions = (inputValue, callback) => {
82+
const loadOptions = (inputValue: string, callback: (options: (MemberOption | GroupOption | CombinedOption)[]) => void) => {
83+
const intListMatch = inputValue.match(/^(\d+[\s,]*)+$/);
84+
if (intListMatch) {
85+
const ids = inputValue
86+
.split(/[\s,]+/)
87+
.map((v) => parseInt(v, 10))
88+
.filter((v) => !isNaN(v));
89+
if (ids.length > 0) {
90+
Promise.all(ids.map(getMember)).then(members => {
91+
const options = members.map(memberOption);
92+
callback([
93+
{
94+
type: "combined",
95+
label: `${options.map(o => o.label).join(", ")}`,
96+
value: "combined-" + ids.join("-"),
97+
inner: options,
98+
},
99+
]);
100+
});
101+
return;
102+
}
103+
}
104+
51105
Promise.all([
52106
get({
53107
url: "/membership/group",
@@ -65,16 +119,16 @@ const MessageForm = ({ message, onSave, recipientSelect }) => {
65119
sort_order: "asc",
66120
},
67121
}),
68-
]).then(([{ data: groups }, { data: members }]) =>
122+
]).then(([{ data: groups }, { data: members }]: [{ data: Group[] }, { data: Member[] }]) =>
69123
callback(
70124
groups
71-
.map((d) => groupOption(d))
125+
.map((d) => groupOption(d) as GroupOption | MemberOption)
72126
.concat(members.map((d) => memberOption(d))),
73127
),
74128
);
75129
};
76130

77-
const handleSubmit = (e) => {
131+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
78132
e.preventDefault();
79133
onSave();
80134
};
@@ -87,18 +141,20 @@ const MessageForm = ({ message, onSave, recipientSelect }) => {
87141
Mottagare
88142
</label>
89143
<div className="uk-form-controls">
90-
<Async
144+
<Async<(MemberOption | GroupOption | CombinedOption), true>
91145
name="recipients"
92146
isMulti
93-
cache={false}
94147
placeholder="Type to search for member or group"
95148
getOptionValue={(e) => e.value}
96149
getOptionLabel={(e) => e.label}
97150
loadOptions={loadOptions}
98151
value={recipients}
99152
onChange={(values) => {
100-
message.recipients = values;
101-
setRecipients(values);
153+
const flattened = values.flatMap((v) =>
154+
v.type === "combined" ? v.inner : v
155+
);
156+
message.recipients = flattened;
157+
setRecipients(flattened);
102158
}}
103159
/>
104160
</div>

admin/src/Models/Group.js

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

admin/src/Models/Group.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Base from "./Base";
2+
3+
export default class Group extends Base<Group> {
4+
created_at!: string | null
5+
updated_at!: string | null
6+
deleted_at!: string | null
7+
name!: string
8+
title!: string
9+
description!: string
10+
num_members!: number
11+
12+
static model = {
13+
id: "group_id",
14+
root: "/membership/group",
15+
attributes: {
16+
created_at: null,
17+
updated_at: null,
18+
deleted_at: null,
19+
name: "",
20+
title: "",
21+
description: "",
22+
num_members: 0,
23+
},
24+
};
25+
26+
27+
override deleteConfirmMessage() {
28+
return `Are you sure you want to delete group ${this.title}?`;
29+
}
30+
31+
override canSave() {
32+
return this.isDirty() && !!this.title && !!this.name;
33+
}
34+
}

admin/src/Models/Message.js

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

admin/src/Models/Message.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Base from "./Base";
2+
3+
export type MessageRecipient = {
4+
id: number,
5+
type: "member" | "group"
6+
}
7+
8+
export default class Message extends Base<Message> {
9+
subject!: string
10+
body!: string
11+
status!: string
12+
template!: string
13+
recipient!: string
14+
recipients!: MessageRecipient[]
15+
created_at!: string | null
16+
updated_at!: string | null
17+
sent_at!: string | null
18+
member_id!: number
19+
20+
static model = {
21+
id: "id",
22+
root: "/messages/message",
23+
attributes: {
24+
subject: "",
25+
body: "",
26+
status: "",
27+
template: "",
28+
recipient: "",
29+
recipients: [],
30+
created_at: null,
31+
updated_at: null,
32+
sent_at: null,
33+
member_id: 0,
34+
},
35+
};
36+
37+
override del(): Promise<void> {
38+
throw new Error("Message delete not supported.");
39+
}
40+
41+
override canSave(): boolean {
42+
return (
43+
!!this.body &&
44+
!!this.recipients &&
45+
this.recipients.length > 0 &&
46+
!!this.subject
47+
);
48+
}
49+
50+
statusText(message: Message): string {
51+
switch (message.status) {
52+
case "queued":
53+
return "Queued";
54+
case "failed":
55+
return "Failed";
56+
case "sent":
57+
return "Sent";
58+
default:
59+
return "Unknown";
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)