diff --git a/web-client/src/core/doc/settingsReducers.ts b/web-client/src/core/doc/settingsReducers.ts index c77fcf21..7aff1cde 100644 --- a/web-client/src/core/doc/settingsReducers.ts +++ b/web-client/src/core/doc/settingsReducers.ts @@ -1,6 +1,6 @@ //! Reducers for the document settings -import { ReducerDeclWithPayload, withPayload } from "low/store"; +import { withPayload } from "low/store"; import { DocSettingsState, @@ -10,37 +10,35 @@ import { } from "./state"; /// Set the document viewer theme -export const setDocTheme: ReducerDeclWithPayload = - withPayload((state, theme) => { +export const setDocTheme = withPayload( + (state, theme) => { state.theme = theme; - }); + }, +); /// Set whether to sync map view to doc -export const setSyncMapToDoc: ReducerDeclWithPayload< - DocSettingsState, - boolean -> = withPayload((state: DocSettingsState, syncMapToDoc: boolean) => { - state.syncMapToDoc = syncMapToDoc; -}); +export const setSyncMapToDoc = withPayload( + (state, syncMapToDoc) => { + state.syncMapToDoc = syncMapToDoc; + }, +); /// Set whether position should be remembered on close -export const setRememberDocPosition: ReducerDeclWithPayload< - DocSettingsState, - boolean -> = withPayload((state: DocSettingsState, value: boolean) => { - state.rememberDocPosition = value; -}); +export const setRememberDocPosition = withPayload( + (state, value) => { + state.rememberDocPosition = value; + }, +); /// Set whether to force notes to be popups -export const setForcePopupNotes: ReducerDeclWithPayload< - DocSettingsState, - boolean -> = withPayload((state: DocSettingsState, value: boolean) => { - state.forcePopupNotes = value; -}); +export const setForcePopupNotes = withPayload( + (state, value) => { + state.forcePopupNotes = value; + }, +); /// Set key bindings -export const setDocKeyBinding: ReducerDeclWithPayload< +export const setDocKeyBinding = withPayload< DocSettingsState, { /// name of the key binding to set @@ -48,7 +46,7 @@ export const setDocKeyBinding: ReducerDeclWithPayload< /// new value of the key binding value: KeyBinding; } -> = withPayload((state: DocSettingsState, { name, value }) => { +>((state, { name, value }) => { state[name] = value; }); @@ -57,13 +55,13 @@ export const setDocKeyBinding: ReducerDeclWithPayload< type PerDocPayload = { docId: string } & T; /// Set doc initial location -export const setInitialDocLocation: ReducerDeclWithPayload< +export const setInitialDocLocation = withPayload< DocSettingsState, PerDocPayload<{ section: number; line: number; }> -> = withPayload((state: DocSettingsState, { docId, section, line }) => { +>((state, { docId, section, line }) => { if (!state.perDoc[docId]) { state.perDoc[docId] = { ...initialPerDocSettings }; } @@ -72,12 +70,12 @@ export const setInitialDocLocation: ReducerDeclWithPayload< }); /// Set doc excluded diagnostic sources -export const setExcludedDiagnosticSources: ReducerDeclWithPayload< +export const setExcludedDiagnosticSources = withPayload< DocSettingsState, PerDocPayload<{ value: string[]; }> -> = withPayload((state: DocSettingsState, { docId, value }) => { +>((state, { docId, value }) => { if (!state.perDoc[docId]) { state.perDoc[docId] = { ...initialPerDocSettings }; } @@ -85,12 +83,12 @@ export const setExcludedDiagnosticSources: ReducerDeclWithPayload< }); /// Set tags to not split on -export const setExcludedSplitTags: ReducerDeclWithPayload< +export const setExcludedSplitTags = withPayload< DocSettingsState, PerDocPayload<{ value: string[]; }> -> = withPayload((state: DocSettingsState, { docId, value }) => { +>((state, { docId, value }) => { if (!state.perDoc[docId]) { state.perDoc[docId] = { ...initialPerDocSettings }; } diff --git a/web-client/src/core/doc/useDocDiagnostics.ts b/web-client/src/core/doc/useDocDiagnostics.ts index 838960ea..c82a1835 100644 --- a/web-client/src/core/doc/useDocDiagnostics.ts +++ b/web-client/src/core/doc/useDocDiagnostics.ts @@ -57,6 +57,7 @@ export const useDocDiagnostics = (): DiagnosticSection[] => { }); return output; }, [serial]); + /* eslint-enable react-hooks/exhaustive-deps*/ return diagnostics; }; diff --git a/web-client/src/core/doc/useDocSections.ts b/web-client/src/core/doc/useDocSections.ts index 6b82e10d..c334c527 100644 --- a/web-client/src/core/doc/useDocSections.ts +++ b/web-client/src/core/doc/useDocSections.ts @@ -20,4 +20,5 @@ export const useDocSections = (): string[] => { } return document.route.map((section) => section.name); }, [serial]); + /* eslint-enable react-hooks/exhaustive-deps*/ }; diff --git a/web-client/src/core/editor/settingsReducers.ts b/web-client/src/core/editor/settingsReducers.ts index 1ad94657..60897a8c 100644 --- a/web-client/src/core/editor/settingsReducers.ts +++ b/web-client/src/core/editor/settingsReducers.ts @@ -1,38 +1,33 @@ //! Reducers for editor settings -import { ReducerDeclWithPayload, withPayload } from "low/store"; +import { withPayload } from "low/store"; import { EditorSettingsState } from "./state"; /// Set if the file tree is shown -export const setShowFileTree: ReducerDeclWithPayload< - EditorSettingsState, - boolean -> = withPayload((state: EditorSettingsState, showFileTree: boolean) => { - state.showFileTree = showFileTree; -}); +export const setShowFileTree = withPayload( + (state, showFileTree) => { + state.showFileTree = showFileTree; + }, +); /// Set if auto load is enabled -export const setAutoLoadEnabled: ReducerDeclWithPayload< - EditorSettingsState, - boolean -> = withPayload((state: EditorSettingsState, autoLoadEnabled: boolean) => { - state.autoLoadEnabled = autoLoadEnabled; -}); +export const setAutoLoadEnabled = withPayload( + (state, autoLoadEnabled) => { + state.autoLoadEnabled = autoLoadEnabled; + }, +); /// Set if auto save is enabled -export const setAutoSaveEnabled: ReducerDeclWithPayload< - EditorSettingsState, - boolean -> = withPayload((state: EditorSettingsState, autoSaveEnabled: boolean) => { - state.autoSaveEnabled = autoSaveEnabled; -}); +export const setAutoSaveEnabled = withPayload( + (state, autoSaveEnabled) => { + state.autoSaveEnabled = autoSaveEnabled; + }, +); /// Set the time of inactivity after which auto load is disabled -export const setDeactivateAutoLoadAfterMinutes: ReducerDeclWithPayload< +export const setDeactivateAutoLoadAfterMinutes = withPayload< EditorSettingsState, number -> = withPayload( - (state: EditorSettingsState, deactivateAutoLoadAfterMinutes: number) => { - state.deactivateAutoLoadAfterMinutes = deactivateAutoLoadAfterMinutes; - }, -); +>((state, deactivateAutoLoadAfterMinutes) => { + state.deactivateAutoLoadAfterMinutes = deactivateAutoLoadAfterMinutes; +}); diff --git a/web-client/src/core/kernel/Kernel.ts b/web-client/src/core/kernel/Kernel.ts index 35a2bc5a..af984e83 100644 --- a/web-client/src/core/kernel/Kernel.ts +++ b/web-client/src/core/kernel/Kernel.ts @@ -4,6 +4,7 @@ import { AppStore, SettingsState, initStore, + saveSettings, settingsSelector, viewActions, } from "core/store"; @@ -105,13 +106,8 @@ export class Kernel { // persist settings to local storage TODO const unwatchSettings = store.subscribe( watchSettings((newVal: SettingsState, oldVal: SettingsState) => { - // TODO #46: persist settings to local storage - // eslint-disable-next-line no-console - console.log({ - message: "settings changed", - new: newVal, - old: oldVal, - }); + // save settings to local storage + saveSettings(newVal); // switch theme if (newVal.theme !== oldVal.theme) { diff --git a/web-client/src/core/layout/settingsReducers.ts b/web-client/src/core/layout/settingsReducers.ts index 5ac67688..d2ce8898 100644 --- a/web-client/src/core/layout/settingsReducers.ts +++ b/web-client/src/core/layout/settingsReducers.ts @@ -1,6 +1,6 @@ //! Layout store reducers - -import { ReducerDecl, ReducerDeclWithPayload, withPayload } from "low/store"; +import { StageMode } from "core/stage"; +import { ReducerDecl, withPayload } from "low/store"; import { LayoutSettingsState, Layout, WidgetType } from "./state"; import { @@ -10,61 +10,60 @@ import { } from "./utils"; /// Modify the current layout -export const setCurrentLayout: ReducerDeclWithPayload< - LayoutSettingsState, - Layout -> = withPayload((state, layout) => { - if (!isCurrentLayoutDefault(state)) { - state.savedLayouts[state.currentLayout] = fitLayoutToGrid(layout); - } -}); +export const setCurrentLayout = withPayload( + (state, layout) => { + if (!isCurrentLayoutDefault(state)) { + state.savedLayouts[state.currentLayout] = fitLayoutToGrid(layout); + } + }, +); /// Set the toolbar location of the current layout -export const setCurrentLayoutToolbarLocation: ReducerDeclWithPayload< +export const setCurrentLayoutToolbarLocation = withPayload< LayoutSettingsState, WidgetType -> = withPayload((state: LayoutSettingsState, location: WidgetType) => { +>((state, location) => { if (!isCurrentLayoutDefault(state)) { state.savedLayouts[state.currentLayout].toolbar = location; } }); /// Set the toolbar anchor location of the current layout -export const setCurrentLayoutToolbarAnchor: ReducerDeclWithPayload< +export const setCurrentLayoutToolbarAnchor = withPayload< LayoutSettingsState, "top" | "bottom" -> = withPayload((state: LayoutSettingsState, location: "top" | "bottom") => { +>((state, location) => { if (!isCurrentLayoutDefault(state)) { state.savedLayouts[state.currentLayout].toolbarAnchor = location; } }); /// Switch to a layout -export const switchLayout: ReducerDeclWithPayload = - withPayload((state, index) => { +export const switchLayout = withPayload( + (state, index) => { state.currentLayout = index; - }); + }, +); /// Duplicate the current layout and switch to it /// /// If the current layout is the default layout, the actual /// current layout will be duplicated and switched to. -export const duplicateLayout: ReducerDeclWithPayload< - LayoutSettingsState, - "view" | "edit" -> = withPayload((state: LayoutSettingsState, mode: "view" | "edit") => { - if (isCurrentLayoutDefault(state)) { - const layout = getDefaultLayout( - window.innerWidth, - window.innerHeight, - mode, - ); - state.savedLayouts.push(layout); - } else { - state.savedLayouts.push(state.savedLayouts[state.currentLayout]); - } - state.currentLayout = state.savedLayouts.length - 1; -}); +export const duplicateLayout = withPayload( + (state, mode) => { + if (isCurrentLayoutDefault(state)) { + const layout = getDefaultLayout( + window.innerWidth, + window.innerHeight, + mode, + ); + state.savedLayouts.push(layout); + } else { + state.savedLayouts.push(state.savedLayouts[state.currentLayout]); + } + state.currentLayout = state.savedLayouts.length - 1; + }, +); /// Delete current layout and switch to default layout export const deleteCurrentLayout: ReducerDecl = ( diff --git a/web-client/src/core/map/settingsReducers.ts b/web-client/src/core/map/settingsReducers.ts index 7db50497..dfe3c61b 100644 --- a/web-client/src/core/map/settingsReducers.ts +++ b/web-client/src/core/map/settingsReducers.ts @@ -1,131 +1,120 @@ //! map setting state reducers -import { ReducerDeclWithPayload, withPayload } from "low/store"; +import { withPayload } from "low/store"; import { LayerMode, MapSettingsState, SectionMode, VisualSize } from "./state"; /// Set the current line section mode -export const setLineSectionMode: ReducerDeclWithPayload< - MapSettingsState, - SectionMode -> = withPayload((state: MapSettingsState, value: SectionMode) => { - state.lineSectionMode = value; -}); +export const setLineSectionMode = withPayload( + (state, value) => { + state.lineSectionMode = value; + }, +); /// Set the current line layer mode -export const setLineLayerMode: ReducerDeclWithPayload< - MapSettingsState, - LayerMode -> = withPayload((state: MapSettingsState, value: LayerMode) => { - state.lineLayerMode = value; -}); +export const setLineLayerMode = withPayload( + (state, value) => { + state.lineLayerMode = value; + }, +); /// Set if lines not on current layer should fade -export const setFadeNonCurrentLayerLines: ReducerDeclWithPayload< +export const setFadeNonCurrentLayerLines = withPayload< MapSettingsState, boolean -> = withPayload((state: MapSettingsState, value: boolean) => { +>((state, value) => { state.fadeNonCurrentLayerLines = value; }); /// Set the current icon section mode -export const setIconSectionMode: ReducerDeclWithPayload< - MapSettingsState, - SectionMode -> = withPayload((state: MapSettingsState, value: SectionMode) => { - state.iconSectionMode = value; -}); +export const setIconSectionMode = withPayload( + (state, value) => { + state.iconSectionMode = value; + }, +); /// Set the current icon layer mode -export const setIconLayerMode: ReducerDeclWithPayload< - MapSettingsState, - LayerMode -> = withPayload((state: MapSettingsState, value: LayerMode) => { - state.iconLayerMode = value; -}); +export const setIconLayerMode = withPayload( + (state, value) => { + state.iconLayerMode = value; + }, +); /// Set if icons not on current layer should fade -export const setFadeNonCurrentLayerIcons: ReducerDeclWithPayload< +export const setFadeNonCurrentLayerIcons = withPayload< MapSettingsState, boolean -> = withPayload((state: MapSettingsState, value: boolean) => { +>((state, value) => { state.fadeNonCurrentLayerIcons = value; }); /// Set the current marker section mode -export const setMarkerSectionMode: ReducerDeclWithPayload< - MapSettingsState, - SectionMode -> = withPayload((state: MapSettingsState, value: SectionMode) => { - state.markerSectionMode = value; -}); +export const setMarkerSectionMode = withPayload( + (state, value) => { + state.markerSectionMode = value; + }, +); /// Set the current marker layer mode -export const setMarkerLayerMode: ReducerDeclWithPayload< - MapSettingsState, - LayerMode -> = withPayload((state: MapSettingsState, value: LayerMode) => { - state.markerLayerMode = value; -}); +export const setMarkerLayerMode = withPayload( + (state, value) => { + state.markerLayerMode = value; + }, +); /// Set if markers not on current layer should fade -export const setFadeNonCurrentLayerMarkers: ReducerDeclWithPayload< +export const setFadeNonCurrentLayerMarkers = withPayload< MapSettingsState, boolean -> = withPayload((state: MapSettingsState, value: boolean) => { +>((state, value) => { state.fadeNonCurrentLayerMarkers = value; }); /// Set primary icon size -export const setPrimaryIconSize: ReducerDeclWithPayload< - MapSettingsState, - VisualSize -> = withPayload((state: MapSettingsState, value: VisualSize) => { - state.primaryIconSize = value; -}); +export const setPrimaryIconSize = withPayload( + (state, value) => { + state.primaryIconSize = value; + }, +); /// Set secondary icon size -export const setSecondaryIconSize: ReducerDeclWithPayload< - MapSettingsState, - VisualSize -> = withPayload((state: MapSettingsState, value: VisualSize) => { - state.secondaryIconSize = value; -}); +export const setSecondaryIconSize = withPayload( + (state, value) => { + state.secondaryIconSize = value; + }, +); /// Set other icon size -export const setOtherIconSize: ReducerDeclWithPayload< - MapSettingsState, - VisualSize -> = withPayload((state: MapSettingsState, value: VisualSize) => { - state.otherIconSize = value; -}); +export const setOtherIconSize = withPayload( + (state, value) => { + state.otherIconSize = value; + }, +); /// Set line size (thickness) -export const setLineSize: ReducerDeclWithPayload = - withPayload((state: MapSettingsState, value: VisualSize) => { +export const setLineSize = withPayload( + (state, value) => { state.lineSize = value; - }); + }, +); /// Set arrow size -export const setArrowSize: ReducerDeclWithPayload< - MapSettingsState, - VisualSize -> = withPayload((state: MapSettingsState, value: VisualSize) => { - state.arrowSize = value; -}); +export const setArrowSize = withPayload( + (state, value) => { + state.arrowSize = value; + }, +); /// Set arrow frequency -export const setArrowFrequency: ReducerDeclWithPayload< - MapSettingsState, - VisualSize -> = withPayload((state: MapSettingsState, value: VisualSize) => { - state.arrowFrequency = value; -}); +export const setArrowFrequency = withPayload( + (state, value) => { + state.arrowFrequency = value; + }, +); /// Set marker size -export const setMarkerSize: ReducerDeclWithPayload< - MapSettingsState, - VisualSize -> = withPayload((state: MapSettingsState, value: VisualSize) => { - state.markerSize = value; -}); +export const setMarkerSize = withPayload( + (state, value) => { + state.markerSize = value; + }, +); diff --git a/web-client/src/core/stage/state.ts b/web-client/src/core/stage/state.ts index 4cc29bc1..dbb85ede 100644 --- a/web-client/src/core/stage/state.ts +++ b/web-client/src/core/stage/state.ts @@ -12,7 +12,7 @@ export type StageViewState = { export type StageMode = "view" | "edit"; -export type SettingsTab = "map" | "doc" | "editor" | "info"; +export type SettingsTab = "map" | "doc" | "editor" | "meta"; export const initialStageViewState: StageViewState = { stageMode: window.location.pathname.startsWith("/edit") ? "edit" : "view", diff --git a/web-client/src/core/store/settings.ts b/web-client/src/core/store/settings.ts index a25b4efb..49cdf698 100644 --- a/web-client/src/core/store/settings.ts +++ b/web-client/src/core/store/settings.ts @@ -38,12 +38,19 @@ export type SettingsState = LayoutSettingsState & const loadState = (): SettingsState => { const state = localStorage.getItem(LOCAL_STORAGE_KEY); const loadedState = state ? JSON.parse(state) : {}; + return Object.assign(getInitialState(), loadedState); +}; + +export const saveSettings = (state: SettingsState) => { + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state)); +}; + +const getInitialState = (): SettingsState => { return { ...initialLayoutSettingsState, ...initialMapSettingsState, ...initialDocSettingsState, ...initialEditorSettingsState, - ...loadedState, }; }; @@ -57,5 +64,6 @@ export const { settingsReducer, settingsActions, settingsSelector } = ...mapSettingsReducers, ...docSettingsReducers, ...editorSettingsReducers, + resetAllSettings: () => getInitialState(), }, }); diff --git a/web-client/src/low/fetch.ts b/web-client/src/low/fetch.ts index 001809b7..e88cf2e3 100644 --- a/web-client/src/low/fetch.ts +++ b/web-client/src/low/fetch.ts @@ -1,14 +1,34 @@ import { sleep } from "./utils"; -export const fetchAsBytes = async (url: string): Promise => { +export const fetchAsBytes = (url: string): Promise => { + return doFetch(url, async (response) => { + const buffer = await response.arrayBuffer(); + return new Uint8Array(buffer); + }); +}; + +export const fetchAsString = (url: string): Promise => { + return doFetch(url, (response) => { + return response.text(); + }); +}; + +const API_PREFIX = "/api/v1"; +export const getApiUrl = (path: string) => { + return API_PREFIX + path; +}; + +const doFetch = async ( + url: string, + handler: (response: Response) => Promise, +): Promise => { const RETRY_COUNT = 3; let error: unknown; for (let i = 0; i < RETRY_COUNT; i++) { try { const response = await fetch(url); if (response.ok) { - const buffer = await response.arrayBuffer(); - return new Uint8Array(buffer); + return await handler(response); } } catch (e) { console.error(e); diff --git a/web-client/src/low/store.ts b/web-client/src/low/store.ts index 6325da00..97e5a3b4 100644 --- a/web-client/src/low/store.ts +++ b/web-client/src/low/store.ts @@ -44,17 +44,12 @@ export type ReducerDeclWithPayload = ( /// Use this to define a reducer without payload. No need to wrap the function with `withPayload` export type ReducerDecl = (state: Draft) => void; -/// Helper type for inner function of `withPayload` -type ReducerEffect = P extends undefined - ? ReducerDecl - : (state: Draft, payload: P) => void; - /// Convenience function for defining a reducer with payload export const withPayload = ( - effect: ReducerEffect, + effect: (state: Draft, payload: P) => void, ): ReducerDeclWithPayload => { - return (state, action) => { - effect(state, action.payload); + return (state: Draft, action) => { + return effect(state, action.payload); }; }; diff --git a/web-client/src/ui/editor/EditorRoot.tsx b/web-client/src/ui/editor/EditorRoot.tsx index bb44caa8..c6f270f9 100644 --- a/web-client/src/ui/editor/EditorRoot.tsx +++ b/web-client/src/ui/editor/EditorRoot.tsx @@ -29,6 +29,7 @@ export const EditorRoot: React.FC = () => { }, [serial], ); + /* eslint-enable react-hooks/exhaustive-deps*/ return (
diff --git a/web-client/src/ui/editor/tree/EditorTree.tsx b/web-client/src/ui/editor/tree/EditorTree.tsx index fc230a7c..d062ddd7 100644 --- a/web-client/src/ui/editor/tree/EditorTree.tsx +++ b/web-client/src/ui/editor/tree/EditorTree.tsx @@ -99,6 +99,7 @@ const TreeDirNode: React.FC = ({ }; loadEntries(); }, [path.join("/"), isExpanded, listDir]); + /* eslint-enable react-hooks/exhaustive-deps*/ const isLoading = isExpanded && entries === undefined; diff --git a/web-client/src/ui/map/MapRoot.tsx b/web-client/src/ui/map/MapRoot.tsx index ada71a43..e050db62 100644 --- a/web-client/src/ui/map/MapRoot.tsx +++ b/web-client/src/ui/map/MapRoot.tsx @@ -26,6 +26,7 @@ export const MapRoot: React.FC = () => { mapState.current.attach(); } }, [serial, store]); + /* eslint-enable react-hooks/exhaustive-deps*/ if (!document) { return ; diff --git a/web-client/src/ui/toolbar/Header.tsx b/web-client/src/ui/toolbar/Header.tsx index 20203df7..ff0aa44c 100644 --- a/web-client/src/ui/toolbar/Header.tsx +++ b/web-client/src/ui/toolbar/Header.tsx @@ -65,13 +65,15 @@ export const Header: React.FC = ({ toolbarAnchor }) => { {title} - + {headerControls.map((group, i) => ( {group.controls.map((Control, j) => ( > = ({ children, groupId }) => { const groupVisibleState = useIsOverflowGroupVisible(groupId); - if (groupVisibleState !== "hidden") { + if (groupVisibleState !== "visible") { return <>{children}; } return null; diff --git a/web-client/src/ui/toolbar/OpenDocs.tsx b/web-client/src/ui/toolbar/OpenDocs.tsx new file mode 100644 index 00000000..424eefe5 --- /dev/null +++ b/web-client/src/ui/toolbar/OpenDocs.tsx @@ -0,0 +1,43 @@ +//! Control to open documentation page + +import { MenuItem, ToolbarButton, Tooltip } from "@fluentui/react-components"; +import { BookQuestionMark20Regular } from "@fluentui/react-icons"; +import { forwardRef } from "react"; + +import { ToolbarControl } from "./util"; + +export const OpenDocs: ToolbarControl = { + ToolbarButton: forwardRef((_, ref) => { + return ( + + } + onClick={openDocs} + /> + + ); + }), + MenuItem: () => { + return ( + + } + onClick={openDocs} + > + Help + + + ); + }, +}; + +const openDocs = () => { + const a = document.createElement("a"); + a.target = "_blank"; + a.href = "/docs"; + a.click(); +}; diff --git a/web-client/src/ui/toolbar/Settings.tsx b/web-client/src/ui/toolbar/Settings.tsx index 34aa454c..5c877efa 100644 --- a/web-client/src/ui/toolbar/Settings.tsx +++ b/web-client/src/ui/toolbar/Settings.tsx @@ -40,6 +40,7 @@ export const Settings: ToolbarControl = { ); }, + priority: 999, }; /// Internal settings dialog component diff --git a/web-client/src/ui/toolbar/ViewDiagnostics.tsx b/web-client/src/ui/toolbar/ViewDiagnostics.tsx index 5e8191f5..0dad8a03 100644 --- a/web-client/src/ui/toolbar/ViewDiagnostics.tsx +++ b/web-client/src/ui/toolbar/ViewDiagnostics.tsx @@ -47,9 +47,7 @@ export const ViewDiagnostics: ToolbarControl = { ) } - > - {diagnostics.length && diagnostics.length} - + /> ); diff --git a/web-client/src/ui/toolbar/getHeaderControls.ts b/web-client/src/ui/toolbar/getHeaderControls.ts index 2b533067..861155a1 100644 --- a/web-client/src/ui/toolbar/getHeaderControls.ts +++ b/web-client/src/ui/toolbar/getHeaderControls.ts @@ -12,6 +12,7 @@ import { CloseProject } from "./CloseProject"; import { SyncProject } from "./SyncProject"; import { SaveProject } from "./SaveProject"; import { CompileProject } from "./CompileProject"; +import { OpenDocs } from "./OpenDocs"; /// Header controls. /// @@ -45,10 +46,10 @@ export const getHeaderControls = (mode: StageMode): HeaderControlList => { : []), ], }, - // Setting + // Misc { - priority: 99, - controls: [Settings], + priority: 10, + controls: [Settings, OpenDocs], }, ]; }; diff --git a/web-client/src/ui/toolbar/settings/InfoField.tsx b/web-client/src/ui/toolbar/settings/InfoField.tsx new file mode 100644 index 00000000..41c6b2ea --- /dev/null +++ b/web-client/src/ui/toolbar/settings/InfoField.tsx @@ -0,0 +1,19 @@ +//! Component for displaying one row of information with a label and a value + +import { Label, Text } from "@fluentui/react-components"; + +type InfoFieldProps = { + label: string; + value: string; +}; + +export const InfoField: React.FC = ({ label, value }) => { + return ( +
+ +
+ {value} +
+
+ ); +}; diff --git a/web-client/src/ui/toolbar/settings/MetaSettings.tsx b/web-client/src/ui/toolbar/settings/MetaSettings.tsx new file mode 100644 index 00000000..88ffa81c --- /dev/null +++ b/web-client/src/ui/toolbar/settings/MetaSettings.tsx @@ -0,0 +1,71 @@ +//! Meta tab of the settings dialog + +import { Button, Field } from "@fluentui/react-components"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { documentSelector, settingsActions, viewSelector } from "core/store"; +import { fetchAsString, getApiUrl } from "low/fetch"; + +import { useActions } from "low/store"; +import { SettingsSection } from "./SettingsSection"; +import { InfoField } from "./InfoField"; + +declare global { + interface Window { + __CELER_VERSION: string; + } +} + +export const MetaSettings: React.FC = () => { + const { stageMode } = useSelector(viewSelector); + const { document } = useSelector(documentSelector); + const { resetAllSettings } = useActions(settingsActions); + const project = document?.project; + + const [serverVersion, setServerVersion] = useState("Loading..."); + useEffect(() => { + const fetchVersion = async () => { + try { + const version = await fetchAsString(getApiUrl("/version")); + if (version.split(" ", 3).length === 3) { + setServerVersion("Cannot read version"); + } else { + setServerVersion(version); + } + } catch { + setServerVersion("Cannot read version"); + } + }; + fetchVersion(); + }, [stageMode]); + return ( + <> + + + + + + + + + + + + + + + + + ); +}; diff --git a/web-client/src/ui/toolbar/settings/SettingsDialog.css b/web-client/src/ui/toolbar/settings/SettingsDialog.css index 798fe568..7810653c 100644 --- a/web-client/src/ui/toolbar/settings/SettingsDialog.css +++ b/web-client/src/ui/toolbar/settings/SettingsDialog.css @@ -20,7 +20,7 @@ } #settings-separator.horizontal-tabs { - padding: 8px 0; + padding: 16px 0; } #settings-panel { @@ -57,3 +57,21 @@ div.settings-value-slider label { #settings-dialog-root > .fui-DialogBody { height: calc(min(100vh, 800px) - 48px); } + +.settings-info-field { + display: flex; + flex-direction: row; +} + +.settings-info-field > label { + min-width: 160px; +} + +.settings-info-field > div { + flex: 1; + overflow: hidden; +} + +.settings-info-field > div > span { + word-wrap: break-word; +} diff --git a/web-client/src/ui/toolbar/settings/SettingsDialog.tsx b/web-client/src/ui/toolbar/settings/SettingsDialog.tsx index bf0edb40..995e645d 100644 --- a/web-client/src/ui/toolbar/settings/SettingsDialog.tsx +++ b/web-client/src/ui/toolbar/settings/SettingsDialog.tsx @@ -3,13 +3,21 @@ import "./SettingsDialog.css"; import clsx from "clsx"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useSelector } from "react-redux"; -import { TabList, Tab, Divider } from "@fluentui/react-components"; +import { + TabList, + Tab, + Divider, + Dropdown, + Option, + Field, +} from "@fluentui/react-components"; import { Document20Regular, Map20Regular, Code20Regular, + Info20Regular, } from "@fluentui/react-icons"; import { useWindowSize } from "ui/shared"; @@ -20,6 +28,7 @@ import { useActions } from "low/store"; import { MapSettings } from "./MapSettings"; import { DocSettings } from "./DocSettings"; import { EditorSettings } from "./EditorSettings"; +import { MetaSettings } from "./MetaSettings"; type TabData = { id: SettingsTab; @@ -32,7 +41,7 @@ export const SettingsDialog: React.FC = () => { const { windowWidth } = useWindowSize(); const { setEditingKeyBinding, setSettingsTab } = useActions(viewActions); const { stageMode, settingsTab } = useSelector(viewSelector); - const verticalTabs = windowWidth > 400; + const verticalTabs = windowWidth > 480; const tabs: TabData[] = useMemo(() => { return [ @@ -58,29 +67,71 @@ export const SettingsDialog: React.FC = () => { } as const, ] : []), + { + id: "meta", + text: "Meta", + Icon: Info20Regular, + Page: MetaSettings, + }, ] satisfies TabData[]; }, [stageMode]); + const [selectedText, setSelectedText] = useState(""); + + // Refresh tab selection when switching to small screen display + /* eslint-disable react-hooks/exhaustive-deps*/ + useEffect(() => { + if (!verticalTabs) { + const text = tabs.find(({ id }) => id === settingsTab)?.text ?? ""; + setSelectedText(text); + } + }, [verticalTabs]); + /* eslint-enable react-hooks/exhaustive-deps*/ + + const switchTab = (tab: SettingsTab) => { + // cancel editing key binding when switching tabs + setEditingKeyBinding(undefined); + setSettingsTab(tab); + }; + return (
- { - // cancel editing key binding when switching tabs - setEditingKeyBinding(undefined); - setSettingsTab(data.value as SettingsTab); - }} - > - {tabs.map(({ id, text, Icon }) => ( - }> - {text} - - ))} - + {verticalTabs ? ( + { + switchTab(data.value as SettingsTab); + }} + > + {tabs.map(({ id, text, Icon }) => ( + }> + {text} + + ))} + + ) : ( + + { + switchTab(data.selectedOptions[0] as SettingsTab); + setSelectedText(data.optionText ?? ""); + }} + > + {tabs.map(({ id, text, Icon }) => ( + + ))} + + + )}