Desktop: Resolves #14788: Add sync status icon and collapsible sync report to sidebar (#15115)

This commit is contained in:
Laurent Cozic
2026-04-16 11:53:44 +01:00
committed by GitHub
parent 4b9918a770
commit 270a2d0a3a
6 changed files with 87 additions and 78 deletions
+40 -43
View File
@@ -1,9 +1,9 @@
import * as React from 'react';
import { useCallback } from 'react';
import { StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton, StyledRoot } from './styles';
import { ButtonLevel } from '../Button/Button';
import CommandService from '@joplin/lib/services/CommandService';
import Synchronizer from '@joplin/lib/Synchronizer';
import Setting from '@joplin/lib/models/Setting';
import { _ } from '@joplin/lib/locale';
import { AppState } from '../../app.reducer';
import { StateDecryptionWorker, StateResourceFetcher } from '@joplin/lib/reducer';
@@ -11,8 +11,6 @@ import { connect } from 'react-redux';
import { themeStyle } from '@joplin/lib/theme';
import { Dispatch } from 'redux';
import FolderAndTagList from './FolderAndTagList';
import Setting from '@joplin/lib/models/Setting';
import time from '@joplin/lib/time';
interface Props {
@@ -23,18 +21,26 @@ interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
syncReport: any;
syncStarted: boolean;
syncReportLogExpanded: boolean;
syncPending: boolean;
syncReportIsVisible: boolean;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The generated report does not currently have a type
const syncCompletedWithoutError = (syncReport: any) => {
return syncReport.completedTime && (!syncReport.errors || !syncReport.errors.length);
};
const SidebarComponent = (props: Props) => {
const renderSynchronizeButton = (type: string) => {
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
const nothingToSync = type === 'sync' && !props.syncPending && syncCompletedWithoutError(props.syncReport);
const iconName = nothingToSync ? 'fas fa-check' : 'icon-sync';
return (
<StyledSynchronizeButton
level={ButtonLevel.SidebarSecondary}
className={`sidebar-sync-button ${type === 'sync' ? '' : '-syncing'}`}
iconName="icon-sync"
className={`sidebar-sync-button ${type === 'sync' ? '' : '-syncing'} ${nothingToSync ? '-synced' : ''}`}
iconName={iconName}
key="sync_button"
title={label}
onClick={() => {
@@ -56,56 +62,46 @@ const SidebarComponent = (props: Props) => {
resourceFetcherText = _('Fetching resources: %d/%d', props.resourceFetcher.fetchingCount, props.resourceFetcher.toFetchCount);
}
const syncReportExpanded = props.syncReportLogExpanded;
const toggleSyncReport = useCallback(() => {
Setting.setValue('syncReportLogExpanded', !syncReportExpanded);
}, [syncReportExpanded]);
const lines = Synchronizer.reportToLines(props.syncReport);
if (resourceFetcherText) lines.push(resourceFetcherText);
if (decryptionReportText) lines.push(decryptionReportText);
const completedTime = props.syncReport && props.syncReport.completedTime
? time.formatMsToLocal(props.syncReport.completedTime)
: null;
const syncReportText = [];
for (let i = 0; i < lines.length; i++) {
syncReportText.push(
<StyledSyncReportText key={i}>
{lines[i]}
</StyledSyncReportText>,
);
}
const syncButton = renderSynchronizeButton(props.syncStarted ? 'cancel' : 'sync');
// Toggle to show/hide sync log output
const toggleButton = (
const hasSyncReport = syncReportText.length > 0;
const syncReportComp = !hasSyncReport || !props.syncReportIsVisible ? null : (
<StyledSyncReport key="sync_report" id="sync-report">
{syncReportText}
</StyledSyncReport>
);
const syncReportToggle = (
<button
className="sidebar-sync-toggle"
onClick={toggleSyncReport}
aria-expanded={syncReportExpanded}
aria-label={syncReportExpanded ? _('Hide sync log') : _('Show sync log')}
title={syncReportExpanded ? _('Hide sync log') : _('Show sync log')}
className="sync-report-toggle"
style={{ color: theme.color2 }}
onClick={() => Setting.toggle('syncReportIsVisible')}
aria-label={_('Sync report')}
aria-expanded={props.syncReportIsVisible}
aria-controls="sync-report"
>
<i className={`fas fa-caret-${syncReportExpanded ? 'down' : 'right'}`} />
{(completedTime || props.syncStarted) ? (
<span className="timestamp">
{props.syncStarted ? _('Last sync: In progress...') : _('Last sync: %s', completedTime)}
</span>
) : ''}
<i className={`fas fa-chevron-${props.syncReportIsVisible ? 'down' : 'up'}`}/>
</button>
);
// Sync log output, only visible when expanded
const syncReportComp = (syncReportExpanded && lines.length > 0) ? (
<StyledSyncReport key="sync_report">
{lines.map((line, i) => (
<StyledSyncReportText key={i}>
{line}
</StyledSyncReportText>
))}
</StyledSyncReport>
) : null;
return (
<StyledRoot className='sidebar _scrollbar2' role='navigation' aria-label={_('Sidebar')}>
<div style={{ flex: 1 }}><FolderAndTagList /></div>
<div style={{ flex: 1 }}><FolderAndTagList/></div>
<div style={{ flex: 0, padding: theme.mainPadding }}>
{(completedTime || props.syncStarted) ? toggleButton : null}
{syncReportToggle}
{syncReportComp}
{syncButton}
</div>
@@ -117,6 +113,7 @@ const mapStateToProps = (state: AppState) => {
return {
searches: state.searches,
syncStarted: state.syncStarted,
syncPending: state.syncPending,
syncReport: state.syncReport,
selectedSearchId: state.selectedSearchId,
selectedSmartFilterId: state.selectedSmartFilterId,
@@ -125,7 +122,7 @@ const mapStateToProps = (state: AppState) => {
collapsedFolderIds: state.collapsedFolderIds,
decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher,
syncReportLogExpanded: state.settings.syncReportLogExpanded,
syncReportIsVisible: state.settings.syncReportIsVisible,
};
};
+1 -2
View File
@@ -6,5 +6,4 @@
@use 'styles/sidebar-header-container.scss';
@use 'styles/sidebar-spacer-item.scss';
@use 'styles/sidebar-header-button.scss';
@use 'styles/sidebar-sync-button.scss';
@use 'styles/sidebar-sync-toggle.scss';
@use 'styles/sidebar-sync-button.scss';
@@ -105,7 +105,7 @@ export const StyledSyncReport = styled.div`
opacity: 0.5;
display: flex;
flex-direction: column;
margin-left: 25px;
margin-left: 5px;
margin-right: 5px;
margin-bottom: 10px;
word-wrap: break-word;
@@ -5,6 +5,25 @@
}
}
@keyframes icon-fade-in-a {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes icon-fade-in-b {
from { opacity: 0; }
to { opacity: 1; }
}
.sidebar-sync-button > .icon {
display: inline-flex !important;
align-items: center;
justify-content: center;
width: 16px !important;
height: 16px;
margin-right: 8px !important;
}
.sidebar-sync-button {
&.-syncing > .icon {
animation: icon-infinite-rotation 1s linear infinite;
@@ -13,4 +32,29 @@
animation: none;
}
}
&:not(.-syncing).-synced > .icon {
animation: icon-fade-in-a 300ms ease-in-out;
font-size: 0.85em;
}
&:not(.-syncing):not(.-synced) > .icon {
animation: icon-fade-in-b 300ms ease-in-out;
}
}
.sync-report-toggle {
display: block;
width: 100%;
background: none;
border: none;
padding: 0;
text-align: center;
cursor: pointer;
opacity: 0.5;
margin-bottom: 4px;
&:hover {
opacity: 1;
}
}
@@ -1,31 +0,0 @@
.sidebar-sync-toggle {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 4px 5px 4px 5px;
background: none;
border: none;
color: var(--joplin-color2);
opacity: 0.5;
cursor: pointer;
width: 100%;
font-size: calc(var(--joplin-font-size) * 1.6);
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
i {
width: 16px;
display: inline-flex;
justify-content: center;
font-size: calc(var(--joplin-toolbar-icon-size) * 0.8);
}
>.timestamp {
font-size: 0.6em;
margin-left: 4px;
}
}
@@ -1479,7 +1479,7 @@ const builtInMetadata = (Setting: typeof SettingType) => {
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] },
tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
syncReportLogExpanded: { value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
syncReportIsVisible: { value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Cli, AppType.Desktop], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
'export.pdfPageSize': { value: 'A4', type: SettingItemType.String, advanced: true, storage: SettingStorage.File, isGlobal: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page size for PDF export'), options: () => {
return {