@kevlid/discordmenus 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/package.json +1 -1
- package/src/handle/component.js +16 -7
- package/src/handle/modal.js +4 -4
- package/src/menuInstance.js +22 -12
- package/src/render/selectMenu.js +49 -15
package/README.md
CHANGED
|
@@ -60,3 +60,8 @@ client.on("interactionCreate", async (interaction) => {
|
|
|
60
60
|
|
|
61
61
|
client.login(process.env.BOT_TOKEN);
|
|
62
62
|
```
|
|
63
|
+
|
|
64
|
+
### Role / user / channel selects
|
|
65
|
+
|
|
66
|
+
- Persist IDs as **strings** (Discord snowflakes are too large for JS numbers; Mongo/JSON numbers will corrupt them and `default_values` won’t show on rerender).
|
|
67
|
+
- Select `custom_id`s include **category + page** so paging doesn’t reuse the same component identity in the same slot across pages.
|
package/package.json
CHANGED
package/src/handle/component.js
CHANGED
|
@@ -74,7 +74,7 @@ async function handleListBranch({
|
|
|
74
74
|
const props = option.itemConfig.properties;
|
|
75
75
|
const addItems = normalizeObjectListItems(
|
|
76
76
|
props,
|
|
77
|
-
arrayClone(await menu.get(optionKey, { guildId, userId })),
|
|
77
|
+
arrayClone(await menu.get(optionKey, { guildId, userId, cat: category })),
|
|
78
78
|
);
|
|
79
79
|
addItems.push(shapeObjectListItem(props, {}));
|
|
80
80
|
await menu.save(optionKey, addItems, {
|
|
@@ -101,7 +101,7 @@ async function handleListBranch({
|
|
|
101
101
|
case Presets.previousCategory:
|
|
102
102
|
case Presets.nextCategory:
|
|
103
103
|
if (objectItems) {
|
|
104
|
-
const jumpItems = arrayClone(await menu.get(optionKey, { guildId, userId }));
|
|
104
|
+
const jumpItems = arrayClone(await menu.get(optionKey, { guildId, userId, cat: category }));
|
|
105
105
|
const currentItemIndex = parseInt(options.get("item") ?? 0);
|
|
106
106
|
const delta = subAction === Presets.previousCategory ? -listJumpSize(jumpItems.length) : listJumpSize(jumpItems.length);
|
|
107
107
|
return updateMessage(
|
|
@@ -116,7 +116,7 @@ async function handleListBranch({
|
|
|
116
116
|
case Presets.listItemDone: {
|
|
117
117
|
const itemIndex = parseInt(options.get("item") ?? -1);
|
|
118
118
|
if (objectItems && itemIndex >= 0) {
|
|
119
|
-
const doneItems = arrayClone(await menu.get(optionKey, { guildId, userId }));
|
|
119
|
+
const doneItems = arrayClone(await menu.get(optionKey, { guildId, userId, cat: category }));
|
|
120
120
|
const currentItem = doneItems[itemIndex];
|
|
121
121
|
if (objectListItemFailsRequired(option, currentItem)) {
|
|
122
122
|
doneItems.splice(itemIndex, 1);
|
|
@@ -149,7 +149,7 @@ async function handleListBranch({
|
|
|
149
149
|
const props = option.itemConfig.properties;
|
|
150
150
|
const boolItems = normalizeObjectListItems(
|
|
151
151
|
props,
|
|
152
|
-
arrayClone(await menu.get(optionKey, { guildId, userId })),
|
|
152
|
+
arrayClone(await menu.get(optionKey, { guildId, userId, cat: category })),
|
|
153
153
|
);
|
|
154
154
|
boolItems[itemIndex] = shapeObjectListItem(props, {
|
|
155
155
|
...asObject(boolItems[itemIndex]),
|
|
@@ -168,7 +168,7 @@ async function handleListBranch({
|
|
|
168
168
|
case Presets.delete: {
|
|
169
169
|
const itemIndex = parseInt(values[3]);
|
|
170
170
|
const props = option.itemConfig.properties;
|
|
171
|
-
let deleteItems = arrayClone(await menu.get(optionKey, { guildId, userId }));
|
|
171
|
+
let deleteItems = arrayClone(await menu.get(optionKey, { guildId, userId, cat: category }));
|
|
172
172
|
if (objectItems) {
|
|
173
173
|
deleteItems = normalizeObjectListItems(props, deleteItems);
|
|
174
174
|
}
|
|
@@ -230,7 +230,7 @@ async function handleObjectBranch({
|
|
|
230
230
|
const subKey = values[3];
|
|
231
231
|
const newValue = values[4] === "true";
|
|
232
232
|
const boolObjectOption = menu.getOption(category, page, objectKey);
|
|
233
|
-
const baseObj = asObject(await menu.get(objectKey, { guildId, userId }));
|
|
233
|
+
const baseObj = asObject(await menu.get(objectKey, { guildId, userId, cat: category }));
|
|
234
234
|
const defaults = boolObjectOption ? objectDefaultsFromKeys(boolObjectOption.options) : {};
|
|
235
235
|
const updatedObj = Object.assign(defaults, baseObj);
|
|
236
236
|
updatedObj[subKey] = newValue;
|
|
@@ -289,7 +289,7 @@ async function handleObjectBranch({
|
|
|
289
289
|
}
|
|
290
290
|
if (listAction === Presets.delete) {
|
|
291
291
|
const itemIndex = parseInt(values[5]);
|
|
292
|
-
const obj = asObject(await menu.get(objectKey, { guildId, userId }));
|
|
292
|
+
const obj = asObject(await menu.get(objectKey, { guildId, userId, cat: category }));
|
|
293
293
|
const items = arrayClone(obj[subKey]);
|
|
294
294
|
items.splice(itemIndex, 1);
|
|
295
295
|
await menu.save(objectKey, Object.assign({}, obj, { [subKey]: items }), {
|
|
@@ -398,6 +398,15 @@ async function handleComponent(interaction, menu, menu_key, renderMenu) {
|
|
|
398
398
|
const maxSel = Number(opt?.maxValues);
|
|
399
399
|
const multi = Number.isFinite(maxSel) && maxSel > 1;
|
|
400
400
|
let saveValue = multi ? [...selected] : selected[0] ?? null;
|
|
401
|
+
const snowflakeSelectTypes = new Set([
|
|
402
|
+
OptionTypes.UserSelect,
|
|
403
|
+
OptionTypes.RoleSelect,
|
|
404
|
+
OptionTypes.ChannelSelect,
|
|
405
|
+
OptionTypes.MentionableSelect,
|
|
406
|
+
]);
|
|
407
|
+
if (opt && snowflakeSelectTypes.has(opt.type)) {
|
|
408
|
+
saveValue = multi ? selected.map(v => String(v)) : selected[0] != null ? String(selected[0]) : null;
|
|
409
|
+
}
|
|
401
410
|
if (
|
|
402
411
|
!multi &&
|
|
403
412
|
saveValue == null &&
|
package/src/handle/modal.js
CHANGED
|
@@ -39,7 +39,7 @@ async function handleListModal(menu, values, options, rawValue, category, page,
|
|
|
39
39
|
throw new Error(`Option "${optionKey}" not found`);
|
|
40
40
|
}
|
|
41
41
|
const props = option.itemConfig.properties;
|
|
42
|
-
const objItems = normalizeObjectListItems(props, arrayClone(await menu.get(optionKey, { guildId, userId })));
|
|
42
|
+
const objItems = normalizeObjectListItems(props, arrayClone(await menu.get(optionKey, { guildId, userId, cat: category })));
|
|
43
43
|
let property;
|
|
44
44
|
const propertyIndex = parseInt(propertyToken, 10);
|
|
45
45
|
if (!Number.isNaN(propertyIndex)) {
|
|
@@ -79,7 +79,7 @@ async function handleListModal(menu, values, options, rawValue, category, page,
|
|
|
79
79
|
if (!option) {
|
|
80
80
|
throw new Error(`Option "${optionKey}" not found`);
|
|
81
81
|
}
|
|
82
|
-
const items = arrayClone(await menu.get(optionKey, { guildId, userId }));
|
|
82
|
+
const items = arrayClone(await menu.get(optionKey, { guildId, userId, cat: category }));
|
|
83
83
|
let parsedValue;
|
|
84
84
|
if (option.itemType === OptionTypes.String) {
|
|
85
85
|
parsedValue = rawValue;
|
|
@@ -150,7 +150,7 @@ async function handleObjectModal(menu, values, options, rawValue, category, page
|
|
|
150
150
|
} else {
|
|
151
151
|
throw new Error(`Sub-option type "${subOption.type}" does not support modal input`);
|
|
152
152
|
}
|
|
153
|
-
const baseObj = asObject(await menu.get(optionKey, { guildId, userId }));
|
|
153
|
+
const baseObj = asObject(await menu.get(optionKey, { guildId, userId, cat: category }));
|
|
154
154
|
const defaults = objectDefaultsFromKeys(option.options);
|
|
155
155
|
const updatedObj = Object.assign(defaults, baseObj);
|
|
156
156
|
updatedObj[subKey] = parsedValue;
|
|
@@ -169,7 +169,7 @@ async function handleObjectModal(menu, values, options, rawValue, category, page
|
|
|
169
169
|
if (!subOption) {
|
|
170
170
|
throw new Error(`Sub-option "${subKey}" not found`);
|
|
171
171
|
}
|
|
172
|
-
const baseObj = asObject(await menu.get(optionKey, { guildId, userId }));
|
|
172
|
+
const baseObj = asObject(await menu.get(optionKey, { guildId, userId, cat: category }));
|
|
173
173
|
const items = arrayClone(baseObj[subKey]);
|
|
174
174
|
let parsedValue;
|
|
175
175
|
if (subOption.itemType === OptionTypes.String) {
|
package/src/menuInstance.js
CHANGED
|
@@ -41,12 +41,22 @@ class MenuInstance {
|
|
|
41
41
|
this.builder = builder;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
withCategoryCtx(ctx = {}) {
|
|
45
|
+
// Ensure ctx.category is always present (string key or null).
|
|
46
|
+
if (ctx && Object.prototype.hasOwnProperty.call(ctx, "category")) {
|
|
47
|
+
return ctx;
|
|
48
|
+
}
|
|
49
|
+
const catIndex = typeof ctx?.cat === "number" ? ctx.cat : typeof ctx?.categoryIndex === "number" ? ctx.categoryIndex : null;
|
|
50
|
+
const categoryKey = catIndex != null ? this.getCategoryKey(catIndex) : null;
|
|
51
|
+
return Object.assign({}, ctx, { category: categoryKey });
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
async get(key, ctx = {}) {
|
|
45
|
-
return this.getData(decodeKey(key), ctx);
|
|
55
|
+
return this.getData(decodeKey(key), this.withCategoryCtx(ctx));
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
async save(key, value, ctx = {}) {
|
|
49
|
-
return this.saveData(decodeKey(key), value, ctx);
|
|
59
|
+
return this.saveData(decodeKey(key), value, this.withCategoryCtx(ctx));
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
ensureBuilt() {
|
|
@@ -177,7 +187,7 @@ class MenuInstance {
|
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
const page = categoryPages[index];
|
|
180
|
-
const ctx = { guildId, userId };
|
|
190
|
+
const ctx = { guildId, userId, cat: category };
|
|
181
191
|
const navigation = renderNavigation(this.key, userId, this.getNavigationTargets(category, index), category, index);
|
|
182
192
|
const optionComponents = [];
|
|
183
193
|
for (const option of page.options) {
|
|
@@ -209,7 +219,7 @@ class MenuInstance {
|
|
|
209
219
|
if (!option) {
|
|
210
220
|
throw new Error(`Option "${optionKey}" not found on page ${category}:${index}`);
|
|
211
221
|
}
|
|
212
|
-
const currentValue = await this.get(option.key, { guildId, userId });
|
|
222
|
+
const currentValue = await this.get(option.key, { guildId, userId, cat: category });
|
|
213
223
|
return renderModal(this.key, this.title, [renderTextInput(option, currentValue)], { category, page: index });
|
|
214
224
|
}
|
|
215
225
|
|
|
@@ -219,7 +229,7 @@ class MenuInstance {
|
|
|
219
229
|
if (!option) {
|
|
220
230
|
throw new Error(`Option "${optionKey}" not found on page ${category}:${index}`);
|
|
221
231
|
}
|
|
222
|
-
let items = await this.get(option.key, { guildId, userId }) ?? [];
|
|
232
|
+
let items = await this.get(option.key, { guildId, userId, cat: category }) ?? [];
|
|
223
233
|
if (option.itemType === OptionTypes.Object) {
|
|
224
234
|
const props = option.itemConfig.properties;
|
|
225
235
|
if (objectListItemsNeedReshape(props, items)) {
|
|
@@ -240,7 +250,7 @@ class MenuInstance {
|
|
|
240
250
|
if (!option) {
|
|
241
251
|
throw new Error(`Option "${optionKey}" not found on page ${category}:${index}`);
|
|
242
252
|
}
|
|
243
|
-
const items = await this.get(option.key, { guildId, userId }) ?? [];
|
|
253
|
+
const items = await this.get(option.key, { guildId, userId, cat: category }) ?? [];
|
|
244
254
|
const currentValue = itemIndex != null ? items[itemIndex] : null;
|
|
245
255
|
return renderModal(
|
|
246
256
|
this.key,
|
|
@@ -256,7 +266,7 @@ class MenuInstance {
|
|
|
256
266
|
if (!option) {
|
|
257
267
|
throw new Error(`Object "${objectKey}" not found on page ${category}:${index}`);
|
|
258
268
|
}
|
|
259
|
-
const objectValue = await this.get(option.key, { guildId, userId }) ?? {};
|
|
269
|
+
const objectValue = await this.get(option.key, { guildId, userId, cat: category }) ?? {};
|
|
260
270
|
return renderObjectPage(this.key, option, objectValue, userId, category, index, objectPage);
|
|
261
271
|
}
|
|
262
272
|
|
|
@@ -270,7 +280,7 @@ class MenuInstance {
|
|
|
270
280
|
if (!subOption) {
|
|
271
281
|
throw new Error(`Sub-option "${subKey}" not found in Object "${objectKey}"`);
|
|
272
282
|
}
|
|
273
|
-
const objectValue = await this.get(objectKey, { guildId, userId }) ?? {};
|
|
283
|
+
const objectValue = await this.get(objectKey, { guildId, userId, cat: category }) ?? {};
|
|
274
284
|
const currentValue = objectValue[subKey] != null ? objectValue[subKey] : null;
|
|
275
285
|
return renderModal(
|
|
276
286
|
this.key,
|
|
@@ -290,7 +300,7 @@ class MenuInstance {
|
|
|
290
300
|
if (!subOption) {
|
|
291
301
|
throw new Error(`Sub-option "${subKey}" not found in Object "${objectKey}"`);
|
|
292
302
|
}
|
|
293
|
-
const objectValue = await this.get(objectKey, { guildId, userId }) ?? {};
|
|
303
|
+
const objectValue = await this.get(objectKey, { guildId, userId, cat: category }) ?? {};
|
|
294
304
|
const items = Array.isArray(objectValue[subKey]) ? objectValue[subKey] : [];
|
|
295
305
|
return renderObjectListPage(this.key, option, subOption, items, listPage, userId, category, index, objectPage);
|
|
296
306
|
}
|
|
@@ -301,7 +311,7 @@ class MenuInstance {
|
|
|
301
311
|
if (!option) {
|
|
302
312
|
throw new Error(`Option "${optionKey}" not found on page ${category}:${index}`);
|
|
303
313
|
}
|
|
304
|
-
let items = await this.get(option.key, { guildId, userId }) ?? [];
|
|
314
|
+
let items = await this.get(option.key, { guildId, userId, cat: category }) ?? [];
|
|
305
315
|
if (option.itemType === OptionTypes.Object) {
|
|
306
316
|
const props = option.itemConfig.properties;
|
|
307
317
|
if (objectListItemsNeedReshape(props, items)) {
|
|
@@ -328,7 +338,7 @@ class MenuInstance {
|
|
|
328
338
|
if (!property) {
|
|
329
339
|
throw new Error(`Property "${propertyKey}" not found in ObjectList "${optionKey}"`);
|
|
330
340
|
}
|
|
331
|
-
const items = await this.get(option.key, { guildId, userId }) ?? [];
|
|
341
|
+
const items = await this.get(option.key, { guildId, userId, cat: category }) ?? [];
|
|
332
342
|
const currentItem = items[itemIndex] ?? null;
|
|
333
343
|
const currentValue =
|
|
334
344
|
currentItem != null && typeof currentItem === "object"
|
|
@@ -352,7 +362,7 @@ class MenuInstance {
|
|
|
352
362
|
if (!subOption) {
|
|
353
363
|
throw new Error(`Sub-option "${subKey}" not found in Object "${objectKey}"`);
|
|
354
364
|
}
|
|
355
|
-
const objectValue = await this.get(objectKey, { guildId, userId }) ?? {};
|
|
365
|
+
const objectValue = await this.get(objectKey, { guildId, userId, cat: category }) ?? {};
|
|
356
366
|
const items = Array.isArray(objectValue[subKey]) ? objectValue[subKey] : [];
|
|
357
367
|
const currentValue = itemIndex != null ? items[itemIndex] : null;
|
|
358
368
|
return renderModal(
|
package/src/render/selectMenu.js
CHANGED
|
@@ -2,20 +2,48 @@ const { OptionTypes } = require("../utils/types");
|
|
|
2
2
|
const { renderTextDisplay, renderActionRow, renderSeparator } = require("./utils");
|
|
3
3
|
const { encodeId } = require("../utils/customIds");
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Discord snowflakes as JS numbers lose precision; DBs should store string IDs.
|
|
7
|
+
* Coerce to string and keep plausible snowflake digit strings for default_values.
|
|
8
|
+
*/
|
|
9
|
+
function collectEntityIds(value, maxCount) {
|
|
10
|
+
const max = Math.max(0, Math.min(25, Number(maxCount) || 25));
|
|
11
|
+
const out = [];
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const push = v => {
|
|
14
|
+
if (out.length >= max) return;
|
|
15
|
+
if (v == null) return;
|
|
16
|
+
let s;
|
|
17
|
+
if (typeof v === "bigint") {
|
|
18
|
+
s = v.toString();
|
|
19
|
+
} else if (typeof v === "number" && Number.isFinite(v)) {
|
|
20
|
+
s = String(Math.trunc(v));
|
|
21
|
+
} else if (typeof v === "string") {
|
|
22
|
+
s = v.trim();
|
|
23
|
+
} else if (typeof v === "object" && v !== null && v.id != null) {
|
|
24
|
+
s = String(v.id).trim();
|
|
25
|
+
} else {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!/^\d{17,22}$/.test(s)) return;
|
|
29
|
+
if (seen.has(s)) return;
|
|
30
|
+
seen.add(s);
|
|
31
|
+
out.push(s);
|
|
32
|
+
};
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
for (const item of value) {
|
|
35
|
+
push(item);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
push(value);
|
|
11
39
|
}
|
|
12
|
-
return
|
|
40
|
+
return out;
|
|
13
41
|
}
|
|
14
42
|
|
|
15
43
|
function renderSelectMenuComponent(menuKey, opt, currentValue, userId, category, page) {
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
const ctx = { u: userId };
|
|
44
|
+
// Include cat/page so each physical select is unique when paging; avoids client mix-ups
|
|
45
|
+
// when the same row position shows a different option on another page.
|
|
46
|
+
const ctx = { u: userId, cat: category, page };
|
|
19
47
|
const customId = encodeId(menuKey, [opt.key, opt.type], ctx);
|
|
20
48
|
|
|
21
49
|
const component = {
|
|
@@ -29,8 +57,14 @@ function renderSelectMenuComponent(menuKey, opt, currentValue, userId, category,
|
|
|
29
57
|
component.placeholder = opt.placeholder;
|
|
30
58
|
}
|
|
31
59
|
|
|
60
|
+
const maxDefaults = Math.min(25, Number(opt.maxValues) || 1);
|
|
61
|
+
|
|
32
62
|
if (opt.type === OptionTypes.StringSelect) {
|
|
33
|
-
const selectedValues =
|
|
63
|
+
const selectedValues = Array.isArray(currentValue)
|
|
64
|
+
? currentValue.map(String)
|
|
65
|
+
: currentValue != null
|
|
66
|
+
? [String(currentValue)]
|
|
67
|
+
: [];
|
|
34
68
|
component.options = opt.choices.map(c => ({
|
|
35
69
|
label: c.label,
|
|
36
70
|
value: c.value,
|
|
@@ -40,14 +74,14 @@ function renderSelectMenuComponent(menuKey, opt, currentValue, userId, category,
|
|
|
40
74
|
}
|
|
41
75
|
|
|
42
76
|
if (opt.type === OptionTypes.UserSelect) {
|
|
43
|
-
const ids =
|
|
77
|
+
const ids = collectEntityIds(currentValue, maxDefaults);
|
|
44
78
|
if (ids.length > 0) {
|
|
45
79
|
component.default_values = ids.map(id => ({ id, type: "user" }));
|
|
46
80
|
}
|
|
47
81
|
}
|
|
48
82
|
|
|
49
83
|
if (opt.type === OptionTypes.RoleSelect) {
|
|
50
|
-
const ids =
|
|
84
|
+
const ids = collectEntityIds(currentValue, maxDefaults);
|
|
51
85
|
if (ids.length > 0) {
|
|
52
86
|
component.default_values = ids.map(id => ({ id, type: "role" }));
|
|
53
87
|
}
|
|
@@ -57,7 +91,7 @@ function renderSelectMenuComponent(menuKey, opt, currentValue, userId, category,
|
|
|
57
91
|
if (opt.channelTypes && opt.channelTypes.length > 0) {
|
|
58
92
|
component.channel_types = opt.channelTypes;
|
|
59
93
|
}
|
|
60
|
-
const ids =
|
|
94
|
+
const ids = collectEntityIds(currentValue, maxDefaults);
|
|
61
95
|
if (ids.length > 0) {
|
|
62
96
|
component.default_values = ids.map(id => ({ id, type: "channel" }));
|
|
63
97
|
}
|
|
@@ -80,4 +114,4 @@ function renderSelectMenuOption(menuKey, opt, currentValue, userId, category, pa
|
|
|
80
114
|
return [textDisplay, actionRow, renderSeparator()];
|
|
81
115
|
}
|
|
82
116
|
|
|
83
|
-
module.exports = { renderSelectMenuOption };
|
|
117
|
+
module.exports = { renderSelectMenuOption, collectEntityIds };
|