diff --git a/frontend/src/components/editor/Editor.tsx b/frontend/src/components/editor/Editor.tsx index 82bf2ca7..4008f44b 100644 --- a/frontend/src/components/editor/Editor.tsx +++ b/frontend/src/components/editor/Editor.tsx @@ -14,7 +14,7 @@ import { useFormatUtils } from "../../hooks/useFormatUtils"; import { useToolBar } from "../../hooks/useToolBar"; import { CodeKeyType, selectConfig } from "../../store/configSlice"; import { selectEditor, setCmView } from "../../store/editorSlice"; -import { selectSetting } from "../../store/settingSlice"; +import { selectFeatureSetting } from "../../store/featureSettingSlice"; import { selectWorkspace } from "../../store/workspaceSlice"; import { imageUploader } from "../../utils/imageUploader"; import { intelligencePivot } from "../../utils/intelligence/intelligencePivot"; @@ -34,7 +34,7 @@ function Editor(props: EditorProps) { const [element, setElement] = useState(); const editorStore = useSelector(selectEditor); const configStore = useSelector(selectConfig); - const settingStore = useSelector(selectSetting); + const featureSettingStore = useSelector(selectFeatureSetting); const workspaceStore = useSelector(selectWorkspace); const { mutateAsync: createUploadUrl } = useCreateUploadUrlMutation(); const { mutateAsync: uploadFile } = useUploadFileMutation(); @@ -51,7 +51,7 @@ function Editor(props: EditorProps) { !element || !editorStore.doc || !editorStore.client || - typeof settingStore.fileUpload?.enable !== "boolean" + typeof featureSettingStore.fileUpload?.enable !== "boolean" ) { return; } @@ -87,7 +87,7 @@ function Editor(props: EditorProps) { }), yorkieCodeMirror(editorStore.doc, editorStore.client), intelligencePivot, - ...(settingStore.fileUpload.enable + ...(featureSettingStore.fileUpload.enable ? [imageUploader(handleUploadImage, editorStore.doc)] : []), urlHyperlinkInserter(editorStore.doc), @@ -107,7 +107,7 @@ function Editor(props: EditorProps) { configStore.codeKey, themeMode, workspaceStore.data, - settingStore.fileUpload?.enable, + featureSettingStore.fileUpload?.enable, dispatch, createUploadUrl, uploadFile, diff --git a/frontend/src/components/editor/YorkieIntelligence.tsx b/frontend/src/components/editor/YorkieIntelligence.tsx index 26b7c335..2e8a717c 100644 --- a/frontend/src/components/editor/YorkieIntelligence.tsx +++ b/frontend/src/components/editor/YorkieIntelligence.tsx @@ -1,16 +1,16 @@ import { Button, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; +import { useSelector } from "react-redux"; import { INTELLIGENCE_FOOTER_ID } from "../../constants/intelligence"; +import { selectFeatureSetting } from "../../store/featureSettingSlice"; import YorkieIntelligenceFooter from "./YorkieIntelligenceFooter"; -import { useSelector } from "react-redux"; -import { selectSetting } from "../../store/settingSlice"; function YorkieIntelligence() { const [footerOpen, setFooterOpen] = useState(false); const [intelligenceFooterPivot, setIntelligenceFooterPivot] = useState(null); - const { yorkieIntelligence } = useSelector(selectSetting); + const { yorkieIntelligence } = useSelector(selectFeatureSetting); useEffect(() => { // initialize intelligence footer pivot diff --git a/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx b/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx index 8ac46502..a03149a9 100644 --- a/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx +++ b/frontend/src/components/editor/YorkieIntelligenceFeatureList.tsx @@ -7,10 +7,10 @@ import { Stack, TextField, } from "@mui/material"; -import { useMemo, useState } from "react"; import { matchSorter } from "match-sorter"; -import { selectSetting } from "../../store/settingSlice"; +import { useMemo, useState } from "react"; import { useSelector } from "react-redux"; +import { selectFeatureSetting } from "../../store/featureSettingSlice"; interface YorkieIntelligenceFeatureListProps { onSelectFeature: (feature: string, title: string) => void; @@ -18,13 +18,17 @@ interface YorkieIntelligenceFeatureListProps { function YorkieIntelligenceFeatureList(props: YorkieIntelligenceFeatureListProps) { const { onSelectFeature } = props; - const settingStore = useSelector(selectSetting); + const featureSettingStore = useSelector(selectFeatureSetting); const [featureText, setFeatureText] = useState(""); const filteredFeatureInfoList = useMemo(() => { - return matchSorter(settingStore.yorkieIntelligence?.config.features ?? [], featureText, { - keys: ["title", "feature"], - }); - }, [featureText, settingStore.yorkieIntelligence?.config.features]); + return matchSorter( + featureSettingStore.yorkieIntelligence?.config.features ?? [], + featureText, + { + keys: ["title", "feature"], + } + ); + }, [featureText, featureSettingStore.yorkieIntelligence?.config.features]); const handleFeatureTextChange: React.ChangeEventHandler< HTMLInputElement | HTMLTextAreaElement diff --git a/frontend/src/components/headers/DocumentHeader.tsx b/frontend/src/components/headers/DocumentHeader.tsx index e6a4374f..aae5a4e4 100644 --- a/frontend/src/components/headers/DocumentHeader.tsx +++ b/frontend/src/components/headers/DocumentHeader.tsx @@ -1,9 +1,11 @@ import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; import EditIcon from "@mui/icons-material/Edit"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; import VerticalSplitIcon from "@mui/icons-material/VerticalSplit"; import VisibilityIcon from "@mui/icons-material/Visibility"; import { AppBar, + Grid2 as Grid, IconButton, Paper, Stack, @@ -11,24 +13,22 @@ import { ToggleButtonGroup, Toolbar, Tooltip, - Grid2 as Grid, Typography, } from "@mui/material"; +import { useSnackbar } from "notistack"; import { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; +import { useUpdateDocumentTitleMutation } from "../../hooks/api/workspaceDocument"; import { useUserPresence } from "../../hooks/useUserPresence"; +import { selectDocument } from "../../store/documentSlice"; import { EditorModeType, selectEditor, setMode } from "../../store/editorSlice"; import { selectWorkspace } from "../../store/workspaceSlice"; import { ShareRole } from "../../utils/share"; import DownloadMenu from "../common/DownloadMenu"; import ShareButton from "../common/ShareButton"; -import UserPresenceList from "./UserPresenceList"; -import { selectDocument } from "../../store/documentSlice"; -import { useUpdateDocumentTitleMutation } from "../../hooks/api/workspaceDocument"; -import { useSnackbar } from "notistack"; import DocumentPopover from "../popovers/DocumentPopover"; -import MoreVertIcon from "@mui/icons-material/MoreVert"; +import UserPresenceList from "./UserPresenceList"; function DocumentHeader() { const dispatch = useDispatch(); @@ -43,7 +43,7 @@ function DocumentHeader() { ); const isEditingDisabled = Boolean(editorState.shareRole); const { enqueueSnackbar } = useSnackbar(); - const [moreButtonanchorEl, setMoreButtonAnchorEl] = useState(null); + const [moreButtonAnchorEl, setMoreButtonAnchorEl] = useState(null); useEffect(() => { if (editorState.shareRole === ShareRole.READ) { @@ -162,8 +162,8 @@ function DocumentHeader() { diff --git a/frontend/src/hooks/api/settings.ts b/frontend/src/hooks/api/settings.ts index 884d9bf8..0d479116 100644 --- a/frontend/src/hooks/api/settings.ts +++ b/frontend/src/hooks/api/settings.ts @@ -1,9 +1,13 @@ import { useQuery } from "@tanstack/react-query"; import axios from "axios"; -import { GetSettingsResponse } from "./types/settings"; -import { useDispatch, useSelector } from "react-redux"; -import { selectSetting, setFileUpload, setYorkieIntelligence } from "../../store/settingSlice"; import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + selectFeatureSetting, + setFileUpload, + setYorkieIntelligence, +} from "../../store/featureSettingSlice"; +import { GetSettingsResponse } from "./types/settings"; export const generateGetSettingsQueryKey = () => { return ["settings"]; @@ -11,10 +15,12 @@ export const generateGetSettingsQueryKey = () => { export const useGetSettingsQuery = () => { const dispatch = useDispatch(); - const settingStore = useSelector(selectSetting); + const featureSettingStore = useSelector(selectFeatureSetting); const query = useQuery({ queryKey: generateGetSettingsQueryKey(), - enabled: settingStore.yorkieIntelligence === null && settingStore.fileUpload === null, + enabled: + featureSettingStore.yorkieIntelligence === null && + featureSettingStore.fileUpload === null, queryFn: async () => { const res = await axios.get("/settings"); return res.data; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 8245f9de..cf95f0b2 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,21 +1,18 @@ +import { SnackbarProvider } from "notistack"; import React from "react"; import ReactDOM from "react-dom/client"; -import "./index.css"; -import App from "./App"; -import { store } from "./store/store"; +import ReactGA from "react-ga4"; import { Provider } from "react-redux"; import { PersistGate } from "redux-persist/integration/react"; -import { persistStore } from "redux-persist"; -import { SnackbarProvider } from "notistack"; -import ReactGA from "react-ga4"; +import App from "./App"; +import "./index.css"; +import { persistor, store } from "./store/store"; const trackingCode = `${import.meta.env.VITE_APP_GOOGLE_ANALYTICS}`; if (trackingCode) { ReactGA.initialize(trackingCode); } -const persistor = persistStore(store); - ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/frontend/src/store/authSlice.ts b/frontend/src/store/authSlice.ts index aefa66f2..3ca961fd 100644 --- a/frontend/src/store/authSlice.ts +++ b/frontend/src/store/authSlice.ts @@ -35,4 +35,13 @@ export const { setAccessToken, setRefreshToken, logout } = authSlice.actions; export const selectAuth = (state: RootState) => state.auth; -export default authSlice.reducer; +/** + * Manages user authentication state, including login information and tokens. + * + * * This slice handles: + * - `accessToken`: The user's access token for authenticated API requests. + * - `refreshToken`: The user's refresh token for obtaining new access tokens. + */ +const reducer = authSlice.reducer; + +export default reducer; diff --git a/frontend/src/store/configSlice.ts b/frontend/src/store/configSlice.ts index 8f8c67a1..56299bf6 100644 --- a/frontend/src/store/configSlice.ts +++ b/frontend/src/store/configSlice.ts @@ -51,4 +51,15 @@ export const { setTheme, setDrawerOpen, setCodeKeyType, setDisableScrollSync } = export const selectConfig = (state: RootState) => state.config; -export default configSlice.reducer; +/** + * Handles global application settings. + * + * * This slice handles: + * - `theme`: The application theme (default, dark, or light). + * - `drawerOpen`: Whether the application drawer (sidebar) is open. + * - `codeKey`: The preferred keybinding type for code editing (Sublime, Vim, etc.). + * - `disableScrollSync`: A flag to enable or disable scroll synchronization. + */ +const reducer = configSlice.reducer; + +export default reducer; diff --git a/frontend/src/store/documentSlice.ts b/frontend/src/store/documentSlice.ts index d735a374..a828df8b 100644 --- a/frontend/src/store/documentSlice.ts +++ b/frontend/src/store/documentSlice.ts @@ -1,5 +1,5 @@ -import { createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; +import { createSlice } from "@reduxjs/toolkit"; import { RootState } from "./store"; export interface Document { @@ -20,7 +20,7 @@ const initialState: DocumentState = { data: null, }; -export const documentSlice = createSlice({ +const documentSlice = createSlice({ name: "document", initialState, reducers: { @@ -34,4 +34,10 @@ export const { setDocumentData } = documentSlice.actions; export const selectDocument = (state: RootState) => state.document; -export default documentSlice.reducer; +/** + * Handles document management state. + * This slice is designed to manage the currently active document, its metadata, and related state in the application. + */ +const reducer = documentSlice.reducer; + +export default reducer; diff --git a/frontend/src/store/editorSlice.ts b/frontend/src/store/editorSlice.ts index b4652f32..cb55836c 100644 --- a/frontend/src/store/editorSlice.ts +++ b/frontend/src/store/editorSlice.ts @@ -59,4 +59,16 @@ export const { setMode, setShareRole, setDoc, setClient, setCmView } = editorSli export const selectEditor = (state: RootState) => state.editor; -export default editorSlice.reducer; +/** + * Manages the state of the collaborative code editor + * + * * This slice handles: + * - `mode`: The editor's current mode (edit, read, or both). + * - `shareRole`: The user's role in the session (e.g., viewer, editor). + * - `doc`: The Yorkie document for real-time collaboration. + * - `client`: The Yorkie client for syncing data with the server. + * - `cmView`: The CodeMirror editor instance. + */ +const reducer = editorSlice.reducer; + +export default reducer; diff --git a/frontend/src/store/settingSlice.ts b/frontend/src/store/featureSettingSlice.ts similarity index 55% rename from frontend/src/store/settingSlice.ts rename to frontend/src/store/featureSettingSlice.ts index d4754766..c00c7939 100644 --- a/frontend/src/store/settingSlice.ts +++ b/frontend/src/store/featureSettingSlice.ts @@ -17,18 +17,18 @@ interface FileUploadSetting { enable: boolean; } -export interface SettingState { +export interface FeatureSettingState { yorkieIntelligence: YorkieIntelligenceSetting | null; fileUpload: FileUploadSetting | null; } -const initialState: SettingState = { +const initialState: FeatureSettingState = { yorkieIntelligence: null, fileUpload: null, }; -export const settingSlice = createSlice({ - name: "setting", +export const featureSettingSlice = createSlice({ + name: "featureSetting", initialState, reducers: { setYorkieIntelligence: (state, action: PayloadAction) => { @@ -40,8 +40,17 @@ export const settingSlice = createSlice({ }, }); -export const { setYorkieIntelligence, setFileUpload } = settingSlice.actions; +export const { setYorkieIntelligence, setFileUpload } = featureSettingSlice.actions; -export const selectSetting = (state: RootState) => state.setting; +export const selectFeatureSetting = (state: RootState) => state.featureSetting; -export default settingSlice.reducer; +/** + * Manages settings for specific features (e.g., experimental feature toggles). + * + * * This slice handles: + * - `yorkieIntelligence`: Settings for the Yorkie Intelligence feature + * - `fileUpload`: Settings for file upload functionality + */ +const reducer = featureSettingSlice.reducer; + +export default reducer; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 81fb07f7..826e4557 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -1,33 +1,41 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit"; -import editorSlice from "./editorSlice"; -import configSlice from "./configSlice"; -import storage from "redux-persist/lib/storage"; import { persistReducer } from "redux-persist"; +import persistStore from "redux-persist/es/persistStore"; +import storage from "redux-persist/lib/storage"; import authSlice from "./authSlice"; +import configSlice from "./configSlice"; +import documentSlice from "./documentSlice"; +import editorSlice from "./editorSlice"; +import featureSettingSlice from "./featureSettingSlice"; import userSlice from "./userSlice"; import workspaceSlice from "./workspaceSlice"; -import documentSlice from "./documentSlice"; -import settingSlice from "./settingSlice"; -const reducers = combineReducers({ - // Persistence +const persistConfig = { + key: "root", + storage, // Use local storage + whitelist: ["auth", "config"], // Only persis these slices +}; + +const rootReducer = combineReducers({ + /* + * Persistent slices: + * These slices persist their state in local storage and can restore it when the app restarts. + */ auth: authSlice, config: configSlice, - // Volatile + + /** + * Volatile slices: + * These slices only retain their state during a session. Their state is reset when the app restarts. + */ user: userSlice, editor: editorSlice, workspace: workspaceSlice, document: documentSlice, - setting: settingSlice, + featureSetting: featureSettingSlice, }); -const persistConfig = { - key: "root", - storage, // Local Storage - whitelist: ["auth", "config"], -}; - -const persistedReducer = persistReducer(persistConfig, reducers); +const persistedReducer = persistReducer(persistConfig, rootReducer); export const store = configureStore({ reducer: persistedReducer, @@ -48,5 +56,7 @@ export const store = configureStore({ }), }); +export const persistor = persistStore(store); + export type RootState = ReturnType; export type AppDispatch = typeof store.dispatch; diff --git a/frontend/src/store/userSlice.ts b/frontend/src/store/userSlice.ts index a38540ed..4af12c05 100644 --- a/frontend/src/store/userSlice.ts +++ b/frontend/src/store/userSlice.ts @@ -32,4 +32,17 @@ export const { setUserData } = userSlice.actions; export const selectUser = (state: RootState) => state.user; -export default userSlice.reducer; +/** + * Manage user profile and user-specific information. + * + * * This slice handles: + * - Storing user data, including: + * - `id`: Unique identifier for the user. + * - `nickname`: User's nickname, or `null` if not set. + * - `lastWorkspaceSlug`: The last accessed workspace's slug. + * - `updatedAt`: Timestamp of the last user update. + * - `createdAt`: Timestamp of when the user was created. + */ +const reducer = userSlice.reducer; + +export default reducer; diff --git a/frontend/src/store/workspaceSlice.ts b/frontend/src/store/workspaceSlice.ts index 26319cca..0e2beabc 100644 --- a/frontend/src/store/workspaceSlice.ts +++ b/frontend/src/store/workspaceSlice.ts @@ -1,5 +1,5 @@ -import { createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; +import { createSlice } from "@reduxjs/toolkit"; import { RootState } from "./store"; export interface Workspace { @@ -32,4 +32,17 @@ export const { setWorkspaceData } = workspaceSlice.actions; export const selectWorkspace = (state: RootState) => state.workspace; -export default workspaceSlice.reducer; +/** + * Manages workspace-related state. + * + * * This slice handles: + * - `data`: The currently active workspace, including: + * - `id`: Unique identifier for the workspace. + * - `title`: The name of the workspace. + * - `slug`: A URL-friendly identifier for the workspace. + * - `updatedAt`: The timestamp of the last update to the workspace. + * - `createdAt`: The timestamp when the workspace was created. + */ +const reducer = workspaceSlice.reducer; + +export default reducer;