Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2c4f2f3
feat: add Projects and Stats sections to profile page
Sep 4, 2026
b38a726
feat: add View My Profile menu item to account popup
Sep 4, 2026
7088ac5
fix: use single value (not array) for members query predicate
Sep 4, 2026
4bb3bc8
fix: remove broken createdBy/assignee queries, use workspace totals
Sep 4, 2026
74b3e95
Revert "fix: remove broken createdBy/assignee queries, use workspace …
Sep 4, 2026
6be0232
fix: resolve Person and Employee identifiers before querying
Sep 4, 2026
3e79d91
debug: use tracker.class.Project with no filter to diagnose zero results
Sep 4, 2026
8a80f4d
Revert "debug: use tracker.class.Project with no filter to diagnose z…
Sep 4, 2026
d5e5ba2
fix: navigate to profile within workspace context so workspace client…
Sep 4, 2026
9282655
Revert "fix: navigate to profile within workspace context so workspac…
Sep 4, 2026
88291ae
feat: add projects and stats to workspace settings profile page
Sep 4, 2026
5455e3b
Revert "feat: add projects and stats to workspace settings profile page"
Sep 7, 2026
6304c03
feat: add server-side cross-workspace profile aggregation (getPersonW…
Sep 7, 2026
8e19aca
chore: remove unused UserProjects.svelte and UserStats.svelte (replac…
Sep 7, 2026
b1995f4
fix: resolve compile errors in server-side cross-workspace aggregation
Sep 7, 2026
b56c26b
fix: resolve Person document before querying assignee, close storage …
Sep 7, 2026
9e81d52
fix: remove stale imports of deleted UserStats and UserProjects from …
Sep 7, 2026
672674f
fix: add missing ProfileWorkspaceData import to client.ts and client.…
Sep 7, 2026
30529be
fix: use core.account.ConfigUser instead of systemAccountUuid for TxO…
Sep 7, 2026
34f6dd0
fix: add empty state fallback for getPersonWorkspaceData RPC
Sep 7, 2026
ff8b829
fix: use account DB only for getPersonWorkspaceData (no pipeline)
Sep 7, 2026
2b472f0
refactor: replace server-side RPC with client-side cross-workspace ag…
Sep 7, 2026
2467a76
fix: add padding to workspace sections
Sep 7, 2026
4d79510
Revert "fix: add padding to workspace sections"
Sep 7, 2026
d59fd6f
Reapply "fix: add padding to workspace sections"
Sep 7, 2026
715d7f0
fix: add margin-top to workspace section for vertical spacing from pr…
Sep 7, 2026
5e5e591
fix: add padding-top 1.5rem to workspace-section
Sep 7, 2026
6903dbc
feat: make project rows clickable, navigate to per-project tracker view
Sep 7, 2026
83f908f
fix: make stat cards equal width/height using flex:1 and min-height
Sep 7, 2026
fbc8b38
feat: split stats into Issues Assigned, Ongoing, Completed with statu…
Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
677 changes: 368 additions & 309 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion foundations/core/packages/account-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@ export interface AccountClient {

setMyProfile: (profile: Partial<Omit<UserProfile, 'personUuid'>>) => Promise<void>
getUserProfile: (personUuid?: PersonUuid) => Promise<PersonWithProfile | null>

getSubscriptions: (workspaceUuid?: WorkspaceUuid | undefined, activeOnly?: boolean) => Promise<Subscription[]>
getSubscriptionByProviderId: (provider: string, providerSubscriptionId: string) => Promise<Subscription | null>
getSubscriptionById: (subscriptionId: string) => Promise<Subscription | null>
Expand Down
22 changes: 22 additions & 0 deletions foundations/core/packages/account-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,28 @@ export interface UserProfile {

export type PersonWithProfile = Person & Omit<UserProfile, 'personUuid'>

/**
* @public
* Workspace-level data for a person's profile, used by client-side cross-workspace aggregation
*/
export interface ProfileWorkspaceProject {
id: string
name: string
description?: string
}

/**
* @public
*/
export interface ProfileWorkspaceData {
workspaceName: string
workspaceUrl: string
projects: ProfileWorkspaceProject[]
issuesAssigned: number
issuesOngoing: number
issuesCompleted: number
}

/**
* Subscription status enum
* Reflects the subscription lifecycle from active to canceled/expired
Expand Down
6 changes: 5 additions & 1 deletion plugins/global-profile-assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"PrivateProfileDescription": "Your profile is only visible to you",
"AddBioPlaceholder": "Add a bio to tell others about yourself...",
"AddLocationPlaceholder": "Add your location...",
"ProfileNotAvailable": "Profile not available"
"ProfileNotAvailable": "Profile not available",
"Projects": "Projects",
"NoProjects": "No projects found",
"Stats": "Stats",
"NoStats": "No stats available"
}
}
8 changes: 6 additions & 2 deletions plugins/global-profile-resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,12 @@
"@hcengineering/presentation": "workspace:^0.7.0",
"@hcengineering/global-profile": "workspace:^0.7.0",
"@hcengineering/account-client": "workspace:^0.7.25",
"@hcengineering/client": "workspace:^0.7.0",
"@hcengineering/login": "workspace:^0.7.0",
"@hcengineering/contact": "workspace:^0.7.0",
"@hcengineering/theme": "workspace:^0.7.0"
"@hcengineering/theme": "workspace:^0.7.0",
"@hcengineering/task": "workspace:^0.7.0",
"@hcengineering/tracker": "workspace:^0.7.0",
"@hcengineering/workbench": "workspace:^0.7.0"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@
Label,
TooltipInstance,
getLocation as getPlatformLocation,
Loading
Loading,
navigate
} from '@hcengineering/ui'
import { PersonWithProfile } from '@hcengineering/account-client'
import { type AccountUuid, type PersonUuid } from '@hcengineering/core'
import { type ProfileWorkspaceData, PersonWithProfile } from '@hcengineering/account-client'
import core, { type AccountUuid, type PersonUuid } from '@hcengineering/core'
import globalProfile from '@hcengineering/global-profile'
import view from '@hcengineering/view'
import { getMetadata } from '@hcengineering/platform'
import { getMetadata, getResource } from '@hcengineering/platform'
import client from '@hcengineering/client'
import task from '@hcengineering/task'
import tracker, { trackerId } from '@hcengineering/tracker'
import { workbenchId } from '@hcengineering/workbench'
import contact from '@hcengineering/contact'

import { getAvatarText, getDisplayName, getLocation, getAccountClient, getAvatarColorForId } from '../utils'
import EditProfilePopup from './EditGlobalProfilePopup.svelte'
Expand All @@ -42,6 +48,7 @@
const userId = loc.path[1] as PersonUuid
const accountClient = getAccountClient()
let myAccount: AccountUuid | null = null
let workspaceData: ProfileWorkspaceData[] = []
let loading: boolean = false
$: isMyProfile = myAccount != null && userId === myAccount

Expand Down Expand Up @@ -69,6 +76,57 @@
}

profile = await accountClient.getUserProfile(userId)

// Client-side cross-workspace aggregation
try {
const workspaces = await accountClient.getUserWorkspaces()
const clientFactory = await getResource(client.function.GetClient)
const accountUuid = userId as unknown as AccountUuid

for (const ws of workspaces) {
try {
const loginInfo = await accountClient.selectWorkspace(ws.url)
const wsClient = await clientFactory(loginInfo.token, loginInfo.endpoint)

const [projects, person, allStatuses] = await Promise.all([
wsClient.findAll(task.class.Project, { members: accountUuid }),
wsClient.findOne(contact.class.Person, { personUuid: userId }),
wsClient.findAll(core.class.Status, {})
])

const ongoingStatusIds = allStatuses
.filter((s: any) => s.category === task.statusCategory.Active || s.category === task.statusCategory.ToDo)
.map((s: any) => s._id)
const doneStatusIds = allStatuses
.filter((s: any) => s.category === task.statusCategory.Won)
.map((s: any) => s._id)

const allIssues = person != null
? await wsClient.findAll(tracker.class.Issue, { assignee: person._id })
: []

await wsClient.close()

workspaceData = [...workspaceData, {
workspaceName: ws.name,
workspaceUrl: ws.url,
projects: projects.map((p: any) => ({
id: p._id,
name: (p as any).name ?? '',
description: (p as any).description ?? ''
})),
issuesAssigned: allIssues.length,
issuesOngoing: allIssues.filter((i: any) => ongoingStatusIds.includes(i.status)).length,
issuesCompleted: allIssues.filter((i: any) => doneStatusIds.includes(i.status)).length
}]
} catch (e) {
console.error('Failed to load workspace data for', ws.url, e)
// Skip failed workspaces
}
}
} catch (e) {
console.error('Failed to load workspace list', e)
}
} catch (e) {
console.error(e)
} finally {
Expand Down Expand Up @@ -173,6 +231,43 @@
</div>
{/if}
</div>
{#if workspaceData.length > 0}
{#each workspaceData as ws}
<div class="workspace-section">
<div class="workspace-header">{ws.workspaceName}</div>
<div class="section-title">Projects</div>
{#if ws.projects.length === 0}
<div class="empty-text">No projects found</div>
{:else}
<div class="projects-list">
{#each ws.projects as p}
<div class="project-item" on:click={() => navigate({ path: [workbenchId, ws.workspaceUrl, trackerId, p.id] })}>{p.name}</div>
{/each}
</div>
{/if}
<div class="section-title">Stats</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-count">{ws.issuesAssigned}</div>
<div class="stat-label">Issues Assigned</div>
</div>
<div class="stat-card">
<div class="stat-count">{ws.issuesOngoing}</div>
<div class="stat-label">Ongoing</div>
</div>
<div class="stat-card">
<div class="stat-count">{ws.issuesCompleted}</div>
<div class="stat-label">Completed</div>
</div>
</div>
</div>
{/each}
{:else}
<div class="empty-section">
<div class="empty-text">No workspace data available</div>
</div>
{/if}

<div class="avatarCtr">
<div class="avatar" style:background-color={avatarColor.icon}>
<div class="avatarText" style:color={avatarColor.iconText} data-name={avatarName.toLocaleUpperCase()} />
Expand Down Expand Up @@ -343,6 +438,86 @@
}
}

.workspace-section {
padding: 1.5rem;
margin-top: 2rem;
border-top: 1px solid var(--theme-divider-color);
}

.workspace-section:last-of-type {
padding-bottom: 4rem;
}

.workspace-header {
font-size: 1.25rem;
font-weight: 600;
color: var(--theme-caption-color);
margin-bottom: 1rem;
}

.section-title {
font-size: 1rem;
font-weight: 500;
color: var(--theme-content-color);
margin: 0.75rem 0 0.5rem 0;
}

.empty-text {
color: var(--theme-text-placeholder-color);
font-style: italic;
font-size: 0.875rem;
}

.projects-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.project-item {
background: var(--theme-button-default);
border: 1px solid var(--theme-divider-color);
border-radius: 0.5rem;
padding: 0.75rem 1rem;
color: var(--theme-caption-color);
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
}

.project-item:hover {
background: var(--theme-button-hovered);
}

.stats-grid {
display: flex;
gap: 1rem;
}

.stat-card {
flex: 1;
background: var(--theme-button-default);
border: 1px solid var(--theme-divider-color);
border-radius: 0.5rem;
padding: 1rem 1.5rem;
text-align: center;
min-height: 5rem;
display: flex;
flex-direction: column;
justify-content: center;
}

.stat-count {
font-size: 1.75rem;
font-weight: 600;
color: var(--theme-caption-color);
}

.stat-label {
font-size: 0.875rem;
color: var(--theme-content-color);
}

.location {
margin-top: 1rem;
color: var(--theme-text-placeholder-color);
Expand Down
6 changes: 5 additions & 1 deletion plugins/global-profile/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export default plugin(globalProfileId, {
PrivateProfileDescription: '' as IntlString,
AddBioPlaceholder: '' as IntlString,
AddLocationPlaceholder: '' as IntlString,
ProfileNotAvailable: '' as IntlString
ProfileNotAvailable: '' as IntlString,
Projects: "" as IntlString,
NoProjects: "" as IntlString,
Stats: "" as IntlString,
NoStats: "" as IntlString
}
})
5 changes: 3 additions & 2 deletions plugins/workbench-assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"ConfigureWidgets": "Configure widgets",
"Tab": "Tab",
"WorkspaceIsArchived": "Workspace is archived because of being unused, Please contact us to restore...",
"WorkspaceIsMigrating": "Workspace is being updated. Please wait..."
"WorkspaceIsMigrating": "Workspace is being updated. Please wait...",
"ViewMyProfile": "View My Profile"
}
}
}
15 changes: 15 additions & 0 deletions plugins/workbench-resources/src/components/AccountPopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,21 @@
}
})
}
if (hasAccountRole(account, AccountRole.User) && person?.personUuid) {
const myUuid = person.personUuid
actions.push({
icon: contact.icon.User,
label: workbench.string.ViewMyProfile,
action: async () => {
closePopup()
const loc = getCurrentResolvedLocation()
loc.path[0] = 'user'
loc.path[1] = myUuid
loc.path.length = 2
navigate(loc)
}
})
}
actions.push(...getMenu(items, ['main']))
if (hasAccountRole(account, AccountRole.User) && !isDisabled('invites') && canGenerateInviteLink) {
actions.push({
Expand Down
3 changes: 2 additions & 1 deletion plugins/workbench/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export const workbenchPlugin = plugin(workbenchId, {
OpenInSidebarNewTab: '' as IntlString,
ConfigureWidgets: '' as IntlString,
WorkspaceIsArchived: '' as IntlString,
WorkspaceIsMigrating: '' as IntlString
WorkspaceIsMigrating: '' as IntlString,
ViewMyProfile: '' as IntlString
},
icon: {
Search: '' as Asset
Expand Down
6 changes: 4 additions & 2 deletions server/account/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
"@hcengineering/server-storage": "workspace:^0.7.16",
"@hcengineering/server-core": "workspace:^0.7.19",
"otplib": "^12.0.1",
"@hcengineering/server-pipeline": "workspace:^0.7.0"
"@hcengineering/server-pipeline": "workspace:^0.7.0",
"@hcengineering/server-client": "workspace:^0.7.16",
"@hcengineering/account-client": "workspace:^0.7.25"
}
}
}
2 changes: 2 additions & 0 deletions server/account/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ import {
type Token
} from '@hcengineering/server-token'


import { randomUUID } from 'crypto'
import { isAdminEmail } from './admin'
import { accountPlugin } from './plugin'

import { type AccountServiceMethods, getServiceMethods } from './serviceOperations'
import {
AccountEventType,
Expand Down