From b6b6f6004cbcd768bf3a2abe5caf096f08ce7245 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:27:06 +0530 Subject: [PATCH 1/3] refactor TextViewer Content component --- .../components/text-viewer/TextViewer.css.ts | 5 ++- src/app/components/text-viewer/TextViewer.tsx | 38 +++++++++++++++---- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/app/components/text-viewer/TextViewer.css.ts b/src/app/components/text-viewer/TextViewer.css.ts index 2b79fa64af..83ee6058bd 100644 --- a/src/app/components/text-viewer/TextViewer.css.ts +++ b/src/app/components/text-viewer/TextViewer.css.ts @@ -31,8 +31,11 @@ export const TextViewerContent = style([ export const TextViewerPre = style([ DefaultReset, { - padding: config.space.S600, whiteSpace: 'pre-wrap', wordBreak: 'break-word', }, ]); + +export const TextViewerPrePadding = style({ + padding: config.space.S600, +}); diff --git a/src/app/components/text-viewer/TextViewer.tsx b/src/app/components/text-viewer/TextViewer.tsx index 7829fb35b3..f39ef9535d 100644 --- a/src/app/components/text-viewer/TextViewer.tsx +++ b/src/app/components/text-viewer/TextViewer.tsx @@ -1,5 +1,5 @@ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ -import React, { Suspense, lazy } from 'react'; +import React, { ComponentProps, HTMLAttributes, Suspense, forwardRef, lazy } from 'react'; import classNames from 'classnames'; import { Box, Chip, Header, Icon, IconButton, Icons, Scroll, Text, as } from 'folds'; import { ErrorBoundary } from 'react-error-boundary'; @@ -8,6 +8,29 @@ import { copyToClipboard } from '../../utils/dom'; const ReactPrism = lazy(() => import('../../plugins/react-prism/ReactPrism')); +type TextViewerContentProps = { + text: string; + langName: string; + size?: ComponentProps['size']; +} & HTMLAttributes; +export const TextViewerContent = forwardRef( + ({ text, langName, size, className, ...props }, ref) => ( + + {text}}> + {text}}> + {(codeRef) => {text}} + + + + ) +); + export type TextViewerProps = { name: string; text: string; @@ -43,6 +66,7 @@ export const TextViewer = as<'div', TextViewerProps>( + ( alignItems="Center" > - - {text}}> - {text}}> - {(codeRef) => {text}} - - - + From 0af2bce87135378db8363b801330e99b4cfbe3eb Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:27:25 +0530 Subject: [PATCH 2/3] open account data inside setting window --- .../settings/developer-tools/AccountData.tsx | 90 ++++ .../developer-tools/AccountDataEditor.tsx | 447 +++++++++++------- .../settings/developer-tools/DevelopTools.tsx | 232 +-------- 3 files changed, 393 insertions(+), 376 deletions(-) create mode 100644 src/app/features/settings/developer-tools/AccountData.tsx diff --git a/src/app/features/settings/developer-tools/AccountData.tsx b/src/app/features/settings/developer-tools/AccountData.tsx new file mode 100644 index 0000000000..743c28b7c6 --- /dev/null +++ b/src/app/features/settings/developer-tools/AccountData.tsx @@ -0,0 +1,90 @@ +import React, { useCallback, useState } from 'react'; +import { Box, Text, Icon, Icons, Chip, Button } from 'folds'; +import { SequenceCard } from '../../../components/sequence-card'; +import { SequenceCardStyle } from '../styles.css'; +import { SettingTile } from '../../../components/setting-tile'; +import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { useAccountDataCallback } from '../../../hooks/useAccountDataCallback'; + +type AccountDataProps = { + expand: boolean; + onExpandToggle: (expand: boolean) => void; + onSelect: (type: string | null) => void; +}; +export function AccountData({ expand, onExpandToggle, onSelect }: AccountDataProps) { + const mx = useMatrixClient(); + const [accountData, setAccountData] = useState(() => Array.from(mx.store.accountData.values())); + + useAccountDataCallback( + mx, + useCallback( + () => setAccountData(Array.from(mx.store.accountData.values())), + [mx, setAccountData] + ) + ); + + return ( + + Account Data + + onExpandToggle(!expand)} + variant="Secondary" + fill="Soft" + size="300" + radii="300" + outlined + before={ + + } + > + {expand ? 'Collapse' : 'Expand'} + + } + /> + {expand && ( + + + Types + + } + onClick={() => onSelect(null)} + > + + Add New + + + {accountData.map((mEvent) => ( + onSelect(mEvent.getType())} + > + + {mEvent.getType()} + + + ))} + + + + )} + + + ); +} diff --git a/src/app/features/settings/developer-tools/AccountDataEditor.tsx b/src/app/features/settings/developer-tools/AccountDataEditor.tsx index 52e9870ecc..4a87562190 100644 --- a/src/app/features/settings/developer-tools/AccountDataEditor.tsx +++ b/src/app/features/settings/developer-tools/AccountDataEditor.tsx @@ -8,9 +8,7 @@ import React, { useState, } from 'react'; import { - as, Box, - Header, Text, Icon, Icons, @@ -20,6 +18,9 @@ import { TextArea as TextAreaComponent, color, Spinner, + Chip, + Scroll, + config, } from 'folds'; import { isKeyHotkey } from 'is-hotkey'; import { MatrixError } from 'matrix-js-sdk'; @@ -30,182 +31,298 @@ import { GetTarget } from '../../../plugins/text-area/type'; import { syntaxErrorPosition } from '../../../utils/dom'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { Page, PageHeader } from '../../../components/page'; +import { useAlive } from '../../../hooks/useAlive'; +import { SequenceCard } from '../../../components/sequence-card'; +import { TextViewerContent } from '../../../components/text-viewer'; const EDITOR_INTENT_SPACE_COUNT = 2; -export type AccountDataEditorProps = { - type?: string; - content?: object; - requestClose: () => void; +type AccountDataInfo = { + type: string; + content: object; }; -export const AccountDataEditor = as<'div', AccountDataEditorProps>( - ({ type, content, requestClose, ...props }, ref) => { - const mx = useMatrixClient(); - const defaultContent = useMemo( - () => JSON.stringify(content, null, EDITOR_INTENT_SPACE_COUNT), - [content] - ); - const textAreaRef = useRef(null); - const [jsonError, setJSONError] = useState(); - - const getTarget: GetTarget = useCallback(() => { - const target = textAreaRef.current; - if (!target) throw new Error('TextArea element not found!'); - return target; - }, []); - - const { textArea, operations, intent } = useMemo(() => { - const ta = new TextArea(getTarget); - const op = new TextAreaOperations(getTarget); - return { - textArea: ta, - operations: op, - intent: new Intent(EDITOR_INTENT_SPACE_COUNT, ta, op), - }; - }, [getTarget]); - - const intentHandler = useTextAreaIntentHandler(textArea, operations, intent); - - const handleKeyDown: KeyboardEventHandler = (evt) => { - intentHandler(evt); - if (isKeyHotkey('escape', evt)) { - const cursor = Cursor.fromTextAreaElement(getTarget()); - operations.deselect(cursor); - } - }; +type AccountDataEditProps = { + type: string; + defaultContent: string; + onCancel: () => void; + onSave: (info: AccountDataInfo) => void; +}; +function AccountDataEdit({ type, defaultContent, onCancel, onSave }: AccountDataEditProps) { + const mx = useMatrixClient(); + const alive = useAlive(); - const [submitState, submit] = useAsyncCallback( - useCallback((dataType, data) => mx.setAccountData(dataType, data), [mx]) - ); - const submitting = submitState.status === AsyncStatus.Loading; - - const handleSubmit: FormEventHandler = (evt) => { - evt.preventDefault(); - if (submitting) return; - - const target = evt.target as HTMLFormElement | undefined; - const typeInput = target?.typeInput as HTMLInputElement | undefined; - const contentTextArea = target?.contentTextArea as HTMLTextAreaElement | undefined; - if (!typeInput || !contentTextArea) return; - - const typeStr = typeInput.value.trim(); - const contentStr = contentTextArea.value.trim(); - - let parsedContent: object; - try { - parsedContent = JSON.parse(contentStr); - } catch (e) { - setJSONError(e as SyntaxError); - return; - } - setJSONError(undefined); - - if ( - !typeStr || - parsedContent === null || - defaultContent === JSON.stringify(parsedContent, null, EDITOR_INTENT_SPACE_COUNT) - ) { - return; - } + const textAreaRef = useRef(null); + const [jsonError, setJSONError] = useState(); + + const getTarget: GetTarget = useCallback(() => { + const target = textAreaRef.current; + if (!target) throw new Error('TextArea element not found!'); + return target; + }, []); - submit(typeStr, parsedContent); + const { textArea, operations, intent } = useMemo(() => { + const ta = new TextArea(getTarget); + const op = new TextAreaOperations(getTarget); + return { + textArea: ta, + operations: op, + intent: new Intent(EDITOR_INTENT_SPACE_COUNT, ta, op), }; + }, [getTarget]); - useEffect(() => { - if (jsonError) { - const errorPosition = syntaxErrorPosition(jsonError) ?? 0; - const cursor = new Cursor(errorPosition, errorPosition, 'none'); - operations.select(cursor); - getTarget()?.focus(); - } - }, [jsonError, operations, getTarget]); + const intentHandler = useTextAreaIntentHandler(textArea, operations, intent); + + const handleKeyDown: KeyboardEventHandler = (evt) => { + intentHandler(evt); + if (isKeyHotkey('escape', evt)) { + const cursor = Cursor.fromTextAreaElement(getTarget()); + operations.deselect(cursor); + } + }; + + const [submitState, submit] = useAsyncCallback( + useCallback((dataType, data) => mx.setAccountData(dataType, data), [mx]) + ); + const submitting = submitState.status === AsyncStatus.Loading; - useEffect(() => { - if (submitState.status === AsyncStatus.Success) { - requestClose(); + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + if (submitting) return; + + const target = evt.target as HTMLFormElement | undefined; + const typeInput = target?.typeInput as HTMLInputElement | undefined; + const contentTextArea = target?.contentTextArea as HTMLTextAreaElement | undefined; + if (!typeInput || !contentTextArea) return; + + const typeStr = typeInput.value.trim(); + const contentStr = contentTextArea.value.trim(); + + let parsedContent: object; + try { + parsedContent = JSON.parse(contentStr); + } catch (e) { + setJSONError(e as SyntaxError); + return; + } + setJSONError(undefined); + + if ( + !typeStr || + parsedContent === null || + defaultContent === JSON.stringify(parsedContent, null, EDITOR_INTENT_SPACE_COUNT) + ) { + return; + } + + submit(typeStr, parsedContent).then(() => { + if (alive()) { + onSave({ + type: typeStr, + content: parsedContent, + }); } - }, [submitState, requestClose]); - - return ( - -
- - - - Account Data - - - - - - - - -
- - - Type - - - - - - - - {submitState.status === AsyncStatus.Error && ( - - {submitState.error.message} - - )} - - - - JSON Content - - { + if (jsonError) { + const errorPosition = syntaxErrorPosition(jsonError) ?? 0; + const cursor = new Cursor(errorPosition, errorPosition, 'none'); + operations.select(cursor); + getTarget()?.focus(); + } + }, [jsonError, operations, getTarget]); + + return ( + + + Account Data + + + 0 || submitting ? 'SurfaceVariant' : 'Background'} + name="typeInput" + size="400" + radii="300" + readOnly={type.length > 0 || submitting} + defaultValue={type} required - readOnly={submitting} /> - {jsonError && ( - - - {jsonError.name}: {jsonError.message} - - - )} + + + + + + {submitState.status === AsyncStatus.Error && ( + + {submitState.error.message} + + )} + + + + JSON Content + + + {jsonError && ( + + + {jsonError.name}: {jsonError.message} + + + )} + + + ); +} + +type AccountDataViewProps = { + type: string; + defaultContent: string; + onEdit: () => void; +}; +function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps) { + return ( + + + + Account Data + + + + + + JSON Content + + + + + + + + ); +} + +export type AccountDataEditorProps = { + type?: string; + requestClose: () => void; +}; + +export function AccountDataEditor({ type, requestClose }: AccountDataEditorProps) { + const mx = useMatrixClient(); + + const [data, setData] = useState({ + type: type ?? '', + content: mx.getAccountData(type ?? '')?.getContent() ?? {}, + }); + + const [edit, setEdit] = useState(!type); + + const closeEdit = useCallback(() => { + setEdit(false); + }, []); + + const handleSave = useCallback((info: AccountDataInfo) => { + setData(info); + setEdit(false); + }, []); + + const contentJSONStr = useMemo( + () => JSON.stringify(data.content, null, EDITOR_INTENT_SPACE_COUNT), + [data.content] + ); + + return ( + + + + + } + > + Developer Tools + + + + + + + + + {edit ? ( + + ) : ( + setEdit(true)} + /> + )} - ); - } -); + + ); +} diff --git a/src/app/features/settings/developer-tools/DevelopTools.tsx b/src/app/features/settings/developer-tools/DevelopTools.tsx index 081a26e3b8..b66452f50b 100644 --- a/src/app/features/settings/developer-tools/DevelopTools.tsx +++ b/src/app/features/settings/developer-tools/DevelopTools.tsx @@ -1,26 +1,5 @@ -import React, { MouseEventHandler, useCallback, useState } from 'react'; -import { - Box, - Text, - IconButton, - Icon, - Icons, - Scroll, - Switch, - Overlay, - OverlayBackdrop, - OverlayCenter, - Modal, - Chip, - Button, - PopOut, - RectCords, - Menu, - config, - MenuItem, -} from 'folds'; -import { MatrixEvent } from 'matrix-js-sdk'; -import FocusTrap from 'focus-trap-react'; +import React, { useState } from 'react'; +import { Box, Text, IconButton, Icon, Icons, Scroll, Switch, Button } from 'folds'; import { Page, PageContent, PageHeader } from '../../../components/page'; import { SequenceCard } from '../../../components/sequence-card'; import { SequenceCardStyle } from '../styles.css'; @@ -28,195 +7,9 @@ import { SettingTile } from '../../../components/setting-tile'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; -import { useAccountDataCallback } from '../../../hooks/useAccountDataCallback'; -import { TextViewer } from '../../../components/text-viewer'; -import { stopPropagation } from '../../../utils/keyboard'; import { AccountDataEditor } from './AccountDataEditor'; import { copyToClipboard } from '../../../utils/dom'; - -function AccountData() { - const mx = useMatrixClient(); - const [view, setView] = useState(false); - const [accountData, setAccountData] = useState(() => Array.from(mx.store.accountData.values())); - const [selectedEvent, selectEvent] = useState(); - const [menuCords, setMenuCords] = useState(); - const [selectedOption, selectOption] = useState<'edit' | 'inspect'>(); - - useAccountDataCallback( - mx, - useCallback( - () => setAccountData(Array.from(mx.store.accountData.values())), - [mx, setAccountData] - ) - ); - - const handleMenu: MouseEventHandler = (evt) => { - const target = evt.currentTarget; - const eventType = target.getAttribute('data-event-type'); - if (eventType) { - const mEvent = accountData.find((mEvt) => mEvt.getType() === eventType); - setMenuCords(evt.currentTarget.getBoundingClientRect()); - selectEvent(mEvent); - } - }; - - const handleMenuClose = () => setMenuCords(undefined); - - const handleEdit = () => { - selectOption('edit'); - setMenuCords(undefined); - }; - const handleInspect = () => { - selectOption('inspect'); - setMenuCords(undefined); - }; - const handleClose = useCallback(() => { - selectEvent(undefined); - selectOption(undefined); - }, []); - - return ( - - Account Data - - setView(!view)} - variant="Secondary" - fill="Soft" - size="300" - radii="300" - outlined - before={ - - } - > - {view ? 'Collapse' : 'Expand'} - - } - /> - {view && ( - - - Types - - } - > - - Add New - - - {accountData.map((mEvent) => ( - - - {mEvent.getType()} - - - ))} - - - - )} - - evt.key === 'ArrowDown' || evt.key === 'ArrowRight', - isKeyBackward: (evt: KeyboardEvent) => - evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', - escapeDeactivates: stopPropagation, - }} - > - - - - Inspect - - - Edit - - - - - } - /> - - {selectedEvent && selectedOption === 'inspect' && ( - }> - - - - - - - - - )} - {selectedOption === 'edit' && ( - }> - - - - - - - - - )} - - ); -} +import { AccountData } from './AccountData'; type DeveloperToolsProps = { requestClose: () => void; @@ -224,6 +17,17 @@ type DeveloperToolsProps = { export function DeveloperTools({ requestClose }: DeveloperToolsProps) { const mx = useMatrixClient(); const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools'); + const [expand, setExpend] = useState(false); + const [accountDataType, setAccountDataType] = useState(); + + if (accountDataType !== undefined) { + return ( + setAccountDataType(undefined)} + /> + ); + } return ( @@ -292,7 +96,13 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) { )} - {developerTools && } + {developerTools && ( + + )} From c10b138876f5e7035f6ab36a748dd206e1bc94ed Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:30:09 +0530 Subject: [PATCH 3/3] close account data edit window on cancel when adding new --- .../features/settings/developer-tools/AccountDataEditor.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/features/settings/developer-tools/AccountDataEditor.tsx b/src/app/features/settings/developer-tools/AccountDataEditor.tsx index 4a87562190..b5ac0f8aa2 100644 --- a/src/app/features/settings/developer-tools/AccountDataEditor.tsx +++ b/src/app/features/settings/developer-tools/AccountDataEditor.tsx @@ -273,8 +273,12 @@ export function AccountDataEditor({ type, requestClose }: AccountDataEditorProps const [edit, setEdit] = useState(!type); const closeEdit = useCallback(() => { + if (!type) { + requestClose(); + return; + } setEdit(false); - }, []); + }, [type, requestClose]); const handleSave = useCallback((info: AccountDataInfo) => { setData(info);