-
Notifications
You must be signed in to change notification settings - Fork 3
Profile interface and web components lifecycle improvements #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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() | ||||||||||||||||||||||||||
|
|
@@ -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 }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * 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
|
||||||||||||||||||||||||||
| 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) | |
| } |
| 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() | ||||||||||||||||
|
|
@@ -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 {} | ||||||||||||||||
|
||||||||||||||||
| 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) | |
| } |
| 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() | ||||||||||
|
|
@@ -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
|
||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * 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 => { | ||||||||||
|
|
@@ -56,21 +82,11 @@ class ReportStats extends HTMLElement { | |||||||||
| } | ||||||||||
|
|
||||||||||
| disconnectedCallback() { | ||||||||||
| try { this._unsubUser?.() } catch {} | ||||||||||
| try { this._unsubProjects?.() } catch {} | ||||||||||
|
Comment on lines
+85
to
+86
|
||||||||||
| 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) } |
| 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() | ||||||||||||||||||||||||||||
|
|
@@ -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
|
||||||||||||||||||||||||||||
| 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
AI
Feb 3, 2026
There was a problem hiding this comment.
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.
| 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) | |
| } |
| 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
|
||||||||||||||||||||||||||||||||||||||
| } 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
AI
Feb 3, 2026
There was a problem hiding this comment.
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.
| TPEN.getUserProjects(token) | |
| TPEN.getUserProjects(token) | |
| .catch((error) => { | |
| console.error('Failed to load user projects', error) | |
| }) |
Uh oh!
There was an error while loading. Please reload this page.