@@ -9,6 +9,7 @@ import { CleanupRegistry } from '../../utilities/CleanupRegistry.js'
99 */
1010export 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