Skip to content

Commit 7b3982c

Browse files
authored
491 all projects (#492)
* Refactor auth flow; handle project fetch errors On authentication, call TPEN.getUserProjects and dispatch a tpen-toast on error instead of letting failures be silent. Listen for tpen-user-projects-loaded to populate this.projects from TPEN.userProjects (avoids duplicate remote fetches). Register event listeners before calling TPEN.attachAuthentication. Also apply a small whitespace cleanup around an await(new Project(...).fetch()) call. * Update list-navigation.js * only draw once * Update list-navigation.js * local touch up suggestions
1 parent eb2cc8e commit 7b3982c

1 file changed

Lines changed: 96 additions & 64 deletions

File tree

components/projects/list-navigation.js

Lines changed: 96 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
99
*/
1010
export default class ProjectsListNavigation extends HTMLElement {
1111
#projects = []
12+
#renderPromise = null
1213

1314
/** @type {CleanupRegistry} Registry for cleanup handlers */
1415
cleanup = new CleanupRegistry()
@@ -117,25 +118,28 @@ export default class ProjectsListNavigation extends HTMLElement {
117118
}
118119

119120
connectedCallback() {
120-
TPEN.attachAuthentication(this)
121-
122-
this.cleanup.onEvent(TPEN.eventDispatcher, "tpen-authenticated", async (ev) => {
123-
try {
124-
this.projects = await TPEN.getUserProjects(ev.detail)
125-
} catch (error) {
126-
const status = error.status ?? 500
127-
const text = error.statusText ?? error.message ?? "Internal Error"
128-
const toast = new CustomEvent('tpen-toast', {
129-
detail: {
121+
this.cleanup.onEvent(TPEN.eventDispatcher, "tpen-authenticated", (ev) => {
122+
TPEN.getUserProjects(ev.detail) // ev.detail is the token
123+
.catch((error) => {
124+
const status = error.status ?? 500
125+
const text = error.statusText ?? error.message ?? "Internal Error"
126+
TPEN.eventDispatcher.dispatch('tpen-toast', {
130127
message: `Error fetching projects: ${text}`,
131128
status: status
132-
}
129+
})
130+
const list = this.shadowRoot?.getElementById('projectsListView')
131+
if (list) list.innerHTML = `<li>Failed to load projects</li>`
133132
})
134-
TPEN.eventDispatcher.dispatch(toast)
135-
this.shadowRoot.getElementById('projectsListView').innerHTML = `No projects found`
136-
}
137133
})
138134

135+
// Listen for when projects are loaded
136+
this.cleanup.onEvent(TPEN.eventDispatcher, "tpen-user-projects-loaded", () => {
137+
this.projects = TPEN.userProjects
138+
// Track the render promise to prevent concurrent updates
139+
this.#renderPromise?.catch(() => {}) // Suppress if previous render failed
140+
})
141+
TPEN.attachAuthentication(this)
142+
139143
// Handle empty recent activity signal from other components via central dispatcher
140144
this.cleanup.onEvent(TPEN.eventDispatcher, 'tpen-no-recent-activity', () => {
141145
// Show the welcome/empty state message
@@ -147,67 +151,95 @@ export default class ProjectsListNavigation extends HTMLElement {
147151
this.cleanup.run()
148152
}
149153
set projects(projects) {
154+
// Only update if projects actually changed
155+
if (this.#projects === projects) return
156+
150157
this.#projects = projects
151-
this.updateList()
158+
// Store the render promise so we can track pending updates
159+
this.#renderPromise = this.updateList().catch((error) => {
160+
// Error already handled in updateList, but store the rejection
161+
// so we can suppress it if a new render starts
162+
console.debug('Project list render failed:', error)
163+
})
152164
}
153165
get projects() {
154166
return this.#projects
155167
}
156168

157169
/**
158170
* Updates the project list in the DOM. Handles async permission checks.
171+
* Catches any errors during rendering and shows an error message.
159172
*/
160173
async updateList() {
161-
const root = this.shadowRoot
162-
let list = root.getElementById('projectsListView')
163-
if (!this.#projects?.length) {
164-
const welcome = document.createElement('section')
165-
welcome.className = 'welcome-message'
166-
welcome.innerHTML = `
167-
<p><strong>Welcome to TPEN!</strong></p>
168-
<p>Get started by creating your first project or importing a manuscript.</p>
169-
<ul class="welcome-list">
170-
<li aria-label="View Tutorials"><span aria-hidden="true">📚</span> <a href="https://three.t-pen.org/category/tutorials/" target="_blank" rel="noopener noreferrer">View Tutorials</a></li>
171-
<li aria-label="Frequently Asked Questions"><span aria-hidden="true">❓</span> <a href="https://three.t-pen.org/faq/" target="_blank" rel="noopener noreferrer">Frequently Asked Questions</a></li>
172-
<li aria-label="Find IIIF Resources"><span aria-hidden="true">🖼️</span> <a href="https://iiif.io/guides/finding_resources/" target="_blank" rel="noopener noreferrer">Find IIIF Resources</a></li>
173-
</ul>
174-
`
175-
if (list) list.replaceWith(welcome)
176-
else {
177-
const existingWelcome = root.querySelector('section.welcome-message')
178-
if (existingWelcome) existingWelcome.replaceWith(welcome)
179-
else root.appendChild(welcome)
174+
try {
175+
const root = this.shadowRoot
176+
let list = root.getElementById('projectsListView')
177+
if (!this.#projects?.length) {
178+
const welcome = document.createElement('section')
179+
welcome.className = 'welcome-message'
180+
welcome.innerHTML = `
181+
<p><strong>Welcome to TPEN!</strong></p>
182+
<p>Get started by creating your first project or importing a manuscript.</p>
183+
<ul class="welcome-list">
184+
<li aria-label="View Tutorials"><span aria-hidden="true">📚</span> <a href="https://three.t-pen.org/category/tutorials/" target="_blank" rel="noopener noreferrer">View Tutorials</a></li>
185+
<li aria-label="Frequently Asked Questions"><span aria-hidden="true">❓</span> <a href="https://three.t-pen.org/faq/" target="_blank" rel="noopener noreferrer">Frequently Asked Questions</a></li>
186+
<li aria-label="Find IIIF Resources"><span aria-hidden="true">🖼️</span> <a href="https://iiif.io/guides/finding_resources/" target="_blank" rel="noopener noreferrer">Find IIIF Resources</a></li>
187+
</ul>
188+
`
189+
if (list) list.replaceWith(welcome)
190+
else {
191+
const existingWelcome = root.querySelector('section.welcome-message')
192+
if (existingWelcome) existingWelcome.replaceWith(welcome)
193+
else root.appendChild(welcome)
194+
}
195+
return
180196
}
181-
return
182-
}
183-
// Ensure the list element exists when we have projects
184-
if (!list) {
185-
list = document.createElement('ol')
186-
list.id = 'projectsListView'
187-
if (this.classList.contains('unbounded')) list.classList.add('unbounded')
188-
const existingWelcome = root.querySelector('section.welcome-message')
189-
if (existingWelcome) existingWelcome.replaceWith(list)
190-
else root.appendChild(list)
191-
} else {
192-
list.innerHTML = ""
193-
}
194-
for (const project of this.#projects) {
195-
let manageLink = ``
196-
try {
197-
await(new Project(project._id).fetch())
198-
const isManageProjectPermission = CheckPermissions.checkEditAccess('PROJECT')
199-
manageLink = isManageProjectPermission ? `<a title="Manage Project" part="project-opt" href="/project/manage?projectID=${project._id}" aria-label="Manage Project">⚙</a>` : ``
200-
} catch (error) {
201-
console.warn(`Failed to check permissions for project ${project._id}:`, error)
202-
}
203-
list.innerHTML += `
204-
<li tpen-project-id="${project._id}">
205-
<a title="See Project Details" class="static" href="/project?projectID=${project._id}" part="project-link">
206-
${project.label ?? project.title}
207-
</a>
208-
${manageLink}
209-
</li>
210-
`
197+
// Ensure the list element exists when we have projects
198+
if (!list) {
199+
list = document.createElement('ol')
200+
list.id = 'projectsListView'
201+
if (this.classList.contains('unbounded')) list.classList.add('unbounded')
202+
const existingWelcome = root.querySelector('section.welcome-message')
203+
if (existingWelcome) existingWelcome.replaceWith(list)
204+
else root.appendChild(list)
205+
} else {
206+
list.innerHTML = ""
207+
}
208+
const projectItems = []
209+
for (const project of this.#projects) {
210+
let manageLink = ``
211+
try {
212+
await (new Project(project._id).fetch())
213+
const isManageProjectPermission = CheckPermissions.checkEditAccess('PROJECT')
214+
manageLink = isManageProjectPermission ? `<a title="Manage Project" part="project-opt" href="/project/manage?projectID=${project._id}" aria-label="Manage Project">⚙</a>` : ``
215+
} catch (error) {
216+
console.warn(`Failed to check permissions for project ${project._id}:`, error)
217+
// Continue rendering the project even if permission check fails
218+
}
219+
projectItems.push(`
220+
<li tpen-project-id="${project._id}">
221+
<a title="See Project Details" class="static" href="/project?projectID=${project._id}" part="project-link">
222+
${project.label ?? project.title}
223+
</a>
224+
${manageLink}
225+
</li>
226+
`)
227+
}
228+
list.innerHTML = projectItems.join('')
229+
} catch (error) {
230+
// Handle any errors that occur during updateList
231+
console.error('Error updating project list:', error)
232+
const root = this.shadowRoot
233+
if (!root) return // Shadow root was removed
234+
235+
const list = root.getElementById('projectsListView')
236+
if (list) {
237+
list.innerHTML = `<li>Failed to load projects</li>`
238+
}
239+
TPEN.eventDispatcher.dispatch('tpen-toast', {
240+
message: `Failed to load projects: ${error.message}`,
241+
status: 500
242+
})
211243
}
212244
}
213245
}

0 commit comments

Comments
 (0)