Upload Workspace logo during onboarding (#542)
* Upload image * Upload image * Fix tests * Remove pictures from seeds * Fix storybook * Fix storybook * Fix storybook
This commit is contained in:
@ -1044,6 +1044,7 @@ export type Mutation = {
|
|||||||
uploadFile: Scalars['String'];
|
uploadFile: Scalars['String'];
|
||||||
uploadImage: Scalars['String'];
|
uploadImage: Scalars['String'];
|
||||||
uploadProfilePicture: Scalars['String'];
|
uploadProfilePicture: Scalars['String'];
|
||||||
|
uploadWorkspaceLogo: Scalars['String'];
|
||||||
verify: Verify;
|
verify: Verify;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1162,6 +1163,11 @@ export type MutationUploadProfilePictureArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUploadWorkspaceLogoArgs = {
|
||||||
|
file: Scalars['Upload'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationVerifyArgs = {
|
export type MutationVerifyArgs = {
|
||||||
loginToken: Scalars['String'];
|
loginToken: Scalars['String'];
|
||||||
};
|
};
|
||||||
@ -3099,6 +3105,18 @@ export type UpdateWorkspaceMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null } };
|
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null } };
|
||||||
|
|
||||||
|
export type UploadWorkspaceLogoMutationVariables = Exact<{
|
||||||
|
file: Scalars['Upload'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type UploadWorkspaceLogoMutation = { __typename?: 'Mutation', uploadWorkspaceLogo: string };
|
||||||
|
|
||||||
|
export type RemoveWorkspaceLogoMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type RemoveWorkspaceLogoMutation = { __typename?: 'Mutation', updateWorkspace: { __typename?: 'Workspace', id: string } };
|
||||||
|
|
||||||
|
|
||||||
export const CreateEventDocument = gql`
|
export const CreateEventDocument = gql`
|
||||||
mutation CreateEvent($type: String!, $data: JSON!) {
|
mutation CreateEvent($type: String!, $data: JSON!) {
|
||||||
@ -4686,4 +4704,67 @@ export function useUpdateWorkspaceMutation(baseOptions?: Apollo.MutationHookOpti
|
|||||||
}
|
}
|
||||||
export type UpdateWorkspaceMutationHookResult = ReturnType<typeof useUpdateWorkspaceMutation>;
|
export type UpdateWorkspaceMutationHookResult = ReturnType<typeof useUpdateWorkspaceMutation>;
|
||||||
export type UpdateWorkspaceMutationResult = Apollo.MutationResult<UpdateWorkspaceMutation>;
|
export type UpdateWorkspaceMutationResult = Apollo.MutationResult<UpdateWorkspaceMutation>;
|
||||||
export type UpdateWorkspaceMutationOptions = Apollo.BaseMutationOptions<UpdateWorkspaceMutation, UpdateWorkspaceMutationVariables>;
|
export type UpdateWorkspaceMutationOptions = Apollo.BaseMutationOptions<UpdateWorkspaceMutation, UpdateWorkspaceMutationVariables>;
|
||||||
|
export const UploadWorkspaceLogoDocument = gql`
|
||||||
|
mutation UploadWorkspaceLogo($file: Upload!) {
|
||||||
|
uploadWorkspaceLogo(file: $file)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type UploadWorkspaceLogoMutationFn = Apollo.MutationFunction<UploadWorkspaceLogoMutation, UploadWorkspaceLogoMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useUploadWorkspaceLogoMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useUploadWorkspaceLogoMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useUploadWorkspaceLogoMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [uploadWorkspaceLogoMutation, { data, loading, error }] = useUploadWorkspaceLogoMutation({
|
||||||
|
* variables: {
|
||||||
|
* file: // value for 'file'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useUploadWorkspaceLogoMutation(baseOptions?: Apollo.MutationHookOptions<UploadWorkspaceLogoMutation, UploadWorkspaceLogoMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<UploadWorkspaceLogoMutation, UploadWorkspaceLogoMutationVariables>(UploadWorkspaceLogoDocument, options);
|
||||||
|
}
|
||||||
|
export type UploadWorkspaceLogoMutationHookResult = ReturnType<typeof useUploadWorkspaceLogoMutation>;
|
||||||
|
export type UploadWorkspaceLogoMutationResult = Apollo.MutationResult<UploadWorkspaceLogoMutation>;
|
||||||
|
export type UploadWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions<UploadWorkspaceLogoMutation, UploadWorkspaceLogoMutationVariables>;
|
||||||
|
export const RemoveWorkspaceLogoDocument = gql`
|
||||||
|
mutation RemoveWorkspaceLogo {
|
||||||
|
updateWorkspace(data: {logo: {set: null}}) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type RemoveWorkspaceLogoMutationFn = Apollo.MutationFunction<RemoveWorkspaceLogoMutation, RemoveWorkspaceLogoMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useRemoveWorkspaceLogoMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useRemoveWorkspaceLogoMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useRemoveWorkspaceLogoMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [removeWorkspaceLogoMutation, { data, loading, error }] = useRemoveWorkspaceLogoMutation({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useRemoveWorkspaceLogoMutation(baseOptions?: Apollo.MutationHookOptions<RemoveWorkspaceLogoMutation, RemoveWorkspaceLogoMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<RemoveWorkspaceLogoMutation, RemoveWorkspaceLogoMutationVariables>(RemoveWorkspaceLogoDocument, options);
|
||||||
|
}
|
||||||
|
export type RemoveWorkspaceLogoMutationHookResult = ReturnType<typeof useRemoveWorkspaceLogoMutation>;
|
||||||
|
export type RemoveWorkspaceLogoMutationResult = Apollo.MutationResult<RemoveWorkspaceLogoMutation>;
|
||||||
|
export type RemoveWorkspaceLogoMutationOptions = Apollo.BaseMutationOptions<RemoveWorkspaceLogoMutation, RemoveWorkspaceLogoMutationVariables>;
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
useUploadProfilePictureMutation,
|
useUploadProfilePictureMutation,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
export function PictureUploader() {
|
export function ProfilePictureUploader() {
|
||||||
const [uploadPicture] = useUploadProfilePictureMutation();
|
const [uploadPicture] = useUploadProfilePictureMutation();
|
||||||
const [removePicture] = useRemoveProfilePictureMutation();
|
const [removePicture] = useRemoveProfilePictureMutation();
|
||||||
const [currentUser] = useRecoilState(currentUserState);
|
const [currentUser] = useRecoilState(currentUserState);
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
|
import { ImageInput } from '@/ui/components/inputs/ImageInput';
|
||||||
|
import { GET_CURRENT_USER } from '@/users/queries';
|
||||||
|
import { getImageAbsoluteURI } from '@/users/utils/getProfilePictureAbsoluteURI';
|
||||||
|
import {
|
||||||
|
useRemoveWorkspaceLogoMutation,
|
||||||
|
useUploadWorkspaceLogoMutation,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
|
export function WorkspaceLogoUploader() {
|
||||||
|
const [uploadLogo] = useUploadWorkspaceLogoMutation();
|
||||||
|
const [removeLogo] = useRemoveWorkspaceLogoMutation();
|
||||||
|
const [currentUser] = useRecoilState(currentUserState);
|
||||||
|
async function onUpload(file: File) {
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await uploadLogo({
|
||||||
|
variables: {
|
||||||
|
file,
|
||||||
|
},
|
||||||
|
refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRemove() {
|
||||||
|
await removeLogo({
|
||||||
|
refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImageInput
|
||||||
|
picture={getImageAbsoluteURI(
|
||||||
|
currentUser?.workspaceMember?.workspace.logo,
|
||||||
|
)}
|
||||||
|
onUpload={onUpload}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { mockedUsersData } from '~/testing/mock-data/users';
|
import { getImageAbsoluteURI } from '@/users/utils/getProfilePictureAbsoluteURI';
|
||||||
|
|
||||||
import NavCollapseButton from './NavCollapseButton';
|
import NavCollapseButton from './NavCollapseButton';
|
||||||
|
|
||||||
@ -50,14 +50,17 @@ function NavWorkspaceButton() {
|
|||||||
const currentUser = useRecoilValue(currentUserState);
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
|
|
||||||
const currentWorkspace = currentUser?.workspaceMember?.workspace;
|
const currentWorkspace = currentUser?.workspaceMember?.workspace;
|
||||||
|
const DEFAULT_LOGO =
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<LogoAndNameContainer>
|
<LogoAndNameContainer>
|
||||||
<StyledLogo
|
<StyledLogo
|
||||||
logo={
|
logo={
|
||||||
currentWorkspace?.logo ??
|
currentWorkspace?.logo
|
||||||
mockedUsersData[0].workspaceMember.workspace.logo
|
? getImageAbsoluteURI(currentWorkspace.logo)
|
||||||
|
: DEFAULT_LOGO
|
||||||
}
|
}
|
||||||
></StyledLogo>
|
></StyledLogo>
|
||||||
<StyledName>{currentWorkspace?.displayName ?? 'Twenty'}</StyledName>
|
<StyledName>{currentWorkspace?.displayName ?? 'Twenty'}</StyledName>
|
||||||
|
|||||||
@ -10,3 +10,17 @@ export const UPDATE_WORKSPACE = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_WORKSPACE_LOGO = gql`
|
||||||
|
mutation UploadWorkspaceLogo($file: Upload!) {
|
||||||
|
uploadWorkspaceLogo(file: $file)
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const REMOVE_WORKSPACE_LOGO = gql`
|
||||||
|
mutation RemoveWorkspaceLogo {
|
||||||
|
updateWorkspace(data: { logo: { set: null } }) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMou
|
|||||||
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
||||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||||
import { NameFields } from '@/settings/profile/components/NameFields';
|
import { NameFields } from '@/settings/profile/components/NameFields';
|
||||||
import { PictureUploader } from '@/settings/profile/components/PictureUploader';
|
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
||||||
import { MainButton } from '@/ui/components/buttons/MainButton';
|
import { MainButton } from '@/ui/components/buttons/MainButton';
|
||||||
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
||||||
import { GET_CURRENT_USER } from '@/users/queries';
|
import { GET_CURRENT_USER } from '@/users/queries';
|
||||||
@ -111,7 +111,7 @@ export function CreateProfile() {
|
|||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
<StyledSectionContainer>
|
<StyledSectionContainer>
|
||||||
<SubSectionTitle title="Picture" />
|
<SubSectionTitle title="Picture" />
|
||||||
<PictureUploader />
|
<ProfilePictureUploader />
|
||||||
</StyledSectionContainer>
|
</StyledSectionContainer>
|
||||||
<StyledSectionContainer>
|
<StyledSectionContainer>
|
||||||
<SubSectionTitle
|
<SubSectionTitle
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
|||||||
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
|
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
|
||||||
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
||||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||||
|
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
||||||
import { MainButton } from '@/ui/components/buttons/MainButton';
|
import { MainButton } from '@/ui/components/buttons/MainButton';
|
||||||
import { ImageInput } from '@/ui/components/inputs/ImageInput';
|
|
||||||
import { TextInput } from '@/ui/components/inputs/TextInput';
|
import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||||
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
||||||
import { GET_CURRENT_USER } from '@/users/queries';
|
import { GET_CURRENT_USER } from '@/users/queries';
|
||||||
@ -103,7 +103,7 @@ export function CreateWorkspace() {
|
|||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
<StyledSectionContainer>
|
<StyledSectionContainer>
|
||||||
<SubSectionTitle title="Workspace logo" />
|
<SubSectionTitle title="Workspace logo" />
|
||||||
<ImageInput picture={null} disabled />
|
<WorkspaceLogoUploader />
|
||||||
</StyledSectionContainer>
|
</StyledSectionContainer>
|
||||||
<StyledSectionContainer>
|
<StyledSectionContainer>
|
||||||
<SubSectionTitle
|
<SubSectionTitle
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMou
|
|||||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||||
import { EmailField } from '@/settings/profile/components/EmailField';
|
import { EmailField } from '@/settings/profile/components/EmailField';
|
||||||
import { NameFields } from '@/settings/profile/components/NameFields';
|
import { NameFields } from '@/settings/profile/components/NameFields';
|
||||||
import { PictureUploader } from '@/settings/profile/components/PictureUploader';
|
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
||||||
import { MainSectionTitle } from '@/ui/components/section-titles/MainSectionTitle';
|
import { MainSectionTitle } from '@/ui/components/section-titles/MainSectionTitle';
|
||||||
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
||||||
import { NoTopBarContainer } from '@/ui/layout/containers/NoTopBarContainer';
|
import { NoTopBarContainer } from '@/ui/layout/containers/NoTopBarContainer';
|
||||||
@ -37,7 +37,7 @@ export function SettingsProfile() {
|
|||||||
<MainSectionTitle>Profile</MainSectionTitle>
|
<MainSectionTitle>Profile</MainSectionTitle>
|
||||||
<StyledSectionContainer>
|
<StyledSectionContainer>
|
||||||
<SubSectionTitle title="Picture" />
|
<SubSectionTitle title="Picture" />
|
||||||
<PictureUploader />
|
<ProfilePictureUploader />
|
||||||
</StyledSectionContainer>
|
</StyledSectionContainer>
|
||||||
<StyledSectionContainer>
|
<StyledSectionContainer>
|
||||||
<SubSectionTitle
|
<SubSectionTitle
|
||||||
|
|||||||
@ -26,8 +26,7 @@ export const mockedUsersData: Array<MockedUser> = [
|
|||||||
displayName: 'Charles Test',
|
displayName: 'Charles Test',
|
||||||
firstName: 'Charles',
|
firstName: 'Charles',
|
||||||
lastName: 'Test',
|
lastName: 'Test',
|
||||||
avatarUrl:
|
avatarUrl: null,
|
||||||
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4',
|
|
||||||
workspaceMember: {
|
workspaceMember: {
|
||||||
__typename: 'WorkspaceMember',
|
__typename: 'WorkspaceMember',
|
||||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||||
@ -36,7 +35,7 @@ export const mockedUsersData: Array<MockedUser> = [
|
|||||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||||
displayName: 'Twenty',
|
displayName: 'Twenty',
|
||||||
domainName: 'twenty.com',
|
domainName: 'twenty.com',
|
||||||
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=',
|
logo: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -55,7 +54,7 @@ export const mockedUsersData: Array<MockedUser> = [
|
|||||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||||
displayName: 'Twenty',
|
displayName: 'Twenty',
|
||||||
domainName: 'twenty.com',
|
domainName: 'twenty.com',
|
||||||
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=',
|
logo: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export class AbilityFactory {
|
|||||||
|
|
||||||
// Workspace
|
// Workspace
|
||||||
can(AbilityAction.Read, 'Workspace', { id: workspace.id });
|
can(AbilityAction.Read, 'Workspace', { id: workspace.id });
|
||||||
|
can(AbilityAction.Update, 'Workspace', { id: workspace.id });
|
||||||
|
|
||||||
// Workspace Member
|
// Workspace Member
|
||||||
can(AbilityAction.Read, 'WorkspaceMember', { userId: user.id });
|
can(AbilityAction.Read, 'WorkspaceMember', { userId: user.id });
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { AppAbility } from '../ability.factory';
|
|||||||
import { IAbilityHandler } from '../interfaces/ability-handler.interface';
|
import { IAbilityHandler } from '../interfaces/ability-handler.interface';
|
||||||
import {
|
import {
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@ -11,6 +12,7 @@ import { subject } from '@casl/ability';
|
|||||||
import { WorkspaceWhereInput } from 'src/core/@generated/workspace/workspace-where.input';
|
import { WorkspaceWhereInput } from 'src/core/@generated/workspace/workspace-where.input';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import { assert } from 'src/utils/assert';
|
import { assert } from 'src/utils/assert';
|
||||||
|
import { getRequest } from 'src/utils/extract-request';
|
||||||
|
|
||||||
class WorksapceArgs {
|
class WorksapceArgs {
|
||||||
where?: WorkspaceWhereInput;
|
where?: WorkspaceWhereInput;
|
||||||
@ -42,10 +44,11 @@ export class UpdateWorkspaceAbilityHandler implements IAbilityHandler {
|
|||||||
constructor(private readonly prismaService: PrismaService) {}
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
async handle(ability: AppAbility, context: ExecutionContext) {
|
async handle(ability: AppAbility, context: ExecutionContext) {
|
||||||
const gqlContext = GqlExecutionContext.create(context);
|
const request = getRequest(context);
|
||||||
const args = gqlContext.getArgs<WorksapceArgs>();
|
assert(request.user.workspace.id, '', ForbiddenException);
|
||||||
const workspace = await this.prismaService.workspace.findFirst({
|
|
||||||
where: args.where,
|
const workspace = await this.prismaService.workspace.findUnique({
|
||||||
|
where: { id: request.user.workspace.id },
|
||||||
});
|
});
|
||||||
assert(workspace, '', NotFoundException);
|
assert(workspace, '', NotFoundException);
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import { Settings } from './interfaces/settings.interface';
|
|||||||
export const settings: Settings = {
|
export const settings: Settings = {
|
||||||
storage: {
|
storage: {
|
||||||
imageCropSizes: {
|
imageCropSizes: {
|
||||||
profilePicture: ['original'],
|
'profile-picture': ['original'],
|
||||||
|
'workspace-logo': ['original'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { KebabCase } from 'type-fest';
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { basename } from 'path';
|
import { basename } from 'path';
|
||||||
import { settings } from 'src/constants/settings';
|
import { settings } from 'src/constants/settings';
|
||||||
import { camelCase } from 'src/utils/camel-case';
|
|
||||||
|
|
||||||
type AllowedFolders = KebabCase<keyof typeof FileFolder>;
|
type AllowedFolders = KebabCase<keyof typeof FileFolder>;
|
||||||
|
|
||||||
@ -20,10 +19,7 @@ export function checkFilePath(filePath: string): string {
|
|||||||
throw new BadRequestException(`Folder ${folder} is not allowed`);
|
throw new BadRequestException(`Folder ${folder} is not allowed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (size && !settings.storage.imageCropSizes[folder]?.includes(size)) {
|
||||||
size &&
|
|
||||||
!settings.storage.imageCropSizes[camelCase(folder)]?.includes(size)
|
|
||||||
) {
|
|
||||||
throw new BadRequestException(`Size ${size} is not allowed`);
|
throw new BadRequestException(`Size ${size} is not allowed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { registerEnumType } from '@nestjs/graphql';
|
import { registerEnumType } from '@nestjs/graphql';
|
||||||
|
|
||||||
export enum FileFolder {
|
export enum FileFolder {
|
||||||
ProfilePicture = 'profilePicture',
|
ProfilePicture = 'profile-picture',
|
||||||
|
WorkspaceLogo = 'workspace-logo',
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEnumType(FileFolder, {
|
registerEnumType(FileFolder, {
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export class FileUploadService {
|
|||||||
images.map(async (image, index) => {
|
images.map(async (image, index) => {
|
||||||
const buffer = await image.toBuffer();
|
const buffer = await image.toBuffer();
|
||||||
|
|
||||||
paths.push(`profile-picture/${cropSizes[index]}/${name}`);
|
paths.push(`${fileFolder}/${cropSizes[index]}/${name}`);
|
||||||
|
|
||||||
return this.uploadFile({
|
return this.uploadFile({
|
||||||
file: buffer,
|
file: buffer,
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { WorkspaceResolver } from './workspace.resolver';
|
import { WorkspaceResolver } from './workspace.resolver';
|
||||||
import { WorkspaceService } from '../services/workspace.service';
|
import { WorkspaceService } from '../services/workspace.service';
|
||||||
|
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
||||||
|
import { AbilityFactory } from 'src/ability/ability.factory';
|
||||||
|
|
||||||
describe('WorkspaceMemberResolver', () => {
|
describe('WorkspaceResolver', () => {
|
||||||
let resolver: WorkspaceResolver;
|
let resolver: WorkspaceResolver;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -10,6 +12,8 @@ describe('WorkspaceMemberResolver', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
WorkspaceResolver,
|
WorkspaceResolver,
|
||||||
{ provide: WorkspaceService, useValue: {} },
|
{ provide: WorkspaceService, useValue: {} },
|
||||||
|
{ provide: AbilityFactory, useValue: {} },
|
||||||
|
{ provide: FileUploadService, useValue: {} },
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -11,13 +11,25 @@ import { WorkspaceUpdateInput } from 'src/core/@generated/workspace/workspace-up
|
|||||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { assert } from 'src/utils/assert';
|
import { assert } from 'src/utils/assert';
|
||||||
|
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
||||||
|
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||||
|
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||||
|
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
|
||||||
|
import { AbilityGuard } from 'src/guards/ability.guard';
|
||||||
|
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
|
||||||
|
import { UpdateWorkspaceAbilityHandler } from 'src/ability/handlers/workspace.ability-handler';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Resolver(() => Workspace)
|
@Resolver(() => Workspace)
|
||||||
export class WorkspaceResolver {
|
export class WorkspaceResolver {
|
||||||
constructor(private readonly workspaceService: WorkspaceService) {}
|
constructor(
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly fileUploadService: FileUploadService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Mutation(() => Workspace)
|
@Mutation(() => Workspace)
|
||||||
|
@UseGuards(AbilityGuard)
|
||||||
|
@CheckAbilities(UpdateWorkspaceAbilityHandler)
|
||||||
async updateWorkspace(
|
async updateWorkspace(
|
||||||
@Args('data') data: WorkspaceUpdateInput,
|
@Args('data') data: WorkspaceUpdateInput,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@ -51,4 +63,33 @@ export class WorkspaceResolver {
|
|||||||
|
|
||||||
return selectedWorkspace;
|
return selectedWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(AbilityGuard)
|
||||||
|
@CheckAbilities(UpdateWorkspaceAbilityHandler)
|
||||||
|
@Mutation(() => String)
|
||||||
|
async uploadWorkspaceLogo(
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||||
|
{ createReadStream, filename, mimetype }: FileUpload,
|
||||||
|
): Promise<string> {
|
||||||
|
const stream = createReadStream();
|
||||||
|
const buffer = await streamToBuffer(stream);
|
||||||
|
const fileFolder = FileFolder.WorkspaceLogo;
|
||||||
|
|
||||||
|
const { paths } = await this.fileUploadService.uploadImage({
|
||||||
|
file: buffer,
|
||||||
|
filename,
|
||||||
|
mimeType: mimetype,
|
||||||
|
fileFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.workspaceService.update({
|
||||||
|
where: { id: workspace.id },
|
||||||
|
data: {
|
||||||
|
logo: paths[0],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return paths[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import { WorkspaceService } from './services/workspace.service';
|
|||||||
import { WorkspaceMemberService } from './services/workspace-member.service';
|
import { WorkspaceMemberService } from './services/workspace-member.service';
|
||||||
import { WorkspaceMemberResolver } from './resolvers/workspace-member.resolver';
|
import { WorkspaceMemberResolver } from './resolvers/workspace-member.resolver';
|
||||||
import { WorkspaceResolver } from './resolvers/workspace.resolver';
|
import { WorkspaceResolver } from './resolvers/workspace.resolver';
|
||||||
|
import { FileUploadService } from '../file/services/file-upload.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
|
FileUploadService,
|
||||||
WorkspaceMemberService,
|
WorkspaceMemberService,
|
||||||
WorkspaceMemberResolver,
|
WorkspaceMemberResolver,
|
||||||
WorkspaceResolver,
|
WorkspaceResolver,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -3,7 +3,6 @@ import { createReadStream, existsSync } from 'fs';
|
|||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { StorageDriver } from './interfaces/storage-driver.interface';
|
import { StorageDriver } from './interfaces/storage-driver.interface';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { kebabCase } from 'src/utils/kebab-case';
|
|
||||||
|
|
||||||
export interface LocalDriverOptions {
|
export interface LocalDriverOptions {
|
||||||
storagePath: string;
|
storagePath: string;
|
||||||
@ -32,7 +31,7 @@ export class LocalDriver implements StorageDriver {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const filePath = join(
|
const filePath = join(
|
||||||
`${this.options.storagePath}/`,
|
`${this.options.storagePath}/`,
|
||||||
kebabCase(params.folder),
|
params.folder,
|
||||||
params.name,
|
params.name,
|
||||||
);
|
);
|
||||||
const folderPath = dirname(filePath);
|
const folderPath = dirname(filePath);
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import {
|
|||||||
} from '@aws-sdk/client-s3';
|
} from '@aws-sdk/client-s3';
|
||||||
import { StorageDriver } from './interfaces/storage-driver.interface';
|
import { StorageDriver } from './interfaces/storage-driver.interface';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { kebabCase } from 'src/utils/kebab-case';
|
|
||||||
|
|
||||||
export interface S3DriverOptions extends S3ClientConfig {
|
export interface S3DriverOptions extends S3ClientConfig {
|
||||||
bucketName: string;
|
bucketName: string;
|
||||||
@ -42,7 +41,7 @@ export class S3Driver implements StorageDriver {
|
|||||||
mimeType: string | undefined;
|
mimeType: string | undefined;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const command = new PutObjectCommand({
|
const command = new PutObjectCommand({
|
||||||
Key: `${kebabCase(params.folder)}/${params.name}`,
|
Key: `${params.folder}/${params.name}`,
|
||||||
Body: params.file,
|
Body: params.file,
|
||||||
ContentType: params.mimeType,
|
ContentType: params.mimeType,
|
||||||
Bucket: this.bucketName,
|
Bucket: this.bucketName,
|
||||||
|
|||||||
Reference in New Issue
Block a user