Skip to content

Commit

Permalink
Merge pull request #3065 from Northeastern-Electric-Racing/#3060-Caio…
Browse files Browse the repository at this point in the history
…-EditOrgLogo

#3060-Edit Organization Logo
  • Loading branch information
Peyton-McKee authored Dec 21, 2024
2 parents 4b28cc8 + a1d8637 commit b0e6b87
Show file tree
Hide file tree
Showing 17 changed files with 379 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import workPackageTemplatesRouter from './src/routes/work-package-templates.rout
import carsRouter from './src/routes/cars.routes';
import organizationRouter from './src/routes/organizations.routes';
import recruitmentRouter from './src/routes/recruitment.routes';
import onboardingRouter from './src/routes/onboarding.routes';

const app = express();

Expand Down Expand Up @@ -68,6 +69,7 @@ app.use('/templates', workPackageTemplatesRouter);
app.use('/cars', carsRouter);
app.use('/organizations', organizationRouter);
app.use('/recruitment', recruitmentRouter);
app.use('/onboarding', onboardingRouter);
app.use('/', (_req, res) => {
res.status(200).json('Welcome to FinishLine');
});
Expand Down
21 changes: 21 additions & 0 deletions src/backend/src/controllers/onboarding.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NextFunction, Request, Response } from 'express';
import OnboardingServices from '../services/onboarding.services';

export default class OnboardingController {
static async downloadImage(req: Request, res: Response, next: NextFunction) {
try {
const { fileId } = req.params;

const imageData = await OnboardingServices.downloadImage(fileId);

// Set the appropriate headers for the HTTP response
res.setHeader('content-type', String(imageData.type));
res.setHeader('content-length', imageData.buffer.length);

// Send the Buffer as the response body
res.send(imageData.buffer);
} catch (error: unknown) {
return next(error);
}
}
}
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
8 changes: 8 additions & 0 deletions src/backend/src/routes/onboarding.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from 'express';
import OnboardingController from '../controllers/onboarding.controller';

const onboardingRouter = express.Router();

onboardingRouter.get('/image/:fileId', OnboardingController.downloadImage);

export default onboardingRouter;
12 changes: 12 additions & 0 deletions src/backend/src/services/onboarding.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NotFoundException } from '../utils/errors.utils';
import { downloadImageFile } from '../utils/google-integration.utils';

export default class OnboardingServices {
static async downloadImage(fileId: string) {
const fileData = await downloadImageFile(fileId);
console.log('FILE DATA RECEIVED');

if (!fileData) throw new NotFoundException('Image File', fileId);
return fileData;
}
}
10 changes: 5 additions & 5 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 All @@ -216,7 +220,7 @@ export default class OrganizationsService {
* @param organizationId the id of the organization
* @returns the id of the image
*/
static async getLogoImage(organizationId: string): Promise<string> {
static async getLogoImage(organizationId: string): Promise<string | null> {
const organization = await prisma.organization.findUnique({
where: { organizationId }
});
Expand All @@ -225,10 +229,6 @@ export default class OrganizationsService {
throw new NotFoundException('Organization', organizationId);
}

if (!organization.logoImageId) {
throw new HttpException(404, `Organization ${organizationId} does not have a logo image`);
}

return organization.logoImageId;
}

Expand Down
10 changes: 2 additions & 8 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 Expand Up @@ -273,12 +273,6 @@ describe('Organization Tests', () => {
);
});

it('Fails if the organization does not have a logo image', async () => {
await expect(async () => await OrganizationsService.getLogoImage(orgId)).rejects.toThrow(
new HttpException(404, `Organization ${orgId} does not have a logo image`)
);
});

it('Succeeds and gets the image', async () => {
const testBatman = await createTestUser(batmanAppAdmin, orgId);
await OrganizationsService.setLogoImage(
Expand Down
16 changes: 16 additions & 0 deletions src/frontend/src/apis/onboarding.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import axios from 'axios';
import { apiUrls } from '../utils/urls';

/**
* API Call to download a google image
* @param fileId file id to be downloaded
* @returns an image blob
*/
export const downloadGoogleImage = async (fileId: string): Promise<Blob> => {
const response = await axios.get(apiUrls.imageById(fileId), {
responseType: 'arraybuffer' // Set the response type to 'arraybuffer' to receive the image as a Buffer
});
const imageBuffer = new Uint8Array(response.data);
const imageBlob = new Blob([imageBuffer], { type: response.headers['content-type'] });
return imageBlob;
};
27 changes: 27 additions & 0 deletions src/frontend/src/apis/organizations.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,35 @@ 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
});
};

/**
* Downloads a given fileId from google drive into a blob
*
* @param fileId the google id of the file to download
* @returns the downloaded file as a Blob
*/
export const downloadGoogleImage = async (fileId: string): Promise<Blob> => {
const response = await axios.get(apiUrls.imageById(fileId), {
responseType: 'arraybuffer' // Set the response type to 'arraybuffer' to receive the image as a Buffer
});
const imageBuffer = new Uint8Array(response.data);
const imageBlob = new Blob([imageBuffer], { type: response.headers['content-type'] });
return imageBlob;
};
22 changes: 22 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,11 @@ import {
getFeaturedProjects,
getCurrentOrganization,
setOrganizationDescription,
getOrganizationLogo,
setOrganizationLogo,
setOrganizationFeaturedProjects
} from '../apis/organizations.api';
import { downloadGoogleImage } from '../apis/organizations.api';

interface OrganizationProvider {
organizationId: string;
Expand Down Expand Up @@ -84,3 +87,22 @@ 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 | undefined, Error>(['organizations', 'logo'], async () => {
const { data: fileId } = await getOrganizationLogo();
if (!fileId) {
return;
}
return await downloadGoogleImage(fileId);
});
};
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
93 changes: 93 additions & 0 deletions src/frontend/src/pages/AdminToolsPage/EditGuestView/EditLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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';
import ErrorPage from '../../ErrorPage';

const EditLogo = () => {
const {
data: organization,
isLoading: organizationIsLoading,
isError: organizationIsError,
error: organizationError
} = useCurrentOrganization();
const { data: imageData, isLoading: imageDataIsLoading, isError: imageIsError, error: imageError } = useOrganizationLogo();
const { mutateAsync, isLoading } = useSetOrganizationLogo();
const toast = useToast();
const [isEditMode, setIsEditMode] = useState(false);
const theme = useTheme();

if (isLoading || !mutateAsync || organizationIsLoading || !organization || imageDataIsLoading) return <LoadingIndicator />;
if (organizationIsError) return <ErrorPage message={organizationError.message} />;
if (imageIsError) return <ErrorPage message={imageError.message} />;

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

const onSubmit = async (logoInput: EditLogoInput) => {
try {
if (!logoInput.logoImage) {
toast.error('No logo image submitted.');
handleClose();
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={imageData ? new File([imageData], imageData.name, { type: imageData.type }) : undefined}
/>
) : (
<>
<Box sx={{ display: 'flex', flexDirection: 'column', height: 350, width: 300 }}>
<LogoDisplay imageUrl={imageData ? URL.createObjectURL(imageData) : undefined} />
<Box
sx={{
display: 'flex',
justifyContent: 'end'
}}
>
<NERButton variant="contained" sx={{ my: 2 }} onClick={() => setIsEditMode(true)}>
Update
</NERButton>
</Box>
</Box>
</>
)}
</Card>
);
};

export default EditLogo;
Loading

0 comments on commit b0e6b87

Please sign in to comment.