From e470a46e298c2b820161385d8753fcdc3413a5b4 Mon Sep 17 00:00:00 2001 From: wavehassman Date: Wed, 19 Aug 2026 21:22:39 -0400 Subject: [PATCH 1/8] restructure links and link types --- .../src/controllers/projects.controllers.ts | 25 ++------- .../change-requests.query-args.ts | 16 +++--- .../prisma/factories/config-data.factory.ts | 54 ++++++++++--------- .../migration.sql | 27 ++++++++++ src/backend/src/prisma/schema.prisma | 6 +-- src/backend/src/routes/projects.routes.ts | 6 --- src/backend/src/services/projects.services.ts | 39 ++------------ src/backend/src/utils/organizations.utils.ts | 10 ++++ src/backend/src/utils/validation.utils.ts | 8 ++- .../test-data/organizations.test-data.ts | 5 +- src/backend/tests/unit/organization.test.ts | 30 ++++++++--- .../tests/unmocked/organization.test.ts | 30 ++++++++--- src/frontend/src/components/LinksEditView.tsx | 20 ++++++- .../EditGuestView/GuestViewConfig.tsx | 7 --- .../NewMemberDashboardUsefulLinksSection.tsx | 2 - .../OnboardingConfigSection.tsx | 2 - .../UsefulLinks/CreateUsefulLinkModal.tsx | 16 +++++- .../UsefulLinks/EditUsefulLinkModal.tsx | 17 +++++- .../UsefulLinks/UsefulLinkFormModal.tsx | 18 +++++-- .../UsefulLinks/UsefulLinksTable.tsx | 36 ++++++------- .../LinkTypes/CreateLinkTypeModal.tsx | 24 +-------- .../LinkTypes/EditLinkTypeModal.tsx | 3 -- .../LinkTypes/LinkTypeFormModal.tsx | 44 ++++----------- .../LinkTypes/LinkTypeTable.tsx | 32 ++++------- .../EditProjectBudgetModal.tsx | 5 +- .../GanttTimeLineChangeModal.tsx | 5 +- .../src/pages/HomePage/IntroGuestHomePage.tsx | 2 +- .../components/GuestOrganizationInfo.tsx | 2 +- .../components/NewMemberUsefulLinksWidget.tsx | 2 +- .../ProjectForm/ProjectEditContainer.tsx | 16 +++++- .../test-support/test-data/projects.stub.ts | 30 +++++------ src/frontend/src/utils/link.utils.ts | 5 +- src/shared/src/types/project-types.ts | 12 ++--- 33 files changed, 292 insertions(+), 264 deletions(-) create mode 100644 src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql diff --git a/src/backend/src/controllers/projects.controllers.ts b/src/backend/src/controllers/projects.controllers.ts index 6eaff761c3..4ddad5eafc 100644 --- a/src/backend/src/controllers/projects.controllers.ts +++ b/src/backend/src/controllers/projects.controllers.ts @@ -183,18 +183,9 @@ export default class ProjectsController { static async createLinkType(req: Request, res: Response, next: NextFunction) { try { - const { name, iconName, required, isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboardingDashboard } = req.body; + const { name, iconName, required } = req.body; - const newLinkType = await ProjectsService.createLinkType( - req.currentUser, - name, - iconName, - required, - req.organization, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard - ); + const newLinkType = await ProjectsService.createLinkType(req.currentUser, name, iconName, required, req.organization); res.status(200).json(newLinkType); } catch (error: unknown) { next(error); @@ -471,23 +462,13 @@ export default class ProjectsController { static async editLinkType(req: Request, res: Response, next: NextFunction) { try { const { linkTypeName } = req.params as Record; - const { - name: newName, - iconName, - required, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard - } = req.body; + const { name: newName, iconName, required } = req.body; const linkTypeUpdated = await ProjectsService.editLinkType( linkTypeName, iconName, required, req.currentUser, req.organization, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard, newName ); res.status(200).json(linkTypeUpdated); diff --git a/src/backend/src/prisma-query-args/change-requests.query-args.ts b/src/backend/src/prisma-query-args/change-requests.query-args.ts index b4aa4c9806..9b426a92b4 100644 --- a/src/backend/src/prisma-query-args/change-requests.query-args.ts +++ b/src/backend/src/prisma-query-args/change-requests.query-args.ts @@ -39,14 +39,14 @@ const getWorkPackageProposedChangesQueryArgs = (organizationId: string) => select: { linkId: true, url: true, + isOnGuestHomePage: true, + isOnNewMemberDashboard: true, + isOnOnboardingDashboard: true, linkType: { select: { name: true, required: true, - iconName: true, - isOnGuestHomePage: true, - isOnNewMemberDashboard: true, - isOnOnboardingDashboard: true + iconName: true } } } @@ -84,14 +84,14 @@ const getWbsProposedChangesQueryArgs = (organizationId: string) => select: { linkId: true, url: true, + isOnGuestHomePage: true, + isOnNewMemberDashboard: true, + isOnOnboardingDashboard: true, linkType: { select: { name: true, required: true, - iconName: true, - isOnGuestHomePage: true, - isOnNewMemberDashboard: true, - isOnOnboardingDashboard: true + iconName: true } } } diff --git a/src/backend/src/prisma/factories/config-data.factory.ts b/src/backend/src/prisma/factories/config-data.factory.ts index 2efd493334..a58fbf6bca 100644 --- a/src/backend/src/prisma/factories/config-data.factory.ts +++ b/src/backend/src/prisma/factories/config-data.factory.ts @@ -55,7 +55,6 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Google Drive', iconName: 'add_to_drive', required: false, - isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -63,7 +62,6 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Confluence', iconName: 'article', required: false, - isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -71,7 +69,6 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Github', iconName: 'code', required: false, - isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -79,7 +76,6 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Altium', iconName: 'electric_bolt', required: false, - isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -87,7 +83,6 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Application', iconName: 'ballot', required: false, - isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -95,7 +90,6 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Sign Ups', iconName: 'ballot', required: false, - isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -103,7 +97,6 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'facebook', iconName: 'facebook', required: false, - isOnGuestHomePage: true, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -111,7 +104,6 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Instagram', iconName: 'Instagram', required: false, - isOnGuestHomePage: true, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -119,9 +111,6 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Handbook', iconName: 'menu_book', required: true, - isOnGuestHomePage: false, - isOnNewMemberDashboard: true, - isOnOnboardingDashboard: true, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -129,29 +118,42 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Team Directory', iconName: 'groups', required: true, - isOnGuestHomePage: false, - isOnNewMemberDashboard: true, creator: connectUser(creatorId), organization: connectOrganization(organizationId) } ]; -// URLs for the link types that are actually surfaced somewhere (guest home page / new-member -// dashboard) -- the rest of linkTypeCreateInputs are categories only, with no seeded Link yet. -const USEFUL_LINK_URL_BY_TYPE_NAME: Record = { - facebook: 'https://facebook.com/example-org', - Instagram: 'https://instagram.com/example-org', - Handbook: 'https://example.com/handbook', - 'Team Directory': 'https://example.com/team-directory' +type UsefulLinkPlacement = { + url: string; + isOnGuestHomePage?: boolean; + isOnNewMemberDashboard?: boolean; + isOnOnboardingDashboard?: boolean; +}; + +// Placements for the link types that are actually surfaced somewhere (guest home page / new-member +// dashboard / onboarding dashboard) -- the rest of linkTypeCreateInputs are categories only, with no +// seeded Link yet. A type can have more than one placement (e.g. Handbook shows on both the +// new-member and onboarding dashboards), which now means one seeded Link per placement. +const USEFUL_LINK_PLACEMENTS_BY_TYPE_NAME: Record = { + facebook: [{ url: 'https://facebook.com/example-org', isOnGuestHomePage: true }], + Instagram: [{ url: 'https://instagram.com/example-org', isOnGuestHomePage: true }], + Handbook: [ + { url: 'https://example.com/handbook', isOnNewMemberDashboard: true }, + { url: 'https://example.com/handbook', isOnOnboardingDashboard: true } + ], + 'Team Directory': [{ url: 'https://example.com/team-directory', isOnNewMemberDashboard: true }] }; export const usefulLinkCreateInput = ( creatorId: string, organizationId: string, linkTypeId: string, - url: string + placement: UsefulLinkPlacement ): Prisma.LinkCreateInput => ({ - url, + url: placement.url, + isOnGuestHomePage: placement.isOnGuestHomePage ?? false, + isOnNewMemberDashboard: placement.isOnNewMemberDashboard ?? false, + isOnOnboardingDashboard: placement.isOnOnboardingDashboard ?? false, creator: connectUser(creatorId), organization: connectOrganization(organizationId), linkType: { connect: { id: linkTypeId } } @@ -163,9 +165,11 @@ export const usefulLinkCreateInputsForTypes = ( linkTypes: { id: string; name: string }[] ): Prisma.LinkCreateInput[] => linkTypes - .filter((linkType) => linkType.name in USEFUL_LINK_URL_BY_TYPE_NAME) - .map((linkType) => - usefulLinkCreateInput(creatorId, organizationId, linkType.id, USEFUL_LINK_URL_BY_TYPE_NAME[linkType.name]) + .filter((linkType) => linkType.name in USEFUL_LINK_PLACEMENTS_BY_TYPE_NAME) + .flatMap((linkType) => + USEFUL_LINK_PLACEMENTS_BY_TYPE_NAME[linkType.name].map((placement) => + usefulLinkCreateInput(creatorId, organizationId, linkType.id, placement) + ) ); export const descriptionBulletTypeCreateInputs = ( diff --git a/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql b/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql new file mode 100644 index 0000000000..05e6a34c48 --- /dev/null +++ b/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql @@ -0,0 +1,27 @@ +-- Move dashboard placement (guest home page / new member dashboard / onboarding dashboard) off +-- Link_Type (a global category, e.g. "GitHub", "Confluence") and onto Link (a specific instance). +-- This lets the same Link_Type be reused across a project link and any number of dashboard links. + +-- AlterTable: Link - add the placement columns +ALTER TABLE "Link" +ADD COLUMN "isOnGuestHomePage" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "isOnNewMemberDashboard" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "isOnOnboardingDashboard" BOOLEAN NOT NULL DEFAULT false; + +-- Backfill: copy each existing link's placement down from its current Link_Type before the +-- Link_Type columns are dropped. Only org-level useful links (organizationId set) ever had +-- meaningful placement; project links (wbsElementId) were never flagged and stay false. +UPDATE "Link" l +SET + "isOnGuestHomePage" = lt."isOnGuestHomePage", + "isOnNewMemberDashboard" = lt."isOnNewMemberDashboard", + "isOnOnboardingDashboard" = lt."isOnOnboardingDashboard" +FROM "Link_Type" lt +WHERE l."linkTypeId" = lt."id" + AND l."organizationId" IS NOT NULL; + +-- AlterTable: Link_Type - drop the now-relocated placement columns +ALTER TABLE "Link_Type" +DROP COLUMN "isOnGuestHomePage", +DROP COLUMN "isOnNewMemberDashboard", +DROP COLUMN "isOnOnboardingDashboard"; diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index cfc701cbb8..ca8e6b3bff 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -588,9 +588,6 @@ model Link_Type { links Link[] @relation(name: "linkTypes") organizationId String organization Organization @relation(fields: [organizationId], references: [organizationId]) - isOnGuestHomePage Boolean @default(false) - isOnNewMemberDashboard Boolean @default(false) - isOnOnboardingDashboard Boolean @default(false) @@unique([name, organizationId], name: "uniqueLinkType") @@index([organizationId]) @@ -605,6 +602,9 @@ model Link { dateDeleted DateTime? linkTypeId String linkType Link_Type @relation(name: "linkTypes", fields: [linkTypeId], references: [id]) + isOnGuestHomePage Boolean @default(false) + isOnNewMemberDashboard Boolean @default(false) + isOnOnboardingDashboard Boolean @default(false) // Either its on a wbsElement, a wbsProposedChanges, or an organization wbsElementId String? diff --git a/src/backend/src/routes/projects.routes.ts b/src/backend/src/routes/projects.routes.ts index 60e43d8794..1d8999f339 100644 --- a/src/backend/src/routes/projects.routes.ts +++ b/src/backend/src/routes/projects.routes.ts @@ -26,9 +26,6 @@ projectRouter.post( nonEmptyString(body('name')), nonEmptyString(body('iconName')), body('required').isBoolean(), - body('isOnGuestHomePage').isBoolean(), - body('isOnNewMemberDashboard').isBoolean(), - body('isOnOnboardingDashboard').isBoolean(), validateInputs, ProjectsController.createLinkType ); @@ -37,9 +34,6 @@ projectRouter.post( nonEmptyString(body('name').optional()), nonEmptyString(body('iconName')), body('required').isBoolean(), - body('isOnGuestHomePage').isBoolean(), - body('isOnNewMemberDashboard').isBoolean(), - body('isOnOnboardingDashboard').isBoolean(), validateInputs, ProjectsController.editLinkType ); diff --git a/src/backend/src/services/projects.services.ts b/src/backend/src/services/projects.services.ts index 304205f595..7cb4e036f4 100644 --- a/src/backend/src/services/projects.services.ts +++ b/src/backend/src/services/projects.services.ts @@ -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 @@ -612,9 +601,6 @@ export default class ProjectsService { * @param required is the new LinkType required * @param user the user who is creating the new LinkType * @param orgainzationId the organization the link type is being created for - * @param isOnGuestHomePage whether the LinkType shows on the guest home page - * @param isOnNewMemberDashboard whether the LinkType shows on the new member dashboard - * @param isOnOnboardingDashboard whether the LinkType shows on the onboarding checklist page * @throws AccessDeniedException if the submitter of the request is not an admin * @throws HttpException if a LinkType of the given name already exists * @returns the created LinkType @@ -624,16 +610,11 @@ export default class ProjectsService { name: string, iconName: string, required: boolean, - organization: Organization, - isOnGuestHomePage: boolean, - isOnNewMemberDashboard: boolean, - isOnOnboardingDashboard: boolean + organization: Organization ): Promise { 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 } } }); @@ -646,10 +627,7 @@ export default class ProjectsService { creatorId: user.userId, iconName, required, - organizationId: organization.organizationId, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard + organizationId: organization.organizationId } }); @@ -663,9 +641,6 @@ export default class ProjectsService { * @param required the new required status * @param submitter user requesting the edit * @param organizationId the organization the user is currently in - * @param isOnGuestHomePage whether the LinkType shows on the guest home page - * @param isOnNewMemberDashboard whether the LinkType shows on the new member dashboard - * @param isOnOnboardingDashboard whether the LinkType shows on the onboarding checklist page * @param newName the new name of the linkType, if being renamed * @returns the updated linkType */ @@ -675,16 +650,11 @@ export default class ProjectsService { required: boolean, submitter: User, organization: Organization, - isOnGuestHomePage: boolean, - isOnNewMemberDashboard: boolean, - isOnOnboardingDashboard: boolean, newName?: string ): Promise { 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: { @@ -717,10 +687,7 @@ export default class ProjectsService { data: { name: newName && newName ? newName : linkName, iconName, - required, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard + required } }); return linkTypeUpdated; diff --git a/src/backend/src/utils/organizations.utils.ts b/src/backend/src/utils/organizations.utils.ts index 576baeb742..05f440ba27 100644 --- a/src/backend/src/utils/organizations.utils.ts +++ b/src/backend/src/utils/organizations.utils.ts @@ -5,6 +5,13 @@ import { HttpException } from './errors.utils.js'; export const createUsefulLinks = async (links: LinkCreateArgs[], organizationId: string, submitter: User) => { const newLinks = []; for (const link of links) { + const dashboardFlagCount = [link.isOnGuestHomePage, link.isOnNewMemberDashboard, link.isOnOnboardingDashboard].filter( + Boolean + ).length; + if (dashboardFlagCount > 1) { + throw new HttpException(400, 'A useful link can only be on one dashboard at a time'); + } + const linkType = await prisma.link_Type.findUnique({ where: { uniqueLinkType: { @@ -26,6 +33,9 @@ export const createUsefulLinks = async (links: LinkCreateArgs[], organizationId: } }, url: link.url, + isOnGuestHomePage: link.isOnGuestHomePage, + isOnNewMemberDashboard: link.isOnNewMemberDashboard, + isOnOnboardingDashboard: link.isOnOnboardingDashboard, creator: { connect: { userId: submitter.userId diff --git a/src/backend/src/utils/validation.utils.ts b/src/backend/src/utils/validation.utils.ts index 13c7325208..8086289486 100644 --- a/src/backend/src/utils/validation.utils.ts +++ b/src/backend/src/utils/validation.utils.ts @@ -186,6 +186,9 @@ export const projectProposedChangesValidators = [ projectProposedChangesExists(body('projectProposedChanges.links')).isArray(), nonEmptyString(body('projectProposedChanges.links.*.url')), nonEmptyString(body('projectProposedChanges.links.*.linkTypeName')), + body('projectProposedChanges.links.*.isOnGuestHomePage').isBoolean(), + body('projectProposedChanges.links.*.isOnNewMemberDashboard').isBoolean(), + body('projectProposedChanges.links.*.isOnOnboardingDashboard').isBoolean(), nonEmptyString(body('projectProposedChanges.leadId').optional()), nonEmptyString(body('projectProposedChanges.managerId').optional()), nonEmptyString(projectProposedChangesExists(body('projectProposedChanges.summary'))), @@ -260,7 +263,10 @@ export const blockedByValidators = [ export const linkValidators = [ body('links').isArray(), nonEmptyString(body('links.*.url')), - nonEmptyString(body('links.*.linkTypeName')) + nonEmptyString(body('links.*.linkTypeName')), + body('links.*.isOnGuestHomePage').isBoolean(), + body('links.*.isOnNewMemberDashboard').isBoolean(), + body('links.*.isOnOnboardingDashboard').isBoolean() ]; export const projectValidators = [ diff --git a/src/backend/tests/test-data/organizations.test-data.ts b/src/backend/tests/test-data/organizations.test-data.ts index 3f3d3293bb..1c6e7bddf5 100644 --- a/src/backend/tests/test-data/organizations.test-data.ts +++ b/src/backend/tests/test-data/organizations.test-data.ts @@ -4,6 +4,9 @@ export const testLink1: LinkCreateArgs[] = [ { linkId: '-1', linkTypeName: 'example link type', - url: 'https://example.com/link1' + url: 'https://example.com/link1', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false } ]; diff --git a/src/backend/tests/unit/organization.test.ts b/src/backend/tests/unit/organization.test.ts index 9fca867b26..9529e916dc 100644 --- a/src/backend/tests/unit/organization.test.ts +++ b/src/backend/tests/unit/organization.test.ts @@ -60,12 +60,18 @@ describe('Organization Tests', () => { { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 1' + url: 'link 1', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }, { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 2' + url: 'link 2', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false } ]; const testBatman = await createTestUser(batmanAppAdmin, orgId); @@ -91,12 +97,18 @@ describe('Organization Tests', () => { { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 3' + url: 'link 3', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }, { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 4' + url: 'link 4', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false } ]; await OrganizationsService.setUsefulLinks(testBatman, orgId, testLinks2); @@ -123,12 +135,18 @@ describe('Organization Tests', () => { { linkId: '1', linkTypeName: 'Link type 1', - url: 'link 1' + url: 'link 1', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }, { linkId: '2', linkTypeName: 'Link type 1', - url: 'link 2' + url: 'link 2', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false } ]; const testBatman = await createTestUser(batmanAppAdmin, orgId); diff --git a/src/backend/tests/unmocked/organization.test.ts b/src/backend/tests/unmocked/organization.test.ts index 4712a97137..5731928e33 100644 --- a/src/backend/tests/unmocked/organization.test.ts +++ b/src/backend/tests/unmocked/organization.test.ts @@ -59,12 +59,18 @@ describe('Organization Tests', () => { { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 1' + url: 'link 1', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }, { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 2' + url: 'link 2', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false } ]; const testBatman = await createTestUser(batmanAppAdmin, orgId); @@ -90,12 +96,18 @@ describe('Organization Tests', () => { { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 3' + url: 'link 3', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }, { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 4' + url: 'link 4', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false } ]; await OrganizationsService.setUsefulLinks(testBatman, orgId, testLinks2); @@ -122,12 +134,18 @@ describe('Organization Tests', () => { { linkId: '1', linkTypeName: 'Link type 1', - url: 'link 1' + url: 'link 1', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }, { linkId: '2', linkTypeName: 'Link type 1', - url: 'link 2' + url: 'link 2', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false } ]; const testBatman = await createTestUser(batmanAppAdmin, orgId); diff --git a/src/frontend/src/components/LinksEditView.tsx b/src/frontend/src/components/LinksEditView.tsx index 26096db428..b816686468 100644 --- a/src/frontend/src/components/LinksEditView.tsx +++ b/src/frontend/src/components/LinksEditView.tsx @@ -42,7 +42,14 @@ const LinksEditView: React.FC<{ if (enforceRequired) { requiredLinkTypeNames.forEach((linkTypeName) => { if (links.some((link) => link.linkTypeName === linkTypeName)) return; - append({ linkId: '-1', url: '', linkTypeName }); + append({ + linkId: '-1', + url: '', + linkTypeName, + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false + }); }); } }, [append, enforceRequired, linkTypes, links, requiredLinkTypeNames]); @@ -103,7 +110,16 @@ const LinksEditView: React.FC<{ append({ linkId: '-1', url: '', linkTypeName: '-1' })} + onClick={() => + append({ + linkId: '-1', + url: '', + linkTypeName: '-1', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false + }) + } sx={{ mt: 2, width: 'max-content' }} > + Add Link diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx index de745504c6..dd6d1e58eb 100644 --- a/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx @@ -19,7 +19,6 @@ import ReactHookTextField from '../../../components/ReactHookTextField'; import { useToast } from '../../../hooks/toasts.hooks'; import { MAX_FILE_SIZE } from 'shared'; import UsefulLinksTable from '../OnboardingConfig/UsefulLinks/UsefulLinksTable'; -import LinkTypeTable from '../ProjectsConfig/LinkTypes/LinkTypeTable'; import GuestDefinitionsTable from './GuestDefinitionsTable'; import { GuestDefinitionType } from 'shared'; @@ -174,12 +173,6 @@ const GuestViewConfig: React.FC = () => { - - - Links Config - - - Project Management Definitions diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx index 6fa92594c4..61e32cebbd 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Box, Typography, useTheme } from '@mui/material'; import UsefulLinksTable from './UsefulLinks/UsefulLinksTable'; -import LinkTypeTable from '../ProjectsConfig/LinkTypes/LinkTypeTable'; const NewMemberDashboardUsefulLinksSection: React.FC = () => { const theme = useTheme(); @@ -26,7 +25,6 @@ const NewMemberDashboardUsefulLinksSection: React.FC = () => { > New Member Dashboard Useful Links - ); diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx index d583dd4fd7..927ba6462b 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx @@ -1,7 +1,6 @@ import { Grid, Typography, List, ListItem, Link, useTheme } from '@mui/material'; import { Box } from '@mui/system'; import UsefulLinksTable from './UsefulLinks/UsefulLinksTable'; -import LinkTypeTable from '../ProjectsConfig/LinkTypes/LinkTypeTable'; import NewMemberMilestoneTable from '../RecruitmentConfig/NewMemberMilestoneTable'; import { useCurrentOrganization } from '../../../hooks/organizations.hooks'; import ErrorPage from '../../ErrorPage'; @@ -61,7 +60,6 @@ const OnboardingConfigSection: React.FC = () => { > Onboarding Page Useful Links - diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx index 8f1ad9184d..ce312f8103 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx @@ -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 ; @@ -24,6 +35,9 @@ const CreateUsefulLinkModal = ({ open, handleClose, linkTypes, currentLinks }: C onSubmit={mutateAsync} linkTypes={linkTypes} currentLinks={currentLinks} + isOnGuestHomePage={isOnGuestHomePage} + isOnNewMemberDashboard={isOnNewMemberDashboard} + isOnOnboardingDashboard={isOnOnboardingDashboard} /> ); }; diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx index 4cb9b3abb0..33e8eac2e2 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx @@ -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 ; @@ -26,6 +38,9 @@ const EditUsefulLinkModal = ({ open, handleClose, linkType, linkTypes, currentLi defaulValues={linkType} linkTypes={linkTypes} currentLinks={currentLinks} + isOnGuestHomePage={isOnGuestHomePage} + isOnNewMemberDashboard={isOnNewMemberDashboard} + isOnOnboardingDashboard={isOnOnboardingDashboard} /> ); }; diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx index 0fbc3b2b34..4486ddef3a 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx @@ -15,6 +15,9 @@ interface UsefulLinkFormModalProps { onSubmit: (data: LinkCreateArgs[]) => void; linkTypes: LinkType[]; currentLinks: Link[]; + isOnGuestHomePage?: boolean; + isOnNewMemberDashboard?: boolean; + isOnOnboardingDashboard?: boolean; } const UsefulLinkFormModal = ({ @@ -23,7 +26,10 @@ const UsefulLinkFormModal = ({ defaulValues, onSubmit, linkTypes, - currentLinks + currentLinks, + isOnGuestHomePage, + isOnNewMemberDashboard, + isOnOnboardingDashboard }: UsefulLinkFormModalProps) => { const toast = useToast(); @@ -47,10 +53,16 @@ const UsefulLinkFormModal = ({ const onFormSubmit = async (data: LinkCreateArgs) => { try { + const dataWithPlacement: LinkCreateArgs = { + ...data, + isOnGuestHomePage: isOnGuestHomePage ?? false, + isOnNewMemberDashboard: isOnNewMemberDashboard ?? false, + isOnOnboardingDashboard: isOnOnboardingDashboard ?? false + }; const previousLinks = linkToLinkCreateArgs(currentLinks); const newLinks = defaulValues - ? [...previousLinks.filter((link) => link.linkId !== defaulValues.linkId), data] - : [...previousLinks, data]; + ? [...previousLinks.filter((link) => link.linkId !== defaulValues.linkId), dataWithPlacement] + : [...previousLinks, dataWithPlacement]; onSubmit(newLinks); } catch (error: unknown) { if (error instanceof Error) { diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx index 473a4b647f..6513f8dc64 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx @@ -42,12 +42,7 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa error: usefulLinksError } = useAllUsefulLinks(); const { mutateAsync } = useSetUsefulLinks(); - const { - data: linkTypesBeforeFilter, - isLoading: linkTypesIsLoading, - isError: linkTypesIsError, - error: linkTypesError - } = useAllLinkTypes(); + const { data: linkTypes, isLoading: linkTypesIsLoading, isError: linkTypesIsError, error: linkTypesError } = useAllLinkTypes(); const toast = useToast(); const [linkToDelete, setLinkToDelete] = useState(); @@ -56,7 +51,7 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa if (usefulLinksIsError) return ; if (linkTypesIsError) return ; - if (!links || usefulLinksIsLoading || !linkTypesBeforeFilter || linkTypesIsLoading) return ; + if (!links || usefulLinksIsLoading || !linkTypes || linkTypesIsLoading) return ; const handleDelete = async (allLinks: Link[], linkToDelete: Link) => { const updatedLinks = allLinks.filter((link) => link.linkId !== linkToDelete.linkId); @@ -71,21 +66,14 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa } }; - const matchesDashboard = (linkType?: { - isOnGuestHomePage: boolean; - isOnNewMemberDashboard: boolean; - isOnOnboardingDashboard: boolean; - }) => { - if (!linkType) return false; - if (isOnNewMemberDashboard) return linkType.isOnNewMemberDashboard; - if (isOnOnboardingDashboard) return linkType.isOnOnboardingDashboard; - if (isOnGuestHomePage) return linkType.isOnGuestHomePage; - return !linkType.isOnGuestHomePage && !linkType.isOnNewMemberDashboard && !linkType.isOnOnboardingDashboard; + const matchesDashboard = (link: { isOnGuestHomePage: boolean; isOnNewMemberDashboard: boolean; isOnOnboardingDashboard: boolean }) => { + if (isOnNewMemberDashboard) return link.isOnNewMemberDashboard; + if (isOnOnboardingDashboard) return link.isOnOnboardingDashboard; + if (isOnGuestHomePage) return link.isOnGuestHomePage; + return !link.isOnGuestHomePage && !link.isOnNewMemberDashboard && !link.isOnOnboardingDashboard; }; - const linkTypes = linkTypesBeforeFilter.filter(matchesDashboard); - - const usefulLinks = links.filter((link) => matchesDashboard(link.linkType)); + const usefulLinks = links.filter(matchesDashboard); return ( @@ -94,6 +82,9 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa handleClose={() => setShowCreateModel(false)} linkTypes={linkTypes} currentLinks={links} + isOnGuestHomePage={isOnGuestHomePage} + isOnNewMemberDashboard={isOnNewMemberDashboard} + isOnOnboardingDashboard={isOnOnboardingDashboard} /> {editingLink && ( )} @@ -180,7 +174,7 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa onHide={() => setLinkToDelete(undefined)} submitText="Delete" onSubmit={() => { - handleDelete(usefulLinks, linkToDelete!); + handleDelete(links, linkToDelete!); }} > diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/CreateLinkTypeModal.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/CreateLinkTypeModal.tsx index f656147c72..ef6085daec 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/CreateLinkTypeModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/CreateLinkTypeModal.tsx @@ -8,35 +8,15 @@ interface CreateLinkTypeModalProps { open: boolean; handleClose: () => void; linkTypes: LinkType[]; - isOnGuestHomePage?: boolean; - isOnNewMemberDashboard?: boolean; - isOnOnboardingDashboard?: boolean; } -const CreateLinkTypeModal = ({ - open, - handleClose, - linkTypes, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard -}: CreateLinkTypeModalProps) => { +const CreateLinkTypeModal = ({ open, handleClose, linkTypes }: CreateLinkTypeModalProps) => { const { isLoading, isError, error, mutateAsync } = useCreateLinkType(); if (isError) return ; if (isLoading) return ; - return ( - - ); + return ; }; export default CreateLinkTypeModal; diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/EditLinkTypeModal.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/EditLinkTypeModal.tsx index b6cf826a5d..785b8982d9 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/EditLinkTypeModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/EditLinkTypeModal.tsx @@ -24,9 +24,6 @@ const EditLinkTypeModal = ({ open, handleClose, linkType, linkTypes }: EditLinkT onSubmit={mutateAsync} defaultValues={linkType} linkTypes={linkTypes} - isOnGuestHomePage={linkType.isOnGuestHomePage} - isOnNewMemberDashboard={linkType.isOnNewMemberDashboard} - isOnOnboardingDashboard={linkType.isOnOnboardingDashboard} /> ); }; diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx index 5e339e47c8..9fbd094d21 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx @@ -16,21 +16,9 @@ interface LinkTypeFormModalProps { defaultValues?: LinkType; onSubmit: (data: LinkTypeCreatePayload) => void; linkTypes: LinkType[]; - isOnGuestHomePage?: boolean; - isOnNewMemberDashboard?: boolean; - isOnOnboardingDashboard?: boolean; } -const LinkTypeFormModal = ({ - open, - handleClose, - defaultValues, - onSubmit, - linkTypes, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard -}: LinkTypeFormModalProps) => { +const LinkTypeFormModal = ({ open, handleClose, defaultValues, onSubmit, linkTypes }: LinkTypeFormModalProps) => { const toast = useToast(); const creatingNew = defaultValues === undefined; @@ -43,10 +31,7 @@ const LinkTypeFormModal = ({ .required('LinkType Name is Required') .test('unique-LinkType-test', 'LinkType name must be unique', uniqueLinkTypeTest), iconName: yup.string().required('Icon name is required'), - required: yup.boolean().required('Required field must be specified'), - isOnGuestHomePage: yup.boolean().required('Guest page field must be specified'), - isOnNewMemberDashboard: yup.boolean().required('New member dashboard field must be specified'), - isOnOnboardingDashboard: yup.boolean().required('Onboarding dashboard field must be specified') + required: yup.boolean().required('Required field must be specified') }); const theme = useTheme(); @@ -62,10 +47,7 @@ const LinkTypeFormModal = ({ defaultValues: { name: defaultValues?.name ?? '', iconName: defaultValues?.iconName ?? '', - required: defaultValues?.required ?? false, - isOnGuestHomePage: isOnGuestHomePage ?? false, - isOnNewMemberDashboard: isOnNewMemberDashboard ?? false, - isOnOnboardingDashboard: isOnOnboardingDashboard ?? false + required: defaultValues?.required ?? false } }); @@ -106,19 +88,13 @@ const LinkTypeFormModal = ({ {errors.name?.message} - {!isOnGuestHomePage && !isOnNewMemberDashboard && !isOnOnboardingDashboard && ( - - - Required - } - /> - {errors.required?.message} - - - )} + + + Required + } /> + {errors.required?.message} + + diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeTable.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeTable.tsx index e442b5b31c..aae45e2a84 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeTable.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeTable.tsx @@ -10,27 +10,20 @@ import NERTable from '../../../../components/NERTable'; import { isAdmin, LinkType } from 'shared'; import { useCurrentUser } from '../../../../hooks/users.hooks'; -interface LinkTypeTableProps { - isOnGuestHomePage?: boolean; - isOnNewMemberDashboard?: boolean; - isOnOnboardingDashboard?: boolean; -} - -const LinkTypeTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboardingDashboard }: LinkTypeTableProps) => { +const LinkTypeTable = () => { const currentUser = useCurrentUser(); - const { data: links, isLoading: linkTypeIsLoading, isError: linkTypeIsError, error: linkTypeError } = useAllLinkTypes(); + const { + data: linkTypes, + isLoading: linkTypeIsLoading, + isError: linkTypeIsError, + error: linkTypeError + } = useAllLinkTypes(); const [createModalShow, setCreateModalShow] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [clickedLinkType, setClickedLinkType] = useState(); - if (!links || linkTypeIsLoading) return ; + if (!linkTypes || linkTypeIsLoading) return ; if (linkTypeIsError) return ; - const linkTypes = links.filter((linkType) => { - if (isOnNewMemberDashboard) return linkType.isOnNewMemberDashboard; - if (isOnOnboardingDashboard) return linkType.isOnOnboardingDashboard; - if (isOnGuestHomePage) return linkType.isOnGuestHomePage; - return !linkType.isOnGuestHomePage && !linkType.isOnNewMemberDashboard && !linkType.isOnOnboardingDashboard; - }); const linkTypeTableRows = linkTypes.map((linkType, index) => ( - setCreateModalShow(false)} - linkTypes={linkTypes} - isOnGuestHomePage={isOnGuestHomePage} - isOnNewMemberDashboard={isOnNewMemberDashboard} - isOnOnboardingDashboard={isOnOnboardingDashboard} - /> + setCreateModalShow(false)} linkTypes={linkTypes} /> {clickedLinkType && ( = ({ links: currentProject.links.map((link) => ({ linkTypeName: link.linkType.name, url: link.url, - linkId: link.linkId + linkId: link.linkId, + isOnGuestHomePage: link.isOnGuestHomePage, + isOnNewMemberDashboard: link.isOnNewMemberDashboard, + isOnOnboardingDashboard: link.isOnOnboardingDashboard })), budget: data.budget, summary: currentProject.summary, diff --git a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx index 61608ec74f..f5d9446b87 100644 --- a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx +++ b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx @@ -51,7 +51,10 @@ export const GanttTimeLineChangeModal = ({ change, handleClose, open }: GanttTim return { linkId: link.linkId, linkTypeName: link.linkType.name, - url: link.url + url: link.url, + isOnGuestHomePage: link.isOnGuestHomePage, + isOnNewMemberDashboard: link.isOnNewMemberDashboard, + isOnOnboardingDashboard: link.isOnOnboardingDashboard }; }; diff --git a/src/frontend/src/pages/HomePage/IntroGuestHomePage.tsx b/src/frontend/src/pages/HomePage/IntroGuestHomePage.tsx index 85c80e32b7..a86b47637a 100644 --- a/src/frontend/src/pages/HomePage/IntroGuestHomePage.tsx +++ b/src/frontend/src/pages/HomePage/IntroGuestHomePage.tsx @@ -53,7 +53,7 @@ const IntroGuestHomePage = () => { if (!organization || organizationIsLoading || !usefulLinks || usefulLinksIsLoading) return ; - const guestPageLinks = usefulLinks?.filter((link) => link.linkType.isOnGuestHomePage); + const guestPageLinks = usefulLinks?.filter((link) => link.isOnGuestHomePage); return ( diff --git a/src/frontend/src/pages/HomePage/components/GuestOrganizationInfo.tsx b/src/frontend/src/pages/HomePage/components/GuestOrganizationInfo.tsx index 505a8efbc9..e05c44e5af 100644 --- a/src/frontend/src/pages/HomePage/components/GuestOrganizationInfo.tsx +++ b/src/frontend/src/pages/HomePage/components/GuestOrganizationInfo.tsx @@ -49,7 +49,7 @@ const GuestOrganizationInfo = () => { if (isLoading || !organization) return ; if (!links || usefulLinksIsLoading || !linkTypes || linkTypesIsLoading) return ; - const usefulLinks = links?.filter((link) => link.linkType.isOnGuestHomePage); + const usefulLinks = links?.filter((link) => link.isOnGuestHomePage); return ( = ({ if (isError) return ; if (isLoading || !usefulLinks) return ; - const links = usefulLinks.filter((link) => link.linkType[dashboardFlag]); + const links = usefulLinks.filter((link) => link[dashboardFlag]); return ( = ({ project, ex const links = project.links.map((link) => ({ linkId: link.linkId, url: link.url, - linkTypeName: link.linkType.name + linkTypeName: link.linkType.name, + isOnGuestHomePage: link.isOnGuestHomePage, + isOnNewMemberDashboard: link.isOnNewMemberDashboard, + isOnOnboardingDashboard: link.isOnOnboardingDashboard })); if (isCRHookLoading || isLeadershipCRLoading) return ; @@ -62,7 +65,16 @@ const ProjectEditContainer: React.FC = ({ project, ex const projectLinkTypeNames = links.map((link) => link.linkTypeName); requiredLinkTypeNames .filter((name) => !projectLinkTypeNames.includes(name)) - .forEach((name) => links.push({ linkId: '-1', url: '', linkTypeName: name })); + .forEach((name) => + links.push({ + linkId: '-1', + url: '', + linkTypeName: name, + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false + }) + ); const defaultValues: ProjectFormInput = { name, diff --git a/src/frontend/src/tests/test-support/test-data/projects.stub.ts b/src/frontend/src/tests/test-support/test-data/projects.stub.ts index 1288541dcd..61a8520786 100644 --- a/src/frontend/src/tests/test-support/test-data/projects.stub.ts +++ b/src/frontend/src/tests/test-support/test-data/projects.stub.ts @@ -13,45 +13,45 @@ import { exampleResearchWorkPackage, exampleDesignWorkPackage, exampleManufactur const exampleConfluenceLinkType: LinkType = { name: 'Confluence', iconName: 'confluence', - required: true, - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + required: true }; const exampleBomLinkType: LinkType = { name: 'BOM', iconName: 'bom', - required: true, - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + required: true }; const exampleGDriveLinkType: LinkType = { name: 'Google Drive', iconName: 'google-drive', - required: true, - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + required: true }; const exampleLinks: Link[] = [ { linkId: '1', linkType: exampleConfluenceLinkType, - url: 'https://www.google.com' + url: 'https://www.google.com', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }, { linkId: '2', linkType: exampleBomLinkType, - url: 'https://www.google.com' + url: 'https://www.google.com', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }, { linkId: '3', linkType: exampleGDriveLinkType, - url: 'https://www.google.com' + url: 'https://www.google.com', + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false } ]; export const exampleProject1: Project = { diff --git a/src/frontend/src/utils/link.utils.ts b/src/frontend/src/utils/link.utils.ts index a6f14753e3..c2e3095bb8 100644 --- a/src/frontend/src/utils/link.utils.ts +++ b/src/frontend/src/utils/link.utils.ts @@ -9,7 +9,10 @@ export const linkToLinkCreateArgs = (links: Link[]) => { return { linkId: link.linkId, linkTypeName: link.linkType.name, - url: link.url + url: link.url, + isOnGuestHomePage: link.isOnGuestHomePage, + isOnNewMemberDashboard: link.isOnNewMemberDashboard, + isOnOnboardingDashboard: link.isOnOnboardingDashboard }; }); }; diff --git a/src/shared/src/types/project-types.ts b/src/shared/src/types/project-types.ts index 9694ad371b..25ce91df49 100644 --- a/src/shared/src/types/project-types.ts +++ b/src/shared/src/types/project-types.ts @@ -133,21 +133,24 @@ export interface LinkType { name: string; required: boolean; iconName: string; - isOnGuestHomePage: boolean; - isOnNewMemberDashboard: boolean; - isOnOnboardingDashboard: boolean; } export interface Link { linkId: string; linkType: LinkType; url: string; + isOnGuestHomePage: boolean; + isOnNewMemberDashboard: boolean; + isOnOnboardingDashboard: boolean; } export interface LinkCreateArgs { linkId: string; linkTypeName: string; url: string; + isOnGuestHomePage: boolean; + isOnNewMemberDashboard: boolean; + isOnOnboardingDashboard: boolean; } export interface WbsProposedChanges { @@ -190,9 +193,6 @@ export interface LinkTypeCreatePayload { name: string; iconName: string; required: boolean; - isOnGuestHomePage: boolean; - isOnNewMemberDashboard: boolean; - isOnOnboardingDashboard: boolean; } export interface DescriptionBulletTypeCreatePayload { From a812129e8f90770bf3aaf84e643f0a01b6a50b59 Mon Sep 17 00:00:00 2001 From: wavehassman Date: Wed, 19 Aug 2026 21:23:59 -0400 Subject: [PATCH 2/8] prettier --- .../UsefulLinks/UsefulLinksTable.tsx | 13 +++++++++++-- .../ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx index 6513f8dc64..6e1ea65cca 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx @@ -42,7 +42,12 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa error: usefulLinksError } = useAllUsefulLinks(); const { mutateAsync } = useSetUsefulLinks(); - const { data: linkTypes, isLoading: linkTypesIsLoading, isError: linkTypesIsError, error: linkTypesError } = useAllLinkTypes(); + const { + data: linkTypes, + isLoading: linkTypesIsLoading, + isError: linkTypesIsError, + error: linkTypesError + } = useAllLinkTypes(); const toast = useToast(); const [linkToDelete, setLinkToDelete] = useState(); @@ -66,7 +71,11 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa } }; - const matchesDashboard = (link: { isOnGuestHomePage: boolean; isOnNewMemberDashboard: boolean; isOnOnboardingDashboard: boolean }) => { + const matchesDashboard = (link: { + isOnGuestHomePage: boolean; + isOnNewMemberDashboard: boolean; + isOnOnboardingDashboard: boolean; + }) => { if (isOnNewMemberDashboard) return link.isOnNewMemberDashboard; if (isOnOnboardingDashboard) return link.isOnOnboardingDashboard; if (isOnGuestHomePage) return link.isOnGuestHomePage; diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx index 9fbd094d21..e41485d1cb 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx @@ -91,7 +91,11 @@ const LinkTypeFormModal = ({ open, handleClose, defaultValues, onSubmit, linkTyp Required - } /> + } + /> {errors.required?.message} From 9c3a6c456b484336832e2243fe3a16544767553e Mon Sep 17 00:00:00 2001 From: wavehassman Date: Wed, 19 Aug 2026 21:48:42 -0400 Subject: [PATCH 3/8] harden security --- .../migration.sql | 13 +++++ .../src/services/organizations.services.ts | 50 ++++++++++--------- src/backend/src/utils/organizations.utils.ts | 14 ++++-- .../UsefulLinks/UsefulLinkFormModal.tsx | 6 +-- .../LinkTypes/LinkTypeTable.tsx | 2 +- 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql b/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql index 05e6a34c48..35912b06f3 100644 --- a/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql +++ b/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql @@ -20,6 +20,19 @@ FROM "Link_Type" lt WHERE l."linkTypeId" = lt."id" AND l."organizationId" IS NOT NULL; +-- Enforce the "one dashboard per link" invariant: some legacy Link_Type rows (e.g. the old +-- seeded "Handbook" type) had more than one placement flag set, which the backfill above would +-- otherwise carry over verbatim. Keep only the highest-priority flag per link, prioritizing +-- onboarding dashboard, then new member dashboard, then guest home page. +UPDATE "Link" +SET "isOnNewMemberDashboard" = false +WHERE "isOnNewMemberDashboard" = true AND "isOnOnboardingDashboard" = true; + +UPDATE "Link" +SET "isOnGuestHomePage" = false +WHERE "isOnGuestHomePage" = true + AND ("isOnNewMemberDashboard" = true OR "isOnOnboardingDashboard" = true); + -- AlterTable: Link_Type - drop the now-relocated placement columns ALTER TABLE "Link_Type" DROP COLUMN "isOnGuestHomePage", diff --git a/src/backend/src/services/organizations.services.ts b/src/backend/src/services/organizations.services.ts index b11daf7e92..9b12c7570a 100644 --- a/src/backend/src/services/organizations.services.ts +++ b/src/backend/src/services/organizations.services.ts @@ -87,32 +87,34 @@ export default class OrganizationsService { const currentLinkIds = organization.usefulLinks.map((link) => link.linkId); - // deleting all current useful links so they are empty before repopulating - await prisma.link.deleteMany({ - where: { - linkId: { in: currentLinkIds } - } - }); - - const newLinks = await createUsefulLinks(links, organization.organizationId, submitter); - - const newLinkIds = newLinks.map((link) => { - return { linkId: link.linkId }; - }); - - // setting the useful links to the newly created ones - await prisma.organization.update({ - where: { - organizationId: organization.organizationId - }, - data: { - usefulLinks: { - connect: newLinkIds + return prisma.$transaction(async (tx) => { + // deleting all current useful links so they are empty before repopulating + await tx.link.deleteMany({ + where: { + linkId: { in: currentLinkIds } } - } - }); + }); + + const newLinks = await createUsefulLinks(tx, links, organization.organizationId, submitter); + + const newLinkIds = newLinks.map((link) => { + return { linkId: link.linkId }; + }); + + // setting the useful links to the newly created ones + await tx.organization.update({ + where: { + organizationId: organization.organizationId + }, + data: { + usefulLinks: { + connect: newLinkIds + } + } + }); - return newLinks; + return newLinks; + }); } /** diff --git a/src/backend/src/utils/organizations.utils.ts b/src/backend/src/utils/organizations.utils.ts index 05f440ba27..f676ecdba0 100644 --- a/src/backend/src/utils/organizations.utils.ts +++ b/src/backend/src/utils/organizations.utils.ts @@ -2,7 +2,14 @@ import { LinkCreateArgs, User } from 'shared'; import prisma from '../prisma/prisma.js'; import { HttpException } from './errors.utils.js'; -export const createUsefulLinks = async (links: LinkCreateArgs[], organizationId: string, submitter: User) => { +export type PrismaTransactionClient = Parameters[0]>[0]; + +export const createUsefulLinks = async ( + tx: PrismaTransactionClient, + links: LinkCreateArgs[], + organizationId: string, + submitter: User +) => { const newLinks = []; for (const link of links) { const dashboardFlagCount = [link.isOnGuestHomePage, link.isOnNewMemberDashboard, link.isOnOnboardingDashboard].filter( @@ -12,7 +19,7 @@ export const createUsefulLinks = async (links: LinkCreateArgs[], organizationId: throw new HttpException(400, 'A useful link can only be on one dashboard at a time'); } - const linkType = await prisma.link_Type.findUnique({ + const linkType = await tx.link_Type.findUnique({ where: { uniqueLinkType: { name: link.linkTypeName, @@ -25,7 +32,7 @@ export const createUsefulLinks = async (links: LinkCreateArgs[], organizationId: throw new HttpException(400, `Link type with name '${link.linkTypeName}' not found`); } - const newLink = await prisma.link.create({ + const newLink = await tx.link.create({ data: { linkType: { connect: { @@ -43,6 +50,7 @@ export const createUsefulLinks = async (links: LinkCreateArgs[], organizationId: } } }); + newLinks.push(newLink); } return newLinks; diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx index 4486ddef3a..505604754a 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx @@ -12,7 +12,7 @@ interface UsefulLinkFormModalProps { open: boolean; handleClose: () => void; defaulValues?: Link; - onSubmit: (data: LinkCreateArgs[]) => void; + onSubmit: (data: LinkCreateArgs[]) => Promise; linkTypes: LinkType[]; currentLinks: Link[]; isOnGuestHomePage?: boolean; @@ -63,13 +63,13 @@ const UsefulLinkFormModal = ({ const newLinks = defaulValues ? [...previousLinks.filter((link) => link.linkId !== defaulValues.linkId), dataWithPlacement] : [...previousLinks, dataWithPlacement]; - onSubmit(newLinks); + await onSubmit(newLinks); + handleClose(); } catch (error: unknown) { if (error instanceof Error) { toast.error(error.message); } } - handleClose(); }; return ( { const [showEditModal, setShowEditModal] = useState(false); const [clickedLinkType, setClickedLinkType] = useState(); - if (!linkTypes || linkTypeIsLoading) return ; if (linkTypeIsError) return ; + if (!linkTypes || linkTypeIsLoading) return ; const linkTypeTableRows = linkTypes.map((linkType, index) => ( Date: Fri, 21 Aug 2026 15:55:28 -0400 Subject: [PATCH 4/8] Revert "harden security" This reverts commit 9c3a6c456b484336832e2243fe3a16544767553e. --- .../migration.sql | 13 ----- .../src/services/organizations.services.ts | 50 +++++++++---------- src/backend/src/utils/organizations.utils.ts | 14 ++---- .../UsefulLinks/UsefulLinkFormModal.tsx | 6 +-- .../LinkTypes/LinkTypeTable.tsx | 2 +- 5 files changed, 31 insertions(+), 54 deletions(-) diff --git a/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql b/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql index 35912b06f3..05e6a34c48 100644 --- a/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql +++ b/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql @@ -20,19 +20,6 @@ FROM "Link_Type" lt WHERE l."linkTypeId" = lt."id" AND l."organizationId" IS NOT NULL; --- Enforce the "one dashboard per link" invariant: some legacy Link_Type rows (e.g. the old --- seeded "Handbook" type) had more than one placement flag set, which the backfill above would --- otherwise carry over verbatim. Keep only the highest-priority flag per link, prioritizing --- onboarding dashboard, then new member dashboard, then guest home page. -UPDATE "Link" -SET "isOnNewMemberDashboard" = false -WHERE "isOnNewMemberDashboard" = true AND "isOnOnboardingDashboard" = true; - -UPDATE "Link" -SET "isOnGuestHomePage" = false -WHERE "isOnGuestHomePage" = true - AND ("isOnNewMemberDashboard" = true OR "isOnOnboardingDashboard" = true); - -- AlterTable: Link_Type - drop the now-relocated placement columns ALTER TABLE "Link_Type" DROP COLUMN "isOnGuestHomePage", diff --git a/src/backend/src/services/organizations.services.ts b/src/backend/src/services/organizations.services.ts index 9b12c7570a..b11daf7e92 100644 --- a/src/backend/src/services/organizations.services.ts +++ b/src/backend/src/services/organizations.services.ts @@ -87,34 +87,32 @@ export default class OrganizationsService { const currentLinkIds = organization.usefulLinks.map((link) => link.linkId); - return prisma.$transaction(async (tx) => { - // deleting all current useful links so they are empty before repopulating - await tx.link.deleteMany({ - where: { - linkId: { in: currentLinkIds } - } - }); - - const newLinks = await createUsefulLinks(tx, links, organization.organizationId, submitter); - - const newLinkIds = newLinks.map((link) => { - return { linkId: link.linkId }; - }); - - // setting the useful links to the newly created ones - await tx.organization.update({ - where: { - organizationId: organization.organizationId - }, - data: { - usefulLinks: { - connect: newLinkIds - } - } - }); + // deleting all current useful links so they are empty before repopulating + await prisma.link.deleteMany({ + where: { + linkId: { in: currentLinkIds } + } + }); + + const newLinks = await createUsefulLinks(links, organization.organizationId, submitter); + + const newLinkIds = newLinks.map((link) => { + return { linkId: link.linkId }; + }); - return newLinks; + // setting the useful links to the newly created ones + await prisma.organization.update({ + where: { + organizationId: organization.organizationId + }, + data: { + usefulLinks: { + connect: newLinkIds + } + } }); + + return newLinks; } /** diff --git a/src/backend/src/utils/organizations.utils.ts b/src/backend/src/utils/organizations.utils.ts index f676ecdba0..05f440ba27 100644 --- a/src/backend/src/utils/organizations.utils.ts +++ b/src/backend/src/utils/organizations.utils.ts @@ -2,14 +2,7 @@ import { LinkCreateArgs, User } from 'shared'; import prisma from '../prisma/prisma.js'; import { HttpException } from './errors.utils.js'; -export type PrismaTransactionClient = Parameters[0]>[0]; - -export const createUsefulLinks = async ( - tx: PrismaTransactionClient, - links: LinkCreateArgs[], - organizationId: string, - submitter: User -) => { +export const createUsefulLinks = async (links: LinkCreateArgs[], organizationId: string, submitter: User) => { const newLinks = []; for (const link of links) { const dashboardFlagCount = [link.isOnGuestHomePage, link.isOnNewMemberDashboard, link.isOnOnboardingDashboard].filter( @@ -19,7 +12,7 @@ export const createUsefulLinks = async ( throw new HttpException(400, 'A useful link can only be on one dashboard at a time'); } - const linkType = await tx.link_Type.findUnique({ + const linkType = await prisma.link_Type.findUnique({ where: { uniqueLinkType: { name: link.linkTypeName, @@ -32,7 +25,7 @@ export const createUsefulLinks = async ( throw new HttpException(400, `Link type with name '${link.linkTypeName}' not found`); } - const newLink = await tx.link.create({ + const newLink = await prisma.link.create({ data: { linkType: { connect: { @@ -50,7 +43,6 @@ export const createUsefulLinks = async ( } } }); - newLinks.push(newLink); } return newLinks; diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx index 505604754a..4486ddef3a 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx @@ -12,7 +12,7 @@ interface UsefulLinkFormModalProps { open: boolean; handleClose: () => void; defaulValues?: Link; - onSubmit: (data: LinkCreateArgs[]) => Promise; + onSubmit: (data: LinkCreateArgs[]) => void; linkTypes: LinkType[]; currentLinks: Link[]; isOnGuestHomePage?: boolean; @@ -63,13 +63,13 @@ const UsefulLinkFormModal = ({ const newLinks = defaulValues ? [...previousLinks.filter((link) => link.linkId !== defaulValues.linkId), dataWithPlacement] : [...previousLinks, dataWithPlacement]; - await onSubmit(newLinks); - handleClose(); + onSubmit(newLinks); } catch (error: unknown) { if (error instanceof Error) { toast.error(error.message); } } + handleClose(); }; return ( { const [showEditModal, setShowEditModal] = useState(false); const [clickedLinkType, setClickedLinkType] = useState(); - if (linkTypeIsError) return ; if (!linkTypes || linkTypeIsLoading) return ; + if (linkTypeIsError) return ; const linkTypeTableRows = linkTypes.map((linkType, index) => ( Date: Fri, 21 Aug 2026 15:55:28 -0400 Subject: [PATCH 5/8] Revert "prettier" This reverts commit a812129e8f90770bf3aaf84e643f0a01b6a50b59. --- .../UsefulLinks/UsefulLinksTable.tsx | 13 ++----------- .../ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx | 6 +----- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx index 6e1ea65cca..6513f8dc64 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx @@ -42,12 +42,7 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa error: usefulLinksError } = useAllUsefulLinks(); const { mutateAsync } = useSetUsefulLinks(); - const { - data: linkTypes, - isLoading: linkTypesIsLoading, - isError: linkTypesIsError, - error: linkTypesError - } = useAllLinkTypes(); + const { data: linkTypes, isLoading: linkTypesIsLoading, isError: linkTypesIsError, error: linkTypesError } = useAllLinkTypes(); const toast = useToast(); const [linkToDelete, setLinkToDelete] = useState(); @@ -71,11 +66,7 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa } }; - const matchesDashboard = (link: { - isOnGuestHomePage: boolean; - isOnNewMemberDashboard: boolean; - isOnOnboardingDashboard: boolean; - }) => { + const matchesDashboard = (link: { isOnGuestHomePage: boolean; isOnNewMemberDashboard: boolean; isOnOnboardingDashboard: boolean }) => { if (isOnNewMemberDashboard) return link.isOnNewMemberDashboard; if (isOnOnboardingDashboard) return link.isOnOnboardingDashboard; if (isOnGuestHomePage) return link.isOnGuestHomePage; diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx index e41485d1cb..9fbd094d21 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx @@ -91,11 +91,7 @@ const LinkTypeFormModal = ({ open, handleClose, defaultValues, onSubmit, linkTyp Required - } - /> + } /> {errors.required?.message} From d8a7ae1be92243ee4c901f7886a9e320e3d66ed1 Mon Sep 17 00:00:00 2001 From: wavehassman Date: Fri, 21 Aug 2026 15:55:29 -0400 Subject: [PATCH 6/8] Revert "restructure links and link types" This reverts commit e470a46e298c2b820161385d8753fcdc3413a5b4. --- .../src/controllers/projects.controllers.ts | 25 +++++++-- .../change-requests.query-args.ts | 16 +++--- .../prisma/factories/config-data.factory.ts | 54 +++++++++---------- .../migration.sql | 27 ---------- src/backend/src/prisma/schema.prisma | 6 +-- src/backend/src/routes/projects.routes.ts | 6 +++ src/backend/src/services/projects.services.ts | 39 ++++++++++++-- src/backend/src/utils/organizations.utils.ts | 10 ---- src/backend/src/utils/validation.utils.ts | 8 +-- .../test-data/organizations.test-data.ts | 5 +- src/backend/tests/unit/organization.test.ts | 30 +++-------- .../tests/unmocked/organization.test.ts | 30 +++-------- src/frontend/src/components/LinksEditView.tsx | 20 +------ .../EditGuestView/GuestViewConfig.tsx | 7 +++ .../NewMemberDashboardUsefulLinksSection.tsx | 2 + .../OnboardingConfigSection.tsx | 2 + .../UsefulLinks/CreateUsefulLinkModal.tsx | 16 +----- .../UsefulLinks/EditUsefulLinkModal.tsx | 17 +----- .../UsefulLinks/UsefulLinkFormModal.tsx | 18 ++----- .../UsefulLinks/UsefulLinksTable.tsx | 36 +++++++------ .../LinkTypes/CreateLinkTypeModal.tsx | 24 ++++++++- .../LinkTypes/EditLinkTypeModal.tsx | 3 ++ .../LinkTypes/LinkTypeFormModal.tsx | 44 +++++++++++---- .../LinkTypes/LinkTypeTable.tsx | 32 +++++++---- .../EditProjectBudgetModal.tsx | 5 +- .../GanttTimeLineChangeModal.tsx | 5 +- .../src/pages/HomePage/IntroGuestHomePage.tsx | 2 +- .../components/GuestOrganizationInfo.tsx | 2 +- .../components/NewMemberUsefulLinksWidget.tsx | 2 +- .../ProjectForm/ProjectEditContainer.tsx | 16 +----- .../test-support/test-data/projects.stub.ts | 30 +++++------ src/frontend/src/utils/link.utils.ts | 5 +- src/shared/src/types/project-types.ts | 12 ++--- 33 files changed, 264 insertions(+), 292 deletions(-) delete mode 100644 src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql diff --git a/src/backend/src/controllers/projects.controllers.ts b/src/backend/src/controllers/projects.controllers.ts index 4ddad5eafc..6eaff761c3 100644 --- a/src/backend/src/controllers/projects.controllers.ts +++ b/src/backend/src/controllers/projects.controllers.ts @@ -183,9 +183,18 @@ export default class ProjectsController { static async createLinkType(req: Request, res: Response, next: NextFunction) { try { - const { name, iconName, required } = req.body; + const { name, iconName, required, isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboardingDashboard } = req.body; - const newLinkType = await ProjectsService.createLinkType(req.currentUser, name, iconName, required, req.organization); + const newLinkType = await ProjectsService.createLinkType( + req.currentUser, + name, + iconName, + required, + req.organization, + isOnGuestHomePage, + isOnNewMemberDashboard, + isOnOnboardingDashboard + ); res.status(200).json(newLinkType); } catch (error: unknown) { next(error); @@ -462,13 +471,23 @@ export default class ProjectsController { static async editLinkType(req: Request, res: Response, next: NextFunction) { try { const { linkTypeName } = req.params as Record; - const { name: newName, iconName, required } = req.body; + const { + name: newName, + iconName, + required, + isOnGuestHomePage, + isOnNewMemberDashboard, + isOnOnboardingDashboard + } = req.body; const linkTypeUpdated = await ProjectsService.editLinkType( linkTypeName, iconName, required, req.currentUser, req.organization, + isOnGuestHomePage, + isOnNewMemberDashboard, + isOnOnboardingDashboard, newName ); res.status(200).json(linkTypeUpdated); diff --git a/src/backend/src/prisma-query-args/change-requests.query-args.ts b/src/backend/src/prisma-query-args/change-requests.query-args.ts index 9b426a92b4..b4aa4c9806 100644 --- a/src/backend/src/prisma-query-args/change-requests.query-args.ts +++ b/src/backend/src/prisma-query-args/change-requests.query-args.ts @@ -39,14 +39,14 @@ const getWorkPackageProposedChangesQueryArgs = (organizationId: string) => select: { linkId: true, url: true, - isOnGuestHomePage: true, - isOnNewMemberDashboard: true, - isOnOnboardingDashboard: true, linkType: { select: { name: true, required: true, - iconName: true + iconName: true, + isOnGuestHomePage: true, + isOnNewMemberDashboard: true, + isOnOnboardingDashboard: true } } } @@ -84,14 +84,14 @@ const getWbsProposedChangesQueryArgs = (organizationId: string) => select: { linkId: true, url: true, - isOnGuestHomePage: true, - isOnNewMemberDashboard: true, - isOnOnboardingDashboard: true, linkType: { select: { name: true, required: true, - iconName: true + iconName: true, + isOnGuestHomePage: true, + isOnNewMemberDashboard: true, + isOnOnboardingDashboard: true } } } diff --git a/src/backend/src/prisma/factories/config-data.factory.ts b/src/backend/src/prisma/factories/config-data.factory.ts index a58fbf6bca..2efd493334 100644 --- a/src/backend/src/prisma/factories/config-data.factory.ts +++ b/src/backend/src/prisma/factories/config-data.factory.ts @@ -55,6 +55,7 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Google Drive', iconName: 'add_to_drive', required: false, + isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -62,6 +63,7 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Confluence', iconName: 'article', required: false, + isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -69,6 +71,7 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Github', iconName: 'code', required: false, + isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -76,6 +79,7 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Altium', iconName: 'electric_bolt', required: false, + isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -83,6 +87,7 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Application', iconName: 'ballot', required: false, + isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -90,6 +95,7 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Sign Ups', iconName: 'ballot', required: false, + isOnGuestHomePage: false, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -97,6 +103,7 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'facebook', iconName: 'facebook', required: false, + isOnGuestHomePage: true, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -104,6 +111,7 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Instagram', iconName: 'Instagram', required: false, + isOnGuestHomePage: true, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -111,6 +119,9 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Handbook', iconName: 'menu_book', required: true, + isOnGuestHomePage: false, + isOnNewMemberDashboard: true, + isOnOnboardingDashboard: true, creator: connectUser(creatorId), organization: connectOrganization(organizationId) }, @@ -118,42 +129,29 @@ export const linkTypeCreateInputs = (creatorId: string, organizationId: string): name: 'Team Directory', iconName: 'groups', required: true, + isOnGuestHomePage: false, + isOnNewMemberDashboard: true, creator: connectUser(creatorId), organization: connectOrganization(organizationId) } ]; -type UsefulLinkPlacement = { - url: string; - isOnGuestHomePage?: boolean; - isOnNewMemberDashboard?: boolean; - isOnOnboardingDashboard?: boolean; -}; - -// Placements for the link types that are actually surfaced somewhere (guest home page / new-member -// dashboard / onboarding dashboard) -- the rest of linkTypeCreateInputs are categories only, with no -// seeded Link yet. A type can have more than one placement (e.g. Handbook shows on both the -// new-member and onboarding dashboards), which now means one seeded Link per placement. -const USEFUL_LINK_PLACEMENTS_BY_TYPE_NAME: Record = { - facebook: [{ url: 'https://facebook.com/example-org', isOnGuestHomePage: true }], - Instagram: [{ url: 'https://instagram.com/example-org', isOnGuestHomePage: true }], - Handbook: [ - { url: 'https://example.com/handbook', isOnNewMemberDashboard: true }, - { url: 'https://example.com/handbook', isOnOnboardingDashboard: true } - ], - 'Team Directory': [{ url: 'https://example.com/team-directory', isOnNewMemberDashboard: true }] +// URLs for the link types that are actually surfaced somewhere (guest home page / new-member +// dashboard) -- the rest of linkTypeCreateInputs are categories only, with no seeded Link yet. +const USEFUL_LINK_URL_BY_TYPE_NAME: Record = { + facebook: 'https://facebook.com/example-org', + Instagram: 'https://instagram.com/example-org', + Handbook: 'https://example.com/handbook', + 'Team Directory': 'https://example.com/team-directory' }; export const usefulLinkCreateInput = ( creatorId: string, organizationId: string, linkTypeId: string, - placement: UsefulLinkPlacement + url: string ): Prisma.LinkCreateInput => ({ - url: placement.url, - isOnGuestHomePage: placement.isOnGuestHomePage ?? false, - isOnNewMemberDashboard: placement.isOnNewMemberDashboard ?? false, - isOnOnboardingDashboard: placement.isOnOnboardingDashboard ?? false, + url, creator: connectUser(creatorId), organization: connectOrganization(organizationId), linkType: { connect: { id: linkTypeId } } @@ -165,11 +163,9 @@ export const usefulLinkCreateInputsForTypes = ( linkTypes: { id: string; name: string }[] ): Prisma.LinkCreateInput[] => linkTypes - .filter((linkType) => linkType.name in USEFUL_LINK_PLACEMENTS_BY_TYPE_NAME) - .flatMap((linkType) => - USEFUL_LINK_PLACEMENTS_BY_TYPE_NAME[linkType.name].map((placement) => - usefulLinkCreateInput(creatorId, organizationId, linkType.id, placement) - ) + .filter((linkType) => linkType.name in USEFUL_LINK_URL_BY_TYPE_NAME) + .map((linkType) => + usefulLinkCreateInput(creatorId, organizationId, linkType.id, USEFUL_LINK_URL_BY_TYPE_NAME[linkType.name]) ); export const descriptionBulletTypeCreateInputs = ( diff --git a/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql b/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql deleted file mode 100644 index 05e6a34c48..0000000000 --- a/src/backend/src/prisma/migrations/20260818120000_link_dashboard_placement/migration.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Move dashboard placement (guest home page / new member dashboard / onboarding dashboard) off --- Link_Type (a global category, e.g. "GitHub", "Confluence") and onto Link (a specific instance). --- This lets the same Link_Type be reused across a project link and any number of dashboard links. - --- AlterTable: Link - add the placement columns -ALTER TABLE "Link" -ADD COLUMN "isOnGuestHomePage" BOOLEAN NOT NULL DEFAULT false, -ADD COLUMN "isOnNewMemberDashboard" BOOLEAN NOT NULL DEFAULT false, -ADD COLUMN "isOnOnboardingDashboard" BOOLEAN NOT NULL DEFAULT false; - --- Backfill: copy each existing link's placement down from its current Link_Type before the --- Link_Type columns are dropped. Only org-level useful links (organizationId set) ever had --- meaningful placement; project links (wbsElementId) were never flagged and stay false. -UPDATE "Link" l -SET - "isOnGuestHomePage" = lt."isOnGuestHomePage", - "isOnNewMemberDashboard" = lt."isOnNewMemberDashboard", - "isOnOnboardingDashboard" = lt."isOnOnboardingDashboard" -FROM "Link_Type" lt -WHERE l."linkTypeId" = lt."id" - AND l."organizationId" IS NOT NULL; - --- AlterTable: Link_Type - drop the now-relocated placement columns -ALTER TABLE "Link_Type" -DROP COLUMN "isOnGuestHomePage", -DROP COLUMN "isOnNewMemberDashboard", -DROP COLUMN "isOnOnboardingDashboard"; diff --git a/src/backend/src/prisma/schema.prisma b/src/backend/src/prisma/schema.prisma index ca8e6b3bff..cfc701cbb8 100644 --- a/src/backend/src/prisma/schema.prisma +++ b/src/backend/src/prisma/schema.prisma @@ -588,6 +588,9 @@ model Link_Type { links Link[] @relation(name: "linkTypes") organizationId String organization Organization @relation(fields: [organizationId], references: [organizationId]) + isOnGuestHomePage Boolean @default(false) + isOnNewMemberDashboard Boolean @default(false) + isOnOnboardingDashboard Boolean @default(false) @@unique([name, organizationId], name: "uniqueLinkType") @@index([organizationId]) @@ -602,9 +605,6 @@ model Link { dateDeleted DateTime? linkTypeId String linkType Link_Type @relation(name: "linkTypes", fields: [linkTypeId], references: [id]) - isOnGuestHomePage Boolean @default(false) - isOnNewMemberDashboard Boolean @default(false) - isOnOnboardingDashboard Boolean @default(false) // Either its on a wbsElement, a wbsProposedChanges, or an organization wbsElementId String? diff --git a/src/backend/src/routes/projects.routes.ts b/src/backend/src/routes/projects.routes.ts index 1d8999f339..60e43d8794 100644 --- a/src/backend/src/routes/projects.routes.ts +++ b/src/backend/src/routes/projects.routes.ts @@ -26,6 +26,9 @@ projectRouter.post( nonEmptyString(body('name')), nonEmptyString(body('iconName')), body('required').isBoolean(), + body('isOnGuestHomePage').isBoolean(), + body('isOnNewMemberDashboard').isBoolean(), + body('isOnOnboardingDashboard').isBoolean(), validateInputs, ProjectsController.createLinkType ); @@ -34,6 +37,9 @@ projectRouter.post( nonEmptyString(body('name').optional()), nonEmptyString(body('iconName')), body('required').isBoolean(), + body('isOnGuestHomePage').isBoolean(), + body('isOnNewMemberDashboard').isBoolean(), + body('isOnOnboardingDashboard').isBoolean(), validateInputs, ProjectsController.editLinkType ); diff --git a/src/backend/src/services/projects.services.ts b/src/backend/src/services/projects.services.ts index 7cb4e036f4..304205f595 100644 --- a/src/backend/src/services/projects.services.ts +++ b/src/backend/src/services/projects.services.ts @@ -46,6 +46,17 @@ 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 @@ -601,6 +612,9 @@ export default class ProjectsService { * @param required is the new LinkType required * @param user the user who is creating the new LinkType * @param orgainzationId the organization the link type is being created for + * @param isOnGuestHomePage whether the LinkType shows on the guest home page + * @param isOnNewMemberDashboard whether the LinkType shows on the new member dashboard + * @param isOnOnboardingDashboard whether the LinkType shows on the onboarding checklist page * @throws AccessDeniedException if the submitter of the request is not an admin * @throws HttpException if a LinkType of the given name already exists * @returns the created LinkType @@ -610,11 +624,16 @@ export default class ProjectsService { name: string, iconName: string, required: boolean, - organization: Organization + organization: Organization, + isOnGuestHomePage: boolean, + isOnNewMemberDashboard: boolean, + isOnOnboardingDashboard: boolean ): Promise { 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 } } }); @@ -627,7 +646,10 @@ export default class ProjectsService { creatorId: user.userId, iconName, required, - organizationId: organization.organizationId + organizationId: organization.organizationId, + isOnGuestHomePage, + isOnNewMemberDashboard, + isOnOnboardingDashboard } }); @@ -641,6 +663,9 @@ export default class ProjectsService { * @param required the new required status * @param submitter user requesting the edit * @param organizationId the organization the user is currently in + * @param isOnGuestHomePage whether the LinkType shows on the guest home page + * @param isOnNewMemberDashboard whether the LinkType shows on the new member dashboard + * @param isOnOnboardingDashboard whether the LinkType shows on the onboarding checklist page * @param newName the new name of the linkType, if being renamed * @returns the updated linkType */ @@ -650,11 +675,16 @@ export default class ProjectsService { required: boolean, submitter: User, organization: Organization, + isOnGuestHomePage: boolean, + isOnNewMemberDashboard: boolean, + isOnOnboardingDashboard: boolean, newName?: string ): Promise { 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: { @@ -687,7 +717,10 @@ export default class ProjectsService { data: { name: newName && newName ? newName : linkName, iconName, - required + required, + isOnGuestHomePage, + isOnNewMemberDashboard, + isOnOnboardingDashboard } }); return linkTypeUpdated; diff --git a/src/backend/src/utils/organizations.utils.ts b/src/backend/src/utils/organizations.utils.ts index 05f440ba27..576baeb742 100644 --- a/src/backend/src/utils/organizations.utils.ts +++ b/src/backend/src/utils/organizations.utils.ts @@ -5,13 +5,6 @@ import { HttpException } from './errors.utils.js'; export const createUsefulLinks = async (links: LinkCreateArgs[], organizationId: string, submitter: User) => { const newLinks = []; for (const link of links) { - const dashboardFlagCount = [link.isOnGuestHomePage, link.isOnNewMemberDashboard, link.isOnOnboardingDashboard].filter( - Boolean - ).length; - if (dashboardFlagCount > 1) { - throw new HttpException(400, 'A useful link can only be on one dashboard at a time'); - } - const linkType = await prisma.link_Type.findUnique({ where: { uniqueLinkType: { @@ -33,9 +26,6 @@ export const createUsefulLinks = async (links: LinkCreateArgs[], organizationId: } }, url: link.url, - isOnGuestHomePage: link.isOnGuestHomePage, - isOnNewMemberDashboard: link.isOnNewMemberDashboard, - isOnOnboardingDashboard: link.isOnOnboardingDashboard, creator: { connect: { userId: submitter.userId diff --git a/src/backend/src/utils/validation.utils.ts b/src/backend/src/utils/validation.utils.ts index 8086289486..13c7325208 100644 --- a/src/backend/src/utils/validation.utils.ts +++ b/src/backend/src/utils/validation.utils.ts @@ -186,9 +186,6 @@ export const projectProposedChangesValidators = [ projectProposedChangesExists(body('projectProposedChanges.links')).isArray(), nonEmptyString(body('projectProposedChanges.links.*.url')), nonEmptyString(body('projectProposedChanges.links.*.linkTypeName')), - body('projectProposedChanges.links.*.isOnGuestHomePage').isBoolean(), - body('projectProposedChanges.links.*.isOnNewMemberDashboard').isBoolean(), - body('projectProposedChanges.links.*.isOnOnboardingDashboard').isBoolean(), nonEmptyString(body('projectProposedChanges.leadId').optional()), nonEmptyString(body('projectProposedChanges.managerId').optional()), nonEmptyString(projectProposedChangesExists(body('projectProposedChanges.summary'))), @@ -263,10 +260,7 @@ export const blockedByValidators = [ export const linkValidators = [ body('links').isArray(), nonEmptyString(body('links.*.url')), - nonEmptyString(body('links.*.linkTypeName')), - body('links.*.isOnGuestHomePage').isBoolean(), - body('links.*.isOnNewMemberDashboard').isBoolean(), - body('links.*.isOnOnboardingDashboard').isBoolean() + nonEmptyString(body('links.*.linkTypeName')) ]; export const projectValidators = [ diff --git a/src/backend/tests/test-data/organizations.test-data.ts b/src/backend/tests/test-data/organizations.test-data.ts index 1c6e7bddf5..3f3d3293bb 100644 --- a/src/backend/tests/test-data/organizations.test-data.ts +++ b/src/backend/tests/test-data/organizations.test-data.ts @@ -4,9 +4,6 @@ export const testLink1: LinkCreateArgs[] = [ { linkId: '-1', linkTypeName: 'example link type', - url: 'https://example.com/link1', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'https://example.com/link1' } ]; diff --git a/src/backend/tests/unit/organization.test.ts b/src/backend/tests/unit/organization.test.ts index 9529e916dc..9fca867b26 100644 --- a/src/backend/tests/unit/organization.test.ts +++ b/src/backend/tests/unit/organization.test.ts @@ -60,18 +60,12 @@ describe('Organization Tests', () => { { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 1', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 1' }, { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 2', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 2' } ]; const testBatman = await createTestUser(batmanAppAdmin, orgId); @@ -97,18 +91,12 @@ describe('Organization Tests', () => { { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 3', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 3' }, { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 4', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 4' } ]; await OrganizationsService.setUsefulLinks(testBatman, orgId, testLinks2); @@ -135,18 +123,12 @@ describe('Organization Tests', () => { { linkId: '1', linkTypeName: 'Link type 1', - url: 'link 1', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 1' }, { linkId: '2', linkTypeName: 'Link type 1', - url: 'link 2', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 2' } ]; const testBatman = await createTestUser(batmanAppAdmin, orgId); diff --git a/src/backend/tests/unmocked/organization.test.ts b/src/backend/tests/unmocked/organization.test.ts index 5731928e33..4712a97137 100644 --- a/src/backend/tests/unmocked/organization.test.ts +++ b/src/backend/tests/unmocked/organization.test.ts @@ -59,18 +59,12 @@ describe('Organization Tests', () => { { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 1', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 1' }, { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 2', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 2' } ]; const testBatman = await createTestUser(batmanAppAdmin, orgId); @@ -96,18 +90,12 @@ describe('Organization Tests', () => { { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 3', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 3' }, { linkId: '-1', linkTypeName: 'Link type 1', - url: 'link 4', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 4' } ]; await OrganizationsService.setUsefulLinks(testBatman, orgId, testLinks2); @@ -134,18 +122,12 @@ describe('Organization Tests', () => { { linkId: '1', linkTypeName: 'Link type 1', - url: 'link 1', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 1' }, { linkId: '2', linkTypeName: 'Link type 1', - url: 'link 2', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'link 2' } ]; const testBatman = await createTestUser(batmanAppAdmin, orgId); diff --git a/src/frontend/src/components/LinksEditView.tsx b/src/frontend/src/components/LinksEditView.tsx index b816686468..26096db428 100644 --- a/src/frontend/src/components/LinksEditView.tsx +++ b/src/frontend/src/components/LinksEditView.tsx @@ -42,14 +42,7 @@ const LinksEditView: React.FC<{ if (enforceRequired) { requiredLinkTypeNames.forEach((linkTypeName) => { if (links.some((link) => link.linkTypeName === linkTypeName)) return; - append({ - linkId: '-1', - url: '', - linkTypeName, - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false - }); + append({ linkId: '-1', url: '', linkTypeName }); }); } }, [append, enforceRequired, linkTypes, links, requiredLinkTypeNames]); @@ -110,16 +103,7 @@ const LinksEditView: React.FC<{ - append({ - linkId: '-1', - url: '', - linkTypeName: '-1', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false - }) - } + onClick={() => append({ linkId: '-1', url: '', linkTypeName: '-1' })} sx={{ mt: 2, width: 'max-content' }} > + Add Link diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx index dd6d1e58eb..de745504c6 100644 --- a/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx @@ -19,6 +19,7 @@ import ReactHookTextField from '../../../components/ReactHookTextField'; import { useToast } from '../../../hooks/toasts.hooks'; import { MAX_FILE_SIZE } from 'shared'; import UsefulLinksTable from '../OnboardingConfig/UsefulLinks/UsefulLinksTable'; +import LinkTypeTable from '../ProjectsConfig/LinkTypes/LinkTypeTable'; import GuestDefinitionsTable from './GuestDefinitionsTable'; import { GuestDefinitionType } from 'shared'; @@ -173,6 +174,12 @@ const GuestViewConfig: React.FC = () => { + + + Links Config + + + Project Management Definitions diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx index 61e32cebbd..6fa92594c4 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Box, Typography, useTheme } from '@mui/material'; import UsefulLinksTable from './UsefulLinks/UsefulLinksTable'; +import LinkTypeTable from '../ProjectsConfig/LinkTypes/LinkTypeTable'; const NewMemberDashboardUsefulLinksSection: React.FC = () => { const theme = useTheme(); @@ -25,6 +26,7 @@ const NewMemberDashboardUsefulLinksSection: React.FC = () => { > New Member Dashboard Useful Links + ); diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx index 927ba6462b..d583dd4fd7 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx @@ -1,6 +1,7 @@ import { Grid, Typography, List, ListItem, Link, useTheme } from '@mui/material'; import { Box } from '@mui/system'; import UsefulLinksTable from './UsefulLinks/UsefulLinksTable'; +import LinkTypeTable from '../ProjectsConfig/LinkTypes/LinkTypeTable'; import NewMemberMilestoneTable from '../RecruitmentConfig/NewMemberMilestoneTable'; import { useCurrentOrganization } from '../../../hooks/organizations.hooks'; import ErrorPage from '../../ErrorPage'; @@ -60,6 +61,7 @@ const OnboardingConfigSection: React.FC = () => { > Onboarding Page Useful Links + diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx index ce312f8103..8f1ad9184d 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx @@ -9,20 +9,9 @@ interface CreateUsefulLinkModalProps { handleClose: () => void; linkTypes: LinkType[]; currentLinks: Link[]; - isOnGuestHomePage?: boolean; - isOnNewMemberDashboard?: boolean; - isOnOnboardingDashboard?: boolean; } -const CreateUsefulLinkModal = ({ - open, - handleClose, - linkTypes, - currentLinks, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard -}: CreateUsefulLinkModalProps) => { +const CreateUsefulLinkModal = ({ open, handleClose, linkTypes, currentLinks }: CreateUsefulLinkModalProps) => { const { isLoading, isError, error, mutateAsync } = useSetUsefulLinks(); if (isError) return ; @@ -35,9 +24,6 @@ const CreateUsefulLinkModal = ({ onSubmit={mutateAsync} linkTypes={linkTypes} currentLinks={currentLinks} - isOnGuestHomePage={isOnGuestHomePage} - isOnNewMemberDashboard={isOnNewMemberDashboard} - isOnOnboardingDashboard={isOnOnboardingDashboard} /> ); }; diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx index 33e8eac2e2..4cb9b3abb0 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx @@ -10,21 +10,9 @@ interface EditUsefulLinkModalProps { linkType: Link; linkTypes: LinkType[]; currentLinks: Link[]; - isOnGuestHomePage?: boolean; - isOnNewMemberDashboard?: boolean; - isOnOnboardingDashboard?: boolean; } -const EditUsefulLinkModal = ({ - open, - handleClose, - linkType, - linkTypes, - currentLinks, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard -}: EditUsefulLinkModalProps) => { +const EditUsefulLinkModal = ({ open, handleClose, linkType, linkTypes, currentLinks }: EditUsefulLinkModalProps) => { const { isLoading, isError, error, mutateAsync } = useSetUsefulLinks(); if (isError) return ; @@ -38,9 +26,6 @@ const EditUsefulLinkModal = ({ defaulValues={linkType} linkTypes={linkTypes} currentLinks={currentLinks} - isOnGuestHomePage={isOnGuestHomePage} - isOnNewMemberDashboard={isOnNewMemberDashboard} - isOnOnboardingDashboard={isOnOnboardingDashboard} /> ); }; diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx index 4486ddef3a..0fbc3b2b34 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx @@ -15,9 +15,6 @@ interface UsefulLinkFormModalProps { onSubmit: (data: LinkCreateArgs[]) => void; linkTypes: LinkType[]; currentLinks: Link[]; - isOnGuestHomePage?: boolean; - isOnNewMemberDashboard?: boolean; - isOnOnboardingDashboard?: boolean; } const UsefulLinkFormModal = ({ @@ -26,10 +23,7 @@ const UsefulLinkFormModal = ({ defaulValues, onSubmit, linkTypes, - currentLinks, - isOnGuestHomePage, - isOnNewMemberDashboard, - isOnOnboardingDashboard + currentLinks }: UsefulLinkFormModalProps) => { const toast = useToast(); @@ -53,16 +47,10 @@ const UsefulLinkFormModal = ({ const onFormSubmit = async (data: LinkCreateArgs) => { try { - const dataWithPlacement: LinkCreateArgs = { - ...data, - isOnGuestHomePage: isOnGuestHomePage ?? false, - isOnNewMemberDashboard: isOnNewMemberDashboard ?? false, - isOnOnboardingDashboard: isOnOnboardingDashboard ?? false - }; const previousLinks = linkToLinkCreateArgs(currentLinks); const newLinks = defaulValues - ? [...previousLinks.filter((link) => link.linkId !== defaulValues.linkId), dataWithPlacement] - : [...previousLinks, dataWithPlacement]; + ? [...previousLinks.filter((link) => link.linkId !== defaulValues.linkId), data] + : [...previousLinks, data]; onSubmit(newLinks); } catch (error: unknown) { if (error instanceof Error) { diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx index 6513f8dc64..473a4b647f 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinksTable.tsx @@ -42,7 +42,12 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa error: usefulLinksError } = useAllUsefulLinks(); const { mutateAsync } = useSetUsefulLinks(); - const { data: linkTypes, isLoading: linkTypesIsLoading, isError: linkTypesIsError, error: linkTypesError } = useAllLinkTypes(); + const { + data: linkTypesBeforeFilter, + isLoading: linkTypesIsLoading, + isError: linkTypesIsError, + error: linkTypesError + } = useAllLinkTypes(); const toast = useToast(); const [linkToDelete, setLinkToDelete] = useState(); @@ -51,7 +56,7 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa if (usefulLinksIsError) return ; if (linkTypesIsError) return ; - if (!links || usefulLinksIsLoading || !linkTypes || linkTypesIsLoading) return ; + if (!links || usefulLinksIsLoading || !linkTypesBeforeFilter || linkTypesIsLoading) return ; const handleDelete = async (allLinks: Link[], linkToDelete: Link) => { const updatedLinks = allLinks.filter((link) => link.linkId !== linkToDelete.linkId); @@ -66,14 +71,21 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa } }; - const matchesDashboard = (link: { isOnGuestHomePage: boolean; isOnNewMemberDashboard: boolean; isOnOnboardingDashboard: boolean }) => { - if (isOnNewMemberDashboard) return link.isOnNewMemberDashboard; - if (isOnOnboardingDashboard) return link.isOnOnboardingDashboard; - if (isOnGuestHomePage) return link.isOnGuestHomePage; - return !link.isOnGuestHomePage && !link.isOnNewMemberDashboard && !link.isOnOnboardingDashboard; + const matchesDashboard = (linkType?: { + isOnGuestHomePage: boolean; + isOnNewMemberDashboard: boolean; + isOnOnboardingDashboard: boolean; + }) => { + if (!linkType) return false; + if (isOnNewMemberDashboard) return linkType.isOnNewMemberDashboard; + if (isOnOnboardingDashboard) return linkType.isOnOnboardingDashboard; + if (isOnGuestHomePage) return linkType.isOnGuestHomePage; + return !linkType.isOnGuestHomePage && !linkType.isOnNewMemberDashboard && !linkType.isOnOnboardingDashboard; }; - const usefulLinks = links.filter(matchesDashboard); + const linkTypes = linkTypesBeforeFilter.filter(matchesDashboard); + + const usefulLinks = links.filter((link) => matchesDashboard(link.linkType)); return ( @@ -82,9 +94,6 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa handleClose={() => setShowCreateModel(false)} linkTypes={linkTypes} currentLinks={links} - isOnGuestHomePage={isOnGuestHomePage} - isOnNewMemberDashboard={isOnNewMemberDashboard} - isOnOnboardingDashboard={isOnOnboardingDashboard} /> {editingLink && ( )} @@ -174,7 +180,7 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa onHide={() => setLinkToDelete(undefined)} submitText="Delete" onSubmit={() => { - handleDelete(links, linkToDelete!); + handleDelete(usefulLinks, linkToDelete!); }} > diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/CreateLinkTypeModal.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/CreateLinkTypeModal.tsx index ef6085daec..f656147c72 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/CreateLinkTypeModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/CreateLinkTypeModal.tsx @@ -8,15 +8,35 @@ interface CreateLinkTypeModalProps { open: boolean; handleClose: () => void; linkTypes: LinkType[]; + isOnGuestHomePage?: boolean; + isOnNewMemberDashboard?: boolean; + isOnOnboardingDashboard?: boolean; } -const CreateLinkTypeModal = ({ open, handleClose, linkTypes }: CreateLinkTypeModalProps) => { +const CreateLinkTypeModal = ({ + open, + handleClose, + linkTypes, + isOnGuestHomePage, + isOnNewMemberDashboard, + isOnOnboardingDashboard +}: CreateLinkTypeModalProps) => { const { isLoading, isError, error, mutateAsync } = useCreateLinkType(); if (isError) return ; if (isLoading) return ; - return ; + return ( + + ); }; export default CreateLinkTypeModal; diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/EditLinkTypeModal.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/EditLinkTypeModal.tsx index 785b8982d9..b6cf826a5d 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/EditLinkTypeModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/EditLinkTypeModal.tsx @@ -24,6 +24,9 @@ const EditLinkTypeModal = ({ open, handleClose, linkType, linkTypes }: EditLinkT onSubmit={mutateAsync} defaultValues={linkType} linkTypes={linkTypes} + isOnGuestHomePage={linkType.isOnGuestHomePage} + isOnNewMemberDashboard={linkType.isOnNewMemberDashboard} + isOnOnboardingDashboard={linkType.isOnOnboardingDashboard} /> ); }; diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx index 9fbd094d21..5e339e47c8 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeFormModal.tsx @@ -16,9 +16,21 @@ interface LinkTypeFormModalProps { defaultValues?: LinkType; onSubmit: (data: LinkTypeCreatePayload) => void; linkTypes: LinkType[]; + isOnGuestHomePage?: boolean; + isOnNewMemberDashboard?: boolean; + isOnOnboardingDashboard?: boolean; } -const LinkTypeFormModal = ({ open, handleClose, defaultValues, onSubmit, linkTypes }: LinkTypeFormModalProps) => { +const LinkTypeFormModal = ({ + open, + handleClose, + defaultValues, + onSubmit, + linkTypes, + isOnGuestHomePage, + isOnNewMemberDashboard, + isOnOnboardingDashboard +}: LinkTypeFormModalProps) => { const toast = useToast(); const creatingNew = defaultValues === undefined; @@ -31,7 +43,10 @@ const LinkTypeFormModal = ({ open, handleClose, defaultValues, onSubmit, linkTyp .required('LinkType Name is Required') .test('unique-LinkType-test', 'LinkType name must be unique', uniqueLinkTypeTest), iconName: yup.string().required('Icon name is required'), - required: yup.boolean().required('Required field must be specified') + required: yup.boolean().required('Required field must be specified'), + isOnGuestHomePage: yup.boolean().required('Guest page field must be specified'), + isOnNewMemberDashboard: yup.boolean().required('New member dashboard field must be specified'), + isOnOnboardingDashboard: yup.boolean().required('Onboarding dashboard field must be specified') }); const theme = useTheme(); @@ -47,7 +62,10 @@ const LinkTypeFormModal = ({ open, handleClose, defaultValues, onSubmit, linkTyp defaultValues: { name: defaultValues?.name ?? '', iconName: defaultValues?.iconName ?? '', - required: defaultValues?.required ?? false + required: defaultValues?.required ?? false, + isOnGuestHomePage: isOnGuestHomePage ?? false, + isOnNewMemberDashboard: isOnNewMemberDashboard ?? false, + isOnOnboardingDashboard: isOnOnboardingDashboard ?? false } }); @@ -88,13 +106,19 @@ const LinkTypeFormModal = ({ open, handleClose, defaultValues, onSubmit, linkTyp {errors.name?.message} - - - Required - } /> - {errors.required?.message} - - + {!isOnGuestHomePage && !isOnNewMemberDashboard && !isOnOnboardingDashboard && ( + + + Required + } + /> + {errors.required?.message} + + + )} diff --git a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeTable.tsx b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeTable.tsx index aae45e2a84..e442b5b31c 100644 --- a/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeTable.tsx +++ b/src/frontend/src/pages/AdminToolsPage/ProjectsConfig/LinkTypes/LinkTypeTable.tsx @@ -10,20 +10,27 @@ import NERTable from '../../../../components/NERTable'; import { isAdmin, LinkType } from 'shared'; import { useCurrentUser } from '../../../../hooks/users.hooks'; -const LinkTypeTable = () => { +interface LinkTypeTableProps { + isOnGuestHomePage?: boolean; + isOnNewMemberDashboard?: boolean; + isOnOnboardingDashboard?: boolean; +} + +const LinkTypeTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboardingDashboard }: LinkTypeTableProps) => { const currentUser = useCurrentUser(); - const { - data: linkTypes, - isLoading: linkTypeIsLoading, - isError: linkTypeIsError, - error: linkTypeError - } = useAllLinkTypes(); + const { data: links, isLoading: linkTypeIsLoading, isError: linkTypeIsError, error: linkTypeError } = useAllLinkTypes(); const [createModalShow, setCreateModalShow] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [clickedLinkType, setClickedLinkType] = useState(); - if (!linkTypes || linkTypeIsLoading) return ; + if (!links || linkTypeIsLoading) return ; if (linkTypeIsError) return ; + const linkTypes = links.filter((linkType) => { + if (isOnNewMemberDashboard) return linkType.isOnNewMemberDashboard; + if (isOnOnboardingDashboard) return linkType.isOnOnboardingDashboard; + if (isOnGuestHomePage) return linkType.isOnGuestHomePage; + return !linkType.isOnGuestHomePage && !linkType.isOnNewMemberDashboard && !linkType.isOnOnboardingDashboard; + }); const linkTypeTableRows = linkTypes.map((linkType, index) => ( { return ( - setCreateModalShow(false)} linkTypes={linkTypes} /> + setCreateModalShow(false)} + linkTypes={linkTypes} + isOnGuestHomePage={isOnGuestHomePage} + isOnNewMemberDashboard={isOnNewMemberDashboard} + isOnOnboardingDashboard={isOnOnboardingDashboard} + /> {clickedLinkType && ( = ({ links: currentProject.links.map((link) => ({ linkTypeName: link.linkType.name, url: link.url, - linkId: link.linkId, - isOnGuestHomePage: link.isOnGuestHomePage, - isOnNewMemberDashboard: link.isOnNewMemberDashboard, - isOnOnboardingDashboard: link.isOnOnboardingDashboard + linkId: link.linkId })), budget: data.budget, summary: currentProject.summary, diff --git a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx index f5d9446b87..61608ec74f 100644 --- a/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx +++ b/src/frontend/src/pages/GanttPage/ProjectGanttChart/ProjectGanttChangeModals/GanttTimeLineChangeModal.tsx @@ -51,10 +51,7 @@ export const GanttTimeLineChangeModal = ({ change, handleClose, open }: GanttTim return { linkId: link.linkId, linkTypeName: link.linkType.name, - url: link.url, - isOnGuestHomePage: link.isOnGuestHomePage, - isOnNewMemberDashboard: link.isOnNewMemberDashboard, - isOnOnboardingDashboard: link.isOnOnboardingDashboard + url: link.url }; }; diff --git a/src/frontend/src/pages/HomePage/IntroGuestHomePage.tsx b/src/frontend/src/pages/HomePage/IntroGuestHomePage.tsx index a86b47637a..85c80e32b7 100644 --- a/src/frontend/src/pages/HomePage/IntroGuestHomePage.tsx +++ b/src/frontend/src/pages/HomePage/IntroGuestHomePage.tsx @@ -53,7 +53,7 @@ const IntroGuestHomePage = () => { if (!organization || organizationIsLoading || !usefulLinks || usefulLinksIsLoading) return ; - const guestPageLinks = usefulLinks?.filter((link) => link.isOnGuestHomePage); + const guestPageLinks = usefulLinks?.filter((link) => link.linkType.isOnGuestHomePage); return ( diff --git a/src/frontend/src/pages/HomePage/components/GuestOrganizationInfo.tsx b/src/frontend/src/pages/HomePage/components/GuestOrganizationInfo.tsx index e05c44e5af..505a8efbc9 100644 --- a/src/frontend/src/pages/HomePage/components/GuestOrganizationInfo.tsx +++ b/src/frontend/src/pages/HomePage/components/GuestOrganizationInfo.tsx @@ -49,7 +49,7 @@ const GuestOrganizationInfo = () => { if (isLoading || !organization) return ; if (!links || usefulLinksIsLoading || !linkTypes || linkTypesIsLoading) return ; - const usefulLinks = links?.filter((link) => link.isOnGuestHomePage); + const usefulLinks = links?.filter((link) => link.linkType.isOnGuestHomePage); return ( = ({ if (isError) return ; if (isLoading || !usefulLinks) return ; - const links = usefulLinks.filter((link) => link[dashboardFlag]); + const links = usefulLinks.filter((link) => link.linkType[dashboardFlag]); return ( = ({ project, ex const links = project.links.map((link) => ({ linkId: link.linkId, url: link.url, - linkTypeName: link.linkType.name, - isOnGuestHomePage: link.isOnGuestHomePage, - isOnNewMemberDashboard: link.isOnNewMemberDashboard, - isOnOnboardingDashboard: link.isOnOnboardingDashboard + linkTypeName: link.linkType.name })); if (isCRHookLoading || isLeadershipCRLoading) return ; @@ -65,16 +62,7 @@ const ProjectEditContainer: React.FC = ({ project, ex const projectLinkTypeNames = links.map((link) => link.linkTypeName); requiredLinkTypeNames .filter((name) => !projectLinkTypeNames.includes(name)) - .forEach((name) => - links.push({ - linkId: '-1', - url: '', - linkTypeName: name, - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false - }) - ); + .forEach((name) => links.push({ linkId: '-1', url: '', linkTypeName: name })); const defaultValues: ProjectFormInput = { name, diff --git a/src/frontend/src/tests/test-support/test-data/projects.stub.ts b/src/frontend/src/tests/test-support/test-data/projects.stub.ts index 61a8520786..1288541dcd 100644 --- a/src/frontend/src/tests/test-support/test-data/projects.stub.ts +++ b/src/frontend/src/tests/test-support/test-data/projects.stub.ts @@ -13,45 +13,45 @@ import { exampleResearchWorkPackage, exampleDesignWorkPackage, exampleManufactur const exampleConfluenceLinkType: LinkType = { name: 'Confluence', iconName: 'confluence', - required: true + required: true, + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }; const exampleBomLinkType: LinkType = { name: 'BOM', iconName: 'bom', - required: true + required: true, + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }; const exampleGDriveLinkType: LinkType = { name: 'Google Drive', iconName: 'google-drive', - required: true + required: true, + isOnGuestHomePage: false, + isOnNewMemberDashboard: false, + isOnOnboardingDashboard: false }; const exampleLinks: Link[] = [ { linkId: '1', linkType: exampleConfluenceLinkType, - url: 'https://www.google.com', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'https://www.google.com' }, { linkId: '2', linkType: exampleBomLinkType, - url: 'https://www.google.com', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'https://www.google.com' }, { linkId: '3', linkType: exampleGDriveLinkType, - url: 'https://www.google.com', - isOnGuestHomePage: false, - isOnNewMemberDashboard: false, - isOnOnboardingDashboard: false + url: 'https://www.google.com' } ]; export const exampleProject1: Project = { diff --git a/src/frontend/src/utils/link.utils.ts b/src/frontend/src/utils/link.utils.ts index c2e3095bb8..a6f14753e3 100644 --- a/src/frontend/src/utils/link.utils.ts +++ b/src/frontend/src/utils/link.utils.ts @@ -9,10 +9,7 @@ export const linkToLinkCreateArgs = (links: Link[]) => { return { linkId: link.linkId, linkTypeName: link.linkType.name, - url: link.url, - isOnGuestHomePage: link.isOnGuestHomePage, - isOnNewMemberDashboard: link.isOnNewMemberDashboard, - isOnOnboardingDashboard: link.isOnOnboardingDashboard + url: link.url }; }); }; diff --git a/src/shared/src/types/project-types.ts b/src/shared/src/types/project-types.ts index 25ce91df49..9694ad371b 100644 --- a/src/shared/src/types/project-types.ts +++ b/src/shared/src/types/project-types.ts @@ -133,24 +133,21 @@ export interface LinkType { name: string; required: boolean; iconName: string; + isOnGuestHomePage: boolean; + isOnNewMemberDashboard: boolean; + isOnOnboardingDashboard: boolean; } export interface Link { linkId: string; linkType: LinkType; url: string; - isOnGuestHomePage: boolean; - isOnNewMemberDashboard: boolean; - isOnOnboardingDashboard: boolean; } export interface LinkCreateArgs { linkId: string; linkTypeName: string; url: string; - isOnGuestHomePage: boolean; - isOnNewMemberDashboard: boolean; - isOnOnboardingDashboard: boolean; } export interface WbsProposedChanges { @@ -193,6 +190,9 @@ export interface LinkTypeCreatePayload { name: string; iconName: string; required: boolean; + isOnGuestHomePage: boolean; + isOnNewMemberDashboard: boolean; + isOnOnboardingDashboard: boolean; } export interface DescriptionBulletTypeCreatePayload { From 96993639211eba0d5965919f72a9cc701752b703 Mon Sep 17 00:00:00 2001 From: wavehassman Date: Sat, 22 Aug 2026 18:58:53 -0400 Subject: [PATCH 7/8] make link types belong to multiple dashboards --- src/backend/src/services/projects.services.ts | 15 --- src/backend/tests/unmocked/link-type.test.ts | 98 +++++++++++++++++++ src/frontend/src/hooks/projects.hooks.ts | 21 ++++ .../AdminToolsPage/AdminToolsRecruitment.tsx | 2 +- .../EditGuestView/GuestViewConfig.tsx | 2 +- .../NewMemberDashboardUsefulLinksSection.tsx | 2 +- .../OnboardingConfigSection.tsx | 2 +- .../UsefulLinks/CreateUsefulLinkModal.tsx | 16 ++- .../UsefulLinks/EditUsefulLinkModal.tsx | 17 +++- .../UsefulLinks/UsefulLinkFormModal.tsx | 37 ++++++- .../UsefulLinks/UsefulLinksTable.tsx | 18 ++-- 11 files changed, 198 insertions(+), 32 deletions(-) create mode 100644 src/backend/tests/unmocked/link-type.test.ts diff --git a/src/backend/src/services/projects.services.ts b/src/backend/src/services/projects.services.ts index 304205f595..eb1c9d2726 100644 --- a/src/backend/src/services/projects.services.ts +++ b/src/backend/src/services/projects.services.ts @@ -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 @@ -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 } } }); @@ -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: { diff --git a/src/backend/tests/unmocked/link-type.test.ts b/src/backend/tests/unmocked/link-type.test.ts new file mode 100644 index 0000000000..22af282d76 --- /dev/null +++ b/src/backend/tests/unmocked/link-type.test.ts @@ -0,0 +1,98 @@ +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); + }); + }); +}); diff --git a/src/frontend/src/hooks/projects.hooks.ts b/src/frontend/src/hooks/projects.hooks.ts index 1381a0064c..5f49d5a89c 100644 --- a/src/frontend/src/hooks/projects.hooks.ts +++ b/src/frontend/src/hooks/projects.hooks.ts @@ -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( + ['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 */ diff --git a/src/frontend/src/pages/AdminToolsPage/AdminToolsRecruitment.tsx b/src/frontend/src/pages/AdminToolsPage/AdminToolsRecruitment.tsx index ff4cdd052c..64d0dfa99e 100644 --- a/src/frontend/src/pages/AdminToolsPage/AdminToolsRecruitment.tsx +++ b/src/frontend/src/pages/AdminToolsPage/AdminToolsRecruitment.tsx @@ -20,7 +20,7 @@ const AdminToolsProjectsConfig: React.FC = () => { Useful Links - + Description Bullet Types diff --git a/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx index de745504c6..5431c01302 100644 --- a/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx +++ b/src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx @@ -170,7 +170,7 @@ const GuestViewConfig: React.FC = () => { > Guest Page Links - + diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx index 6fa92594c4..98533394de 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/NewMemberDashboardUsefulLinksSection.tsx @@ -27,7 +27,7 @@ const NewMemberDashboardUsefulLinksSection: React.FC = () => { New Member Dashboard Useful Links - + ); }; diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx index d583dd4fd7..4617aa48a9 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/OnboardingConfigSection.tsx @@ -62,7 +62,7 @@ const OnboardingConfigSection: React.FC = () => { Onboarding Page Useful Links - + diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx index 8f1ad9184d..1309680ef5 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/CreateUsefulLinkModal.tsx @@ -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 ; @@ -24,6 +35,9 @@ const CreateUsefulLinkModal = ({ open, handleClose, linkTypes, currentLinks }: C onSubmit={mutateAsync} linkTypes={linkTypes} currentLinks={currentLinks} + isOnGuestHomePage={isOnGuestHomePage} + isOnNewMemberDashboard={isOnNewMemberDashboard} + isOnOnboardingDashboard={isOnOnboardingDashboard} /> ); }; diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx index 4cb9b3abb0..1c3caf21f3 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/EditUsefulLinkModal.tsx @@ -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 ; @@ -26,6 +38,9 @@ const EditUsefulLinkModal = ({ open, handleClose, linkType, linkTypes, currentLi defaulValues={linkType} linkTypes={linkTypes} currentLinks={currentLinks} + isOnGuestHomePage={isOnGuestHomePage} + isOnNewMemberDashboard={isOnNewMemberDashboard} + isOnOnboardingDashboard={isOnOnboardingDashboard} /> ); }; diff --git a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx index 0fbc3b2b34..686897ba61 100644 --- a/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx +++ b/src/frontend/src/pages/AdminToolsPage/OnboardingConfig/UsefulLinks/UsefulLinkFormModal.tsx @@ -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; linkTypes: LinkType[]; currentLinks: Link[]; + isOnGuestHomePage: boolean; + isOnNewMemberDashboard: boolean; + isOnOnboardingDashboard: boolean; } const UsefulLinkFormModal = ({ @@ -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'), @@ -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 ( { @@ -83,8 +83,6 @@ 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 ( @@ -92,8 +90,11 @@ const UsefulLinksTable = ({ isOnGuestHomePage, isOnNewMemberDashboard, isOnOnboa setShowCreateModel(false)} - linkTypes={linkTypes} + linkTypes={linkTypesBeforeFilter} currentLinks={links} + isOnGuestHomePage={isOnGuestHomePage} + isOnNewMemberDashboard={isOnNewMemberDashboard} + isOnOnboardingDashboard={isOnOnboardingDashboard} /> {editingLink && ( )} From c1814bf098f855c6b6688e92305aa3eb307f4cfc Mon Sep 17 00:00:00 2001 From: wavehassman Date: Sat, 22 Aug 2026 19:02:06 -0400 Subject: [PATCH 8/8] prettier --- src/backend/tests/unmocked/link-type.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/backend/tests/unmocked/link-type.test.ts b/src/backend/tests/unmocked/link-type.test.ts index 22af282d76..369367ee02 100644 --- a/src/backend/tests/unmocked/link-type.test.ts +++ b/src/backend/tests/unmocked/link-type.test.ts @@ -44,7 +44,16 @@ describe('LinkType Tests', () => { }); 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); + const linkType = await ProjectsService.createLinkType( + testBatman, + 'GitHub', + 'code', + false, + organization, + false, + true, + true + ); expect(linkType.isOnNewMemberDashboard).toBe(true); expect(linkType.isOnOnboardingDashboard).toBe(true);