@kevlid/discordmenus 0.1.5 → 0.1.7

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # @kevlid/discordmenus
2
2
 
3
+ > ⚠️ Usage is __**NOT**__ recommended
4
+
3
5
  ```js
4
6
  const fs = require("fs");
5
7
  const path = require("path");
@@ -58,3 +60,8 @@ client.on("interactionCreate", async (interaction) => {
58
60
 
59
61
  client.login(process.env.BOT_TOKEN);
60
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/index.d.ts CHANGED
@@ -265,5 +265,5 @@ export declare function fromDiscordJS(interaction: DiscordInteraction): DiscordI
265
265
 
266
266
  /** Encode option/menu keys for custom IDs (uppercase → _lowercase, spaces → _). */
267
267
  export declare function formatKey(key: string): string;
268
- /** Restore original key before get/save callbacks (inverse of formatKey). */
268
+ /** No-op: casing is not reconstructed anywhere. */
269
269
  export declare function decodeKey(encoded: string): string;
package/package.json CHANGED
@@ -1,18 +1,11 @@
1
1
  {
2
2
  "name": "@kevlid/discordmenus",
3
- "version": "0.1.5",
4
- "description": "Components v2 settings menus for Discord bots",
3
+ "version": "0.1.7",
4
+ "description": "some menu stuff for discord stuff",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
7
7
  "author": "kevlid",
8
8
  "license": "ISC",
9
- "keywords": [
10
- "discord",
11
- "discord.js",
12
- "menu",
13
- "typescript",
14
- "builder"
15
- ],
16
9
  "files": [
17
10
  "src",
18
11
  "index.d.ts",
@@ -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 &&
@@ -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
- function toIdArray(currentValue) {
6
- if (Array.isArray(currentValue)) {
7
- return currentValue;
8
- }
9
- if (currentValue != null) {
10
- return [currentValue];
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
- // Stateless: do not depend on current rendered page in the custom id.
17
- // The handler can locate the option by key in the built menu.
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 = toIdArray(currentValue);
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 = toIdArray(currentValue);
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 = toIdArray(currentValue);
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 = toIdArray(currentValue);
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 };
@@ -19,25 +19,13 @@ function formatKey(key) {
19
19
  }
20
20
 
21
21
  /**
22
- * Reverses formatKey: "_" followed by a-z becomes A–Z. Literal "_" + letter in keys that were
23
- * not produced by an uppercase letter may decode incorrectly; prefer camelCase/PascalCase keys.
22
+ * NOTE: decodeKey is intentionally a no-op.
23
+ * Keys are encoded with `formatKey()` for stability in custom IDs, and we do not attempt to
24
+ * reconstruct original casing anywhere (including get/save contexts).
24
25
  */
25
26
  function decodeKey(encoded) {
26
27
  if (typeof encoded !== "string") throw new TypeError("Key must be a string");
27
- let out = "";
28
- for (let i = 0; i < encoded.length; i++) {
29
- const c = encoded[i];
30
- if (c === "_" && i + 1 < encoded.length) {
31
- const next = encoded[i + 1];
32
- if (next >= "a" && next <= "z") {
33
- out += next.toUpperCase();
34
- i++;
35
- continue;
36
- }
37
- }
38
- out += c;
39
- }
40
- return out;
28
+ return String(encoded);
41
29
  }
42
30
 
43
31
  module.exports = {