diff --git a/src/library/Settings/Wrappers.ts b/src/library/Settings/Wrappers.ts index d1fe13a6..1eabe110 100644 --- a/src/library/Settings/Wrappers.ts +++ b/src/library/Settings/Wrappers.ts @@ -8,6 +8,7 @@ export const SettingsHeaderWrapper = styled.div` display: flex; align-items: center; margin-bottom: 0.75rem; + min-height: 2.5rem; > h2 { flex-grow: 1; diff --git a/src/screens/Default/Menu/index.tsx b/src/screens/Default/Menu/index.tsx index 50d99932..1fe11a8f 100644 --- a/src/screens/Default/Menu/index.tsx +++ b/src/screens/Default/Menu/index.tsx @@ -11,59 +11,32 @@ import { useLocation } from 'react-router-dom'; import { HeaderMenuWrapper } from 'library/HeaderMenu/Wrappers'; import { useSection } from 'library/Page/provider'; import { ButtonWithTooltip } from './ButtonWithTooltip'; -import { useApi } from 'contexts/Api'; import { useRedirectOnInactive } from 'hooks/useRedirectOnInactive'; +import { useScreenSections } from '../Route'; export const ChainMenu = () => { - const { getApiStatus, getApiActive } = useApi(); const { pathname } = useLocation(); + const { label, sections } = useScreenSections(); const { activeSection, setActiveSection } = useSection(); const { tabsHidden, setTabsHidden, activeTabId } = useTabs(); - // Redirect to section 0 if api becomes inactive. + // Redirect to section 0 if Api becomes inactive. useRedirectOnInactive(activeTabId); - const apiStatus = getApiStatus(activeTabId); - const apiActive = getApiActive(activeTabId); - - let screenLabel; - switch (apiStatus) { - case 'connecting': - case 'ready': - case 'connected': - screenLabel = 'Chain'; - break; - default: - screenLabel = 'Connect'; - } - return (
-
{screenLabel}
- - {apiActive && ( - <> - - - - )} +
{label}
+ {Object.entries(sections).map(([key, section], index) => ( + + ))}
{/* Additional links right side */}
diff --git a/src/screens/Default/Route.tsx b/src/screens/Default/Route.tsx index 1bb3706d..36726d06 100644 --- a/src/screens/Default/Route.tsx +++ b/src/screens/Default/Route.tsx @@ -4,6 +4,65 @@ import { ChainMenu } from './Menu'; import { Default } from '.'; import { PageWithMenu } from 'screens/Common/PageWithMenu'; +import type { ScreenSections } from 'screens/types'; +import { useApi } from 'contexts/Api'; +import { useTabs } from 'contexts/Tabs'; +import { Overview } from './Overview'; +import { Extrinsics } from './Extrinsics'; +import { ChainState } from './ChainState'; + +export const ScreenLabel = 'Settings'; + +export const useScreenSections = (): { + label: string; + sections: ScreenSections; +} => { + const { activeTabId } = useTabs(); + const { getApiStatus, getApiActive } = useApi(); + const apiStatus = getApiStatus(activeTabId); + const apiActive = getApiActive(activeTabId); + + // If Api status is not actively connecting or connected, route to Connect. + const API_STATUSES = ['ready', 'connected', 'connecting']; + + // Determine screen sections based on Api status. + const sections: ScreenSections = { + 0: API_STATUSES.includes(apiStatus) + ? { + label: 'Overview', + Component: Overview, + } + : { + label: 'Search Chain', + Component: Default, + }, + }; + + if (apiActive) { + sections[1] = { + label: 'Chain State', + Component: ChainState, + }; + sections[2] = { + label: 'Extrinsics', + Component: Extrinsics, + }; + } + + // Determine screen label based on Api status. + let label; + switch (apiStatus) { + case 'connecting': + case 'ready': + case 'connected': + label = 'Chain'; + break; + default: + label = 'Connect'; + } + + return { label, sections }; +}; export const DefaultRoute = () => ( diff --git a/src/screens/Settings/Menu.tsx b/src/screens/Settings/Menu.tsx index 6237c04c..4f8fb7af 100644 --- a/src/screens/Settings/Menu.tsx +++ b/src/screens/Settings/Menu.tsx @@ -6,6 +6,7 @@ import { useSection } from '../../library/Page/provider'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheck } from '@fortawesome/free-solid-svg-icons'; import { useNavigate } from 'react-router-dom'; +import { SettingsSections, ScreenLabel } from './Route'; export const SettingsMenu = () => { const navigate = useNavigate(); @@ -15,19 +16,16 @@ export const SettingsMenu = () => {
-
Settings
- - +
{ScreenLabel}
+ {Object.entries(SettingsSections).map(([key, { label }], index) => ( + + ))}
diff --git a/src/screens/Settings/Route.tsx b/src/screens/Settings/Route.tsx index f488ae5a..58bf0ef0 100644 --- a/src/screens/Settings/Route.tsx +++ b/src/screens/Settings/Route.tsx @@ -4,6 +4,27 @@ import { SettingsMenu } from './Menu'; import { Settings } from '.'; import { PageWithMenu } from 'screens/Common/PageWithMenu'; +import { WorkspaceSettings } from './WorkspaceSettings'; +import { TabSettings } from './TabSettings'; +import { TagSettings } from './TagSettings'; +import type { ScreenSections } from 'screens/types'; + +export const ScreenLabel = 'Settings'; + +export const SettingsSections: ScreenSections = { + 0: { + label: 'Tabs', + Component: TabSettings, + }, + 1: { + label: 'Tags', + Component: TagSettings, + }, + 2: { + label: 'Workspace', + Component: WorkspaceSettings, + }, +}; export const SettingsRoute = () => ( diff --git a/src/screens/Settings/TabSettings/Wrappers.ts b/src/screens/Settings/TabSettings/Wrappers.ts index 51c70197..3281f14d 100644 --- a/src/screens/Settings/TabSettings/Wrappers.ts +++ b/src/screens/Settings/TabSettings/Wrappers.ts @@ -22,7 +22,15 @@ export const SettingsToggleWrapper = styled.div` > h3 { color: var(--text-color-primary); + line-height: 1.3rem; flex: 0; + + &.inline { + margin-top: 0.75rem; + } + &.danger { + color: var(--status-danger-color); + } } } @@ -42,6 +50,10 @@ export const SettingsSubmitWrapper = styled.div` color: var(--accent-color-primary); padding: 0.35rem 0.75rem; border-radius: 0.4rem; + + > svg { + margin-right: 0.5rem; + } } } `; diff --git a/src/screens/Settings/WorkspaceSettings/Utils.ts b/src/screens/Settings/WorkspaceSettings/Utils.ts new file mode 100644 index 00000000..86d5bd48 --- /dev/null +++ b/src/screens/Settings/WorkspaceSettings/Utils.ts @@ -0,0 +1,56 @@ +// Copyright 2024 @rossbulat/console authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyJson } from '@w3ux/utils/types'; + +// The supported localStorage keys for import and export. +// TODO: Reformat local pageSection keys to host in one `pageSections` key. +const SUPPORTED_WORKSPACE_LOCAL_STORAGE_KEYS = [ + 'activeTabs', + 'activeTabId', + 'activeTabIndex', + 'customNodeUrls', + 'searchTerms', + 'appliedTags', +]; + +// Exporting workspace settings. +export const exportWorkspace = () => { + // Fetch all keys from localStorage + const storageKeys = SUPPORTED_WORKSPACE_LOCAL_STORAGE_KEYS; + const exportData = storageKeys.reduce( + (acc: Record, key: string) => { + try { + const data = localStorage.getItem(key); + // Add local storage item if not falsy. + if (data) { + acc[key] = JSON.parse(data); + } + return acc; + } catch (e) { + // Continue accumulating on error. + return acc; + } + }, + {} + ); + + try { + // Convert to JSON and create a data URI to download the file. + const dataStr = JSON.stringify(exportData, undefined); + const dataUri = + 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); + + const exportFileDefaultName = 'workspace-settings.json'; + + const linkElement = document.createElement('a'); + linkElement.setAttribute('href', dataUri); + linkElement.setAttribute('download', exportFileDefaultName); + linkElement.click(); + linkElement.remove(); + + return true; + } catch (e) { + return false; + } +}; diff --git a/src/screens/Settings/WorkspaceSettings/index.tsx b/src/screens/Settings/WorkspaceSettings/index.tsx new file mode 100644 index 00000000..69d698ce --- /dev/null +++ b/src/screens/Settings/WorkspaceSettings/index.tsx @@ -0,0 +1,83 @@ +// Copyright 2024 @rossbulat/console authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { SettingsHeaderWrapper } from 'library/Settings/Wrappers'; +import { + SettingsSubmitWrapper, + SettingsToggleWrapper, +} from '../TabSettings/Wrappers'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faDownload, + faFileImport, + faTriangleExclamation, +} from '@fortawesome/free-solid-svg-icons'; +import { useMenu } from 'contexts/Menu'; +import { InDevelopment } from 'library/HelpMenu/InDevelopment'; +import { exportWorkspace } from './Utils'; +import { NotificationsController } from 'controllers/NotificationsController'; + +export const WorkspaceSettings = () => { + const { openMenu } = useMenu(); + + return ( + <> + +

