diff --git a/apps/ui-community/src/App.tsx b/apps/ui-community/src/App.tsx index 83cd0fd0f..27b80d1c8 100644 --- a/apps/ui-community/src/App.tsx +++ b/apps/ui-community/src/App.tsx @@ -1,5 +1,5 @@ import { RequireAuth } from '@cellix/ui-core'; -import { Accounts } from '@ocom/ui-community-route-accounts'; +import { Accounts, Member } from '@ocom/ui-community-route-accounts'; import { Admin } from '@ocom/ui-community-route-admin'; import { Root } from '@ocom/ui-community-route-root'; import { Route, Routes } from 'react-router-dom'; @@ -27,6 +27,10 @@ export default function App() { path="/accounts/*" element={} /> + } + /> } diff --git a/packages/ocom-verification/acceptance-ui/package.json b/packages/ocom-verification/acceptance-ui/package.json index 5e4cc8c05..889bc39dd 100644 --- a/packages/ocom-verification/acceptance-ui/package.json +++ b/packages/ocom-verification/acceptance-ui/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@apollo/client": "^3.13.9", + "@cellix/serenity-framework": "workspace:*", "@cucumber/cucumber": "catalog:", "@dr.pogodin/react-helmet": "^3.0.4", - "@cellix/serenity-framework": "workspace:*", "@serenity-js/console-reporter": "catalog:", "@serenity-js/core": "catalog:", "@serenity-js/cucumber": "catalog:", @@ -33,7 +33,7 @@ "@types/node": "catalog:", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", - "c8": "^10.1.3", + "c8": "^11.0.0", "tsx": "catalog:", "typescript": "catalog:" } diff --git a/packages/ocom/application-services/src/contexts/community/member/member-management.operations.test.ts b/packages/ocom/application-services/src/contexts/community/member/member-management.operations.test.ts index fd7736c34..81b6cf9b0 100644 --- a/packages/ocom/application-services/src/contexts/community/member/member-management.operations.test.ts +++ b/packages/ocom/application-services/src/contexts/community/member/member-management.operations.test.ts @@ -321,6 +321,58 @@ describe('member-management operations', () => { expect(member.profile.showProfile).toBe(true); }); + it('updates member profile without service-level actor permission checks', async () => { + const member = { + memberName: 'Old Name', + profile: { + name: '', + email: '', + bio: '', + showProfile: false, + showEmail: false, + showInterests: false, + showLocation: false, + showProperties: false, + }, + }; + memberRepository.getById.mockResolvedValue(member); + memberRepository.save.mockResolvedValue({ id: 'member-1' }); + + const result = await updateMemberProfile(dataSources)({ + memberId: 'member-1', + profile: { + name: 'Jane Doe', + }, + }); + + expect(result.id).toBe('member-1'); + expect(member.profile.name).toBe('Jane Doe'); + expect(member.memberName).toBe('Jane Doe'); + }); + + it('throws when a member tries to update another member profile', async () => { + const profile = {} as Record; + Object.defineProperty(profile, 'name', { + get: () => 'Old Name', + set: () => { + throw new Error('You do not have permission to update this profile'); + }, + enumerable: true, + }); + const member = { + memberName: 'Old Name', + profile, + }; + memberRepository.getById.mockResolvedValue(member); + + await expect( + updateMemberProfile(dataSources)({ + memberId: 'member-1', + profile: { name: 'New Name' }, + }), + ).rejects.toThrow('You do not have permission to update this profile'); + }); + it('throws when update member role save returns nothing', async () => { const role = { id: 'role-1', community: { id: 'community-1' } }; const member = { communityId: 'community-1', role: null }; diff --git a/packages/ocom/domain/src/domain/contexts/community/member/features/member.feature b/packages/ocom/domain/src/domain/contexts/community/member/features/member.feature index 6fd619bc8..99593a23b 100644 --- a/packages/ocom/domain/src/domain/contexts/community/member/features/member.feature +++ b/packages/ocom/domain/src/domain/contexts/community/member/features/member.feature @@ -32,6 +32,11 @@ Feature: Member When I set the memberName to "Bob" Then the member's memberName should be "Bob" + Scenario: Changing the memberName with permission to edit own member profile and is editing own member account + Given a Member aggregate with permission to edit own member profile and is editing own member account + When I set the memberName to "Bob" + Then the member's memberName should be "Bob" + Scenario: Changing the memberName without permission Given a Member aggregate without permission to manage members or system account When I try to set the memberName to "Bob" diff --git a/packages/ocom/domain/src/domain/contexts/community/member/member.test.ts b/packages/ocom/domain/src/domain/contexts/community/member/member.test.ts index 95d0fe438..51d67e991 100644 --- a/packages/ocom/domain/src/domain/contexts/community/member/member.test.ts +++ b/packages/ocom/domain/src/domain/contexts/community/member/member.test.ts @@ -18,15 +18,21 @@ const feature = await loadFeature(path.resolve(__dirname, 'features/member.featu function makePassport( overrides: Partial<{ canManageMembers: boolean; + canEditOwnMemberProfile: boolean; + isEditingOwnMemberAccount: boolean; isSystemAccount: boolean; }> = {}, ) { return vi.mocked({ community: { forCommunity: vi.fn(() => ({ - determineIf: (fn: (p: { canManageMembers: boolean; isSystemAccount: boolean }) => boolean) => + determineIf: ( + fn: (p: { canManageMembers: boolean; canEditOwnMemberProfile: boolean; isEditingOwnMemberAccount: boolean; isSystemAccount: boolean }) => boolean, + ) => fn({ canManageMembers: overrides.canManageMembers ?? true, + canEditOwnMemberProfile: overrides.canEditOwnMemberProfile ?? false, + isEditingOwnMemberAccount: overrides.isEditingOwnMemberAccount ?? false, isSystemAccount: overrides.isSystemAccount ?? false, }), })), @@ -216,6 +222,24 @@ test.for(feature, ({ Scenario, Background, BeforeEachScenario }) => { }); }); + Scenario('Changing the memberName with permission to edit own member profile and is editing own member account', ({ Given, When, Then }) => { + Given('a Member aggregate with permission to edit own member profile and is editing own member account', () => { + passport = makePassport({ + canManageMembers: false, + canEditOwnMemberProfile: true, + isEditingOwnMemberAccount: true, + isSystemAccount: false, + }); + member = new Member(makeBaseProps(), passport); + }); + When('I set the memberName to "Bob"', () => { + member.memberName = 'Bob'; + }); + Then('the member\'s memberName should be "Bob"', () => { + expect(member.memberName).toBe('Bob'); + }); + }); + Scenario('Changing the memberName without permission', ({ Given, When, Then }) => { let changeMemberNameWithoutPermission: () => void; Given('a Member aggregate without permission to manage members or system account', () => { diff --git a/packages/ocom/domain/src/domain/contexts/community/member/member.ts b/packages/ocom/domain/src/domain/contexts/community/member/member.ts index 43ceac195..9e792acd5 100644 --- a/packages/ocom/domain/src/domain/contexts/community/member/member.ts +++ b/packages/ocom/domain/src/domain/contexts/community/member/member.ts @@ -203,7 +203,7 @@ export class Member extends AggregateRoot domainPermissions.canManageMembers || domainPermissions.isSystemAccount)) { + if (!this.isNew && !this.visa.determineIf((domainPermissions) => domainPermissions.canManageMembers || domainPermissions.isSystemAccount || (domainPermissions.canEditOwnMemberProfile && domainPermissions.isEditingOwnMemberAccount))) { throw new PermissionError('Cannot set member name'); } this.props.memberName = new ValueObjects.MemberName(memberName).valueOf(); diff --git a/packages/ocom/domain/src/domain/iam/member/contexts/features/member.community.visa.feature b/packages/ocom/domain/src/domain/iam/member/contexts/features/member.community.visa.feature index 8a81be5b1..3cf5d1698 100644 --- a/packages/ocom/domain/src/domain/iam/member/contexts/features/member.community.visa.feature +++ b/packages/ocom/domain/src/domain/iam/member/contexts/features/member.community.visa.feature @@ -39,6 +39,16 @@ Feature: MemberCommunityVisa And I call determineIf with a function that returns canManageCommunitySettings Then the result should be false + Scenario: determineIf sets isEditingOwnMemberAccount to true when the actor has a matching member account + Given a MemberCommunityVisa for the member and community with a matching user account + When I call determineIf with a function that returns isEditingOwnMemberAccount + Then the result should be true + + Scenario: determineIf sets isEditingOwnMemberAccount to false when the actor does not own the member account + Given a MemberCommunityVisa for the member and community with a different user account + When I call determineIf with a function that returns isEditingOwnMemberAccount + Then the result should be false + Scenario: determineIf sets isEditingOwnMemberAccount to false Given a MemberCommunityVisa for the member and community When I call determineIf with a function that returns isEditingOwnMemberAccount diff --git a/packages/ocom/domain/src/domain/iam/member/contexts/member.community.passport.ts b/packages/ocom/domain/src/domain/iam/member/contexts/member.community.passport.ts index 45ba23ce7..5d4ed2469 100644 --- a/packages/ocom/domain/src/domain/iam/member/contexts/member.community.passport.ts +++ b/packages/ocom/domain/src/domain/iam/member/contexts/member.community.passport.ts @@ -6,6 +6,6 @@ import { MemberCommunityVisa } from './member.community.visa.ts'; export class MemberCommunityPassport extends MemberPassportBase implements CommunityPassport { forCommunity(root: CommunityEntityReference): CommunityVisa { - return new MemberCommunityVisa(root, this._member); + return new MemberCommunityVisa(root, this._member, this._user); } } diff --git a/packages/ocom/domain/src/domain/iam/member/contexts/member.community.visa.test.ts b/packages/ocom/domain/src/domain/iam/member/contexts/member.community.visa.test.ts index 86e729a4b..65996b60d 100644 --- a/packages/ocom/domain/src/domain/iam/member/contexts/member.community.visa.test.ts +++ b/packages/ocom/domain/src/domain/iam/member/contexts/member.community.visa.test.ts @@ -20,10 +20,12 @@ function makeMember( roleOverrides: Partial<{ communityPermissions: Record; }> = {}, + accountUserIds: string[] = [], ) { return { id, community: makeCommunity(communityId), + accounts: accountUserIds.map((userId) => ({ user: { id: userId } })), role: { permissions: { communityPermissions: { @@ -151,6 +153,36 @@ test.for(feature, ({ Scenario, Background, BeforeEachScenario }) => { }); }); + Scenario('determineIf sets isEditingOwnMemberAccount to true when the actor has a matching member account', ({ Given, When, Then }) => { + let result: boolean; + const currentUser = { id: 'user-42' } as any; + Given('a MemberCommunityVisa for the member and community with a matching user account', () => { + member = makeMember('member-1', 'community-1', {}, ['user-42']); + visa = new MemberCommunityVisa(community, member, currentUser); + }); + When('I call determineIf with a function that returns isEditingOwnMemberAccount', () => { + result = visa.determineIf((p) => p.isEditingOwnMemberAccount); + }); + Then('the result should be true', () => { + expect(result).toBe(true); + }); + }); + + Scenario('determineIf sets isEditingOwnMemberAccount to false when the actor does not own the member account', ({ Given, When, Then }) => { + let result: boolean; + const currentUser = { id: 'another-user' } as any; + Given('a MemberCommunityVisa for the member and community with a different user account', () => { + member = makeMember('member-1', 'community-1', {}, ['user-42']); + visa = new MemberCommunityVisa(community, member, currentUser); + }); + When('I call determineIf with a function that returns isEditingOwnMemberAccount', () => { + result = visa.determineIf((p) => p.isEditingOwnMemberAccount); + }); + Then('the result should be false', () => { + expect(result).toBe(false); + }); + }); + Scenario('determineIf sets isEditingOwnMemberAccount to false', ({ Given, When, Then }) => { let result: boolean; Given('a MemberCommunityVisa for the member and community', () => { diff --git a/packages/ocom/domain/src/domain/iam/member/contexts/member.community.visa.ts b/packages/ocom/domain/src/domain/iam/member/contexts/member.community.visa.ts index 10072b516..71215fba2 100644 --- a/packages/ocom/domain/src/domain/iam/member/contexts/member.community.visa.ts +++ b/packages/ocom/domain/src/domain/iam/member/contexts/member.community.visa.ts @@ -2,14 +2,17 @@ import type { CommunityEntityReference } from '../../../contexts/community/commu import type { CommunityDomainPermissions } from '../../../contexts/community/community.domain-permissions.ts'; import type { CommunityVisa } from '../../../contexts/community/community.visa.ts'; import type { MemberEntityReference } from '../../../contexts/community/member/member.ts'; +import type { EndUserEntityReference } from '../../../contexts/user/end-user/end-user.ts'; export class MemberCommunityVisa implements CommunityVisa { private readonly root: root; private readonly member: MemberEntityReference; + private readonly user?: EndUserEntityReference; - constructor(root: root, member: MemberEntityReference) { + constructor(root: root, member: MemberEntityReference, user?: EndUserEntityReference) { this.root = root; this.member = member; + this.user = user; } determineIf(func: (permissions: CommunityDomainPermissions) => boolean): boolean { @@ -32,7 +35,9 @@ export class MemberCommunityVisa implemen canEditOwnMemberAccounts: communityPermissions.canEditOwnMemberAccounts, canManageEndUserRolesAndPermissions: communityPermissions.canManageEndUserRolesAndPermissions, canManageSiteContent: communityPermissions.canManageSiteContent, - isEditingOwnMemberAccount: false, + isEditingOwnMemberAccount: this.user + ? this.member.accounts.some((account) => account.user.id === this.user?.id) + : false, canCreateCommunities: true, //TODO: add a more complext rule here like can only create one community for free, otherwise need a paid plan canManageVendorUserRolesAndPermissions: false, // end user roles cannot manage vendor user roles isSystemAccount: false, diff --git a/packages/ocom/graphql/src/schema/types/member.graphql b/packages/ocom/graphql/src/schema/types/member.graphql index 967234086..bd81fb272 100644 --- a/packages/ocom/graphql/src/schema/types/member.graphql +++ b/packages/ocom/graphql/src/schema/types/member.graphql @@ -39,11 +39,18 @@ type MemberProfile { showProperties: Boolean } +type MemberProfileVisibility { + showEmail: Boolean! + showBio: Boolean! + showInterests: Boolean! +} + extend type Query { member(id: ObjectID!): Member! membersByCommunityId(communityId: ObjectID!): [Member!]! memberForCurrentCommunity(communityId: ObjectID!): Member membersForCurrentEndUser: [Member!]! + memberMyProfile(communityId: ID!): Member } extend type Mutation { @@ -69,6 +76,8 @@ extend type Mutation { # Role Management memberRoleUpdate(input: UpdateMemberRoleInput!): MemberMutationResult! + + memberUpdateMyProfile(communityId: ID!, input: UpdateMyMemberProfileInput!): MemberMutationResult! } type MemberMutationResult implements MutationResult { @@ -207,3 +216,20 @@ input MemberProfileInput { showLocation: Boolean showProperties: Boolean } + +input UpdateMemberProfileVisibilityInput { + showEmail: Boolean! + showBio: Boolean! + showInterests: Boolean! + showProfile: Boolean + showLocation: Boolean + showProperties: Boolean +} + +input UpdateMyMemberProfileInput { + name: String! + email: String! + bio: String + interests: [String!]! + visibility: UpdateMemberProfileVisibilityInput! +} diff --git a/packages/ocom/graphql/src/schema/types/member.resolvers.ts b/packages/ocom/graphql/src/schema/types/member.resolvers.ts index 2fcb81bd5..3abb10803 100644 --- a/packages/ocom/graphql/src/schema/types/member.resolvers.ts +++ b/packages/ocom/graphql/src/schema/types/member.resolvers.ts @@ -24,6 +24,7 @@ import type { MutationMemberRemoveAccountArgs, MutationMemberRoleUpdateArgs, MutationMemberUpdateAccountArgs, + MutationMemberUpdateMyProfileArgs, MutationMemberUpdateProfileArgs, MutationRemoveMemberArgs, Resolvers, @@ -155,6 +156,18 @@ const member: Resolvers = { const members = await context.applicationServices.Community.Member.queryByEndUserExternalId({ externalId }); return members.find((m) => String(m.communityId) === String(args.communityId)) ?? null; }, + memberMyProfile: async (_parent: unknown, args: { communityId: string }, context: GraphContext) => { + if (!context.applicationServices.verifiedUser?.verifiedJwt) { + throw new Error('Unauthorized'); + } + const actorMemberId = await getActorMemberIdForCommunity(context, String(args.communityId)); + if (!actorMemberId) { + return null; + } + return await context.applicationServices.Community.Member.queryById({ + id: actorMemberId, + }); + }, }, Mutation: { memberCreate: async (_parent, args: MutationMemberCreateArgs, context: GraphContext, _info: GraphQLResolveInfo) => { @@ -759,6 +772,55 @@ const member: Resolvers = { }; } }, + + // ...existing code... + memberUpdateMyProfile: async (_parent: unknown, args: MutationMemberUpdateMyProfileArgs, context: GraphContext) => { + try { + if (!context.applicationServices.verifiedUser?.verifiedJwt) { + return { + status: { success: false, errorMessage: 'Unauthorized' }, + member: null, + }; + } + + const actorMemberId = await getActorMemberIdForCommunity(context, String(args.communityId)); + if (!actorMemberId) { + return { + status: { success: false, errorMessage: 'Forbidden' }, + member: null, + }; + } + + const command: MemberUpdateProfileCommand = { + memberId: actorMemberId, + profile: { + name: args.input.name, + email: args.input.email, + ...(args.input.bio !== undefined ? { bio: args.input.bio } : {}), + ...(args.input.interests != null ? { interests: [...args.input.interests] } : {}), + ...(args.input.visibility?.showInterests !== undefined ? { showInterests: args.input.visibility.showInterests } : {}), + ...(args.input.visibility?.showEmail !== undefined ? { showEmail: args.input.visibility.showEmail } : {}), + // Domain model has no `showBio`; not mapped. + }, + }; + + const result = await context.applicationServices.Community.Member.updateMemberProfile(command); + + return { + status: { success: true }, + member: result, + }; + } catch (error: unknown) { + return { + status: { + success: false, + errorMessage: error instanceof Error ? error.message : 'Failed to update profile', + }, + member: null, + }; + } + }, + // ...existing code... }, }; diff --git a/packages/ocom/ui-community-route-accounts/src/accounts.test.tsx b/packages/ocom/ui-community-route-accounts/src/accounts.test.tsx new file mode 100644 index 000000000..6b4e787e0 --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/accounts.test.tsx @@ -0,0 +1,70 @@ +// @vitest-environment jsdom +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { MemoryRouter } from 'react-router-dom'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Accounts } from './accounts.tsx'; + +vi.mock('./pages/home.tsx', () => ({ + Home: () =>
home
, +})); + +vi.mock('./pages/create-community.tsx', () => ({ + CreateCommunity: () =>
create community
, +})); + +vi.mock('./section-layout.tsx', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + SectionLayout: () => ( +
+ +
+ ), + }; +}); + +describe('Accounts', () => { + let container!: HTMLDivElement; + let root!: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + if (root) { + act(() => { + root.unmount(); + }); + } + container?.remove(); + }); + + it('renders the home page at the root route', () => { + act(() => { + root.render( + + + , + ); + }); + + expect(container.querySelector('[data-testid="home-page"]')).not.toBeNull(); + }); + + it('renders the create community page', () => { + act(() => { + root.render( + + + , + ); + }); + + expect(container.querySelector('[data-testid="create-community-page"]')).not.toBeNull(); + }); +}); diff --git a/packages/ocom/ui-community-route-accounts/src/components/member-section-layout.container.tsx b/packages/ocom/ui-community-route-accounts/src/components/member-section-layout.container.tsx new file mode 100644 index 000000000..02b83823e --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/components/member-section-layout.container.tsx @@ -0,0 +1,31 @@ +import { useQuery } from '@apollo/client'; +import { ComponentQueryLoader } from '@cellix/ui-core'; +import type { PageLayoutProps } from '@ocom/ui-shared'; +import { useParams } from 'react-router-dom'; +import { type MemberSectionLayoutContainerMemberFieldsFragment, MemberSectionLayoutContainerMembersForCurrentEndUserDocument } from '../generated.tsx'; +import { MemberSectionLayout } from '../member-section-layout.tsx'; + +interface MemberSectionLayoutContainerProps { + pageLayouts: PageLayoutProps[]; +} + +export const MemberSectionLayoutContainer: React.FC = (props) => { + const params = useParams(); + + const { data: membersData, loading: membersLoading, error: membersError } = useQuery(MemberSectionLayoutContainerMembersForCurrentEndUserDocument); + + return ( + member.id === params['memberId']) as MemberSectionLayoutContainerMemberFieldsFragment} + /> + } + error={membersError} + /> + ); +}; diff --git a/packages/ocom/ui-community-route-accounts/src/index.tsx b/packages/ocom/ui-community-route-accounts/src/index.tsx index 1236c09d3..d4dfdb1d0 100644 --- a/packages/ocom/ui-community-route-accounts/src/index.tsx +++ b/packages/ocom/ui-community-route-accounts/src/index.tsx @@ -1 +1,2 @@ export { Accounts } from './accounts.tsx'; +export { Member } from './member.tsx'; diff --git a/packages/ocom/ui-community-route-accounts/src/member-home.graphql b/packages/ocom/ui-community-route-accounts/src/member-home.graphql new file mode 100644 index 000000000..d9357f34f --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/member-home.graphql @@ -0,0 +1,14 @@ +query MemberHomeContainerMemberMyProfile($communityId: ID!) { + memberMyProfile(communityId: $communityId) { + ...MemberHomeContainerMemberFields + } +} + +fragment MemberHomeContainerMemberFields on Member { + id + memberName + community { + id + name + } +} diff --git a/packages/ocom/ui-community-route-accounts/src/member-section-layout.css b/packages/ocom/ui-community-route-accounts/src/member-section-layout.css new file mode 100644 index 000000000..e1a89f088 --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/member-section-layout.css @@ -0,0 +1,19 @@ +#components-layout-demo-fixed-sider .logo { + height: 32px; + margin: 16px; + background: rgba(255, 255, 255, 0.2); +} + +.site-layout .site-layout-background { + background: #fff; +} + +.ant-dropdown .ant-menu-root.ant-menu-vertical { + box-shadow: 0 0 8px rgba(0, 0, 0, 0.1); +} + +.allowBoxShadow { + box-shadow: 0 0 8px rgba(0, 0, 0, 0.1); + border-radius: 4px; + padding: 8px 12px; +} diff --git a/packages/ocom/ui-community-route-accounts/src/member-section-layout.graphql b/packages/ocom/ui-community-route-accounts/src/member-section-layout.graphql new file mode 100644 index 000000000..445f768bd --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/member-section-layout.graphql @@ -0,0 +1,15 @@ +query MemberSectionLayoutContainerMembersForCurrentEndUser { + membersForCurrentEndUser { + ...MemberSectionLayoutContainerMemberFields + } +} + +fragment MemberSectionLayoutContainerMemberFields on Member { + id + memberName + isAdmin + community { + id + name + } +} diff --git a/packages/ocom/ui-community-route-accounts/src/member-section-layout.tsx b/packages/ocom/ui-community-route-accounts/src/member-section-layout.tsx new file mode 100644 index 000000000..077b49401 --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/member-section-layout.tsx @@ -0,0 +1,118 @@ +import { CommunitiesDropdownContainer, LoggedInUserContainer, MenuComponent, type MenuComponentProps, type PageLayoutProps } from '@ocom/ui-shared'; +import { Layout, theme } from 'antd'; +import { useState } from 'react'; +import { Link, Outlet, useParams } from 'react-router-dom'; +import type { MemberSectionLayoutContainerMemberFieldsFragment } from './generated.tsx'; +import './member-section-layout.css'; + +const { Sider, Header } = Layout; + +const LocalSettingsKeys = { + SidebarCollapsed: 'MemberSidebarCollapsed', +} as const; + +const handleToggler = (isExpanded: boolean, setIsExpanded: (value: boolean) => void) => { + const newValue = !isExpanded; + setIsExpanded(newValue); + if (newValue) { + localStorage.removeItem(LocalSettingsKeys.SidebarCollapsed); + } else { + localStorage.setItem(LocalSettingsKeys.SidebarCollapsed, 'true'); + } +}; + +interface MemberSectionLayoutProps { + pageLayouts: PageLayoutProps[]; + memberData: MemberSectionLayoutContainerMemberFieldsFragment; +} + +export const MemberSectionLayout: React.FC = (props) => { + const params = useParams(); + const sidebarCollapsed = localStorage.getItem(LocalSettingsKeys.SidebarCollapsed); + const [isExpanded, setIsExpanded] = useState(!sidebarCollapsed); + const { + token: { colorBgContainer }, + } = theme.useToken(); + + const menuComponentProps: MenuComponentProps = { + pageLayouts: props.pageLayouts, + memberData: { member: props.memberData }, + theme: 'light', + mode: 'inline', + }; + + return ( + +
+
+
+ +
+ + Back to Accounts + + + +
+
+ + + handleToggler(isExpanded, setIsExpanded)} + style={{ + overflow: 'auto', + height: 'calc(100vh - 64px)', + position: 'relative', + left: 0, + top: 0, + bottom: 0, + backgroundColor: colorBgContainer, + }} + > +
+ + + + + + + + + + ); +}; diff --git a/packages/ocom/ui-community-route-accounts/src/member.tsx b/packages/ocom/ui-community-route-accounts/src/member.tsx new file mode 100644 index 000000000..e1aa3829c --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/member.tsx @@ -0,0 +1,43 @@ +import { HomeOutlined, IdcardOutlined } from '@ant-design/icons'; +import type { PageLayoutProps } from '@ocom/ui-shared'; +import type React from 'react'; +import { Route, Routes } from 'react-router-dom'; +import { MemberSectionLayoutContainer } from './components/member-section-layout.container.tsx'; +import { MemberHome } from './pages/member-home.tsx'; +import { MemberProfilePage } from './pages/member-profile.tsx'; + +export const Member: React.FC = () => { + const pageLayouts: PageLayoutProps[] = [ + { + path: '/community/:communityId/member/:memberId', + title: 'Home', + icon: , + id: 'ROOT', + }, + { + path: '/community/:communityId/member/:memberId/profile', + title: 'Profile', + icon: , + id: 2, + parent: 'ROOT', + }, + ]; + + return ( + + } + > + } + /> + } + /> + + + ); +}; diff --git a/packages/ocom/ui-community-route-accounts/src/pages/create-community.stories.tsx b/packages/ocom/ui-community-route-accounts/src/pages/create-community.stories.tsx index 981e131de..6fccb034c 100644 --- a/packages/ocom/ui-community-route-accounts/src/pages/create-community.stories.tsx +++ b/packages/ocom/ui-community-route-accounts/src/pages/create-community.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; import { expect, within } from 'storybook/test'; -import { Accounts } from '../index.tsx'; +import { Accounts } from '../accounts.tsx'; const meta = { title: 'Pages/Accounts/Create Community', diff --git a/packages/ocom/ui-community-route-accounts/src/pages/home.stories.tsx b/packages/ocom/ui-community-route-accounts/src/pages/home.stories.tsx index 9d728b04e..b114059ce 100644 --- a/packages/ocom/ui-community-route-accounts/src/pages/home.stories.tsx +++ b/packages/ocom/ui-community-route-accounts/src/pages/home.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; import { expect, within } from 'storybook/test'; -import { Accounts } from '../index.tsx'; +import { Accounts } from '../accounts.tsx'; const meta = { title: 'Pages/Accounts/Home', diff --git a/packages/ocom/ui-community-route-accounts/src/pages/member-home.stories.tsx b/packages/ocom/ui-community-route-accounts/src/pages/member-home.stories.tsx new file mode 100644 index 000000000..c71b86f5f --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/pages/member-home.stories.tsx @@ -0,0 +1,53 @@ +import { MockedProvider } from '@apollo/client/testing'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { MemberHomeContainerMemberMyProfileDocument } from '../generated.tsx'; +import { MemberHome } from './member-home.tsx'; + +const meta = { + title: 'Pages/Accounts/MemberHome', + component: MemberHome, + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mocks = [ + { + request: { + query: MemberHomeContainerMemberMyProfileDocument, + variables: { communityId: 'community-1' }, + }, + result: { + data: { + memberMyProfile: { + __typename: 'Member', + id: 'member-1', + memberName: 'jane-doe', + community: { + __typename: 'Community', + id: 'community-1', + name: 'Sample Community', + }, + }, + }, + }, + }, +]; + +export const Default: Story = { + decorators: [ + (Story) => ( + + + + } /> + + + + ), + ], +}; diff --git a/packages/ocom/ui-community-route-accounts/src/pages/member-home.tsx b/packages/ocom/ui-community-route-accounts/src/pages/member-home.tsx new file mode 100644 index 000000000..0919fb40e --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/pages/member-home.tsx @@ -0,0 +1,95 @@ +import { useQuery } from '@apollo/client'; +import { ComponentQueryLoader } from '@cellix/ui-core'; +import { Descriptions, Typography, theme } from 'antd'; +import type React from 'react'; +import { useParams } from 'react-router-dom'; +import { type MemberHomeContainerMemberFieldsFragment, MemberHomeContainerMemberMyProfileDocument } from '../generated.tsx'; + +const { Text, Title } = Typography; + +interface MemberDetailProps { + data: MemberHomeContainerMemberFieldsFragment; +} + +const MemberDetail: React.FC = ({ data }) => { + const { + token: { colorText, colorBgContainer }, + } = theme.useToken(); + + return ( +
+
+ Community Member +

Welcome to your community portal. Use the navigation on the left to access your profile and other community features.

+
+ + + + + {data.id} + + + + + {data.memberName ?? '—'} + + + + + {data.community?.name ?? '—'} + + + +
+ ); +}; + +const MemberHomeContainer: React.FC = () => { + const params = useParams(); + // biome-ignore lint:useLiteralKeys + const communityId = params['communityId'] ?? ''; + + const { data, loading, error } = useQuery(MemberHomeContainerMemberMyProfileDocument, { + variables: { communityId }, + skip: !communityId, + }); + + return ( + } + error={error} + /> + ); +}; + +export const MemberHome: React.FC = () => { + const { + token: { colorTextBase }, + } = theme.useToken(); + + return ( +
+ + Home + + +
+ ); +}; diff --git a/packages/ocom/ui-community-route-accounts/src/pages/member-profile.stories.tsx b/packages/ocom/ui-community-route-accounts/src/pages/member-profile.stories.tsx new file mode 100644 index 000000000..07e68941b --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/pages/member-profile.stories.tsx @@ -0,0 +1,61 @@ +import { MockedProvider } from '@apollo/client/testing'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { SharedMemberProfileContainerMemberSelfProfileDocument } from '@ocom/ui-community-shared/src/generated.tsx'; +import { MemberProfilePage } from './member-profile.tsx'; + +const meta = { + title: 'Pages/Accounts/MemberProfile', + component: MemberProfilePage, + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mocks = [ + { + request: { + query: SharedMemberProfileContainerMemberSelfProfileDocument, + variables: { communityId: 'community-1' }, + }, + result: { + data: { + memberMyProfile: { + __typename: 'Member', + id: 'member-1', + memberName: 'jane-doe', + createdAt: '2024-01-15T00:00:00.000Z', + updatedAt: '2024-02-15T00:00:00.000Z', + profile: { + __typename: 'MemberProfile', + name: 'Jane Doe', + email: 'jane@example.com', + bio: 'Community member and product designer.', + showInterests: true, + showEmail: true, + showProfile: true, + showLocation: true, + showProperties: false, + }, + }, + }, + }, + }, +]; + +export const Default: Story = { + decorators: [ + (Story) => ( + + + + } /> + + + + ), + ], +}; diff --git a/packages/ocom/ui-community-route-accounts/src/pages/member-profile.tsx b/packages/ocom/ui-community-route-accounts/src/pages/member-profile.tsx new file mode 100644 index 000000000..95182ace6 --- /dev/null +++ b/packages/ocom/ui-community-route-accounts/src/pages/member-profile.tsx @@ -0,0 +1,6 @@ +import { MemberProfileContainer } from '@ocom/ui-community-shared'; +import type React from 'react'; + +export const MemberProfilePage: React.FC = () => { + return ; +}; diff --git a/packages/ocom/ui-community-shared/src/components/member-profile.container.graphql b/packages/ocom/ui-community-shared/src/components/member-profile.container.graphql index 525a71e72..1f9ad69a2 100644 --- a/packages/ocom/ui-community-shared/src/components/member-profile.container.graphql +++ b/packages/ocom/ui-community-shared/src/components/member-profile.container.graphql @@ -4,6 +4,12 @@ query SharedMemberProfileContainerMember($id: ObjectID!) { } } +query SharedMemberProfileContainerMemberSelfProfile($communityId: ID!) { + memberMyProfile(communityId: $communityId) { + ...SharedMemberProfileContainerMemberFields + } +} + mutation SharedMemberProfileContainerMemberUpdateProfile($input: MemberUpdateProfileInput!) { memberUpdateProfile(input: $input) { status { @@ -16,6 +22,18 @@ mutation SharedMemberProfileContainerMemberUpdateProfile($input: MemberUpdatePro } } +mutation SharedMemberProfileContainerMemberUpdateMyProfile($communityId: ID!, $input: UpdateMyMemberProfileInput!) { + memberUpdateMyProfile(communityId: $communityId, input: $input) { + status { + success + errorMessage + } + member { + ...SharedMemberProfileContainerMemberFields + } + } +} + fragment SharedMemberProfileContainerMemberFields on Member { id memberName diff --git a/packages/ocom/ui-community-shared/src/components/member-profile.container.stories.tsx b/packages/ocom/ui-community-shared/src/components/member-profile.container.stories.tsx new file mode 100644 index 000000000..c3f8b69b7 --- /dev/null +++ b/packages/ocom/ui-community-shared/src/components/member-profile.container.stories.tsx @@ -0,0 +1,64 @@ +import { MockedProvider } from '@apollo/client/testing'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { SharedMemberProfileContainerMemberSelfProfileDocument } from '../generated.tsx'; +import { MemberProfileContainer } from './member-profile.container.tsx'; + +const meta = { + title: 'Shared/Components/MemberProfileContainer', + component: MemberProfileContainer, + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mocks = [ + { + request: { + query: SharedMemberProfileContainerMemberSelfProfileDocument, + variables: { communityId: 'community-1' }, + }, + result: { + data: { + memberMyProfile: { + __typename: 'Member', + id: 'member-1', + memberName: 'jane-doe', + createdAt: '2024-01-15T00:00:00.000Z', + updatedAt: '2024-02-15T00:00:00.000Z', + profile: { + __typename: 'MemberProfile', + name: 'Jane Doe', + email: 'jane@example.com', + bio: 'Community member and product designer.', + showInterests: true, + showEmail: true, + showProfile: true, + showLocation: true, + showProperties: false, + }, + }, + }, + }, + }, +]; + +export const SelfProfile: Story = { + args: { + mode: 'self', + }, + decorators: [ + (Story) => ( + + + + } /> + + + + ), + ], +}; diff --git a/packages/ocom/ui-community-shared/src/components/member-profile.container.tsx b/packages/ocom/ui-community-shared/src/components/member-profile.container.tsx index 4464b0a78..189497172 100644 --- a/packages/ocom/ui-community-shared/src/components/member-profile.container.tsx +++ b/packages/ocom/ui-community-shared/src/components/member-profile.container.tsx @@ -6,20 +6,92 @@ import { useParams } from 'react-router-dom'; import { SharedMemberProfileContainerMemberDocument, type SharedMemberProfileContainerMemberFieldsFragment, - type SharedMemberProfileContainerMemberQuery, - type SharedMemberProfileContainerMemberQueryVariables, + SharedMemberProfileContainerMemberSelfProfileDocument, + SharedMemberProfileContainerMemberUpdateMyProfileDocument, + type SharedMemberProfileContainerMemberUpdateMyProfileMutationVariables, SharedMemberProfileContainerMemberUpdateProfileDocument, + type SharedMemberProfileContainerMemberUpdateProfileMutationVariables, } from '../generated.tsx'; import { MemberProfile, type MemberProfileFormValues } from './member-profile.tsx'; export interface MemberProfileContainerProps { isAdmin?: boolean; + mode?: 'admin' | 'self'; +} + +type SelfSaveVariables = { + variables: SharedMemberProfileContainerMemberUpdateMyProfileMutationVariables; +}; + +type AdminSaveVariables = { + variables: SharedMemberProfileContainerMemberUpdateProfileMutationVariables; +}; + +interface SelfBuildMemberProfileSaveVariablesArgs { + mode: 'self'; + communityId: string; + values: MemberProfileFormValues; +} + +interface AdminBuildMemberProfileSaveVariablesArgs { + mode: 'admin'; + memberObjectId: string; + values: MemberProfileFormValues; +} + +type BuildMemberProfileSaveVariablesArgs = SelfBuildMemberProfileSaveVariablesArgs | AdminBuildMemberProfileSaveVariablesArgs; + +export function buildMemberProfileSaveVariables(args: SelfBuildMemberProfileSaveVariablesArgs): SelfSaveVariables; +export function buildMemberProfileSaveVariables(args: AdminBuildMemberProfileSaveVariablesArgs): AdminSaveVariables; +export function buildMemberProfileSaveVariables({ mode, ...rest }: BuildMemberProfileSaveVariablesArgs): SelfSaveVariables | AdminSaveVariables { + if (mode === 'self') { + const { communityId, values } = rest as SelfBuildMemberProfileSaveVariablesArgs; + return { + variables: { + communityId, + input: { + name: values.name, + email: values.email, + bio: values.bio, + interests: [], + visibility: { + showEmail: values.showEmail, + showBio: false, + showInterests: values.showInterests, + showProfile: values.showProfile, + showLocation: values.showLocation, + showProperties: values.showProperties, + }, + }, + }, + }; + } + + const { memberObjectId, values } = rest as AdminBuildMemberProfileSaveVariablesArgs; + return { + variables: { + input: { + memberId: memberObjectId, + profile: { + name: values.name, + email: values.email, + bio: values.bio, + showInterests: values.showInterests, + showEmail: values.showEmail, + showProfile: values.showProfile, + showLocation: values.showLocation, + showProperties: values.showProperties, + }, + }, + }, + }; } export const MemberProfileContainer: React.FC = (props) => { const { message } = App.useApp(); - const { id, memberId } = useParams<{ id?: string; memberId?: string }>(); + const { id, memberId, communityId } = useParams<{ id?: string; memberId?: string; communityId?: string }>(); const memberObjectId = id ?? memberId; + const isSelfMode = props.mode === 'self' || (!memberObjectId && Boolean(communityId)); const [memberUpdateProfile, { loading: profileUpdateLoading, error: profileUpdateError }] = useMutation(SharedMemberProfileContainerMemberUpdateProfileDocument, { update(cache, { data }) { @@ -28,7 +100,7 @@ export const MemberProfileContainer: React.FC = (pr return; } - cache.writeQuery({ + cache.writeQuery({ query: SharedMemberProfileContainerMemberDocument, variables: { id: memberObjectId }, data: { @@ -38,32 +110,83 @@ export const MemberProfileContainer: React.FC = (pr }, }); + const [memberUpdateMyProfile, { loading: selfProfileUpdateLoading, error: selfProfileUpdateError }] = useMutation(SharedMemberProfileContainerMemberUpdateMyProfileDocument, { + update(cache, { data }) { + const updatedMember = data?.memberUpdateMyProfile.member; + if (!updatedMember || !communityId) { + return; + } + + cache.writeQuery({ + query: SharedMemberProfileContainerMemberSelfProfileDocument, + variables: { communityId }, + data: { + memberMyProfile: updatedMember, + }, + }); + }, + }); + const { data: memberData, loading: memberLoading, error: memberError, - } = useQuery(SharedMemberProfileContainerMemberDocument, { + } = useQuery(SharedMemberProfileContainerMemberDocument, { variables: { id: memberObjectId ?? '', }, - skip: !memberObjectId, + skip: isSelfMode || !memberObjectId, + }); + + const { + data: memberSelfData, + loading: memberSelfLoading, + error: memberSelfError, + } = useQuery(SharedMemberProfileContainerMemberSelfProfileDocument, { + variables: { + communityId: communityId ?? '', + }, + skip: !isSelfMode || !communityId, }); const handleSave = async (values: MemberProfileFormValues): Promise => { + if (isSelfMode) { + if (!communityId) { + message.error('Community not found'); + return false; + } + + try { + const variables = buildMemberProfileSaveVariables({ + mode: 'self', + communityId, + values, + }); + const result = await memberUpdateMyProfile({ variables: variables.variables }); + if (result.data?.memberUpdateMyProfile.status.success) { + message.success('Profile updated'); + return true; + } + message.error(result.data?.memberUpdateMyProfile.status.errorMessage ?? 'Failed to update profile'); + return false; + } catch (saveError) { + message.error(`Error updating profile: ${saveError instanceof Error ? saveError.message : JSON.stringify(saveError)}`); + return false; + } + } + if (!memberObjectId) { message.error('Member not found'); return false; } try { - const result = await memberUpdateProfile({ - variables: { - input: { - memberId: memberObjectId, - profile: values, - }, - }, + const variables = buildMemberProfileSaveVariables({ + mode: 'admin', + memberObjectId, + values, }); + const result = await memberUpdateProfile({ variables: variables.variables }); if (result.data?.memberUpdateProfile.status.success) { message.success('Profile updated'); @@ -79,18 +202,18 @@ export const MemberProfileContainer: React.FC = (pr }; const memberProfileProps = { - data: memberData?.member as SharedMemberProfileContainerMemberFieldsFragment, + data: (isSelfMode ? memberSelfData?.memberMyProfile : memberData?.member) as SharedMemberProfileContainerMemberFieldsFragment, isAdmin: props.isAdmin ?? false, - loading: profileUpdateLoading, + loading: isSelfMode ? selfProfileUpdateLoading : profileUpdateLoading, onSave: handleSave, }; return ( } - error={memberError ?? profileUpdateError} + error={isSelfMode ? (memberSelfError ?? selfProfileUpdateError) : (memberError ?? profileUpdateError)} /> ); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 641b87b53..07cc2ac6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,7 +136,8 @@ overrides: jiti: 2.6.1 rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 - brace-expansion: 5.0.8 + '@apollo/protobufjs': 1.2.8 + brace-expansion: 5.0.9 diff@4.0.2: 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 @@ -166,12 +167,12 @@ overrides: playwright: 1.59.0 postcss: ^8.5.18 protobufjs: 7.6.5 - ip-address: ^10.2.1 - fast-uri: ^4.1.1 + ip-address: ^10.1.1 + fast-uri: ^4.1.2 '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 - js-yaml: 4.3.0 - js-yaml@3.14.2: 3.15.0 + js-yaml: 4.3.1 + js-yaml@3.14.2: 3.15.1 shell-quote@<1.8.4: 1.8.4 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 '@opentelemetry/core@2.7.1': 2.8.0 @@ -181,11 +182,15 @@ overrides: '@grpc/grpc-js': '>=1.14.4' form-data: ^4.0.6 webpack-dev-server>http-proxy-middleware: 3.0.7 + image-size: 2.0.2 + nanoid: 3.3.17 joi: ^17.13.4 brace-expansion@2: 2.0.3 + adm-zip@0.5.16: 0.5.17 + immutable@3.8.3: 4.3.9 dottie: 2.0.7 http-proxy-middleware: 3.0.7 - immutable: 3.8.3 + immutable: 4.3.9 launch-editor: ^2.14.1 sequelize: 6.37.8 smol-toml: 1.6.1 @@ -1447,8 +1452,8 @@ importers: specifier: ^19.1.6 version: 19.2.3(@types/react@19.2.7) c8: - specifier: ^10.1.3 - version: 10.1.3 + specifier: ^11.0.0 + version: 11.0.0 tsx: specifier: 'catalog:' version: 4.21.0 @@ -3089,8 +3094,8 @@ packages: subscriptions-transport-ws: optional: true - '@apollo/protobufjs@1.2.7': - resolution: {integrity: sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==} + '@apollo/protobufjs@1.2.8': + resolution: {integrity: sha512-r7xNeUqZX+eBBEmyvaPw0/cSz6zgf5jdH8mjUz8ynKpNs/GU7vi2T7sNcZINk2ZID7wwjG91FCgdpCrQuJ8rzA==} hasBin: true '@apollo/server-gateway-interface@2.0.0': @@ -6146,10 +6151,6 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@playwright/test@1.59.0': resolution: {integrity: sha512-TOA5sTLd49rTDaZpYpvCQ9hGefHQq/OYOyCVnGqS2mjMfX+lGZv2iddIJd0I48cfxqSPttS9S3OuLKyylHcO1w==} engines: {node: '>=18'} @@ -6179,15 +6180,9 @@ packages: '@protobufjs/codegen@2.0.5': resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - '@protobufjs/eventemitter@1.1.1': resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - '@protobufjs/fetch@1.1.1': resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} @@ -7559,8 +7554,8 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} - adm-zip@0.5.16: - resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + adm-zip@0.5.17: + resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} engines: {node: '>=12.0'} agent-base@6.0.2: @@ -7927,8 +7922,8 @@ packages: brace-expansion@2.0.3: resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} - brace-expansion@5.0.8: - resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} braces@3.0.3: @@ -8014,16 +8009,6 @@ packages: resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} engines: {node: '>=6.0.0'} - c8@10.1.3: - resolution: {integrity: sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - monocart-coverage-reports: ^2 - peerDependenciesMeta: - monocart-coverage-reports: - optional: true - c8@11.0.0: resolution: {integrity: sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==} engines: {node: 20 || >=22} @@ -9150,8 +9135,8 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-uri@4.1.1: - resolution: {integrity: sha512-YPOs1zD5TG2+EZt+r88LwF6mclA7TPkpwMP7ZN3TO2HiHS8TXvq7QA/17iJsV9dubcLo/f8eEYqMBruyQV21hQ==} + fast-uri@4.1.2: + resolution: {integrity: sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==} fast-xml-builder@1.2.0: resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} @@ -9411,11 +9396,6 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} @@ -9794,11 +9774,6 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - image-size@0.5.5: - resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} - engines: {node: '>=0.10.0'} - hasBin: true - image-size@2.0.2: resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} engines: {node: '>=16.x'} @@ -9807,9 +9782,8 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - immutable@3.8.3: - resolution: {integrity: sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==} - engines: {node: '>=0.10.0'} + immutable@4.3.9: + resolution: {integrity: sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -10196,9 +10170,6 @@ packages: iterall@1.3.0: resolution: {integrity: sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.1.1: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} @@ -10231,12 +10202,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.15.0: - resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==} + js-yaml@3.15.1: + resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true jsbi@4.3.2: @@ -11122,8 +11093,8 @@ packages: resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} engines: {node: '>=12.0.0'} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -11508,10 +11479,6 @@ packages: resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} engines: {node: '>=0.10.0'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -13239,10 +13206,6 @@ packages: engines: {node: '>=10'} hasBin: true - test-exclude@7.0.2: - resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} - engines: {node: '>=18'} - test-exclude@8.0.0: resolution: {integrity: sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==} engines: {node: 20 || >=22} @@ -14467,13 +14430,13 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@apollo/protobufjs@1.2.7': + '@apollo/protobufjs@1.2.8': dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 '@protobufjs/float': 1.0.2 '@protobufjs/inquire': 1.1.1 '@protobufjs/path': 1.1.2 @@ -14519,7 +14482,7 @@ snapshots: '@apollo/usage-reporting-protobuf@4.1.1': dependencies: - '@apollo/protobufjs': 1.2.7 + '@apollo/protobufjs': 1.2.8 '@apollo/utils.createhash@3.0.1': dependencies: @@ -14578,7 +14541,7 @@ snapshots: chalk: 4.1.2 fb-watchman: 2.0.2 graphql: 16.12.0 - immutable: 3.8.3 + immutable: 4.3.9 invariant: 2.2.4 nullthrows: 1.1.1 relay-runtime: 12.0.0 @@ -16572,7 +16535,7 @@ snapshots: '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.2 - js-yaml: 4.3.0 + js-yaml: 4.3.1 lodash: 4.18.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -17018,7 +16981,7 @@ snapshots: '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) fs-extra: 11.3.2 joi: 17.13.4 - js-yaml: 4.3.0 + js-yaml: 4.3.1 lodash: 4.18.1 tslib: 2.8.1 transitivePeerDependencies: @@ -17043,7 +17006,7 @@ snapshots: globby: 11.1.0 gray-matter: 4.0.3 jiti: 2.6.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 lodash: 4.18.1 micromatch: 4.0.8 p-queue: 6.6.2 @@ -17619,7 +17582,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 jose: 5.10.0 - js-yaml: 4.3.0 + js-yaml: 4.3.1 lodash: 4.18.1 scuid: 1.1.0 tslib: 2.8.1 @@ -18698,9 +18661,6 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 - '@pkgjs/parseargs@0.11.0': - optional: true - '@playwright/test@1.59.0': dependencies: playwright: 1.59.0 @@ -18725,15 +18685,8 @@ snapshots: '@protobufjs/codegen@2.0.5': {} - '@protobufjs/eventemitter@1.1.0': {} - '@protobufjs/eventemitter@1.1.1': {} - '@protobufjs/fetch@1.1.0': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.1 - '@protobufjs/fetch@1.1.1': dependencies: '@protobufjs/aspromise': 1.1.2 @@ -19349,7 +19302,7 @@ snapshots: '@sonar/scan@4.3.2(supports-color@8.1.1)': dependencies: - adm-zip: 0.5.16 + adm-zip: 0.5.17 axios: 1.18.1(supports-color@8.1.1) commander: 13.1.0 fs-extra: 11.3.2 @@ -20304,7 +20257,7 @@ snapshots: address@1.2.2: {} - adm-zip@0.5.16: {} + adm-zip@0.5.17: {} agent-base@6.0.2(supports-color@8.1.1): dependencies: @@ -20350,7 +20303,7 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 4.1.1 + fast-uri: 4.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -20795,7 +20748,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.8: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -20901,20 +20854,6 @@ snapshots: bytestreamjs@2.0.1: {} - c8@10.1.3: - dependencies: - '@bcoe/v8-coverage': 1.0.2 - '@istanbuljs/schema': 0.1.3 - find-up: 5.0.0 - foreground-child: 3.3.1 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - test-exclude: 7.0.2 - v8-to-istanbul: 9.3.0 - yargs: 17.7.2 - yargs-parser: 21.1.1 - c8@11.0.0: dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -21343,7 +21282,7 @@ snapshots: cosmiconfig@8.3.6(typescript@6.0.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -22177,7 +22116,7 @@ snapshots: fast-json-stable-stringify@2.1.0: {} - fast-uri@4.1.1: {} + fast-uri@4.1.2: {} fast-xml-builder@1.2.0: dependencies: @@ -22462,15 +22401,6 @@ snapshots: glob-to-regexp@0.4.1: {} - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.9 - minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - glob@11.1.0: dependencies: foreground-child: 3.3.1 @@ -22632,7 +22562,7 @@ snapshots: gray-matter@4.0.3: dependencies: - js-yaml: 3.15.0 + js-yaml: 3.15.1 kind-of: 6.0.3 section-matter: 1.0.0 strip-bom-string: 1.0.0 @@ -23000,14 +22930,11 @@ snapshots: ignore@7.0.5: {} - image-size@0.5.5: - optional: true - image-size@2.0.2: {} immediate@3.0.6: {} - immutable@3.8.3: {} + immutable@4.3.9: {} import-fresh@3.3.1: dependencies: @@ -23348,12 +23275,6 @@ snapshots: iterall@1.3.0: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jackspeak@4.1.1: dependencies: '@isaacs/cliui': 8.0.2 @@ -23396,12 +23317,12 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.15.0: + js-yaml@3.15.1: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.3.0: + js-yaml@4.3.1: dependencies: argparse: 2.0.1 @@ -23529,7 +23450,7 @@ snapshots: fast-glob: 3.3.3 formatly: 0.3.0 jiti: 2.6.1 - js-yaml: 4.3.0 + js-yaml: 4.3.1 minimist: 1.2.8 oxc-resolver: 11.14.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) picocolors: 1.1.1 @@ -23565,7 +23486,7 @@ snapshots: optionalDependencies: errno: 0.1.8 graceful-fs: 4.2.11 - image-size: 0.5.5 + image-size: 2.0.2 make-dir: 2.1.0 mime: 1.6.0 needle: 3.3.1 @@ -24376,15 +24297,15 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minimatch@10.2.5: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minimatch@3.1.5: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minimatch@9.0.9: dependencies: @@ -24590,7 +24511,7 @@ snapshots: dependencies: lru-cache: 7.18.3 - nanoid@3.3.16: {} + nanoid@3.3.17: {} native-duplexpair@1.0.0: {} @@ -25084,11 +25005,6 @@ snapshots: dependencies: path-root-regex: 0.1.2 - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.3 - path-scurry@2.0.2: dependencies: lru-cache: 11.3.5 @@ -25644,7 +25560,7 @@ snapshots: postcss@8.5.25: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.17 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -27063,17 +26979,11 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - test-exclude@7.0.2: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 10.5.0 - minimatch: 10.2.4 - test-exclude@8.0.0: dependencies: '@istanbuljs/schema': 0.1.3 glob: 13.0.6 - minimatch: 10.2.4 + minimatch: 10.2.5 text-decoder@1.2.3: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3357912aa..53f48ef58 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -65,6 +65,8 @@ auditConfig: - GHSA-q7rr-3cgh-j5r3 - GHSA-869p-cjfg-cm3x # jws@4.0.0: Improperly Verifies HMAC Signature (transitive from azurite) - GHSA-qwww-vcr4-c8h2 # react-router 7.x: defer upgrade until the react-router-dom v8 migration is completed + - GHSA-w3rx-r6r6-pgpr # image-size ICNS parser DoS advisory; no patched release available in the Docusaurus dependency tree + - GHSA-5p2g-fcmc-qvqq # image-size JXL/HEIF parser DoS advisory; no patched release available in the Docusaurus dependency tree allowBuilds: '@apollo/protobufjs': true @@ -88,7 +90,8 @@ overrides: jiti: 2.6.1 rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 - brace-expansion: 5.0.8 + '@apollo/protobufjs': 1.2.8 + brace-expansion: 5.0.9 'diff@4.0.2': 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 @@ -118,12 +121,12 @@ overrides: playwright: 1.59.0 postcss: ^8.5.18 protobufjs: 7.6.5 - ip-address: ^10.2.1 - fast-uri: ^4.1.1 + ip-address: ^10.1.1 + fast-uri: ^4.1.2 '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 - js-yaml: 4.3.0 - 'js-yaml@3.14.2': 3.15.0 + js-yaml: 4.3.1 + 'js-yaml@3.14.2': 3.15.1 'shell-quote@<1.8.4': 1.8.4 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 '@opentelemetry/core@2.7.1': 2.8.0 @@ -133,11 +136,15 @@ overrides: '@grpc/grpc-js': '>=1.14.4' form-data: ^4.0.6 'webpack-dev-server>http-proxy-middleware': 3.0.7 + image-size: 2.0.2 + nanoid: 3.3.17 joi: ^17.13.4 - brace-expansion@2: 2.0.3 + 'brace-expansion@2': 2.0.3 + 'adm-zip@0.5.16': 0.5.17 + 'immutable@3.8.3': 4.3.9 dottie: 2.0.7 http-proxy-middleware: 3.0.7 - immutable: 3.8.3 + immutable: 4.3.9 launch-editor: ^2.14.1 sequelize: 6.37.8 smol-toml: 1.6.1