From 25dee8a19fd9fde0e5d13272d3eac8e2567d36c3 Mon Sep 17 00:00:00 2001 From: Katherine Jensen Date: Tue, 24 Oct 2023 13:58:41 -0700 Subject: [PATCH 1/2] Hook Up Select Multiple Projects Dialog --- .../web-views/hello-world.web-view.tsx | 43 ++++++++--- lib/papi-dts/papi.d.ts | 3 + .../services/project-lookup.service-host.ts | 2 +- .../dialogs/dialog-definition.model.ts | 4 +- src/renderer/components/dialogs/index.ts | 2 + ...t-multiple-projects-dialog.component.scss} | 4 +- ...ect-multiple-projects-dialog.component.tsx | 73 +++++++++++++++++++ .../platform-dock-layout.component.tsx | 5 -- .../open-multiple-projects-tab.component.tsx | 68 ----------------- .../projects/project-list.component.tsx | 62 +++++++++++++--- 10 files changed, 168 insertions(+), 98 deletions(-) rename src/renderer/components/{projects/open-multiple-projects-tab.component.scss => dialogs/select-multiple-projects-dialog.component.scss} (54%) create mode 100644 src/renderer/components/dialogs/select-multiple-projects-dialog.component.tsx delete mode 100644 src/renderer/components/projects/open-multiple-projects-tab.component.tsx diff --git a/extensions/src/hello-world/web-views/hello-world.web-view.tsx b/extensions/src/hello-world/web-views/hello-world.web-view.tsx index 9f574bff28..b0dd6d443b 100644 --- a/extensions/src/hello-world/web-views/hello-world.web-view.tsx +++ b/extensions/src/hello-world/web-views/hello-world.web-view.tsx @@ -17,6 +17,7 @@ import type { UsfmProviderDataTypes } from 'usfm-data-provider'; import { Key, useCallback, useContext, useMemo, useRef, useState } from 'react'; import type { HelloWorldEvent } from 'hello-world'; import type { DialogTypes } from 'renderer/components/dialogs/dialog-definition.model'; +import { ProjectDataTypes } from 'papi-shared-types'; import Clock from './components/clock.component'; type Row = { @@ -28,7 +29,15 @@ type Row = { const { react: { context: { TestContext }, - hooks: { useData, useDataProvider, usePromise, useEvent, useSetting, useDialogCallback }, + hooks: { + useData, + useDataProvider, + useProjectData, + usePromise, + useEvent, + useSetting, + useDialogCallback, + }, }, logger, } = papi; @@ -64,10 +73,10 @@ globalThis.webViewComponent = function HelloWorld() { const [rows, setRows] = useState(initializeRows()); const [selectedRows, setSelectedRows] = useState(new Set()); const [scrRef, setScrRef] = useSetting('platform.verseRef', defaultScrRef); - /* const verseRef = useMemo( + const verseRef = useMemo( () => new VerseRef(scrRef.bookNum, scrRef.chapterNum, scrRef.verseNum), [scrRef], - ); */ + ); // Update the clicks when we are informed helloWorld has been run useEvent( @@ -108,6 +117,18 @@ globalThis.webViewComponent = function HelloWorld() { 'Loading latest Scripture text...', ); + const [projects, selectProjects] = useDialogCallback( + 'platform.selectMultipleProjects', + useRef({ + prompt: 'Please select one or more projects for Hello World WebView:', + iconUrl: 'papi-extension://hello-world/assets/offline.svg', + title: 'Select List of Hello World Projects', + }).current, + // Assert as string type rather than string literal type. + // eslint-disable-next-line no-type-assertion/no-type-assertion + ['None'] as DialogTypes['platform.selectMultipleProjects']['responseType'], + ); + const [name, setName] = useState('Bill'); const peopleDataProvider = useDataProvider('helloSomeone.people'); @@ -132,12 +153,11 @@ globalThis.webViewComponent = function HelloWorld() { 'Loading John 1:1...', ); - // TODO: Uncomment this or similar sample code once https://github.com/paranext/paranext-core/issues/440 is resolved - /* const [webVerse] = useProjectData.VerseUSFM( - '32664dc3288a28df2e2bb75ded887fc8f17a15fb', + const [webVerse] = useProjectData.VerseUSFM( + project, verseRef, 'Loading WEB Verse', - ); */ + ); return (
@@ -184,8 +204,13 @@ globalThis.webViewComponent = function HelloWorld() {
{john11}

Psalm 1

{psalm1}
- {/*

{verseRef.toString()} WEB

-
{webVerse}
*/} +

{verseRef.toString()} WEB

+
{webVerse}
+

List of Selected Project Id(s):

+
{projects.join(', ')}
+
+ +

diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 424dfda3c2..e8ce8f1e7a 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -3124,6 +3124,8 @@ declare module 'renderer/components/dialogs/dialog-definition.model' { import { ReactElement } from 'react'; /** The tabType for the select project dialog in `select-project.dialog.tsx` */ export const SELECT_PROJECT_DIALOG_TYPE = 'platform.selectProject'; + /** The tabType for the select multiple projects dialog in `select-multiple-projects.dialog.tsx` */ + export const SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE = 'platform.selectMultipleProjects'; /** * Mapped type for dialog functions to use in getting various types for dialogs * @@ -3133,6 +3135,7 @@ declare module 'renderer/components/dialogs/dialog-definition.model' { */ export interface DialogTypes { [SELECT_PROJECT_DIALOG_TYPE]: DialogDataTypes; + [SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE]: DialogDataTypes; } /** Each type of dialog. These are the tab types used in the dock layout */ export type DialogTabTypes = keyof DialogTypes; diff --git a/src/extension-host/services/project-lookup.service-host.ts b/src/extension-host/services/project-lookup.service-host.ts index bde3f2dd58..d5deaf50ef 100644 --- a/src/extension-host/services/project-lookup.service-host.ts +++ b/src/extension-host/services/project-lookup.service-host.ts @@ -75,7 +75,7 @@ async function reloadMetadata(): Promise { const allMetadata = await loadAllProjectsMetadata(); localProjects.clear(); allMetadata.forEach((metadata) => { - localProjects.set(metadata.id.toUpperCase(), metadata); + localProjects.set(metadata.id, metadata); }); } diff --git a/src/renderer/components/dialogs/dialog-definition.model.ts b/src/renderer/components/dialogs/dialog-definition.model.ts index dbc9c6db95..cdb50d02ba 100644 --- a/src/renderer/components/dialogs/dialog-definition.model.ts +++ b/src/renderer/components/dialogs/dialog-definition.model.ts @@ -4,6 +4,8 @@ import { ReactElement } from 'react'; /** The tabType for the select project dialog in `select-project.dialog.tsx` */ export const SELECT_PROJECT_DIALOG_TYPE = 'platform.selectProject'; +/** The tabType for the select multiple projects dialog in `select-multiple-projects.dialog.tsx` */ +export const SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE = 'platform.selectMultipleProjects'; /** * Mapped type for dialog functions to use in getting various types for dialogs @@ -14,7 +16,7 @@ export const SELECT_PROJECT_DIALOG_TYPE = 'platform.selectProject'; */ export interface DialogTypes { [SELECT_PROJECT_DIALOG_TYPE]: DialogDataTypes; - // 'platform.selectMultipleProjects': DialogDataTypes; + [SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE]: DialogDataTypes; } /** Each type of dialog. These are the tab types used in the dock layout */ diff --git a/src/renderer/components/dialogs/index.ts b/src/renderer/components/dialogs/index.ts index 2970cf2351..e0c8bf0f64 100644 --- a/src/renderer/components/dialogs/index.ts +++ b/src/renderer/components/dialogs/index.ts @@ -1,5 +1,6 @@ import SELECT_PROJECT_DIALOG from '@renderer/components/dialogs/select-project.dialog'; import { DialogDefinition, DialogTabTypes } from './dialog-definition.model'; +import SELECT_MULTIPLE_PROJECTS_DIALOG from './select-multiple-projects-dialog.component'; /** * Map of all available dialog definitions used to create dialogs @@ -8,6 +9,7 @@ import { DialogDefinition, DialogTabTypes } from './dialog-definition.model'; */ const DIALOGS: { [DialogTabType in DialogTabTypes]: DialogDefinition } = { [SELECT_PROJECT_DIALOG.tabType]: SELECT_PROJECT_DIALOG, + [SELECT_MULTIPLE_PROJECTS_DIALOG.tabType]: SELECT_MULTIPLE_PROJECTS_DIALOG, }; /** All tab types for available dialogs */ diff --git a/src/renderer/components/projects/open-multiple-projects-tab.component.scss b/src/renderer/components/dialogs/select-multiple-projects-dialog.component.scss similarity index 54% rename from src/renderer/components/projects/open-multiple-projects-tab.component.scss rename to src/renderer/components/dialogs/select-multiple-projects-dialog.component.scss index b08b2cc274..b64c32aea7 100644 --- a/src/renderer/components/projects/open-multiple-projects-tab.component.scss +++ b/src/renderer/components/dialogs/select-multiple-projects-dialog.component.scss @@ -1,8 +1,8 @@ -.open-multiple-projects-dialog { +.select-multiple-projects-dialog { overflow-y: auto; } -.open-multiple-projects-submit-button { +.select-multiple-projects-submit-button { display: flex !important; justify-content: flex-end !important; } diff --git a/src/renderer/components/dialogs/select-multiple-projects-dialog.component.tsx b/src/renderer/components/dialogs/select-multiple-projects-dialog.component.tsx new file mode 100644 index 0000000000..1fca6967be --- /dev/null +++ b/src/renderer/components/dialogs/select-multiple-projects-dialog.component.tsx @@ -0,0 +1,73 @@ +import { ListItemIcon } from '@mui/material'; +import { useMemo, useState } from 'react'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import DoneIcon from '@mui/icons-material/Done'; +import { Button } from 'papi-components'; +import ProjectList from '@renderer/components/projects/project-list.component'; +import './select-multiple-projects-dialog.component.scss'; +import projectLookupService from '@shared/services/project-lookup.service'; +import usePromise from '@renderer/hooks/papi-hooks/use-promise.hook'; +import { ProjectMetadata } from '@shared/models/project-metadata.model'; +import DIALOG_BASE, { DialogProps } from './dialog-base.data'; +import { DialogDefinition, SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE } from './dialog-definition.model'; + +type SelectMultipleProjectDialogProps = DialogProps; + +function SelectMultipleProjectsDialog({ prompt, submitDialog }: SelectMultipleProjectDialogProps) { + const [downloadedProjects, isLoadingProjects] = usePromise( + projectLookupService.getMetadataForAllProjects, + useMemo(() => [], []), + ); + + const [selectedProjects, setSelectedProjects] = useState([]); + + const handleProjectToggle = (projectId: string) => { + if (selectedProjects.some((project) => project.id === projectId)) { + setSelectedProjects(selectedProjects.filter((project) => project.id !== projectId)); + } else { + const selectedProject = downloadedProjects.find((project) => project.id === projectId); + if (selectedProject) setSelectedProjects([...selectedProjects, selectedProject]); + } + }; + + return ( +
+
{prompt}
+ {isLoadingProjects ? ( +
Loading Projects
+ ) : ( + + + + + + )} +
+ +
+
+ ); +} + +const SELECT_MULTIPLE_PROJECTS_DIALOG: DialogDefinition< + typeof SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE +> = Object.freeze({ + ...DIALOG_BASE, + tabType: SELECT_MULTIPLE_PROJECTS_DIALOG_TYPE, + defaultTitle: 'Select Projects', + initialSize: { + width: 500, + height: 350, + }, + Component: SelectMultipleProjectsDialog, +}); + +export default SELECT_MULTIPLE_PROJECTS_DIALOG; diff --git a/src/renderer/components/docking/platform-dock-layout.component.tsx b/src/renderer/components/docking/platform-dock-layout.component.tsx index 872c89618f..5aba2d0f13 100644 --- a/src/renderer/components/docking/platform-dock-layout.component.tsx +++ b/src/renderer/components/docking/platform-dock-layout.component.tsx @@ -52,10 +52,6 @@ import { loadDownloadUpdateProjectTab, TAB_TYPE_DOWNLOAD_UPDATE_PROJECT_DIALOG, } from '@renderer/components/projects/download-update-project-tab.component'; -import { - loadOpenMultipleProjectsTab, - TAB_TYPE_OPEN_MULTIPLE_PROJECTS_DIALOG, -} from '@renderer/components/projects/open-multiple-projects-tab.component'; import { TAB_TYPE_EXTENSION_MANAGER, loadExtensionManagerTab, @@ -114,7 +110,6 @@ const tabLoaderMap = new Map([ [TAB_TYPE_TEST, loadTestTab], [TAB_TYPE_WEBVIEW, loadWebViewTab], [TAB_TYPE_DOWNLOAD_UPDATE_PROJECT_DIALOG, loadDownloadUpdateProjectTab], - [TAB_TYPE_OPEN_MULTIPLE_PROJECTS_DIALOG, loadOpenMultipleProjectsTab], [TAB_TYPE_EXTENSION_MANAGER, loadExtensionManagerTab], [TAB_TYPE_SETTINGS_DIALOG, loadSettingsDialog], [TAB_TYPE_RUN_BASIC_CHECKS, loadRunBasicChecksTab], diff --git a/src/renderer/components/projects/open-multiple-projects-tab.component.tsx b/src/renderer/components/projects/open-multiple-projects-tab.component.tsx deleted file mode 100644 index 745ab6809e..0000000000 --- a/src/renderer/components/projects/open-multiple-projects-tab.component.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { ListItemIcon } from '@mui/material'; -import { useMemo, useState } from 'react'; -import logger from '@shared/services/logger.service'; -import FolderOpenIcon from '@mui/icons-material/FolderOpen'; -import DoneIcon from '@mui/icons-material/Done'; -import { SavedTabInfo, TabInfo } from '@shared/data/web-view.model'; -import { Button } from 'papi-components'; -import ProjectList, { - fetchProjects, - Project, -} from '@renderer/components/projects/project-list.component'; -import './open-multiple-projects-tab.component.scss'; - -export const TAB_TYPE_OPEN_MULTIPLE_PROJECTS_DIALOG = 'open-multiple-projects-dialog'; - -export default function OpenMultipleProjectsTab() { - const downloadedProjects = useMemo( - () => fetchProjects().filter((project) => project.isDownloaded), - [], - ); - - const [selectedProjects, setSelectedProjects] = useState([]); - - const handleProjectToggle = (projectId: string) => { - if (selectedProjects.some((project) => project.id === projectId)) { - setSelectedProjects(selectedProjects.filter((project) => project.id !== projectId)); - } else { - const selectedProject = downloadedProjects.find((project) => project.id === projectId); - if (selectedProject) setSelectedProjects([...selectedProjects, selectedProject]); - } - }; - - const handleSubmit = () => { - setSelectedProjects([]); - logger.info( - 'Selected projects:', - selectedProjects.map((proj) => proj.name), - ); - }; - - return ( -
- - - - - -
- -
-
- ); -} - -export const loadOpenMultipleProjectsTab = (savedTabInfo: SavedTabInfo): TabInfo => { - return { - ...savedTabInfo, - tabTitle: 'Open Multiple Projects', - content: , - }; -}; diff --git a/src/renderer/components/projects/project-list.component.tsx b/src/renderer/components/projects/project-list.component.tsx index add65314b8..a7ad401874 100644 --- a/src/renderer/components/projects/project-list.component.tsx +++ b/src/renderer/components/projects/project-list.component.tsx @@ -1,7 +1,8 @@ import { List, ListItem, ListItemButton, ListItemText, ListSubheader } from '@mui/material'; import { ProjectMetadata } from '@shared/models/project-metadata.model'; +import { Checkbox } from 'papi-components'; import { ProjectTypes } from 'papi-shared-types'; -import { PropsWithChildren, useCallback } from 'react'; +import { PropsWithChildren, useCallback, useState, JSX } from 'react'; export type Project = ProjectMetadata & { id: string; @@ -91,7 +92,8 @@ export type ProjectListProps = PropsWithChildren<{ isMultiselect?: boolean; /** - * If multiple is selected, then the array of selected projects is passed to control the selected flag on ListItemButton + * If multiselect is selected, then the array of selected projects is passed to control + * the selected flag on ListItemButton */ selectedProjects?: ProjectMetadata[] | undefined; @@ -99,6 +101,12 @@ export type ProjectListProps = PropsWithChildren<{ * Optional subheader */ subheader?: string; + + /** + * Adds a checkbox to the end of each list item that reflects the selected state of + * each project + */ + isCheckable?: boolean; }>; /** @@ -112,6 +120,7 @@ export default function ProjectList({ isMultiselect, selectedProjects, subheader, + isCheckable, children, }: ProjectListProps) { const isSelected = useCallback( @@ -124,20 +133,49 @@ export default function ProjectList({ [isMultiselect, selectedProjects], ); + const [checked, setChecked] = useState([-1]); + + /* This function is based off of an example on https://mui.com/material-ui/react-list/ */ + const handleToggle = (value: number, projectId: string) => () => { + const currentIndex = checked.indexOf(value); + const newChecked = [...checked]; + + if (currentIndex === -1) { + newChecked.push(value); + } else { + newChecked.splice(currentIndex, 1); + } + setChecked(newChecked); + handleSelectProject(projectId); + }; + + const createListItemContents = (project: ProjectMetadata, index: number): JSX.Element => { + if (!isCheckable) { + return ( + handleSelectProject(project.id)} + > + {children} + + + ); + } + return ( + + {children} + + + + ); + }; + return (
{subheader} - {projects.map((project) => ( - - handleSelectProject(project.id)} - > - {children} - - - + {projects.map((project, index) => ( + {createListItemContents(project, index)} ))}
From c6c10affc896dfc9f8e571541778c87c0e8594fd Mon Sep 17 00:00:00 2001 From: Katherine Jensen Date: Tue, 24 Oct 2023 14:42:49 -0700 Subject: [PATCH 2/2] Consolidate redundant code --- .../services/project-lookup.service-host.ts | 2 +- .../projects/project-list.component.tsx | 44 +++++-------------- 2 files changed, 11 insertions(+), 35 deletions(-) diff --git a/src/extension-host/services/project-lookup.service-host.ts b/src/extension-host/services/project-lookup.service-host.ts index 5bb1ae2547..b33945ab93 100644 --- a/src/extension-host/services/project-lookup.service-host.ts +++ b/src/extension-host/services/project-lookup.service-host.ts @@ -75,7 +75,7 @@ async function reloadMetadata(): Promise { const allMetadata = await loadAllProjectsMetadata(); localProjects.clear(); allMetadata.forEach((metadata) => { - localProjects.set(metadata.id, metadata); + localProjects.set(metadata.id.toUpperCase(), metadata); }); } diff --git a/src/renderer/components/projects/project-list.component.tsx b/src/renderer/components/projects/project-list.component.tsx index a7ad401874..2e635e8de7 100644 --- a/src/renderer/components/projects/project-list.component.tsx +++ b/src/renderer/components/projects/project-list.component.tsx @@ -2,7 +2,7 @@ import { List, ListItem, ListItemButton, ListItemText, ListSubheader } from '@mu import { ProjectMetadata } from '@shared/models/project-metadata.model'; import { Checkbox } from 'papi-components'; import { ProjectTypes } from 'papi-shared-types'; -import { PropsWithChildren, useCallback, useState, JSX } from 'react'; +import { PropsWithChildren, useCallback, JSX } from 'react'; export type Project = ProjectMetadata & { id: string; @@ -133,39 +133,15 @@ export default function ProjectList({ [isMultiselect, selectedProjects], ); - const [checked, setChecked] = useState([-1]); - - /* This function is based off of an example on https://mui.com/material-ui/react-list/ */ - const handleToggle = (value: number, projectId: string) => () => { - const currentIndex = checked.indexOf(value); - const newChecked = [...checked]; - - if (currentIndex === -1) { - newChecked.push(value); - } else { - newChecked.splice(currentIndex, 1); - } - setChecked(newChecked); - handleSelectProject(projectId); - }; - - const createListItemContents = (project: ProjectMetadata, index: number): JSX.Element => { - if (!isCheckable) { - return ( - handleSelectProject(project.id)} - > - {children} - - - ); - } + const createListItemContents = (project: ProjectMetadata): JSX.Element => { return ( - + handleSelectProject(project.id)} + > {children} - - + + {isCheckable && } ); }; @@ -174,8 +150,8 @@ export default function ProjectList({
{subheader} - {projects.map((project, index) => ( - {createListItemContents(project, index)} + {projects.map((project) => ( + {createListItemContents(project)} ))}