Workspace Settings

+
+ + +
+

Export Workspace

+

+ Back up your current workspace state. Exports your tabs, tags, chain + search settings. +

+
+
+ + +
+ +
+
+ + +
+

Import Workspace

+

Import a workspace configuration.

+

+ +   Importing a workspace will replace your current workspace - + all current state, including your current tabs and custom tag + settings, will be lost. Export your workspace first if you wish to + restore it later. +

+
+
+ + +
+ +
+
+ + ); +}; diff --git a/src/screens/Settings/index.tsx b/src/screens/Settings/index.tsx index 6c256a9f..ccb174cb 100644 --- a/src/screens/Settings/index.tsx +++ b/src/screens/Settings/index.tsx @@ -1,18 +1,17 @@ // Copyright 2024 @rossbulat/console authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { TagSettings } from './TagSettings'; import { useSection } from 'library/Page/provider'; -import { TabSettings } from './TabSettings'; import { PageContentWrapper } from 'library/Page/Wrapper'; +import { SettingsSections } from './Route'; export const Settings = () => { const { activeSection } = useSection(); + const { Component } = SettingsSections[activeSection]; return ( - {activeSection === 0 && } - {activeSection === 1 && } + {Component !== undefined && } ); }; diff --git a/src/screens/types.ts b/src/screens/types.ts new file mode 100644 index 00000000..f3c39257 --- /dev/null +++ b/src/screens/types.ts @@ -0,0 +1,12 @@ +// Copyright 2024 @rossbulat/console authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { FC } from 'react'; + +export type ScreenSections = Record< + number, + { + label: string; + Component: FC; + } +>;