WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/showProfileEditor.js
packages/app-desktop/commands/startExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/switchProfile.js
Expand Down Expand Up @@ -393,6 +394,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/ProfileEditor.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ packages/app-desktop/commands/openProfileDirectory.js
packages/app-desktop/commands/openSecondaryAppInstance.js
packages/app-desktop/commands/replaceMisspelling.js
packages/app-desktop/commands/restoreNoteRevision.js
packages/app-desktop/commands/showProfileEditor.js
packages/app-desktop/commands/startExternalEditing.js
packages/app-desktop/commands/stopExternalEditing.js
packages/app-desktop/commands/switchProfile.js
Expand Down Expand Up @@ -365,6 +366,7 @@ packages/app-desktop/gui/PopupNotification/NotificationItem.js
packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
packages/app-desktop/gui/PopupNotification/types.js
packages/app-desktop/gui/ProfileEditor.js
packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
Expand Down
2 changes: 2 additions & 0 deletions packages/app-desktop/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as openProfileDirectory from './openProfileDirectory';
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision';
import * as showProfileEditor from './showProfileEditor';
import * as startExternalEditing from './startExternalEditing';
import * as stopExternalEditing from './stopExternalEditing';
import * as switchProfile from './switchProfile';
Expand All @@ -40,6 +41,7 @@ const index: any[] = [
openSecondaryAppInstance,
replaceMisspelling,
restoreNoteRevision,
showProfileEditor,
startExternalEditing,
stopExternalEditing,
switchProfile,
Expand Down
20 changes: 20 additions & 0 deletions packages/app-desktop/commands/showProfileEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';

export const declaration: CommandDeclaration = {
name: 'showProfileEditor',
label: () => _('Manage profiles'),
};

export const runtime = (): CommandRuntime => {
return {
execute: async (context: CommandContext) => {
context.dispatch({
type: 'NAV_GO',
routeName: 'ProfileEditor',
});
},
enabledCondition: 'hasMultiProfiles',
};
};

2 changes: 1 addition & 1 deletion packages/app-desktop/gui/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: an

switchProfileMenuItems.push({ type: 'separator' });
switchProfileMenuItems.push(menuItemDic.addProfile);
switchProfileMenuItems.push(menuItemDic.editProfileConfig);
switchProfileMenuItems.push(menuItemDic.showProfileEditor);

return switchProfileMenuItems;
}, [profileConfig, menuItemDic]);
Expand Down
47 changes: 47 additions & 0 deletions packages/app-desktop/gui/ProfileEditor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.profile-management {
font-family: var(--joplin-font-family);
display: flex;
flex-direction: column;

> .tableContainer {
overflow-y: scroll;
padding: 20px;
box-sizing: border-box;
flex: 1 1 0%;
color: var(--joplin-color);

> .notification {
margin-bottom: 10px;
}
}
}

.profile-table {
width: 100%;

> thead > tr > .headerCell {
white-space: nowrap;
font-weight: bold;
width: 1px;
}

> tbody > tr {
> .nameCell {
text-overflow: ellipsis;
overflow-x: hidden;
max-width: 1px;
width: 100%;
white-space: nowrap;
}

> .dataCell {
white-space: nowrap;
width: 1px;
color: var(--joplin-color-faded);
}

> .profileActions > button {
margin-right: 10px;
}
}
}
193 changes: 193 additions & 0 deletions packages/app-desktop/gui/ProfileEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import * as React from 'react';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be called ProfileEditor like on mobile

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

