From e1330420a92c2faa32aef925bca9e39e2b12c543 Mon Sep 17 00:00:00 2001 From: Hebi Li Date: Fri, 30 Dec 2022 10:04:47 -0800 Subject: [PATCH 1/3] properly sync repoName to remote; better UI --- ui/src/lib/store/repoMetaSlice.tsx | 43 +++++++- ui/src/pages/repo.tsx | 151 +++++++++++++++++++++-------- 2 files changed, 154 insertions(+), 40 deletions(-) diff --git a/ui/src/lib/store/repoMetaSlice.tsx b/ui/src/lib/store/repoMetaSlice.tsx index d7194185..2c470afb 100644 --- a/ui/src/lib/store/repoMetaSlice.tsx +++ b/ui/src/lib/store/repoMetaSlice.tsx @@ -4,6 +4,7 @@ import produce from "immer"; import { Doc } from "yjs"; import { WebsocketProvider } from "y-websocket"; import { MyState } from "."; +import { gql } from "@apollo/client"; let serverURL; if (window.location.protocol === "http:") { @@ -15,9 +16,12 @@ console.log("yjs server url: ", serverURL); export interface RepoMetaSlice { repoName: string | null; + repoNameSyncing: boolean; + repoNameDirty: boolean; repoId: string | null; setRepo: (repoId: string) => void; setRepoName: (name: string) => void; + remoteUpdateRepoName: (client) => void; } export const createRepoMetaSlice: StateCreator< @@ -28,6 +32,8 @@ export const createRepoMetaSlice: StateCreator< > = (set, get) => ({ repoId: null, repoName: null, + repoNameSyncing: false, + repoNameDirty: false, setRepo: (repoId: string) => set( produce((state: MyState) => { @@ -51,8 +57,43 @@ export const createRepoMetaSlice: StateCreator< ), setRepoName: (name) => { set( - produce((state) => { + produce((state: MyState) => { state.repoName = name; + state.repoNameDirty = true; + }) + ); + }, + remoteUpdateRepoName: async (client) => { + if (get().repoNameSyncing) return; + if (!get().repoNameDirty) return; + let { repoId, repoName } = get(); + if (!repoId || !repoName) return; + // Prevent double syncing. + set( + produce((state: MyState) => { + state.repoNameSyncing = true; + }) + ); + // Do the actual syncing. + await client.mutate({ + mutation: gql` + mutation UpdateRepo($id: ID!, $name: String) { + updateRepo(id: $id, name: $name) + } + `, + variables: { + id: repoId, + name: repoName, + }, + refetchQueries: ["GetRepos", "GetCollabRepos"], + }); + set((state) => + produce(state, (state) => { + state.repoNameSyncing = false; + // Set it as synced IF the name is still the same. + if (state.repoName === repoName) { + state.repoNameDirty = false; + } }) ); }, diff --git a/ui/src/pages/repo.tsx b/ui/src/pages/repo.tsx index 05bdfd8f..51a8d29a 100644 --- a/ui/src/pages/repo.tsx +++ b/ui/src/pages/repo.tsx @@ -8,7 +8,9 @@ import ShareIcon from "@mui/icons-material/Share"; import Button from "@mui/material/Button"; import { gql, useApolloClient, useMutation } from "@apollo/client"; -import { useEffect, useState, useRef, useContext } from "react"; +import { useEffect, useState, useRef, useContext, memo } from "react"; + +import * as React from "react"; import { useStore } from "zustand"; @@ -19,32 +21,127 @@ import { Canvas } from "../components/Canvas"; import { Header } from "../components/Header"; import { Sidebar } from "../components/Sidebar"; import { useLocalStorage } from "../hooks/useLocalStorage"; -import { Stack, TextField } from "@mui/material"; +import { Stack, TextField, Tooltip } from "@mui/material"; import { useAuth } from "../lib/auth"; import { initParser } from "../lib/parser"; +import { usePrompt } from "../lib/prompt"; + const DrawerWidth = 240; const SIDEBAR_KEY = "sidebar"; +const HeaderItem = memo(({ id }) => { + const store = useContext(RepoContext)!; + const repoName = useStore(store, (state) => state.repoName); + const repoNameDirty = useStore(store, (state) => state.repoNameDirty); + const setRepoName = useStore(store, (state) => state.setRepoName); + const apolloClient = useApolloClient(); + const remoteUpdateRepoName = useStore( + store, + (state) => state.remoteUpdateRepoName + ); + const role = useStore(store, (state) => state.role); + + usePrompt( + "Repo name not saved. Do you want to leave this page?", + repoNameDirty + ); + + useEffect(() => { + let intervalId = setInterval(() => { + remoteUpdateRepoName(apolloClient); + }, 1000); + return () => { + console.log("removing interval"); + clearInterval(intervalId); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [focus, setFocus] = useState(false); + const [enter, setEnter] = useState(false); + + const textfield = ( + { + setFocus(true); + }} + onKeyDown={(e) => { + if (["Enter", "Escape"].includes(e.key)) { + e.preventDefault(); + setFocus(false); + } + }} + onMouseEnter={() => { + setEnter(true); + }} + onMouseLeave={() => { + setEnter(false); + }} + autoFocus={focus ? true : false} + onBlur={() => { + setFocus(false); + }} + InputProps={{ + ...(focus + ? {} + : { + disableUnderline: true, + }), + }} + sx={{ + maxWidth: "100%", + border: "none", + }} + disabled={role !== RoleType.OWNER} + onChange={(e) => { + const name = e.target.value; + setRepoName(name); + }} + /> + ); + + return ( + + {!focus && enter ? ( + + {textfield} + + ) : ( + textfield + )} + {repoNameDirty && saving..} + + ); +}); + function RepoWrapper({ children, id }) { // this component is used to provide a foldable layout const [open, setOpen] = useLocalStorage(SIDEBAR_KEY, true); const store = useContext(RepoContext); if (!store) throw new Error("Missing BearContext.Provider in the tree"); - const repoName = useStore(store, (state) => state.repoName); - const setRepoName = useStore(store, (state) => state.setRepoName); - const setShareOpen = useStore(store, (state) => state.setShareOpen); - const role = useStore(store, (state) => state.role); - const [updateRepo, { error }] = useMutation( - gql` - mutation UpdateRepo($id: ID!, $name: String) { - updateRepo(id: $id, name: $name) - } - `, - { refetchQueries: ["GetRepos", "GetCollabRepos"] } - ); + const setShareOpen = useStore(store, (state) => state.setShareOpen); return ( - { - const name = e.target.value; - setRepoName(name); - updateRepo({ - variables: { - id, - name, - }, - }); - }} - /> - {error && ERROR: {error.message}} - - } + breadcrumbItem={} shareButton={