From 1eff4daef115ad7c65b0d8b9380e44dbfa1a442e Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Mon, 6 May 2024 17:41:34 +1000 Subject: [PATCH 01/54] WIP: Extract search bar into a React Context --- .../dashboard/PermissionDisplay.tsx | 3 +- .../dashboard/column/SharedWithColumn.tsx | 23 +++- .../lib/dashboard/src/layouts/AssetPanel.tsx | 13 +-- .../dashboard/src/layouts/AssetProperties.tsx | 13 +-- .../lib/dashboard/src/layouts/AssetsTable.tsx | 20 +++- .../lib/dashboard/src/layouts/Drive.tsx | 21 ++-- .../lib/dashboard/src/layouts/TopBar.tsx | 33 ++---- .../src/pages/dashboard/Dashboard.tsx | 25 +--- .../dashboard/src/providers/ModalProvider.tsx | 24 ++-- .../src/providers/SearchBarProvider.tsx | 109 ++++++++++++++++++ 10 files changed, 188 insertions(+), 96 deletions(-) create mode 100644 app/ide-desktop/lib/dashboard/src/providers/SearchBarProvider.tsx diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/PermissionDisplay.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/PermissionDisplay.tsx index 0472967709bb..16b76103f449 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/PermissionDisplay.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/PermissionDisplay.tsx @@ -14,7 +14,7 @@ import * as permissionsModule from '#/utilities/permissions' export interface PermissionDisplayProps extends Readonly { readonly action: permissionsModule.PermissionAction readonly className?: string - readonly onPress?: (event: aria.PressEvent) => void + readonly onPress?: ((event: aria.PressEvent) => void) | null } /** Colored border around icons and text indicating permissions. */ @@ -44,6 +44,7 @@ export default function PermissionDisplay(props: PermissionDisplayProps) { case permissionsModule.Permission.view: { return ( {})} > diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/column/SharedWithColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/column/SharedWithColumn.tsx index 897932ddd4e9..0b53fe7261c4 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/column/SharedWithColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/column/SharedWithColumn.tsx @@ -27,7 +27,9 @@ import * as uniqueString from '#/utilities/uniqueString' /** The type of the `state` prop of a {@link SharedWithColumn}. */ interface SharedWithColumnStateProp - extends Pick {} + extends Pick { + readonly setQuery: column.AssetColumnProps['state']['setQuery'] | null +} /** Props for a {@link SharedWithColumn}. */ interface SharedWithColumnPropsInternal extends Pick { @@ -67,11 +69,20 @@ export default function SharedWithColumn(props: SharedWithColumnPropsInternal) { { - setQuery(oldQuery => - oldQuery.withToggled('owners', 'negativeOwners', otherUser.user.name, event.shiftKey) - ) - }} + onPress={ + setQuery == null + ? null + : event => { + setQuery(oldQuery => + oldQuery.withToggled( + 'owners', + 'negativeOwners', + otherUser.user.name, + event.shiftKey + ) + ) + } + } > {otherUser.user.name} diff --git a/app/ide-desktop/lib/dashboard/src/layouts/AssetPanel.tsx b/app/ide-desktop/lib/dashboard/src/layouts/AssetPanel.tsx index aa8ba1ec4578..29987caa67fc 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/AssetPanel.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/AssetPanel.tsx @@ -15,7 +15,6 @@ import UnstyledButton from '#/components/UnstyledButton' import * as backend from '#/services/Backend' import * as array from '#/utilities/array' -import type AssetQuery from '#/utilities/AssetQuery' import type * as assetTreeNode from '#/utilities/AssetTreeNode' import LocalStorage from '#/utilities/LocalStorage' @@ -58,7 +57,6 @@ export interface AssetPanelRequiredProps { /** Props for an {@link AssetPanel}. */ export interface AssetPanelProps extends AssetPanelRequiredProps { readonly isReadonly?: boolean - readonly setQuery: React.Dispatch> readonly category: Category readonly labels: backend.Label[] readonly dispatchAssetEvent: (event: assetEvent.AssetEvent) => void @@ -66,15 +64,7 @@ export interface AssetPanelProps extends AssetPanelRequiredProps { /** A panel containing the description and settings for an asset. */ export default function AssetPanel(props: AssetPanelProps) { - const { - item, - setItem, - setQuery, - category, - labels, - dispatchAssetEvent, - isReadonly = false, - } = props + const { item, setItem, category, labels, dispatchAssetEvent, isReadonly = false } = props const { getText } = textProvider.useText() const { localStorage } = localStorageProvider.useLocalStorage() @@ -149,7 +139,6 @@ export default function AssetPanel(props: AssetPanelProps) { setItem={setItem} category={category} labels={labels} - setQuery={setQuery} dispatchAssetEvent={dispatchAssetEvent} /> )} diff --git a/app/ide-desktop/lib/dashboard/src/layouts/AssetProperties.tsx b/app/ide-desktop/lib/dashboard/src/layouts/AssetProperties.tsx index 630af21eb328..090bbca3418f 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/AssetProperties.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/AssetProperties.tsx @@ -25,7 +25,6 @@ import UnstyledButton from '#/components/UnstyledButton' import * as backendModule from '#/services/Backend' -import type AssetQuery from '#/utilities/AssetQuery' import type * as assetTreeNode from '#/utilities/AssetTreeNode' import * as object from '#/utilities/object' import * as permissions from '#/utilities/permissions' @@ -40,21 +39,13 @@ export interface AssetPropertiesProps { readonly setItem: React.Dispatch> readonly category: Category readonly labels: backendModule.Label[] - readonly setQuery: React.Dispatch> readonly dispatchAssetEvent: (event: assetEvent.AssetEvent) => void readonly isReadonly?: boolean } /** Display and modify the properties of an asset. */ export default function AssetProperties(props: AssetPropertiesProps) { - const { - item: itemRaw, - setItem: setItemRaw, - category, - labels, - setQuery, - isReadonly = false, - } = props + const { item: itemRaw, setItem: setItemRaw, category, labels, isReadonly = false } = props const { dispatchAssetEvent } = props const { user } = authProvider.useNonPartialUserSession() @@ -215,7 +206,7 @@ export default function AssetProperties(props: AssetPropertiesProps) { isReadonly={isReadonly} item={item} setItem={setItem} - state={{ category, dispatchAssetEvent, setQuery }} + state={{ category, dispatchAssetEvent, setQuery: () => {} }} /> diff --git a/app/ide-desktop/lib/dashboard/src/layouts/AssetsTable.tsx b/app/ide-desktop/lib/dashboard/src/layouts/AssetsTable.tsx index dcad506d19de..401c703fb1c0 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/AssetsTable.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/AssetsTable.tsx @@ -14,6 +14,7 @@ import * as inputBindingsProvider from '#/providers/InputBindingsProvider' import * as localStorageProvider from '#/providers/LocalStorageProvider' import * as modalProvider from '#/providers/ModalProvider' import * as navigator2DProvider from '#/providers/Navigator2DProvider' +import * as searchBarProvider from '#/providers/SearchBarProvider' import * as textProvider from '#/providers/TextProvider' import type * as assetEvent from '#/events/assetEvent' @@ -23,6 +24,7 @@ import AssetListEventType from '#/events/AssetListEventType' import type * as assetPanel from '#/layouts/AssetPanel' import type * as assetSearchBar from '#/layouts/AssetSearchBar' +import AssetSearchBar from '#/layouts/AssetSearchBar' import AssetsTableContextMenu from '#/layouts/AssetsTableContextMenu' import Category from '#/layouts/CategorySwitcher/Category' @@ -350,7 +352,6 @@ export interface AssetsTableProps { readonly setCanDownload: (canDownload: boolean) => void readonly category: Category readonly allLabels: Map - readonly setSuggestions: (suggestions: assetSearchBar.Suggestion[]) => void readonly initialProjectName: string | null readonly projectStartupInfo: backendModule.ProjectStartupInfo | null readonly deletedLabelNames: Set @@ -376,7 +377,7 @@ export interface AssetsTableProps { /** The table of project assets. */ export default function AssetsTable(props: AssetsTableProps) { const { hidden, hideRows, query, setQuery, setCanDownload, category, allLabels } = props - const { setSuggestions, deletedLabelNames, initialProjectName, projectStartupInfo } = props + const { deletedLabelNames, initialProjectName, projectStartupInfo } = props const { queuedAssetEvents: rawQueuedAssetEvents } = props const { assetListEvents, dispatchAssetListEvent, assetEvents, dispatchAssetEvent } = props const { setAssetPanelProps, doOpenEditor, doCloseEditor: rawDoCloseEditor, doCreateLabel } = props @@ -385,6 +386,7 @@ export default function AssetsTable(props: AssetsTableProps) { const { user, accessToken } = authProvider.useNonPartialUserSession() const { backend } = backendProvider.useBackend() const { setModal, unsetModal } = modalProvider.useSetModal() + const { setSearchBar } = searchBarProvider.useSetSearchBar() const { localStorage } = localStorageProvider.useLocalStorage() const { getText } = textProvider.useText() const inputBindings = inputBindingsProvider.useInputBindings() @@ -393,6 +395,7 @@ export default function AssetsTable(props: AssetsTableProps) { const [initialized, setInitialized] = React.useState(false) const [isLoading, setIsLoading] = React.useState(true) const [enabledColumns, setEnabledColumns] = React.useState(columnUtils.DEFAULT_ENABLED_COLUMNS) + const [suggestions, setSuggestions] = React.useState([]) const [sortInfo, setSortInfo] = React.useState | null>(null) const [selectedKeys, setSelectedKeysRaw] = React.useState>( @@ -644,6 +647,19 @@ export default function AssetsTable(props: AssetsTableProps) { } }, [targetDirectoryNodeRef, selectedKeys]) + React.useEffect(() => { + setSearchBar( + 'assets table', + + ) + }, [isCloud, query, setQuery, allLabels, suggestions, /* should never change */ setSearchBar]) + React.useEffect(() => { const nodeToSuggestion = ( node: assetTreeNode.AnyAssetTreeNode, diff --git a/app/ide-desktop/lib/dashboard/src/layouts/Drive.tsx b/app/ide-desktop/lib/dashboard/src/layouts/Drive.tsx index 04151df96a82..8fc580f7ae2f 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/Drive.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/Drive.tsx @@ -18,12 +18,12 @@ import type * as assetListEvent from '#/events/assetListEvent' import AssetListEventType from '#/events/AssetListEventType' import type * as assetPanel from '#/layouts/AssetPanel' -import type * as assetSearchBar from '#/layouts/AssetSearchBar' import AssetsTable from '#/layouts/AssetsTable' import CategorySwitcher from '#/layouts/CategorySwitcher' import Category from '#/layouts/CategorySwitcher/Category' import DriveBar from '#/layouts/DriveBar' import Labels from '#/layouts/Labels' +import * as pageSwitcher from '#/layouts/PageSwitcher' import * as aria from '#/components/aria' import type * as spinner from '#/components/Spinner' @@ -32,7 +32,7 @@ import UnstyledButton from '#/components/UnstyledButton' import * as backendModule from '#/services/Backend' import * as projectManager from '#/services/ProjectManager' -import type AssetQuery from '#/utilities/AssetQuery' +import AssetQuery from '#/utilities/AssetQuery' import type AssetTreeNode from '#/utilities/AssetTreeNode' import * as download from '#/utilities/download' import * as github from '#/utilities/github' @@ -64,6 +64,7 @@ enum DriveStatus { export interface DriveProps { readonly category: Category readonly setCategory: (category: Category) => void + readonly setPage: (page: pageSwitcher.Page) => void readonly supportsLocalBackend: boolean readonly hidden: boolean readonly hideRows: boolean @@ -75,11 +76,8 @@ export interface DriveProps { readonly dispatchAssetListEvent: (directoryEvent: assetListEvent.AssetListEvent) => void readonly assetEvents: assetEvent.AssetEvent[] readonly dispatchAssetEvent: (directoryEvent: assetEvent.AssetEvent) => void - readonly query: AssetQuery - readonly setQuery: React.Dispatch> readonly labels: backendModule.Label[] readonly setLabels: React.Dispatch> - readonly setSuggestions: (suggestions: assetSearchBar.Suggestion[]) => void readonly projectStartupInfo: backendModule.ProjectStartupInfo | null readonly setAssetPanelProps: (props: assetPanel.AssetPanelRequiredProps | null) => void readonly setIsAssetPanelTemporarilyVisible: (visible: boolean) => void @@ -95,11 +93,10 @@ export interface DriveProps { /** Contains directory path and directory contents (projects, folders, secrets and files). */ export default function Drive(props: DriveProps) { const { supportsLocalBackend, hidden, hideRows, initialProjectName, queuedAssetEvents } = props - const { query, setQuery, labels, setLabels, setSuggestions, projectStartupInfo } = props + const { labels, setLabels, category, setCategory, setPage, projectStartupInfo } = props const { assetListEvents, dispatchAssetListEvent, assetEvents, dispatchAssetEvent } = props const { setAssetPanelProps, doOpenEditor, doCloseEditor } = props const { setIsAssetPanelTemporarilyVisible } = props - const { category, setCategory } = props const navigate = navigateHooks.useNavigate() const toastAndLog = toastAndLogHooks.useToastAndLog() @@ -107,6 +104,7 @@ export default function Drive(props: DriveProps) { const { backend } = backendProvider.useBackend() const { localStorage } = localStorageProvider.useLocalStorage() const { getText } = textProvider.useText() + const [query, setQuery] = React.useState(() => AssetQuery.fromString('')) const [canDownload, setCanDownload] = React.useState(false) const [didLoadingProjectManagerFail, setDidLoadingProjectManagerFail] = React.useState(false) const [newLabelNames, setNewLabelNames] = React.useState(new Set()) @@ -155,6 +153,12 @@ export default function Drive(props: DriveProps) { } }, []) + React.useEffect(() => { + if (query.query !== '') { + setPage(pageSwitcher.Page.drive) + } + }, [query, setPage]) + React.useEffect(() => { void (async () => { if (backend.type !== backendModule.BackendType.local && user?.isEnabled === true) { @@ -246,7 +250,6 @@ export default function Drive(props: DriveProps) { const doDeleteLabel = React.useCallback( async (id: backendModule.TagId, value: backendModule.LabelName) => { setDeletedLabelNames(oldNames => new Set([...oldNames, value])) - setQuery(oldQuery => oldQuery.deleteFromEveryTerm({ labels: [value] })) try { await backend.deleteTag(id, value) dispatchAssetEvent({ @@ -264,7 +267,6 @@ export default function Drive(props: DriveProps) { [ backend, toastAndLog, - /* should never change */ setQuery, /* should never change */ dispatchAssetEvent, /* should never change */ setLabels, ] @@ -405,7 +407,6 @@ export default function Drive(props: DriveProps) { setCanDownload={setCanDownload} category={category} allLabels={allLabels} - setSuggestions={setSuggestions} initialProjectName={initialProjectName} projectStartupInfo={projectStartupInfo} deletedLabelNames={deletedLabelNames} diff --git a/app/ide-desktop/lib/dashboard/src/layouts/TopBar.tsx b/app/ide-desktop/lib/dashboard/src/layouts/TopBar.tsx index 3d545ff1c552..77f3e7dd3599 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/TopBar.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/TopBar.tsx @@ -1,8 +1,8 @@ /** @file The top-bar of dashboard. */ import * as React from 'react' -import type * as assetSearchBar from '#/layouts/AssetSearchBar' -import AssetSearchBar from '#/layouts/AssetSearchBar' +import * as searchBarProvider from '#/providers/SearchBarProvider' + import BackendSwitcher from '#/layouts/BackendSwitcher' import PageSwitcher, * as pageSwitcher from '#/layouts/PageSwitcher' import UserBar from '#/layouts/UserBar' @@ -11,8 +11,6 @@ import AssetInfoBar from '#/components/dashboard/AssetInfoBar' import type * as backendModule from '#/services/Backend' -import type AssetQuery from '#/utilities/AssetQuery' - // ============== // === TopBar === // ============== @@ -21,7 +19,6 @@ import type AssetQuery from '#/utilities/AssetQuery' export interface TopBarProps { /** Whether the application may have the local backend running. */ readonly supportsLocalBackend: boolean - readonly isCloud: boolean readonly page: pageSwitcher.Page readonly setPage: (page: pageSwitcher.Page) => void readonly projectAsset: backendModule.ProjectAsset | null @@ -30,10 +27,6 @@ export interface TopBarProps { readonly setBackendType: (backendType: backendModule.BackendType) => void readonly isHelpChatOpen: boolean readonly setIsHelpChatOpen: (isHelpChatOpen: boolean) => void - readonly query: AssetQuery - readonly setQuery: React.Dispatch> - readonly labels: backendModule.Label[] - readonly suggestions: assetSearchBar.Suggestion[] readonly isAssetPanelVisible: boolean readonly isAssetPanelEnabled: boolean readonly setIsAssetPanelEnabled: React.Dispatch> @@ -44,10 +37,12 @@ export interface TopBarProps { /** The {@link TopBarProps.setQuery} parameter is used to communicate with the parent component, * because `searchVal` may change parent component's project list. */ export default function TopBar(props: TopBarProps) { - const { supportsLocalBackend, isCloud, page, setPage, projectAsset, setProjectAsset } = props + const { supportsLocalBackend, page, setPage, projectAsset, setProjectAsset } = props const { isEditorDisabled, setBackendType, isHelpChatOpen, setIsHelpChatOpen } = props - const { query, setQuery, labels, suggestions, isAssetPanelEnabled } = props - const { isAssetPanelVisible, setIsAssetPanelEnabled, doRemoveSelf, onSignOut } = props + const { isAssetPanelEnabled, isAssetPanelVisible, setIsAssetPanelEnabled, doRemoveSelf } = props + const { onSignOut } = props + + const searchBar = searchBarProvider.useSearchBar() const supportsCloudBackend = process.env.ENSO_CLOUD_API_URL != null const shouldMakeSpaceForExtendedEditorMenu = page === pageSwitcher.Page.editor @@ -59,19 +54,7 @@ export default function TopBar(props: TopBarProps) { {supportsLocalBackend && supportsCloudBackend && page !== pageSwitcher.Page.editor && ( )} - {page === pageSwitcher.Page.editor ? ( -
- ) : ( -
- -
- )} +
{searchBar}
diff --git a/app/ide-desktop/lib/dashboard/src/pages/dashboard/Dashboard.tsx b/app/ide-desktop/lib/dashboard/src/pages/dashboard/Dashboard.tsx index fcbd1470c47b..1009753c8075 100644 --- a/app/ide-desktop/lib/dashboard/src/pages/dashboard/Dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/pages/dashboard/Dashboard.tsx @@ -22,7 +22,6 @@ import AssetListEventType from '#/events/AssetListEventType' import type * as assetPanel from '#/layouts/AssetPanel' import AssetPanel from '#/layouts/AssetPanel' -import type * as assetSearchBar from '#/layouts/AssetSearchBar' import Category from '#/layouts/CategorySwitcher/Category' import Chat from '#/layouts/Chat' import ChatPlaceholder from '#/layouts/ChatPlaceholder' @@ -43,7 +42,6 @@ import type * as projectManager from '#/services/ProjectManager' import RemoteBackend, * as remoteBackendModule from '#/services/RemoteBackend' import * as array from '#/utilities/array' -import AssetQuery from '#/utilities/AssetQuery' import HttpClient from '#/utilities/HttpClient' import LocalStorage from '#/utilities/LocalStorage' import * as object from '#/utilities/object' @@ -146,9 +144,7 @@ export default function Dashboard(props: DashboardProps) { array.includes(Object.values(pageSwitcher.Page), value) ) const [queuedAssetEvents, setQueuedAssetEvents] = React.useState([]) - const [query, setQuery] = React.useState(() => AssetQuery.fromString('')) const [labels, setLabels] = React.useState([]) - const [suggestions, setSuggestions] = React.useState([]) const [projectStartupInfo, setProjectStartupInfo] = React.useState(null) const [openProjectAbortController, setOpenProjectAbortController] = @@ -168,7 +164,6 @@ export default function Dashboard(props: DashboardProps) { (value): value is Category => array.includes(Object.values(Category), value) ) - const isCloud = backend.type === backendModule.BackendType.remote const rootDirectoryId = React.useMemo( () => session.user?.rootDirectoryId ?? backendModule.DirectoryId(''), [session.user] @@ -180,12 +175,6 @@ export default function Dashboard(props: DashboardProps) { setInitialized(true) }, []) - React.useEffect(() => { - if (query.query !== '') { - setPage(pageSwitcher.Page.drive) - } - }, [query, setPage]) - React.useEffect(() => { let currentBackend = backend if ( @@ -500,7 +489,6 @@ export default function Dashboard(props: DashboardProps) { >