Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 36 additions & 20 deletions components/user-profile/contributionActivity.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import TPEN from '../../api/TPEN.js'
import User from '../../api/User.js'
import Project from '../../api/Project.js'
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
import { onUserReady } from '../../utilities/userReady.js'
import { onUserProjectsReady } from '../../utilities/userProjectsReady.js'

/**
* ContributionActivity - Displays user's contribution activity across projects.
* @element contribution-activity
*/
class ContributionActivity extends HTMLElement {
static get observedAttributes() {
return ['tpen-user-id']
}

/** @type {CleanupRegistry} Registry for cleanup handlers */
cleanup = new CleanupRegistry()
/** @type {CleanupRegistry} Registry for render-specific handlers */
renderCleanup = new CleanupRegistry()
/** @type {Function|null} Unsubscribe function for user ready listener */
_unsubUser = null
/** @type {Function|null} Unsubscribe function for projects ready listener */
_unsubProjects = null
/** @type {Object|null} Cached profile data */
_profile = null
/** @type {Array|null} Cached projects data */
_projects = null
/** @type {boolean} Flag to prevent double rendering */
_isRendering = false

constructor() {
super()
Expand All @@ -24,34 +31,43 @@ class ContributionActivity extends HTMLElement {

connectedCallback() {
TPEN.attachAuthentication(this)
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-user-loaded', () => this.loadAndRender())
this._unsubUser = onUserReady(this, (user) => {
this._profile = user
this.renderIfReady()
})
this._unsubProjects = onUserProjectsReady(this, (projects) => {
this._projects = projects
this.renderIfReady()
})
}

/**
* Loads user projects and renders the contribution activity.
* Renders only when both profile and projects are available.
* Guards against double rendering when both callbacks fire quickly.
*/
renderIfReady() {
if (this._profile && this._projects && !this._isRendering) {
this._isRendering = true
this.loadAndRender()
.finally(() => { this._isRendering = false })
}
Comment thread
cubap marked this conversation as resolved.
}

/**
* Processes cached projects and renders the contribution activity.
*/
async loadAndRender() {
const projects = await TPEN.getUserProjects(TPEN.getAuthorization())
const projects = this._projects
await this.processAndRender(projects)
}

disconnectedCallback() {
try { this._unsubUser?.() } catch {}
try { this._unsubProjects?.() } catch {}
Comment on lines +65 to +66
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch blocks in disconnectedCallback silently suppress any errors from the unsubscribe functions. While unsubscription errors are typically rare, they could indicate issues with the event system. Consider logging these errors to aid debugging, similar to how errors are handled elsewhere in the codebase.

Suggested change
try { this._unsubUser?.() } catch {}
try { this._unsubProjects?.() } catch {}
try {
this._unsubUser?.()
} catch (err) {
console.error('contribution-activity: error during user unsubscribe in disconnectedCallback', err)
}
try {
this._unsubProjects?.()
} catch (err) {
console.error('contribution-activity: error during projects unsubscribe in disconnectedCallback', err)
}

Copilot uses AI. Check for mistakes.
this.renderCleanup.run()
this.cleanup.run()
}

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'tpen-user-id') {
if (oldValue !== newValue) {
const currVal = this?.user?._id
if (newValue === currVal) return
const loadedUser = new User(newValue)
loadedUser.authentication = TPEN.getAuthorization()
loadedUser.getProfile()
}
}
}

/**
* Processes project data and renders the contribution activity.
* @param {Array} projects - Array of project data
Expand Down
32 changes: 13 additions & 19 deletions components/user-profile/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import TPEN from '../../api/TPEN.js'
import User from '../../api/User.js'
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
import { onUserReady } from '../../utilities/userReady.js'

/**
* UserProfile - Displays and allows editing of user profile information.
* @element tpen-user-profile
*/
class UserProfile extends HTMLElement {
static get observedAttributes() {
return ['tpen-user-id']
}
user = TPEN.currentUser
/** @type {CleanupRegistry} Registry for cleanup handlers */
cleanup = new CleanupRegistry()
/** @type {CleanupRegistry} Registry for render-specific handlers */
renderCleanup = new CleanupRegistry()
/** @type {Function|null} Unsubscribe function for user ready listener */
_unsubUser = null

constructor() {
super()
Expand All @@ -23,28 +22,23 @@ class UserProfile extends HTMLElement {

connectedCallback() {
TPEN.attachAuthentication(this)
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-user-loaded', ev => {
this.render(ev.detail)
this.updateProfile(ev.detail)
this.user = ev.detail
})
this._unsubUser = onUserReady(this, this.handleUserReady)
}

disconnectedCallback() {
try { this._unsubUser?.() } catch {}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch block in disconnectedCallback silently suppresses any errors from the unsubscribe function. While unsubscription errors are typically rare, they could indicate issues with the event system. Consider logging these errors to aid debugging, similar to how errors are handled elsewhere in the codebase.

Suggested change
try { this._unsubUser?.() } catch {}
try {
this._unsubUser?.()
} catch (error) {
// Log unsubscribe errors to aid debugging without breaking teardown.
console.error('Error while unsubscribing user-ready listener in UserProfile.disconnectedCallback:', error)
}

Copilot uses AI. Check for mistakes.
this.renderCleanup.run()
this.cleanup.run()
}

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'tpen-user-id') {
if (oldValue !== newValue) {
const currVal = this?.user?._id
if (newValue === currVal) return
const loadedUser = new User(newValue)
loadedUser.authentication = TPEN.getAuthorization()
loadedUser.getProfile()
}
}
/**
* Handler for when user data is ready.
* @param {Object} user - The loaded user object
*/
handleUserReady(user) {
this.user = user
this.render(user)
this.updateProfile(user)
}

updateProfile(profile) {
Expand Down
56 changes: 36 additions & 20 deletions components/user-profile/report.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import TPEN from '../../api/TPEN.js'
import User from '../../api/User.js'
import Project from '../../api/Project.js'
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
import { onUserReady } from '../../utilities/userReady.js'
import { onUserProjectsReady } from '../../utilities/userProjectsReady.js'

/**
* ReportStats - Displays summary statistics about user's projects.
* @element report-stats
*/
class ReportStats extends HTMLElement {
static get observedAttributes() {
return ['tpen-user-id']
}

/** @type {CleanupRegistry} Registry for cleanup handlers */
cleanup = new CleanupRegistry()
/** @type {Function|null} Unsubscribe function for user ready listener */
_unsubUser = null
/** @type {Function|null} Unsubscribe function for projects ready listener */
_unsubProjects = null
/** @type {Object|null} Cached profile data */
_profile = null
/** @type {Array|null} Cached projects data */
_projects = null
/** @type {boolean} Flag to prevent double rendering */
_isRendering = false

constructor() {
super()
Expand All @@ -22,14 +29,33 @@ class ReportStats extends HTMLElement {

connectedCallback() {
TPEN.attachAuthentication(this)
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-user-loaded', () => this.loadAndRender())
this._unsubUser = onUserReady(this, (user) => {
this._profile = user
this.renderIfReady()
})
this._unsubProjects = onUserProjectsReady(this, (projects) => {
this._projects = projects
this.renderIfReady()
})
}

/**
* Loads user projects, calculates statistics, and renders the report.
* Renders only when both profile and projects are available.
* Guards against double rendering when both callbacks fire quickly.
*/
renderIfReady() {
if (this._profile && this._projects && !this._isRendering) {
this._isRendering = true
this.loadAndRender()
.finally(() => { this._isRendering = false })
}
Comment on lines +47 to +51
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _isRendering flag might not effectively prevent duplicate renders in all scenarios. If renderIfReady() is called while loadAndRender() is executing but before the async operations begin, the flag won't be set yet. Additionally, if loadAndRender() throws an error synchronously before reaching the .finally(), the flag won't be reset. Consider setting _isRendering = true at the start of renderIfReady() before calling loadAndRender(), or wrapping the entire operation in a try-finally block.

Copilot uses AI. Check for mistakes.
}

/**
* Calculates statistics from cached projects and renders the report.
*/
async loadAndRender() {
const projects = await TPEN.getUserProjects(TPEN.getAuthorization())
const projects = this._projects

const uniqueCollaborators = new Set()
projects.forEach(project => {
Expand All @@ -56,21 +82,11 @@ class ReportStats extends HTMLElement {
}

disconnectedCallback() {
try { this._unsubUser?.() } catch {}
try { this._unsubProjects?.() } catch {}
Comment on lines +85 to +86
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch blocks in disconnectedCallback silently suppress any errors from the unsubscribe functions. While unsubscription errors are typically rare, they could indicate issues with the event system. Consider logging these errors to aid debugging, similar to how errors are handled elsewhere in the codebase.

Suggested change
try { this._unsubUser?.() } catch {}
try { this._unsubProjects?.() } catch {}
try { this._unsubUser?.() } catch (err) { console.error('ReportStats: error while unsubscribing user listener:', err) }
try { this._unsubProjects?.() } catch (err) { console.error('ReportStats: error while unsubscribing projects listener:', err) }

Copilot uses AI. Check for mistakes.
this.cleanup.run()
}

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'tpen-user-id') {
if (oldValue !== newValue) {
const currVal = this?.user?._id
if (newValue === currVal) return
const loadedUser = new User(newValue)
loadedUser.authentication = TPEN.getAuthorization()
loadedUser.getProfile()
}
}
}

/**
* Renders the report with pre-calculated statistics.
* @param {number} projectCount - Number of projects
Expand Down
58 changes: 37 additions & 21 deletions components/user-profile/userStats.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import TPEN from '../../api/TPEN.js'
import User from '../../api/User.js'
import Project from '../../api/Project.js'
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
import { onUserReady } from '../../utilities/userReady.js'
import { onUserProjectsReady } from '../../utilities/userProjectsReady.js'

/**
* UserStats - Displays user profile card with stats and collaborators.
* @element user-stats
*/
class UserStats extends HTMLElement {
static get observedAttributes() {
return ['tpen-user-id']
}

/** @type {CleanupRegistry} Registry for cleanup handlers */
cleanup = new CleanupRegistry()
/** @type {Function|null} Unsubscribe function for user ready listener */
_unsubUser = null
/** @type {Function|null} Unsubscribe function for projects ready listener */
_unsubProjects = null
/** @type {Object|null} Cached profile data */
_profile = null
/** @type {Array|null} Cached projects data */
_projects = null
/** @type {boolean} Flag to prevent double rendering */
_isRendering = false

constructor() {
super()
Expand All @@ -22,35 +29,44 @@ class UserStats extends HTMLElement {

connectedCallback() {
TPEN.attachAuthentication(this)
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-user-loaded', ev => this.loadAndRender(ev.detail))
this._unsubUser = onUserReady(this, (user) => {
this._profile = user
this.renderIfReady()
})
this._unsubProjects = onUserProjectsReady(this, (projects) => {
this._projects = projects
this.renderIfReady()
})
}

/**
* Loads user projects and renders the stats.
* Renders only when both profile and projects are available.
* Guards against double rendering when both callbacks fire quickly.
*/
renderIfReady() {
if (this._profile && this._projects && !this._isRendering) {
this._isRendering = true
this.loadAndRender(this._profile, this._projects)
.finally(() => { this._isRendering = false })
Comment on lines +49 to +50
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _isRendering flag might not effectively prevent duplicate renders in all scenarios. If renderIfReady() is called while loadAndRender() is executing but before the async operations begin, the flag won't be set yet. Additionally, if loadAndRender() throws an error synchronously before reaching the .finally(), the flag won't be reset. Consider setting _isRendering = true at the start of renderIfReady() before calling loadAndRender(), or wrapping the entire operation in a try-finally block.

Suggested change
this.loadAndRender(this._profile, this._projects)
.finally(() => { this._isRendering = false })
try {
const result = this.loadAndRender(this._profile, this._projects)
Promise
.resolve(result)
.finally(() => {
this._isRendering = false
})
} catch (error) {
this._isRendering = false
throw error
}

Copilot uses AI. Check for mistakes.
}
}

/**
* Processes profile and project data, then renders the stats.
* @param {Object} profile - User profile data
* @param {Array} projects - Array of project data
*/
async loadAndRender(profile) {
const projects = await TPEN.getUserProjects(TPEN.getAuthorization())
async loadAndRender(profile, projects) {
await this.processAndRender(profile, projects)
this.updateProfile(profile)
}

disconnectedCallback() {
try { this._unsubUser?.() } catch {}
try { this._unsubProjects?.() } catch {}
Comment on lines +65 to +66
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch blocks in disconnectedCallback silently suppress any errors from the unsubscribe functions. While unsubscription errors are typically rare, they could indicate issues with the event system. Consider logging these errors to aid debugging, similar to how errors are handled elsewhere in the codebase.

Suggested change
try { this._unsubUser?.() } catch {}
try { this._unsubProjects?.() } catch {}
try { this._unsubUser?.() } catch (error) {
console.error('Error while unsubscribing user listener in <user-stats>:', error)
}
try { this._unsubProjects?.() } catch (error) {
console.error('Error while unsubscribing projects listener in <user-stats>:', error)
}

Copilot uses AI. Check for mistakes.
this.cleanup.run()
}

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'tpen-user-id') {
if (oldValue !== newValue) {
const currVal = this?.user?._id
if (newValue === currVal) return
const loadedUser = new User(newValue)
loadedUser.authentication = TPEN.getAuthorization()
loadedUser.getProfile()
}
}
}

updateProfile(profile) {
const publicProfile = this.getPublicProfile(profile)
if(publicProfile.imageURL) {
Expand Down
49 changes: 49 additions & 0 deletions utilities/userProjectsReady.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import TPEN from "../api/TPEN.js"

// Module-level flag to prevent multiple simultaneous fetches
let isFetching = false

/**
* Utility to handle user projects readiness with caching.
* Checks if projects are already loaded before fetching.
* Triggers fetch if needed, subscribes to event for results.
* @param {Object} ctx - The context to bind the handler to
* @param {Function} handler - The handler function to invoke with projects
* @returns {Function} Unsubscribe function
*/
export const onUserProjectsReady = (ctx, handler) => {
if (!ctx || typeof handler !== 'function') return () => {}
const bound = handler.bind(ctx)

// Check if projects are already cached
try {
if (Array.isArray(TPEN.userProjects)) {
bound(TPEN.userProjects)
}
} catch (_) {}

// Subscribe to future updates
const eventHandler = () => {
try {
bound(TPEN.userProjects)
} catch (_) {}
Comment on lines +23 to +29
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch blocks silently swallow errors from the handler invocation. While this may prevent errors in one handler from affecting others, it makes debugging difficult. Consider at minimum logging caught errors to the console so developers can identify issues when handlers fail.

Suggested change
} catch (_) {}
// Subscribe to future updates
const eventHandler = () => {
try {
bound(TPEN.userProjects)
} catch (_) {}
} catch (err) {
console.error("Error in onUserProjectsReady initial handler invocation:", err)
}
// Subscribe to future updates
const eventHandler = () => {
try {
bound(TPEN.userProjects)
} catch (err) {
console.error("Error in onUserProjectsReady event handler invocation:", err)
}

Copilot uses AI. Check for mistakes.
}
TPEN.eventDispatcher.on('tpen-user-projects-loaded', eventHandler)

// Trigger fetch if not already cached and not currently fetching
if (!TPEN.userProjects && !isFetching) {
isFetching = true
const token = TPEN.getAuthorization()
if (token) {
TPEN.getUserProjects(token)
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The promise returned by getUserProjects() is not being caught for errors. If the API request fails, the error will be unhandled and isFetching will still be reset in finally(), but the error state won't be communicated to waiting components. Consider adding a .catch() handler to log the error or dispatch a user-projects-load-failed event for consistency with other API patterns in the codebase.

Suggested change
TPEN.getUserProjects(token)
TPEN.getUserProjects(token)
.catch((error) => {
console.error('Failed to load user projects', error)
})

Copilot uses AI. Check for mistakes.
.catch((error) => {
console.error("Failed to load user projects:", error)
})
.finally(() => { isFetching = false })
} else {
isFetching = false
}
}

return () => TPEN.eventDispatcher.off('tpen-user-projects-loaded', eventHandler)
}
Loading