PageSourceSearch

https://controltab.io/assets/controllers/admin_theme_controller-55…0eb426d94a4c7108f881fe95ef9ae42076b2dd41.js

js controltab.io collected 2026-09-24 15:03:29 UTC 28,495 bytes, 670 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// Connects to data-controller="admin-theme"
4export default class extends Controller {
5  static targets = ["sidebar", "overlay", "sunIcon", "moonIcon", "apiModal", "modalOverlay", "workspaceModal", "workspaceModalOverlay", "inviteModal", "inviteModalOverlay", "editWorkspaceModal", "editWorkspaceModalOverlay", "deleteWorkspaceModal", "deleteWorkspaceModalOverlay", "credentialModal", "credentialModalOverlay", "editCredentialModal", "editCredentialModalOverlay", "deleteCredentialModal", "deleteCredentialModalOverlay", "viewTokenModal", "viewTokenModalOverlay", "associateCredentialModal", "associateCredentialModalOverlay", "oauthAppDetailsModal", "oauthAppDetailsModalOverlay", "revokeAllTokensModal", "revokeAllTokensModalOverlay", "deleteOAuthAppModal", "deleteOAuthAppModalOverlay", "docModal", "docModalOverlay"]
6
7  connect() {
8    // Inicializar dark mode desde localStorage
9    const initialDarkMode = localStorage.theme === 'dark'
10    this.applyDarkMode(initialDarkMode)
11
12    // Inicializar estado del sidebar desde localStorage
13    const initialSidebarState = localStorage.getItem('sidebarCollapsed') === 'true'
14    this.applySidebarState(initialSidebarState)
15  }
16
17  // Dark Mode Toggle
18  toggleDarkMode() {
19    const isDark = document.documentElement.classList.contains('dark')
20    this.applyDarkMode(!isDark)
21  }
22
23  applyDarkMode(isDark) {
24    if (isDark) {
25      document.documentElement.classList.add('dark')
26      document.documentElement.classList.remove('light')
27      if (this.hasSunIconTarget) this.sunIconTarget.classList.add('hidden')
28      if (this.hasMoonIconTarget) this.moonIconTarget.classList.remove('hidden')
29      localStorage.theme = 'dark'
30    } else {
31      document.documentElement.classList.add('light')
32      document.documentElement.classList.remove('dark')
33      if (this.hasSunIconTarget) this.sunIconTarget.classList.remove('hidden')
34      if (this.hasMoonIconTarget) this.moonIconTarget.classList.add('hidden')
35      localStorage.theme = 'light'
36    }
37  }
38
39  // Sidebar Toggle (Desktop - Collapse/Expand)
40  toggleSidebar() {
41    const sidebar = document.getElementById('sidebar')
42    const mainContent = document.getElementById('main-content')
43    const sidebarToggleDesktop = document.getElementById('sidebar-toggle-desktop')
44    const sidebarTextContents = document.querySelectorAll('.sidebar-text-content')
45    const sidebarNavLinks = document.getElementById('sidebar-nav-links')
46
47    const isCollapsed = sidebar.classList.contains('w-20')
48    this.applySidebarState(!isCollapsed, sidebar, mainContent, sidebarToggleDesktop, sidebarTextContents, sidebarNavLinks)
49  }
50
51  applySidebarState(isCollapsed, sidebar = null, mainContent = null, toggleBtn = null, textContents = null, navLinks = null) {
52    sidebar = sidebar || document.getElementById('sidebar')
53    mainContent = mainContent || document.getElementById('main-content')
54    toggleBtn = toggleBtn || document.getElementById('sidebar-toggle-desktop')
55    textContents = textContents || document.querySelectorAll('.sidebar-text-content')
56    navLinks = navLinks || document.getElementById('sidebar-nav-links')
57
58    if (isCollapsed) {
59      // Colapsar
60      sidebar.classList.remove('w-64')
61      sidebar.classList.add('w-20')
62      mainContent.classList.remove('md:ml-64')
63      mainContent.classList.add('md:ml-20')
64      textContents.forEach(span => span.classList.add('hidden'))
65      navLinks.querySelectorAll('a').forEach(a => {
66        a.classList.add('justify-center')
67        const svg = a.querySelector('svg')
68        if (svg) svg.classList.remove('mr-3')
69      })
70      if (toggleBtn) toggleBtn.classList.add('rotate-180')
71      localStorage.setItem('sidebarCollapsed', 'true')
72    } else {
73      // Expandir
74      sidebar.classList.add('w-64')
75      sidebar.classList.remove('w-20')
76      mainContent.classList.add('md:ml-64')
77      mainContent.classList.remove('md:ml-20')
78      textContents.forEach(span => span.classList.remove('hidden'))
79      navLinks.querySelectorAll('a').forEach(a => {
80        a.classList.remove('justify-center')
81        const svg = a.querySelector('svg')
82        if (svg) svg.classList.add('mr-3')
83      })
84      if (toggleBtn) toggleBtn.classList.remove('rotate-180')
85      localStorage.setItem('sidebarCollapsed', 'false')
86    }
87  }
88
89  // Sidebar Mobile - Open
90  openSidebar() {
91    const sidebar = document.getElementById('sidebar')
92    const overlay = document.getElementById('sidebar-overlay')
93
94    sidebar.classList.remove('-translate-x-full')
95    overlay.classList.remove('hidden')
96    setTimeout(() => overlay.classList.add('opacity-100'), 10)
97  }
98
99  // Sidebar Mobile - Close
100  closeSidebar() {
101    const sidebar = document.getElementById('sidebar')
102    const overlay = document.getElementById('sidebar-overlay')
103
104    sidebar.classList.add('-translate-x-full')
105    overlay.classList.remove('opacity-100')
106    setTimeout(() => overlay.classList.add('hidden'), 300)
107  }
108
109  // API Modal - Open
110  openApiModal() {
111    if (this.hasApiModalTarget && this.hasModalOverlayTarget) {
112      this.apiModalTarget.classList.remove('hidden')
113      this.modalOverlayTarget.classList.remove('hidden')
114      setTimeout(() => {
115        this.apiModalTarget.classList.add('opacity-100', 'scale-100')
116        this.apiModalTarget.classList.remove('scale-95')
117        this.modalOverlayTarget.classList.add('opacity-100')
118      }, 10)
119    }
120  }
121
122  // API Modal - Close
123  closeApiModal() {
124    if (this.hasApiModalTarget && this.hasModalOverlayTarget) {
125      this.apiModalTarget.classList.remove('opacity-100', 'scale-100')
126      this.apiModalTarget.classList.add('scale-95')
127      this.modalOverlayTarget.classList.remove('opacity-100')
128      setTimeout(() => {
129        this.apiModalTarget.classList.add('hidden')
130        this.modalOverlayTarget.classList.add('hidden')
131      }, 300)
132    }
133  }
134
135  // Workspace Modal - Open
136  openWorkspaceModal() {
137    if (this.hasWorkspaceModalTarget && this.hasWorkspaceModalOverlayTarget) {
138      this.workspaceModalTarget.classList.remove('hidden')
139      this.workspaceModalOverlayTarget.classList.remove('hidden')
140      setTimeout(() => {
141        this.workspaceModalTarget.classList.add('opacity-100', 'scale-100')
142        this.workspaceModalTarget.classList.remove('scale-95')
143        this.workspaceModalOverlayTarget.classList.add('opacity-100')
144      }, 10)
145    }
146  }
147
148  // Workspace Modal - Close
149  closeWorkspaceModal() {
150    if (this.hasWorkspaceModalTarget && this.hasWorkspaceModalOverlayTarget) {
151      this.workspaceModalTarget.classList.remove('opacity-100', 'scale-100')
152      this.workspaceModalTarget.classList.add('scale-95')
153      this.workspaceModalOverlayTarget.classList.remove('opacity-100')
154      setTimeout(() => {
155        this.workspaceModalTarget.classList.add('hidden')
156        this.workspaceModalOverlayTarget.classList.add('hidden')
157      }, 300)
158    }
159  }
160
161  // Invite Modal - Open
162  openInviteModal() {
163    if (this.hasInviteModalTarget && this.hasInviteModalOverlayTarget) {
164      this.inviteModalTarget.classList.remove('hidden')
165      this.inviteModalOverlayTarget.classList.remove('hidden')
166      setTimeout(() => {
167        this.inviteModalTarget.classList.add('opacity-100', 'scale-100')
168        this.inviteModalTarget.classList.remove('scale-95')
169        this.inviteModalOverlayTarget.classList.add('opacity-100')
170      }, 10)
171    }
172  }
173
174  // Invite Modal - Close
175  closeInviteModal() {
176    if (this.hasInviteModalTarget && this.hasInviteModalOverlayTarget) {
177      this.inviteModalTarget.classList.remove('opacity-100', 'scale-100')
178      this.inviteModalTarget.classList.add('scale-95')
179      this.inviteModalOverlayTarget.classList.remove('opacity-100')
180      setTimeout(() => {
181        this.inviteModalTarget.classList.add('hidden')
182        this.inviteModalOverlayTarget.classList.add('hidden')
183      }, 300)
184    }
185  }
186
187  // Edit Workspace Modal - Open
188  openEditWorkspaceModal(event) {
189    event.preventDefault()
190    const button = event.currentTarget
191    const workspaceId = button.dataset.workspaceId
192    const workspaceName = button.dataset.workspaceName
193
194    if (this.hasEditWorkspaceModalTarget && this.hasEditWorkspaceModalOverlayTarget) {
195      // Actualizar la acción del formulario y el valor del campo nombre
196      const form = document.getElementById('edit-workspace-form')
197      const nameInput = document.getElementById('edit_workspace_name')
198
199      form.action = `/admin/workspaces/${workspaceId}`
200      nameInput.value = workspaceName
201
202      this.editWorkspaceModalTarget.classList.remove('hidden')
203      this.editWorkspaceModalOverlayTarget.classList.remove('hidden')
204      setTimeout(() => {
205        this.editWorkspaceModalTarget.classList.add('opacity-100', 'scale-100')
206        this.editWorkspaceModalTarget.classList.remove('scale-95')
207        this.editWorkspaceModalOverlayTarget.classList.add('opacity-100')
208      }, 10)
209    }
210  }
211
212  // Edit Workspace Modal - Close
213  closeEditWorkspaceModal() {
214    if (this.hasEditWorkspaceModalTarget && this.hasEditWorkspaceModalOverlayTarget) {
215      this.editWorkspaceModalTarget.classList.remove('opacity-100', 'scale-100')
216      this.editWorkspaceModalTarget.classList.add('scale-95')
217      this.editWorkspaceModalOverlayTarget.classList.remove('opacity-100')
218      setTimeout(() => {
219        this.editWorkspaceModalTarget.classList.add('hidden')
220        this.editWorkspaceModalOverlayTarget.classList.add('hidden')
221      }, 300)
222    }
223  }
224
225  // Delete Workspace Modal - Open
226  openDeleteWorkspaceModal(event) {
227    event.preventDefault()
228    const button = event.currentTarget
229    const workspaceId = button.dataset.workspaceId
230    const workspaceName = button.dataset.workspaceName
231
232    if (this.hasDeleteWorkspaceModalTarget && this.hasDeleteWorkspaceModalOverlayTarget) {
233      // Actualizar la acción del formulario y el nombre del workspace en el mensaje
234      const form = document.getElementById('delete-workspace-form')
235      const nameDisplay = document.getElementById('delete-workspace-name')
236
237      form.action = `/admin/workspaces/${workspaceId}`
238      nameDisplay.textContent = workspaceName
239
240      this.deleteWorkspaceModalTarget.classList.remove('hidden')
241      this.deleteWorkspaceModalOverlayTarget.classList.remove('hidden')
242      setTimeout(() => {
243        this.deleteWorkspaceModalTarget.classList.add('opacity-100', 'scale-100')
244        this.deleteWorkspaceModalTarget.classList.remove('scale-95')
245        this.deleteWorkspaceModalOverlayTarget.classList.add('opacity-100')
246      }, 10)
247    }
248  }
249
250  // Delete Workspace Modal - Close
251  closeDeleteWorkspaceModal() {
252    if (this.hasDeleteWorkspaceModalTarget && this.hasDeleteWorkspaceModalOverlayTarget) {
253      this.deleteWorkspaceModalTarget.classList.remove('opacity-100', 'scale-100')
254      this.deleteWorkspaceModalTarget.classList.add('scale-95')
255      this.deleteWorkspaceModalOverlayTarget.classList.remove('opacity-100')
256      setTimeout(() => {
257        this.deleteWorkspaceModalTarget.classList.add('hidden')
258        this.deleteWorkspaceModalOverlayTarget.classList.add('hidden')
259      }, 300)
260    }
261  }
262
263  // Credential Modal - Open
264  openCredentialModal() {
265    if (this.hasCredentialModalTarget && this.hasCredentialModalOverlayTarget) {
266      this.credentialModalTarget.classList.remove('hidden')
267      this.credentialModalOverlayTarget.classList.remove('hidden')
268      setTimeout(() => {
269        this.credentialModalTarget.classList.add('opacity-100', 'scale-100')
270        this.credentialModalTarget.classList.remove('scale-95')
271        this.credentialModalOverlayTarget.classList.add('opacity-100')
272      }, 10)
273    }
274  }
275
276  // Credential Modal - Close
277  closeCredentialModal() {
278    if (this.hasCredentialModalTarget && this.hasCredentialModalOverlayTarget) {
279      this.credentialModalTarget.classList.remove('opacity-100', 'scale-100')
280      this.credentialModalTarget.classList.add('scale-95')
281      this.credentialModalOverlayTarget.classList.remove('opacity-100')
282      setTimeout(() => {
283        this.credentialModalTarget.classList.add('hidden')
284        this.credentialModalOverlayTarget.classList.add('hidden')
285      }, 300)
286    }
287  }
288
289  // Edit Credential Modal - Open
290  openEditCredentialModal(event) {
291    event.preventDefault()
292    const button = event.currentTarget
293    const credentialId = button.dataset.credentialId
294    const credentialName = button.dataset.credentialName
295    const credentialEnabled = button.dataset.credentialEnabled === 'true'
296
297    if (this.hasEditCredentialModalTarget && this.hasEditCredentialModalOverlayTarget) {
298      const form = document.getElementById('edit-credential-form')
299      const nameInput = document.getElementById('edit_credential_name')
300      const enabledInput = document.getElementById('edit_credential_enabled')
301
302      form.action = `/admin/credentials/${credentialId}`
303      nameInput.value = credentialName
304      enabledInput.checked = credentialEnabled
305
306      this.editCredentialModalTarget.classList.remove('hidden')
307      this.editCredentialModalOverlayTarget.classList.remove('hidden')
308      setTimeout(() => {
309        this.editCredentialModalTarget.classList.add('opacity-100', 'scale-100')
310        this.editCredentialModalTarget.classList.remove('scale-95')
311        this.editCredentialModalOverlayTarget.classList.add('opacity-100')
312      }, 10)
313    }
314  }
315
316  // Edit Credential Modal - Close
317  closeEditCredentialModal() {
318    if (this.hasEditCredentialModalTarget && this.hasEditCredentialModalOverlayTarget) {
319      this.editCredentialModalTarget.classList.remove('opacity-100', 'scale-100')
320      this.editCredentialModalTarget.classList.add('scale-95')
321      this.editCredentialModalOverlayTarget.classList.remove('opacity-100')
322      setTimeout(() => {
323        this.editCredentialModalTarget.classList.add('hidden')
324        this.editCredentialModalOverlayTarget.classList.add('hidden')
325      }, 300)
326    }
327  }
328
329  // Delete Credential Modal - Open
330  openDeleteCredentialModal(event) {
331    event.preventDefault()
332    const button = event.currentTarget
333    const credentialId = button.dataset.credentialId
334    const credentialName = button.dataset.credentialName
335    const workspaceCount = parseInt(button.dataset.credentialWorkspaceCount || 0)
336
337    if (this.hasDeleteCredentialModalTarget && this.hasDeleteCredentialModalOverlayTarget) {
338      const form = document.getElementById('delete-credential-form')
339      const nameDisplay = document.getElementById('delete-credential-name')
340      const warning = document.getElementById('delete-credential-warning')
341      const warningText = document.getElementById('delete-credential-warning-text')
342
343      form.action = `/admin/credentials/${credentialId}`
344      nameDisplay.textContent = credentialName
345
346      if (workspaceCount > 0) {
347        warning.classList.remove('hidden')
348        warningText.textContent = `This credential is associated with ${workspaceCount} workspace(s). Deleting it will remove authentication from these workspaces.`
349      } else {
350        warning.classList.add('hidden')
351      }
352
353      this.deleteCredentialModalTarget.classList.remove('hidden')
354      this.deleteCredentialModalOverlayTarget.classList.remove('hidden')
355      setTimeout(() => {
356        this.deleteCredentialModalTarget.classList.add('opacity-100', 'scale-100')
357        this.deleteCredentialModalTarget.classList.remove('scale-95')
358        this.deleteCredentialModalOverlayTarget.classList.add('opacity-100')
359      }, 10)
360    }
361  }
362
363  // Delete Credential Modal - Close
364  closeDeleteCredentialModal() {
365    if (this.hasDeleteCredentialModalTarget && this.hasDeleteCredentialModalOverlayTarget) {
366      this.deleteCredentialModalTarget.classList.remove('opacity-100', 'scale-100')
367      this.deleteCredentialModalTarget.classList.add('scale-95')
368      this.deleteCredentialModalOverlayTarget.classList.remove('opacity-100')
369      setTimeout(() => {
370        this.deleteCredentialModalTarget.classList.add('hidden')
371        this.deleteCredentialModalOverlayTarget.classList.add('hidden')
372      }, 300)
373    }
374  }
375
376  // View Token Modal - Open
377  openViewTokenModal(event) {
378    event.preventDefault()
379    const button = event.currentTarget
380    const credentialId = button.dataset.credentialId
381    const credentialName = button.dataset.credentialName
382
383    if (this.hasViewTokenModalTarget && this.hasViewTokenModalOverlayTarget) {
384      const tokenInput = document.getElementById('view-token-input')
385      const nameDisplay = document.getElementById('view-token-credential-name')
386
387      nameDisplay.textContent = credentialName
388
389      // TODO: El token solo se muestra una vez al crear la credencial
390      // No es posible recuperarlo posteriormente por seguridad
391      tokenInput.value = 'Token no disponible (solo se muestra al crear)'
392
393      this.viewTokenModalTarget.classList.remove('hidden')
394      this.viewTokenModalOverlayTarget.classList.remove('hidden')
395      setTimeout(() => {
396        this.viewTokenModalTarget.classList.add('opacity-100', 'scale-100')
397        this.viewTokenModalTarget.classList.remove('scale-95')
398        this.viewTokenModalOverlayTarget.classList.add('opacity-100')
399      }, 10)
400    }
401  }
402
403  // View Token Modal - Close
404  closeViewTokenModal() {
405    if (this.hasViewTokenModalTarget && this.hasViewTokenModalOverlayTarget) {
406      this.viewTokenModalTarget.classList.remove('opacity-100', 'scale-100')
407      this.viewTokenModalTarget.classList.add('scale-95')
408      this.viewTokenModalOverlayTarget.classList.remove('opacity-100')
409      setTimeout(() => {
410        this.viewTokenModalTarget.classList.add('hidden')
411        this.viewTokenModalOverlayTarget.classList.add('hidden')
412      }, 300)
413    }
414  }
415
416  // Associate Credential Modal - Open
417  openAssociateCredentialModal(event) {
418    event.preventDefault()
419    const button = event.currentTarget
420    const credentialId = button.dataset.credentialId
421    const credentialName = button.dataset.credentialName
422
423    if (this.hasAssociateCredentialModalTarget && this.hasAssociateCredentialModalOverlayTarget) {
424      const form = document.getElementById('associate-credential-form')
425      const nameDisplay = document.getElementById('associate-credential-name')
426      const workspacesList = document.getElementById('workspaces-list')
427      const loadingDiv = document.getElementById('workspaces-loading')
428      const emptyDiv = document.getElementById('workspaces-empty')
429      const submitBtn = document.getElementById('associate-submit-btn')
430
431      form.action = `/admin/credentials/${credentialId}/associate`
432      nameDisplay.textContent = credentialName
433
434      // Reset state
435      form.classList.add('hidden')
436      loadingDiv.classList.remove('hidden')
437      emptyDiv.classList.add('hidden')
438      workspacesList.innerHTML = ''
439      submitBtn.disabled = true
440
441      // Load workspaces from Rails endpoint (which fetches from Go backend)
442      fetch(`/admin/credentials/workspaces_for_association?credential_id=${credentialId}`)
443        .then(response => response.text())
444        .then(html => {
445          loadingDiv.classList.add('hidden')
446          workspacesList.innerHTML = html
447
448          // Check if workspaces were loaded
449          const hasWorkspaces = workspacesList.querySelector('.workspace-c
449heckbox')
450          if (hasWorkspaces) {
451            form.classList.remove('hidden')
452
453            // Store initial state of checkboxes
454            const checkboxes = workspacesList.querySelectorAll('.workspace-checkbox')
455            const initialState = new Map()
456            checkboxes.forEach(checkbox => {
457              initialState.set(checkbox.value, checkbox.checked)
458            })
459
460            // Function to check if there are changes
461            const hasChanges = () => {
462              return Array.from(checkboxes).some(checkbox => {
463                return checkbox.checked !== initialState.get(checkbox.value)
464              })
465            }
466
467            // Initially disable button (no changes yet)
468            submitBtn.disabled = !hasChanges()
469
470            // Add event listener to checkboxes to enable/disable submit button based on changes
471            checkboxes.forEach(checkbox => {
472              checkbox.addEventListener('change', () => {
473                submitBtn.disabled = !hasChanges()
474              })
475            })
476          }
477          // If no workspaces, the partial already shows the empty state message
478        })
479        .catch(error => {
480          console.error('Error loading workspaces:', error)
481          loadingDiv.classList.add('hidden')
482          workspacesList.innerHTML = '<p class="text-sm text-red-600 dark:text-red-400 text-center py-4">Error al cargar workspaces</p>'
483        })
484
485      this.associateCredentialModalTarget.classList.remove('hidden')
486      this.associateCredentialModalOverlayTarget.classList.remove('hidden')
487      setTimeout(() => {
488        this.associateCredentialModalTarget.classList.add('opacity-100', 'scale-100')
489        this.associateCredentialModalTarget.classList.remove('scale-95')
490        this.associateCredentialModalOverlayTarget.classList.add('opacity-100')
491      }, 10)
492    }
493  }
494
495  // Associate Credential Modal - Close
496  closeAssociateCredentialModal() {
497    if (this.hasAssociateCredentialModalTarget && this.hasAssociateCredentialModalOverlayTarget) {
498      this.associateCredentialModalTarget.classList.remove('opacity-100', 'scale-100')
499      this.associateCredentialModalTarget.classList.add('scale-95')
500      this.associateCredentialModalOverlayTarget.classList.remove('opacity-100')
501      setTimeout(() => {
502        this.associateCredentialModalTarget.classList.add('hidden')
503        this.associateCredentialModalOverlayTarget.classList.add('hidden')
504      }, 300)
505    }
506  }
507
508  // OAuth App Details Modal - Open
509  openOAuthAppDetailsModal(event) {
510    if (this.hasOauthAppDetailsModalTarget && this.hasOauthAppDetailsModalOverlayTarget) {
511      const button = event.currentTarget
512      const appId = button.dataset.appId
513      const appName = button.dataset.appName
514      const clientId = button.dataset.appUid
515      const clientSecret = button.dataset.appSecret
516      const redirectUris = button.dataset.appRedirectUri
517      const scopes = button.dataset.appScopes
518
519      // Llenar el modal con los datos
520      document.getElementById('oauth-app-details-name').textContent = appName
521      document.getElementById('oauth-app-details-app-name').textContent = appName
522      document.getElementById('oauth-app-details-client-id').value = clientId
523      document.getElementById('oauth-app-details-secret').value = clientSecret
524      document.getElementById('oauth-app-details-redirect-uris').textContent = redirectUris
525      document.getElementById('oauth-app-details-scopes').textContent = scopes || 'mcp:*'
526
527      // Mostrar modal con animación
528      this.oauthAppDetailsModalTarget.classList.remove('hidden')
529      this.oauthAppDetailsModalOverlayTarget.classList.remove('hidden')
530      setTimeout(() => {
531        this.oauthAppDetailsModalTarget.classList.remove('opacity-0', 'scale-95')
532        this.oauthAppDetailsModalTarget.classList.add('opacity-100', 'scale-100')
533        this.oauthAppDetailsModalOverlayTarget.classList.add('opacity-100')
534      }, 10)
535    }
536  }
537
538  // OAuth App Details Modal - Close
539  closeOAuthAppDetailsModal() {
540    if (this.hasOauthAppDetailsModalTarget && this.hasOauthAppDetailsModalOverlayTarget) {
541      this.oauthAppDetailsModalTarget.classList.remove('opacity-100', 'scale-100')
542      this.oauthAppDetailsModalTarget.classList.add('scale-95')
543      this.oauthAppDetailsModalOverlayTarget.classList.remove('opacity-100')
544      setTimeout(() => {
545        this.oauthAppDetailsModalTarget.classList.add('hidden')
546        this.oauthAppDetailsModalOverlayTarget.classList.add('hidden')
547      }, 300)
548    }
549  }
550
551  // Revoke All Tokens Modal - Open
552  openRevokeAllTokensModal(event) {
553    if (this.hasRevokeAllTokensModalTarget && this.hasRevokeAllTokensModalOverlayTarget) {
554      const button = event.currentTarget
555      const appId = button.dataset.appId
556      const appName = button.dataset.appName
557
558      // Llenar el modal con los datos
559      document.getElementById('revoke-all-tokens-app-name').textContent = appName
560      document.getElementById('revoke-all-tokens-app-id').value = appId
561
562      // Configurar el action del formulario
563      const form = document.getElementById('revoke-all-tokens-form')
564      form.action = button.dataset.revokeUrl || '/admin/oauth/applications/' + appId + '/revoke_all'
565
566      // Mostrar modal con animación
567      this.revokeAllTokensModalTarget.classList.remove('hidden')
568      this.revokeAllTokensModalOverlayTarget.classList.remove('hidden')
569      setTimeout(() => {
570        this.revokeAllTokensModalTarget.classList.remove('opacity-0', 'scale-95')
571        this.revokeAllTokensModalTarget.classList.add('opacity-100', 'scale-100')
572        this.revokeAllTokensModalOverlayTarget.classList.add('opacity-100')
573      }, 10)
574    }
575  }
576
577  // Revoke All Tokens Modal - Close
578  closeRevokeAllTokensModal() {
579    if (this.hasRevokeAllTokensModalTarget && this.hasRevokeAllTokensModalOverlayTarget) {
580      this.revokeAllTokensModalTarget.classList.remove('opacity-100', 'scale-100')
581      this.revokeAllTokensModalTarget.classList.add('scale-95')
582      this.revokeAllTokensModalOverlayTarget.classList.remove('opacity-100')
583      setTimeout(() => {
584        this.revokeAllTokensModalTarget.classList.add('hidden')
585        this.revokeAllTokensModalOverlayTarget.classList.add('hidden')
586      }, 300)
587    }
588  }
589
590  // Delete OAuth App Modal - Open
591  openDeleteOAuthAppModal(event) {
592    if (this.hasDeleteOAuthAppModalTarget && this.hasDeleteOAuthAppModalOverlayTarget) {
593      const button = event.currentTarget
594      const appId = button.dataset.appId
595      const appName = button.dataset.appName
596
597      // Llenar el modal con los datos
598      document.getElementById('delete-oauth-app-name').textContent = appName
599
600      // Configurar el action del formulario
601      const form = document.getElementById('delete-oauth-app-form')
602      form.action = button.dataset.deleteUrl || '/oauth/applications/' + appId
603
604      // Mostrar modal con animación
605      this.deleteOAuthAppModalTarget.classList.remove('hidden')
606      this.deleteOAuthAppModalOverlayTarget.classList.remove('hidden')
607      setTimeout(() => {
608        this.deleteOAuthAppModalTarget.classList.remove('opacity-0', 'scale-95')
609        this.deleteOAuthAppModalTarget.classList.add('opacity-100', 'scale-100')
610        this.deleteOAuthAppModalOverlayTarget.classList.add('opacity-100')
611      }, 10)
612    }
613  }
614
615  // Delete OAuth App Modal - Close
616  closeDeleteOAuthAppModal() {
617    if (this.hasDeleteOAuthAppModalTarget && this.hasDeleteOAuthAppModalOverlayTarget) {
618      this.deleteOAuthAppModalTarget.classList.remove('opacity-100', 'scale-100')
619      this.deleteOAuthAppModalTarget.classList.add('scale-95')
620      this.deleteOAuthAppModalOverlayTarget.classList.remove('opacity-100')
621      setTimeout(() => {
622        this.deleteOAuthAppModalTarget.classList.add('hidden')
623        this.deleteOAuthAppModalOverlayTarget.classList.add('hidden')
624      }, 300)
625    }
626  }
627
628  // Doc Modal - Open
629  openDocModal() {
630    if (this.hasDocModalTarget && this.hasDocModalOverlayTarget) {
631      this.docModalTarget.classList.remove('hidden')
632      this.docModalOverlayTarget.classList.remove('hidden')
633      setTimeout(() => {
634        this.docModalTarget.classList.add('opacity-100', 'scale-100')
635        this.docModalTarget.classList.remove('scale-95')
636        this.docModalOverlayTarget.classList.add('opacity-100')
637      }, 10)
638    }
639  }
640
641  // Doc Modal - Close
642  closeDocModal() {
643    if (this.hasDocModalTarget && this.hasDocModalOverlayTarget) {
644      this.docModalTarget.classList.remove('opacity-100', 'scale-100')
645      this.docModalTarget.classList.add('scale-95')
646      this.docModalOverlayTarget.classList.remove('opacity-100')
647      setTimeout(() => {
648        this.docModalTarget.classList.add('hidden')
649        this.docModalOverlayTarget.classList.add('hidden')
650      }, 300)
651    }
652  }
653
654  // Copy to Clipboard
655  copyToClipboard(event) {
656    const button = event.currentTarget
657    const textToCopy = button.dataset.clipboardText
658    const originalText = button.innerHTML
659    const feedbackText = button.dataset.feedbackText || 'Copied!'
660
661    navigator.clipboard.writeText(textToCopy).then(() => {
662      button.innerHTML = feedbackText
663      button.disabled = true
664      setTimeout(() => {
665        button.innerHTML = originalText
666        button.disabled = false
667      }, 2000)
668    })
669  }
670};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.