Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Workspace CRUD #261

Merged
merged 30 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
92a6127
- workspace update name
jashanbhullar Jun 18, 2024
287fcb5
- setCurrentWorkspace to update in navigation
jashanbhullar Jun 19, 2024
029e57c
- role type fix
jashanbhullar Jun 19, 2024
d80e5a8
- typescript errors
jashanbhullar Jun 19, 2024
b810efd
- Minor changes to type
jashanbhullar Jun 19, 2024
20fffbf
- search user query implementation
jashanbhullar Jun 19, 2024
18a6c9e
- update type on Role and related changes
jashanbhullar Jun 19, 2024
d76262f
- type error
jashanbhullar Jun 19, 2024
b3c1745
- language stuff
jashanbhullar Jun 19, 2024
036d20f
- updated workspace crud
jashanbhullar Jun 19, 2024
df32b20
- updated comment
jashanbhullar Jun 19, 2024
98301e0
Merge branch 'main' of github.com:reearth/reearth-flow into feat(ui)/…
jashanbhullar Jun 19, 2024
e90819e
- sort the members
jashanbhullar Jun 19, 2024
84f2a86
- a single comma can ruin the layout
jashanbhullar Jun 19, 2024
1f3d267
- minor changes
jashanbhullar Jun 20, 2024
81cd26a
- revert back
jashanbhullar Jun 20, 2024
fe433a8
- an actual bug
jashanbhullar Jun 20, 2024
245f9f7
Merge branch 'main' of github.com:reearth/reearth-flow into feat(ui)/…
jashanbhullar Jun 20, 2024
f967436
Merge branch 'main' of github.com:reearth/reearth-flow into feat(ui)/…
jashanbhullar Jun 20, 2024
b4fd0d0
- using local cache rather than api calls
jashanbhullar Jun 20, 2024
2614e90
- fixes and fixes
jashanbhullar Jun 20, 2024
3c9a3a2
- remove workspace description
jashanbhullar Jun 20, 2024
c7d5e28
- added minor changes
jashanbhullar Jun 20, 2024
72b0a0e
- Minor refactor
jashanbhullar Jun 20, 2024
7a65653
Merge branch 'main' of github.com:reearth/reearth-flow into feat(ui)/…
jashanbhullar Jun 20, 2024
272dcc9
- minor fix
jashanbhullar Jun 20, 2024
3a2b617
- added a comment
jashanbhullar Jun 20, 2024
a19129e
Merge branch 'main' of github.com:reearth/reearth-flow into feat(ui)/…
jashanbhullar Jul 1, 2024
fcff46d
- minor change
jashanbhullar Jul 1, 2024
7f28272
- removed flatmap
jashanbhullar Jul 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ui/src/features/NotFoundPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ type Props = {

const NotFoundPage: React.FC<Props> = ({ message }) => {
const t = useT();
const { getMe } = useUser();
const { me } = getMe();
const { useGetMe } = useUser();
const { me } = useGetMe();

return (
<div className="bg-zinc-800 h-[100vh] flex justify-center items-center">
Expand Down
5 changes: 2 additions & 3 deletions ui/src/features/PageWrapper/WorkspaceId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Props = {
};

const WorkspaceIdWrapper: React.FC<Props> = ({ children }) => {
const [currentWorkspace, setCurrentWorkspace] = useCurrentWorkspace();
const [_, setCurrentWorkspace] = useCurrentWorkspace();

const { workspaceId }: { workspaceId: string } = useParams({
strict: false,
Expand All @@ -23,11 +23,10 @@ const WorkspaceIdWrapper: React.FC<Props> = ({ children }) => {

useEffect(() => {
if (!workspace) return;
if (currentWorkspace?.id === workspace.id) return;
setCurrentWorkspace(workspace);

return;
}, [workspace, currentWorkspace, setCurrentWorkspace]);
}, [workspace, setCurrentWorkspace]);

if (isLoading) return <Loading />;

Expand Down
8 changes: 4 additions & 4 deletions ui/src/features/TopNavigation/components/UserNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ const UserNavigation: React.FC<Props> = ({
const t = useT();
const [, setDialogType] = useDialogType();
const { logout: handleLogout, user } = useAuth();
const { getMe } = useUser();
const data = getMe().me;
const { useGetMe } = useUser();
const { me } = useGetMe();

const { tosUrl, documentationUrl } = config();

Expand All @@ -47,12 +47,12 @@ const UserNavigation: React.FC<Props> = ({
<div className={`flex gap-2 mr-2 ${className}`}>
<Avatar className="h-8 w-8">
<AvatarImage src={user?.picture} />
<AvatarFallback>{data?.name ? data?.name.charAt(0).toUpperCase() : "F"}</AvatarFallback>
<AvatarFallback>{me?.name ? me.name.charAt(0).toUpperCase() : "F"}</AvatarFallback>
</Avatar>
{!iconOnly ? (
<div className="flex items-center gap-2 self-center">
<p className="text-zinc-300 text-sm font-extralight max-w-28 truncate transition-all delay-0 duration-500 hover:max-w-[30vw] hover:delay-500">
{data?.name ? data?.name : "User"}
{me?.name ? me.name : "User"}
</p>
<CaretDown className="w-[12px]" weight="thin" />
</div>
Expand Down
61 changes: 41 additions & 20 deletions ui/src/features/WorkspaceSettings/components/GeneralSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
import { useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useEffect, useState } from "react";

import { Button, Input, Label } from "@flow/components";
import { useWorkspace } from "@flow/lib/gql";
import { useT } from "@flow/lib/i18n";
import { useCurrentWorkspace } from "@flow/stores";

type Errors = "delete" | "update";

const GeneralSettings: React.FC = () => {
const t = useT();
const [currentWorkspace] = useCurrentWorkspace();
const { deleteWorkspace } = useWorkspace();
const { deleteWorkspace, updateWorkspace } = useWorkspace();
const navigate = useNavigate();
const [showError, setShowError] = useState(false);
const [showError, setShowError] = useState<Errors | undefined>(undefined);
const [workspaceName, setWorkspaceName] = useState(currentWorkspace?.name);
const [loading, setLoading] = useState(false);

const handleDeleteWorkspace = async () => {
setShowError(false);
setLoading(true);
jashanbhullar marked this conversation as resolved.
Show resolved Hide resolved
setShowError(undefined);
if (!currentWorkspace) return;
// TODO: this trigger a pop up for confirming
// TODO: this should trigger a pop up for confirming
KaWaite marked this conversation as resolved.
Show resolved Hide resolved
const { workspaceId } = await deleteWorkspace(currentWorkspace.id);

if (!workspaceId) {
setShowError(true);
setShowError("delete");
return;
}
navigate({ to: "/workspace" });
};

const handleUpdateWorkspace = async () => {
setLoading(true);
setShowError(undefined);
if (!currentWorkspace?.id || !workspaceName) return;
const { workspace } = await updateWorkspace(currentWorkspace?.id, workspaceName);
setLoading(false);
if (!workspace) {
setShowError("update");
return;
}
};
Comment on lines +34 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle Errors in Update Workspace

The handleUpdateWorkspace function should also handle potential errors from the updateWorkspace mutation, such as network issues or API errors, and provide user feedback accordingly.

-    const { workspace } = await updateWorkspace(currentWorkspace?.id, workspaceName);
+    try {
+      const { workspace } = await updateWorkspace(currentWorkspace?.id, workspaceName);
+      setLoading(false);
+      if (!workspace) {
+        setShowError("update");
+        return;
+      }
+    } catch (error) {
+      setLoading(false);
+      setShowError("update");
+    }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleUpdateWorkspace = async () => {
setLoading(true);
setShowError(undefined);
if (!currentWorkspace?.id || !workspaceName) return;
const { workspace } = await updateWorkspace(currentWorkspace?.id, workspaceName);
setLoading(false);
if (!workspace) {
setShowError("update");
return;
}
};
const handleUpdateWorkspace = async () => {
setLoading(true);
setShowError(undefined);
if (!currentWorkspace?.id || !workspaceName) return;
try {
const { workspace } = await updateWorkspace(currentWorkspace?.id, workspaceName);
setLoading(false);
if (!workspace) {
setShowError("update");
return;
}
} catch (error) {
setLoading(false);
setShowError("update");
}
};


// currentWorkspace can be changed from the navigation
useEffect(() => {
setWorkspaceName(currentWorkspace?.name);
}, [currentWorkspace]);

return (
<div>
<p className="text-lg font-extralight">{t("General Settings")}</p>
Expand All @@ -34,29 +57,27 @@ const GeneralSettings: React.FC = () => {
<Input
id="workspace-name"
placeholder={t("Workspace Name")}
readOnly={true}
disabled={true}
value={currentWorkspace?.name}
disabled={currentWorkspace?.personal || loading}
value={workspaceName}
onChange={e => setWorkspaceName(e.target.value)}
/>
</div>
{/* <div className="flex flex-col gap-2">
<Label htmlFor="workspace-description">{t("Workspace Description")}</Label>
<Input
id="workspace-description"
placeholder={t("Workspace Description")}
defaultValue={currentWorkspace?.description}
/>
</div> */}
<Button className="self-end">{t("Save")}</Button>
<Button
className="self-end"
disabled={loading || !workspaceName || currentWorkspace?.personal}
onClick={handleUpdateWorkspace}>
{t("Save")}
</Button>
<Button
variant={"destructive"}
disabled={currentWorkspace?.personal}
disabled={currentWorkspace?.personal || loading}
className="self-end"
onClick={() => handleDeleteWorkspace()}>
{t("Delete Workspace")}
</Button>
<div className={`text-xs text-red-400 self-end ${showError ? "opacity-70" : "opacity-0"}`}>
{t("Failed to delete workspace")}
{showError === "delete" && t("Failed to delete Workspace")}
{showError === "update" && t("Failed to update Workspace")}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { IntegrationMember } from "@flow/types/integration";

type Filter = "all" | Role;

const roles = ["admin", "reader", "writer"];
const roles: Role[] = Object.values(Role);

const IntegrationsSettings: React.FC = () => {
const t = useT();
Expand All @@ -26,9 +26,10 @@ const IntegrationsSettings: React.FC = () => {

const filters: { id: Filter; title: string }[] = [
{ id: "all", title: t("All") },
{ id: "admin", title: t("Admin") },
{ id: "reader", title: t("Reader") },
{ id: "writer", title: t("Writer") },
{ id: Role.Owner, title: t("Owner") },
{ id: Role.Reader, title: t("Reader") },
{ id: Role.Reader, title: t("Maintainer") },
{ id: Role.Writer, title: t("Writer") },
];

const integrations: IntegrationMember[] =
Expand Down
161 changes: 98 additions & 63 deletions ui/src/features/WorkspaceSettings/components/MembersSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,87 +3,109 @@ import { useState } from "react";

import {
Button,
Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Input,
} from "@flow/components";
import { useUser, useWorkspace } from "@flow/lib/gql";
import { useT } from "@flow/lib/i18n";
import { useCurrentWorkspace } from "@flow/stores";
import { Role, UserMember } from "@flow/types";

type Filter = "all" | Role;

const roles = ["admin", "reader", "writer"];
const roles: Role[] = Object.values(Role);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix role duplication and ensure consistency.

The Role array contains all roles, including Role.Maintainer.

const roles: Role[] = Object.values(Role);

Committable suggestion was skipped due to low confidence.


const MembersSettings: React.FC = () => {
const t = useT();
const [currentWorkspace] = useCurrentWorkspace();

const { addMemberToWorkspace, removeMemberFromWorkspace, updateMemberOfWorkspace } =
useWorkspace();
const { searchUser, useGetMe } = useUser();
const [email, setEmail] = useState<string>("");
const [currentFilter, setFilter] = useState<Filter>("all");
const [error, setError] = useState<string | undefined>();

const { me } = useGetMe();

const filters: { id: Filter; title: string }[] = [
{ id: "all", title: t("All") },
{ id: "admin", title: t("Admin") },
{ id: "reader", title: t("Reader") },
{ id: "writer", title: t("Writer") },
{ id: Role.Owner, title: t("Owner") },
{ id: Role.Reader, title: t("Reader") },
{ id: Role.Maintainer, title: t("Maintainer") },
{ id: Role.Writer, title: t("Writer") },
];

const members =
(currentWorkspace?.members?.filter(
m => "userId" in m && (currentFilter !== "all" ? m.role === currentFilter : true),
) as UserMember[]) ?? [];
const members = currentWorkspace?.members?.filter(
m => "userId" in m && (currentFilter === "all" || m.role === currentFilter),
) as UserMember[];

const handleAddMember = async (email: string) => {
setError(undefined);
if (!currentWorkspace?.id) return;
const { user } = await searchUser(email);
if (!user) {
setError(t("Could not find the user"));
return;
}
const { workspace } = await addMemberToWorkspace(currentWorkspace.id, user.id, Role.Reader);

Comment on lines +56 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle potential errors in async functions.

Wrap the async function calls in try-catch blocks to handle potential errors.

  const handleAddMember = async (email: string) => {
    setError(undefined);
    if (!currentWorkspace?.id) return;
    try {
      const { user } = await searchUser(email);
      if (!user) {
        setError(t("Could not find the user"));
        return;
      }
      const { workspace } = await addMemberToWorkspace(currentWorkspace.id, user.id, Role.Reader);
      if (!workspace) {
        setError(t("Failed to add member"));
        return;
      }
      setEmail("");
    } catch (error) {
      setError(t("An error occurred while adding the member"));
    }
  };
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { user } = await searchUser(email);
if (!user) {
setError(t("Could not find the user"));
return;
}
const { workspace } = await addMemberToWorkspace(currentWorkspace.id, user.id, Role.Reader);
const handleAddMember = async (email: string) => {
setError(undefined);
if (!currentWorkspace?.id) return;
try {
const { user } = await searchUser(email);
if (!user) {
setError(t("Could not find the user"));
return;
}
const { workspace } = await addMemberToWorkspace(currentWorkspace.id, user.id, Role.Reader);
if (!workspace) {
setError(t("Failed to add member"));
return;
}
setEmail("");
} catch (error) {
setError(t("An error occurred while adding the member"));
}
};

if (!workspace) {
setError(t("Failed to add member"));
return;
}
setEmail("");
};

const handleChangeRole = async (userId: string, role: Role) => {
setError(undefined);
if (!currentWorkspace?.id) return;
const { workspace } = await updateMemberOfWorkspace(currentWorkspace.id, userId, role);
if (!workspace) {
setError(t("Failed to change role of the member"));
return;
}
};
Comment on lines +70 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle potential errors in async functions.

Wrap the async function calls in try-catch blocks to handle potential errors.

  const handleChangeRole = async (userId: string, role: Role) => {
    setError(undefined);
    if (!currentWorkspace?.id) return;
    try {
      const { workspace } = await updateMemberOfWorkspace(currentWorkspace.id, userId, role);
      if (!workspace) {
        setError(t("Failed to change role of the member"));
        return;
      }
    } catch (error) {
      setError(t("An error occurred while changing the role"));
    }
  };
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleChangeRole = async (userId: string, role: Role) => {
setError(undefined);
if (!currentWorkspace?.id) return;
const { workspace } = await updateMemberOfWorkspace(currentWorkspace.id, userId, role);
if (!workspace) {
setError(t("Failed to change role of the member"));
return;
}
};
const handleChangeRole = async (userId: string, role: Role) => {
setError(undefined);
if (!currentWorkspace?.id) return;
try {
const { workspace } = await updateMemberOfWorkspace(currentWorkspace.id, userId, role);
if (!workspace) {
setError(t("Failed to change role of the member"));
return;
}
} catch (error) {
setError(t("An error occurred while changing the role"));
}
};


const [selectedMembers, setSelectedMembers] = useState<string[]>([]);
const handleRemoveMembers = async (userId: string) => {
setError(undefined);
if (!currentWorkspace?.id) return;
const { workspace } = await removeMemberFromWorkspace(currentWorkspace.id, userId);
if (!workspace) {
setError(t("Failed to remove member"));
return;
}
};
Comment on lines +80 to +88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle potential errors in async functions.

Wrap the async function calls in try-catch blocks to handle potential errors.

  const handleRemoveMembers = async (userId: string) => {
    setError(undefined);
    if (!currentWorkspace?.id) return;
    try {
      const { workspace } = await removeMemberFromWorkspace(currentWorkspace.id, userId);
      if (!workspace) {
        setError(t("Failed to remove member"));
        return;
      }
    } catch (error) {
      setError(t("An error occurred while removing the member"));
    }
  };
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleRemoveMembers = async (userId: string) => {
setError(undefined);
if (!currentWorkspace?.id) return;
const { workspace } = await removeMemberFromWorkspace(currentWorkspace.id, userId);
if (!workspace) {
setError(t("Failed to remove member"));
return;
}
};
const handleRemoveMembers = async (userId: string) => {
setError(undefined);
if (!currentWorkspace?.id) return;
try {
const { workspace } = await removeMemberFromWorkspace(currentWorkspace.id, userId);
if (!workspace) {
setError(t("Failed to remove member"));
return;
}
} catch (error) {
setError(t("An error occurred while removing the member"));
}
};


return (
<div>
<div className="flex flex-col gap-6 mt-4 max-w-[800px]">
<div className="flex justify-between">
<p className="text-lg font-extralight">{t("Members Settings")}</p>
<Button>{t("Add Members")}</Button>
</div>
<div className="flex justify-between items-center">
{/* TODO: This will be a dialog component */}
<Input
className="w-2/4"
placeholder={t("Enter email")}
value={email}
disabled={currentWorkspace?.personal}
onChange={e => setEmail(e.target.value)}
/>
<Button
onClick={() => handleAddMember(email)}
disabled={!email || currentWorkspace?.personal}>
{t("Add Member")}
</Button>
</div>
<div className="border border-zinc-700 rounded font-extralight">
<div className="flex justify-between items-center gap-2 p-2 border-b border-zinc-700 h-[42px]">
<div className="flex items-center gap-2">
<Checkbox
className="border-zinc-600 mx-2"
checked={selectedMembers.length === members.length}
onClick={() =>
setSelectedMembers(
selectedMembers.length !== members.length ? members.map(m => m.userId) : [],
)
}
/>
<User weight="thin" />
<p>
{selectedMembers.length
? `${selectedMembers.length} ${selectedMembers.length === 1 ? t("member selected") : t("members selected")}`
: `${members.length} ${t("Members")}`}
</p>
<p>{`${members.length} ${t("Members")}`}</p>
</div>
{selectedMembers.length > 0 && (
<div className="flex gap-4">
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-1">
<p className="text-sm">{t("Change role")}</p>
<CaretDown className="w-2 h-2" />
</DropdownMenuTrigger>

<DropdownMenuContent className="min-w-[70px]">
{roles.map((role, idx) => (
<DropdownMenuItem key={idx} onClick={() => console.log(role)}>
{role}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Button className="h-[25px]" size="sm" variant="destructive">
{t("Remove selected")}
</Button>
</div>
)}
<div>
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-2">
Expand All @@ -105,26 +127,39 @@ const MembersSettings: React.FC = () => {
</div>
</div>
<div className="max-h-[50vh] overflow-auto">
{members.map(member => (
<div key={member.userId} className="flex items-center gap-4 px-4 py-2">
<Checkbox
className="border-zinc-600"
checked={selectedMembers.includes(member.userId)}
onClick={() =>
setSelectedMembers(prev =>
prev.includes(member.userId)
? [...prev.filter(pm => pm !== member.userId)]
: [...prev, member.userId],
)
}
/>
<p>{member.user.name}</p>
<p className="px-4 font-thin capitalize text-sm">{member.role}</p>
{members.map(m => (
<div key={m.userId} className="flex gap-4 px-4 py-2">
<p className="flex-1">{m.user?.name}</p>
<p className="flex-1 px-4 font-thin capitalize text-sm">{m.role}</p>
<DropdownMenu>
<DropdownMenuTrigger
disabled={m.userId === me?.id}
className={`flex-1 flex items-center gap-1 ${m.userId === me?.id ? "opacity-50" : ""}`}>
<p className="text-sm">{t("Change role")}</p>
<CaretDown className="w-2 h-2" />
</DropdownMenuTrigger>

<DropdownMenuContent className="min-w-[70px]">
{roles.map((role, idx) => (
<DropdownMenuItem key={idx} onClick={() => handleChangeRole(m.userId, role)}>
{role}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<Button
className="flex-1 h-[25px]"
size="sm"
variant="outline"
disabled={m.userId === me?.id}
onClick={() => handleRemoveMembers(m.userId)}>
{t("Remove")}
</Button>
</div>
))}
</div>
</div>
<Button className="self-end">{t("Save")}</Button>
<p className="text-sm text-red-400">{error}</p>
</div>
</div>
);
Expand Down
Loading
Loading