import { useState, useEffect, CSSProperties } from 'react';
import ButtonBar from './ConfigScreen/ButtonBar';
import { _ } from '@joplin/lib/locale';
import { connect } from 'react-redux';
import { themeStyle } from '@joplin/lib/theme';
import bridge from '../services/bridge';
import dialogs from './dialogs';
import { Profile, ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import { deleteProfileById, saveProfileConfig } from '@joplin/lib/services/profileConfig';
import Setting from '@joplin/lib/models/Setting';
import shim from '@joplin/lib/shim';
import Logger from '@joplin/utils/Logger';
import { AppState } from '../app.reducer';
import { Dispatch } from 'redux';

const logger = Logger.create('ProfileEditor');

interface Props {
themeId: number;
dispatch: Dispatch;
style: CSSProperties;
profileConfig: ProfileConfig;
}

interface ProfileTableProps {
profiles: Profile[];
currentProfileId: string;
onProfileRename: (profile: Profile)=> void;
onProfileDelete: (profile: Profile)=> void;
themeId: number;
}

const ProfileTableComp: React.FC<ProfileTableProps> = props => {
const theme = themeStyle(props.themeId);

return (
<table className="profile-table">
<thead>
<tr>
<th className="headerCell">{_('Profile name')}</th>
<th className="headerCell">{_('ID')}</th>
<th className="headerCell">{_('Status')}</th>
<th className="headerCell">{_('Actions')}</th>
</tr>
</thead>
<tbody>
{props.profiles.map((profile: Profile, index: number) => {
const isCurrentProfile = profile.id === props.currentProfileId;
return (
<tr key={index}>
<td id={`name-${profile.id}`} className="nameCell">
<span style={{ fontWeight: isCurrentProfile ? 'bold' : 'normal' }}>
{profile.name || `(${_('Untitled')})`}
</span>
</td>
<td className="dataCell">{profile.id}</td>
<td className="dataCell">
{isCurrentProfile ? _('Active') : ''}
</td>
<td className="dataCell profileActions">
<button
id={`rename-${profile.id}`}
aria-labelledby={`rename-${profile.id} name-${profile.id}`}
style={theme.buttonStyle}
onClick={() => props.onProfileRename(profile)}
>
{_('Rename')}
</button>
{!isCurrentProfile && (
<button
id={`delete-${profile.id}`}
aria-labelledby={`delete-${profile.id} name-${profile.id}`}
style={theme.buttonStyle}
onClick={() => props.onProfileDelete(profile)}
>
{_('Delete')}
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
);
};

const ProfileEditorComponent: React.FC<Props> = props => {
const { profileConfig, themeId, dispatch } = props;
const theme = themeStyle(themeId);
const style = props.style;
const containerHeight = style.height;

const [profiles, setProfiles] = useState<Profile[]>(profileConfig.profiles);

useEffect(() => {
setProfiles(profileConfig.profiles);
}, [profileConfig]);

const saveNewProfileConfig = async (makeNewProfileConfig: ()=> ProfileConfig) => {
try {
const newProfileConfig = makeNewProfileConfig();
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newProfileConfig);
dispatch({
type: 'PROFILE_CONFIG_SET',
value: newProfileConfig,
});
} catch (error) {
logger.error(error);
bridge().showErrorMessageBox(error.message);
}
};

const onProfileRename = async (profile: Profile) => {
const newName = await dialogs.prompt(_('Profile name:'), '', profile.name);
if (newName === null || newName === undefined || newName === profile.name) return;

if (!newName.trim()) {
bridge().showErrorMessageBox(_('Profile name cannot be empty'));
return;
}

const makeNewProfileConfig = () => {
const newProfiles = profileConfig.profiles.map(p =>
p.id === profile.id ? { ...p, name: newName.trim() } : p,
);

const newProfileConfig = {
...profileConfig,
profiles: newProfiles,
};

return newProfileConfig;
};

await saveNewProfileConfig(makeNewProfileConfig);
};

const onProfileDelete = async (profile: Profile) => {
const isCurrentProfile = profile.id === profileConfig.currentProfileId;
if (isCurrentProfile) {
bridge().showErrorMessageBox(_('The active profile cannot be deleted. Switch to a different profile and try again.'));
return;
}

const ok = bridge().showConfirmMessageBox(_('Delete profile "%s"?\n\nAll data, including notes, notebooks and tags will be permanently deleted.', profile.name), {
buttons: [_('Delete'), _('Cancel')],
defaultId: 1,
});
if (!ok) return;

const rootDir = Setting.value('rootProfileDir');
const profileDir = `${rootDir}/profile-${profile.id}`;

try {
await shim.fsDriver().remove(profileDir);
logger.info('Deleted profile directory: ', profileDir);
} catch (error) {
logger.error('Error deleting profile directory: ', error);
bridge().showErrorMessageBox(error.message);
}

await saveNewProfileConfig(() => deleteProfileById(profileConfig, profile.id));
};

return (
<div className="profile-management" style={{ ...theme.containerStyle, height: containerHeight }}>
<div className="tableContainer">
<div className="notification" style={theme.notificationBox}>
{_('Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.')}
</div>
<ProfileTableComp
themeId={themeId}
profiles={profiles}
currentProfileId={profileConfig.currentProfileId}
onProfileRename={onProfileRename}
onProfileDelete={onProfileDelete}
/>
</div>
<ButtonBar
onCancelClick={() => dispatch({ type: 'NAV_BACK' })}
/>
</div>
);
};

const mapStateToProps = (state: AppState) => ({
themeId: state.settings.theme,
profileConfig: state.profileConfig,
});

export default connect(mapStateToProps)(ProfileEditorComponent);
2 changes: 2 additions & 0 deletions packages/app-desktop/gui/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Dialog from './Dialog';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
import ImportScreen from './ImportScreen';
import ResourceScreen from './ResourceScreen';
import ProfileEditor from './ProfileEditor';
import Navigator from './Navigator';
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
Expand Down Expand Up @@ -165,6 +166,7 @@ class RootComponent extends React.Component<Props, any> {
Import: { screen: ImportScreen, title: () => _('Import') },
Config: { screen: ConfigScreen, title: () => _('Options') },
Resources: { screen: ResourceScreen, title: () => _('Note attachments') },
ProfileEditor: { screen: ProfileEditor, title: () => _('Manage profiles') },
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
};

Expand Down
2 changes: 1 addition & 1 deletion packages/app-desktop/gui/menuCommandNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default function() {
'commandPalette',
'openMasterPasswordDialog',
'addProfile',
'editProfileConfig',
'showProfileEditor',
'switchProfile1',
'switchProfile2',
'switchProfile3',
Expand Down
1 change: 1 addition & 0 deletions packages/app-desktop/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
@use 'gui/Sidebar/style.scss' as sidebar-styles;
@use 'gui/NoteEditor/style.scss' as note-editor-styles;
@use 'gui/KeymapConfig/style.scss' as keymap-styles;
@use 'gui/ProfileEditor.scss' as profile-editor;
@use 'services/plugins/styles/index.scss' as plugins-styles;
@use 'gui/styles/index.scss' as gui-styles;
@use 'main.scss' as main;
Expand Down