diff --git a/components-sdk/src/Capsule.module.css b/components-sdk/src/Capsule.module.css
index ce1692d..b1aedb6 100644
--- a/components-sdk/src/Capsule.module.css
+++ b/components-sdk/src/Capsule.module.css
@@ -10,12 +10,24 @@
color: #dcddde;
background: #36393f;
font-family: "gg sans",Whitney,"Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
+ overflow-wrap: anywhere;
font-size: 1.6rem;
line-height: 2.2rem;
letter-spacing: normal;
font-weight: 400;
position: relative;
+
+ &.modal {
+ background: #242429;
+ .header {
+ font-size: 2.4rem;
+ color: white;
+ font-weight: bold;
+ margin-top: 1rem;
+ margin-bottom: 24px;
+ }
+ }
}
.component {
diff --git a/components-sdk/src/Capsule.tsx b/components-sdk/src/Capsule.tsx
index c10eff6..e5945e7 100644
--- a/components-sdk/src/Capsule.tsx
+++ b/components-sdk/src/Capsule.tsx
@@ -11,7 +11,7 @@ import { StringSelect } from './components/StringSelect';
import { File } from './components/File';
import { CapsuleInner } from './CapsuleInner';
import { generateRandomAnimal, randomSentence, uuidv4 } from './utils/randomGen';
-import { addKeyType, appendKeyType, deleteKeyType, stateKeyType, StateManager } from './polyfills/StateManager';
+import { stateKeyType, StateManager } from './polyfills/StateManager';
import {
ActionRowComponent,
ButtonComponent,
@@ -23,22 +23,33 @@ import {
MediaGalleryComponent,
MediaGalleryItem,
PassProps,
+ RenderMode,
SeparatorComponent,
SeparatorSpacingSize,
StringSelectComponent,
TextDisplayComponent,
ThumbnailComponent,
} from './utils/componentTypes';
+import {
+ FileUploadComponent,
+ LabelComponent,
+ ModalStringSelectComponent,
+ TextInputComponent,
+ TextInputStyle,
+} from './utils/componentTypesModal';
import { DragContextProvider } from './dnd/DragContext';
import { DroppableID } from './dnd/components';
import { KeyToDeleteType } from './dnd/types';
import { BoundariesProps } from './dnd/boundaries';
import { RegenerateContextProvider } from './utils/useRegenerate';
-import { useMemo } from 'react';
+import { Label } from './components/Label';
+import { useTranslation } from 'react-i18next';
+import { TextInput } from './components/TextInput';
+import { FileUpload } from './components/FileUpload';
const _Button = {
- type: 2,
+ type: ComponentType.BUTTON,
style: ButtonStyle.GREY,
label: '',
emoji: null,
@@ -54,7 +65,7 @@ const _Image = {
} as MediaGalleryItem
const _StringSelect = () => ({
- type: 3,
+ type: ComponentType.STRING_SELECT,
custom_id: uuidv4(),
options: [
{
@@ -71,70 +82,119 @@ const _StringSelect = () => ({
disabled: false,
} as StringSelectComponent)
+const _TextInput = (style: TextInputStyle) => ({
+ type: ComponentType.TEXT_INPUT,
+ custom_id: uuidv4(),
+ style,
+ min_length: null,
+ max_length: null,
+ required: true,
+ value: null,
+ placeholder: null,
+});
+
+// We want this app to create components with all properties (except 'id') defined
+type Req = Required>;
+
export const default_settings = {
Button: () => ({
- type: 1,
+ type: ComponentType.ACTION_ROW,
components: [{
..._Button,
custom_id: uuidv4(),
- label: generateRandomAnimal()
+ label: generateRandomAnimal(),
}]
}),
LinkButton: () => ({
- type: 1,
+ type: ComponentType.ACTION_ROW,
components: [{
..._Button,
style: ButtonStyle.URL,
url: 'https://google.com',
- label: generateRandomAnimal()
+ label: generateRandomAnimal(),
}]
}),
StringSelect: () => ({
- type: 1,
- components: [_StringSelect()]
+ type: ComponentType.ACTION_ROW,
+ components: [_StringSelect()],
}),
TextDisplay: () => ({
- type: 10,
+ type: ComponentType.TEXT_DISPLAY,
content: randomSentence(),
}),
Thumbnail: {
- type: 11,
- ..._Image
+ type: ComponentType.THUMBNAIL,
+ ..._Image,
},
MediaGallery: {
- type: 12,
- items: [
- _Image
- ]
+ type: ComponentType.MEDIA_GALLERY,
+ items: [_Image],
},
File: {
- type: 13,
+ type: ComponentType.FILE,
file: {
url: ''
},
spoiler: false,
},
Separator: {
- type: 14,
+ type: ComponentType.SEPARATOR,
divider: true,
spacing: SeparatorSpacingSize.SMALL
},
Container: {
- type: 17,
+ type: ComponentType.CONTAINER,
accent_color: null,
spoiler: false,
components: [],
},
+ ModalShortInput: () => ({
+ type: ComponentType.LABEL,
+ label: generateRandomAnimal(),
+ description: null,
+ component: _TextInput(TextInputStyle.SHORT),
+ }),
+ ModalParagraphInput: () => ({
+ type: ComponentType.LABEL,
+ label: generateRandomAnimal(),
+ description: null,
+ component: _TextInput(TextInputStyle.PARAGRAPH),
+ }),
+ ModalStringSelect: () => {
+ const {disabled, ...other} = _StringSelect();
+ return {
+ type: ComponentType.LABEL,
+ label: generateRandomAnimal(),
+ description: null,
+ component: {...other, required: true},
+ };
+ },
+ ModalFileUpload: () => ({
+ type: ComponentType.LABEL,
+ label: generateRandomAnimal(),
+ description: null,
+ component: {
+ type: ComponentType.FILE_UPLOAD,
+ custom_id: uuidv4(),
+ min_values: 1,
+ max_values: 1,
+ required: true,
+ },
+ }),
} as {
- Button: () => ActionRowComponent,
- LinkButton: () => ActionRowComponent,
- StringSelect: () => ActionRowComponent,
- TextDisplay: () => TextDisplayComponent,
- Thumbnail: ThumbnailComponent,
- MediaGallery: MediaGalleryComponent,
- Separator: SeparatorComponent,
- Container:ContainerComponent,
- File: FileComponent
+ Button: () => Req>>;
+ LinkButton: () => Req>>;
+ StringSelect: () => Req>>;
+ TextDisplay: () => Req;
+ Thumbnail: Req;
+ MediaGallery: Req;
+ Separator: Req;
+ Container: Req;
+ File: Req;
+ ModalShortInput: () => Req>>;
+ ModalParagraphInput: () => Req>>;
+ ModalStringSelect: () => Req>>;
+ ModalFileUpload: () => Req>>;
}
export type ComponentsProps = {
@@ -145,23 +205,27 @@ export type ComponentsProps = {
removeKeyParent?: stateKeyType,
dragKeyToDeleteOverwrite?: Omit, // Available only for Section accessory
droppableId?: DroppableID, // Available only for Section accessory
+ fromLabel?: boolean // Available only for Label component
errors?: Record | null,
actionCallback?: (custom_id: string | null) => void,
}
-export const COMPONENTS = {
- 1: ActionRow,
- 2: Button,
- 3: StringSelect,
- 9: Section,
- 10: TextDisplay,
- 11: Thumbnail,
- 12: MediaGallery,
- 14: Separator,
- 17: Container,
- 13: File,
-} as unknown as {
- [K: number]: (props: ComponentsProps) => JSX.Element
+export const COMPONENTS: {
+ [K in ComponentType]: (props: Omit & { state: any }) => JSX.Element | null;
+} = {
+ [ComponentType.ACTION_ROW]: ActionRow,
+ [ComponentType.BUTTON]: Button,
+ [ComponentType.STRING_SELECT]: StringSelect,
+ [ComponentType.TEXT_INPUT]: TextInput,
+ [ComponentType.SECTION]: Section,
+ [ComponentType.TEXT_DISPLAY]: TextDisplay,
+ [ComponentType.THUMBNAIL]: Thumbnail,
+ [ComponentType.MEDIA_GALLERY]: MediaGallery,
+ [ComponentType.SEPARATOR]: Separator,
+ [ComponentType.CONTAINER]: Container,
+ [ComponentType.FILE]: File,
+ [ComponentType.LABEL]: Label,
+ [ComponentType.FILE_UPLOAD]: FileUpload,
}
export const SECTIONABLE = [
@@ -175,19 +239,27 @@ export function Capsule(props : {
className?: string | null,
passProps: PassProps,
errors: Record | null,
+ modalTitle?: string
} & BoundariesProps ) {
+ const isModal = props.passProps.renderMode === RenderMode.MODAL;
+
const cls = props.className ? ' ' + props.className : '';
+ const modCls = isModal ? ' ' + Styles.modal : '';
- return
-
{stateMng =>
+ return
+ {!!(isModal && props.modalTitle) &&
+ {props.modalTitle}
+
}
+
{stateMng =>
}
diff --git a/components-sdk/src/CapsuleButton.tsx b/components-sdk/src/CapsuleButton.tsx
index 29ef674..2a4bc0d 100644
--- a/components-sdk/src/CapsuleButton.tsx
+++ b/components-sdk/src/CapsuleButton.tsx
@@ -8,13 +8,16 @@ import SeparatorIcon from './icons/Separator.svg';
import ButtonIcon from './icons/Button.svg';
import LinkButtonIcon from './icons/ButtonLink.svg';
import SelectIcon from './icons/Select.svg';
+import UploadFile from './icons/UploadFile.svg';
+import Input from './icons/Input.svg';
+import Textarea from './icons/Textarea.svg';
import { default_settings } from './Capsule';
import { CSSProperties, useRef } from 'react';
import { Component } from './utils/componentTypes';
import { useStateOpen } from './utils/useStateOpen';
import { useTranslation } from 'react-i18next';
-export type capsuleButtonCtx = 'main' | 'container' | 'inline' | 'button-row' | 'frame';
+export type capsuleButtonCtx = 'main' | 'container' | 'inline' | 'button-row' | 'frame' | 'modal';
type props = {
context: capsuleButtonCtx,
@@ -39,6 +42,7 @@ export function CapsuleButton({context, callback, className, style, interactiveD
{context === "inline" && t('button.add-inline')}
{context === "container" && t('button.add-content')}
{context === "button-row" && t('button.add-button')}
+ {context === "modal" && t('button.add-input')}
{ open &&
{['main', 'inline', 'container'].includes(context) &&
{
@@ -95,6 +99,36 @@ export function CapsuleButton({context, callback, className, style, interactiveD
{t('components.thumbnail')}
}
+ {['modal'].includes(context) &&
{
+ callback(default_settings.ModalShortInput())
+ }}>
+
+
{t('components.modal.short-input')}
+
}
+ {['modal'].includes(context) &&
{
+ callback(default_settings.ModalParagraphInput())
+ }}>
+
+
{t('components.modal.paragraph-input')}
+
}
+ {['modal'].includes(context) &&
{
+ callback(default_settings.TextDisplay())
+ }}>
+
+
{t('components.modal.content')}
+
}
+ {['modal'].includes(context) &&
{
+ callback(default_settings.ModalStringSelect())
+ }}>
+
+
{t('components.modal.string-select')}
+
}
+ {['modal'].includes(context) &&
{
+ callback(default_settings.ModalFileUpload())
+ }}>
+
+
{t('components.modal.file-upload')}
+
}
}
)
diff --git a/components-sdk/src/components/Button.module.css b/components-sdk/src/components/Button.module.css
index ac67393..6d34366 100644
--- a/components-sdk/src/components/Button.module.css
+++ b/components-sdk/src/components/Button.module.css
@@ -37,11 +37,16 @@
border: 1px solid rgba(255,255,255, 0.1);
}
-.input[type="text"] {
+.input[type="text"], .input[type="number"] {
padding: 0.8rem 1.6rem;
width: 200px;
}
+textarea.textarea {
+ padding: 0.8rem 1.6rem;
+ width: 500px;
+}
+
.link_btn {
line-height: 100%;
.text {
diff --git a/components-sdk/src/components/Button.tsx b/components-sdk/src/components/Button.tsx
index e081a8d..a338062 100644
--- a/components-sdk/src/components/Button.tsx
+++ b/components-sdk/src/components/Button.tsx
@@ -1,16 +1,17 @@
import Styles from './Button.module.css';
-import {text_display_input} from './TextDisplay.module.css';
+import { text_display_input } from './TextDisplay.module.css';
import CapsuleStyles from '../Capsule.module.css';
import {
Dispatch,
- Fragment, MouseEventHandler,
+ Fragment,
+ MouseEventHandler,
RefObject,
SetStateAction,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
- useState
+ useState,
} from 'react';
import ColorBlue from '../icons/ColorBlue.svg';
import ColorGrey from '../icons/ColorGrey.svg';
@@ -115,8 +116,8 @@ export function Button(
{!!open &&
{open === 1 && }
{open === 2 && }
- {open === 3 && }
- {open === 4 && }
+ {open === 3 && }
+ {open === 4 && }
}
)
@@ -184,21 +185,53 @@ function MenuFirst({state, stateKey, stateManager, setOpen, removeKeyParent, act
}
export function MenuOption({ src, text, onClick, className }: {
- src: string;
+ src: string | null;
text: string;
onClick?: MouseEventHandler;
className?: string;
}) {
return (
-
+ {src !== null &&

-
+
}
{text}
);
}
+export function MenuArea({state, stateKey, stateManager, setOpen, closeLockRef, max} : {
+ state: (string | number | null),
+ stateKey: ComponentsProps['stateKey'],
+ stateManager: ComponentsProps['stateManager'],
+ setOpen: Dispatch>,
+ closeLockRef: RefObject,
+ max: number,
+}) {
+ const ref = useRef(null);
+ useImperativeHandle(closeLockRef, () => "SHIFT");
+
+ useEffect(() => {
+ if (ref.current) ref.current.focus();
+ }, [ref.current]);
+ return
+}
+
export function MenuEmoji({stateKey, stateManager, passProps} : {
stateKey: ComponentsProps['stateKey'],
stateManager: ComponentsProps['stateManager'],
@@ -213,15 +246,36 @@ export function MenuEmoji({stateKey, stateManager, passProps} : {
/>
}
-export function MenuLabel({state, stateKey, stateManager, setOpen, nullable = false, closeLockRef, placeholder} : {
- state: string,
+function processText(text: string, nullable: boolean) {
+ return nullable ? (text || null) : (text || "")
+}
+
+function processNumber(text: string, nullable: boolean, min?: number, max?: number) {
+ if (text.trim() === '' && nullable) return null;
+
+ try {
+ let no = Number(text);
+ if (min != null && no < min) no = min;
+ if (max != null && no > max) no = max;
+ return no;
+ } catch {
+ return 0;
+ }
+}
+
+export function MenuLabel({state, stateKey, stateManager, setOpen, nullable = false, closeLockRef, placeholder, number, min, max} : {
+ state: (string | number | null),
stateKey: ComponentsProps['stateKey'],
stateManager: ComponentsProps['stateManager'],
setOpen: Dispatch>,
nullable?: boolean,
closeLockRef: RefObject,
- placeholder?: string
+ placeholder?: string,
+ number?: boolean,
+ min?: number,
+ max: number,
}) {
+ const func = number ? processNumber : processText;
const ref = useRef(null);
useImperativeHandle(closeLockRef, () => true);
@@ -231,15 +285,21 @@ export function MenuLabel({state, stateKey, stateManager, setOpen, nullable = fa
return
-}
+}
\ No newline at end of file
diff --git a/components-sdk/src/components/FileUpload.module.css b/components-sdk/src/components/FileUpload.module.css
new file mode 100644
index 0000000..76dd4a2
--- /dev/null
+++ b/components-sdk/src/components/FileUpload.module.css
@@ -0,0 +1,46 @@
+.upload {
+ padding: 1.8rem 1.6rem;
+ background: rgba(0, 0, 0, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ color: #dcddde;
+ width: 100%;
+ position: relative;
+ cursor: pointer;
+
+ &:hover, &.open {
+ background: #101013;
+ }
+
+ .with_badge {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ justify-content: space-between;
+ min-width: 0;
+ gap: 1rem;
+ }
+
+ .badge {
+ border: 1px solid rgba(255, 255, 255, 0.7);
+ color: white;
+ border-radius: 100px;
+ font-size: 12px;
+ padding: 0 1.6rem;
+ white-space: nowrap;
+ &.invalid {
+ border-color: #F69083;
+ color: #F69083;
+ }
+ }
+
+ .icon {
+ display: flex;
+ gap: 0.8rem;
+ align-items: center;
+ img {
+ width: 3.6rem;
+ height: 3.6rem;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components-sdk/src/components/FileUpload.tsx b/components-sdk/src/components/FileUpload.tsx
new file mode 100644
index 0000000..54402c9
--- /dev/null
+++ b/components-sdk/src/components/FileUpload.tsx
@@ -0,0 +1,97 @@
+import Styles from './FileUpload.module.css';
+import { ComponentsProps } from '../Capsule';
+import { FileUploadComponent } from '../utils/componentTypesModal';
+import { useTranslation } from 'react-i18next';
+import CapsuleStyles from '../Capsule.module.css';
+import { MenuOption } from './Button';
+import { useStateOpen } from '../utils/useStateOpen';
+import { Dispatch, SetStateAction, useRef } from 'react';
+import Minimum from '../icons/Minimum.svg';
+import Maximum from '../icons/Maximum.svg';
+import FileUploadIcon from '../icons/FileUpload.svg';
+import { MenuRange } from './StringSelect';
+
+export function FileUpload({
+ state,
+ stateKey,
+ stateManager,
+ passProps,
+}: ComponentsProps & { state: FileUploadComponent }) {
+ const { open, setOpen, ignoreRef, closeLockRef } = useStateOpen(0);
+ const { t } = useTranslation('components-sdk');
+ const btn_select = useRef(null);
+
+ const min_values = state.min_values ?? 1;
+ const max_values = state.max_values ?? 1;
+ const isInvalid = min_values > max_values;
+
+ return (
+ {
+ if (btn_select.current && btn_select.current.contains(ev.target as HTMLElement)) return;
+ setOpen(1);
+ }}
+ ref={ignoreRef}
+ >
+
+
+

+ {max_values > 1 ? t('modal.file-upload.info-multiple') : t('modal.file-upload.info-one')}
+
+
+ {isInvalid && t('string-select.invalid')}{' '}
+ {min_values === max_values ? min_values : `${min_values} – ${max_values}`}
+
+
+ {!!open && (
+
+ {open === 1 && (
+
+ )}
+ {open === 2 && (
+
+ )}
+ {open === 3 && (
+
+ )}
+
+ )}
+
+ );
+}
+
+function MenuFirst({state, stateKey, stateManager, setOpen} : {
+ state: FileUploadComponent,
+ stateKey: ComponentsProps['stateKey'],
+ stateManager: ComponentsProps['stateManager'],
+ setOpen: Dispatch>,
+}) {
+
+ const {t} = useTranslation("components-sdk");
+
+ return <>
+
+ {
+ setOpen(2);
+ ev.stopPropagation();
+ }} />
+
+ {
+ setOpen(3);
+ ev.stopPropagation();
+ }} />
+ >
+}
diff --git a/components-sdk/src/components/Label.module.css b/components-sdk/src/components/Label.module.css
new file mode 100644
index 0000000..7f78c86
--- /dev/null
+++ b/components-sdk/src/components/Label.module.css
@@ -0,0 +1,71 @@
+
+.label {
+ .label_text {
+ position: relative;
+ cursor: pointer;
+ display: inline-flex;
+ flex-direction: column;
+ padding-right: 32px;
+
+ &:before {
+ content: "";
+ display: none;
+ position: absolute;
+ top: -4px;
+ bottom: -4px;
+ left: 0;
+ right: 0;
+ border-radius: 4px;
+ background: #1c1d20;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
+ max-width: 100%;
+ }
+
+ &.open, &:hover {
+ color: #86c232;
+ }
+
+ &.open:before, &:hover:before {
+ display: block;
+ }
+
+ margin-bottom: 8px;
+ }
+
+ .header {
+ font-size: 1.6rem;
+ position: relative;
+ font-weight: 600;
+ .required {
+ color: #ff6b6b;
+ margin-inline-start: 4px;
+ }
+ }
+ .description {
+ font-size: 1.4rem;
+ position: relative;
+ color: #a8a8a8;
+ line-height: 1.8rem;
+ }
+ .badge {
+ &.required, &.optional {
+ font-size: 1.2rem;
+ line-height: 100%;
+ padding: 2px 8px;
+ border-radius: 12px;
+ margin-top: 4px;
+ }
+
+ &.required {
+ background: #503a17;
+ color: white;
+ }
+
+ &.optional {
+ background: #175033;
+ color: white;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/components-sdk/src/components/Label.tsx b/components-sdk/src/components/Label.tsx
new file mode 100644
index 0000000..d76cd0c
--- /dev/null
+++ b/components-sdk/src/components/Label.tsx
@@ -0,0 +1,122 @@
+import Styles from './Label.module.css';
+import { COMPONENTS, ComponentsProps } from '../Capsule';
+import { LabelComponent, LabelPossible, MODAL_SUPPORTS_REQUIRED } from '../utils/componentTypesModal';
+import { Dispatch, SetStateAction, useMemo, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import CapsuleStyles from '../Capsule.module.css';
+import { MenuLabel, MenuOption } from './Button';
+import { useStateOpen } from '../utils/useStateOpen';
+import DescriptionText from '../icons/DescriptionText.svg';
+import DescriptionTextActive from '../icons/DescriptionTextActive.svg';
+import DescriptionPen from '../icons/DescriptionPen.svg';
+import RequiredActive from '../icons/RequiredActive.svg';
+import Required from '../icons/Required.svg';
+
+export function Label({
+ state,
+ stateKey,
+ stateManager,
+ passProps,
+ actionCallback,
+}: ComponentsProps & { state: LabelComponent }) {
+ const Child = COMPONENTS[state.component.type];
+ if (typeof Child === 'undefined') return null;
+ const btn_select = useRef(null);
+ const { open, setOpen, ignoreRef, closeLockRef } = useStateOpen(0);
+ const stateKeyChild = useMemo(() => [...stateKey, 'component'], [...stateKey]);
+
+ return (
+
+
{
+ if (btn_select.current && btn_select.current.contains(ev.target as HTMLElement)) return;
+ setOpen(1);
+ }}
+ ref={ignoreRef}
+ >
+
+ {state.label}
+ {(state.component.required ?? true) && *}
+
+
+
{state.description}
+ {!!open && (
+
+ {open === 1 && (
+
+ )}
+ {open === 2 && (
+
+ )}
+ {open === 3 && (
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+
+
+function MenuFirst({state, stateKey, stateManager, setOpen} : {
+ state: LabelComponent,
+ stateKey: ComponentsProps['stateKey'],
+ stateManager: ComponentsProps['stateManager'],
+ setOpen: Dispatch>,
+}) {
+
+ const {t} = useTranslation("components-sdk");
+
+ const requireable = MODAL_SUPPORTS_REQUIRED.includes(state.component.type);
+ const req = state.component.required ?? true;
+
+ return <>
+ {
+ setOpen(2);
+ ev.stopPropagation();
+ }} />
+
+ {
+ setOpen(3);
+ ev.stopPropagation();
+ }} />
+ {state.description != null && {
+ stateManager.setKey({key: [...stateKey, "description"], value: null})
+ }} />}
+
+ {requireable && {
+ stateManager.setKey({key: [...stateKey, "component", "required"], value: !req})
+ }} />}
+ >
+}
diff --git a/components-sdk/src/components/StringSelect.tsx b/components-sdk/src/components/StringSelect.tsx
index a59d119..80ad3c2 100644
--- a/components-sdk/src/components/StringSelect.tsx
+++ b/components-sdk/src/components/StringSelect.tsx
@@ -32,7 +32,8 @@ export function StringSelect({
stateManager,
passProps,
removeKeyParent = undefined,
- actionCallback
+ actionCallback,
+ fromLabel,
}: ComponentsProps & { state: StringSelectComponent }) {
// useEffect(() => {
// if (state.min_values > state.max_values) {
@@ -50,7 +51,7 @@ export function StringSelect({
return
-
+
{state.options.map((option, index) =>
}
-function GlobalSettings({state, stateKey, stateManager} : {
+function GlobalSettings({state, stateKey, stateManager, fromLabel} : {
state: StringSelectComponent,
stateKey: ComponentsProps['stateKey'],
stateManager: ComponentsProps['stateManager'],
+ fromLabel?: boolean
}) {
const {open, setOpen, ignoreRef, closeLockRef} = useStateOpen(0);
const btn_select = useRef(null);
@@ -100,9 +102,9 @@ function GlobalSettings({state, stateKey, stateManager} : {
{isInvalid && t('string-select.invalid')} {min_values === max_values ? min_values : `${min_values} – ${max_values}` }
{ !!open &&
- {open === 1 && }
- {open === 2 && }
- {open === 3 && }
+ {open === 1 && }
+ {open === 2 && }
+ {open === 3 && }
{open === 4 && }
}
@@ -126,61 +128,41 @@ export function MenuRange({min, max, state, stateKey, stateManager} : {
}
return
- i)}
- value={state}
- onChange={(no: any) => stateManager.setKey({key: stateKey, value: no})}
+ i+min)}
+ value={state-min}
+ onChange={(no: any) => stateManager.setKey({key: stateKey, value: no+min})}
/>
}
-function GlobalSettingsFirst({state, stateKey, stateManager, setOpen} : {
+function GlobalSettingsFirst({state, stateKey, stateManager, setOpen, fromLabel} : {
state: StringSelectComponent,
stateKey: ComponentsProps['stateKey'],
stateManager: ComponentsProps['stateManager'],
setOpen: Dispatch
>,
+ fromLabel?: boolean
}) {
const {t} = useTranslation("components-sdk")
return <>
- {
+ {!fromLabel &&
{
stateManager.setKey({key: [...stateKey, "disabled"], value: !state.disabled})
- }}>
-
- {state.disabled ? t("string-select.mark-enabled") : t('string-select.mark-disabled')}
-
-
- {
+ }} />}
+
{
setOpen(2);
ev.stopPropagation();
- }}>
-
- {!state.placeholder ? t('string-select.add-placeholder') : t('string-select.change-placeholder')}
-
-
- {!!state.placeholder && {
+ }} />
+ {!!state.placeholder &&
{
stateManager.setKey({key: [...stateKey, "placeholder"], value: null})
- }}>
-
- {t('string-select.clear-placeholder')}
- }
-
- {
+ }} />}
+
{
setOpen(3);
ev.stopPropagation();
- }}>
-
- {t('string-select.set-minimum')}
-
-
- {
+ }} />
+
{
setOpen(4);
ev.stopPropagation();
- }}>
-
- {t('string-select.set-maximum')}
-
-
+ }} />
>
}
@@ -213,8 +195,8 @@ function StringSelectOption({state, stateKey: stateParent, index, stateManager,
{ !!open &&
{open === 1 && }
{open === 2 && }
- {open === 3 && }
- {open === 4 && }
+ {open === 3 && }
+ {open === 4 && }
}
)
@@ -237,52 +219,29 @@ function MenuFirst({state, stateKey, stateManager, setOpen, removeKeyParent, act
actionCallback(state.value || null);
ev.stopPropagation();
}} />}
-
- {
+
{
stateManager.setKey({key: [...stateKey, "default"], value: !state.default})
- }}>
-
- {state.default ? t('string-select.default-unselect') : t('string-select.default-select')}
-
- {
+ }} />
+
{
setOpen(2);
ev.stopPropagation();
- }}>
-
- {state.emoji == null ? t('string-select.set-emoji') : t('string-select.change-emoji')}
-
- {state.emoji != null && {
+ }} />
+ {state.emoji != null &&
{
stateManager.setKey({key: [...stateKey, "emoji"], value: null})
- }}>
-
- {t('string-select.clear-emoji')}
- }
- {
+ }} />}
+
{
setOpen(3);
ev.stopPropagation();
- }}>
-
- {t('string-select.change-label')}
-
- {
+ }} />
+
{
setOpen(4);
ev.stopPropagation();
- }}>
-
- {state.description == null ? t('string-select.add-description') : t('string-select.change-description')}
-
- {state.description != null && {
+ }} />
+ {state.description != null &&
{
stateManager.setKey({key: [...stateKey, "description"], value: null})
- }}>
-
- {t('string-select.clear-description')}
- }
- {
+ }} />}
+
{
stateManager.deleteKey({key: stateKey, removeKeyParent});
- }}>
-
- {t('string-select.delete')}
-
+ }} />
>
}
diff --git a/components-sdk/src/components/TextDisplay.module.css b/components-sdk/src/components/TextDisplay.module.css
index 2b3479b..0f7559c 100644
--- a/components-sdk/src/components/TextDisplay.module.css
+++ b/components-sdk/src/components/TextDisplay.module.css
@@ -3,7 +3,7 @@
border-radius: 3px;
}
-.text_display_input[type="text"], .text_display_input[type="textbox"], .text_display [type="text"], .text_display [role="textbox"] {
+.text_display_input[type="text"], textarea.text_display_input, .text_display_input[type="number"], .text_display_input[type="textbox"], .text_display [type="text"], .text_display textarea, .text_display [type="number"], .text_display [role="textbox"] {
padding: 0;
min-height: 0;
border: unset;
diff --git a/components-sdk/src/components/TextInput.module.css b/components-sdk/src/components/TextInput.module.css
new file mode 100644
index 0000000..c3b880d
--- /dev/null
+++ b/components-sdk/src/components/TextInput.module.css
@@ -0,0 +1,45 @@
+.textInput {
+ padding: 0.8rem 1.6rem;
+ background: rgba(0, 0, 0, 0.4);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 4px;
+ color: #dcddde;
+ width: 100%;
+ position: relative;
+ cursor: pointer;
+
+ &:hover, &.open {
+ background: #101013;
+ }
+
+ .length {
+ position: absolute;
+ bottom: 4px;
+ right: 8px;
+ font-size: 1.2rem;
+ color: #8e8e99;
+ }
+
+ .desc {
+ font-style: italic !important;
+ color: gray !important;
+ }
+
+ .caret {
+ margin-inline-start: 1px;
+ width: 1px;
+ height: 1.6rem;
+ vertical-align: middle;
+ background: #dcddde;
+ display: inline-block;
+ }
+
+ .placeholder {
+ color: #8e8e99;
+ font-size: 1.3rem;
+ }
+
+ &.paragraph {
+ height: 100px;
+ }
+}
diff --git a/components-sdk/src/components/TextInput.tsx b/components-sdk/src/components/TextInput.tsx
new file mode 100644
index 0000000..48facb0
--- /dev/null
+++ b/components-sdk/src/components/TextInput.tsx
@@ -0,0 +1,170 @@
+import Styles from './TextInput.module.css';
+import { ComponentsProps } from '../Capsule';
+import { TextInputComponent, TextInputStyle } from '../utils/componentTypesModal';
+import { useTranslation } from 'react-i18next';
+import CapsuleStyles from '../Capsule.module.css';
+import { MenuArea, MenuLabel, MenuOption } from './Button';
+import { useStateOpen } from '../utils/useStateOpen';
+import { Dispatch, SetStateAction, useRef } from 'react';
+import Minimum from '../icons/Minimum.svg';
+import Maximum from '../icons/Maximum.svg';
+import MinimumActive from '../icons/MinimumActive.svg';
+import MaximumActive from '../icons/MaximumActive.svg';
+import AddDescription from '../icons/AddDescription.svg';
+import AddDescriptionActive from '../icons/AddDescriptionActive.svg';
+import DescriptionText from '../icons/DescriptionText.svg';
+import DescriptionTextActive from '../icons/DescriptionTextActive.svg';
+
+export function TextInput({ state, stateKey, stateManager }: ComponentsProps & { state: TextInputComponent }) {
+ const { open, setOpen, ignoreRef, closeLockRef } = useStateOpen(0);
+ const { t } = useTranslation('components-sdk');
+ const btn_select = useRef(null);
+
+ const len = (state.value || '').length;
+
+ return (
+ {
+ if (btn_select.current && btn_select.current.contains(ev.target as HTMLElement)) return;
+ setOpen(1);
+ }}
+ ref={ignoreRef}
+ >
+
+ {state.value ? (
+ <>
+ {state.value}
+
+ >
+ ) : (
+
+
+ {state.placeholder}
+
+ )}
+ {!!state.max_length && (
+
+ {len}/{state.max_length}
+
+ )}
+
+ {state.value && state.placeholder && (
+
+ {t('modal.text-input.placeholder-info')} {state.placeholder}
+
+ )}
+ {!!state.min_length && (
+
+ {t('modal.text-input.minimum-len-info')} {state.min_length}
+
+ )}
+ {!!open && (
+
+ {open === 1 && (
+
+ )}
+ {open === 2 && (
+
+ )}
+ {open === 3 && (
+
+ )}
+ {open === 4 && (
+
+ )}
+ {open === 5 && (
+
+ )}
+
+ )}
+
+ );
+}
+
+function MenuFirst({state, stateKey, stateManager, setOpen} : {
+ state: TextInputComponent,
+ stateKey: ComponentsProps['stateKey'],
+ stateManager: ComponentsProps['stateManager'],
+ setOpen: Dispatch>,
+}) {
+
+ const {t} = useTranslation("components-sdk");
+
+ return <>
+
+ {
+ setOpen(2);
+ ev.stopPropagation();
+ }} />
+
+ {!!state.value && {
+ stateManager.setKey({key: [...stateKey, "value"], value: null})
+ }} />}
+
+ {
+ setOpen(3);
+ ev.stopPropagation();
+ }} />
+
+ {!!state.placeholder && {
+ stateManager.setKey({key: [...stateKey, "placeholder"], value: null})
+ }} />}
+
+ {
+ setOpen(4);
+ ev.stopPropagation();
+ }} />
+
+ {!!state.min_length && {
+ stateManager.setKey({key: [...stateKey, "min_length"], value: null})
+ }} />}
+
+ {
+ setOpen(5);
+ ev.stopPropagation();
+ }} />
+
+ {!!state.max_length && {
+ stateManager.setKey({key: [...stateKey, "max_length"], value: null})
+ }} />}
+ >
+}
diff --git a/components-sdk/src/components/Thumbnail.tsx b/components-sdk/src/components/Thumbnail.tsx
index ae7a56d..8f2e60b 100644
--- a/components-sdk/src/components/Thumbnail.tsx
+++ b/components-sdk/src/components/Thumbnail.tsx
@@ -7,7 +7,7 @@ import AddDescription from '../icons/AddDescription.svg';
import AddDescriptionActive from '../icons/AddDescriptionActive.svg';
import { Dispatch, SetStateAction, useRef } from 'react';
import CapsuleStyles from '../Capsule.module.css';
-import { MenuLabel } from './Button';
+import { MenuLabel, MenuOption } from './Button';
import { ComponentsProps, default_settings } from '../Capsule';
import { MediaGalleryItem, ThumbnailComponent } from '../utils/componentTypes';
import { stateKeyType } from '../polyfills/StateManager';
@@ -18,7 +18,6 @@ import { useStateOpen } from '../utils/useStateOpen';
import { ClosestType } from '../dnd/types';
import { DragLines } from '../dnd/DragLine';
import { useTranslation } from 'react-i18next';
-import DescriptionTextActive from '../icons/DescriptionTextActive.svg';
export function Thumbnail({
state,
@@ -89,6 +88,7 @@ export function Thumbnail({
stateManager={stateManager}
setOpen={setOpen}
placeholder={t('thumbnail.image-url')}
+ max={512} // guessing, official limit unknown
/>
)}
{open === 3 && (
@@ -99,6 +99,7 @@ export function Thumbnail({
stateManager={stateManager}
nullable={true}
setOpen={setOpen}
+ max={1024}
/>
)}
@@ -120,53 +121,33 @@ function MenuFirst({state, stateKey, stateManager, setOpen, openFileSelector, re
}) {
const {t} = useTranslation('components-sdk');
return <>
- {
+
{
stateManager.setKey({key: [...stateKey, "media", "url"], value: ''});
setOpen(2)
ev.stopPropagation();
- }}>
-
- {t('thumbnail.set-image-url')}
-
- {
+ }} />
+
{
openFileSelector();
- }}>
-
- {t('thumbnail.upload-image')}
-
- {
+ }} />
+
{
setOpen(3)
ev.stopPropagation();
- }}>
-
- {state.description ? t('thumbnail.change-description') : t('thumbnail.add-description')}
-
- {!!state.description && {
+ }} />
+ {!!state.description &&
{
stateManager.setKey({key: [...stateKey, "description"], value: null})
- }}>
-
- {t('thumbnail.clear-description')}
- }
- {
+ }} />}
+
{
stateManager.setKey({key: [...stateKey, "spoiler"], value: !state.spoiler});
- }}>
-
- {state.spoiler ? t('thumbnail.remove-spoiler') : t('thumbnail.set-spoiler')}
-
- {!!removeKeyParent && {
+ }} />
+ {!!removeKeyParent &&
{
setOpen(0);
stateManager.deleteKey({key: stateKey, removeKeyParent});
ev.stopPropagation();
- }}>
-
- {t('thumbnail.delete')}
- }
- {(!!removeKeyParent && allowAddition) && {
+ }} />}
+ {(!!removeKeyParent && allowAddition) &&
{
setOpen(0);
stateManager.appendKey({key: [...removeKeyParent, 'items'], value: default_settings.MediaGallery.items[0]});
ev.stopPropagation();
- }}>
- {t('thumbnail.add-image')}
- }
+ }} />}
>
}
\ No newline at end of file
diff --git a/components-sdk/src/dnd/DragContext.tsx b/components-sdk/src/dnd/DragContext.tsx
index 154b0ec..543a00f 100644
--- a/components-sdk/src/dnd/DragContext.tsx
+++ b/components-sdk/src/dnd/DragContext.tsx
@@ -3,6 +3,7 @@ import { createContext, FC, ReactNode, useCallback, useContext, useMemo, useRef,
import { StateManager } from '../polyfills/StateManager';
import { DragEvents } from './DragEvents';
import { BoundariesProps } from './boundaries';
+import { RenderMode } from '../utils/componentTypes';
const DragContext = createContext(undefined);
@@ -16,11 +17,13 @@ export function useDragContext(): DragContextType {
export const DragContextProvider: FC<{
children: ReactNode;
- stateManager: StateManager
+ stateManager: StateManager;
+ renderMode: RenderMode
} & BoundariesProps> = ({
children,
stateManager,
- boundaries
+ boundaries,
+ renderMode
}) => {
const refs = useRef>(new Set());
@@ -46,7 +49,7 @@ export const DragContextProvider: FC<{
return (
- {children}
+ {children}
);
};
\ No newline at end of file
diff --git a/components-sdk/src/dnd/DragEvents.tsx b/components-sdk/src/dnd/DragEvents.tsx
index a0a5888..96c8168 100644
--- a/components-sdk/src/dnd/DragEvents.tsx
+++ b/components-sdk/src/dnd/DragEvents.tsx
@@ -6,6 +6,7 @@ import { useDragContext } from './DragContext';
import { handleDragDrop } from './handleDragDrop';
import { handleDragOver } from './handleDragOver';
import { BoundariesProps, testBoundaries } from './boundaries';
+import { RenderMode } from '../utils/componentTypes';
const handleDragStart = (e: DragEvent, { boundaries }: BoundariesProps) => {
if (!testBoundaries(e.target, boundaries)) return;
@@ -32,7 +33,8 @@ const handleDragEnd = (
export const DragEvents: FC<{
children: ReactNode;
stateManager: StateManager;
-} & BoundariesProps> = ({ children, stateManager, boundaries }) => {
+ renderMode: RenderMode;
+} & BoundariesProps> = ({ children, stateManager, boundaries, renderMode }) => {
const { refs, visible, setVisible, keyToDelete } = useDragContext();
useEffect(() => {
@@ -48,6 +50,7 @@ export const DragEvents: FC<{
visible,
setVisible,
stateManager,
+ renderMode,
keyToDelete,
boundaries: boundaries
});
@@ -67,7 +70,7 @@ export const DragEvents: FC<{
window.removeEventListener('mouseup', onMouseUp);
};
}, [
- visible, stateManager,
+ visible, stateManager, renderMode,
]);
return <>{children}>;
diff --git a/components-sdk/src/dnd/components.ts b/components-sdk/src/dnd/components.ts
index 1a9f169..952b7e0 100644
--- a/components-sdk/src/dnd/components.ts
+++ b/components-sdk/src/dnd/components.ts
@@ -2,14 +2,18 @@ import {
Component,
ComponentType,
ComponentTypeUnofficial,
+ MediaGalleryComponent,
+ MediaGalleryItem,
parseComponent,
StringSelectComponent,
+ StringSelectComponentOption,
} from '../utils/componentTypes';
import { SECTIONABLE } from '../Capsule';
import { uuidv4 } from '../utils/randomGen';
import { DistanceProps, DistanceReturn, KeyToDeleteType } from './types';
import { distanceCenter, distanceHorizontal, distanceVertical } from './distance';
import { stateKeyType, StateManager } from '../polyfills/StateManager';
+import { parseModalComponent } from '../utils/componentTypesModal';
/*
* This file gathers all configuration how components can be dragged and dropped
@@ -25,6 +29,7 @@ export enum DroppableID {
GALLERY_ITEM,
STRING_SELECT,
CONTAINER,
+ MODAL_TOP_LEVEL,
}
@@ -32,6 +37,7 @@ export enum DroppableID {
export function getDroppableOrientation(droppableId: DroppableID): (props: DistanceProps) => DistanceReturn {
switch (droppableId) {
case DroppableID.TOP_LEVEL:
+ case DroppableID.MODAL_TOP_LEVEL:
case DroppableID.STRING_SELECT:
case DroppableID.CONTAINER:
case DroppableID.SECTION_CONTENT:
@@ -58,7 +64,18 @@ export function isValidLocation(compType: ComponentType | ComponentTypeUnofficia
return (droppableId: DroppableID) => {
switch (droppableId) {
case DroppableID.TOP_LEVEL:
- return true;
+ return compType === null || [
+ ComponentType.ACTION_ROW, ComponentType.BUTTON, ComponentType.STRING_SELECT, ComponentType.SECTION,
+ ComponentType.TEXT_DISPLAY, ComponentType.THUMBNAIL, ComponentType.MEDIA_GALLERY, ComponentType.FILE,
+ ComponentType.SEPARATOR, ComponentType.CONTAINER, ComponentTypeUnofficial.MEDIA_GALLERY_ITEM,
+ ComponentTypeUnofficial.STRING_SELECT_OPTION, ComponentType.LABEL,
+ ].includes(compType as any);
+ case DroppableID.MODAL_TOP_LEVEL:
+ return compType === null || [
+ ComponentType.ACTION_ROW, ComponentType.STRING_SELECT, ComponentType.TEXT_INPUT, ComponentType.LABEL,
+ ComponentType.FILE_UPLOAD, ComponentTypeUnofficial.STRING_SELECT_OPTION, ComponentType.TEXT_DISPLAY,
+ ComponentType.SECTION
+ ].includes(compType as any);
case DroppableID.BUTTON:
return compType == ComponentType.BUTTON;
case DroppableID.SECTION_ADD_ACCESSORY:
@@ -67,7 +84,12 @@ export function isValidLocation(compType: ComponentType | ComponentTypeUnofficia
case DroppableID.STRING_SELECT:
return compType == ComponentTypeUnofficial.STRING_SELECT_OPTION
case DroppableID.CONTAINER:
- return compType === null || compType !== ComponentType.CONTAINER;
+ return compType === null || [
+ ComponentType.ACTION_ROW, ComponentType.BUTTON, ComponentType.STRING_SELECT, ComponentType.SECTION,
+ ComponentType.TEXT_DISPLAY, ComponentType.THUMBNAIL, ComponentType.MEDIA_GALLERY, ComponentType.FILE,
+ ComponentType.SEPARATOR, ComponentTypeUnofficial.MEDIA_GALLERY_ITEM, ComponentTypeUnofficial.STRING_SELECT_OPTION,
+ ComponentType.LABEL,
+ ].includes(compType as any);
case DroppableID.SECTION_CONTENT:
return SECTIONABLE.includes(compType as any)
case DroppableID.GALLERY_ITEM:
@@ -78,68 +100,75 @@ export function isValidLocation(compType: ComponentType | ComponentTypeUnofficia
};
}
-function randomizeIds(data: any): any {
+export function randomizeIds(data: object): object;
+export function randomizeIds(data: unknown): unknown {
if (Array.isArray(data)) {
return data.map(randomizeIds);
} else if (data && typeof data === 'object') {
+ // "value" in TextInput should not be randomized
+ const val = "type" in data && data.type == ComponentType.TEXT_INPUT ? ['custom_id'] : ['custom_id', 'value'];
+
return Object.fromEntries(
Object.entries(data).map(([key, value]) => [
key,
- ['custom_id', 'value'].includes(key) ? uuidv4() : randomizeIds(value),
+ val.includes(key) ? uuidv4() : randomizeIds(value),
])
);
}
return data;
}
-export function guessComponentType(arg: object): ComponentType | ComponentTypeUnofficial | null {
- if ('type' in arg && typeof arg.type === 'number' && arg.type in ComponentType) {
- return arg.type;
- }
- if ('label' in arg && typeof arg.label === 'string' && 'value' in arg && typeof arg.value === 'string') {
- return ComponentTypeUnofficial.STRING_SELECT_OPTION;
- }
- if (
+export function isOfficialComponent(arg: object): arg is Component {
+ return 'type' in arg && typeof arg.type === 'number' && arg.type in ComponentType;
+}
+
+export function isStringSelectComponentOption(arg: object): arg is StringSelectComponentOption {
+ return 'label' in arg && typeof arg.label === 'string' && 'value' in arg && typeof arg.value === 'string';
+}
+
+export function isMediaGalleryItem(arg: object): arg is MediaGalleryItem {
+ return (
'media' in arg &&
typeof arg.media === 'object' &&
arg.media !== null &&
!Array.isArray(arg.media) &&
'url' in arg.media &&
typeof arg.media.url === 'string'
- ) {
- return ComponentTypeUnofficial.MEDIA_GALLERY_ITEM;
- }
+ )
+}
+
+export function guessComponentType(arg: object): ComponentType | ComponentTypeUnofficial | null {
+ if (isOfficialComponent(arg)) return arg.type;
+ if (isStringSelectComponentOption(arg)) return ComponentTypeUnofficial.STRING_SELECT_OPTION;
+ if (isMediaGalleryItem(arg)) return ComponentTypeUnofficial.MEDIA_GALLERY_ITEM;
return null;
}
-export function getValidObj(comp: object, droppableId: DroppableID, randomizeId: boolean) {
- let compValid;
- const compType = guessComponentType(comp);
- if (!compType) return null;
+export function getValidObj(comp_: object, droppableId: DroppableID, randomizeId: boolean) {
+ const comp = randomizeId ? randomizeIds(comp_) : comp_;
+ const isOfficial = isOfficialComponent(comp);
+ const isMediaItem = isMediaGalleryItem(comp);
+ const isStringOption = isStringSelectComponentOption(comp);
- // Don't assume that component matches the droppableId as it's not always the case
- if (!isValidLocation(compType)(droppableId)) return null;
+ if (isMediaItem || (isOfficial && comp.type === ComponentType.THUMBNAIL)) {
+ if ([DroppableID.SECTION_ADD_ACCESSORY, DroppableID.SECTION_EDIT_ACCESSORY].includes(droppableId))
+ return parseComponent[ComponentType.THUMBNAIL](comp) ?? null;
- if (randomizeId) comp = randomizeIds(comp);
+ else if (droppableId === DroppableID.GALLERY_ITEM)
+ return parseComponent[ComponentTypeUnofficial.MEDIA_GALLERY_ITEM](comp) ?? null;
- if (
- compType === ComponentType.BUTTON &&
- ![DroppableID.BUTTON, DroppableID.SECTION_ADD_ACCESSORY, DroppableID.SECTION_EDIT_ACCESSORY].includes(
- droppableId
- )
- ) {
- compValid = parseComponent[ComponentType.ACTION_ROW]({
- components: [comp],
- type: ComponentType.ACTION_ROW,
- } as Component);
- } else if (compType === ComponentType.STRING_SELECT) { // Not possible via UI
- compValid = parseComponent[ComponentType.ACTION_ROW]({
- components: [comp],
- type: ComponentType.ACTION_ROW,
- } as Component);
- } else if (compType === ComponentTypeUnofficial.STRING_SELECT_OPTION && droppableId !== DroppableID.STRING_SELECT) {
- compValid = parseComponent[ComponentType.ACTION_ROW]({
+ return parseComponent[ComponentType.MEDIA_GALLERY]({
+ items: [comp],
+ type: ComponentType.MEDIA_GALLERY,
+ } as MediaGalleryComponent) ?? null;
+ }
+
+ if (isStringOption) {
+ if (droppableId === DroppableID.STRING_SELECT)
+ return parseComponent[ComponentTypeUnofficial.STRING_SELECT_OPTION](comp) ?? null;
+
+ return parseComponent[ComponentType.ACTION_ROW]({
components: [{
type: ComponentType.STRING_SELECT,
custom_id: uuidv4(),
@@ -148,25 +177,46 @@ export function getValidObj(comp: object, droppableId: DroppableID, randomizeId:
]
} as StringSelectComponent],
type: ComponentType.ACTION_ROW,
- } as Component);
- } else if ([ComponentType.THUMBNAIL, ComponentTypeUnofficial.MEDIA_GALLERY_ITEM].includes(compType)) {
- if ([DroppableID.SECTION_ADD_ACCESSORY, DroppableID.SECTION_EDIT_ACCESSORY].includes(droppableId)) {
- compValid = parseComponent[ComponentType.THUMBNAIL](comp);
- } else if (droppableId === DroppableID.GALLERY_ITEM) {
- compValid = parseComponent[ComponentTypeUnofficial.MEDIA_GALLERY_ITEM](comp);
- } else {
- compValid = parseComponent[ComponentType.MEDIA_GALLERY]({
- items: [comp],
- type: ComponentType.MEDIA_GALLERY,
- } as Component);
+ } as Component) ?? null;
+ }
+
+ if (!isOfficial) return null;
+
+ // Only official components beyond this point
+
+ if (comp.type === ComponentType.BUTTON) {
+ if (
+ [DroppableID.BUTTON, DroppableID.SECTION_ADD_ACCESSORY, DroppableID.SECTION_EDIT_ACCESSORY]
+ .includes(droppableId)
+ )
+ return parseComponent[ComponentType.BUTTON](comp) ?? null;
+
+ return parseComponent[ComponentType.ACTION_ROW]({
+ components: [comp],
+ type: ComponentType.ACTION_ROW,
+ } as Component) ?? null;
+ }
+
+ if (comp.type === ComponentType.STRING_SELECT) {
+ return parseComponent[ComponentType.ACTION_ROW]({
+ components: [comp],
+ type: ComponentType.ACTION_ROW,
+ } as Component) ?? null;
+ }
+
+ // Select compatibility with modal components
+ if (comp.type === ComponentType.LABEL) {
+ const label = parseModalComponent[ComponentType.LABEL](comp);
+ if (label !== null && label.component.type === ComponentType.STRING_SELECT) {
+ return parseComponent[ComponentType.ACTION_ROW]({
+ components: [label.component],
+ type: ComponentType.ACTION_ROW,
+ } as Component) ?? null;
}
- } else {
- // @ts-ignore We trust that guessComponentType() will ensure that comp is a valid component
- compValid = parseComponent[compType](comp);
+ return null;
}
- if (!compValid) return null;
- return compValid;
+ return parseComponent[comp.type](comp) ?? null;
}
function arraysEqual(arr1: T[], arr2: T[]): boolean {
diff --git a/components-sdk/src/dnd/componentsModal.ts b/components-sdk/src/dnd/componentsModal.ts
new file mode 100644
index 0000000..3d06be7
--- /dev/null
+++ b/components-sdk/src/dnd/componentsModal.ts
@@ -0,0 +1,64 @@
+import { ComponentType, ComponentTypeUnofficial, parseComponent, TextDisplayComponent } from '../utils/componentTypes';
+import { LabelComponent, LabelPossible, parseModalComponent } from '../utils/componentTypesModal';
+import { generateRandomAnimal, uuidv4 } from '../utils/randomGen';
+import { DroppableID, isOfficialComponent, isStringSelectComponentOption, randomizeIds } from './components';
+
+function labelize(comp: object) {
+ return {
+ type: ComponentType.LABEL,
+ label: generateRandomAnimal(),
+ component: comp,
+ } as LabelComponent;
+}
+
+export function getValidModalObj(comp_: object, droppableId: DroppableID, randomizeId: boolean) {
+ const comp = randomizeId ? randomizeIds(comp_) : comp_;
+
+ const isOfficial = isOfficialComponent(comp);
+ const isStringOption = isStringSelectComponentOption(comp);
+
+ if (isStringOption) {
+ if (droppableId === DroppableID.STRING_SELECT)
+ return parseModalComponent[ComponentTypeUnofficial.STRING_SELECT_OPTION](comp) ?? null;
+
+ return parseModalComponent[ComponentType.LABEL](labelize({
+ type: ComponentType.STRING_SELECT,
+ custom_id: uuidv4(),
+ options: [comp]
+ })) ?? null;
+ }
+
+ if (!isOfficial) return null;
+
+ // Are top-level components are labels
+
+ // Select compatibility with chat components
+ if (comp.type === ComponentType.ACTION_ROW) {
+ const actionRow = parseComponent[ComponentType.ACTION_ROW](comp);
+ if (actionRow !== null && actionRow.components.length === 1 && actionRow.components[0].type === ComponentType.STRING_SELECT) {
+ return parseModalComponent[ComponentType.LABEL](labelize(actionRow.components[0]));
+ }
+ return null;
+ }
+
+ // Compatibility: convert sections to text displays
+ if (comp.type === ComponentType.SECTION) {
+ const sec = parseComponent[ComponentType.SECTION](comp);
+ if (sec === null) return null;
+
+ const text = sec.components.filter(c => c.type === ComponentType.TEXT_DISPLAY).map( c => c.content ).join('\n');
+ return {
+ type: ComponentType.TEXT_DISPLAY,
+ content: text,
+ } as TextDisplayComponent;
+ }
+
+ if (comp.type === ComponentType.LABEL || comp.type === ComponentType.TEXT_DISPLAY)
+ return parseModalComponent[comp.type](comp) ?? null;
+
+ return parseModalComponent[ComponentType.LABEL]({
+ type: ComponentType.LABEL,
+ label: generateRandomAnimal(),
+ component: comp,
+ } as LabelComponent) ?? null;
+}
\ No newline at end of file
diff --git a/components-sdk/src/dnd/handleDragDrop.ts b/components-sdk/src/dnd/handleDragDrop.ts
index 38c0fc1..ef75983 100644
--- a/components-sdk/src/dnd/handleDragDrop.ts
+++ b/components-sdk/src/dnd/handleDragDrop.ts
@@ -1,9 +1,10 @@
import { ClosestType, DragContextType } from './types';
import { StateManager } from '../polyfills/StateManager';
-import { customDropActions, getValidObj } from './components';
+import { customDropActions, getValidObj, guessComponentType, isValidLocation } from './components';
import { default_settings } from '../Capsule';
-import { TextDisplayComponent } from '../utils/componentTypes';
+import { RenderMode, TextDisplayComponent } from '../utils/componentTypes';
import { BoundariesProps, testBoundaries } from './boundaries';
+import { getValidModalObj } from './componentsModal';
function assertValidJSON(arg: unknown): asserts arg is object {
if (typeof arg !== 'object' || arg === null) throw new Error('Invalid component type');
@@ -36,12 +37,14 @@ export const handleDragDrop = (
visible,
setVisible,
stateManager,
+ renderMode,
keyToDelete: keyToDeleteRef,
boundaries,
}: {
visible: DragContextType['visible'];
setVisible: DragContextType['setVisible'];
stateManager: StateManager;
+ renderMode: RenderMode;
keyToDelete: DragContextType['keyToDelete'];
} & BoundariesProps
) => {
@@ -64,7 +67,12 @@ export const handleDragDrop = (
const comp = getJSON(e.dataTransfer);
if (!comp) return;
- const value = getValidObj(comp, visible.ref.droppableId, e.dataTransfer.dropEffect === 'copy');
+ const compType = guessComponentType(comp);
+ if (!compType) return;
+ if (!isValidLocation(compType)(visible.ref.droppableId)) return;
+
+ const compFunc = renderMode === RenderMode.MODAL ? getValidModalObj : getValidObj;
+ const value = compFunc(comp, visible.ref.droppableId, e.dataTransfer.dropEffect === 'copy');
if (value === null) return;
e.preventDefault();
diff --git a/components-sdk/src/icons/FileUpload.svg b/components-sdk/src/icons/FileUpload.svg
new file mode 100644
index 0000000..9f15053
--- /dev/null
+++ b/components-sdk/src/icons/FileUpload.svg
@@ -0,0 +1,11 @@
+
diff --git a/components-sdk/src/icons/Input.svg b/components-sdk/src/icons/Input.svg
new file mode 100644
index 0000000..ecea13b
--- /dev/null
+++ b/components-sdk/src/icons/Input.svg
@@ -0,0 +1,4 @@
+
diff --git a/components-sdk/src/icons/MaximumActive.svg b/components-sdk/src/icons/MaximumActive.svg
new file mode 100644
index 0000000..2d10672
--- /dev/null
+++ b/components-sdk/src/icons/MaximumActive.svg
@@ -0,0 +1,3 @@
+
diff --git a/components-sdk/src/icons/MinimumActive.svg b/components-sdk/src/icons/MinimumActive.svg
new file mode 100644
index 0000000..21d467f
--- /dev/null
+++ b/components-sdk/src/icons/MinimumActive.svg
@@ -0,0 +1,3 @@
+
diff --git a/components-sdk/src/icons/Required.svg b/components-sdk/src/icons/Required.svg
new file mode 100644
index 0000000..6ef8250
--- /dev/null
+++ b/components-sdk/src/icons/Required.svg
@@ -0,0 +1 @@
+
diff --git a/components-sdk/src/icons/RequiredActive.svg b/components-sdk/src/icons/RequiredActive.svg
new file mode 100644
index 0000000..5111070
--- /dev/null
+++ b/components-sdk/src/icons/RequiredActive.svg
@@ -0,0 +1 @@
+
diff --git a/components-sdk/src/icons/Textarea.svg b/components-sdk/src/icons/Textarea.svg
new file mode 100644
index 0000000..624b1d9
--- /dev/null
+++ b/components-sdk/src/icons/Textarea.svg
@@ -0,0 +1,4 @@
+
diff --git a/components-sdk/src/icons/UploadFile.svg b/components-sdk/src/icons/UploadFile.svg
new file mode 100644
index 0000000..77ff619
--- /dev/null
+++ b/components-sdk/src/icons/UploadFile.svg
@@ -0,0 +1,4 @@
+
diff --git a/components-sdk/src/locales/en.json b/components-sdk/src/locales/en.json
index 95f095a..89890f8 100644
--- a/components-sdk/src/locales/en.json
+++ b/components-sdk/src/locales/en.json
@@ -59,5 +59,36 @@
"button.change-emoji": "Change emoji",
"button.clear-emoji": "Clear emoji",
"button.change-label": "Change label",
- "button.delete": "Delete"
+ "button.delete": "Delete",
+ "button.add-input": "Add question",
+ "components.modal.file-upload": "Ask for file",
+ "components.modal.short-input": "Ask for short text",
+ "components.modal.paragraph-input": "Ask for long text",
+ "components.modal.content": "Add helper text",
+ "components.modal.string-select": "Add select menu",
+ "modal.text-input.placeholder-info": "When empty: ",
+ "modal.text-input.minimum-len-info": "Minimum length: ",
+ "modal.text-input.change-value": "Change prefilled value",
+ "modal.text-input.add-value": "Add prefilled value",
+ "modal.text-input.clear-value": "Clear prefilled value",
+ "modal.text-input.change-placeholder": "Change placeholder",
+ "modal.text-input.add-placeholder": "Add placeholder",
+ "modal.text-input.clear-placeholder": "Clear placeholder",
+ "modal.text-input.set-minimum": "Set minimum length",
+ "modal.text-input.change-minimum": "Change minimum length",
+ "modal.text-input.clear-minimum": "Clear minimum length",
+ "modal.text-input.set-maximum": "Set maximum length",
+ "modal.text-input.change-maximum": "Change maximum length",
+ "modal.text-input.clear-maximum": "Clear maximum length",
+ "modal.label.change-label": "Change label",
+ "modal.label.add-description": "Add description",
+ "modal.label.change-description": "Change description",
+ "modal.label.clear-description": "Clear description",
+ "modal.label.unset-required": "Set as optional",
+ "modal.label.set-required": "Set as required",
+ "modal.label.required": "required",
+ "modal.file-upload.info-one": "Users can send one file or image here.",
+ "modal.file-upload.info-multiple": "Users can send multiple files or images here.",
+ "modal.file-upload.set-maximum": "Set maximum number of files",
+ "modal.file-upload.set-minimum": "Set minimum number of files"
}
\ No newline at end of file
diff --git a/components-sdk/src/locales/fr.json b/components-sdk/src/locales/fr.json
index b7138c6..6df8a99 100644
--- a/components-sdk/src/locales/fr.json
+++ b/components-sdk/src/locales/fr.json
@@ -59,5 +59,36 @@
"button.change-emoji": "Modifier l'emoji",
"button.clear-emoji": "Effacer l'emoji",
"button.change-label": "Modifier l'étiquette",
- "button.delete": "Supprimer"
+ "button.delete": "Supprimer",
+ "button.add-input": "Ajouter une question",
+ "components.modal.file-upload": "Demander un fichier",
+ "components.modal.short-input": "Saisir un texte court",
+ "components.modal.paragraph-input": "Saisir un texte long"
+ "components.modal.content": "ajouter un texte d'aide",
+ "components.modal.string-select": "Ajouter un menu de sélection",
+ "modal.text-input.placeholder-info": "When empty: ",
+ "modal.text-input.minimum-len-info": "Taille minimum : ",
+ "modal.text-input.change-value": "Changer le texte préremplis",
+ "modal.text-input.add-value": "Ajouter le texte préremplis",
+ "modal.text-input.clear-value": "Supprime le texte préremplis",
+ "modal.text-input.change-placeholder": "Changer le placeholder",
+ "modal.text-input.add-placeholder": "Ajouter le placeholder",
+ "modal.text-input.clear-placeholder": "Supprimer le placeholder",
+ "modal.text-input.set-minimum": "Définir la longueur minimal",
+ "modal.text-input.change-minimum": "Modifier la longueur minimal",
+ "modal.text-input.clear-minimum": "Supprimer la longueur minimal",
+ "modal.text-input.set-maximum": "Définir la longueur maximale",
+ "modal.text-input.change-maximum": "Modifier la longueur maximale",
+ "modal.text-input.clear-maximum": "Supprimer la longueur maximale",
+ "modal.label.change-label": "Modifier le libellé",
+ "modal.label.add-description": "Ajouter une description",
+ "modal.label.change-description": "Modifier la description",
+ "modal.label.clear-description": "Supprimer la description",
+ "modal.label.unset-required": "Rendre optionnel",
+ "modal.label.set-required": "Rendre obligatoire",
+ "modal.label.required": "obligatoire",
+ "modal.file-upload.info-one": "Les utilisateurs pourront envoyer un fichier ou une image ici.",
+ "modal.file-upload.info-multiple": "Les utilisateurs pourront envoyer plusieurs fichiers ou images ici.",
+ "modal.file-upload.set-maximum": "Définir le nombre maximum de fichiers",
+ "modal.file-upload.set-minimum": "Définir le nombre minimum de fichiers"
}
diff --git a/components-sdk/src/locales/pl.json b/components-sdk/src/locales/pl.json
index 0976af2..b0978f5 100644
--- a/components-sdk/src/locales/pl.json
+++ b/components-sdk/src/locales/pl.json
@@ -59,5 +59,36 @@
"button.change-emoji": "Zmień emoji",
"button.clear-emoji": "Usuń emoji",
"button.change-label": "Zmień etykietę",
- "button.delete": "Usuń"
+ "button.delete": "Usuń",
+ "button.add-input": "Dodaj pytanie",
+ "components.modal.file-upload": "Zapytaj o plik",
+ "components.modal.short-input": "Zapytaj o krótki tekst",
+ "components.modal.paragraph-input": "Zapytaj o długi tekst",
+ "components.modal.content": "Dodaj tekst pomocniczy",
+ "components.modal.string-select": "Dodaj menu rozwijane",
+ "modal.text-input.placeholder-info": "Gdy puste: ",
+ "modal.text-input.minimum-len-info": "Minimalna długość: ",
+ "modal.text-input.change-value": "Zmień wstępnie wpisaną wartość",
+ "modal.text-input.add-value": "Dodaj wstępnie wpisaną wartość",
+ "modal.text-input.clear-value": "Usuń wstępnie wpisaną wartość",
+ "modal.text-input.change-placeholder": "Zmień tekst przed wyborem",
+ "modal.text-input.add-placeholder": "Dodaj tekst przed wyborem",
+ "modal.text-input.clear-placeholder": "Usuń tekst przed wyborem",
+ "modal.text-input.set-minimum": "Ustaw minimalną długość",
+ "modal.text-input.change-minimum": "Zmień minimalną długość",
+ "modal.text-input.clear-minimum": "Usuń minimalną długość",
+ "modal.text-input.set-maximum": "Ustaw maksymalną długość",
+ "modal.text-input.change-maximum": "Zmień maksymalną długość",
+ "modal.text-input.clear-maximum": "Usuń maksymalną długość",
+ "modal.label.change-label": "Zmień etykietę",
+ "modal.label.add-description": "Dodaj opis",
+ "modal.label.change-description": "Zmień opis",
+ "modal.label.clear-description": "Usuń opis",
+ "modal.label.unset-required": "Ustaw jako opcjonalne",
+ "modal.label.set-required": "Ustaw jako wymagane",
+ "modal.label.required": "wymagane",
+ "modal.file-upload.info-one": "Użytkownicy wyślą tutaj jeden plik lub obrazek.",
+ "modal.file-upload.info-multiple": "Użytkownicy wyślą tutaj wiele plików lub obrazków.",
+ "modal.file-upload.set-maximum": "Ustaw maksymalną liczbę plików",
+ "modal.file-upload.set-minimum": "Ustaw minimalną liczbę plików"
}
\ No newline at end of file
diff --git a/components-sdk/src/utils/componentTypes.ts b/components-sdk/src/utils/componentTypes.ts
index 495d84b..0a2e78d 100644
--- a/components-sdk/src/utils/componentTypes.ts
+++ b/components-sdk/src/utils/componentTypes.ts
@@ -1,13 +1,12 @@
-import { BetterInput, BetterInputProps } from '../polyfills/BetterInput';
+import { BetterInput } from '../polyfills/BetterInput';
import { EmojiPicker } from '../polyfills/EmojiPicker';
import { EmojiShow } from '../polyfills/EmojiShow';
import { getFileNameType, getFileType, setFileType } from '../polyfills/files';
import { ColorPicker } from '../polyfills/ColorPicker';
-import { FC } from 'react';
import { ActionMenu } from '../polyfills/ActionMenu';
// This fragment of code is written in dedication to the JS devs who have to deal with this mess every day.
-function isObject(arg: unknown): arg is object {
+export function isObject(arg: unknown): arg is object {
return typeof arg === 'object' && arg !== null && !Array.isArray(arg);
}
@@ -16,6 +15,7 @@ export enum ComponentType {
ACTION_ROW = 1,
BUTTON = 2,
STRING_SELECT = 3,
+ TEXT_INPUT = 4,
SECTION = 9,
TEXT_DISPLAY = 10,
@@ -23,7 +23,9 @@ export enum ComponentType {
MEDIA_GALLERY = 12,
FILE = 13,
SEPARATOR = 14,
- CONTAINER = 17
+ CONTAINER = 17,
+ LABEL = 18,
+ FILE_UPLOAD = 19,
}
export enum ComponentTypeUnofficial {
@@ -42,8 +44,16 @@ export const parseComponent = {
[ComponentType.FILE]: parseFileComponent,
[ComponentType.SEPARATOR]: parseSeparatorComponent,
[ComponentType.CONTAINER]: parseContainerComponent,
+ [ComponentType.TEXT_INPUT]: () => null,
+ [ComponentType.LABEL]: () => null,
+ [ComponentType.FILE_UPLOAD]: () => null,
[ComponentTypeUnofficial.MEDIA_GALLERY_ITEM]: parseMediaGalleryItem,
[ComponentTypeUnofficial.STRING_SELECT_OPTION]: parseStringSelectComponentOption,
+} as const;
+
+export enum RenderMode {
+ MESSAGE = 0,
+ MODAL = 1
}
export type PassProps = {
@@ -56,6 +66,7 @@ export type PassProps = {
EmojiShow: EmojiShow,
ActionMenu?: ActionMenu,
interactiveDisabled: boolean,
+ renderMode?: RenderMode,
}
export enum ButtonStyle {
diff --git a/components-sdk/src/utils/componentTypesModal.ts b/components-sdk/src/utils/componentTypesModal.ts
new file mode 100644
index 0000000..1b50b57
--- /dev/null
+++ b/components-sdk/src/utils/componentTypesModal.ts
@@ -0,0 +1,168 @@
+import {
+ Component,
+ ComponentType,
+ ComponentTypeUnofficial,
+ parseBaseComponent,
+ parseComponent,
+ parseStringSelectComponentOption,
+ StringSelectComponent,
+ TextDisplayComponent,
+} from './componentTypes';
+
+export type LabelPossible = TextInputComponent | ModalStringSelectComponent | FileUploadComponent;
+
+export const parseModalComponent = {
+ [ComponentType.ACTION_ROW]: () => null,
+ [ComponentType.BUTTON]: () => null,
+ [ComponentType.STRING_SELECT]: parseStringSelectComponent,
+ [ComponentType.SECTION]: () => null,
+ [ComponentType.TEXT_DISPLAY]: parseTextDisplayComponent,
+ [ComponentType.THUMBNAIL]: () => null,
+ [ComponentType.MEDIA_GALLERY]: () => null,
+ [ComponentType.FILE]: () => null,
+ [ComponentType.SEPARATOR]: () => null,
+ [ComponentType.CONTAINER]: () => null,
+ [ComponentType.TEXT_INPUT]: parseTextInputComponent,
+ [ComponentType.LABEL]: parseLabelComponent,
+ [ComponentType.FILE_UPLOAD]: parseFileUploadComponent,
+ [ComponentTypeUnofficial.MEDIA_GALLERY_ITEM]: () => null,
+ [ComponentTypeUnofficial.STRING_SELECT_OPTION]: parseStringSelectComponentOption,
+} as const;
+
+// All components that have 'required' field supported in modals
+export const MODAL_SUPPORTS_REQUIRED: ComponentType[] = [
+ ComponentType.TEXT_INPUT,
+ ComponentType.STRING_SELECT,
+ ComponentType.FILE_UPLOAD,
+]
+
+export enum TextInputStyle {
+ SHORT = 1,
+ PARAGRAPH = 2,
+}
+
+export interface TextInputComponent extends Component {
+ type: ComponentType.TEXT_INPUT;
+ custom_id: string;
+ style: TextInputStyle;
+ // label?: string; deprecated
+ min_length?: number | null;
+ max_length?: number | null;
+ required?: boolean;
+ value?: string | null;
+ placeholder?: string | null;
+}
+
+function parseTextInputComponent(component: Component): TextInputComponent | null {
+ if (!('custom_id' in component) || typeof component.custom_id !== 'string') return null;
+ if (
+ !('style' in component) ||
+ !(component.style === TextInputStyle.SHORT || component.style === TextInputStyle.PARAGRAPH)
+ )
+ return null;
+
+ const min_length = (
+ 'min_length' in component && typeof component.min_length === 'number' &&
+ component.min_length >= 0 && component.min_length <= 4000
+ ) ? component.min_length : null;
+
+ const max_length = (
+ 'max_length' in component && typeof component.max_length === 'number' &&
+ component.max_length >= 1 && component.max_length <= 4000
+ ) ? component.max_length : null;
+
+ const required = ('required' in component) ? !!component.required : true;
+ const value = ('value' in component && typeof component.value === 'string') ? component.value : null;
+ const placeholder = ('placeholder' in component && typeof component.placeholder === 'string') ? component.placeholder : null;
+
+ return {
+ type: ComponentType.TEXT_INPUT,
+ custom_id: component.custom_id,
+ style: component.style,
+ min_length,
+ max_length,
+ required,
+ value,
+ placeholder
+ }
+}
+
+
+export interface LabelComponent extends Component {
+ type: ComponentType.LABEL;
+ label: string;
+ description?: string | null;
+ component: T;
+}
+
+function parseLabelComponent(component: Component): LabelComponent | null {
+ if (!('label' in component) || typeof component.label !== 'string') return null;
+ if (!('component' in component)) return null;
+ const child = parseBaseComponent(component.component);
+ if (child === null) return null;
+
+ let childParsed: LabelPossible | null = null;
+
+ switch (child.type) {
+ case ComponentType.TEXT_INPUT:
+ case ComponentType.STRING_SELECT:
+ case ComponentType.FILE_UPLOAD:
+ const func = parseModalComponent[child.type];
+ if (typeof func !== 'undefined') childParsed = func(child);
+ break;
+ }
+
+ if (childParsed === null) return null;
+
+ const description =
+ 'description' in component && typeof component.description === 'string' ? component.description : null;
+
+ return {
+ type: ComponentType.LABEL,
+ label: component.label,
+ description,
+ component: childParsed,
+ };
+}
+
+export interface FileUploadComponent extends Component {
+ type: ComponentType.FILE_UPLOAD;
+ custom_id: string;
+ min_values?: number;
+ max_values?: number;
+ required?: boolean;
+}
+
+function parseFileUploadComponent(component: Component): FileUploadComponent | null {
+ if (!('custom_id' in component) || typeof component.custom_id !== 'string') return null;
+
+ const min_values = 'min_values' in component && typeof component.min_values === 'number' ? component.min_values : 1;
+ const max_values = 'max_values' in component && typeof component.max_values === 'number' ? component.max_values : 1;
+ const required = 'required' in component ? !!component.required : true;
+
+ return {
+ type: ComponentType.FILE_UPLOAD,
+ custom_id: component.custom_id,
+ min_values,
+ max_values,
+ required,
+ };
+}
+
+export type ModalStringSelectComponent = Omit & {required?: boolean};
+
+function parseStringSelectComponent(component: Component): ModalStringSelectComponent | null {
+ const comp = parseComponent[ComponentType.STRING_SELECT](component);
+ if (comp === null) return null;
+
+ const { disabled, ...base } = comp;
+ const required = ('required' in component) ? !!component.required : true;
+ return {
+ ...base,
+ required,
+ }
+}
+
+function parseTextDisplayComponent(component: Component): TextDisplayComponent | null {
+ return parseComponent[ComponentType.TEXT_DISPLAY](component);
+}
\ No newline at end of file
diff --git a/components-sdk/src/utils/useStateOpen.ts b/components-sdk/src/utils/useStateOpen.ts
index 8eefe7f..16b76cb 100644
--- a/components-sdk/src/utils/useStateOpen.ts
+++ b/components-sdk/src/utils/useStateOpen.ts
@@ -17,6 +17,9 @@ export function useStateOpen(defaultValue: T): {
}, [ignoreRef.current]);
const documentKeyDown = useCallback((ev: KeyboardEvent) => {
+ // @ts-ignore
+ if (closeLockRef.current == 'SHIFT' && ev.key == 'Enter' && !ev.shiftKey) return;
+
if (ev.key == "Enter" || ev.key == "Escape") {
setOpen(defaultValue);
}
diff --git a/website/libs.config.ts b/website/libs.config.ts
index 4fde2b3..70911b3 100644
--- a/website/libs.config.ts
+++ b/website/libs.config.ts
@@ -8,6 +8,12 @@ export const libs: {
path: string;
};
} = {
+ modal: {
+ name: 'JSON: Modal Format',
+ language: 'json',
+ path: '/modal'
+ },
+
dpp: {
name: 'C++: DPP',
language: 'cpp',
diff --git a/website/src/App.tsx b/website/src/App.tsx
index e867979..7c1c17a 100644
--- a/website/src/App.tsx
+++ b/website/src/App.tsx
@@ -1,7 +1,7 @@
-import { Capsule, PassProps } from 'components-sdk';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Capsule, PassProps, RenderMode } from 'components-sdk';
+import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { actions, DisplaySliceManager, RootState } from './state';
+import { actions, displaySlice, DisplaySliceManager, RootState } from './state';
import { BetterInput } from './BetterInput';
import { EmojiPicker } from './EmojiPicker';
import { EmojiShow } from './EmojiShow';
@@ -15,31 +15,19 @@ import { useRouter } from './useRouter';
import { Trans, useTranslation } from 'react-i18next';
import i18next from 'i18next';
import { supportedLngs } from '../libs.config';
-
+import defaultJson from './defaultJson';
+import defaultJsonModal from './defaultJsonModal';
+import { Webhook } from './Webhook';
webhookImplementation.init();
-function getThreadId(webhookUrl: string) {
- try {
- const parsed_url = new URL(webhookUrl);
- const parsed_query = new URLSearchParams(parsed_url.search);
- const thread_id = parsed_query.get('thread_id');
- return thread_id || null;
- } catch (e) {
- return null;
- }
-}
-
function App() {
const dispatch = useDispatch();
const stateManager = useMemo(() => new DisplaySliceManager(dispatch), [dispatch]);
const state = useSelector((state: RootState) => state.display.data)
- const webhookUrl = useSelector((state: RootState) => state.display.webhookUrl);
const response = useSelector((state: RootState) => state.display.webhookResponse);
- const showThread = useSelector((state: RootState) => state.display.showThread);
const isDefault = useSelector((state: RootState) => state.display.isDefault);
const [page, setPage] = useRouter();
- const [postTitle, setPostTitle] = useState("");
useHashRouter();
const setFile = useCallback(webhookImplementation.setFile, []);
@@ -55,69 +43,16 @@ function App() {
// ActionMenu,
EmojiShow,
interactiveDisabled: false,
- }), []);
-
+ renderMode: page === 'modal' ? RenderMode.MODAL : RenderMode.MESSAGE
+ }), [page]);
useEffect(() => {
- const getData = setTimeout(() => localStorage.setItem("discord.builders__webhookToken", webhookUrl), 1000)
- return () => clearTimeout(getData)
- }, [webhookUrl]);
-
-
- let parsed_url: URL | null = null;
- try {
- parsed_url = new URL(webhookUrl);
-
- if (parsed_url.pathname.startsWith('/api/webhooks/') && parsed_url.hostname === 'discord.com') {
- parsed_url.protocol = 'https:';
- parsed_url.pathname = '/api/v10/webhooks/' + parsed_url.pathname.slice('/api/webhooks/'.length);
- }
-
- const parsed_query = new URLSearchParams(parsed_url.search);
- parsed_query.set('with_components', 'true');
- parsed_url.search = parsed_query.toString();
- } catch (e) {}
+ if (isDefault) dispatch(displaySlice.actions.overwriteDefaultState(page === 'modal' ? defaultJsonModal : defaultJson))
+ }, [isDefault, page]);
const stateKey = useMemo(() => ['data'], [])
const errors = useMemo(() => webhookImplementation.getErrors(response), [response]);
- const threadId = useMemo(() => getThreadId(webhookUrl), [webhookUrl]);
- useEffect(() => {
- if (threadId) dispatch(actions.setShowThread())
- }, [threadId]);
-
- const sendMessage = async () => {
- const req = await fetch(String(parsed_url), webhookImplementation.prepareRequest(state))
-
- const status_code = req.status;
- if (status_code === 204) return dispatch(actions.setWebhookResponse({"status": "204 Success"}));
-
- const error_data = await req.json();
-
- if (error_data?.code === 220001 && dialog.current !== null) {
- dialog.current.showModal();
- dispatch(actions.setWebhookResponse(null))
- return;
- }
-
- dispatch(actions.setWebhookResponse(error_data))
- }
-
- const sendMessageWithTitle = async () => {
- if (!postTitle) return;
- dialog.current?.close();
-
- const req = await fetch(String(parsed_url), webhookImplementation.prepareRequest(state, postTitle))
-
- const status_code = req.status;
- if (status_code === 204) return dispatch(actions.setWebhookResponse({"status": "204 Success"}));
-
- const error_data = await req.json();
- dispatch(actions.setWebhookResponse(error_data))
- }
-
- const dialog = useRef(null);
-
if (page === '404.not-found') {
if (!window.location.href.includes('/not-found')) window.location.href = '/not-found';
return 404 — Page not found
;
@@ -139,7 +74,7 @@ function App() {
dispatch(actions.setKey({key: ['data'], value: []}));
}}>{t('welcome.clear')}
}
- {(isDefault && page !== '200.home') &&
+ {(isDefault && page !== '200.home' && page !== 'modal') &&
{t('welcome.welcome')}
,
@@ -147,7 +82,7 @@ function App() {
+ }}>{t('welcome.clear')}
}
>}>
@@ -169,39 +105,7 @@ function App() {
src="https://img.shields.io/github/commit-activity/t/StartITBot/discord.builders?style=for-the-badge&color=248045" />
-
{t('webhook.title')}{!showThread && <> ( dispatch(actions.setShowThread())}>{t('webhook.thread')}) >}
-
-
- dispatch(actions.setWebhookUrl(ev.target.value))}/>
-
-
-
-
-
{t('webhook.warning')}
-
- {showThread &&
-
{t('thread.id')}
-
dispatch(actions.setThreadId(ev.target.value))} placeholder={t('thread.placeholder')}/>
-
}
-
-
-
- {!!response &&
{JSON.stringify(response, undefined, 4)}
}
-
+ {page !== 'modal' &&
}
diff --git a/website/src/Codegen.tsx b/website/src/Codegen.tsx
index e0ae4c6..78a6ece 100644
--- a/website/src/Codegen.tsx
+++ b/website/src/Codegen.tsx
@@ -2,10 +2,8 @@ import { CodeBlock, dracula } from 'react-code-blocks';
import Styles from './App.module.css';
import Select, { Props } from 'react-select';
import { select_styles } from './Select';
-import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { libs } from '../libs.config';
import { ClientFunction, IncludeCallback } from 'ejs';
-import { RootState } from './state';
import { OnChangeValue } from 'react-select/dist/declarations/src/types';
import { Component } from 'components-sdk';
import { useTranslation } from 'react-i18next';
@@ -51,6 +49,11 @@ export function Codegen({state, page, setPage} : {
label: 'JSON',
value: 'json',
},
+
+ {
+ label: 'JSON for modals [ALPHA]',
+ value: 'modal',
+ },
...Object.keys(libComponents).map((comp) => ({
label: libs[comp]?.name || comp,
value: comp,
@@ -70,7 +73,7 @@ export function Codegen({state, page, setPage} : {
return (
<>
-
{t('codegen.title')}
+
{t('codegen.title')}