From 1fca6fb60a99ce7fa714abc2b46c9e27f8a1bd15 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 12 Sep 2022 23:33:16 +0800 Subject: [PATCH] onboarding UI flow --- apps/desktop/src/index.tsx | 3 +- apps/mobile/src/types/bindings.ts | 2 +- core/index.ts | 2 +- .../migration.sql | 0 core/src/library/library_manager.rs | 18 ++---- core/src/util/db.rs | 8 ++- ...urrentLibrary.ts => useCurrentLibrary.tsx} | 53 +++++++++++++++--- packages/interface/src/App.tsx | 17 +++++- packages/interface/src/AppLayout.tsx | 7 +++ packages/interface/src/AppRouter.tsx | 3 +- .../components/dialog/CreateLibraryDialog.tsx | 15 ++++- .../explorer/ExplorerContextMenu.tsx | 2 +- .../src/components/layout/Dialog.tsx | 1 - .../src/components/layout/Sidebar.tsx | 2 +- .../src/components/onboarding/Onboarding.tsx | 19 ++++--- pnpm-lock.yaml | Bin 680797 -> 680869 bytes 16 files changed, 107 insertions(+), 45 deletions(-) rename core/prisma/migrations/{20220909002821_init => 20220909073230_init}/migration.sql (100%) rename packages/client/src/hooks/{useCurrentLibrary.ts => useCurrentLibrary.tsx} (51%) diff --git a/apps/desktop/src/index.tsx b/apps/desktop/src/index.tsx index 7afd43773..30ca6cde5 100644 --- a/apps/desktop/src/index.tsx +++ b/apps/desktop/src/index.tsx @@ -1,4 +1,5 @@ -import { TauriTransport, createClient } from '@rspc/client'; +import { createClient } from '@rspc/client'; +import { TauriTransport } from '@rspc/tauri'; import { OperatingSystem, Operations, PlatformProvider, queryClient, rspc } from '@sd/client'; import SpacedriveInterface, { Platform } from '@sd/interface'; import { dialog, invoke, os } from '@tauri-apps/api'; diff --git a/apps/mobile/src/types/bindings.ts b/apps/mobile/src/types/bindings.ts index 651f94b21..c3d16c9aa 100644 --- a/apps/mobile/src/types/bindings.ts +++ b/apps/mobile/src/types/bindings.ts @@ -26,7 +26,7 @@ export type Operations = { { key: ["files.setNote", LibraryArgs], result: null } | { key: ["jobs.generateThumbsForLocation", LibraryArgs], result: null } | { key: ["jobs.identifyUniqueFiles", LibraryArgs], result: null } | - { key: ["library.create", string], result: null } | + { key: ["library.create", string], result: LibraryConfigWrapped } | { key: ["library.delete", string], result: null } | { key: ["library.edit", EditLibraryArgs], result: null } | { key: ["locations.create", LibraryArgs], result: null } | diff --git a/core/index.ts b/core/index.ts index 651f94b21..c3d16c9aa 100644 --- a/core/index.ts +++ b/core/index.ts @@ -26,7 +26,7 @@ export type Operations = { { key: ["files.setNote", LibraryArgs], result: null } | { key: ["jobs.generateThumbsForLocation", LibraryArgs], result: null } | { key: ["jobs.identifyUniqueFiles", LibraryArgs], result: null } | - { key: ["library.create", string], result: null } | + { key: ["library.create", string], result: LibraryConfigWrapped } | { key: ["library.delete", string], result: null } | { key: ["library.edit", EditLibraryArgs], result: null } | { key: ["locations.create", LibraryArgs], result: null } | diff --git a/core/prisma/migrations/20220909002821_init/migration.sql b/core/prisma/migrations/20220909073230_init/migration.sql similarity index 100% rename from core/prisma/migrations/20220909002821_init/migration.sql rename to core/prisma/migrations/20220909073230_init/migration.sql diff --git a/core/src/library/library_manager.rs b/core/src/library/library_manager.rs index 439a6f42f..f81400d2e 100644 --- a/core/src/library/library_manager.rs +++ b/core/src/library/library_manager.rs @@ -111,20 +111,14 @@ impl LibraryManager { node_context, }); - // TODO: Remove this before merging PR -> Currently it exists to make the app usable - if this.libraries.read().await.len() == 0 { - this.create(LibraryConfig { - name: "My Default Library".into(), - ..Default::default() - }) - .await?; - } - Ok(this) } /// create creates a new library with the given config and mounts it into the running [LibraryManager]. - pub(crate) async fn create(&self, config: LibraryConfig) -> Result<(), LibraryManagerError> { + pub(crate) async fn create( + &self, + config: LibraryConfig, + ) -> Result { let id = Uuid::new_v4(); LibraryConfig::save( Path::new(&self.libraries_dir).join(format!("{id}.sdlibrary")), @@ -135,7 +129,7 @@ impl LibraryManager { let library = Self::load( id, self.libraries_dir.join(format!("{id}.db")), - config, + config.clone(), self.node_context.clone(), ) .await?; @@ -143,7 +137,7 @@ impl LibraryManager { invalidate_query!(library, "library.list"); self.libraries.write().await.push(library); - Ok(()) + Ok(LibraryConfigWrapped { uuid: id, config }) } pub(crate) async fn get_all_libraries_config(&self) -> Vec { diff --git a/core/src/util/db.rs b/core/src/util/db.rs index 66163895d..a572457b8 100644 --- a/core/src/util/db.rs +++ b/core/src/util/db.rs @@ -20,7 +20,7 @@ static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/prisma/migrations #[derive(Error, Debug)] pub enum MigrationError { #[error("An error occurred while initialising a new database connection: {0}")] - NewClient(#[from] NewClientError), + NewClient(#[from] Box), #[error("The temporary file path for the database migrations is invalid.")] InvalidDirectory, #[error("An error occurred creating the temporary directory for the migrations: {0}")] @@ -40,7 +40,9 @@ pub async fn load_and_migrate( base_path: &Path, db_url: &str, ) -> Result { - let client = prisma::new_client_with_url(db_url).await?; + let client = prisma::new_client_with_url(db_url) + .await + .map_err(|err| Box::new(err))?; let temp_migrations_dir = base_path.join("./migrations_temp"); let migrations_directory_path = temp_migrations_dir .to_str() @@ -60,7 +62,7 @@ pub async fn load_and_migrate( .extract(&temp_migrations_dir) .map_err(MigrationError::ExtractMigrations)?; - let mut connector = match &ConnectionInfo::from_url(&db_url)? { + let mut connector = match &ConnectionInfo::from_url(db_url)? { ConnectionInfo::Sqlite { .. } => SqlMigrationConnector::new_sqlite(), ConnectionInfo::InMemorySqlite { .. } => unreachable!(), // This is how it is in the Prisma Rust tests }; diff --git a/packages/client/src/hooks/useCurrentLibrary.ts b/packages/client/src/hooks/useCurrentLibrary.tsx similarity index 51% rename from packages/client/src/hooks/useCurrentLibrary.ts rename to packages/client/src/hooks/useCurrentLibrary.tsx index eecdf9fcd..b43a75f40 100644 --- a/packages/client/src/hooks/useCurrentLibrary.ts +++ b/packages/client/src/hooks/useCurrentLibrary.tsx @@ -1,4 +1,12 @@ -import { useCallback, useMemo } from 'react'; +import { + PropsWithChildren, + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState +} from 'react'; import { proxy, useSnapshot } from 'valtio'; import { useBridgeQuery } from '../index'; @@ -7,13 +15,36 @@ import { explorerStore } from '../stores/explorerStore'; // The name of the localStorage key for caching library data const libraryCacheLocalStorageKey = 'sd-library-list'; +type OnNoLibraryFunc = () => void | Promise; + // Keep this private and use `useCurrentLibrary` hook to access or mutate it -const currentLibraryUuidStore = proxy({ id: null as string | null }); +const CringeContext = createContext<{ + onNoLibrary: OnNoLibraryFunc; + currentLibraryId: string | null; + setCurrentLibraryId: (v: string | null) => void; +}>(undefined!); + +export const LibraryContextProvider = ({ + onNoLibrary, + children +}: PropsWithChildren<{ onNoLibrary: OnNoLibraryFunc }>) => { + const [currentLibraryId, setCurrentLibraryId] = useState(null); + + return ( + + {children} + + ); +}; // this is a hook to get the current library loaded into the UI. It takes care of a bunch of invariants under the hood. export const useCurrentLibrary = () => { - const currentLibraryUuid = useSnapshot(currentLibraryUuidStore).id; - const { data: libraries, isLoading } = useBridgeQuery(['library.get'], { + const ctx = useContext(CringeContext); + if (ctx === undefined) + throw new Error( + "The 'LibraryContextProvider' was not mounted and you attempted do use the 'useCurrentLibrary' hook. Please add the provider in your component tree." + ); + const { data: libraries, isLoading } = useBridgeQuery(['library.list'], { keepPreviousData: true, initialData: () => { const cachedData = localStorage.getItem(libraryCacheLocalStorageKey); @@ -29,25 +60,29 @@ export const useCurrentLibrary = () => { }, onSuccess: (data) => { localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(data)); + + // Redirect to the onboaording flow if the user doesn't have any libraries + if (libraries?.length === 0) { + ctx.onNoLibrary(); + } } }); const switchLibrary = useCallback((libraryUuid: string) => { - currentLibraryUuidStore.id = libraryUuid; + ctx.setCurrentLibraryId(libraryUuid); explorerStore.reset(); }, []); // memorize library to avoid re-running find function const library = useMemo(() => { - const current = libraries?.find((l: any) => l.uuid === currentLibraryUuid); + const current = libraries?.find((l: any) => l.uuid === ctx.currentLibraryId); // switch to first library if none set if (libraries && !current && libraries[0]?.uuid) { switchLibrary(libraries[0]?.uuid); } - return current; - }, [libraries, currentLibraryUuid]); // TODO: This runs when the 'libraries' change causing the whole app to re-render which is cringe. - // TODO: Redirect to onboarding flow if the user hasn't completed it. -> localStorage? + return current; + }, [libraries, ctx.currentLibraryId]); // TODO: This runs when the 'libraries' change causing the whole app to re-render which is cringe. return { library, diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx index 62e8a653d..07a4dedff 100644 --- a/packages/interface/src/App.tsx +++ b/packages/interface/src/App.tsx @@ -1,9 +1,9 @@ import '@fontsource/inter/variable.css'; -import { queryClient } from '@sd/client'; +import { LibraryContextProvider, queryClient } from '@sd/client'; import { QueryClientProvider, defaultContext } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ErrorBoundary } from 'react-error-boundary'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter, useNavigate } from 'react-router-dom'; import { AppRouter } from './AppRouter'; import { ErrorFallback } from './ErrorFallback'; @@ -18,9 +18,20 @@ export default function SpacedriveInterface() { )} - + ); } + +// This can't go in `` cause it needs the router context but it can't go in `` because that requires this context +function AppRouterWrapper() { + const navigate = useNavigate(); + + return ( + navigate('/onboarding')}> + + + ); +} diff --git a/packages/interface/src/AppLayout.tsx b/packages/interface/src/AppLayout.tsx index 828eb294a..14a9ab835 100644 --- a/packages/interface/src/AppLayout.tsx +++ b/packages/interface/src/AppLayout.tsx @@ -1,3 +1,4 @@ +import { useCurrentLibrary } from '@sd/client'; import clsx from 'clsx'; import { Outlet } from 'react-router-dom'; @@ -5,8 +6,14 @@ import { Sidebar } from './components/layout/Sidebar'; import { useOperatingSystem } from './hooks/useOperatingSystem'; export function AppLayout() { + const { libraries } = useCurrentLibrary(); const os = useOperatingSystem(); + // This will ensure nothing is rendered while the `useCurrentLibrary` hook navigates to the onboarding page. This prevents requests with an invalid library id being sent to the backend + if (libraries?.length === 0) { + return null; + } + return (
{ diff --git a/packages/interface/src/AppRouter.tsx b/packages/interface/src/AppRouter.tsx index 8d0d78ddb..f1322631d 100644 --- a/packages/interface/src/AppRouter.tsx +++ b/packages/interface/src/AppRouter.tsx @@ -3,6 +3,7 @@ import { Route, Routes } from 'react-router-dom'; import { AppLayout } from './AppLayout'; import { NotFound } from './NotFound'; +import OnboardingScreen from './components/onboarding/Onboarding'; import { useKeyboardHandler } from './hooks/useKeyboardHandler'; import { ContentScreen } from './screens/Content'; import { DebugScreen } from './screens/Debug'; @@ -41,7 +42,7 @@ export function AppRouter() { return ( - TODO

} /> + } /> }> {/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */} {library === undefined ? ( diff --git a/packages/interface/src/components/dialog/CreateLibraryDialog.tsx b/packages/interface/src/components/dialog/CreateLibraryDialog.tsx index 7fe17ea7a..11a400ce4 100644 --- a/packages/interface/src/components/dialog/CreateLibraryDialog.tsx +++ b/packages/interface/src/components/dialog/CreateLibraryDialog.tsx @@ -5,7 +5,10 @@ import { PropsWithChildren, useState } from 'react'; import Dialog from '../layout/Dialog'; -export default function CreateLibraryDialog({ children }: PropsWithChildren) { +export default function CreateLibraryDialog({ + children, + onSubmit +}: PropsWithChildren<{ onSubmit?: () => void }>) { const [openCreateModal, setOpenCreateModal] = useState(false); const [newLibName, setNewLibName] = useState(''); @@ -13,9 +16,15 @@ export default function CreateLibraryDialog({ children }: PropsWithChildren) { const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation( 'library.create', { - onSuccess: () => { - queryClient.invalidateQueries(['library.get']); // TODO: Change to `library.list` + onSuccess: (library) => { + console.log('SUBMITTING'); setOpenCreateModal(false); + queryClient.setQueryData(['library.list'], (libraries: any) => [ + ...(libraries || []), + library + ]); + + if (onSubmit) onSubmit(); }, onError: (err) => { console.error(err); diff --git a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx index 2014a023b..a1da33ec1 100644 --- a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx +++ b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx @@ -15,7 +15,7 @@ import { import { useSnapshot } from 'valtio'; const AssignTagMenuItems = (props: { objectId: number }) => { - const tags = useLibraryQuery(['tags.getAll'], { suspense: true }); + const tags = useLibraryQuery(['tags.list'], { suspense: true }); const tagsForFile = useLibraryQuery(['tags.getForFile', props.objectId], { suspense: true }); const { mutate: assignTag } = useLibraryMutation('tags.assign'); diff --git a/packages/interface/src/components/layout/Dialog.tsx b/packages/interface/src/components/layout/Dialog.tsx index a71b62644..ed8c2970d 100644 --- a/packages/interface/src/components/layout/Dialog.tsx +++ b/packages/interface/src/components/layout/Dialog.tsx @@ -49,7 +49,6 @@ export default function Dialog(props: DialogProps) { */} + navigate('overview')}> + +
); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e391288132163ea3ce1b2dc1177d1509573ced70..813b06af322abeda250cf6ee17062ae903ba25b3 100644 GIT binary patch delta 182 zcmcb6OmpdR&4w1n7N#xCw{}fl%PBa0gD{uzt}9G9^x0QT`gcK`qY delta 132 zcmZ2_T=VWR&4w1n7N#xCw{}hcD#oQb{lR)x;pxlvGxJaP-^0w-ZoZp&yZLUGkAkv> zx<=kPMTW-u;aO=Zrv7=pE{R#C>4u?BDM7}??agOdfS47C*?^cGh&i@5pXGez4l&i? dG>hW&1OYCd?dL_g=5b7&#woPDP>##Q6#&dOGZp{<