@kevlid/discordmenus 0.0.2
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/LICENSE +15 -0
- package/README.md +60 -0
- package/index.d.ts +255 -0
- package/package.json +27 -0
- package/src/builder.js +50 -0
- package/src/handle/component.js +379 -0
- package/src/handle/modal.js +231 -0
- package/src/index.js +19 -0
- package/src/menuBuilder.js +157 -0
- package/src/menuInstance.js +334 -0
- package/src/menuManager.js +83 -0
- package/src/options/boolean.js +38 -0
- package/src/options/category.js +27 -0
- package/src/options/list.js +105 -0
- package/src/options/number.js +50 -0
- package/src/options/object.js +74 -0
- package/src/options/selectMenu.js +132 -0
- package/src/options/string.js +50 -0
- package/src/render/list.js +347 -0
- package/src/render/modal.js +76 -0
- package/src/render/object.js +417 -0
- package/src/render/page.js +142 -0
- package/src/render/selectMenu.js +1 -0
- package/src/render/types.js +1 -0
- package/src/render/utils.js +60 -0
- package/src/utils/adapters.js +98 -0
- package/src/utils/componentsValidate.js +23 -0
- package/src/utils/constants.js +11 -0
- package/src/utils/customIds.js +67 -0
- package/src/utils/formatKey.js +1 -0
- package/src/utils/helpers.js +100 -0
- package/src/utils/parseModal.js +1 -0
- package/src/utils/storage.js +131 -0
- package/src/utils/types.js +34 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
const { ComponentTypes, ButtonStyles } = require("./types");
|
|
2
|
+
const { OptionTypes } = require("../utils/types");
|
|
3
|
+
const {
|
|
4
|
+
renderButton,
|
|
5
|
+
renderSeparator,
|
|
6
|
+
renderTextDisplay,
|
|
7
|
+
renderActionRow,
|
|
8
|
+
renderHeader,
|
|
9
|
+
renderSubOptionSection,
|
|
10
|
+
validateString,
|
|
11
|
+
validateNumber,
|
|
12
|
+
} = require("./utils");
|
|
13
|
+
const { encodeId, Presets } = require("../utils/customIds");
|
|
14
|
+
const { MaxComponents, ObjectPageOverhead, ComponentsV2MessageFlags } = require("../utils/constants");
|
|
15
|
+
const ModalInputPrefix = "m";
|
|
16
|
+
|
|
17
|
+
function renderObjectPage(menuKey, option, objectValue, userId, category, page, objectPageIndex = 0) {
|
|
18
|
+
const maxPageCost = Math.max(1, MaxComponents - ObjectPageOverhead);
|
|
19
|
+
|
|
20
|
+
const objectPages = [];
|
|
21
|
+
let currentObjectPage = [];
|
|
22
|
+
let currentCost = 0;
|
|
23
|
+
|
|
24
|
+
for (const subOption of option.options) {
|
|
25
|
+
let subOptionCost = Number(subOption.cost);
|
|
26
|
+
if (!Number.isFinite(subOptionCost) || subOptionCost <= 0) {
|
|
27
|
+
subOptionCost = 2;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (currentObjectPage.length > 0 && currentCost + subOptionCost > maxPageCost) {
|
|
31
|
+
objectPages.push(currentObjectPage);
|
|
32
|
+
currentObjectPage = [];
|
|
33
|
+
currentCost = 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
currentObjectPage.push(subOption);
|
|
37
|
+
currentCost += subOptionCost;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (currentObjectPage.length > 0) {
|
|
41
|
+
objectPages.push(currentObjectPage);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (objectPages.length <= 0) {
|
|
45
|
+
objectPages.push([]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const totalObjectPages = objectPages.length;
|
|
49
|
+
const clampedObjectPage = Math.max(0, Math.min(objectPageIndex, totalObjectPages - 1));
|
|
50
|
+
|
|
51
|
+
const ctx = { u: userId, cat: category, page, op: clampedObjectPage };
|
|
52
|
+
const genId = (...v) => encodeId(menuKey, v, ctx);
|
|
53
|
+
|
|
54
|
+
const headerLines = [`## ${option.title}`];
|
|
55
|
+
const mainDesc = (option.description && String(option.description).trim()) || "";
|
|
56
|
+
if (mainDesc) {
|
|
57
|
+
headerLines.push(`-# ${mainDesc}`);
|
|
58
|
+
}
|
|
59
|
+
if (totalObjectPages > 1) {
|
|
60
|
+
headerLines.push(`-# Page ${clampedObjectPage + 1}/${totalObjectPages}`);
|
|
61
|
+
}
|
|
62
|
+
const headerDisplay = renderTextDisplay(headerLines.join("\n"));
|
|
63
|
+
const divider = renderSeparator(true);
|
|
64
|
+
let containerComponents = [headerDisplay, divider];
|
|
65
|
+
const pageOptions = objectPages[clampedObjectPage];
|
|
66
|
+
for (const subOption of pageOptions) {
|
|
67
|
+
let subValue;
|
|
68
|
+
if (objectValue != null && typeof objectValue === "object") {
|
|
69
|
+
subValue = objectValue[subOption.key];
|
|
70
|
+
} else {
|
|
71
|
+
subValue = null;
|
|
72
|
+
}
|
|
73
|
+
const subComponent = renderObjectSubOption(menuKey, option.key, subOption, subValue, ctx);
|
|
74
|
+
containerComponents.push(subComponent);
|
|
75
|
+
}
|
|
76
|
+
containerComponents = containerComponents.filter(c => c && typeof c === "object" && "type" in c);
|
|
77
|
+
|
|
78
|
+
const hasPrevPage = clampedObjectPage > 0;
|
|
79
|
+
const hasNextPage = clampedObjectPage < totalObjectPages - 1;
|
|
80
|
+
|
|
81
|
+
const navRow = renderActionRow(
|
|
82
|
+
renderButton(genId(option.key, OptionTypes.Object, Presets.nav, String(0), "first"), "<<", ButtonStyles.Secondary, !hasPrevPage),
|
|
83
|
+
renderButton(genId(option.key, OptionTypes.Object, Presets.nav, String(clampedObjectPage - 1)), "<", ButtonStyles.Secondary, !hasPrevPage),
|
|
84
|
+
renderButton(genId(option.key, OptionTypes.Object, Presets.done), "Done", ButtonStyles.Secondary),
|
|
85
|
+
renderButton(genId(option.key, OptionTypes.Object, Presets.nav, String(clampedObjectPage + 1)), ">", ButtonStyles.Secondary, !hasNextPage),
|
|
86
|
+
renderButton(genId(option.key, OptionTypes.Object, Presets.nav, String(totalObjectPages - 1), "last"), ">>", ButtonStyles.Secondary, !hasNextPage),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
flags: ComponentsV2MessageFlags,
|
|
91
|
+
components: [
|
|
92
|
+
{ type: ComponentTypes.Container, components: containerComponents },
|
|
93
|
+
navRow,
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderObjectSubOption(menuKey, objectKey, subOption, currentValue, ctx) {
|
|
99
|
+
const genId = (...v) => encodeId(menuKey, v, ctx);
|
|
100
|
+
|
|
101
|
+
const titleSuffix = subOption.required ? " (required)" : "";
|
|
102
|
+
const lines = [`**${subOption.title}**${titleSuffix}`];
|
|
103
|
+
|
|
104
|
+
if (subOption.description) {
|
|
105
|
+
lines.push(`-# ${subOption.description}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (subOption.preview) {
|
|
109
|
+
if (subOption.type === OptionTypes.List) {
|
|
110
|
+
let count;
|
|
111
|
+
if (Array.isArray(currentValue)) {
|
|
112
|
+
count = currentValue.length;
|
|
113
|
+
} else {
|
|
114
|
+
count = 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let pluralSuffix;
|
|
118
|
+
if (count !== 1) {
|
|
119
|
+
pluralSuffix = "s";
|
|
120
|
+
} else {
|
|
121
|
+
pluralSuffix = "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (count > 0) {
|
|
125
|
+
lines.push(`\`${count} Item${pluralSuffix}\``);
|
|
126
|
+
} else {
|
|
127
|
+
lines.push("`(empty)`");
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
if (currentValue != null) {
|
|
131
|
+
lines.push(`\`${currentValue}\``);
|
|
132
|
+
} else if (!subOption.required) {
|
|
133
|
+
lines.push("`(not set)`");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const textContent = renderTextDisplay(lines.join("\n"));
|
|
139
|
+
let button;
|
|
140
|
+
|
|
141
|
+
if (subOption.type === OptionTypes.String || subOption.type === OptionTypes.Number) {
|
|
142
|
+
button = renderButton(genId(objectKey, OptionTypes.Object, Presets.subModal, subOption.key), "Edit", ButtonStyles.Secondary);
|
|
143
|
+
} else if (subOption.type === OptionTypes.Boolean) {
|
|
144
|
+
let newValue;
|
|
145
|
+
if (currentValue) {
|
|
146
|
+
newValue = "false";
|
|
147
|
+
} else {
|
|
148
|
+
newValue = "true";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let label;
|
|
152
|
+
if (currentValue) {
|
|
153
|
+
label = subOption.trueLabel ?? "True";
|
|
154
|
+
} else {
|
|
155
|
+
label = subOption.falseLabel ?? "False";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
button = renderButton(genId(objectKey, OptionTypes.Object, Presets.subBoolean, subOption.key, newValue), label, ButtonStyles.Secondary);
|
|
159
|
+
} else if (subOption.type === OptionTypes.List) {
|
|
160
|
+
button = renderButton(genId(objectKey, OptionTypes.Object, Presets.subList, subOption.key, Presets.view), "View", ButtonStyles.Secondary);
|
|
161
|
+
} else {
|
|
162
|
+
throw new Error(`Unsupported sub-option type "${subOption.type}" inside Object`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return renderSubOptionSection(textContent, button);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderObjectListPage(menuKey, objectOption, subOption, items, listPageIndex, userId, category, page, objectPage = 0) {
|
|
169
|
+
const itemsPerPage = Math.max(1, Math.floor((MaxComponents - ObjectListPageOverhead) / ObjectListItemCost));
|
|
170
|
+
|
|
171
|
+
let safeItems;
|
|
172
|
+
if (Array.isArray(items)) {
|
|
173
|
+
safeItems = items;
|
|
174
|
+
} else {
|
|
175
|
+
safeItems = [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const totalPages = Math.max(1, Math.ceil(safeItems.length / itemsPerPage));
|
|
179
|
+
const clampedPage = Math.min(Math.max(0, listPageIndex), totalPages - 1);
|
|
180
|
+
const start = clampedPage * itemsPerPage;
|
|
181
|
+
const pageItems = safeItems.slice(start, start + itemsPerPage);
|
|
182
|
+
|
|
183
|
+
const ctx = { u: userId, cat: category, page, sub: clampedPage, op: objectPage };
|
|
184
|
+
const genId = (...v) => encodeId(menuKey, v, ctx);
|
|
185
|
+
|
|
186
|
+
const headerText = renderHeader(subOption.title, subOption.description);
|
|
187
|
+
const headerDisplay = renderTextDisplay(headerText);
|
|
188
|
+
const divider = renderSeparator(true);
|
|
189
|
+
const containerComponents = [headerDisplay, divider];
|
|
190
|
+
|
|
191
|
+
if (pageItems.length === 0) {
|
|
192
|
+
containerComponents.push(renderTextDisplay("`(no items)`"));
|
|
193
|
+
} else {
|
|
194
|
+
for (const [i, item] of pageItems.entries()) {
|
|
195
|
+
const itemComponents = renderObjectListItem(menuKey, objectOption.key, subOption, item, start + i, ctx);
|
|
196
|
+
containerComponents.push(...itemComponents);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const hasPrev = clampedPage > 0;
|
|
201
|
+
const hasNext = clampedPage < totalPages - 1;
|
|
202
|
+
|
|
203
|
+
const jumpItems = Math.min(10, Math.max(1, Math.ceil(safeItems.length * 0.1)));
|
|
204
|
+
const jumpPages = Math.max(1, Math.ceil(jumpItems / itemsPerPage));
|
|
205
|
+
const fastPrevPage = Math.max(0, clampedPage - jumpPages);
|
|
206
|
+
const fastNextPage = Math.min(totalPages - 1, clampedPage + jumpPages);
|
|
207
|
+
|
|
208
|
+
const navRow = renderActionRow(
|
|
209
|
+
renderButton(genId(objectOption.key, OptionTypes.Object, Presets.subList, subOption.key, Presets.nav, String(fastPrevPage)), "<<", ButtonStyles.Secondary, fastPrevPage === clampedPage),
|
|
210
|
+
renderButton(genId(objectOption.key, OptionTypes.Object, Presets.subList, subOption.key, Presets.nav, String(clampedPage - 1)), "<", ButtonStyles.Secondary, !hasPrev),
|
|
211
|
+
renderButton(genId(objectOption.key, OptionTypes.Object, Presets.subList, subOption.key, Presets.nav, String(clampedPage + 1)), ">", ButtonStyles.Secondary, !hasNext),
|
|
212
|
+
renderButton(genId(objectOption.key, OptionTypes.Object, Presets.subList, subOption.key, Presets.nav, String(fastNextPage)), ">>", ButtonStyles.Secondary, fastNextPage === clampedPage),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const actionRow = renderActionRow(
|
|
216
|
+
renderButton(genId(objectOption.key, OptionTypes.Object, Presets.subList, subOption.key, Presets.done), "Done", ButtonStyles.Secondary),
|
|
217
|
+
renderButton(genId(objectOption.key, OptionTypes.Object, Presets.subList, subOption.key, Presets.add), "Add", ButtonStyles.Success),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
flags: ComponentsV2MessageFlags,
|
|
222
|
+
components: [
|
|
223
|
+
{ type: ComponentTypes.Container, components: containerComponents },
|
|
224
|
+
navRow,
|
|
225
|
+
actionRow,
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function renderObjectListItem(menuKey, objectKey, subOption, value, absIndex, ctx) {
|
|
231
|
+
let displayValue;
|
|
232
|
+
if (value != null) {
|
|
233
|
+
if (typeof value === "object") {
|
|
234
|
+
const entries = Object.entries(value);
|
|
235
|
+
if (entries.length <= 0) {
|
|
236
|
+
displayValue = "(empty object)";
|
|
237
|
+
} else {
|
|
238
|
+
const lines = [];
|
|
239
|
+
const maxPreviewLines = 4;
|
|
240
|
+
const previewEntries = entries.slice(0, maxPreviewLines);
|
|
241
|
+
for (const [key, entryValue] of previewEntries) {
|
|
242
|
+
let printable;
|
|
243
|
+
if (entryValue == null) {
|
|
244
|
+
printable = "(not set)";
|
|
245
|
+
} else if (typeof entryValue === "object") {
|
|
246
|
+
printable = JSON.stringify(entryValue);
|
|
247
|
+
} else {
|
|
248
|
+
printable = String(entryValue);
|
|
249
|
+
}
|
|
250
|
+
lines.push(`${key}: ${printable}`);
|
|
251
|
+
}
|
|
252
|
+
if (entries.length > maxPreviewLines) {
|
|
253
|
+
lines.push(`... +${entries.length - maxPreviewLines} more`);
|
|
254
|
+
}
|
|
255
|
+
displayValue = lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
displayValue = String(value);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
displayValue = "(not set)";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const genId = (...v) => encodeId(menuKey, v, ctx);
|
|
265
|
+
|
|
266
|
+
const editButton = renderButton(genId(objectKey, OptionTypes.Object, Presets.subList, subOption.key, Presets.edit, String(absIndex)), "Edit", ButtonStyles.Secondary);
|
|
267
|
+
const deleteButton = renderButton(genId(objectKey, OptionTypes.Object, Presets.subList, subOption.key, Presets.delete, String(absIndex)), "Delete", ButtonStyles.Secondary);
|
|
268
|
+
|
|
269
|
+
return [
|
|
270
|
+
renderTextDisplay(displayValue.split("\n").map(line => `> ${line}`).join("\n")),
|
|
271
|
+
renderActionRow(editButton, deleteButton),
|
|
272
|
+
renderSeparator(),
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function renderObjectTextInput(objectKey, subOption, currentValue) {
|
|
277
|
+
const genId = encodeId(ModalInputPrefix, [objectKey, OptionTypes.Object, Presets.subModal, subOption.key]);
|
|
278
|
+
let component;
|
|
279
|
+
|
|
280
|
+
if (subOption.type === OptionTypes.String) {
|
|
281
|
+
const minLen = subOption.minLength ?? 0;
|
|
282
|
+
const maxLen = subOption.maxLength ?? 4000;
|
|
283
|
+
|
|
284
|
+
let prefill;
|
|
285
|
+
if (currentValue != null) {
|
|
286
|
+
prefill = validateString(String(currentValue), minLen, maxLen);
|
|
287
|
+
} else {
|
|
288
|
+
prefill = "";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
component = {
|
|
292
|
+
type: ComponentTypes.TextInput,
|
|
293
|
+
custom_id: genId,
|
|
294
|
+
style: 2,
|
|
295
|
+
placeholder: subOption.description ?? "Enter a value...",
|
|
296
|
+
min_length: minLen,
|
|
297
|
+
max_length: maxLen,
|
|
298
|
+
required: subOption.required || minLen > 0,
|
|
299
|
+
};
|
|
300
|
+
if (prefill !== "") {
|
|
301
|
+
component.value = prefill;
|
|
302
|
+
}
|
|
303
|
+
} else if (subOption.type === OptionTypes.Number) {
|
|
304
|
+
let prefill;
|
|
305
|
+
if (currentValue != null) {
|
|
306
|
+
prefill = validateNumber(String(currentValue), subOption.minValue ?? -Infinity, subOption.maxValue ?? Infinity);
|
|
307
|
+
} else {
|
|
308
|
+
prefill = "";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
component = {
|
|
312
|
+
type: ComponentTypes.TextInput,
|
|
313
|
+
custom_id: genId,
|
|
314
|
+
style: 1,
|
|
315
|
+
placeholder: subOption.description ?? "Enter a number...",
|
|
316
|
+
required: false,
|
|
317
|
+
};
|
|
318
|
+
if (prefill !== "") {
|
|
319
|
+
component.value = prefill;
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
throw new Error(`Sub-option type "${subOption.type}" does not support modal input`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
type: ComponentTypes.Label,
|
|
327
|
+
label: subOption.title,
|
|
328
|
+
description: subOption.description,
|
|
329
|
+
component: component,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function renderObjectListTextInput(objectKey, subOption, currentValue, itemIndex) {
|
|
334
|
+
const isAdd = itemIndex == null;
|
|
335
|
+
|
|
336
|
+
let idValues;
|
|
337
|
+
if (isAdd) {
|
|
338
|
+
idValues = [objectKey, OptionTypes.Object, Presets.subListModal, subOption.key, Presets.add];
|
|
339
|
+
} else {
|
|
340
|
+
idValues = [objectKey, OptionTypes.Object, Presets.subListModal, subOption.key, Presets.edit, String(itemIndex)];
|
|
341
|
+
}
|
|
342
|
+
const genId = encodeId(ModalInputPrefix, idValues);
|
|
343
|
+
|
|
344
|
+
const cfg = subOption.itemConfig;
|
|
345
|
+
let component;
|
|
346
|
+
|
|
347
|
+
if (subOption.itemType === OptionTypes.String) {
|
|
348
|
+
const minLen = cfg.minLength ?? 0;
|
|
349
|
+
const maxLen = cfg.maxLength ?? 4000;
|
|
350
|
+
|
|
351
|
+
let value;
|
|
352
|
+
if (isAdd) {
|
|
353
|
+
value = "";
|
|
354
|
+
} else if (currentValue != null) {
|
|
355
|
+
value = validateString(String(currentValue), minLen, maxLen);
|
|
356
|
+
} else {
|
|
357
|
+
value = "";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
component = {
|
|
361
|
+
type: ComponentTypes.TextInput,
|
|
362
|
+
custom_id: genId,
|
|
363
|
+
style: 2,
|
|
364
|
+
placeholder: cfg.description ?? "Enter a value...",
|
|
365
|
+
min_length: minLen,
|
|
366
|
+
max_length: maxLen,
|
|
367
|
+
required: minLen > 0,
|
|
368
|
+
};
|
|
369
|
+
if (value !== "") {
|
|
370
|
+
component.value = value;
|
|
371
|
+
}
|
|
372
|
+
} else if (subOption.itemType === OptionTypes.Number) {
|
|
373
|
+
let value;
|
|
374
|
+
if (isAdd) {
|
|
375
|
+
value = "";
|
|
376
|
+
} else if (currentValue != null) {
|
|
377
|
+
value = validateNumber(String(currentValue), cfg.minValue ?? -Infinity, cfg.maxValue ?? Infinity);
|
|
378
|
+
} else {
|
|
379
|
+
value = "";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
component = {
|
|
383
|
+
type: ComponentTypes.TextInput,
|
|
384
|
+
custom_id: genId,
|
|
385
|
+
style: 1,
|
|
386
|
+
placeholder: cfg.description ?? "Enter a number...",
|
|
387
|
+
required: true,
|
|
388
|
+
};
|
|
389
|
+
if (value !== "") {
|
|
390
|
+
component.value = value;
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
throw new Error(`List item type "${subOption.itemType}" does not support modal input`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let labelText;
|
|
397
|
+
if (isAdd) {
|
|
398
|
+
labelText = "Add item";
|
|
399
|
+
} else {
|
|
400
|
+
labelText = "Edit item";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
type: ComponentTypes.Label,
|
|
405
|
+
label: labelText,
|
|
406
|
+
description: null,
|
|
407
|
+
component: component,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
module.exports = {
|
|
412
|
+
renderObjectPage,
|
|
413
|
+
renderObjectSubOption,
|
|
414
|
+
renderObjectListPage,
|
|
415
|
+
renderObjectTextInput,
|
|
416
|
+
renderObjectListTextInput,
|
|
417
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const { OptionTypes } = require("../utils/types");
|
|
2
|
+
const { Presets, encodeId } = require("../utils/customIds");
|
|
3
|
+
const { ComponentTypes, ButtonStyles } = require("./types");
|
|
4
|
+
const {
|
|
5
|
+
renderButton,
|
|
6
|
+
renderSeparator,
|
|
7
|
+
renderTextDisplay,
|
|
8
|
+
renderActionRow,
|
|
9
|
+
renderHeader
|
|
10
|
+
} = require("./utils");
|
|
11
|
+
const { renderSelectMenuOption } = require("./selectMenu");
|
|
12
|
+
const SelectOptionTypes = new Set([
|
|
13
|
+
OptionTypes.StringSelect,
|
|
14
|
+
OptionTypes.UserSelect,
|
|
15
|
+
OptionTypes.RoleSelect,
|
|
16
|
+
OptionTypes.MentionableSelect,
|
|
17
|
+
OptionTypes.ChannelSelect,
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
function renderNavigation(menuKey, userId, targets, category, pageIndex) {
|
|
22
|
+
const previousCategory = targets.previousCategory ?? null;
|
|
23
|
+
const previousPage = targets.previousPage ?? null;
|
|
24
|
+
const nextPage = targets.nextPage ?? null;
|
|
25
|
+
const nextCategory = targets.nextCategory ?? null;
|
|
26
|
+
const genId = (preset, target) => encodeId(menuKey, [preset, Presets.nav], {
|
|
27
|
+
u: userId,
|
|
28
|
+
cat: target?.cat ?? 0,
|
|
29
|
+
page: target?.page ?? 0,
|
|
30
|
+
});
|
|
31
|
+
const prevCategoryButton = renderButton(genId(Presets.previousCategory, previousCategory), "<<", ButtonStyles.Secondary, !previousCategory);
|
|
32
|
+
const prevPageButton = renderButton(genId(Presets.previousPage, previousPage), "<", ButtonStyles.Secondary, !previousPage);
|
|
33
|
+
const spacerId = encodeId(menuKey, [Presets.navSpacer, Presets.nav], {
|
|
34
|
+
u: userId,
|
|
35
|
+
cat: category,
|
|
36
|
+
page: pageIndex,
|
|
37
|
+
});
|
|
38
|
+
const spacer = renderButton(spacerId, "O", ButtonStyles.Secondary, true);
|
|
39
|
+
const nextPageButton = renderButton(genId(Presets.nextPage, nextPage), ">", ButtonStyles.Secondary, !nextPage);
|
|
40
|
+
const nextCategoryButton = renderButton(genId(Presets.nextCategory, nextCategory), ">>", ButtonStyles.Secondary, !nextCategory);
|
|
41
|
+
return renderActionRow(prevCategoryButton, prevPageButton, spacer, nextPageButton, nextCategoryButton);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderContainer({ title, description, components }) {
|
|
45
|
+
const headerText = renderHeader(title, description);
|
|
46
|
+
const header = renderTextDisplay(headerText);
|
|
47
|
+
const divider = renderSeparator(true);
|
|
48
|
+
return {
|
|
49
|
+
type: ComponentTypes.Container,
|
|
50
|
+
components: [header, divider, ...components],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderOption(menuKey, opt, currentValue, userId, category, page) {
|
|
55
|
+
if (SelectOptionTypes.has(opt.type)) {
|
|
56
|
+
return renderSelectMenuOption(menuKey, opt, currentValue, userId, category, page);
|
|
57
|
+
}
|
|
58
|
+
const lines = [`**${opt.title}**`];
|
|
59
|
+
if (opt.description) {
|
|
60
|
+
lines.push(`-# ${opt.description}`);
|
|
61
|
+
}
|
|
62
|
+
if (opt.preview) {
|
|
63
|
+
if (opt.type === OptionTypes.List) {
|
|
64
|
+
let count;
|
|
65
|
+
if (Array.isArray(currentValue)) {
|
|
66
|
+
count = currentValue.length;
|
|
67
|
+
} else {
|
|
68
|
+
count = 0;
|
|
69
|
+
}
|
|
70
|
+
let pluralSuffix;
|
|
71
|
+
if (count !== 1) {
|
|
72
|
+
pluralSuffix = "s";
|
|
73
|
+
} else {
|
|
74
|
+
pluralSuffix = "";
|
|
75
|
+
}
|
|
76
|
+
if (count > 0) {
|
|
77
|
+
lines.push(`\`${count} Item${pluralSuffix}\``);
|
|
78
|
+
} else {
|
|
79
|
+
lines.push("`(empty)`");
|
|
80
|
+
}
|
|
81
|
+
} else if (opt.type === OptionTypes.Object) {
|
|
82
|
+
let isConfigured = false;
|
|
83
|
+
if (currentValue != null && typeof currentValue === "object") {
|
|
84
|
+
isConfigured = Object.keys(currentValue).length > 0;
|
|
85
|
+
}
|
|
86
|
+
if (isConfigured) {
|
|
87
|
+
lines.push("`Configured`");
|
|
88
|
+
} else {
|
|
89
|
+
lines.push("`Not configured`");
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
if (currentValue != null) {
|
|
93
|
+
lines.push(`\`${currentValue}\``);
|
|
94
|
+
} else {
|
|
95
|
+
lines.push("`(not set)`");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const textContent = renderTextDisplay(lines.join("\n"));
|
|
100
|
+
const button = renderAccessory(menuKey, opt, currentValue, userId, category, page);
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
type: ComponentTypes.Section,
|
|
104
|
+
components: [textContent],
|
|
105
|
+
accessory: button,
|
|
106
|
+
},
|
|
107
|
+
renderSeparator(),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderAccessory(menuKey, opt, currentValue, userId, category, page) {
|
|
112
|
+
const ctx = { u: userId, cat: category, page: page };
|
|
113
|
+
const genId = (...v) => encodeId(menuKey, v, ctx);
|
|
114
|
+
if (opt.type === OptionTypes.String || opt.type === OptionTypes.Number) {
|
|
115
|
+
return renderButton(genId(opt.key, opt.type, "modal"), "Edit", ButtonStyles.Secondary);
|
|
116
|
+
}
|
|
117
|
+
if (opt.type === OptionTypes.Boolean) {
|
|
118
|
+
let newValue;
|
|
119
|
+
if (currentValue) {
|
|
120
|
+
newValue = "false";
|
|
121
|
+
} else {
|
|
122
|
+
newValue = "true";
|
|
123
|
+
}
|
|
124
|
+
let label;
|
|
125
|
+
if (currentValue) {
|
|
126
|
+
label = opt.trueLabel ?? "True";
|
|
127
|
+
} else {
|
|
128
|
+
label = opt.falseLabel ?? "False";
|
|
129
|
+
}
|
|
130
|
+
return renderButton(genId(opt.key, OptionTypes.Boolean, newValue), label, ButtonStyles.Secondary);
|
|
131
|
+
}
|
|
132
|
+
if (opt.type === OptionTypes.List) {
|
|
133
|
+
return renderButton(genId(opt.key, OptionTypes.List, Presets.view), "View", ButtonStyles.Secondary);
|
|
134
|
+
}
|
|
135
|
+
if (opt.type === OptionTypes.Object) {
|
|
136
|
+
return renderButton(genId(opt.key, OptionTypes.Object, Presets.view), "View", ButtonStyles.Secondary);
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Unknown option type "${opt.type}"`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
module.exports = { renderNavigation, renderContainer, renderOption, renderAccessory };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const { OptionTypes } = require("../utils/types");
|
|
2
|
if (Array.isArray(currentValue)) {
|
|
1
3
|
return currentValue;
|
|
2
4
|
}
|
|
3
5
|
if (currentValue != null) {
|
|
4
6
|
return [currentValue];
|
|
5
7
|
}
|
|
6
8
|
return [];
|
|
7
9
|
const ctx = { userId, category, page };
|
|
8
10
|
const customId = encodeId(menuKey, [opt.key, opt.type], ctx);
|
|
9
11
|
const component = {
|
|
10
12
|
type: opt.selectType,
|
|
11
13
|
custom_id: customId,
|
|
12
14
|
min_values: opt.minValues,
|
|
13
15
|
max_values: opt.maxValues,
|
|
14
16
|
};
|
|
15
17
|
if (opt.placeholder != null) {
|
|
16
18
|
component.placeholder = opt.placeholder;
|
|
17
19
|
}
|
|
18
20
|
if (opt.type === OptionTypes.StringSelect) {
|
|
19
21
|
const selectedValues = toIdArray(currentValue);
|
|
20
22
|
component.options = opt.choices.map(c => ({
|
|
21
23
|
label: c.label,
|
|
22
24
|
value: c.value,
|
|
23
25
|
description: c.description,
|
|
24
26
|
default: selectedValues.includes(c.value),
|
|
25
27
|
}));
|
|
26
28
|
}
|
|
27
29
|
if (opt.type === OptionTypes.UserSelect) {
|
|
28
30
|
const ids = toIdArray(currentValue);
|
|
29
31
|
if (ids.length > 0) {
|
|
30
32
|
component.default_values = ids.map(id => ({ id, type: "user" }));
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
if (opt.type === OptionTypes.RoleSelect) {
|
|
34
36
|
const ids = toIdArray(currentValue);
|
|
35
37
|
if (ids.length > 0) {
|
|
36
38
|
component.default_values = ids.map(id => ({ id, type: "role" }));
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
if (opt.type === OptionTypes.ChannelSelect) {
|
|
40
42
|
if (opt.channelTypes && opt.channelTypes.length > 0) {
|
|
41
43
|
component.channel_types = opt.channelTypes;
|
|
42
44
|
}
|
|
43
45
|
const ids = toIdArray(currentValue);
|
|
44
46
|
if (ids.length > 0) {
|
|
45
47
|
component.default_values = ids.map(id => ({ id, type: "channel" }));
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
return component;
|
|
49
51
|
const lines = [`**${opt.title}**`];
|
|
50
52
|
if (opt.description) {
|
|
51
53
|
lines.push(`-# ${opt.description}`);
|
|
52
54
|
}
|
|
53
55
|
const textDisplay = renderTextDisplay(lines.join("\n"));
|
|
54
56
|
const selectMenu = renderSelectMenuComponent(menuKey, opt, currentValue, userId, category, page);
|
|
55
57
|
const actionRow = renderActionRow(selectMenu);
|
|
56
58
|
return [textDisplay, actionRow, renderSeparator()];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const ComponentTypes = {
|
|
2
|
ActionRow: 1,
|
|
1
3
|
Button: 2,
|
|
2
4
|
StringSelect: 3,
|
|
3
5
|
TextInput: 4,
|
|
4
6
|
UserSelect: 5,
|
|
5
7
|
RoleSelect: 6,
|
|
6
8
|
MentionableSelect: 7,
|
|
7
9
|
ChannelSelect: 8,
|
|
8
10
|
Section: 9,
|
|
9
11
|
TextDisplay: 10,
|
|
10
12
|
Separator: 14,
|
|
11
13
|
Container: 17,
|
|
12
14
|
Label: 18,
|
|
13
15
|
Primary: 1,
|
|
14
16
|
Secondary: 2,
|
|
15
17
|
Success: 3,
|
|
16
18
|
Danger: 4,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { ComponentTypes } = require("./types");
|
|
2
|
+
|
|
3
|
+
function renderButton(custom_id, label, style, disabled = false) {
|
|
4
|
+
return { type: ComponentTypes.Button, custom_id, label, style, disabled };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function renderSeparator(divider = false) {
|
|
8
|
+
return { type: ComponentTypes.Separator, divider, spacing: 1 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function renderTextDisplay(content) {
|
|
12
|
+
return { type: ComponentTypes.TextDisplay, content };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderActionRow(...components) {
|
|
16
|
+
return { type: ComponentTypes.ActionRow, components };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderHeader(title, description) {
|
|
20
|
+
if (description) {
|
|
21
|
+
return `## ${title}\n-# ${description}`;
|
|
22
|
+
}
|
|
23
|
+
return `## ${title}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderSubOptionSection(textContent, button) {
|
|
27
|
+
return {
|
|
28
|
+
type: ComponentTypes.Section,
|
|
29
|
+
components: [textContent],
|
|
30
|
+
accessory: button,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validateString(value, minLen, maxLen) {
|
|
35
|
+
const str = value != null ? String(value) : "";
|
|
36
|
+
if (str.length >= minLen && str.length <= maxLen) {
|
|
37
|
+
return str;
|
|
38
|
+
}
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validateNumber(value, minVal, maxVal) {
|
|
43
|
+
const str = value != null ? String(value) : "";
|
|
44
|
+
const parsed = parseFloat(str);
|
|
45
|
+
if (!isNaN(parsed) && parsed >= minVal && parsed <= maxVal) {
|
|
46
|
+
return str;
|
|
47
|
+
}
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
renderButton,
|
|
53
|
+
renderSeparator,
|
|
54
|
+
renderTextDisplay,
|
|
55
|
+
renderActionRow,
|
|
56
|
+
renderHeader,
|
|
57
|
+
renderSubOptionSection,
|
|
58
|
+
validateString,
|
|
59
|
+
validateNumber,
|
|
60
|
+
};
|