Skip to content

Commit

Permalink
Merge pull request #20 from rossbulat/rb-workspace-tab
Browse files Browse the repository at this point in the history
feat: add workspace tab
  • Loading branch information
Ross Bulat authored Mar 9, 2024
2 parents 111364c + a16f603 commit 5f8cf64
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 57 deletions.
1 change: 1 addition & 0 deletions src/library/Settings/Wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
53 changes: 13 additions & 40 deletions src/screens/Default/Menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<HeaderMenuWrapper>
<div className="menu">
<section className="main">
<div className="label"> {screenLabel}</div>
<button
onClick={() => setActiveSection(0)}
className={activeSection === 0 ? 'active' : undefined}
>
{!apiActive ? 'Search Chain' : 'Overview'}
</button>
{apiActive && (
<>
<button
onClick={() => setActiveSection(2)}
className={activeSection === 2 ? 'active' : undefined}
>
Chain State
</button>
<button
onClick={() => setActiveSection(3)}
className={activeSection === 3 ? 'active' : undefined}
>
Extrinsics
</button>
</>
)}
<div className="label">{label}</div>
{Object.entries(sections).map(([key, section], index) => (
<button
key={`menu-section-${key}-${index}`}
onClick={() => setActiveSection(Number(key))}
className={activeSection === Number(key) ? 'active' : undefined}
>
{section.label}
</button>
))}
</section>
<section className="other">{/* Additional links right side */}</section>
</div>
Expand Down
59 changes: 59 additions & 0 deletions src/screens/Default/Route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => (
<PageWithMenu pageId="default" Page={Default} Menu={ChainMenu} />
Expand Down
24 changes: 11 additions & 13 deletions src/screens/Settings/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -15,19 +16,16 @@ export const SettingsMenu = () => {
<HeaderMenuWrapper>
<div className="menu">
<section>
<div className="label">Settings</div>
<button
className={activeSection === 0 ? 'active' : undefined}
onClick={() => setActiveSection(0)}
>
Tabs
</button>
<button
className={activeSection === 1 ? 'active' : undefined}
onClick={() => setActiveSection(1)}
>
Tags
</button>
<div className="label">{ScreenLabel}</div>
{Object.entries(SettingsSections).map(([key, { label }], index) => (
<button
key={`menu-section-${key}-${index}`}
className={activeSection === Number(key) ? 'active' : undefined}
onClick={() => setActiveSection(Number(key))}
>
{label}
</button>
))}
</section>
</div>
<div className="config">
Expand Down
21 changes: 21 additions & 0 deletions src/screens/Settings/Route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => (
<PageWithMenu pageId="settings" Page={Settings} Menu={SettingsMenu} />
Expand Down
12 changes: 12 additions & 0 deletions src/screens/Settings/TabSettings/Wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand All @@ -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;
}
}
}
`;
56 changes: 56 additions & 0 deletions src/screens/Settings/WorkspaceSettings/Utils.ts
Original file line number Diff line number Diff line change
@@ -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<string, AnyJson>, 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;
}
};
83 changes: 83 additions & 0 deletions src/screens/Settings/WorkspaceSettings/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<SettingsHeaderWrapper>
<h2>Workspace Settings</h2>
</SettingsHeaderWrapper>

<SettingsToggleWrapper>
<div className="text">
<h4>Export Workspace</h4>
<h3>
Back up your current workspace state. Exports your tabs, tags, chain
search settings.
</h3>
</div>
</SettingsToggleWrapper>

<SettingsSubmitWrapper>
<div className="buttons">
<button
onClick={() => {
if (!exportWorkspace()) {
NotificationsController.emit({
title: 'Export Failed',
subtitle: 'There was an issue exporting your workspace.',
});
}
}}
>
<FontAwesomeIcon icon={faDownload} />
Export Workspace
</button>
</div>
</SettingsSubmitWrapper>

<SettingsToggleWrapper>
<div className="text">
<h4>Import Workspace</h4>
<h3>Import a workspace configuration.</h3>
<h3 className="inline danger">
<FontAwesomeIcon icon={faTriangleExclamation} />
&nbsp; 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.
</h3>
</div>
</SettingsToggleWrapper>

<SettingsSubmitWrapper>
<div className="buttons">
<button
onClick={(ev) => openMenu(ev, <InDevelopment />, { size: 'large' })}
>
<FontAwesomeIcon icon={faFileImport} />
Import Workspace
</button>
</div>
</SettingsSubmitWrapper>
</>
);
};
Loading

0 comments on commit 5f8cf64

Please sign in to comment.