From 1634958f7fac7b52d5c4e702c2a503b78a63258d Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 9 Oct 2023 12:49:06 -0500 Subject: [PATCH 01/15] Rename project lookup service files to match new naming scheme --- lib/papi-dts/papi.d.ts | 8 ++++---- src/extension-host/extension-host.ts | 2 +- src/extension-host/services/papi-backend.service.ts | 2 +- ...-backend.service.ts => project-lookup.service.host.ts} | 2 +- src/renderer/services/papi-frontend.service.ts | 2 +- .../project-lookup.service.model.ts} | 2 +- src/shared/services/project-lookup.service.ts | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename src/extension-host/services/{project-lookup-backend.service.ts => project-lookup.service.host.ts} (98%) rename src/shared/{models/project-lookup.model.ts => services/project-lookup.service.model.ts} (91%) diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 7f0afb7f95..79de6e65ac 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -2387,7 +2387,7 @@ declare module 'shared/models/project-metadata.model' { projectType: string; }; } -declare module 'shared/models/project-lookup.model' { +declare module 'shared/services/project-lookup.service.model' { import { ProjectMetadata } from 'shared/models/project-metadata.model'; /** * Provides metadata for projects known by the platform @@ -2408,7 +2408,7 @@ declare module 'shared/models/project-lookup.model' { export const projectLookupServiceNetworkObjectName = 'ProjectLookupService'; } declare module 'shared/services/project-lookup.service' { - import { ProjectLookupServiceType } from 'shared/models/project-lookup.model'; + import { ProjectLookupServiceType } from 'shared/services/project-lookup.service.model'; const projectLookupService: ProjectLookupServiceType; export default projectLookupService; } @@ -2832,7 +2832,7 @@ declare module 'papi-frontend' { import { PapiWebViewService } from 'shared/services/web-view.service'; import { InternetService } from 'shared/services/internet.service'; import { DataProviderService } from 'shared/services/data-provider.service'; - import { ProjectLookupServiceType } from 'shared/models/project-lookup.model'; + import { ProjectLookupServiceType } from 'shared/services/project-lookup.service.model'; import { PapiFrontendProjectDataProviderService } from 'shared/services/project-data-provider.service'; import { PapiContext } from 'renderer/context/papi-context/index'; import { PapiHooks } from 'renderer/hooks/papi-hooks/index'; @@ -3162,7 +3162,7 @@ declare module 'papi-backend' { import { DataProviderService } from 'shared/services/data-provider.service'; import { PapiBackendProjectDataProviderService } from 'shared/services/project-data-provider.service'; import { ExtensionStorageService } from 'extension-host/services/extension-storage.service'; - import { ProjectLookupServiceType } from 'shared/models/project-lookup.model'; + import { ProjectLookupServiceType } from 'shared/services/project-lookup.service.model'; const papi: { /** * Event manager - accepts subscriptions to an event and runs the subscription callbacks when the event is emitted diff --git a/src/extension-host/extension-host.ts b/src/extension-host/extension-host.ts index 6c0a0e388f..1af47fd4ca 100644 --- a/src/extension-host/extension-host.ts +++ b/src/extension-host/extension-host.ts @@ -9,7 +9,7 @@ import dataProviderService from '@shared/services/data-provider.service'; import extensionAssetService from '@shared/services/extension-asset.service'; import { getErrorMessage } from '@shared/utils/util'; import { CommandNames } from 'papi-shared-types'; -import { startProjectLookupService } from '@extension-host/services/project-lookup-backend.service'; +import { startProjectLookupService } from '@extension-host/services/project-lookup.service.host'; // #region Test logs diff --git a/src/extension-host/services/papi-backend.service.ts b/src/extension-host/services/papi-backend.service.ts index 5151afcaf3..830b3a6a56 100644 --- a/src/extension-host/services/papi-backend.service.ts +++ b/src/extension-host/services/papi-backend.service.ts @@ -23,7 +23,7 @@ import { import extensionStorageService, { ExtensionStorageService, } from '@extension-host/services/extension-storage.service'; -import { ProjectLookupServiceType } from '@shared/models/project-lookup.model'; +import { ProjectLookupServiceType } from '@shared/services/project-lookup.service.model'; import projectLookupService from '@shared/services/project-lookup.service'; // IMPORTANT NOTES: diff --git a/src/extension-host/services/project-lookup-backend.service.ts b/src/extension-host/services/project-lookup.service.host.ts similarity index 98% rename from src/extension-host/services/project-lookup-backend.service.ts rename to src/extension-host/services/project-lookup.service.host.ts index 428d95c102..52d547de80 100644 --- a/src/extension-host/services/project-lookup-backend.service.ts +++ b/src/extension-host/services/project-lookup.service.host.ts @@ -4,7 +4,7 @@ import { ProjectMetadata } from '@shared/models/project-metadata.model'; import { projectLookupServiceNetworkObjectName, ProjectLookupServiceType, -} from '@shared/models/project-lookup.model'; +} from '@shared/services/project-lookup.service.model'; import { joinUriPaths } from '@node/utils/util'; import logger from '@shared/services/logger.service'; import networkObjectService from '@shared/services/network-object.service'; diff --git a/src/renderer/services/papi-frontend.service.ts b/src/renderer/services/papi-frontend.service.ts index 7e40b06804..7f57a6e1ef 100644 --- a/src/renderer/services/papi-frontend.service.ts +++ b/src/renderer/services/papi-frontend.service.ts @@ -12,7 +12,7 @@ import { papiNetworkService, PapiNetworkService } from '@shared/services/network import { papiWebViewService, PapiWebViewService } from '@shared/services/web-view.service'; import internetService, { InternetService } from '@shared/services/internet.service'; import dataProviderService, { DataProviderService } from '@shared/services/data-provider.service'; -import { ProjectLookupServiceType } from '@shared/models/project-lookup.model'; +import { ProjectLookupServiceType } from '@shared/services/project-lookup.service.model'; import projectLookupService from '@shared/services/project-lookup.service'; import { papiFrontendProjectDataProviderService, diff --git a/src/shared/models/project-lookup.model.ts b/src/shared/services/project-lookup.service.model.ts similarity index 91% rename from src/shared/models/project-lookup.model.ts rename to src/shared/services/project-lookup.service.model.ts index e0e3abe957..37fed7725b 100644 --- a/src/shared/models/project-lookup.model.ts +++ b/src/shared/services/project-lookup.service.model.ts @@ -1,4 +1,4 @@ -import { ProjectMetadata } from './project-metadata.model'; +import { ProjectMetadata } from '../models/project-metadata.model'; /** JSDOC SOURCE projectLookupService * Provides metadata for projects known by the platform diff --git a/src/shared/services/project-lookup.service.ts b/src/shared/services/project-lookup.service.ts index 11ddcd864d..a2025a94db 100644 --- a/src/shared/services/project-lookup.service.ts +++ b/src/shared/services/project-lookup.service.ts @@ -1,7 +1,7 @@ import { projectLookupServiceNetworkObjectName, ProjectLookupServiceType, -} from '@shared/models/project-lookup.model'; +} from '@shared/services/project-lookup.service.model'; import networkObjectService from '@shared/services/network-object.service'; let networkObject: ProjectLookupServiceType; From 1e944c02b933f4520fe0363197fe2def29625762 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Tue, 10 Oct 2023 12:56:23 -0500 Subject: [PATCH 02/15] Started dialog service - getProjects, dialog response, open project -> select project, tab icons Started dialog service; getProjects pulls up dialog but does nothing more Dialog response, open project -> select project, tab icons --- extensions/src/hello-world/assets/offline.svg | 155 ++++++++++++++++++ extensions/src/hello-world/hello-world.ts | 12 ++ .../web-views/hello-world.web-view.tsx | 38 ++++- lib/papi-dts/papi.d.ts | 99 ++++++++++- .../services/papi-backend.service.ts | 4 + .../services/project-lookup.service.host.ts | 11 +- .../paranext-dock-layout.component.test.ts | 19 ++- .../paranext-dock-layout.component.tsx | 86 +++++++--- .../docking/paranext-tab-title.component.tsx | 16 +- .../open-project-tab.component.scss | 2 +- .../open-project-tab.component.tsx | 43 +++-- .../project-list.component.tsx | 10 +- .../components/web-view.component.tsx | 1 + src/renderer/index.tsx | 2 + src/renderer/services/dialog.service.host.ts | 139 ++++++++++++++++ .../services/papi-frontend.service.ts | 4 + src/renderer/testing/test-layout.data.ts | 10 -- src/shared/data/web-view.model.ts | 29 +++- src/shared/models/dialog-options.model.ts | 9 + src/shared/services/dialog.service.model.ts | 19 +++ src/shared/services/dialog.service.ts | 20 +++ src/shared/services/web-view.service.ts | 33 +++- 22 files changed, 693 insertions(+), 68 deletions(-) create mode 100644 extensions/src/hello-world/assets/offline.svg create mode 100644 src/renderer/services/dialog.service.host.ts create mode 100644 src/shared/models/dialog-options.model.ts create mode 100644 src/shared/services/dialog.service.model.ts create mode 100644 src/shared/services/dialog.service.ts diff --git a/extensions/src/hello-world/assets/offline.svg b/extensions/src/hello-world/assets/offline.svg new file mode 100644 index 0000000000..a16275fc8b --- /dev/null +++ b/extensions/src/hello-world/assets/offline.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/src/hello-world/hello-world.ts b/extensions/src/hello-world/hello-world.ts index abc3747c44..7dc93043b0 100644 --- a/extensions/src/hello-world/hello-world.ts +++ b/extensions/src/hello-world/hello-world.ts @@ -52,6 +52,7 @@ const reactWebViewProvider: IWebViewProviderWithType = { ); return { ...savedWebView, + iconUrl: 'papi-extension://hello-world/assets/offline.svg', title: 'Hello World React', content: helloWorldReactWebView, styles: helloWorldReactWebViewStyles, @@ -164,5 +165,16 @@ export async function activate(context: ExecutionActivationContext): Promise(false); + useEffect(() => { + mounted.current = true; + return () => { + mounted.current = false; + }; + }, []); + + const [project, setProject] = useState(); + const [isSettingProject, setIsSettingProject] = useState(false); + + const selectProject = useCallback(async () => { + if (!isSettingProject) { + setIsSettingProject(true); + try { + const projectId = await papi.dialogs.getProject({ + prompt: 'Please select a project for Hello World WebView:', + iconUrl: 'papi-extension://hello-world/assets/offline.svg', + title: 'Select Hello World Project', + }); + logger.log(`RENDERER dialogs.getProject: ${projectId}`); + if (mounted.current) { + setProject(projectId); + setIsSettingProject(false); + } + } catch (e) { + logger.error(`Failed to select project! ${e}`); + if (mounted.current) setIsSettingProject(false); + } + } else logger.log(`Already choosing a project!`); + }, [isSettingProject]); + const [latestVerseText] = useData.Verse( 'quickVerse.quickVerse', 'latest', @@ -152,6 +184,10 @@ globalThis.webViewComponent = function HelloWorld() {
{personGreeting}
{personAge}
+
Selected Project: {project}
+
+ +

John 1:1

{john11}

Psalm 1

diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 79de6e65ac..ffe73cd9d3 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -1692,6 +1692,12 @@ declare module 'shared/data/web-view.model' { * {@link TabSaver} saves this into {@link SavedTabInfo} */ export type TabInfo = SavedTabInfo & { + /** + * Url of image to show on the title bar of the tab + * + * Defaults to Platform.Bible logo + */ + tabIconUrl?: string; /** * Text to show on the title bar of the tab */ @@ -1720,8 +1726,12 @@ declare module 'shared/data/web-view.model' { * Function that takes a Paranext tab and creates a saved tab out of it. Each type of tab can * provide a {@link TabSaver}. If they do not provide one, the properties added by `TabInfo` are * stripped from TabInfo by `saveTabInfoBase` before saving (so it is just a {@link SavedTabInfo}). + * + * @param tabInfo the Paranext tab to save + * + * @returns The saved tab info for Paranext to persist. If `undefined`, does not save the tab */ - export type TabSaver = (tabInfo: TabInfo) => SavedTabInfo; + export type TabSaver = (tabInfo: TabInfo) => SavedTabInfo | undefined; /** The type of code that defines a webview's content */ export enum WebViewContentType { /** @@ -1744,6 +1754,12 @@ declare module 'shared/data/web-view.model' { id: WebViewId; /** The code for the WebView that papi puts into an iframe */ content: string; + /** + * Url of image to show on the title bar of the tab + * + * Defaults to Platform.Bible logo + */ + iconUrl?: string; /** Name of the tab for the WebView */ title?: string; /** General object to store unique state for this webview */ @@ -1779,6 +1795,14 @@ declare module 'shared/data/web-view.model' { interface TabLayout { type: 'tab'; } + /** + * Indicates where to display a floating window + * + * `cascade` - place the window a bit below and to the right of the previously created floating + * window + * `center` - center the window in the dock layout + */ + type FloatPosition = 'cascade' | 'center'; /** Information about a floating window */ export interface FloatLayout { type: 'float'; @@ -1786,6 +1810,8 @@ declare module 'shared/data/web-view.model' { width: number; height: number; }; + /** Where to display the floating window. Defaults to `cascade` */ + position?: FloatPosition; } export type PanelDirection = | 'left' @@ -1995,7 +2021,17 @@ declare module 'shared/services/web-view.service' { * {@link onLayoutChange} function */ onLayoutChangeRef: MutableRefObject; - /** Function to call to add or update a webview in the layout */ + /** + * Function to call to add or update a tab in the layout + * @param savedTabInfo info for tab to add or update + * @param layout information about where to put a new webview + */ + addTabToDock: (savedTabInfo: SavedTabInfo, layout: Layout) => void; + /** + * Function to call to add or update a webview in the layout + * @param webView web view to add or update + * @param layout information about where to put a new webview + */ addWebViewToDock: (webView: WebViewProps, layout: Layout) => void; /** * The layout to use as the default layout if the dockLayout doesn't have a layout loaded. @@ -2053,8 +2089,22 @@ declare module 'shared/services/web-view.service' { * operations * @param dockLayout dock layout element to register along with other important properties * @returns function used to unregister this dock layout + * + * WARNING: YOU CANNOT USE THIS FUNCTION IN ANYTHING BUT THE RENDERER + * + * Not exposed on the papi */ export function registerDockLayout(dockLayout: PapiDockLayout): Unsubscriber; + /** + * Add or update a tab in the layout + * @param savedTabInfo info for tab to add or update + * @param layout information about where to put a new tab + * + * WARNING: YOU CANNOT USE THIS FUNCTION IN ANYTHING BUT THE RENDERER + * + * Not exposed on the papi + */ + export const addTab: (savedTabInfo: SavedTabInfo, layout: Layout) => Promise; /** * Creates a new web view or gets an existing one depending on if you request an existing one and * if the web view provider decides to give that existing one to you (it is up to the provider). @@ -2819,6 +2869,41 @@ declare module 'renderer/hooks/papi-hooks/index' { const papiHooks: PapiHooks; export default papiHooks; } +declare module 'shared/models/dialog-options.model' { + /** General options to adjust dialogs (created from `papi.dialogs`) */ + export type DialogOptions = { + /** Dialog title to display in the header. Default depends on the dialog */ + title?: string; + /** Url of dialog icon to display in the header. Default is Platform.Bible logo */ + iconUrl?: string; + /** The message to show the user in the dialog. Default depends on the dialog */ + prompt?: string; + }; +} +declare module 'shared/services/dialog.service.model' { + import { DialogOptions } from 'shared/models/dialog-options.model'; + /** + * Prompt the user for responses with dialogs + */ + export interface DialogService { + /** + * Shows a select project dialog to the user and prompts the user to select a dialog + * + * @param options various options for configuring the dialog that shows + * + * @returns If the user selects a project, returns that project's project id. If the user cancels, + * returns `undefined` + */ + getProject(options?: DialogOptions): Promise; + } + /** Prefix on requests that indicates that the request is related to dialog operations */ + export const CATEGORY_DIALOG = 'dialog'; +} +declare module 'shared/services/dialog.service' { + import { DialogService } from 'shared/services/dialog.service.model'; + const dialogService: DialogService; + export default dialogService; +} declare module 'papi-frontend' { /** * Unified module for accessing API features in the renderer. @@ -2837,6 +2922,7 @@ declare module 'papi-frontend' { import { PapiContext } from 'renderer/context/papi-context/index'; import { PapiHooks } from 'renderer/hooks/papi-hooks/index'; import { SettingsService } from 'shared/services/settings.service'; + import { DialogService } from 'shared/services/dialog.service.model'; const papi: { /** * Event manager - accepts subscriptions to an event and runs the subscription callbacks when the event is emitted @@ -2863,6 +2949,10 @@ declare module 'papi-frontend' { * Service exposing various functions related to using webViews */ webViews: PapiWebViewService; + /** + * Prompt the user for responses with dialogs + */ + dialogs: DialogService; /** * Service that provides a way to send and receive network events */ @@ -3163,6 +3253,7 @@ declare module 'papi-backend' { import { PapiBackendProjectDataProviderService } from 'shared/services/project-data-provider.service'; import { ExtensionStorageService } from 'extension-host/services/extension-storage.service'; import { ProjectLookupServiceType } from 'shared/services/project-lookup.service.model'; + import { DialogService } from 'shared/services/dialog.service.model'; const papi: { /** * Event manager - accepts subscriptions to an event and runs the subscription callbacks when the event is emitted @@ -3193,6 +3284,10 @@ declare module 'papi-backend' { * Interface for registering webView providers */ webViewProviders: PapiWebViewProviderService; + /** + * Prompt the user for responses with dialogs + */ + dialogs: DialogService; /** * Service that provides a way to send and receive network events */ diff --git a/src/extension-host/services/papi-backend.service.ts b/src/extension-host/services/papi-backend.service.ts index 830b3a6a56..0c100ea8be 100644 --- a/src/extension-host/services/papi-backend.service.ts +++ b/src/extension-host/services/papi-backend.service.ts @@ -25,6 +25,8 @@ import extensionStorageService, { } from '@extension-host/services/extension-storage.service'; import { ProjectLookupServiceType } from '@shared/services/project-lookup.service.model'; import projectLookupService from '@shared/services/project-lookup.service'; +import dialogService from '@shared/services/dialog.service'; +import { DialogService } from '@shared/services/dialog.service.model'; // IMPORTANT NOTES: // 1) When adding new services here, consider whether they also belong in papi-frontend.service.ts. @@ -50,6 +52,8 @@ const papi = { webViews: papiWebViewService as PapiWebViewService, /** JSDOC DESTINATION papiWebViewProviderService */ webViewProviders: papiWebViewProviderService as PapiWebViewProviderService, + /** JSDOC DESTINATION dialogService */ + dialogs: dialogService as DialogService, /** JSDOC DESTINATION papiNetworkService */ network: papiNetworkService as PapiNetworkService, /** JSDOC DESTINATION logger */ diff --git a/src/extension-host/services/project-lookup.service.host.ts b/src/extension-host/services/project-lookup.service.host.ts index 52d547de80..16dd5c81c2 100644 --- a/src/extension-host/services/project-lookup.service.host.ts +++ b/src/extension-host/services/project-lookup.service.host.ts @@ -118,19 +118,16 @@ const projectLookupService: ProjectLookupServiceType = { getMetadataForProject, }; -let networkObject: ProjectLookupServiceType; - /** * Register the network object that backs the PAPI project lookup service */ +// This doesn't really represent this service module, so we're not making it default. To use this +// service, you should use `project-lookup.service.ts` +// eslint-disable-next-line import/prefer-default-export export async function startProjectLookupService(): Promise { await initialize(); - networkObject = await networkObjectService.set( + await networkObjectService.set( projectLookupServiceNetworkObjectName, projectLookupService, ); } - -export function getNetworkObject(): ProjectLookupServiceType { - return networkObject; -} diff --git a/src/renderer/components/docking/paranext-dock-layout.component.test.ts b/src/renderer/components/docking/paranext-dock-layout.component.test.ts index c52226cbe4..7919c1b877 100644 --- a/src/renderer/components/docking/paranext-dock-layout.component.test.ts +++ b/src/renderer/components/docking/paranext-dock-layout.component.test.ts @@ -17,7 +17,12 @@ jest.mock( import DockLayout, { FloatPosition } from 'rc-dock'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import { FloatLayout, Layout, SavedTabInfo, WebViewProps } from '@shared/data/web-view.model'; -import { addWebViewToDock, getFloatPosition, loadTab } from './paranext-dock-layout.component'; +import { + addTabToDock, + addWebViewToDock, + getFloatPosition, + loadTab, +} from './paranext-dock-layout.component'; describe('Dock Layout Component', () => { const mockDockLayout = mock(DockLayout); @@ -83,6 +88,18 @@ describe('Dock Layout Component', () => { }); }); + describe('addTabToDock()', () => { + it('should throw when tab is not an object', () => { + const dockLayout = instance(mockDockLayout); + const layout: Layout = { type: 'tab' }; + + expect(() => + addTabToDock('this is wrong' as unknown as SavedTabInfo, layout, dockLayout), + ).toThrow(); + }); + // TODO: verify it adds an error tab if no/bad tab type provided + }); + describe('addWebViewToDock()', () => { it('should throw when no id', () => { const dockLayout = instance(mockDockLayout); diff --git a/src/renderer/components/docking/paranext-dock-layout.component.tsx b/src/renderer/components/docking/paranext-dock-layout.component.tsx index c02116620a..88631e44fb 100644 --- a/src/renderer/components/docking/paranext-dock-layout.component.tsx +++ b/src/renderer/components/docking/paranext-dock-layout.component.tsx @@ -42,8 +42,8 @@ import { } from '@shared/services/web-view.service'; import { getErrorMessage } from '@shared/utils/util'; import { - loadOpenProjectTab, - TAB_TYPE_OPEN_PROJECT_DIALOG, + loadSelectProjectTab, + saveSelectProjectTab, } from '@renderer/components/project-dialogs/open-project-tab.component'; import { loadDownloadUpdateProjectTab, @@ -61,6 +61,7 @@ import { TAB_TYPE_RUN_BASIC_CHECKS, loadRunBasicChecksTab, } from '@renderer/components/run-basic-checks-dialog/run-basic-checks-tab.component'; +import { TAB_TYPE_SELECT_PROJECT_DIALOG } from '@renderer/services/dialog.service.host'; type TabType = string; @@ -93,7 +94,7 @@ const tabLoaderMap = new Map([ [TAB_TYPE_QUICK_VERSE_HERESY, loadQuickVerseHeresyTab], [TAB_TYPE_TEST, loadTestTab], [TAB_TYPE_WEBVIEW, loadWebViewTab], - [TAB_TYPE_OPEN_PROJECT_DIALOG, loadOpenProjectTab], + [TAB_TYPE_SELECT_PROJECT_DIALOG, loadSelectProjectTab], [TAB_TYPE_DOWNLOAD_UPDATE_PROJECT_DIALOG, loadDownloadUpdateProjectTab], [TAB_TYPE_OPEN_MULTIPLE_PROJECTS_DIALOG, loadOpenMultipleProjectsTab], [TAB_TYPE_EXTENSION_MANAGER, loadExtensionManagerTab], @@ -101,10 +102,13 @@ const tabLoaderMap = new Map([ ]); /** tab saver functions for each Paranext tab type that wants to override the default */ -const tabSaverMap = new Map([[TAB_TYPE_WEBVIEW, saveWebViewTab]]); +const tabSaverMap = new Map([ + [TAB_TYPE_WEBVIEW, saveWebViewTab], + [TAB_TYPE_SELECT_PROJECT_DIALOG, saveSelectProjectTab], +]); let previousTabId: string | undefined; -let floatPosition: FloatPosition = { left: 0, top: 0, width: 0, height: 0 }; +let previousFloatPosition: FloatPosition = { left: 0, top: 0, width: 0, height: 0 }; /** * Loads tab data from the specified saved tab information by running the tab loader provided by the @@ -139,7 +143,7 @@ export function loadTab(savedTabInfo: SavedTabInfo): RCDockTabInfo { // Translate the data from the loaded tab to be in the form needed by rc-dock return { ...tabInfo, - title: , + title: , content: {tabInfo.content}, group: TAB_GROUP, closable: true, @@ -152,7 +156,7 @@ export function loadTab(savedTabInfo: SavedTabInfo): RCDockTabInfo { * @param dockTabInfo the tab data to save * @returns saved tab info ready to be saved into the layout */ -function saveTab(dockTabInfo: RCDockTabInfo): SavedTabInfo { +function saveTab(dockTabInfo: RCDockTabInfo): SavedTabInfo | undefined { // Remove the rc-dock properties that are not also in SavedTabInfo // We don't need to use the other properties, but we need to remove them // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -200,9 +204,20 @@ export function getFloatPosition( // Defaults are added in `web-view.service.ts`. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { width, height } = layout.floatSize!; + let { left, top } = previousPosition; - left = offsetOrOverflowAxis(left, width, layoutSize.width); - top = offsetOrOverflowAxis(top, height, layoutSize.height); + + switch (layout.position) { + case 'center': + left = layoutSize.width / 2 - width / 2; + top = layoutSize.height / 2 - height / 2; + break; + case 'cascade': + default: + left = offsetOrOverflowAxis(left, width, layoutSize.width); + top = offsetOrOverflowAxis(top, height, layoutSize.height); + break; + } return { left, top, width, height }; } @@ -220,21 +235,20 @@ function findPreviousTab(dockLayout: DockLayout) { } /** - * Function to call to add or update a webview in the layout - * @param webView web view to add or update - * @param layout information about where to put a new webview + * Function to call to add or update a tab in the layout + * @param savedTabInfo info for tab to add or update + * @param layout information about where to put a new tab * @param dockLayout The rc-dock dock layout React component ref. Used to perform operations on the * layout */ -export function addWebViewToDock(webView: WebViewProps, layout: Layout, dockLayout: DockLayout) { - const tabId = webView.id; - const tab = loadTab({ id: tabId, tabType: TAB_TYPE_WEBVIEW, data: webView }); - let targetTab = dockLayout.find(tabId); +export function addTabToDock(savedTabInfo: SavedTabInfo, layout: Layout, dockLayout: DockLayout) { + const tab = loadTab(savedTabInfo); + let targetTab = dockLayout.find(tab.id); // Update existing WebView if (targetTab) { - dockLayout.updateTab(tabId, tab); - if (isTab(targetTab)) previousTabId = tabId; + dockLayout.updateTab(tab.id, tab); + if (isTab(targetTab)) previousTabId = tab.id; return; } @@ -258,14 +272,19 @@ export function addWebViewToDock(webView: WebViewProps, layout: Layout, dockLayo dockLayout.find(() => true) ?? null, 'middle', ); - previousTabId = tabId; + previousTabId = tab.id; break; - case 'float': - floatPosition = getFloatPosition(layout, floatPosition, dockLayout.getLayoutSize()); + case 'float': { + const floatPosition = getFloatPosition( + layout, + previousFloatPosition, + dockLayout.getLayoutSize(), + ); + if (!layout.position || layout.position === 'cascade') previousFloatPosition = floatPosition; dockLayout.dockMove(tab, null, 'float', floatPosition); break; - + } case 'panel': if (layout.targetTabId !== undefined) { // Look for a specific tab @@ -294,6 +313,22 @@ export function addWebViewToDock(webView: WebViewProps, layout: Layout, dockLayo } } +/** + * Function to call to add or update a webview in the layout + * @param webView web view to add or update + * @param layout information about where to put a new webview + * @param dockLayout The rc-dock dock layout React component ref. Used to perform operations on the + * layout + */ +export function addWebViewToDock(webView: WebViewProps, layout: Layout, dockLayout: DockLayout) { + const tabId = webView.id; + if (!tabId) + throw new Error( + `paranext-dock-layout error: WebView of type ${webView.webViewType} has no id!`, + ); + addTabToDock({ id: tabId, tabType: TAB_TYPE_WEBVIEW, data: webView }, layout, dockLayout); +} + // #endregion export default function ParanextDockLayout() { @@ -314,6 +349,8 @@ export default function ParanextDockLayout() { const unsub = registerDockLayout({ dockLayout: dockLayoutRef.current, onLayoutChangeRef, + addTabToDock: (savedTabInfo: SavedTabInfo, layout: Layout) => + addTabToDock(savedTabInfo, layout, dockLayoutRef.current), addWebViewToDock: (webView: WebViewProps, layout: Layout) => addWebViewToDock(webView, layout, dockLayoutRef.current), testLayout, @@ -331,7 +368,10 @@ export default function ParanextDockLayout() { defaultLayout={{ dockbox: { mode: 'horizontal', children: [] } }} dropMode="edge" loadTab={loadTab} - saveTab={saveTab} + // Type assert `saveTab` as not returning `undefined` because rc-dock's types are wrong + // Here, if `saveTab` returns `undefined` the tab is not saved + // https://github.com/ticlo/rc-dock/blob/8b6481dca4b4dd07f89107d6f48b1831bbdf0470/src/Serializer.ts#L68 + saveTab={saveTab as (dockTabInfo: RCDockTabInfo) => SavedTabInfo} onLayoutChange={(...args) => { if (onLayoutChangeRef.current) onLayoutChangeRef.current(...args); }} diff --git a/src/renderer/components/docking/paranext-tab-title.component.tsx b/src/renderer/components/docking/paranext-tab-title.component.tsx index 08cfb31289..bb9104c5e3 100644 --- a/src/renderer/components/docking/paranext-tab-title.component.tsx +++ b/src/renderer/components/docking/paranext-tab-title.component.tsx @@ -1,11 +1,18 @@ import './paranext-tab-title.component.css'; import logger from '@shared/services/logger.service'; +type ParanextTabTitleProps = { + /** Url to image to show on the tab. Defaults to Platform.Bible logo */ + iconUrl?: string; + /** Text to show on the tab */ + text: string; +}; + /** * Custom tab title for all tabs in Paranext * @param text The text to show on the tab title */ -export default function ParanextTabTitle({ text }: { text: string }) { +export default function ParanextTabTitle({ iconUrl, text }: ParanextTabTitleProps) { const toggleDropdown = () => { logger.info('Pretend a menu was shown!'); }; @@ -15,6 +22,13 @@ export default function ParanextTabTitle({ text }: { text: string }) {