一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
240 行
7.4 KiB
TypeScript
240 行
7.4 KiB
TypeScript
import * as React from 'react';
|
|
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, isSubProfile, 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 subProfile = isSubProfile(profile);
|
|
const rootDir = Setting.value('rootProfileDir');
|
|
|
|
// Deleting the default profile must be handled differently. We can't delete the whole directory because it contains other profiles and global settings
|
|
if (subProfile) {
|
|
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));
|
|
} else {
|
|
const dirsToDelete = ['cache', 'resources', 'tmp'];
|
|
const filesToDelete = ['database.sqlite', 'log.txt', 'keymap-desktop.json'];
|
|
|
|
// Reset settings for the default profile, but retain global settings
|
|
try {
|
|
await Setting.resetDefaultProfileSettings();
|
|
} catch (error) {
|
|
// If the first stage fails, nothing has happened, so throw an error. But if there is a failure in later steps, ignore errors but log them
|
|
logger.error('Error deleting the default profile: ', error);
|
|
bridge().showErrorMessageBox(error.message);
|
|
return;
|
|
}
|
|
|
|
// Delete directories
|
|
for (const dir of dirsToDelete) {
|
|
const fullPath = `${rootDir}/${dir}`;
|
|
try {
|
|
if (await shim.fsDriver().exists(fullPath)) {
|
|
await shim.fsDriver().remove(fullPath);
|
|
logger.info('Deleted directory: ', fullPath);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error deleting directory: ', fullPath, error);
|
|
}
|
|
}
|
|
|
|
// Delete files
|
|
for (const file of filesToDelete) {
|
|
const fullPath = `${rootDir}/${file}`;
|
|
try {
|
|
if (await shim.fsDriver().exists(fullPath)) {
|
|
await shim.fsDriver().unlink(fullPath);
|
|
logger.info('Deleted file: ', fullPath);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error deleting file: ', fullPath, error);
|
|
}
|
|
}
|
|
|
|
bridge().showMessageBox(_('The default profile has been reset.'));
|
|
}
|
|
};
|
|
|
|
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);
|