Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
15 changes: 0 additions & 15 deletions src/backend/src/services/projects.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,6 @@ import {
import { getLinkQueryArgs } from '../prisma-query-args/links.query-args.js';
import { getDescriptionBulletQueryArgs } from '../prisma-query-args/description-bullets.query-args.js';

const validateSingleLinkTypeDashboard = (
isOnGuestHomePage: boolean,
isOnNewMemberDashboard: boolean,
isOnOnboardingDashboard: boolean
): void => {
const dashboardFlagCount = [isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboardingDashboard].filter(Boolean).length;
if (dashboardFlagCount > 1) {
throw new HttpException(400, 'A LinkType can only be on one dashboard at a time');
}
};

export default class ProjectsService {
/**
* Get all the non deleted projects in the database for the given organization
Expand Down Expand Up @@ -632,8 +621,6 @@ export default class ProjectsService {
if (!(await userHasPermission(user.userId, organization.organizationId, isAdmin)))
throw new AccessDeniedException('Only admins can create link types');

validateSingleLinkTypeDashboard(isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboardingDashboard);

const existingLinkType = await prisma.link_Type.findUnique({
where: { uniqueLinkType: { name, organizationId: organization.organizationId } }
});
Expand Down Expand Up @@ -683,8 +670,6 @@ export default class ProjectsService {
if (!(await userHasPermission(submitter.userId, organization.organizationId, isAdmin)))
throw new AccessDeniedException('Only an admin can update the linkType');

validateSingleLinkTypeDashboard(isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboardingDashboard);

// check if the linkType we are trying to update exists
const linkType = await prisma.link_Type.findUnique({
where: {
Expand Down
107 changes: 107 additions & 0 deletions src/backend/tests/unmocked/link-type.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Organization, User } from '@prisma/client';
import { AccessDeniedException, HttpException, NotFoundException } from '../../src/utils/errors.utils.js';
import { batmanAppAdmin, wonderwomanGuest } from '../test-data/users.test-data.js';
import { createTestOrganization, createTestUser, resetUsers } from '../test-utils.js';
import ProjectsService from '../../src/services/projects.services.js';

describe('LinkType Tests', () => {
let orgId: string;
let organization: Organization;
let testBatman: User;

beforeEach(async () => {
organization = await createTestOrganization();
orgId = organization.organizationId;
testBatman = await createTestUser(batmanAppAdmin, orgId);
});

afterEach(async () => {
await resetUsers();
});

describe('Create LinkType', () => {
it('Fails if user is not an admin', async () => {
await expect(
ProjectsService.createLinkType(
await createTestUser(wonderwomanGuest, orgId),
'GitHub',
'code',
false,
organization,
false,
false,
false
)
).rejects.toThrow(new AccessDeniedException('Only admins can create link types'));
});

it('Fails if a LinkType with that name already exists in the organization', async () => {
await ProjectsService.createLinkType(testBatman, 'GitHub', 'code', false, organization, false, false, false);

await expect(
ProjectsService.createLinkType(testBatman, 'GitHub', 'code', false, organization, false, false, false)
).rejects.toThrow(new HttpException(400, 'LinkType with that name already exists in this organization.'));
});

it('Succeeds and creates a LinkType flagged for multiple dashboards at once', async () => {
const linkType = await ProjectsService.createLinkType(
testBatman,
'GitHub',
'code',
false,
organization,
false,
true,
true
);

expect(linkType.isOnNewMemberDashboard).toBe(true);
expect(linkType.isOnOnboardingDashboard).toBe(true);
expect(linkType.isOnGuestHomePage).toBe(false);
});
});

describe('Edit LinkType', () => {
it('Fails if user is not an admin', async () => {
await ProjectsService.createLinkType(testBatman, 'GitHub', 'code', false, organization, false, false, false);

await expect(
ProjectsService.editLinkType(
'GitHub',
'code',
false,
await createTestUser(wonderwomanGuest, orgId),
organization,
false,
false,
false
)
).rejects.toThrow(new AccessDeniedException('Only an admin can update the linkType'));
});

it('Fails if the LinkType does not exist', async () => {
await expect(
ProjectsService.editLinkType('Nonexistent', 'code', false, testBatman, organization, false, false, false)
).rejects.toThrow(new NotFoundException('Link Type', 'Nonexistent'));
});

it('Succeeds and adds a second dashboard flag onto a LinkType already on another dashboard', async () => {
await ProjectsService.createLinkType(testBatman, 'GitHub', 'code', false, organization, false, false, true);

const updatedLinkType = await ProjectsService.editLinkType(
'GitHub',
'code',
false,
testBatman,
organization,
false,
true,
true
);

expect(updatedLinkType.isOnOnboardingDashboard).toBe(true);
expect(updatedLinkType.isOnNewMemberDashboard).toBe(true);
expect(updatedLinkType.isOnGuestHomePage).toBe(false);
});
});
});
21 changes: 21 additions & 0 deletions src/frontend/src/hooks/projects.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,27 @@ export const useEditLinkType = (linkTypeName: string) => {
);
};

/**
* Custom React Hook to edit a LinkType, given the name of the LinkType at call time rather than
* hook-mount time. Used when the LinkType being edited isn't known until a form is submitted,
* e.g. flipping a dashboard placement flag on whichever LinkType was picked from a dropdown.
*/
export const useEditLinkTypeByName = () => {
const queryClient = useQueryClient();
return useMutation<LinkType, Error, { name: string; data: LinkTypeCreatePayload }>(
['linkTypes', 'edit'],
async ({ name, data }) => {
const { data: updatedLinkType } = await editLinkType(name, data);
return updatedLinkType;
},
{
onSuccess: () => {
queryClient.invalidateQueries(['linkTypes']);
}
}
);
};

/**
* Custom React Hook to get all useful links
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const AdminToolsProjectsConfig: React.FC = () => {
<Typography variant="h5" gutterBottom borderBottom={1} color="#ef4345" borderColor={'white'}>
Useful Links
</Typography>
<UsefulLinksTable />
<UsefulLinksTable isOnGuestHomePage={false} isOnNewMemberDashboard={false} isOnOnboardingDashboard={false} />
<Typography variant="h5" gutterBottom borderBottom={1} color="#ef4345" borderColor={'white'}>
Description Bullet Types
</Typography>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const GuestViewConfig: React.FC = () => {
>
Guest Page Links
</Typography>
<UsefulLinksTable isOnGuestHomePage={true} />
<UsefulLinksTable isOnGuestHomePage={true} isOnNewMemberDashboard={false} isOnOnboardingDashboard={false} />
</Box>
</Stack>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const NewMemberDashboardUsefulLinksSection: React.FC = () => {
New Member Dashboard Useful Links
</Typography>
<LinkTypeTable isOnNewMemberDashboard />
<UsefulLinksTable isOnNewMemberDashboard />
<UsefulLinksTable isOnGuestHomePage={false} isOnNewMemberDashboard={true} isOnOnboardingDashboard={false} />
</Box>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const OnboardingConfigSection: React.FC = () => {
Onboarding Page Useful Links
</Typography>
<LinkTypeTable isOnOnboardingDashboard />
<UsefulLinksTable isOnOnboardingDashboard />
<UsefulLinksTable isOnGuestHomePage={false} isOnNewMemberDashboard={false} isOnOnboardingDashboard={true} />
</Box>
</Grid>
<Grid item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,20 @@ interface CreateUsefulLinkModalProps {
handleClose: () => void;
linkTypes: LinkType[];
currentLinks: Link[];
isOnGuestHomePage: boolean;
isOnNewMemberDashboard: boolean;
isOnOnboardingDashboard: boolean;
}

const CreateUsefulLinkModal = ({ open, handleClose, linkTypes, currentLinks }: CreateUsefulLinkModalProps) => {
const CreateUsefulLinkModal = ({
open,
handleClose,
linkTypes,
currentLinks,
isOnGuestHomePage,
isOnNewMemberDashboard,
isOnOnboardingDashboard
}: CreateUsefulLinkModalProps) => {
const { isLoading, isError, error, mutateAsync } = useSetUsefulLinks();

if (isError) return <ErrorPage message={error?.message} />;
Expand All @@ -24,6 +35,9 @@ const CreateUsefulLinkModal = ({ open, handleClose, linkTypes, currentLinks }: C
onSubmit={mutateAsync}
linkTypes={linkTypes}
currentLinks={currentLinks}
isOnGuestHomePage={isOnGuestHomePage}
isOnNewMemberDashboard={isOnNewMemberDashboard}
isOnOnboardingDashboard={isOnOnboardingDashboard}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@ interface EditUsefulLinkModalProps {
linkType: Link;
linkTypes: LinkType[];
currentLinks: Link[];
isOnGuestHomePage: boolean;
isOnNewMemberDashboard: boolean;
isOnOnboardingDashboard: boolean;
}

const EditUsefulLinkModal = ({ open, handleClose, linkType, linkTypes, currentLinks }: EditUsefulLinkModalProps) => {
const EditUsefulLinkModal = ({
open,
handleClose,
linkType,
linkTypes,
currentLinks,
isOnGuestHomePage,
isOnNewMemberDashboard,
isOnOnboardingDashboard
}: EditUsefulLinkModalProps) => {
const { isLoading, isError, error, mutateAsync } = useSetUsefulLinks();

if (isError) return <ErrorPage message={error?.message} />;
Expand All @@ -26,6 +38,9 @@ const EditUsefulLinkModal = ({ open, handleClose, linkType, linkTypes, currentLi
defaulValues={linkType}
linkTypes={linkTypes}
currentLinks={currentLinks}
isOnGuestHomePage={isOnGuestHomePage}
isOnNewMemberDashboard={isOnNewMemberDashboard}
isOnOnboardingDashboard={isOnOnboardingDashboard}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import * as yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { Link, LinkCreateArgs, LinkType } from 'shared';
import { linkToLinkCreateArgs } from '../../../../utils/link.utils';
import { useEditLinkTypeByName } from '../../../../hooks/projects.hooks';

interface UsefulLinkFormModalProps {
open: boolean;
handleClose: () => void;
defaulValues?: Link;
onSubmit: (data: LinkCreateArgs[]) => void;
onSubmit: (data: LinkCreateArgs[]) => Promise<unknown>;
linkTypes: LinkType[];
currentLinks: Link[];
isOnGuestHomePage: boolean;
isOnNewMemberDashboard: boolean;
isOnOnboardingDashboard: boolean;
}

const UsefulLinkFormModal = ({
Expand All @@ -23,9 +27,13 @@ const UsefulLinkFormModal = ({
defaulValues,
onSubmit,
linkTypes,
currentLinks
currentLinks,
isOnGuestHomePage,
isOnNewMemberDashboard,
isOnOnboardingDashboard
}: UsefulLinkFormModalProps) => {
const toast = useToast();
const { mutateAsync: editLinkTypeAsync } = useEditLinkTypeByName();

const schema = yup.object().shape({
linkTypeName: yup.string().required('LinkType is Required'),
Expand All @@ -47,17 +55,38 @@ const UsefulLinkFormModal = ({

const onFormSubmit = async (data: LinkCreateArgs) => {
try {
const selectedLinkType = linkTypes.find((linkType) => linkType.name === data.linkTypeName);
const needsDashboardFlag =
selectedLinkType &&
((isOnGuestHomePage && !selectedLinkType.isOnGuestHomePage) ||
(isOnNewMemberDashboard && !selectedLinkType.isOnNewMemberDashboard) ||
(isOnOnboardingDashboard && !selectedLinkType.isOnOnboardingDashboard));

if (selectedLinkType && needsDashboardFlag) {
await editLinkTypeAsync({
name: selectedLinkType.name,
data: {
name: selectedLinkType.name,
iconName: selectedLinkType.iconName,
required: selectedLinkType.required,
isOnGuestHomePage: selectedLinkType.isOnGuestHomePage || isOnGuestHomePage,
isOnNewMemberDashboard: selectedLinkType.isOnNewMemberDashboard || isOnNewMemberDashboard,
isOnOnboardingDashboard: selectedLinkType.isOnOnboardingDashboard || isOnOnboardingDashboard
}
});
}

const previousLinks = linkToLinkCreateArgs(currentLinks);
const newLinks = defaulValues
? [...previousLinks.filter((link) => link.linkId !== defaulValues.linkId), data]
: [...previousLinks, data];
onSubmit(newLinks);
await onSubmit(newLinks);
handleClose();
} catch (error: unknown) {
if (error instanceof Error) {
toast.error(error.message);
}
}
handleClose();
};
return (
<NERFormModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import { useToast } from '../../../../hooks/toasts.hooks';

interface UsefulLinksTableProps {
isOnGuestHomePage?: boolean;
isOnNewMemberDashboard?: boolean;
isOnOnboardingDashboard?: boolean;
isOnGuestHomePage: boolean;
isOnNewMemberDashboard: boolean;
isOnOnboardingDashboard: boolean;
}

const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboardingDashboard }: UsefulLinksTableProps) => {
Expand Down Expand Up @@ -83,17 +83,18 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa
return !linkType.isOnGuestHomePage && !linkType.isOnNewMemberDashboard && !linkType.isOnOnboardingDashboard;
};

const linkTypes = linkTypesBeforeFilter.filter(matchesDashboard);

const usefulLinks = links.filter((link) => matchesDashboard(link.linkType));

return (
<Box>
<CreateUsefulLinkModal
open={showCreateModel}
handleClose={() => setShowCreateModel(false)}
linkTypes={linkTypes}
linkTypes={linkTypesBeforeFilter}
currentLinks={links}
isOnGuestHomePage={isOnGuestHomePage}
isOnNewMemberDashboard={isOnNewMemberDashboard}
isOnOnboardingDashboard={isOnOnboardingDashboard}
/>
{editingLink && (
<EditUsefulLinkModal
Expand All @@ -102,8 +103,11 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa
setEditingLink(undefined);
}}
linkType={editingLink}
linkTypes={linkTypes}
linkTypes={linkTypesBeforeFilter}
currentLinks={links}
isOnGuestHomePage={isOnGuestHomePage}
isOnNewMemberDashboard={isOnNewMemberDashboard}
isOnOnboardingDashboard={isOnOnboardingDashboard}
/>
)}

Expand Down
Loading