- Add defer attribute to <script src="/static/main.js"> in 21 template files - Improves page load performance by allowing HTML parsing to continue without blocking - Maintains script execution order while enabling parallel resource loading - Remove duplicate defer attribute from header.templ line 264 - Add websocket.ts import to main.ts for module registration This optimization reduces page render blocking and improves perceived load times by allowing the browser to continue parsing HTML while the main.js bundle loads. The defer attribute ensures scripts execute in order after HTML parsing completes. Affected templates include: - Admin pages: admin, admin_library, admin_settings, admin_users - Content pages: analytics, bookshelf, collections, conflicts, custom_section - User pages: dashboard, devices, docs, index, login, profile, progress, queue, register - System pages: header, unlinked_books
479 lines
22 KiB
Templ
479 lines
22 KiB
Templ
package templates
|
||
|
||
import "bookhoard/internal/handlers"
|
||
|
||
templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData, errorMessage string, baseURL string) {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>Device Management - Bookhoard</title>
|
||
<script src="/static/htmx.min.js"></script>
|
||
<script src="/static/main.js" defer></script>
|
||
<link href="/static/style.css" rel="stylesheet"/>
|
||
</head>
|
||
<body class="theme-{ user.Theme }" x-data="devices" x-init="setupEventDelegation()">
|
||
@Header(user, "/devices")
|
||
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||
<!-- Header Section -->
|
||
<div class="mb-8">
|
||
<div class="flex justify-between items-center">
|
||
<div>
|
||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Device Management</h1>
|
||
<p style="color: var(--text-secondary)">Manage your reading devices and sync settings</p>
|
||
</div>
|
||
<button @click="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||
➕ Add New Device
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- Devices Grid -->
|
||
<div id="devices-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
<!-- Empty state -->
|
||
if len(devices) == 0 {
|
||
<div id="empty-state" class="text-center py-16 col-span-full" style="color: var(--text-secondary)">
|
||
<div class="text-6xl mb-4">📱</div>
|
||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Devices Yet</h3>
|
||
<p class="mb-4">Add your reading devices to enable cross-device sync</p>
|
||
<button @click="showAddDeviceModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||
Add Your First Device
|
||
</button>
|
||
</div>
|
||
}
|
||
<!-- Devices Grid -->
|
||
if len(devices) > 0 {
|
||
<div id="devices-grid" class="grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
for _, device := range devices {
|
||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||
<div class="flex items-start justify-between mb-4">
|
||
<div class="text-4xl">
|
||
if device.DeviceType == "koreader" {
|
||
📖
|
||
} else if device.DeviceType == "kobo" {
|
||
📚
|
||
} else if device.DeviceType == "web" {
|
||
🌐
|
||
} else {
|
||
📱
|
||
}
|
||
</div>
|
||
<div class="flex space-x-2">
|
||
<button @click="showShelfMappings('{ device.ID }')" class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||
📚
|
||
</button>
|
||
<button @click="showDeviceSettings('{ device.ID }')" class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||
⚙️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<h3 class="text-lg font-semibold mb-1" style="color: var(--text-primary)">{ device.DeviceName }</h3>
|
||
<p class="text-sm mb-4" style="color: var(--text-secondary)">{ device.DeviceType }</p>
|
||
<div class="space-y-2 text-sm mb-4">
|
||
<div class="flex justify-between">
|
||
<span style="color: var(--text-secondary)">Sync Status</span>
|
||
if device.SyncEnabled {
|
||
<span style="color: var(--accent)">✓ Enabled</span>
|
||
} else {
|
||
<span style="color: var(--text-secondary)">✗ Disabled</span>
|
||
}
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span style="color: var(--text-secondary)">Last Sync</span>
|
||
if device.LastSync != nil {
|
||
<span style="color: var(--text-primary)">{ device.LastSync.Format("2006-01-02 15:04") }</span>
|
||
} else {
|
||
<span style="color: var(--text-primary)">Never</span>
|
||
}
|
||
</div>
|
||
<div class="flex justify-between">
|
||
<span style="color: var(--text-secondary)">Last Seen</span>
|
||
if device.LastSeen != nil {
|
||
<span style="color: var(--text-primary)">{ device.LastSeen.Format("2006-01-02 15:04") }</span>
|
||
} else {
|
||
<span style="color: var(--text-primary)">Never</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
<!-- NEW: Sync URL & Token Management -->
|
||
<div class="border-t pt-4" style="border-color: var(--border);">
|
||
<p class="text-xs font-semibold mb-2" style="color: var(--text-secondary)">DEVICE SYNC CONFIGURATION</p>
|
||
if device.DeviceType == "kobo" {
|
||
<!-- Kobo: Copy Full Sync URL -->
|
||
<div class="mb-3">
|
||
<label class="block text-xs mb-1" style="color: var(--text-secondary)">Kobo Sync URL</label>
|
||
<div class="flex space-x-2">
|
||
<input
|
||
type="text"
|
||
id="sync-url-{ device.ID }"
|
||
readonly
|
||
value={ baseURL + "/api/sync/kobo/" + device.AuthToken }
|
||
class="flex-1 px-3 py-2 text-xs rounded border"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
/>
|
||
<button
|
||
@click="copyToClipboard(document.getElementById('sync-url-{ device.ID }').value, 'Kobo sync URL')"
|
||
class="px-3 py-2 text-xs rounded hover:opacity-80"
|
||
style="background-color: var(--accent); color: white;"
|
||
>
|
||
📋 Copy
|
||
</button>
|
||
</div>
|
||
<p class="text-xs mt-1" style="color: var(--text-secondary);">Paste this URL into Kobo's <code class="px-1 py-0.5 rounded" style="background-color: var(--bg-primary);">api_endpoint</code> setting</p>
|
||
</div>
|
||
}
|
||
if device.DeviceType == "koreader" {
|
||
<!-- KOReader: Copy Auth Token -->
|
||
<div class="mb-3">
|
||
<label class="block text-xs mb-1" style="color: var(--text-secondary)">Auth Token (for plugin)</label>
|
||
<div class="flex space-x-2">
|
||
<input
|
||
type="text"
|
||
id="auth-token-{ device.ID }"
|
||
readonly
|
||
value={ device.AuthToken }
|
||
class="flex-1 px-3 py-2 text-xs rounded border"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border); font-family: monospace;"
|
||
/>
|
||
<button
|
||
@click="copyToClipboard(document.getElementById('auth-token-{ device.ID }').value, 'Auth token')"
|
||
class="px-3 py-2 text-xs rounded hover:opacity-80"
|
||
style="background-color: var(--accent); color: white;"
|
||
>
|
||
📋 Copy
|
||
</button>
|
||
</div>
|
||
<p class="text-xs mt-1" style="color: var(--text-secondary);">Enter this token in the KOReader plugin settings</p>
|
||
</div>
|
||
}
|
||
<!-- Regenerate Token Button -->
|
||
<button
|
||
hx-put={ "/api/devices/" + device.ID.String() + "/regenerate-token" }
|
||
hx-confirm="⚠️ Old token will immediately stop working. Regenerate anyway?"
|
||
class="w-full px-3 py-2 text-xs rounded border hover:opacity-80"
|
||
style="border-color: var(--border); color: var(--text-secondary); background-color: var(--bg-primary);"
|
||
>
|
||
🔄 Regenerate Token
|
||
</button>
|
||
<p class="text-xs mt-1" style="color: var(--text-secondary);">⚠️ Old token will immediately stop working</p>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
<!-- Pending Registrations -->
|
||
if len(pendingRegistrations) > 0 {
|
||
<div id="pending-section" class="mt-12">
|
||
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Pending Device Registrations</h2>
|
||
<div class="space-y-4">
|
||
for _, reg := range pendingRegistrations {
|
||
<div class="card p-4 rounded-lg border flex items-center justify-between" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||
<div>
|
||
<h3 class="font-semibold" style="color: var(--text-primary)">{ reg.DeviceName }</h3>
|
||
<p class="text-sm" style="color: var(--text-secondary)">
|
||
{ reg.DeviceType } - Expires in { reg.ExpiresAt }
|
||
</p>
|
||
</div>
|
||
<div class="flex space-x-2">
|
||
<button @click="approveDevice('{ reg.RegistrationID }')" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
||
Approve
|
||
</button>
|
||
<button @click="rejectDevice('{ reg.RegistrationID }')" class="btn-danger px-4 py-2 rounded-lg text-sm">
|
||
Reject
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
<!-- Sync Queue Section -->
|
||
<div id="sync-queue-section" class="mt-12 hidden">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Sync Queue</h2>
|
||
<button @click="clearSyncQueue()" class="btn-secondary px-4 py-2 rounded-lg">
|
||
Clear Queue
|
||
</button>
|
||
</div>
|
||
<div id="sync-queue" class="space-y-3"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Add Device Modal -->
|
||
<div id="add-device-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add New Device</h2>
|
||
<button @click="hideAddDeviceModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||
</div>
|
||
<form id="add-device-form" @submit="handleAddDevice($event)">
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label>
|
||
<input
|
||
type="text"
|
||
id="device-name"
|
||
required
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
placeholder="My Kindle Paperwhite"
|
||
/>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Type</label>
|
||
<select
|
||
id="device-type"
|
||
required
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
>
|
||
<option value="">Select device type...</option>
|
||
<option value="koreader">KOReader (Kindle, Kobo, PocketBook)</option>
|
||
<option value="kobo">Kobo E-Reader</option>
|
||
<option value="web">Web Browser</option>
|
||
<option value="mobile">Mobile App</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Identifier</label>
|
||
<input
|
||
type="text"
|
||
id="device-identifier"
|
||
required
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
placeholder="Hardware ID or Serial Number"
|
||
/>
|
||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Enter your device's unique identifier</p>
|
||
</div>
|
||
<div class="flex justify-end space-x-3">
|
||
<button type="button" @click="hideAddDeviceModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||
Cancel
|
||
</button>
|
||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||
Register Device
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<!-- Device Settings Modal -->
|
||
<div id="device-settings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Device Settings</h2>
|
||
<button @click="hideDeviceSettingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||
</div>
|
||
<form id="device-settings-form" @submit="handleSaveDeviceSettings($event)">
|
||
<input type="hidden" id="settings-device-id"/>
|
||
<input type="hidden" id="settings-device-type"/>
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Name</label>
|
||
<input
|
||
type="text"
|
||
id="settings-device-name"
|
||
required
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
/>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="flex items-center space-x-3 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
id="settings-sync-enabled"
|
||
checked
|
||
class="w-5 h-5 rounded"
|
||
/>
|
||
<span style="color: var(--text-primary)">Enable Sync</span>
|
||
</label>
|
||
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Allow this device to sync reading progress</p>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="flex items-center space-x-3 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
id="settings-auto-sync"
|
||
checked
|
||
class="w-5 h-5 rounded"
|
||
/>
|
||
<span style="color: var(--text-primary)">Auto Sync</span>
|
||
</label>
|
||
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Automatically sync changes</p>
|
||
</div>
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Frequency</label>
|
||
<select
|
||
id="settings-sync-frequency"
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
>
|
||
<option value="1">Every 1 minute</option>
|
||
<option value="5" selected>Every 5 minutes</option>
|
||
<option value="15">Every 15 minutes</option>
|
||
<option value="30">Every 30 minutes</option>
|
||
<option value="60">Every hour</option>
|
||
</select>
|
||
</div>
|
||
<div class="border-t pt-6 mb-6" style="border-color: var(--border);">
|
||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Collection View Settings</h3>
|
||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Configure how collections are displayed on this device</p>
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Default View Mode</label>
|
||
<select
|
||
id="settings-view-mode"
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
>
|
||
<option value="grid">Grid View</option>
|
||
<option value="list">List View</option>
|
||
<option value="compact">Compact View</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sort Collections By</label>
|
||
<select
|
||
id="settings-sort-order"
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
>
|
||
<option value="name">Collection Name</option>
|
||
<option value="created">Date Created</option>
|
||
<option value="book_count">Book Count</option>
|
||
<option value="recent">Recently Added</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Items Per Page</label>
|
||
<select
|
||
id="settings-items-per-page"
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
>
|
||
<option value="12">12 items</option>
|
||
<option value="24" selected>24 items</option>
|
||
<option value="48">48 items</option>
|
||
<option value="96">96 items</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="flex items-center space-x-3 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
id="settings-show-covers"
|
||
checked
|
||
class="w-5 h-5 rounded"
|
||
/>
|
||
<span style="color: var(--text-primary)">Show Cover Images</span>
|
||
</label>
|
||
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Display book covers in collection views</p>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="flex items-center space-x-3 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
id="settings-show-progress"
|
||
class="w-5 h-5 rounded"
|
||
/>
|
||
<span style="color: var(--text-primary)">Show Reading Progress</span>
|
||
</label>
|
||
<p class="text-xs mt-1 ml-8" style="color: var(--text-secondary)">Display progress indicators for books</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex justify-end space-x-3">
|
||
<button type="button" @click="hideDeviceSettingsModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||
Cancel
|
||
</button>
|
||
<button type="button" @click="handleRevokeDevice()" class="btn-danger px-4 py-2 rounded-lg">
|
||
Revoke Device
|
||
</button>
|
||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||
Save Settings
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<!-- Device Shelf Mappings Modal -->
|
||
<div id="shelf-mappings-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||
<div class="card rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Collection to Shelf Mappings</h2>
|
||
<button @click="hideShelfMappingsModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||
</div>
|
||
<input type="hidden" id="mappings-device-id"/>
|
||
<div id="shelf-mappings-container" class="space-y-4 mb-6">
|
||
<p style="color: var(--text-secondary)">Loading mappings...</p>
|
||
</div>
|
||
<div class="flex justify-end space-x-3">
|
||
<button type="button" @click="hideShelfMappingsModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||
Close
|
||
</button>
|
||
<button type="button" @click="showAddMappingModal()" class="btn-primary px-4 py-2 rounded-lg">
|
||
➕ Add Mapping
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Add/Edit Shelf Mapping Modal -->
|
||
<div id="add-mapping-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center" style="background-color: rgba(0, 0, 0, 0.7);">
|
||
<div class="card rounded-lg p-6 w-full max-w-md mx-4" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Add Collection Mapping</h2>
|
||
<button @click="hideAddMappingModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
||
</div>
|
||
<form id="mapping-form" @submit="handleSaveMapping($event)">
|
||
<input type="hidden" id="mapping-device-id"/>
|
||
<input type="hidden" id="mapping-id"/>
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Collection</label>
|
||
<select
|
||
id="mapping-collection"
|
||
required
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
>
|
||
<option value="">Select collection...</option>
|
||
</select>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Device Shelf Name</label>
|
||
<input
|
||
type="text"
|
||
id="mapping-shelf-name"
|
||
required
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
placeholder="e.g., Sci-Fi"
|
||
/>
|
||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Name of the shelf on this device</p>
|
||
</div>
|
||
<div class="mb-6">
|
||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Sync Direction</label>
|
||
<select
|
||
id="mapping-sync-direction"
|
||
required
|
||
class="w-full px-4 py-2 border rounded-lg"
|
||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||
>
|
||
<option value="bidirectional">Bidirectional (sync both ways)</option>
|
||
<option value="book_to_device">Bookhoard → Device</option>
|
||
<option value="device_to_book">Device → Bookhoard</option>
|
||
<option value="none">No sync</option>
|
||
</select>
|
||
</div>
|
||
<div class="flex justify-end space-x-3">
|
||
<button type="button" @click="hideAddMappingModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
||
Cancel
|
||
</button>
|
||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||
Save Mapping
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
@ErrorToast(errorMessage)
|
||
</body>
|
||
</html>
|
||
}
|