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

#3060-Edit Organization Logo #3065

Merged
merged 18 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
1 change: 1 addition & 0 deletions src/backend/src/controllers/organizations.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default class OrganizationsController {
if (!req.file) {
throw new HttpException(400, 'Invalid or undefined image data');
}

const updatedOrg = await OrganizationsService.setLogoImage(req.file, req.currentUser, req.organization);

res.status(200).json(updatedOrg);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-- AlterTable
ALTER TABLE "Organization" ADD COLUMN "logoImage" TEXT;
ALTER TABLE "Organization" ADD COLUMN "logoImageId" TEXT;
Copy link
Contributor

Choose a reason for hiding this comment

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

ensure at some point you consolidate all of your migration files to just one migration called home-page-redesign


-- AlterTable
ALTER TABLE "Project" ADD COLUMN "organizationId" TEXT;
Expand Down

This file was deleted.

4 changes: 4 additions & 0 deletions src/backend/src/services/organizations.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ export default class OrganizationsService {

const logoImageData = await uploadFile(logoImage);

if (!logoImageData?.name) {
throw new HttpException(500, 'Image Name not found');
}

const updatedOrg = await prisma.organization.update({
where: { organizationId: organization.organizationId },
data: {
Expand Down
4 changes: 2 additions & 2 deletions src/backend/tests/unmocked/organization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('Organization Tests', () => {
it('Succeeds and updates all the images', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
(uploadFile as Mock).mockImplementation((file) => {
return Promise.resolve({ id: `uploaded-${file.originalname}` });
return Promise.resolve({ name: `${file.originalname}`, id: `uploaded-${file.originalname}` });
});

await OrganizationsService.setImages(file1, file2, testBatman, organization);
Expand Down Expand Up @@ -240,7 +240,7 @@ describe('Organization Tests', () => {
it('Succeeds and updates the logo', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
(uploadFile as Mock).mockImplementation((file) => {
return Promise.resolve({ id: `uploaded-${file.originalname}` });
return Promise.resolve({ name: `${file.originalname}`, id: `uploaded-${file.originalname}` });
});

await OrganizationsService.setLogoImage(file1, testBatman, organization);
Expand Down
Binary file added src/frontend/public/default-logo.png
Copy link
Contributor

Choose a reason for hiding this comment

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

this should not be here

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions src/frontend/src/apis/organizations.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ export const setOrganizationDescription = async (description: string) => {
});
};

export const getOrganizationLogo = async () => {
return axios.get<string>(apiUrls.organizationsLogoImage(), {
transformResponse: (data) => JSON.parse(data)
});
};

export const setOrganizationLogo = async (file: File) => {
const formData = new FormData();
formData.append('logo', file);
return axios.post(apiUrls.organizationsSetLogoImage(), formData);
};

export const setOrganizationFeaturedProjects = async (featuredProjectIds: string[]) => {
return axios.post<Organization>(apiUrls.organizationsSetFeaturedProjects(), {
projectIds: featuredProjectIds
Expand Down
25 changes: 25 additions & 0 deletions src/frontend/src/hooks/organizations.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import {
getFeaturedProjects,
getCurrentOrganization,
setOrganizationDescription,
getOrganizationLogo,
setOrganizationLogo,
setOrganizationFeaturedProjects
} from '../apis/organizations.api';
import { downloadGoogleImage } from '../apis/finance.api';
import { getDefaultImageData } from '../utils/image.utils';

interface OrganizationProvider {
organizationId: string;
Expand Down Expand Up @@ -84,3 +88,24 @@ export const useSetFeaturedProjects = () => {
}
);
};

export const useSetOrganizationLogo = () => {
const queryClient = useQueryClient();
return useMutation<Organization, Error, File>(['reimbursement-requsts', 'edit'], async (file: File) => {
const { data } = await setOrganizationLogo(file);
queryClient.invalidateQueries(['organizations']);
return data;
});
};

export const useOrganizationLogo = () => {
return useQuery<Blob, Error>(['organizations', 'logo'], async () => {
try {
const { data: fileId } = await getOrganizationLogo();
return await downloadGoogleImage(fileId);
} catch {
// return default logo if fileId was not found
return await getDefaultImageData();
Copy link
Contributor

Choose a reason for hiding this comment

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

no lets not do this. return undefined if it. doesnt exist

}
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const EditDescription: React.FC = () => {
return (
<Card
sx={{
width: { xs: '100%', md: '50%' },
width: '100%',
background: 'transparent',
padding: 2,
...(isEditMode && {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const EditFeaturedProjects = () => {
return (
<Card
sx={{
width: { xs: '100%', md: '50%' },
width: '100%',
background: 'transparent',
padding: 2,
...(isEditMode && {
Expand Down
86 changes: 86 additions & 0 deletions src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { useState } from 'react';
import { useCurrentOrganization, useOrganizationLogo, useSetOrganizationLogo } from '../../../hooks/organizations.hooks';
import LoadingIndicator from '../../../components/LoadingIndicator';
import EditLogoForm, { EditLogoInput } from './EditLogoForm';
import { useToast } from '../../../hooks/toasts.hooks';
import { Box, Card, Typography, useTheme } from '@mui/material';
import LogoDisplay from '../../HomePage/components/LogoDisplay';
import { NERButton } from '../../../components/NERButton';

const EditLogo = () => {
const { data: organization, isLoading: organizationIsLoading } = useCurrentOrganization();
const { data: imageData, isLoading: imageDataIsLoading } = useOrganizationLogo();
Copy link
Contributor

Choose a reason for hiding this comment

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

add error handling for these queries. Ensure all useQueries added have proper error handling.

const { mutateAsync, isLoading } = useSetOrganizationLogo();
const toast = useToast();
const [isEditMode, setIsEditMode] = useState(false);
const theme = useTheme();

if (isLoading || !mutateAsync || organizationIsLoading || !organization || !imageData || imageDataIsLoading)
return <LoadingIndicator />;

const handleClose = () => {
setIsEditMode(false);
};

const onSubmit = async (logoInput: EditLogoInput) => {
try {
console.log(logoInput);
Copy link
Contributor

Choose a reason for hiding this comment

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

remove console

if (!logoInput.logoImage) {
handleClose();
Copy link
Contributor

Choose a reason for hiding this comment

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

we should probably toast why were closing

return;
}
await mutateAsync(logoInput.logoImage);
toast.success('Logo updated successfully!');
} catch (e) {
if (e instanceof Error) {
toast.error(e.message);
}
}
handleClose();
};

return (
<Card
sx={{
width: '100%',
background: 'transparent',
padding: 2,
...(isEditMode && {
background: theme.palette.background.paper,
padding: 1.9,
variant: 'outlined'
})
}}
variant={isEditMode ? 'outlined' : undefined}
>
<Typography variant="h4" mb={1}>
{organization.name} Logo
</Typography>
{isEditMode ? (
<EditLogoForm
onSubmit={onSubmit}
onHide={handleClose}
orgLogo={new File([imageData], imageData.name, { type: imageData.type })}
/>
) : (
<>
<Box sx={{ display: 'flex', flexDirection: 'column', height: 350, width: 300 }}>
<LogoDisplay imageUrl={URL.createObjectURL(imageData)} />
<Box
sx={{
display: 'flex',
justifyContent: 'end'
}}
>
<NERButton variant="contained" sx={{ my: 2 }} onClick={() => setIsEditMode(true)}>
Update
</NERButton>
</Box>
</Box>
</>
)}
</Card>
);
};

export default EditLogo;
121 changes: 121 additions & 0 deletions src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogoForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from 'react';
import { Box, Button, FormControl, Stack, Typography } from '@mui/material';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import { Controller, useForm } from 'react-hook-form';
import NERFailButton from '../../../components/NERFailButton';
import NERSuccessButton from '../../../components/NERSuccessButton';
import ImageIcon from '@mui/icons-material/Image';

export interface EditLogoInput {
logoImage: File;
}

interface EditLogoFormProps {
onSubmit: (logoImage: EditLogoInput) => Promise<void>;
onHide: () => void;
orgLogo: File;
}

const EditLogoForm: React.FC<EditLogoFormProps> = ({ onSubmit, orgLogo, onHide }) => {
const { handleSubmit, control, reset } = useForm({
defaultValues: {
logoImage: orgLogo
}
});

const onSubmitWrapper = async (data: EditLogoInput) => {
await onSubmit(data);
reset();
};

const onHideWrapper = () => {
onHide();
reset();
};

return (
<form
id="edit-organization-logo"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
handleSubmit(onSubmit)(e);
reset();
}}
onKeyPress={(e) => {
e.key === 'Enter' && e.preventDefault();
}}
>
<Stack spacing={2}>
<FormControl sx={{ width: '100%' }}>
<Controller
name={'logoImage'}
control={control}
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<Box
sx={{
display: 'flex',
flexDirection: 'row',
gap: 1
}}
>
<Button
variant="contained"
color="error"
component="label"
startIcon={<FileUploadIcon />}
sx={{
width: 'fit-content',
textTransform: 'none',
mt: '9.75px',
color: 'black'
}}
>
Upload
<input
onChange={(e) => {
onChange(!!e.target.files && e.target.files[0]);
}}
type="file"
id="logo-image"
accept="image/png, image/jpeg"
name="logoImageFile"
multiple
hidden
/>
</Button>
{value.name !== 'undefined' && (
<Stack direction={'row'} spacing={1} mt={2}>
<ImageIcon />
<Typography>{value.name}</Typography>
</Stack>
)}
</Box>
)}
/>
</FormControl>
<Box
sx={{
display: 'flex',
justifyContent: 'end'
}}
>
<NERFailButton sx={{ mx: 1 }} form={'edit-organization-logo'} onClick={onHideWrapper}>
Cancel
</NERFailButton>
<NERSuccessButton
Copy link
Contributor

Choose a reason for hiding this comment

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

this is weird. were gonna be submitting twice. Wee should remove this onclick prop and delete the onSubmitWrapper.

sx={{ mx: 1 }}
type="submit"
form={'edit-organization-logo'}
onClick={handleSubmit(onSubmitWrapper)}
>
Save
</NERSuccessButton>
</Box>
</Stack>
</form>
);
};

export default EditLogoForm;
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { Stack } from '@mui/material';
import { Stack, Grid } from '@mui/material';
import EditDescription from './EditDescription';
import EditFeaturedProjects from './EditFeaturedProjects';
import EditLogo from './EditLogo';

const GuestViewConfig: React.FC = () => {
return (
<Stack spacing={2}>
<EditDescription />
<EditFeaturedProjects />
</Stack>
<Grid container spacing={2}>
<Grid item xs={6}>
<Stack spacing={2}>
<EditDescription />
<EditFeaturedProjects />
</Stack>
</Grid>
<Grid item xs={6}>
<EditLogo />
</Grid>
</Grid>
);
};

Expand Down
33 changes: 33 additions & 0 deletions src/frontend/src/pages/HomePage/components/LogoDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Box, useTheme, Card } from '@mui/material';
import React from 'react';

interface LogoDisplayProps {
imageUrl: string;
}

const LogoDisplay: React.FC<LogoDisplayProps> = ({ imageUrl }) => {
const theme = useTheme();
return (
<Card
variant={'outlined'}
sx={{
background: theme.palette.background.paper,
height: '100%',
width: '100%',
borderRadius: 2
}}
>
<Box
component="img"
src={imageUrl}
sx={{
height: '100%',
width: '100%',
borderRadius: 2
}}
/>
</Card>
);
};

export default LogoDisplay;
Loading
Loading