Skip to content

Commit e8b6760

Browse files
authored
Profile interface and web components lifecycle improvements (#427)
* Profile interface and web component lifecycle improvements * changes during review * changes during review
1 parent 8c19ca0 commit e8b6760

6 files changed

Lines changed: 223 additions & 80 deletions

File tree

components/user-profile/contributionActivity.js

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
import TPEN from '../../api/TPEN.js'
2-
import User from '../../api/User.js'
32
import Project from '../../api/Project.js'
43
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
4+
import { onUserReady } from '../../utilities/userReady.js'
5+
import { onUserProjectsReady } from '../../utilities/userProjectsReady.js'
56

67
/**
78
* ContributionActivity - Displays user's contribution activity across projects.
89
* @element contribution-activity
910
*/
1011
class ContributionActivity extends HTMLElement {
11-
static get observedAttributes() {
12-
return ['tpen-user-id']
13-
}
14-
1512
/** @type {CleanupRegistry} Registry for cleanup handlers */
1613
cleanup = new CleanupRegistry()
1714
/** @type {CleanupRegistry} Registry for render-specific handlers */
1815
renderCleanup = new CleanupRegistry()
16+
/** @type {Function|null} Unsubscribe function for user ready listener */
17+
_unsubUser = null
18+
/** @type {Function|null} Unsubscribe function for projects ready listener */
19+
_unsubProjects = null
20+
/** @type {Object|null} Cached profile data */
21+
_profile = null
22+
/** @type {Array|null} Cached projects data */
23+
_projects = null
24+
/** @type {boolean} Flag to prevent double rendering */
25+
_isRendering = false
1926

2027
constructor() {
2128
super()
@@ -24,34 +31,43 @@ class ContributionActivity extends HTMLElement {
2431

2532
connectedCallback() {
2633
TPEN.attachAuthentication(this)
27-
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-user-loaded', () => this.loadAndRender())
34+
this._unsubUser = onUserReady(this, (user) => {
35+
this._profile = user
36+
this.renderIfReady()
37+
})
38+
this._unsubProjects = onUserProjectsReady(this, (projects) => {
39+
this._projects = projects
40+
this.renderIfReady()
41+
})
2842
}
2943

3044
/**
31-
* Loads user projects and renders the contribution activity.
45+
* Renders only when both profile and projects are available.
46+
* Guards against double rendering when both callbacks fire quickly.
47+
*/
48+
renderIfReady() {
49+
if (this._profile && this._projects && !this._isRendering) {
50+
this._isRendering = true
51+
this.loadAndRender()
52+
.finally(() => { this._isRendering = false })
53+
}
54+
}
55+
56+
/**
57+
* Processes cached projects and renders the contribution activity.
3258
*/
3359
async loadAndRender() {
34-
const projects = await TPEN.getUserProjects(TPEN.getAuthorization())
60+
const projects = this._projects
3561
await this.processAndRender(projects)
3662
}
3763

3864
disconnectedCallback() {
65+
try { this._unsubUser?.() } catch {}
66+
try { this._unsubProjects?.() } catch {}
3967
this.renderCleanup.run()
4068
this.cleanup.run()
4169
}
4270

43-
attributeChangedCallback(name, oldValue, newValue) {
44-
if (name === 'tpen-user-id') {
45-
if (oldValue !== newValue) {
46-
const currVal = this?.user?._id
47-
if (newValue === currVal) return
48-
const loadedUser = new User(newValue)
49-
loadedUser.authentication = TPEN.getAuthorization()
50-
loadedUser.getProfile()
51-
}
52-
}
53-
}
54-
5571
/**
5672
* Processes project data and renders the contribution activity.
5773
* @param {Array} projects - Array of project data

components/user-profile/index.js

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import TPEN from '../../api/TPEN.js'
2-
import User from '../../api/User.js'
32
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
3+
import { onUserReady } from '../../utilities/userReady.js'
44

55
/**
66
* UserProfile - Displays and allows editing of user profile information.
77
* @element tpen-user-profile
88
*/
99
class UserProfile extends HTMLElement {
10-
static get observedAttributes() {
11-
return ['tpen-user-id']
12-
}
1310
user = TPEN.currentUser
1411
/** @type {CleanupRegistry} Registry for cleanup handlers */
1512
cleanup = new CleanupRegistry()
1613
/** @type {CleanupRegistry} Registry for render-specific handlers */
1714
renderCleanup = new CleanupRegistry()
15+
/** @type {Function|null} Unsubscribe function for user ready listener */
16+
_unsubUser = null
1817

1918
constructor() {
2019
super()
@@ -23,28 +22,23 @@ class UserProfile extends HTMLElement {
2322

2423
connectedCallback() {
2524
TPEN.attachAuthentication(this)
26-
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-user-loaded', ev => {
27-
this.render(ev.detail)
28-
this.updateProfile(ev.detail)
29-
this.user = ev.detail
30-
})
25+
this._unsubUser = onUserReady(this, this.handleUserReady)
3126
}
3227

3328
disconnectedCallback() {
29+
try { this._unsubUser?.() } catch {}
3430
this.renderCleanup.run()
3531
this.cleanup.run()
3632
}
3733

38-
attributeChangedCallback(name, oldValue, newValue) {
39-
if (name === 'tpen-user-id') {
40-
if (oldValue !== newValue) {
41-
const currVal = this?.user?._id
42-
if (newValue === currVal) return
43-
const loadedUser = new User(newValue)
44-
loadedUser.authentication = TPEN.getAuthorization()
45-
loadedUser.getProfile()
46-
}
47-
}
34+
/**
35+
* Handler for when user data is ready.
36+
* @param {Object} user - The loaded user object
37+
*/
38+
handleUserReady(user) {
39+
this.user = user
40+
this.render(user)
41+
this.updateProfile(user)
4842
}
4943

5044
updateProfile(profile) {

components/user-profile/report.js

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import TPEN from '../../api/TPEN.js'
2-
import User from '../../api/User.js'
32
import Project from '../../api/Project.js'
43
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
4+
import { onUserReady } from '../../utilities/userReady.js'
5+
import { onUserProjectsReady } from '../../utilities/userProjectsReady.js'
56

67
/**
78
* ReportStats - Displays summary statistics about user's projects.
89
* @element report-stats
910
*/
1011
class ReportStats extends HTMLElement {
11-
static get observedAttributes() {
12-
return ['tpen-user-id']
13-
}
14-
1512
/** @type {CleanupRegistry} Registry for cleanup handlers */
1613
cleanup = new CleanupRegistry()
14+
/** @type {Function|null} Unsubscribe function for user ready listener */
15+
_unsubUser = null
16+
/** @type {Function|null} Unsubscribe function for projects ready listener */
17+
_unsubProjects = null
18+
/** @type {Object|null} Cached profile data */
19+
_profile = null
20+
/** @type {Array|null} Cached projects data */
21+
_projects = null
22+
/** @type {boolean} Flag to prevent double rendering */
23+
_isRendering = false
1724

1825
constructor() {
1926
super()
@@ -22,14 +29,33 @@ class ReportStats extends HTMLElement {
2229

2330
connectedCallback() {
2431
TPEN.attachAuthentication(this)
25-
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-user-loaded', () => this.loadAndRender())
32+
this._unsubUser = onUserReady(this, (user) => {
33+
this._profile = user
34+
this.renderIfReady()
35+
})
36+
this._unsubProjects = onUserProjectsReady(this, (projects) => {
37+
this._projects = projects
38+
this.renderIfReady()
39+
})
2640
}
2741

2842
/**
29-
* Loads user projects, calculates statistics, and renders the report.
43+
* Renders only when both profile and projects are available.
44+
* Guards against double rendering when both callbacks fire quickly.
45+
*/
46+
renderIfReady() {
47+
if (this._profile && this._projects && !this._isRendering) {
48+
this._isRendering = true
49+
this.loadAndRender()
50+
.finally(() => { this._isRendering = false })
51+
}
52+
}
53+
54+
/**
55+
* Calculates statistics from cached projects and renders the report.
3056
*/
3157
async loadAndRender() {
32-
const projects = await TPEN.getUserProjects(TPEN.getAuthorization())
58+
const projects = this._projects
3359

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

5884
disconnectedCallback() {
85+
try { this._unsubUser?.() } catch {}
86+
try { this._unsubProjects?.() } catch {}
5987
this.cleanup.run()
6088
}
6189

62-
attributeChangedCallback(name, oldValue, newValue) {
63-
if (name === 'tpen-user-id') {
64-
if (oldValue !== newValue) {
65-
const currVal = this?.user?._id
66-
if (newValue === currVal) return
67-
const loadedUser = new User(newValue)
68-
loadedUser.authentication = TPEN.getAuthorization()
69-
loadedUser.getProfile()
70-
}
71-
}
72-
}
73-
7490
/**
7591
* Renders the report with pre-calculated statistics.
7692
* @param {number} projectCount - Number of projects

components/user-profile/userStats.js

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import TPEN from '../../api/TPEN.js'
2-
import User from '../../api/User.js'
32
import Project from '../../api/Project.js'
43
import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
4+
import { onUserReady } from '../../utilities/userReady.js'
5+
import { onUserProjectsReady } from '../../utilities/userProjectsReady.js'
56

67
/**
78
* UserStats - Displays user profile card with stats and collaborators.
89
* @element user-stats
910
*/
1011
class UserStats extends HTMLElement {
11-
static get observedAttributes() {
12-
return ['tpen-user-id']
13-
}
14-
1512
/** @type {CleanupRegistry} Registry for cleanup handlers */
1613
cleanup = new CleanupRegistry()
14+
/** @type {Function|null} Unsubscribe function for user ready listener */
15+
_unsubUser = null
16+
/** @type {Function|null} Unsubscribe function for projects ready listener */
17+
_unsubProjects = null
18+
/** @type {Object|null} Cached profile data */
19+
_profile = null
20+
/** @type {Array|null} Cached projects data */
21+
_projects = null
22+
/** @type {boolean} Flag to prevent double rendering */
23+
_isRendering = false
1724

1825
constructor() {
1926
super()
@@ -22,35 +29,44 @@ class UserStats extends HTMLElement {
2229

2330
connectedCallback() {
2431
TPEN.attachAuthentication(this)
25-
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-user-loaded', ev => this.loadAndRender(ev.detail))
32+
this._unsubUser = onUserReady(this, (user) => {
33+
this._profile = user
34+
this.renderIfReady()
35+
})
36+
this._unsubProjects = onUserProjectsReady(this, (projects) => {
37+
this._projects = projects
38+
this.renderIfReady()
39+
})
2640
}
2741

2842
/**
29-
* Loads user projects and renders the stats.
43+
* Renders only when both profile and projects are available.
44+
* Guards against double rendering when both callbacks fire quickly.
45+
*/
46+
renderIfReady() {
47+
if (this._profile && this._projects && !this._isRendering) {
48+
this._isRendering = true
49+
this.loadAndRender(this._profile, this._projects)
50+
.finally(() => { this._isRendering = false })
51+
}
52+
}
53+
54+
/**
55+
* Processes profile and project data, then renders the stats.
3056
* @param {Object} profile - User profile data
57+
* @param {Array} projects - Array of project data
3158
*/
32-
async loadAndRender(profile) {
33-
const projects = await TPEN.getUserProjects(TPEN.getAuthorization())
59+
async loadAndRender(profile, projects) {
3460
await this.processAndRender(profile, projects)
3561
this.updateProfile(profile)
3662
}
3763

3864
disconnectedCallback() {
65+
try { this._unsubUser?.() } catch {}
66+
try { this._unsubProjects?.() } catch {}
3967
this.cleanup.run()
4068
}
4169

42-
attributeChangedCallback(name, oldValue, newValue) {
43-
if (name === 'tpen-user-id') {
44-
if (oldValue !== newValue) {
45-
const currVal = this?.user?._id
46-
if (newValue === currVal) return
47-
const loadedUser = new User(newValue)
48-
loadedUser.authentication = TPEN.getAuthorization()
49-
loadedUser.getProfile()
50-
}
51-
}
52-
}
53-
5470
updateProfile(profile) {
5571
const publicProfile = this.getPublicProfile(profile)
5672
if(publicProfile.imageURL) {

utilities/userProjectsReady.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import TPEN from "../api/TPEN.js"
2+
3+
// Module-level flag to prevent multiple simultaneous fetches
4+
let isFetching = false
5+
6+
/**
7+
* Utility to handle user projects readiness with caching.
8+
* Checks if projects are already loaded before fetching.
9+
* Triggers fetch if needed, subscribes to event for results.
10+
* @param {Object} ctx - The context to bind the handler to
11+
* @param {Function} handler - The handler function to invoke with projects
12+
* @returns {Function} Unsubscribe function
13+
*/
14+
export const onUserProjectsReady = (ctx, handler) => {
15+
if (!ctx || typeof handler !== 'function') return () => {}
16+
const bound = handler.bind(ctx)
17+
18+
// Check if projects are already cached
19+
try {
20+
if (Array.isArray(TPEN.userProjects)) {
21+
bound(TPEN.userProjects)
22+
}
23+
} catch (_) {}
24+
25+
// Subscribe to future updates
26+
const eventHandler = () => {
27+
try {
28+
bound(TPEN.userProjects)
29+
} catch (_) {}
30+
}
31+
TPEN.eventDispatcher.on('tpen-user-projects-loaded', eventHandler)
32+
33+
// Trigger fetch if not already cached and not currently fetching
34+
if (!TPEN.userProjects && !isFetching) {
35+
isFetching = true
36+
const token = TPEN.getAuthorization()
37+
if (token) {
38+
TPEN.getUserProjects(token)
39+
.catch((error) => {
40+
console.error("Failed to load user projects:", error)
41+
})
42+
.finally(() => { isFetching = false })
43+
} else {
44+
isFetching = false
45+
}
46+
}
47+
48+
return () => TPEN.eventDispatcher.off('tpen-user-projects-loaded', eventHandler)
49+
}

0 commit comments

Comments
 (0)