feat: add "sync" pane to preferences -> account tab (#621)
* feat: add "sync" pane to preferences -> account tab * chore: configure eslint to add new line at the end of file and remove trailing spaces * chore: add newline at the end of file
This commit is contained in:
@@ -19,7 +19,10 @@
|
|||||||
"camelcase": "warn",
|
"camelcase": "warn",
|
||||||
"sort-imports": "off",
|
"sort-imports": "off",
|
||||||
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
|
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
|
||||||
"react-hooks/exhaustive-deps": "error" // Checks effect dependencies
|
"react-hooks/exhaustive-deps": "error", // Checks effect dependencies
|
||||||
|
"eol-last": "error",
|
||||||
|
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
|
||||||
|
"no-trailing-spaces": "error"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true
|
"browser": true
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
# OS & IDE
|
# OS & IDE
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
|
||||||
# Ignore bundler config.
|
# Ignore bundler config.
|
||||||
/.bundle
|
/.bundle
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
|
|
||||||
const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content cursor-pointer`;
|
const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content`;
|
||||||
|
|
||||||
const normalClass = `${baseClass} bg-default color-text border-solid border-gray-300 border-1 \
|
const normalClass = `${baseClass} bg-default color-text border-solid border-gray-300 border-1 \
|
||||||
focus:bg-contrast hover:bg-contrast`;
|
focus:bg-contrast hover:bg-contrast`;
|
||||||
@@ -12,15 +12,19 @@ export const Button: FunctionComponent<{
|
|||||||
type: 'normal' | 'primary';
|
type: 'normal' | 'primary';
|
||||||
label: string;
|
label: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}> = ({ type, label, className = '', onClick }) => {
|
disabled?: boolean;
|
||||||
|
}> = ({ type, label, className = '', onClick, disabled = false }) => {
|
||||||
const buttonClass = type === 'primary' ? primaryClass : normalClass;
|
const buttonClass = type === 'primary' ? primaryClass : normalClass;
|
||||||
|
const cursorClass = disabled ? 'cursor-default' : 'cursor-pointer';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`${buttonClass} ${className}`}
|
className={`${buttonClass} ${cursorClass} ${className}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
onClick();
|
onClick();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { RoundIconButton } from '@/components/RoundIconButton';
|
import { RoundIconButton } from '@/components/RoundIconButton';
|
||||||
import { TitleBar, Title } from '@/components/TitleBar';
|
import { TitleBar, Title } from '@/components/TitleBar';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { HelpAndFeedback, Security } from './panes';
|
import { AccountPreferences, HelpAndFeedback, Security } from './panes';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { PreferencesMenu } from './preferences-menu';
|
import { PreferencesMenu } from './preferences-menu';
|
||||||
import { PreferencesMenuView } from './PreferencesMenuView';
|
import { PreferencesMenuView } from './PreferencesMenuView';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
|
||||||
const PaneSelector: FunctionComponent<{
|
const PaneSelector: FunctionComponent<{
|
||||||
prefs: PreferencesMenu;
|
prefs: PreferencesMenu;
|
||||||
}> = observer(({ prefs: menu }) => {
|
application: WebApplication;
|
||||||
|
}> = observer(({ prefs: menu, application }) => {
|
||||||
switch (menu.selectedPaneId) {
|
switch (menu.selectedPaneId) {
|
||||||
case 'general':
|
case 'general':
|
||||||
return null;
|
return null;
|
||||||
case 'account':
|
case 'account':
|
||||||
return null;
|
return <AccountPreferences application={application} />;
|
||||||
case 'appearance':
|
case 'appearance':
|
||||||
return null;
|
return null;
|
||||||
case 'security':
|
case 'security':
|
||||||
@@ -33,15 +35,19 @@ const PaneSelector: FunctionComponent<{
|
|||||||
|
|
||||||
const PreferencesCanvas: FunctionComponent<{
|
const PreferencesCanvas: FunctionComponent<{
|
||||||
preferences: PreferencesMenu;
|
preferences: PreferencesMenu;
|
||||||
}> = observer(({ preferences: prefs }) => (
|
application: WebApplication;
|
||||||
|
}> = observer(({ preferences: prefs, application }) => (
|
||||||
<div className="flex flex-row flex-grow min-h-0 justify-between">
|
<div className="flex flex-row flex-grow min-h-0 justify-between">
|
||||||
<PreferencesMenuView menu={prefs}></PreferencesMenuView>
|
<PreferencesMenuView menu={prefs}></PreferencesMenuView>
|
||||||
<PaneSelector prefs={prefs} />
|
<PaneSelector prefs={prefs} application={application} />
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
const PreferencesView: FunctionComponent<{ close: () => void }> = observer(
|
const PreferencesView: FunctionComponent<{
|
||||||
({ close }) => {
|
close: () => void;
|
||||||
|
application: WebApplication;
|
||||||
|
}> = observer(
|
||||||
|
({ close, application }) => {
|
||||||
const prefs = new PreferencesMenu();
|
const prefs = new PreferencesMenu();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,7 +64,7 @@ const PreferencesView: FunctionComponent<{ close: () => void }> = observer(
|
|||||||
icon="close"
|
icon="close"
|
||||||
/>
|
/>
|
||||||
</TitleBar>
|
</TitleBar>
|
||||||
<PreferencesCanvas preferences={prefs} />
|
<PreferencesCanvas preferences={prefs} application={application} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -66,12 +72,16 @@ const PreferencesView: FunctionComponent<{ close: () => void }> = observer(
|
|||||||
|
|
||||||
export interface PreferencesWrapperProps {
|
export interface PreferencesWrapperProps {
|
||||||
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
|
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
|
||||||
|
application: WebApplication;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PreferencesViewWrapper: FunctionComponent<PreferencesWrapperProps> =
|
export const PreferencesViewWrapper: FunctionComponent<PreferencesWrapperProps> =
|
||||||
observer(({ appState }) => {
|
observer(({ appState, application }) => {
|
||||||
if (!appState.preferences.isOpen) return null;
|
if (!appState.preferences.isOpen) return null;
|
||||||
return (
|
return (
|
||||||
<PreferencesView close={() => appState.preferences.closePreferences()} />
|
<PreferencesView
|
||||||
|
application={application}
|
||||||
|
close={() => appState.preferences.closePreferences()}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Sync } from '@/preferences/panes/account';
|
||||||
|
import { PreferencesPane } from '@/preferences/components';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
|
||||||
|
export const AccountPreferences = observer(({application}: {application: WebApplication}) => {
|
||||||
|
return (
|
||||||
|
<PreferencesPane>
|
||||||
|
<Sync application={application} />
|
||||||
|
</PreferencesPane>
|
||||||
|
);
|
||||||
|
});
|
||||||
60
app/assets/javascripts/preferences/panes/account/Sync.tsx
Normal file
60
app/assets/javascripts/preferences/panes/account/Sync.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/preferences/components';
|
||||||
|
import { Button } from '@/components/Button';
|
||||||
|
import { SyncQueueStrategy } from '@node_modules/@standardnotes/snjs';
|
||||||
|
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
|
||||||
|
import { useState } from '@node_modules/preact/hooks';
|
||||||
|
import { dateToLocalizedString } from '@/utils';
|
||||||
|
import { observer } from '@node_modules/mobx-react-lite';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Sync = observer(({ application }: Props) => {
|
||||||
|
const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||||
|
return dateToLocalizedString(lastUpdatedDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isSyncingInProgress, setIsSyncingInProgress] = useState(false);
|
||||||
|
const [lastSyncDate, setLastSyncDate] = useState(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||||
|
|
||||||
|
const doSynchronization = async () => {
|
||||||
|
setIsSyncingInProgress(true);
|
||||||
|
|
||||||
|
const response = await application.sync({
|
||||||
|
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||||
|
checkIntegrity: true
|
||||||
|
});
|
||||||
|
setIsSyncingInProgress(false);
|
||||||
|
if (response && response.error) {
|
||||||
|
application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||||
|
} else {
|
||||||
|
setLastSyncDate(formatLastSyncDate(application.getLastSyncDate() as Date));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<div className='flex flex-row items-center'>
|
||||||
|
<div className='flex-grow flex flex-col'>
|
||||||
|
<Title>Sync</Title>
|
||||||
|
<Text>
|
||||||
|
Last synced <span className='font-bold'>on {lastSyncDate}</span>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
className='min-w-20 mt-3'
|
||||||
|
type='normal'
|
||||||
|
label='Sync now'
|
||||||
|
disabled={isSyncingInProgress}
|
||||||
|
onClick={doSynchronization}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Sync;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Sync } from './Sync';
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './HelpFeedback';
|
export * from './HelpFeedback';
|
||||||
export * from './Security';
|
export * from './Security';
|
||||||
|
export * from './AccountPreferences';
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
)
|
)
|
||||||
preferences(
|
preferences(
|
||||||
app-state='self.appState'
|
app-state='self.appState'
|
||||||
|
application='self.application'
|
||||||
)
|
)
|
||||||
challenge-modal(
|
challenge-modal(
|
||||||
ng-repeat="challenge in self.challenges track by challenge.id"
|
ng-repeat="challenge in self.challenges track by challenge.id"
|
||||||
|
|||||||
@@ -199,6 +199,10 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor-default {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.fill-current {
|
.fill-current {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
|
|||||||
14
yarn.lock
14
yarn.lock
@@ -2016,6 +2016,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.1.1.tgz#834701c2e14d31eb204bff90457fa05e9183464a"
|
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.1.1.tgz#834701c2e14d31eb204bff90457fa05e9183464a"
|
||||||
integrity sha512-E9zDYZ1gJkVZBEzd7a1L2haQ4GYeH1lUrY87UmDH1AMYUHW+c0SqZ71af1fBNqGzrx3EZSXk+Qzr7RyOa6N1Mw==
|
integrity sha512-E9zDYZ1gJkVZBEzd7a1L2haQ4GYeH1lUrY87UmDH1AMYUHW+c0SqZ71af1fBNqGzrx3EZSXk+Qzr7RyOa6N1Mw==
|
||||||
|
|
||||||
|
"@standardnotes/features@1.0.0":
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.0.0.tgz#906af029b6e58241689ca37436982c37a888a418"
|
||||||
|
integrity sha512-PEQyP/p/TQLVcNYcbu9jEIWNRqBrFFG1Qyy8QIcvNUt5o4lpLZGEY1T+PJUsPSisnuKKNpQrgVLc9LjhUKpuYw==
|
||||||
|
|
||||||
"@standardnotes/sncrypto-common@^1.2.7", "@standardnotes/sncrypto-common@^1.2.9":
|
"@standardnotes/sncrypto-common@^1.2.7", "@standardnotes/sncrypto-common@^1.2.9":
|
||||||
version "1.2.9"
|
version "1.2.9"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.2.9.tgz#5212a959e4ec563584e42480bfd39ef129c3cbdf"
|
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.2.9.tgz#5212a959e4ec563584e42480bfd39ef129c3cbdf"
|
||||||
@@ -2029,12 +2034,13 @@
|
|||||||
"@standardnotes/sncrypto-common" "^1.2.7"
|
"@standardnotes/sncrypto-common" "^1.2.7"
|
||||||
libsodium-wrappers "^0.7.8"
|
libsodium-wrappers "^0.7.8"
|
||||||
|
|
||||||
"@standardnotes/snjs@2.7.21":
|
"@standardnotes/snjs@2.7.23":
|
||||||
version "2.7.21"
|
version "2.7.23"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.7.21.tgz#db451e5facaf5fa41fa509eb1f304723929c3541"
|
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.7.23.tgz#fedc9c025301dbe20ed2d598fb378e36f90ff64e"
|
||||||
integrity sha512-GhkGk1LJmD494COZkSOgyHaUnGnLWNLlSuCZMTwbw3dgkN5PjobbRhfDvEZaLqjwok+h9nkiQt3hugQ3h6Cy5w==
|
integrity sha512-eoEwKlV2PZcJXFbCt6bgovu9nldVoT7DPoterTBo/NZ4odRILOwxLA1SAgL5H5FYPb9NHkwaaCt9uTdIqdNYhA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "3.1.1"
|
"@standardnotes/auth" "3.1.1"
|
||||||
|
"@standardnotes/features" "1.0.0"
|
||||||
"@standardnotes/sncrypto-common" "^1.2.9"
|
"@standardnotes/sncrypto-common" "^1.2.9"
|
||||||
|
|
||||||
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
|
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
|
||||||
|
|||||||
Reference in New Issue
Block a user