diff --git a/src/Contexts.tsx b/src/Contexts.tsx index 2319caf..ea0254a 100644 --- a/src/Contexts.tsx +++ b/src/Contexts.tsx @@ -1,5 +1,6 @@ -import { createContext, useContext, useState } from 'react'; +import { createContext, useContext, useEffect, useState } from 'react'; import { ThemeProvider } from 'styled-components'; +import { Configuration } from './configuration/type'; import { DEFAULT_THEME, THEME_LIST } from './theme'; export interface ConnectToFunc { @@ -56,9 +57,11 @@ export function ThemeContextProvider({ }: { children: React.ReactNode; }): React.ReactElement { - const [themeName, setThemeName] = useState(DEFAULT_THEME.name); + const config = useConfiguration(); + const [themeName, setThemeName] = useState(config.theme); const changeTheme = (newTheme: string) => { + window.config.changeTheme(newTheme); setThemeName(newTheme); }; @@ -70,3 +73,46 @@ export function ThemeContextProvider({ ); } + +const ConfigurationContext = createContext(null); + +export function ConfigurationContextProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [configuration, setConfiguration] = useState( + null + ); + + useEffect(() => { + window.config.getConfiguration().then((c) => { + console.log(c); + setConfiguration(c); + }); + }, []); + + console.log(configuration); + + if (!configuration) { + return null; + } + + return ( + + {children} + + ); +} + +export function useConfiguration(): Configuration { + const value = useContext(ConfigurationContext); + + if (!value) { + throw new Error( + 'useConfiguration must be used within a ConfigurationContextProvider' + ); + } + + return value; +} diff --git a/src/component/Connection/ConnectionForm.tsx b/src/component/Connection/ConnectionForm.tsx index e0cbe93..0017596 100644 --- a/src/component/Connection/ConnectionForm.tsx +++ b/src/component/Connection/ConnectionForm.tsx @@ -1,5 +1,4 @@ import { ConnectionContext, ConnectToFunc } from '../../Contexts'; -import connections from './SavedConnections'; import { ConnectionObject } from './types'; import { ChangeEvent, FormEvent, PureComponent, useContext } from 'react'; @@ -53,7 +52,7 @@ class ConnectionForm extends PureComponent< event.preventDefault(); if (save) { - connections.save(connection.name, connection); + window.config.addConnectionToConfig(connection); } this.props.connectTo(connection); diff --git a/src/component/Connection/ConnectionPage.tsx b/src/component/Connection/ConnectionPage.tsx index 87b854f..fc9c400 100644 --- a/src/component/Connection/ConnectionPage.tsx +++ b/src/component/Connection/ConnectionPage.tsx @@ -1,27 +1,10 @@ import { Link, Navigate } from 'react-router-dom'; -import connections from './SavedConnections'; import { ConnectionObject } from './types'; -import { ConnectionContext } from '../../Contexts'; -import { useContext, useEffect, useState } from 'react'; - -function useRegisteredConnectionList(): null | Record< - string, - ConnectionObject -> { - const [registeredConnections, setRegisteredConnections] = - useState>(null); - - useEffect(() => { - connections - .listConnections() - .then((data) => setRegisteredConnections(data ?? {})); - }, []); - - return registeredConnections; -} +import { ConnectionContext, useConfiguration } from '../../Contexts'; +import { useContext } from 'react'; function ConnectionPage() { - const registeredConnectionList = useRegisteredConnectionList(); + const registeredConnectionList = useConfiguration().connections; const { connectTo } = useContext(ConnectionContext); if (registeredConnectionList === null) { diff --git a/src/component/Connection/SavedConnections.ts b/src/component/Connection/SavedConnections.ts deleted file mode 100644 index c7361fb..0000000 --- a/src/component/Connection/SavedConnections.ts +++ /dev/null @@ -1,37 +0,0 @@ -// import Store from 'electron-store'; -import { ConnectionObject } from './types'; -// import fs from 'node:fs'; -// import path from 'node:path'; - -// Define the path to the JSON file -// const dataFilePath = path.resolve(__dirname, 'data.json'); - -// Usage -// writeDataToFile({ key: 'value' }); -// console.log(readDataFromFile()); - -// type StoreType = { -// [key: string]: ConnectionObject; -// }; - -// TODO need a JSON schema validator here ! -// const storage = new Store({ -// accessPropertiesByDotNotation: false, // remove this to handle "folder-like" -// name: 'connections', -// }); - -// export default storage; -export default { - listConnections: async (): Promise> => { - const r = await window.config.readConfigurationFile(); - - return r?.connections ?? null; - }, - - save: async (name: string, connection: ConnectionObject): Promise => { - await window.config.addConnectionToConfig(name, connection); - }, -}; diff --git a/src/component/Connection/index.ts b/src/component/Connection/index.ts new file mode 100644 index 0000000..0c75d7a --- /dev/null +++ b/src/component/Connection/index.ts @@ -0,0 +1,7 @@ +export type ConnectionObject = { + name: string; + host: string; + port: number; + user: string; + password: string; +}; diff --git a/src/component/ThemeSelector.tsx b/src/component/ThemeSelector.tsx index 5c5f334..8f87dcb 100644 --- a/src/component/ThemeSelector.tsx +++ b/src/component/ThemeSelector.tsx @@ -1,5 +1,5 @@ import { THEME_LIST } from '../../src/theme'; -import { useTheme } from '..//Contexts'; +import { useTheme } from '../Contexts'; export default function ThemeSelector() { const { themeName, changeTheme } = useTheme(); diff --git a/src/configuration.test.ts b/src/configuration/index.test.ts similarity index 83% rename from src/configuration.test.ts rename to src/configuration/index.test.ts index 405aea6..6eae95f 100644 --- a/src/configuration.test.ts +++ b/src/configuration/index.test.ts @@ -1,7 +1,13 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import { existsSync, mkdirSync, readFileSync, writeFile } from 'node:fs'; -import envPaths from 'env-paths'; -import { addConnectionToConfig, readConfigurationFile } from './configuration'; +import { + addConnectionToConfig, + getConfiguration, + changeTheme, + testables, +} from '.'; + +const { getBaseConfig } = testables; vi.mock('env-paths', () => ({ default: () => ({ config: 'config' }), @@ -37,21 +43,21 @@ describe('read configuration from file', () => { test('empty file', () => { mockExistsSync.mockReturnValue(false); - expect(readConfigurationFile()).toBe(null); + expect(getConfiguration()).toStrictEqual(getBaseConfig()); }); test('existing file but empty', () => { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue(''); - expect(readConfigurationFile()).toBe(null); + expect(getConfiguration()).toStrictEqual(getBaseConfig()); }); test('existing file without connexion key', () => { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue('{}'); - expect(readConfigurationFile()).toStrictEqual({ + expect(getConfiguration()).toStrictEqual({ connections: {}, }); }); @@ -60,7 +66,7 @@ describe('read configuration from file', () => { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue('{ "version": 1, "connections": {}}'); - expect(readConfigurationFile()).toStrictEqual({ + expect(getConfiguration()).toStrictEqual({ version: 1, connections: {}, }); @@ -88,7 +94,7 @@ describe('read configuration from file', () => { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue(JSON.stringify(config)); - expect(readConfigurationFile()).toStrictEqual({ + expect(getConfiguration()).toStrictEqual({ version: 1, connections: { local: { @@ -115,7 +121,7 @@ describe('add connection to config', () => { test('empty file', async () => { mockExistsSync.mockReturnValue(false); - await addConnectionToConfig({} as any, 'local', { + await addConnectionToConfig({ name: 'local', host: 'localhost', port: 3306, @@ -128,6 +134,7 @@ describe('add connection to config', () => { JSON.stringify( { version: 1, + theme: 'Dracula', connections: { local: { name: 'local', @@ -168,7 +175,7 @@ describe('add connection to config', () => { mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue(JSON.stringify(config)); - await addConnectionToConfig({} as any, 'test', { + await addConnectionToConfig({ name: 'test', host: 'test', port: 3306, @@ -211,3 +218,30 @@ describe('add connection to config', () => { ); }); }); + +describe('set theme', () => { + afterEach(() => { + vi.resetModules(); + }); + + test('set theme', async () => { + mockExistsSync.mockReturnValue(false); + + await changeTheme('test'); + + expect(mockWriteFile).toHaveBeenCalledWith( + 'config/config.json', + JSON.stringify( + { + version: 1, + theme: 'test', + connections: {}, + }, + null, + 2 + ), + 'utf-8', + expect.any(Function) + ); + }); +}); diff --git a/src/configuration.ts b/src/configuration/index.ts similarity index 63% rename from src/configuration.ts rename to src/configuration/index.ts index c724571..ccbb2cc 100644 --- a/src/configuration.ts +++ b/src/configuration/index.ts @@ -2,24 +2,27 @@ import { dialog, safeStorage } from 'electron'; import { resolve } from 'node:path'; import { existsSync, mkdirSync, readFileSync, writeFile } from 'node:fs'; import envPaths from 'env-paths'; -import { ConnectionObject } from './component/Connection/types'; - -export type Configuration = { - version: 1; - connections: Record; -}; - -type EncryptedConnectionObject = { - password: string; -} & Omit; - -type EncryptedConfiguration = { - connections: Record; -} & Omit; +import { ConnectionObject } from '../component/Connection/types'; +import { DEFAULT_THEME } from '../theme'; +import { + Configuration, + EncryptedConnectionObject, + EncryptedConfiguration, +} from './type'; const envPath = envPaths('TianaTables', { suffix: '' }); const dataFilePath = resolve(envPath.config, 'config.json'); +console.log('Configuration file path:', dataFilePath); + +function getBaseConfig(): Configuration { + return { + version: 1, + theme: DEFAULT_THEME.name, + connections: {}, + }; +} + function encryptConnection( connection: ConnectionObject ): EncryptedConnectionObject { @@ -40,15 +43,15 @@ function decryptConnection( }; } -export function readConfigurationFile(): null | Configuration { +export function getConfiguration(): Configuration { if (!existsSync(dataFilePath)) { - return null; + return getBaseConfig(); } const dataString = readFileSync(dataFilePath, 'utf-8'); if (!dataString) { - return null; + return getBaseConfig(); } const config = JSON.parse(dataString) as EncryptedConfiguration; @@ -63,26 +66,8 @@ export function readConfigurationFile(): null | Configuration { ), }; } -export function addConnectionToConfig( - event: Electron.IpcMainInvokeEvent, - name: string, - connection: ConnectionObject -): void { - let config = readConfigurationFile(); - - if (!config) { - config = { - version: 1, - connections: {}, - }; - } - - if (!config.connections) { - config.connections = {}; - } - - config.connections[name] = connection; +function writeConfiguration(config: Configuration): void { const encryptedConfig = { ...config, connections: Object.fromEntries( @@ -105,3 +90,24 @@ export function addConnectionToConfig( } ); } + +export function addConnectionToConfig(connection: ConnectionObject): void { + const config = getConfiguration() ?? getBaseConfig(); + + if (!config.connections) { + config.connections = {}; + } + + config.connections[connection.name] = connection; + + writeConfiguration(config); +} + +export function changeTheme(theme: string): void { + const config = getConfiguration() ?? getBaseConfig(); + config.theme = theme; + + writeConfiguration(config); +} + +export const testables = { getBaseConfig }; diff --git a/src/configuration/type.ts b/src/configuration/type.ts new file mode 100644 index 0000000..57d2612 --- /dev/null +++ b/src/configuration/type.ts @@ -0,0 +1,15 @@ +import { ConnectionObject } from '../component/Connection/types'; + +export type Configuration = { + version: 1; + theme: string; + connections: Record; +}; + +export type EncryptedConnectionObject = { + password: string; +} & Omit; + +export type EncryptedConfiguration = { + connections: Record; +} & Omit; diff --git a/src/main.ts b/src/main.ts index 3c626af..e452e85 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,13 +4,20 @@ import installExtension, { REACT_DEVELOPER_TOOLS, } from 'electron-devtools-installer'; import connectionStackInstance from './sql'; -import { readConfigurationFile, addConnectionToConfig } from './configuration'; +import { + getConfiguration, + addConnectionToConfig, + changeTheme, +} from './configuration'; +import { ConnectionObject } from './component/Connection'; // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (require('electron-squirrel-startup')) { app.quit(); } +const isMac = process.platform !== 'darwin'; + const createWindow = () => { // Create the browser window. const mainWindow = new BrowserWindow({ @@ -44,8 +51,15 @@ app.whenReady().then(() => { .then((name) => console.log(`Added Extension: ${name}`)) .catch((err) => console.log('An error occurred: ', err)); - ipcMain.handle('config:read', readConfigurationFile); - ipcMain.handle('config:connection:add', addConnectionToConfig); + ipcMain.handle('config:get', getConfiguration); + ipcMain.handle( + 'config:connection:add', + (event: unknown, connection: ConnectionObject) => + addConnectionToConfig(connection) + ); + ipcMain.handle('config:theme:change', (event: unknown, name: string) => + changeTheme(name) + ); connectionStackInstance.bindIpcMain(ipcMain); @@ -59,7 +73,7 @@ app.whenReady().then(() => { // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { + if (isMac) { app.quit(); } }); diff --git a/src/preload.ts b/src/preload.ts index 7d4361c..ad67093 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -3,23 +3,24 @@ import { Connection } from 'mysql2/promise'; import { ConnectionObject } from './component/Connection/types'; -import { Configuration } from './configuration'; +import { Configuration } from './configuration/type'; import { contextBridge, ipcRenderer } from 'electron'; // === Configuration === interface Config { - readConfigurationFile(): Promise; + getConfiguration(): Promise; - addConnectionToConfig( - name: string, - connection: ConnectionObject - ): Promise; + addConnectionToConfig(connection: ConnectionObject): Promise; + + changeTheme(theme: string): void; } const config: Config = { - readConfigurationFile: () => ipcRenderer.invoke('config:read'), - addConnectionToConfig: (name: string, connection: ConnectionObject) => - ipcRenderer.invoke('config:connection:add', name, connection), + getConfiguration: () => ipcRenderer.invoke('config:get'), + addConnectionToConfig: (connection: ConnectionObject) => + ipcRenderer.invoke('config:connection:add', connection), + changeTheme: (theme: string) => + ipcRenderer.invoke('config:theme:change', theme), }; contextBridge.exposeInMainWorld('config', config); diff --git a/src/routes/root.tsx b/src/routes/root.tsx index a1d89d2..cb90941 100644 --- a/src/routes/root.tsx +++ b/src/routes/root.tsx @@ -1,6 +1,9 @@ import { Outlet } from 'react-router'; import ConnectionStack from '../component/Connection/ConnectionStack'; -import { ThemeContextProvider } from '../Contexts'; +import { + ConfigurationContextProvider, + ThemeContextProvider, +} from '../Contexts'; import styled from 'styled-components'; import { getSetting } from '../theme'; import Debug from '../component/Debug'; @@ -23,21 +26,23 @@ const HeaderDiv = styled.header` export default function Root() { return ( - - - - + + + + + - -

Welcome to Tiana Tables !

-
- Theme: - -
-
- -
-
-
+ +

Welcome to Tiana Tables !

+
+ Theme: + +
+
+ +
+
+
+ ); }