@@ -9,6 +9,7 @@ document.addEventListener('DOMContentLoaded', () => {
99 const tableWrapper = document . querySelector ( '.table-wrapper' ) ;
1010 const refreshButton = document . getElementById ( 'refresh-button' ) ;
1111 const storage = window . MB3RStorage ;
12+ const TABLE_COLUMN_COUNT = 7 ;
1213 const getI18n = ( ) => window . MB3RI18n ;
1314 const t = ( key ) => ( getI18n ( ) ?. t ? getI18n ( ) . t ( key ) : key ) ;
1415 const whenI18nReady = ( ) => getI18n ( ) ?. ready || Promise . resolve ( ) ;
@@ -35,6 +36,9 @@ document.addEventListener('DOMContentLoaded', () => {
3536 }
3637 return null ;
3738 } ;
39+ const isDeletableId = ( value ) => Number . isInteger ( Number ( value ) ) && Number ( value ) > 0 ;
40+ const parseDeleteId = ( value ) => ( isDeletableId ( value ) ? Number ( value ) : null ) ;
41+
3842 let currentPassword = '' ;
3943 let hasRemoteSuccess = false ;
4044
@@ -88,7 +92,7 @@ document.addEventListener('DOMContentLoaded', () => {
8892 tableBody . innerHTML = '' ;
8993 const row = document . createElement ( 'tr' ) ;
9094 const cell = document . createElement ( 'td' ) ;
91- cell . colSpan = 6 ;
95+ cell . colSpan = TABLE_COLUMN_COUNT ;
9296 cell . textContent = message ;
9397 row . appendChild ( cell ) ;
9498 tableBody . appendChild ( row ) ;
@@ -146,10 +150,48 @@ document.addEventListener('DOMContentLoaded', () => {
146150 tr . appendChild ( td ) ;
147151 } ) ;
148152
153+ const actionTd = document . createElement ( 'td' ) ;
154+ actionTd . className = 'admin-action-cell' ;
155+
156+ if ( isDeletableId ( row . id ) ) {
157+ const deleteButton = document . createElement ( 'button' ) ;
158+ deleteButton . type = 'button' ;
159+ deleteButton . className = 'button button-ghost button-small admin-delete-button' ;
160+ deleteButton . dataset . deleteId = String ( row . id ) ;
161+ deleteButton . textContent = t ( 'admin.table.actions.delete' ) ;
162+ deleteButton . setAttribute (
163+ 'aria-label' ,
164+ `${ t ( 'admin.table.actions.delete' ) } #${ row . id } `
165+ ) ;
166+ actionTd . appendChild ( deleteButton ) ;
167+ } else {
168+ actionTd . textContent = t ( 'common.placeholder' ) ;
169+ }
170+
171+ tr . appendChild ( actionTd ) ;
149172 tableBody . appendChild ( tr ) ;
150173 } ) ;
151174 } ;
152175
176+ const handleAuthError = ( error ) => {
177+ const isIncorrectPassword = / i n c o r r e c t p a s s w o r d / i. test ( String ( error . message || '' ) ) ;
178+ const isRateLimited = error . status === 429 ;
179+ currentPassword = '' ;
180+ lockTable ( ) ;
181+ setAuthStatus (
182+ isRateLimited ? t ( 'admin.status.tooManyAttempts' ) : error . message ,
183+ 'error'
184+ ) ;
185+ if ( isRateLimited ) {
186+ setOverlayMessage ( t ( 'admin.status.tooManyAttempts' ) , 'admin.status.tooManyAttempts' ) ;
187+ } else if ( isIncorrectPassword ) {
188+ setOverlayMessage ( t ( 'admin.status.incorrectPassword' ) , 'admin.status.incorrectPassword' ) ;
189+ } else {
190+ setOverlayMessage ( error . message || t ( 'admin.errors.loadFailed' ) ) ;
191+ }
192+ openModal ( ) ;
193+ } ;
194+
153195 const fetchApplications = async ( password ) => {
154196 await whenI18nReady ( ) ;
155197
@@ -176,12 +218,6 @@ document.addEventListener('DOMContentLoaded', () => {
176218 const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
177219
178220 if ( ! response . ok ) {
179- if ( response . status === 401 || response . status === 403 ) {
180- const authError = new Error ( data . message || t ( 'admin.errors.loadFailed' ) ) ;
181- authError . status = response . status ;
182- throw authError ;
183- }
184-
185221 const error = new Error ( data . message || t ( 'admin.errors.loadFailed' ) ) ;
186222 error . status = response . status ;
187223 throw error ;
@@ -199,6 +235,52 @@ document.addEventListener('DOMContentLoaded', () => {
199235 }
200236 } ;
201237
238+ const deleteApplication = async ( id , password ) => {
239+ await whenI18nReady ( ) ;
240+
241+ if ( ! password ) {
242+ const error = new Error ( t ( 'admin.errors.passwordRequired' ) ) ;
243+ error . status = 401 ;
244+ throw error ;
245+ }
246+
247+ const endpoint = resolveEndpoint ( '/applications' ) ;
248+ if ( ! endpoint ) {
249+ const configError = new Error ( t ( 'admin.errors.apiNotConfigured' ) ) ;
250+ configError . offline = true ;
251+ throw configError ;
252+ }
253+
254+ try {
255+ const response = await fetch ( endpoint , {
256+ method : 'DELETE' ,
257+ headers : {
258+ 'Content-Type' : 'application/json' ,
259+ 'x-admin-pass' : password
260+ } ,
261+ body : JSON . stringify ( { id } ) ,
262+ cache : 'no-store'
263+ } ) ;
264+
265+ const data = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
266+ if ( ! response . ok ) {
267+ const error = new Error ( data . message || t ( 'admin.errors.deleteFailed' ) ) ;
268+ error . status = response . status ;
269+ throw error ;
270+ }
271+
272+ return data ;
273+ } catch ( error ) {
274+ if ( ! error . status || isOfflineError ( error . status ) ) {
275+ const offlineError = new Error ( error . message || t ( 'admin.errors.backendUnreachable' ) ) ;
276+ offlineError . offline = true ;
277+ offlineError . status = error . status ;
278+ throw offlineError ;
279+ }
280+ throw error ;
281+ }
282+ } ;
283+
202284 const loadApplications = async ( { password = currentPassword , silent = false } = { } ) => {
203285 await whenI18nReady ( ) ;
204286
@@ -222,22 +304,7 @@ document.addEventListener('DOMContentLoaded', () => {
222304 return ;
223305 } catch ( error ) {
224306 if ( error . status === 401 || error . status === 403 || error . status === 429 ) {
225- const isIncorrectPassword = / i n c o r r e c t p a s s w o r d / i. test ( String ( error . message || '' ) ) ;
226- const isRateLimited = error . status === 429 ;
227- currentPassword = '' ;
228- lockTable ( ) ;
229- setAuthStatus (
230- isRateLimited ? t ( 'admin.status.tooManyAttempts' ) : error . message ,
231- 'error'
232- ) ;
233- if ( isRateLimited ) {
234- setOverlayMessage ( t ( 'admin.status.tooManyAttempts' ) , 'admin.status.tooManyAttempts' ) ;
235- } else if ( isIncorrectPassword ) {
236- setOverlayMessage ( t ( 'admin.status.incorrectPassword' ) , 'admin.status.incorrectPassword' ) ;
237- } else {
238- setOverlayMessage ( error . message || t ( 'admin.errors.loadFailed' ) ) ;
239- }
240- openModal ( ) ;
307+ handleAuthError ( error ) ;
241308 throw error ;
242309 }
243310
@@ -252,10 +319,7 @@ document.addEventListener('DOMContentLoaded', () => {
252319 }
253320
254321 if ( ! hasRemoteSuccess ) {
255- setAuthStatus (
256- t ( 'admin.status.apiUnavailable' ) ,
257- 'error'
258- ) ;
322+ setAuthStatus ( t ( 'admin.status.apiUnavailable' ) , 'error' ) ;
259323 currentPassword = '' ;
260324 lockTable ( ) ;
261325 setOverlayMessage ( t ( 'admin.status.apiUnavailableLater' ) , 'admin.status.apiUnavailableLater' ) ;
@@ -296,6 +360,54 @@ document.addEventListener('DOMContentLoaded', () => {
296360 }
297361 } ) ;
298362
363+ tableBody ?. addEventListener ( 'click' , async ( event ) => {
364+ const deleteButton = event . target . closest ( '[data-delete-id]' ) ;
365+ if ( ! deleteButton ) {
366+ return ;
367+ }
368+
369+ await whenI18nReady ( ) ;
370+
371+ if ( ! currentPassword ) {
372+ openModal ( ) ;
373+ return ;
374+ }
375+
376+ const applicationId = parseDeleteId ( deleteButton . dataset . deleteId ) ;
377+ if ( ! applicationId ) {
378+ setAuthStatus ( t ( 'admin.errors.invalidId' ) , 'error' ) ;
379+ return ;
380+ }
381+
382+ const confirmed = window . confirm ( `${ t ( 'admin.table.confirmDelete' ) } #${ applicationId } ` ) ;
383+ if ( ! confirmed ) {
384+ return ;
385+ }
386+
387+ deleteButton . disabled = true ;
388+ setAuthStatus ( t ( 'admin.status.deleting' ) , '' ) ;
389+
390+ try {
391+ await deleteApplication ( applicationId , currentPassword ) ;
392+ setAuthStatus ( t ( 'admin.status.deleted' ) , 'success' ) ;
393+ await loadApplications ( { silent : true } ) ;
394+ } catch ( error ) {
395+ if ( error . status === 401 || error . status === 403 || error . status === 429 ) {
396+ handleAuthError ( error ) ;
397+ return ;
398+ }
399+
400+ if ( error . offline ) {
401+ setAuthStatus ( t ( 'admin.status.apiUnavailableLater' ) , 'error' ) ;
402+ return ;
403+ }
404+
405+ setAuthStatus ( error . message || t ( 'admin.errors.deleteFailed' ) , 'error' ) ;
406+ } finally {
407+ deleteButton . disabled = false ;
408+ }
409+ } ) ;
410+
299411 refreshButton ?. addEventListener ( 'click' , ( ) => {
300412 if ( ! currentPassword ) {
301413 openModal ( ) ;
0 commit comments