@kevlid/discordmenus 0.1.3 → 0.1.5

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/index.d.ts CHANGED
@@ -3,10 +3,15 @@ export type GetFunction = (
3
3
  options?: Record<string, unknown>,
4
4
  ) => unknown | Promise<unknown>;
5
5
 
6
+ /** Passed to save(): decoded option key, value, and context (includes category = decoded category key). */
6
7
  export type SaveFunction = (
7
8
  key: string,
8
9
  value: unknown,
9
- options?: Record<string, unknown>,
10
+ options?: Record<string, unknown> & {
11
+ guildId?: string | null;
12
+ userId?: string | null;
13
+ category?: string | null;
14
+ },
10
15
  ) => unknown | Promise<unknown>;
11
16
 
12
17
  export type PermissionResolvable = string | number | bigint;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kevlid/discordmenus",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Components v2 settings menus for Discord bots",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
@@ -77,7 +77,11 @@ async function handleListBranch({
77
77
  arrayClone(await menu.get(optionKey, { guildId, userId })),
78
78
  );
79
79
  addItems.push(shapeObjectListItem(props, {}));
80
- await menu.save(optionKey, addItems, { guildId, userId });
80
+ await menu.save(optionKey, addItems, {
81
+ guildId,
82
+ userId,
83
+ category: menu.getCategoryKey(category),
84
+ });
81
85
  return updateMessage(
82
86
  await menu.renderListObjectItemPage(optionKey, addItems.length - 1, {
83
87
  ...menuCtx,
@@ -116,11 +120,11 @@ async function handleListBranch({
116
120
  const currentItem = doneItems[itemIndex];
117
121
  if (objectListItemFailsRequired(option, currentItem)) {
118
122
  doneItems.splice(itemIndex, 1);
119
- await menu.save(
120
- optionKey,
121
- normalizeObjectListItems(option.itemConfig.properties, doneItems),
122
- { guildId, userId },
123
- );
123
+ await menu.save(optionKey, normalizeObjectListItems(option.itemConfig.properties, doneItems), {
124
+ guildId,
125
+ userId,
126
+ category: menu.getCategoryKey(category),
127
+ });
124
128
  }
125
129
  return updateMessage(await renderMenu(menu_key, menuCtx));
126
130
  }
@@ -151,7 +155,11 @@ async function handleListBranch({
151
155
  ...asObject(boolItems[itemIndex]),
152
156
  [propertyKey]: newBoolValue,
153
157
  });
154
- await menu.save(optionKey, boolItems, { guildId, userId });
158
+ await menu.save(optionKey, boolItems, {
159
+ guildId,
160
+ userId,
161
+ category: menu.getCategoryKey(category),
162
+ });
155
163
  return updateMessage(
156
164
  await menu.renderListObjectItemPage(optionKey, itemIndex, { ...menuCtx, listPage: sub }),
157
165
  );
@@ -165,7 +173,11 @@ async function handleListBranch({
165
173
  deleteItems = normalizeObjectListItems(props, deleteItems);
166
174
  }
167
175
  deleteItems.splice(itemIndex, 1);
168
- await menu.save(optionKey, deleteItems, { guildId, userId });
176
+ await menu.save(optionKey, deleteItems, {
177
+ guildId,
178
+ userId,
179
+ category: menu.getCategoryKey(category),
180
+ });
169
181
  if (objectItems) {
170
182
  if (deleteItems.length <= 0) {
171
183
  return updateMessage(await renderMenu(menu_key, menuCtx));
@@ -222,7 +234,11 @@ async function handleObjectBranch({
222
234
  const defaults = boolObjectOption ? objectDefaultsFromKeys(boolObjectOption.options) : {};
223
235
  const updatedObj = Object.assign(defaults, baseObj);
224
236
  updatedObj[subKey] = newValue;
225
- await menu.save(objectKey, updatedObj, { guildId, userId });
237
+ await menu.save(objectKey, updatedObj, {
238
+ guildId,
239
+ userId,
240
+ category: menu.getCategoryKey(category),
241
+ });
226
242
  return updateMessage(await menu.renderObjectPage(objectKey, objectCtx));
227
243
  }
228
244
  if (subAction === Presets.subModal) {
@@ -276,7 +292,11 @@ async function handleObjectBranch({
276
292
  const obj = asObject(await menu.get(objectKey, { guildId, userId }));
277
293
  const items = arrayClone(obj[subKey]);
278
294
  items.splice(itemIndex, 1);
279
- await menu.save(objectKey, Object.assign({}, obj, { [subKey]: items }), { guildId, userId });
295
+ await menu.save(objectKey, Object.assign({}, obj, { [subKey]: items }), {
296
+ guildId,
297
+ userId,
298
+ category: menu.getCategoryKey(category),
299
+ });
280
300
  return updateMessage(
281
301
  await menu.renderObjectListPage(objectKey, subKey, sub, { ...menuCtx, objectPage }),
282
302
  );
@@ -312,8 +332,8 @@ async function handleComponent(interaction, menu, menu_key, renderMenu) {
312
332
  }
313
333
  }
314
334
 
315
- const category = parseInt(options.get("cat") ?? 0);
316
- const page = parseInt(options.get("page") ?? 0);
335
+ let category = parseInt(options.get("cat") ?? 0);
336
+ let page = parseInt(options.get("page") ?? 0);
317
337
  const action = values[1];
318
338
 
319
339
  let result;
@@ -325,7 +345,11 @@ async function handleComponent(interaction, menu, menu_key, renderMenu) {
325
345
  } else if (action === OptionTypes.String || action === OptionTypes.Number) {
326
346
  result = openModal(await menu.renderModal(values[0], { category, index: page, guildId }));
327
347
  } else if (action === OptionTypes.Boolean) {
328
- await menu.save(values[0], values[2] === "true", { guildId, userId });
348
+ await menu.save(values[0], values[2] === "true", {
349
+ guildId,
350
+ userId,
351
+ category: menu.getCategoryKey(category),
352
+ });
329
353
  result = updateMessage(
330
354
  await renderMenu(menu_key, { category, index: page, userId, guildId }),
331
355
  );
@@ -362,7 +386,15 @@ async function handleComponent(interaction, menu, menu_key, renderMenu) {
362
386
  ) {
363
387
  const optionKey = values[0];
364
388
  const selected = Array.isArray(interactionData.values) ? interactionData.values : [];
365
- const opt = menu.getOption(category, page, optionKey);
389
+ let opt = menu.getOption(category, page, optionKey);
390
+ if (!opt) {
391
+ const found = menu.findOption(optionKey);
392
+ if (found) {
393
+ opt = found.option;
394
+ category = found.category;
395
+ page = found.page;
396
+ }
397
+ }
366
398
  const maxSel = Number(opt?.maxValues);
367
399
  const multi = Number.isFinite(maxSel) && maxSel > 1;
368
400
  let saveValue = multi ? [...selected] : selected[0] ?? null;
@@ -379,7 +411,11 @@ async function handleComponent(interaction, menu, menu_key, renderMenu) {
379
411
  saveValue = only;
380
412
  }
381
413
  }
382
- await menu.save(optionKey, saveValue, { guildId, userId });
414
+ await menu.save(optionKey, saveValue, {
415
+ guildId,
416
+ userId,
417
+ category: menu.getCategoryKey(category),
418
+ });
383
419
  result = updateMessage(
384
420
  await renderMenu(menu_key, { category, index: page, userId, guildId }),
385
421
  );
@@ -65,7 +65,11 @@ async function handleListModal(menu, values, options, rawValue, category, page,
65
65
  ...asObject(objItems[objItemIndex]),
66
66
  [property.key]: parsedPropertyValue,
67
67
  });
68
- await menu.save(optionKey, objItems, { guildId, userId });
68
+ await menu.save(optionKey, objItems, {
69
+ guildId,
70
+ userId,
71
+ category: menu.getCategoryKey(category),
72
+ });
69
73
  return updateMessage(
70
74
  await menu.renderListObjectItemPage(optionKey, objItemIndex, { ...menuCtx, listPage: sub }),
71
75
  );
@@ -95,7 +99,11 @@ async function handleListModal(menu, values, options, rawValue, category, page,
95
99
  } else {
96
100
  throw new Error(`Unknown list sub-action "${subAction}"`);
97
101
  }
98
- await menu.save(optionKey, items, { guildId, userId });
102
+ await menu.save(optionKey, items, {
103
+ guildId,
104
+ userId,
105
+ category: menu.getCategoryKey(category),
106
+ });
99
107
  return updateMessage(await menu.renderListPage(optionKey, sub, menuCtx));
100
108
  }
101
109
 
@@ -146,7 +154,11 @@ async function handleObjectModal(menu, values, options, rawValue, category, page
146
154
  const defaults = objectDefaultsFromKeys(option.options);
147
155
  const updatedObj = Object.assign(defaults, baseObj);
148
156
  updatedObj[subKey] = parsedValue;
149
- await menu.save(optionKey, updatedObj, { guildId, userId });
157
+ await menu.save(optionKey, updatedObj, {
158
+ guildId,
159
+ userId,
160
+ category: menu.getCategoryKey(category),
161
+ });
150
162
  return updateMessage(await menu.renderObjectPage(optionKey, menuCtx));
151
163
  }
152
164
 
@@ -177,7 +189,11 @@ async function handleObjectModal(menu, values, options, rawValue, category, page
177
189
  } else {
178
190
  throw new Error(`Unknown list action "${listAction}"`);
179
191
  }
180
- await menu.save(optionKey, Object.assign({}, baseObj, { [subKey]: items }), { guildId, userId });
192
+ await menu.save(optionKey, Object.assign({}, baseObj, { [subKey]: items }), {
193
+ guildId,
194
+ userId,
195
+ category: menu.getCategoryKey(category),
196
+ });
181
197
  return updateMessage(
182
198
  await menu.renderObjectListPage(optionKey, subKey, sub, menuCtx),
183
199
  );
@@ -214,7 +230,11 @@ async function handleModal(interaction, menu, menu_key, renderMenu) {
214
230
  return handleObjectModal(menu, values, options, rawValue, category, page, guildId, userId);
215
231
  }
216
232
  if (optionType === OptionTypes.String) {
217
- await menu.save(values[0], rawValue, { guildId, userId });
233
+ await menu.save(values[0], rawValue, {
234
+ guildId,
235
+ userId,
236
+ category: menu.getCategoryKey(category),
237
+ });
218
238
  return updateMessage(await renderMenu(menu_key, menuCtx));
219
239
  }
220
240
  if (optionType === OptionTypes.Number) {
@@ -222,7 +242,11 @@ async function handleModal(interaction, menu, menu_key, renderMenu) {
222
242
  if (Number.isNaN(value)) {
223
243
  throw new Error("Invalid number");
224
244
  }
225
- await menu.save(values[0], value, { guildId, userId });
245
+ await menu.save(values[0], value, {
246
+ guildId,
247
+ userId,
248
+ category: menu.getCategoryKey(category),
249
+ });
226
250
  return updateMessage(await renderMenu(menu_key, menuCtx));
227
251
  }
228
252
  throw new Error(`Option type "${optionType}" does not support modal input`);
@@ -58,6 +58,7 @@ class MenuInstance {
58
58
  build() {
59
59
  const built = this.builder.build();
60
60
  this.pages = [];
61
+ this.categoryKeys = [];
61
62
 
62
63
  for (const category of built.categories) {
63
64
  const categoryPages = [];
@@ -84,6 +85,7 @@ class MenuInstance {
84
85
  }
85
86
  if (categoryPages.length > 0) {
86
87
  this.pages.push(categoryPages);
88
+ this.categoryKeys.push(decodeKey(category.key));
87
89
  }
88
90
  }
89
91
 
@@ -108,6 +110,28 @@ class MenuInstance {
108
110
  return page.options.find(o => o.key === optionKey) ?? null;
109
111
  }
110
112
 
113
+ getCategoryKey(categoryIndex) {
114
+ this.ensureBuilt();
115
+ if (typeof categoryIndex !== "number" || categoryIndex < 0 || categoryIndex >= this.categoryKeys.length) {
116
+ return null;
117
+ }
118
+ return this.categoryKeys[categoryIndex] ?? null;
119
+ }
120
+
121
+ findOption(optionKey) {
122
+ for (let cat = 0; cat < this.pages.length; cat++) {
123
+ const categoryPages = this.pages[cat];
124
+ for (let page = 0; page < categoryPages.length; page++) {
125
+ const p = categoryPages[page];
126
+ const opt = p.options?.find(o => o.key === optionKey) ?? null;
127
+ if (opt) {
128
+ return { option: opt, category: cat, page };
129
+ }
130
+ }
131
+ }
132
+ return null;
133
+ }
134
+
111
135
  getNavigationTargets(category, index) {
112
136
  const categoryPages = this.pages[category];
113
137
  const hasPrevCategory = category > 0;
@@ -200,7 +224,11 @@ class MenuInstance {
200
224
  const props = option.itemConfig.properties;
201
225
  if (objectListItemsNeedReshape(props, items)) {
202
226
  items = normalizeObjectListItems(props, arrayClone(items));
203
- await this.save(option.key, items, { guildId, userId });
227
+ await this.save(option.key, items, {
228
+ guildId,
229
+ userId,
230
+ category: this.getCategoryKey(category),
231
+ });
204
232
  }
205
233
  }
206
234
  return renderListPage(this.key, option, items, listPage, userId, category, index);
@@ -278,7 +306,11 @@ class MenuInstance {
278
306
  const props = option.itemConfig.properties;
279
307
  if (objectListItemsNeedReshape(props, items)) {
280
308
  items = normalizeObjectListItems(props, arrayClone(items));
281
- await this.save(option.key, items, { guildId, userId });
309
+ await this.save(option.key, items, {
310
+ guildId,
311
+ userId,
312
+ category: this.getCategoryKey(category),
313
+ });
282
314
  }
283
315
  }
284
316
  const safeIndex = items.length <= 0 ? 0 : Math.max(0, Math.min(itemIndex, items.length - 1));
@@ -1 +1,83 @@
1
- const { OptionTypes } = require("../utils/types");
2
1
  if (Array.isArray(currentValue)) {
3
2
  return currentValue;
4
3
  }
5
4
  if (currentValue != null) {
6
5
  return [currentValue];
7
6
  }
8
7
  return [];
9
8
  const ctx = { userId, category, page };
10
9
  const customId = encodeId(menuKey, [opt.key, opt.type], ctx);
11
10
  const component = {
12
11
  type: opt.selectType,
13
12
  custom_id: customId,
14
13
  min_values: opt.minValues,
15
14
  max_values: opt.maxValues,
16
15
  };
17
16
  if (opt.placeholder != null) {
18
17
  component.placeholder = opt.placeholder;
19
18
  }
20
19
  if (opt.type === OptionTypes.StringSelect) {
21
20
  const selectedValues = toIdArray(currentValue);
22
21
  component.options = opt.choices.map(c => ({
23
22
  label: c.label,
24
23
  value: c.value,
25
24
  description: c.description,
26
25
  default: selectedValues.includes(c.value),
27
26
  }));
28
27
  }
29
28
  if (opt.type === OptionTypes.UserSelect) {
30
29
  const ids = toIdArray(currentValue);
31
30
  if (ids.length > 0) {
32
31
  component.default_values = ids.map(id => ({ id, type: "user" }));
33
32
  }
34
33
  }
35
34
  if (opt.type === OptionTypes.RoleSelect) {
36
35
  const ids = toIdArray(currentValue);
37
36
  if (ids.length > 0) {
38
37
  component.default_values = ids.map(id => ({ id, type: "role" }));
39
38
  }
40
39
  }
41
40
  if (opt.type === OptionTypes.ChannelSelect) {
42
41
  if (opt.channelTypes && opt.channelTypes.length > 0) {
43
42
  component.channel_types = opt.channelTypes;
44
43
  }
45
44
  const ids = toIdArray(currentValue);
46
45
  if (ids.length > 0) {
47
46
  component.default_values = ids.map(id => ({ id, type: "channel" }));
48
47
  }
49
48
  }
50
49
  return component;
51
50
  const lines = [`**${opt.title}**`];
52
51
  if (opt.description) {
53
52
  lines.push(`-# ${opt.description}`);
54
53
  }
55
54
  const textDisplay = renderTextDisplay(lines.join("\n"));
56
55
  const selectMenu = renderSelectMenuComponent(menuKey, opt, currentValue, userId, category, page);
57
56
  const actionRow = renderActionRow(selectMenu);
58
57
  return [textDisplay, actionRow, renderSeparator()];
58
+ const { OptionTypes } = require("../utils/types");
59
+ const { renderTextDisplay, renderActionRow, renderSeparator } = require("./utils");
60
+ const { encodeId } = require("../utils/customIds");
61
+
62
+ function toIdArray(currentValue) {
63
+ if (Array.isArray(currentValue)) {
64
+ return currentValue;
65
+ }
66
+ if (currentValue != null) {
67
+ return [currentValue];
68
+ }
69
+ return [];
70
+ }
71
+
72
+ function renderSelectMenuComponent(menuKey, opt, currentValue, userId, category, page) {
73
+ // Stateless: do not depend on current rendered page in the custom id.
74
+ // The handler can locate the option by key in the built menu.
75
+ const ctx = { u: userId };
76
+ const customId = encodeId(menuKey, [opt.key, opt.type], ctx);
77
+
78
+ const component = {
79
+ type: opt.selectType,
80
+ custom_id: customId,
81
+ min_values: opt.minValues,
82
+ max_values: opt.maxValues,
83
+ };
84
+
85
+ if (opt.placeholder != null) {
86
+ component.placeholder = opt.placeholder;
87
+ }
88
+
89
+ if (opt.type === OptionTypes.StringSelect) {
90
+ const selectedValues = toIdArray(currentValue);
91
+ component.options = opt.choices.map(c => ({
92
+ label: c.label,
93
+ value: c.value,
94
+ description: c.description,
95
+ default: selectedValues.includes(c.value),
96
+ }));
97
+ }
98
+
99
+ if (opt.type === OptionTypes.UserSelect) {
100
+ const ids = toIdArray(currentValue);
101
+ if (ids.length > 0) {
102
+ component.default_values = ids.map(id => ({ id, type: "user" }));
103
+ }
104
+ }
105
+
106
+ if (opt.type === OptionTypes.RoleSelect) {
107
+ const ids = toIdArray(currentValue);
108
+ if (ids.length > 0) {
109
+ component.default_values = ids.map(id => ({ id, type: "role" }));
110
+ }
111
+ }
112
+
113
+ if (opt.type === OptionTypes.ChannelSelect) {
114
+ if (opt.channelTypes && opt.channelTypes.length > 0) {
115
+ component.channel_types = opt.channelTypes;
116
+ }
117
+ const ids = toIdArray(currentValue);
118
+ if (ids.length > 0) {
119
+ component.default_values = ids.map(id => ({ id, type: "channel" }));
120
+ }
121
+ }
122
+
123
+ return component;
124
+ }
125
+
126
+ function renderSelectMenuOption(menuKey, opt, currentValue, userId, category, page) {
127
+ const lines = [`**${opt.title}**`];
128
+
129
+ if (opt.description) {
130
+ lines.push(`-# ${opt.description}`);
131
+ }
132
+
133
+ const textDisplay = renderTextDisplay(lines.join("\n"));
134
+ const selectMenu = renderSelectMenuComponent(menuKey, opt, currentValue, userId, category, page);
135
+ const actionRow = renderActionRow(selectMenu);
136
+
137
+ return [textDisplay, actionRow, renderSeparator()];
138
+ }
139
+
140
+ module.exports = { renderSelectMenuOption };