diff --git a/packages/app-desktop/commands/switchProfile.ts b/packages/app-desktop/commands/switchProfile.ts index 2a06ca77d..92be23132 100644 --- a/packages/app-desktop/commands/switchProfile.ts +++ b/packages/app-desktop/commands/switchProfile.ts @@ -10,13 +10,13 @@ export const declaration: CommandDeclaration = { export const runtime = (): CommandRuntime => { return { - execute: async (context: CommandContext, profileIndex: number) => { + execute: async (context: CommandContext, profileId: string) => { const currentConfig = context.state.profileConfig; - if (currentConfig.currentProfile === profileIndex) return; + if (currentConfig.currentProfileId === profileId) return; const newConfig: ProfileConfig = { ...currentConfig, - currentProfile: profileIndex, + currentProfileId: profileId, }; await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig); diff --git a/packages/app-desktop/commands/switchProfile1.ts b/packages/app-desktop/commands/switchProfile1.ts index e84d2dfa5..f287689a1 100644 --- a/packages/app-desktop/commands/switchProfile1.ts +++ b/packages/app-desktop/commands/switchProfile1.ts @@ -1,5 +1,6 @@ import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; import { _ } from '@joplin/lib/locale'; +import { profileIdByIndex } from '../../lib/services/profileConfig'; export const declaration: CommandDeclaration = { name: 'switchProfile1', @@ -8,8 +9,8 @@ export const declaration: CommandDeclaration = { export const runtime = (): CommandRuntime => { return { - execute: async (_context: CommandContext) => { - await CommandService.instance().execute('switchProfile', 0); + execute: async (context: CommandContext) => { + await CommandService.instance().execute('switchProfile', profileIdByIndex(context.state.profileConfig, 0)); }, }; }; diff --git a/packages/app-desktop/commands/switchProfile2.ts b/packages/app-desktop/commands/switchProfile2.ts index 70a3b6994..c2bcec259 100644 --- a/packages/app-desktop/commands/switchProfile2.ts +++ b/packages/app-desktop/commands/switchProfile2.ts @@ -1,5 +1,6 @@ import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; import { _ } from '@joplin/lib/locale'; +import { profileIdByIndex } from '../../lib/services/profileConfig'; export const declaration: CommandDeclaration = { name: 'switchProfile2', @@ -8,8 +9,8 @@ export const declaration: CommandDeclaration = { export const runtime = (): CommandRuntime => { return { - execute: async (_context: CommandContext) => { - await CommandService.instance().execute('switchProfile', 1); + execute: async (context: CommandContext) => { + await CommandService.instance().execute('switchProfile', profileIdByIndex(context.state.profileConfig, 1)); }, }; }; diff --git a/packages/app-desktop/commands/switchProfile3.ts b/packages/app-desktop/commands/switchProfile3.ts index fce22262d..a5918724d 100644 --- a/packages/app-desktop/commands/switchProfile3.ts +++ b/packages/app-desktop/commands/switchProfile3.ts @@ -1,5 +1,6 @@ import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; import { _ } from '@joplin/lib/locale'; +import { profileIdByIndex } from '../../lib/services/profileConfig'; export const declaration: CommandDeclaration = { name: 'switchProfile3', @@ -8,8 +9,8 @@ export const declaration: CommandDeclaration = { export const runtime = (): CommandRuntime => { return { - execute: async (_context: CommandContext) => { - await CommandService.instance().execute('switchProfile', 2); + execute: async (context: CommandContext) => { + await CommandService.instance().execute('switchProfile', profileIdByIndex(context.state.profileConfig, 2)); }, }; }; diff --git a/packages/app-desktop/gui/MainScreen/commands/addProfile.ts b/packages/app-desktop/gui/MainScreen/commands/addProfile.ts index acf2049b6..75e0b98b1 100644 --- a/packages/app-desktop/gui/MainScreen/commands/addProfile.ts +++ b/packages/app-desktop/gui/MainScreen/commands/addProfile.ts @@ -19,8 +19,8 @@ export const runtime = (comp: any): CommandRuntime => { value: '', onClose: async (answer: string) => { if (answer) { - const newConfig = await createNewProfile(context.state.profileConfig, answer); - newConfig.currentProfile = newConfig.profiles.length - 1; + const { newConfig, newProfile } = createNewProfile(context.state.profileConfig, answer); + newConfig.currentProfileId = newProfile.id; await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig); bridge().restart(); } diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 261d47058..81444c93b 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -86,14 +86,14 @@ const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: an menuItem = { label: profile.name, click: () => { - void CommandService.instance().execute('switchProfile', i); + void CommandService.instance().execute('switchProfile', profile.id); }, }; } menuItem.label = profile.name; menuItem.type = 'checkbox'; - menuItem.checked = profileConfig.currentProfile === i; + menuItem.checked = profileConfig.currentProfileId === profile.id; switchProfileMenuItems.push(menuItem); } diff --git a/packages/lib/models/Setting.test.ts b/packages/lib/models/Setting.test.ts index 7c9d37871..a282e1936 100644 --- a/packages/lib/models/Setting.test.ts +++ b/packages/lib/models/Setting.test.ts @@ -15,8 +15,9 @@ const switchToSubProfileSettings = async () => { const rootProfileDir = Setting.value('profileDir'); const profileConfigPath = `${rootProfileDir}/profiles.json`; let profileConfig = defaultProfileConfig(); - profileConfig = createNewProfile(profileConfig, 'Sub-profile'); - profileConfig.currentProfile = 1; + const { newConfig, newProfile } = createNewProfile(profileConfig, 'Sub-profile'); + profileConfig = newConfig; + profileConfig.currentProfileId = newProfile.id; await saveProfileConfig(profileConfigPath, profileConfig); const { profileDir } = await initProfile(rootProfileDir); await mkdirp(profileDir); @@ -272,7 +273,7 @@ describe('models/Setting', function() { expect(Setting.value('style.editor.contentMaxWidth')).toBe(600); // Changed })); - it('should load sub-profile settings', async () => { + it('should load sub-profile settings - 1', async () => { await Setting.reset(); Setting.setValue('locale', 'fr_FR'); // Global setting @@ -293,7 +294,7 @@ describe('models/Setting', function() { expect((await Setting.loadOne('sync.target')).value).toBe(undefined); }); - it('should save sub-profile settings', async () => { + it('should save sub-profile settings - 2', async () => { await Setting.reset(); Setting.setValue('locale', 'fr_FR'); // Global setting Setting.setValue('theme', Setting.THEME_DARK); // Global setting diff --git a/packages/lib/services/profileConfig/index.test.ts b/packages/lib/services/profileConfig/index.test.ts index c0c1c0a29..593004760 100644 --- a/packages/lib/services/profileConfig/index.test.ts +++ b/packages/lib/services/profileConfig/index.test.ts @@ -1,7 +1,7 @@ import { writeFile } from 'fs-extra'; -import { createNewProfile, getProfileFullPath, loadProfileConfig, saveProfileConfig } from '.'; +import { createNewProfile, getProfileFullPath, loadProfileConfig, migrateProfileConfig, saveProfileConfig } from '.'; import { tempFilePath } from '../../testing/test-utils'; -import { defaultProfile, defaultProfileConfig, ProfileConfig } from './types'; +import { CurrentProfileVersion, defaultProfile, defaultProfileConfig, DefaultProfileId, Profile, ProfileConfig } from './types'; describe('profileConfig/index', () => { @@ -17,7 +17,7 @@ describe('profileConfig/index', () => { profiles: [ { name: 'Testing', - path: '.', + id: DefaultProfileId, }, ], }; @@ -26,12 +26,12 @@ describe('profileConfig/index', () => { const loadedConfig = await loadProfileConfig(filePath); const expected: ProfileConfig = { - version: 1, - currentProfile: 0, + version: CurrentProfileVersion, + currentProfileId: DefaultProfileId, profiles: [ { name: 'Testing', - path: '.', + id: DefaultProfileId, }, ], }; @@ -50,36 +50,71 @@ describe('profileConfig/index', () => { }); it('should get a profile full path', async () => { - const profile1 = { + const profile1: Profile = { ...defaultProfile(), - path: 'profile-abcd', + id: 'abcd', }; - const profile2 = { + const profile2: Profile = { ...defaultProfile(), - path: '.', - }; - - const profile3 = { - ...defaultProfile(), - path: 'profiles/pro/', + id: DefaultProfileId, }; expect(getProfileFullPath(profile1, '/test/root')).toBe('/test/root/profile-abcd'); expect(getProfileFullPath(profile2, '/test/root')).toBe('/test/root'); - expect(getProfileFullPath(profile3, '/test/root')).toBe('/test/root/profiles/pro'); }); it('should create a new profile', async () => { let config = defaultProfileConfig(); - config = createNewProfile(config, 'new profile 1'); - config = createNewProfile(config, 'new profile 2'); + const r1 = createNewProfile(config, 'new profile 1'); + const r2 = createNewProfile(r1.newConfig, 'new profile 2'); + config = r2.newConfig; expect(config.profiles.length).toBe(3); expect(config.profiles[1].name).toBe('new profile 1'); expect(config.profiles[2].name).toBe('new profile 2'); - expect(config.profiles[1].path).not.toBe(config.profiles[2].path); + expect(config.profiles[1].id).not.toBe(config.profiles[2].id); + }); + + it('should migrate profile config - version 1 to 2', async () => { + const migrated1 = migrateProfileConfig({ + 'version': 1, + 'currentProfile': 2, + 'profiles': [ + { + 'name': 'Default', + 'path': '.', + }, + { + 'name': 'sub1', + 'path': 'profile-sjn25kuh', + }, + { + 'name': 'sub2', + 'path': 'profile-yufzkns3', + }, + ], + }, 2); + + expect(migrated1).toEqual({ + 'version': 2, + 'currentProfileId': 'yufzkns3', + 'profiles': [ + { + 'name': 'Default', + 'id': 'default', + }, + { + 'name': 'sub1', + 'id': 'sjn25kuh', + }, + { + 'name': 'sub2', + 'id': 'yufzkns3', + }, + ], + }); }); }); diff --git a/packages/lib/services/profileConfig/index.ts b/packages/lib/services/profileConfig/index.ts index b3b8339e8..9ad9a5b9d 100644 --- a/packages/lib/services/profileConfig/index.ts +++ b/packages/lib/services/profileConfig/index.ts @@ -1,8 +1,34 @@ -import { rtrimSlashes, trimSlashes } from '../../path-utils'; +import { rtrimSlashes } from '../../path-utils'; import shim from '../../shim'; -import { defaultProfile, defaultProfileConfig, Profile, ProfileConfig } from './types'; +import { CurrentProfileVersion, defaultProfile, defaultProfileConfig, DefaultProfileId, Profile, ProfileConfig } from './types'; import { customAlphabet } from 'nanoid/non-secure'; +export const migrateProfileConfig = (profileConfig: any, toVersion: number): ProfileConfig => { + let version = 2; + + while (profileConfig.version < toVersion) { + if (profileConfig.version === 1) { + for (const profile of profileConfig.profiles) { + if (profile.path === '.') { + profile.id = DefaultProfileId; + } else { + profile.id = profile.path.split('-').pop(); + } + delete profile.path; + } + + const currentProfile = profileConfig.profiles[profileConfig.currentProfile]; + profileConfig.currentProfileId = currentProfile.id; + delete profileConfig.currentProfile; + } + + profileConfig.version = version; + version++; + } + + return profileConfig; +}; + export const loadProfileConfig = async (profileConfigPath: string): Promise => { if (!(await shim.fsDriver().exists(profileConfigPath))) { return defaultProfileConfig(); @@ -10,9 +36,11 @@ export const loadProfileConfig = async (profileConfigPath: string): Promise= output.profiles.length) throw new Error(`Profile index out of range: ${output.currentProfile}`); + if (!output.profiles.find(p => p.id === output.currentProfileId)) throw new Error(`Current profile ID is invalid: ${output.currentProfileId}`); return output; } catch (error) { error.message = `Could not parse profile configuration: ${profileConfigPath}: ${error.message}`; @@ -38,27 +66,39 @@ export const saveProfileConfig = async (profileConfigPath: string, config: Profi }; export const getCurrentProfile = (config: ProfileConfig): Profile => { - return { ...config.profiles[config.currentProfile] }; + return config.profiles.find(p => p.id === config.currentProfileId); }; export const getProfileFullPath = (profile: Profile, rootProfilePath: string): string => { - let p = trimSlashes(profile.path); - if (p === '.') p = ''; - return rtrimSlashes(`${rtrimSlashes(rootProfilePath)}/${p}`); + const folderName = profile.id === DefaultProfileId ? '' : `/profile-${profile.id}`; + return `${rtrimSlashes(rootProfilePath)}${folderName}`; +}; + +export const isSubProfile = (profile: Profile): boolean => { + return profile.id !== DefaultProfileId; }; const profileIdGenerator = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8); export const createNewProfile = (config: ProfileConfig, profileName: string) => { - const newConfig = { + const newConfig: ProfileConfig = { ...config, profiles: config.profiles.slice(), }; - newConfig.profiles.push({ + const newProfile: Profile = { name: profileName, - path: `profile-${profileIdGenerator()}`, - }); + id: profileIdGenerator(), + }; - return newConfig; + newConfig.profiles.push(newProfile); + + return { + newConfig: newConfig, + newProfile: newProfile, + }; +}; + +export const profileIdByIndex = (config: ProfileConfig, index: number): string => { + return config.profiles[index].id; }; diff --git a/packages/lib/services/profileConfig/initProfile.ts b/packages/lib/services/profileConfig/initProfile.ts index 3609cf879..e00203de6 100644 --- a/packages/lib/services/profileConfig/initProfile.ts +++ b/packages/lib/services/profileConfig/initProfile.ts @@ -1,16 +1,16 @@ -import { getCurrentProfile, getProfileFullPath, loadProfileConfig } from '.'; +import { getCurrentProfile, getProfileFullPath, isSubProfile, loadProfileConfig } from '.'; import Setting from '../../models/Setting'; export default async (rootProfileDir: string) => { const profileConfig = await loadProfileConfig(`${rootProfileDir}/profiles.json`); const profileDir = getProfileFullPath(getCurrentProfile(profileConfig), rootProfileDir); - const isSubProfile = profileConfig.currentProfile !== 0; - Setting.setConstant('isSubProfile', isSubProfile); + const isSub = isSubProfile(getCurrentProfile(profileConfig)); + Setting.setConstant('isSubProfile', isSub); Setting.setConstant('rootProfileDir', rootProfileDir); Setting.setConstant('profileDir', profileDir); return { profileConfig, profileDir, - isSubProfile, + isSubProfile: isSub, }; }; diff --git a/packages/lib/services/profileConfig/types.ts b/packages/lib/services/profileConfig/types.ts index d78b44b6a..ea7463ff7 100644 --- a/packages/lib/services/profileConfig/types.ts +++ b/packages/lib/services/profileConfig/types.ts @@ -1,25 +1,28 @@ +export const DefaultProfileId = 'default'; +export const CurrentProfileVersion = 2; + export interface Profile { name: string; - path: string; + id: string; } export interface ProfileConfig { version: number; - currentProfile: number; + currentProfileId: string; profiles: Profile[]; } export const defaultProfile = (): Profile => { return { name: 'Default', - path: '.', + id: DefaultProfileId, }; }; export const defaultProfileConfig = (): ProfileConfig => { return { - version: 1, - currentProfile: 0, + version: CurrentProfileVersion, + currentProfileId: DefaultProfileId, profiles: [defaultProfile()], }; };