open account data in same window instead of popup (#2234)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Thu, 27 Feb 2025 08:34:55 +0000 (19:34 +1100)
committerGitHub <noreply@github.com>
Thu, 27 Feb 2025 08:34:55 +0000 (19:34 +1100)
* refactor TextViewer Content component

* open account data inside setting window

* close account data edit window on cancel when adding new

src/app/components/text-viewer/TextViewer.css.ts
src/app/components/text-viewer/TextViewer.tsx
src/app/features/settings/developer-tools/AccountData.tsx [new file with mode: 0644]
src/app/features/settings/developer-tools/AccountDataEditor.tsx
src/app/features/settings/developer-tools/DevelopTools.tsx

index 2b79fa64afb99fa866b334d3d461a516acbf7c28..83ee6058bdc127e3827afbb7e9168a2311cea8d8 100644 (file)
@@ -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,
+});
index 7829fb35b34c4b1a2e65195760ccb7d9c42db664..f39ef9535db8a87fd47eb1795bbd978c857e476b 100644 (file)
@@ -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<typeof Text>['size'];
+} & HTMLAttributes<HTMLPreElement>;
+export const TextViewerContent = forwardRef<HTMLPreElement, TextViewerContentProps>(
+  ({ text, langName, size, className, ...props }, ref) => (
+    <Text
+      as="pre"
+      size={size}
+      className={classNames(css.TextViewerPre, `language-${langName}`, className)}
+      {...props}
+      ref={ref}
+    >
+      <ErrorBoundary fallback={<code>{text}</code>}>
+        <Suspense fallback={<code>{text}</code>}>
+          <ReactPrism>{(codeRef) => <code ref={codeRef}>{text}</code>}</ReactPrism>
+        </Suspense>
+      </ErrorBoundary>
+    </Text>
+  )
+);
+
 export type TextViewerProps = {
   name: string;
   text: string;
@@ -43,6 +66,7 @@ export const TextViewer = as<'div', TextViewerProps>(
             </Chip>
           </Box>
         </Header>
+
         <Box
           grow="Yes"
           className={css.TextViewerContent}
@@ -50,13 +74,11 @@ export const TextViewer = as<'div', TextViewerProps>(
           alignItems="Center"
         >
           <Scroll hideTrack variant="Background" visibility="Hover">
-            <Text as="pre" className={classNames(css.TextViewerPre, `language-${langName}`)}>
-              <ErrorBoundary fallback={<code>{text}</code>}>
-                <Suspense fallback={<code>{text}</code>}>
-                  <ReactPrism>{(codeRef) => <code ref={codeRef}>{text}</code>}</ReactPrism>
-                </Suspense>
-              </ErrorBoundary>
-            </Text>
+            <TextViewerContent
+              className={css.TextViewerPrePadding}
+              text={text}
+              langName={langName}
+            />
           </Scroll>
         </Box>
       </Box>
diff --git a/src/app/features/settings/developer-tools/AccountData.tsx b/src/app/features/settings/developer-tools/AccountData.tsx
new file mode 100644 (file)
index 0000000..743c28b
--- /dev/null
@@ -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 (
+    <Box direction="Column" gap="100">
+      <Text size="L400">Account Data</Text>
+      <SequenceCard
+        className={SequenceCardStyle}
+        variant="SurfaceVariant"
+        direction="Column"
+        gap="400"
+      >
+        <SettingTile
+          title="Global"
+          description="Data stored in your global account data."
+          after={
+            <Button
+              onClick={() => onExpandToggle(!expand)}
+              variant="Secondary"
+              fill="Soft"
+              size="300"
+              radii="300"
+              outlined
+              before={
+                <Icon src={expand ? Icons.ChevronTop : Icons.ChevronBottom} size="100" filled />
+              }
+            >
+              <Text size="B300">{expand ? 'Collapse' : 'Expand'}</Text>
+            </Button>
+          }
+        />
+        {expand && (
+          <SettingTile>
+            <Box direction="Column" gap="200">
+              <Text size="L400">Types</Text>
+              <Box gap="200" wrap="Wrap">
+                <Chip
+                  variant="Secondary"
+                  fill="Soft"
+                  radii="Pill"
+                  before={<Icon size="50" src={Icons.Plus} />}
+                  onClick={() => onSelect(null)}
+                >
+                  <Text size="T200" truncate>
+                    Add New
+                  </Text>
+                </Chip>
+                {accountData.map((mEvent) => (
+                  <Chip
+                    key={mEvent.getType()}
+                    variant="Secondary"
+                    fill="Soft"
+                    radii="Pill"
+                    onClick={() => onSelect(mEvent.getType())}
+                  >
+                    <Text size="T200" truncate>
+                      {mEvent.getType()}
+                    </Text>
+                  </Chip>
+                ))}
+              </Box>
+            </Box>
+          </SettingTile>
+        )}
+      </SequenceCard>
+    </Box>
+  );
+}
index 52e9870ecc345ec6ffac8b6c9970b1275e51c16c..b5ac0f8aa25dbaa0ef569538a850857d32318973 100644 (file)
@@ -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,302 @@ 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<HTMLTextAreaElement>(null);
-    const [jsonError, setJSONError] = useState<SyntaxError>();
-
-    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<HTMLTextAreaElement> = (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<object, MatrixError, [string, object]>(
-      useCallback((dataType, data) => mx.setAccountData(dataType, data), [mx])
-    );
-    const submitting = submitState.status === AsyncStatus.Loading;
-
-    const handleSubmit: FormEventHandler<HTMLFormElement> = (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<HTMLTextAreaElement>(null);
+  const [jsonError, setJSONError] = useState<SyntaxError>();
+
+  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<HTMLTextAreaElement> = (evt) => {
+    intentHandler(evt);
+    if (isKeyHotkey('escape', evt)) {
+      const cursor = Cursor.fromTextAreaElement(getTarget());
+      operations.deselect(cursor);
+    }
+  };
+
+  const [submitState, submit] = useAsyncCallback<object, MatrixError, [string, object]>(
+    useCallback((dataType, data) => mx.setAccountData(dataType, data), [mx])
+  );
+  const submitting = submitState.status === AsyncStatus.Loading;
 
-    useEffect(() => {
-      if (submitState.status === AsyncStatus.Success) {
-        requestClose();
+  const handleSubmit: FormEventHandler<HTMLFormElement> = (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 (
-      <Box grow="Yes" direction="Column" {...props} ref={ref}>
-        <Header className={css.EditorHeader} size="600">
-          <Box grow="Yes" gap="200">
-            <Box grow="Yes" alignItems="Center" gap="200">
-              <Text size="H3" truncate>
-                Account Data
-              </Text>
-            </Box>
-            <Box shrink="No">
-              <IconButton onClick={requestClose} variant="Surface">
-                <Icon src={Icons.Cross} />
-              </IconButton>
-            </Box>
-          </Box>
-        </Header>
-        <Box
-          as="form"
-          onSubmit={handleSubmit}
-          grow="Yes"
-          className={css.EditorContent}
-          direction="Column"
-          gap="400"
-          aria-disabled={submitting}
-        >
-          <Box shrink="No" direction="Column" gap="100">
-            <Text size="L400">Type</Text>
-            <Box gap="300">
-              <Box grow="Yes" direction="Column">
-                <Input
-                  name="typeInput"
-                  size="400"
-                  readOnly={!!type || submitting}
-                  defaultValue={type}
-                  required
-                />
-              </Box>
-              <Button
-                variant="Primary"
-                size="400"
-                type="submit"
-                disabled={submitting}
-                before={submitting && <Spinner variant="Primary" fill="Solid" size="300" />}
-              >
-                <Text size="B400">Save</Text>
-              </Button>
-            </Box>
-
-            {submitState.status === AsyncStatus.Error && (
-              <Text size="T200" style={{ color: color.Critical.Main }}>
-                <b>{submitState.error.message}</b>
-              </Text>
-            )}
-          </Box>
-          <Box grow="Yes" direction="Column" gap="100">
-            <Box shrink="No">
-              <Text size="L400">JSON Content</Text>
-            </Box>
-            <TextAreaComponent
-              ref={textAreaRef}
-              name="contentTextArea"
-              className={css.EditorTextArea}
-              onKeyDown={handleKeyDown}
-              defaultValue={defaultContent}
-              resize="None"
-              spellCheck="false"
+    });
+  };
+
+  useEffect(() => {
+    if (jsonError) {
+      const errorPosition = syntaxErrorPosition(jsonError) ?? 0;
+      const cursor = new Cursor(errorPosition, errorPosition, 'none');
+      operations.select(cursor);
+      getTarget()?.focus();
+    }
+  }, [jsonError, operations, getTarget]);
+
+  return (
+    <Box
+      as="form"
+      onSubmit={handleSubmit}
+      grow="Yes"
+      className={css.EditorContent}
+      direction="Column"
+      gap="400"
+      aria-disabled={submitting}
+    >
+      <Box shrink="No" direction="Column" gap="100">
+        <Text size="L400">Account Data</Text>
+        <Box gap="300">
+          <Box grow="Yes" direction="Column">
+            <Input
+              variant={type.length > 0 || submitting ? 'SurfaceVariant' : 'Background'}
+              name="typeInput"
+              size="400"
+              radii="300"
+              readOnly={type.length > 0 || submitting}
+              defaultValue={type}
               required
-              readOnly={submitting}
             />
-            {jsonError && (
-              <Text size="T200" style={{ color: color.Critical.Main }}>
-                <b>
-                  {jsonError.name}: {jsonError.message}
-                </b>
-              </Text>
-            )}
+          </Box>
+          <Button
+            variant="Success"
+            size="400"
+            radii="300"
+            type="submit"
+            disabled={submitting}
+            before={submitting && <Spinner variant="Primary" fill="Solid" size="300" />}
+          >
+            <Text size="B400">Save</Text>
+          </Button>
+          <Button
+            variant="Secondary"
+            fill="Soft"
+            size="400"
+            radii="300"
+            onClick={onCancel}
+            disabled={submitting}
+          >
+            <Text size="B400">Cancel</Text>
+          </Button>
+        </Box>
+
+        {submitState.status === AsyncStatus.Error && (
+          <Text size="T200" style={{ color: color.Critical.Main }}>
+            <b>{submitState.error.message}</b>
+          </Text>
+        )}
+      </Box>
+      <Box grow="Yes" direction="Column" gap="100">
+        <Box shrink="No">
+          <Text size="L400">JSON Content</Text>
+        </Box>
+        <TextAreaComponent
+          ref={textAreaRef}
+          name="contentTextArea"
+          className={css.EditorTextArea}
+          onKeyDown={handleKeyDown}
+          defaultValue={defaultContent}
+          resize="None"
+          spellCheck="false"
+          required
+          readOnly={submitting}
+        />
+        {jsonError && (
+          <Text size="T200" style={{ color: color.Critical.Main }}>
+            <b>
+              {jsonError.name}: {jsonError.message}
+            </b>
+          </Text>
+        )}
+      </Box>
+    </Box>
+  );
+}
+
+type AccountDataViewProps = {
+  type: string;
+  defaultContent: string;
+  onEdit: () => void;
+};
+function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps) {
+  return (
+    <Box direction="Column" className={css.EditorContent} gap="400">
+      <Box shrink="No" gap="300" alignItems="End">
+        <Box grow="Yes" direction="Column" gap="100">
+          <Text size="L400">Account Data</Text>
+          <Input
+            variant="SurfaceVariant"
+            size="400"
+            radii="300"
+            readOnly
+            defaultValue={type}
+            required
+          />
+        </Box>
+        <Button variant="Secondary" size="400" radii="300" onClick={onEdit}>
+          <Text size="B400">Edit</Text>
+        </Button>
+      </Box>
+      <Box grow="Yes" direction="Column" gap="100">
+        <Text size="L400">JSON Content</Text>
+        <SequenceCard variant="SurfaceVariant">
+          <Scroll visibility="Always" size="300" hideTrack>
+            <TextViewerContent
+              size="T300"
+              style={{
+                padding: `${config.space.S300} ${config.space.S100} ${config.space.S300} ${config.space.S300}`,
+              }}
+              text={defaultContent}
+              langName="JSON"
+            />
+          </Scroll>
+        </SequenceCard>
+      </Box>
+    </Box>
+  );
+}
+
+export type AccountDataEditorProps = {
+  type?: string;
+  requestClose: () => void;
+};
+
+export function AccountDataEditor({ type, requestClose }: AccountDataEditorProps) {
+  const mx = useMatrixClient();
+
+  const [data, setData] = useState<AccountDataInfo>({
+    type: type ?? '',
+    content: mx.getAccountData(type ?? '')?.getContent() ?? {},
+  });
+
+  const [edit, setEdit] = useState(!type);
+
+  const closeEdit = useCallback(() => {
+    if (!type) {
+      requestClose();
+      return;
+    }
+    setEdit(false);
+  }, [type, requestClose]);
+
+  const handleSave = useCallback((info: AccountDataInfo) => {
+    setData(info);
+    setEdit(false);
+  }, []);
+
+  const contentJSONStr = useMemo(
+    () => JSON.stringify(data.content, null, EDITOR_INTENT_SPACE_COUNT),
+    [data.content]
+  );
+
+  return (
+    <Page>
+      <PageHeader outlined={false} balance>
+        <Box alignItems="Center" grow="Yes" gap="200">
+          <Box alignItems="Inherit" grow="Yes" gap="200">
+            <Chip
+              size="500"
+              radii="Pill"
+              onClick={requestClose}
+              before={<Icon size="100" src={Icons.ArrowLeft} />}
+            >
+              <Text size="T300">Developer Tools</Text>
+            </Chip>
+          </Box>
+          <Box shrink="No">
+            <IconButton onClick={requestClose} variant="Surface">
+              <Icon src={Icons.Cross} />
+            </IconButton>
           </Box>
         </Box>
+      </PageHeader>
+      <Box grow="Yes" direction="Column">
+        {edit ? (
+          <AccountDataEdit
+            type={data.type}
+            defaultContent={contentJSONStr}
+            onCancel={closeEdit}
+            onSave={handleSave}
+          />
+        ) : (
+          <AccountDataView
+            type={data.type}
+            defaultContent={contentJSONStr}
+            onEdit={() => setEdit(true)}
+          />
+        )}
       </Box>
-    );
-  }
-);
+    </Page>
+  );
+}
index 081a26e3b8e5ce2bebd2d4e7f2d32b5927bd5fff..b66452f50bd9cf03468a55ddd277559f6b2b0a0e 100644 (file)
@@ -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<MatrixEvent>();
-  const [menuCords, setMenuCords] = useState<RectCords>();
-  const [selectedOption, selectOption] = useState<'edit' | 'inspect'>();
-
-  useAccountDataCallback(
-    mx,
-    useCallback(
-      () => setAccountData(Array.from(mx.store.accountData.values())),
-      [mx, setAccountData]
-    )
-  );
-
-  const handleMenu: MouseEventHandler<HTMLButtonElement> = (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 (
-    <Box direction="Column" gap="100">
-      <Text size="L400">Account Data</Text>
-      <SequenceCard
-        className={SequenceCardStyle}
-        variant="SurfaceVariant"
-        direction="Column"
-        gap="400"
-      >
-        <SettingTile
-          title="Global"
-          description="Data stored in your global account data."
-          after={
-            <Button
-              onClick={() => setView(!view)}
-              variant="Secondary"
-              fill="Soft"
-              size="300"
-              radii="300"
-              outlined
-              before={
-                <Icon src={view ? Icons.ChevronTop : Icons.ChevronBottom} size="100" filled />
-              }
-            >
-              <Text size="B300">{view ? 'Collapse' : 'Expand'}</Text>
-            </Button>
-          }
-        />
-        {view && (
-          <SettingTile>
-            <Box direction="Column" gap="200">
-              <Text size="L400">Types</Text>
-              <Box gap="200" wrap="Wrap">
-                <Chip
-                  variant="Secondary"
-                  fill="Soft"
-                  radii="Pill"
-                  onClick={handleEdit}
-                  before={<Icon size="50" src={Icons.Plus} />}
-                >
-                  <Text size="T200" truncate>
-                    Add New
-                  </Text>
-                </Chip>
-                {accountData.map((mEvent) => (
-                  <Chip
-                    key={mEvent.getType()}
-                    variant="Secondary"
-                    fill="Soft"
-                    radii="Pill"
-                    aria-pressed={menuCords && selectedEvent?.getType() === mEvent.getType()}
-                    onClick={handleMenu}
-                    data-event-type={mEvent.getType()}
-                  >
-                    <Text size="T200" truncate>
-                      {mEvent.getType()}
-                    </Text>
-                  </Chip>
-                ))}
-              </Box>
-            </Box>
-          </SettingTile>
-        )}
-        <PopOut
-          anchor={menuCords}
-          offset={5}
-          position="Bottom"
-          content={
-            <FocusTrap
-              focusTrapOptions={{
-                initialFocus: false,
-                onDeactivate: handleMenuClose,
-                clickOutsideDeactivates: true,
-                isKeyForward: (evt: KeyboardEvent) =>
-                  evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
-                isKeyBackward: (evt: KeyboardEvent) =>
-                  evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
-                escapeDeactivates: stopPropagation,
-              }}
-            >
-              <Menu>
-                <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
-                  <MenuItem size="300" variant="Surface" radii="300" onClick={handleInspect}>
-                    <Text size="T300">Inspect</Text>
-                  </MenuItem>
-                  <MenuItem size="300" variant="Surface" radii="300" onClick={handleEdit}>
-                    <Text size="T300">Edit</Text>
-                  </MenuItem>
-                </Box>
-              </Menu>
-            </FocusTrap>
-          }
-        />
-      </SequenceCard>
-      {selectedEvent && selectedOption === 'inspect' && (
-        <Overlay open backdrop={<OverlayBackdrop />}>
-          <OverlayCenter>
-            <FocusTrap
-              focusTrapOptions={{
-                initialFocus: false,
-                onDeactivate: handleClose,
-                clickOutsideDeactivates: true,
-                escapeDeactivates: stopPropagation,
-              }}
-            >
-              <Modal variant="Surface" size="500">
-                <TextViewer
-                  name={selectedEvent.getType() ?? 'Source Code'}
-                  langName="json"
-                  text={JSON.stringify(selectedEvent.getContent(), null, 2)}
-                  requestClose={handleClose}
-                />
-              </Modal>
-            </FocusTrap>
-          </OverlayCenter>
-        </Overlay>
-      )}
-      {selectedOption === 'edit' && (
-        <Overlay open backdrop={<OverlayBackdrop />}>
-          <OverlayCenter>
-            <FocusTrap
-              focusTrapOptions={{
-                initialFocus: false,
-                onDeactivate: handleClose,
-                clickOutsideDeactivates: true,
-                escapeDeactivates: stopPropagation,
-              }}
-            >
-              <Modal variant="Surface" size="500">
-                <AccountDataEditor
-                  type={selectedEvent?.getType()}
-                  content={selectedEvent?.getContent()}
-                  requestClose={handleClose}
-                />
-              </Modal>
-            </FocusTrap>
-          </OverlayCenter>
-        </Overlay>
-      )}
-    </Box>
-  );
-}
+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<string | null>();
+
+  if (accountDataType !== undefined) {
+    return (
+      <AccountDataEditor
+        type={accountDataType ?? undefined}
+        requestClose={() => setAccountDataType(undefined)}
+      />
+    );
+  }
 
   return (
     <Page>
@@ -292,7 +96,13 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
                   </SequenceCard>
                 )}
               </Box>
-              {developerTools && <AccountData />}
+              {developerTools && (
+                <AccountData
+                  expand={expand}
+                  onExpandToggle={setExpend}
+                  onSelect={setAccountDataType}
+                />
+              )}
             </Box>
           </PageContent>
         </Scroll>