diff --git a/web/src/beta/features/AccountSetting/index.tsx b/web/src/beta/features/AccountSetting/index.tsx index 3cd239cc58..18c6ab1980 100644 --- a/web/src/beta/features/AccountSetting/index.tsx +++ b/web/src/beta/features/AccountSetting/index.tsx @@ -50,65 +50,67 @@ const AccountSetting: FC = () => { ]; return ( - - - - - - - + <> + + + + + + + - - {t("Password")} - - - { - setChangePasswordModal(true); - }} - size="medium" - hasBorder={true} - /> - - - { - handleUpdateUserLanguage({ lang: value as string }); - }} - /> - - - + + {t("Password")} + + + { + setChangePasswordModal(true); + }} + size="medium" + hasBorder={true} + /> + + + { + handleUpdateUserLanguage({ lang: value as string }); + }} + /> + + + - setChangePasswordModal(false)} - handleUpdateUserPassword={handleUpdateUserPassword} - /> - + setChangePasswordModal(false)} + handleUpdateUserPassword={handleUpdateUserPassword} + /> + + - + ); }; export default AccountSetting; diff --git a/web/src/beta/features/Dashboard/ContentsContainer/Members/ListItem.tsx b/web/src/beta/features/Dashboard/ContentsContainer/Members/ListItem.tsx index d273d5c5de..afd6ec981c 100644 --- a/web/src/beta/features/Dashboard/ContentsContainer/Members/ListItem.tsx +++ b/web/src/beta/features/Dashboard/ContentsContainer/Members/ListItem.tsx @@ -1,10 +1,9 @@ import { Typography } from "@reearth/beta/lib/reearth-ui"; +import { TeamMember } from "@reearth/services/gql"; import { styled } from "@reearth/services/theme"; import { FC } from "react"; -import { Member } from "../../type"; - -const ListItem: FC<{ member: Member }> = ({ member }) => { +const ListItem: FC<{ member: TeamMember }> = ({ member }) => { return ( diff --git a/web/src/beta/features/Dashboard/type.ts b/web/src/beta/features/Dashboard/type.ts index f90f19dcc0..9eff64c7b8 100644 --- a/web/src/beta/features/Dashboard/type.ts +++ b/web/src/beta/features/Dashboard/type.ts @@ -1,4 +1,5 @@ import { IconName } from "@reearth/beta/lib/reearth-ui"; +import { TeamMember } from "@reearth/services/gql"; import { ProjectType } from "@reearth/types"; export type Project = { @@ -47,7 +48,7 @@ export type Member = { export type Workspace = { id: string; name: string; - members?: Member[]; + members?: TeamMember[]; policyId?: string | null; policy?: { id: string; name: string } | null; personal?: boolean; diff --git a/web/src/beta/features/WorkspaceSetting/hooks.ts b/web/src/beta/features/WorkspaceSetting/hooks.ts index b0400e3b99..b2a6f872c6 100644 --- a/web/src/beta/features/WorkspaceSetting/hooks.ts +++ b/web/src/beta/features/WorkspaceSetting/hooks.ts @@ -1,9 +1,9 @@ -import { useWorkspaceFetcher } from "@reearth/services/api"; +import { useMeFetcher, useWorkspaceFetcher } from "@reearth/services/api"; import { Role } from "@reearth/services/gql"; import { useCallback } from "react"; export type WorkspacePayload = { - name: string; + name?: string; userId?: string; teamId: string; role?: Role; @@ -24,16 +24,14 @@ export default () => { useUpdateWorkspace, useDeleteWorkspace, useAddMemberToWorkspace, - useRemoveMemberFromWorkspace + useRemoveMemberFromWorkspace, + useUpdateMemberOfWorkspace } = useWorkspaceFetcher(); // Fetch a specific workspace const handleFetchWorkspace = useCallback( (workspaceId: string) => { const { workspace, loading, error } = useWorkspaceQuery(workspaceId); - if (error) { - console.error("Failed to fetch workspace:", error); - } return { workspace, loading, error }; }, [useWorkspaceQuery] @@ -42,22 +40,14 @@ export default () => { // Fetch all workspaces const handleFetchWorkspaces = useCallback(() => { const { workspaces, loading, error } = useWorkspacesQuery(); - if (error) { - console.error("Failed to fetch workspaces:", error); - } return { workspaces, loading, error }; }, [useWorkspacesQuery]); // Create a new workspace const handleCreateWorkspace = useCallback( async ({ name }: WorkspacePayload) => { - try { - const { status } = await useCreateWorkspace(name); - if (status === "success") { - console.log("Workspace created successfully"); - } - } catch (error) { - console.error("Failed to create workspace:", error); + if (name) { + await useCreateWorkspace(name); } }, [useCreateWorkspace] @@ -66,13 +56,8 @@ export default () => { // Update an existing workspace const handleUpdateWorkspace = useCallback( async ({ teamId, name }: WorkspacePayload) => { - try { - const { status } = await useUpdateWorkspace(teamId, name); - if (status === "success") { - console.log("Workspace updated successfully"); - } - } catch (error) { - console.error("Failed to update workspace:", error); + if (name && teamId) { + await useUpdateWorkspace(teamId, name); } }, [useUpdateWorkspace] @@ -81,13 +66,8 @@ export default () => { // Delete a workspace const handleDeleteWorkspace = useCallback( async (teamId: string) => { - try { - const { status } = await useDeleteWorkspace(teamId); - if (status === "success") { - console.log("Workspace deleted successfully"); - } - } catch (error) { - console.error("Failed to delete workspace:", error); + if (teamId) { + await useDeleteWorkspace(teamId); } }, [useDeleteWorkspace] @@ -96,19 +76,8 @@ export default () => { // Add a member to a workspace const handleAddMemberToWorkspace = useCallback( async ({ teamId, userId, role }: WorkspacePayload) => { - try { - if (userId && role) { - const { status } = await useAddMemberToWorkspace( - teamId, - userId, - role - ); - if (status === "success") { - console.log("Member added successfully"); - } - } - } catch (error) { - console.error("Failed to add member to workspace:", error); + if (userId && role) { + await useAddMemberToWorkspace(teamId, userId, role); } }, [useAddMemberToWorkspace] @@ -117,20 +86,35 @@ export default () => { // Remove a member from a workspace const handleRemoveMemberFromWorkspace = useCallback( async ({ teamId, userId }: WorkspacePayload) => { - try { - if (userId) { - const { status } = await useRemoveMemberFromWorkspace(teamId, userId); - if (status === "success") { - console.log("Member removed successfully"); - } - } - } catch (error) { - console.error("Failed to remove member from workspace:", error); + if (userId) { + await useRemoveMemberFromWorkspace(teamId, userId); } }, [useRemoveMemberFromWorkspace] ); + // update a member of workspace + const handleUpdateMemberOfWorkspace = useCallback( + async ({ teamId, userId, role }: WorkspacePayload) => { + if (userId && role) { + await useUpdateMemberOfWorkspace(teamId, userId, role); + } + }, + [useUpdateMemberOfWorkspace] + ); + + const { useSearchUser } = useMeFetcher(); + const handleSearchUser = useCallback( + (nameOrEmail: string) => { + const { user, status } = useSearchUser(nameOrEmail, { + skip: !nameOrEmail + }); + + return { searchUser: user, searchUserStatus: status }; + }, + [useSearchUser] + ); + return { handleFetchWorkspace, handleFetchWorkspaces, @@ -138,6 +122,8 @@ export default () => { handleUpdateWorkspace, handleDeleteWorkspace, handleAddMemberToWorkspace, - handleRemoveMemberFromWorkspace + handleRemoveMemberFromWorkspace, + handleUpdateMemberOfWorkspace, + handleSearchUser }; }; diff --git a/web/src/beta/features/WorkspaceSetting/index.tsx b/web/src/beta/features/WorkspaceSetting/index.tsx index cb7b11cffe..90853f5937 100644 --- a/web/src/beta/features/WorkspaceSetting/index.tsx +++ b/web/src/beta/features/WorkspaceSetting/index.tsx @@ -6,6 +6,7 @@ import CursorStatus from "../CursorStatus"; import useProjectsHook from "../Dashboard/ContentsContainer/Projects/hooks"; import useWorkspaceHook from "./hooks"; +import Members from "./innerPages/Members/Members"; import Workspace from "./innerPages/Workspaces/Workspaces"; type Props = { @@ -13,11 +14,20 @@ type Props = { workspaceId?: string; }; +enum TABS { + WORKSPACE = "workspace", + MEMBERS = "members" +} + const WorkspaceSetting: FC = ({ tab, workspaceId }) => { const { handleFetchWorkspaces, handleUpdateWorkspace, - handleDeleteWorkspace + handleDeleteWorkspace, + handleAddMemberToWorkspace, + handleSearchUser, + handleUpdateMemberOfWorkspace, + handleRemoveMemberFromWorkspace } = useWorkspaceHook(); const { filtedProjects } = useProjectsHook(workspaceId); @@ -27,7 +37,7 @@ const WorkspaceSetting: FC = ({ tab, workspaceId }) => { return ( <> - {tab === "workspace" && ( + {tab === TABS.WORKSPACE && ( = ({ tab, workspaceId }) => { projectsCount={filtedProjects?.length} /> )} + {tab === TABS.MEMBERS && ( + + )} diff --git a/web/src/beta/features/WorkspaceSetting/innerPages/Members/Members.tsx b/web/src/beta/features/WorkspaceSetting/innerPages/Members/Members.tsx new file mode 100644 index 0000000000..d92a3f58ad --- /dev/null +++ b/web/src/beta/features/WorkspaceSetting/innerPages/Members/Members.tsx @@ -0,0 +1,472 @@ +import { + ButtonWrapper, + InnerPage, + SettingsWrapper +} from "@reearth/beta/features/ProjectSettings/innerPages/common"; +import { + Collapse, + Button, + Modal, + Typography, + ModalPanel, + TextInput, + IconButton, + Icon, + PopupMenu +} from "@reearth/beta/lib/reearth-ui"; +import { SelectField } from "@reearth/beta/ui/fields"; +import { metricsSizes } from "@reearth/beta/utils/metrics"; +import { Role } from "@reearth/services/gql"; +import { useT } from "@reearth/services/i18n"; +import { useWorkspace } from "@reearth/services/state"; +import { styled, useTheme, keyframes } from "@reearth/services/theme"; +import { FC, KeyboardEvent, useEffect, useState } from "react"; +import { Fragment } from "react/jsx-runtime"; + +import { WorkspacePayload } from "../../hooks"; + +type Props = { + handleAddMemberToWorkspace: ({ + teamId, + userId, + role + }: WorkspacePayload) => Promise; + handleSearchUser: (nameOrEmail: string) => + | { + searchUser: { + __typename?: "User"; + id: string; + name: string; + email: string; + } | null; + searchUserStatus: string; + error?: undefined; + } + | { + error: unknown; + searchUser?: undefined; + searchUserStatus?: undefined; + }; + handleUpdateMemberOfWorkspace: ({ + teamId, + userId, + role + }: WorkspacePayload) => Promise; + handleRemoveMemberFromWorkspace: ({ + teamId, + userId + }: WorkspacePayload) => Promise; +}; + +type MemberData = { + id: string; + role: Role; + username?: string; + email?: string; +}; + +type MembersData = MemberData[]; + +type MemberSearchResult = { + userName: string; + email: string; + id: string; +}; + +const Members: FC = ({ + handleSearchUser, + handleAddMemberToWorkspace, + handleUpdateMemberOfWorkspace, + handleRemoveMemberFromWorkspace +}) => { + const theme = useTheme(); + const t = useT(); + const roles = [ + { value: "READER", label: t("Reader") }, + { value: "WRITER", label: t("Writer") }, + { value: "MAINTAINER", label: t("Maintainer") }, + { value: "OWNER", label: t("Owner") } + ]; + + const [currentWorkspace] = useWorkspace(); + const [workspaceMembers, setWorkspaceMembers] = useState([]); + + useEffect(() => { + setWorkspaceMembers( + currentWorkspace?.members + ?.filter((m) => !!m.user) + .map((member) => ({ + id: member.userId, + role: member.role, + username: member.user?.name, + email: member.user?.email + })) ?? [] + ); + }, [currentWorkspace]); + + const [addMemberModal, setAddMemberModal] = useState(false); + const [activeEditIndex, setActiveEditIndex] = useState(null); + + const [memberSearchInput, setMemberSearchInput] = useState(""); + const [debouncedInput, setDebouncedInput] = + useState(memberSearchInput); + const [memberSearchResults, setMemberSearchResults] = useState< + MemberSearchResult[] + >([]); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedInput(memberSearchInput.trim()); + }, 1000); + + return () => { + clearTimeout(handler); + }; + }, [memberSearchInput]); + + const { searchUser, searchUserStatus } = handleSearchUser(debouncedInput); + + useEffect(() => { + if ( + searchUser && + "name" in searchUser && + !memberSearchResults.find( + (memberSearchResult) => memberSearchResult.id === searchUser.id + ) + ) { + setMemberSearchResults([ + ...memberSearchResults, + { + userName: searchUser.name, + email: searchUser.email, + id: searchUser.id + } + ]); + } + }, [memberSearchResults, searchUser]); + + const handleNewMemberClick = () => { + setAddMemberModal(true); + }; + + const handleCloseAddMemberModal = () => { + setAddMemberModal(false); + }; + + const handleChangeRoleButtonClick = (index: number) => { + setActiveEditIndex((prevIndex) => (prevIndex === index ? null : index)); + }; + + const handleChangeRole = async ( + user: MemberData, + index: number, + roleValue: string | string[] + ) => { + if (currentWorkspace?.id) { + await handleUpdateMemberOfWorkspace({ + teamId: currentWorkspace?.id, + userId: user.id, + role: roleValue as Role + }); + setWorkspaceMembers((prevMembers) => { + return prevMembers.map((workspaceMember) => + workspaceMember.id === user.id + ? { + ...workspaceMember, + role: roleValue as Role + } + : workspaceMember + ); + }); + setActiveEditIndex((prevIndex) => (prevIndex === index ? null : index)); + } + }; + + const handleRemoveMemberButtonClick = (userId: string) => { + if (currentWorkspace?.id) { + handleRemoveMemberFromWorkspace({ + teamId: currentWorkspace?.id, + userId + }); + setWorkspaceMembers( + workspaceMembers.filter( + (workspaceMember) => workspaceMember.id !== userId + ) + ); + } + }; + + const handleAddMember = () => { + memberSearchResults.forEach((memberSearchResult) => { + if (currentWorkspace?.id) { + handleAddMemberToWorkspace({ + name: memberSearchResult.userName, + teamId: currentWorkspace?.id, + userId: memberSearchResult.id, + role: Role.Reader + }); + setWorkspaceMembers((prevMembers) => [ + ...prevMembers, + { + username: memberSearchResult.userName, + email: memberSearchResult.email, + role: Role.Reader, + id: memberSearchResult.id + } + ]); + setAddMemberModal(false); + setMemberSearchInput(""); + setDebouncedInput(""); + setMemberSearchResults([]); + } + }); + }; + + const handleDeleteUserForSearchResult = ( + memberSearchResult: MemberSearchResult + ) => { + setMemberSearchInput(""); + setDebouncedInput(""); + setMemberSearchResults( + memberSearchResults.filter( + (element) => element.id !== memberSearchResult.id + ) + ); + }; + + const handleUserSearchInputOnKeyDown = ( + e: KeyboardEvent + ) => { + if (e.key === "Enter" && memberSearchInput.trim() !== "") { + setMemberSearchInput(""); + setDebouncedInput(memberSearchInput.trim()); + } + }; + + return ( + + + + + +