diff --git a/front/src/App.tsx b/front/src/App.tsx index d4f190dea..be23b8ac9 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -19,6 +19,7 @@ import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMemb import { CompanyShow } from './pages/companies/CompanyShow'; import { PersonShow } from './pages/people/PersonShow'; +import { SettingsWorksapce } from './pages/settings/SettingsWorkspace'; import { AppInternalHooks } from './AppInternalHooks'; /** @@ -84,6 +85,10 @@ export function App() { path="workspace-members" element={} /> + } + /> } /> diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 0605f4080..db3442133 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -3376,6 +3376,11 @@ export type RemoveProfilePictureMutationVariables = Exact<{ export type RemoveProfilePictureMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: string } }; +export type GetCurrentWorkspaceQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetCurrentWorkspaceQuery = { __typename?: 'Query', currentWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, domainName?: string | null, logo?: string | null } }; + export type GetWorkspaceMembersQueryVariables = Exact<{ [key: string]: never; }>; @@ -5094,6 +5099,43 @@ export function useRemoveProfilePictureMutation(baseOptions?: Apollo.MutationHoo export type RemoveProfilePictureMutationHookResult = ReturnType; export type RemoveProfilePictureMutationResult = Apollo.MutationResult; export type RemoveProfilePictureMutationOptions = Apollo.BaseMutationOptions; +export const GetCurrentWorkspaceDocument = gql` + query GetCurrentWorkspace { + currentWorkspace { + id + displayName + domainName + logo + } +} + `; + +/** + * __useGetCurrentWorkspaceQuery__ + * + * To run a query within a React component, call `useGetCurrentWorkspaceQuery` and pass it any options that fit your needs. + * When your component renders, `useGetCurrentWorkspaceQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetCurrentWorkspaceQuery({ + * variables: { + * }, + * }); + */ +export function useGetCurrentWorkspaceQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetCurrentWorkspaceDocument, options); + } +export function useGetCurrentWorkspaceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetCurrentWorkspaceDocument, options); + } +export type GetCurrentWorkspaceQueryHookResult = ReturnType; +export type GetCurrentWorkspaceLazyQueryHookResult = ReturnType; +export type GetCurrentWorkspaceQueryResult = Apollo.QueryResult; export const GetWorkspaceMembersDocument = gql` query GetWorkspaceMembers { workspaceMembers: findManyWorkspaceMember { diff --git a/front/src/modules/settings/components/SettingsNavbar.tsx b/front/src/modules/settings/components/SettingsNavbar.tsx index a3b7b14dc..3b2e7fa3a 100644 --- a/front/src/modules/settings/components/SettingsNavbar.tsx +++ b/front/src/modules/settings/components/SettingsNavbar.tsx @@ -56,7 +56,6 @@ export function SettingsNavbar() { label="Members" to="/settings/workspace-members" icon={} - soon={false} active={ !!useMatch({ path: useResolvedPath('/settings/workspace-members').pathname, @@ -68,7 +67,6 @@ export function SettingsNavbar() { label="General" to="/settings/workspace" icon={} - soon={true} active={ !!useMatch({ path: useResolvedPath('/settings/workspace').pathname, diff --git a/front/src/modules/settings/workspace/components/NameField.tsx b/front/src/modules/settings/workspace/components/NameField.tsx new file mode 100644 index 000000000..55899b3c1 --- /dev/null +++ b/front/src/modules/settings/workspace/components/NameField.tsx @@ -0,0 +1,82 @@ +import { useCallback, useEffect, useState } from 'react'; +import { getOperationName } from '@apollo/client/utilities'; +import styled from '@emotion/styled'; +import debounce from 'lodash.debounce'; +import { useRecoilState } from 'recoil'; + +import { currentUserState } from '@/auth/states/currentUserState'; +import { TextInput } from '@/ui/components/inputs/TextInput'; +import { GET_CURRENT_USER } from '@/users/queries'; +import { useUpdateWorkspaceMutation } from '~/generated/graphql'; + +const StyledComboInputContainer = styled.div` + display: flex; + flex-direction: row; + > * + * { + margin-left: ${({ theme }) => theme.spacing(4)}; + } +`; + +type OwnProps = { + autoSave?: boolean; + onNameUpdate?: (name: string) => void; +}; + +export function NameField({ autoSave = true, onNameUpdate }: OwnProps) { + const [currentUser] = useRecoilState(currentUserState); + const workspace = currentUser?.workspaceMember?.workspace; + + const [displayName, setDisplayName] = useState(workspace?.displayName ?? ''); + + const [updateWorkspace] = useUpdateWorkspaceMutation(); + + // TODO: Enhance this with react-hook-form (https://www.react-hook-form.com) + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedUpdate = useCallback( + debounce(async (name: string) => { + if (onNameUpdate) { + onNameUpdate(displayName); + } + if (!autoSave || !name) { + return; + } + try { + const { data, errors } = await updateWorkspace({ + variables: { + data: { + displayName: { + set: name, + }, + }, + }, + refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''], + awaitRefetchQueries: true, + }); + + if (errors || !data?.updateWorkspace) { + throw errors; + } + } catch (error) { + console.error(error); + } + }, 500), + [updateWorkspace], + ); + + useEffect(() => { + debouncedUpdate(displayName); + return debouncedUpdate.cancel; + }, [debouncedUpdate, displayName]); + + return ( + + + + ); +} diff --git a/front/src/modules/ui/components/inputs/TextInput.tsx b/front/src/modules/ui/components/inputs/TextInput.tsx index 09e2e041e..45388080b 100644 --- a/front/src/modules/ui/components/inputs/TextInput.tsx +++ b/front/src/modules/ui/components/inputs/TextInput.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useState } from 'react'; +import { ChangeEvent } from 'react'; import styled from '@emotion/styled'; type OwnProps = Omit< @@ -52,16 +52,13 @@ export function TextInput({ fullWidth, ...props }: OwnProps): JSX.Element { - const [internalValue, setInternalValue] = useState(value); - return ( {label && {label}} ) => { - setInternalValue(event.target.value); if (onChange) { onChange(event.target.value); } diff --git a/front/src/modules/ui/components/inputs/__stories__/TextInput.stories.tsx b/front/src/modules/ui/components/inputs/__stories__/TextInput.stories.tsx index 20726111a..e1f29d057 100644 --- a/front/src/modules/ui/components/inputs/__stories__/TextInput.stories.tsx +++ b/front/src/modules/ui/components/inputs/__stories__/TextInput.stories.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { expect } from '@storybook/jest'; import { jest } from '@storybook/jest'; import type { Meta, StoryObj } from '@storybook/react'; @@ -17,9 +18,22 @@ type Story = StoryObj; const changeJestFn = jest.fn(); +function FakeTextInput({ onChange }: any) { + const [value, setValue] = useState('A good value '); + return ( + { + setValue(text); + onChange(text); + }} + /> + ); +} + export const Default: Story = { render: getRenderWrapperForComponent( - , + , ), play: async ({ canvasElement }) => { const canvas = within(canvasElement); diff --git a/front/src/pages/settings/SettingsWorkspace.tsx b/front/src/pages/settings/SettingsWorkspace.tsx new file mode 100644 index 000000000..23e9c621d --- /dev/null +++ b/front/src/pages/settings/SettingsWorkspace.tsx @@ -0,0 +1,46 @@ +import styled from '@emotion/styled'; + +import { NameField } from '@/settings/workspace/components/NameField'; +import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; +import { MainSectionTitle } from '@/ui/components/section-titles/MainSectionTitle'; +import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle'; +import { NoTopBarContainer } from '@/ui/layout/containers/NoTopBarContainer'; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + padding: ${({ theme }) => theme.spacing(8)}; + width: 350px; + > * + * { + margin-top: ${({ theme }) => theme.spacing(8)}; + } +`; + +const StyledSectionContainer = styled.div` + > * + * { + margin-top: ${({ theme }) => theme.spacing(4)}; + } +`; + +export function SettingsWorksapce() { + return ( + +
+ + General + + + + + + + + + +
+
+ ); +}