@kevlid/discordmenus 0.1.2 → 0.1.4

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
@@ -257,3 +257,8 @@ export declare function normalizeInteraction(
257
257
  ): DiscordInteraction;
258
258
  export declare function fromEris(interaction: DiscordInteraction): DiscordInteraction;
259
259
  export declare function fromDiscordJS(interaction: DiscordInteraction): DiscordInteraction;
260
+
261
+ /** Encode option/menu keys for custom IDs (uppercase → _lowercase, spaces → _). */
262
+ export declare function formatKey(key: string): string;
263
+ /** Restore original key before get/save callbacks (inverse of formatKey). */
264
+ export declare function decodeKey(encoded: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kevlid/discordmenus",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Components v2 settings menus for Discord bots",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
@@ -312,8 +312,8 @@ async function handleComponent(interaction, menu, menu_key, renderMenu) {
312
312
  }
313
313
  }
314
314
 
315
- const category = parseInt(options.get("cat") ?? 0);
316
- const page = parseInt(options.get("page") ?? 0);
315
+ let category = parseInt(options.get("cat") ?? 0);
316
+ let page = parseInt(options.get("page") ?? 0);
317
317
  const action = values[1];
318
318
 
319
319
  let result;
@@ -362,10 +362,31 @@ async function handleComponent(interaction, menu, menu_key, renderMenu) {
362
362
  ) {
363
363
  const optionKey = values[0];
364
364
  const selected = Array.isArray(interactionData.values) ? interactionData.values : [];
365
- const opt = menu.getOption(category, page, optionKey);
365
+ let opt = menu.getOption(category, page, optionKey);
366
+ if (!opt) {
367
+ const found = menu.findOption(optionKey);
368
+ if (found) {
369
+ opt = found.option;
370
+ category = found.category;
371
+ page = found.page;
372
+ }
373
+ }
366
374
  const maxSel = Number(opt?.maxValues);
367
375
  const multi = Number.isFinite(maxSel) && maxSel > 1;
368
- const saveValue = multi ? [...selected] : selected[0] ?? null;
376
+ let saveValue = multi ? [...selected] : selected[0] ?? null;
377
+ if (
378
+ !multi &&
379
+ saveValue == null &&
380
+ opt?.type === OptionTypes.StringSelect &&
381
+ Number(opt.minValues ?? 1) >= 1 &&
382
+ Array.isArray(opt.choices) &&
383
+ opt.choices.length === 1
384
+ ) {
385
+ const only = opt.choices[0]?.value;
386
+ if (only != null && only !== "") {
387
+ saveValue = only;
388
+ }
389
+ }
369
390
  await menu.save(optionKey, saveValue, { guildId, userId });
370
391
  result = updateMessage(
371
392
  await renderMenu(menu_key, { category, index: page, userId, guildId }),
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ const { OptionTypes } = require("./utils/types");
4
4
  const { CustomIdPrefix, EphemeralMessageFlag } = require("./utils/constants");
5
5
  const { MenuStorage, MemoryMenuStorage } = require("./utils/storage");
6
6
  const { normalizeInteraction, fromEris, fromDiscordJS } = require("./utils/adapters");
7
+ const { formatKey, decodeKey } = require("./utils/formatKey");
7
8
 
8
9
  module.exports = {
9
10
  MenuManager,
@@ -16,4 +17,6 @@ module.exports = {
16
17
  normalizeInteraction,
17
18
  fromEris,
18
19
  fromDiscordJS,
19
- }
20
+ formatKey,
21
+ decodeKey,
22
+ };
@@ -6,6 +6,7 @@ const { OptionTypes } = require("./utils/types");
6
6
  const { PageRenderOverhead, OptionRenderCost, MaxComponents, ComponentsV2MessageFlags } = require("./utils/constants");
7
7
  const { validateComponents } = require("./utils/componentsValidate");
8
8
  const { arrayClone, normalizeObjectListItems, objectListItemsNeedReshape } = require("./utils/helpers");
9
+ const { decodeKey } = require("./utils/formatKey");
9
10
 
10
11
  class MenuInstance {
11
12
  constructor(builder, options = {}) {
@@ -41,11 +42,11 @@ class MenuInstance {
41
42
  }
42
43
 
43
44
  async get(key, ctx = {}) {
44
- return this.getData(key, ctx);
45
+ return this.getData(decodeKey(key), ctx);
45
46
  }
46
47
 
47
48
  async save(key, value, ctx = {}) {
48
- return this.saveData(key, value, ctx);
49
+ return this.saveData(decodeKey(key), value, ctx);
49
50
  }
50
51
 
51
52
  ensureBuilt() {
@@ -107,6 +108,20 @@ class MenuInstance {
107
108
  return page.options.find(o => o.key === optionKey) ?? null;
108
109
  }
109
110
 
111
+ findOption(optionKey) {
112
+ for (let cat = 0; cat < this.pages.length; cat++) {
113
+ const categoryPages = this.pages[cat];
114
+ for (let page = 0; page < categoryPages.length; page++) {
115
+ const p = categoryPages[page];
116
+ const opt = p.options?.find(o => o.key === optionKey) ?? null;
117
+ if (opt) {
118
+ return { option: opt, category: cat, page };
119
+ }
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+
110
125
  getNavigationTargets(category, index) {
111
126
  const categoryPages = this.pages[category];
112
127
  const hasPrevCategory = category > 0;
@@ -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 };
@@ -37,7 +37,15 @@ function normalizeInteraction(interaction) {
37
37
  customId = source.id;
38
38
  }
39
39
 
40
- let values = source.data?.values ?? source.values ?? interaction?.data?.values ?? interaction?.values ?? [];
40
+ // Prefer live interaction: toJSON() often omits or strips data.values for select menus,
41
+ // which breaks saves (especially single-value selects).
42
+ let values = interaction?.data?.values;
43
+ if (!Array.isArray(values) && interaction != null && typeof interaction.values !== "undefined") {
44
+ values = interaction.values;
45
+ }
46
+ if (!Array.isArray(values)) {
47
+ values = source.data?.values ?? source.values ?? [];
48
+ }
41
49
  if (!Array.isArray(values)) {
42
50
  values = [];
43
51
  }
@@ -1 +1,46 @@
1
- function formatKey(key) {
2
1
  if (typeof key !== "string") throw new TypeError("Key must be a string");
3
2
  return key.toLowerCase().replace(" ", "_");
4
3
  formatKey
4
+ /**
5
+ * Encodes a key for custom IDs (Discord is case-insensitive in some paths; this keeps uniqueness).
6
+ * Each uppercase letter becomes "_" + its lowercase form (e.g. guildId → guild_id).
7
+ * Spaces become "_".
8
+ * Lowercase letters, digits, and underscores are kept as-is (underscores are not escaped).
9
+ */
10
+ function formatKey(key) {
11
+ if (typeof key !== "string") throw new TypeError("Key must be a string");
12
+ const s = String(key).replace(/ /g, "_");
13
+ let out = "";
14
+ for (const c of s) {
15
+ if (c >= "A" && c <= "Z") {
16
+ out += "_" + c.toLowerCase();
17
+ } else {
18
+ out += c;
19
+ }
20
+ }
21
+ return out;
22
+ }
23
+
24
+ /**
25
+ * Reverses formatKey: "_" followed by a-z becomes A–Z. Literal "_" + letter in keys that were
26
+ * not produced by an uppercase letter may decode incorrectly; prefer camelCase/PascalCase keys.
27
+ */
28
+ function decodeKey(encoded) {
29
+ if (typeof encoded !== "string") throw new TypeError("Key must be a string");
30
+ let out = "";
31
+ for (let i = 0; i < encoded.length; i++) {
32
+ const c = encoded[i];
33
+ if (c === "_" && i + 1 < encoded.length) {
34
+ const next = encoded[i + 1];
35
+ if (next >= "a" && next <= "z") {
36
+ out += next.toUpperCase();
37
+ i++;
38
+ continue;
39
+ }
40
+ }
41
+ out += c;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ module.exports = {
47
+ formatKey,
48
+ decodeKey,
49
+ };