Просмотр исходного кода

feat: add custom ID editor to shop details page in web UI

Add ability to edit shop custom IDs directly from the web UI at /ui/shops. The new Shop Settings section includes:
- Display of internal ID with copy-to-clipboard functionality
- Editable custom ID field with UUID validation
- Save and clear buttons for custom ID management
- Warning when modifying custom IDs while Qdrant is enabled
- Toast notifications for user feedback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 8 месяцев назад
Родитель
Сommit
c17c4d768f
2 измененных файлов с 172 добавлено и 0 удалено
  1. 171 0
      web/src/components/pages/ShopDetailPage.ts
  2. 1 0
      web/src/utils/icons.ts

+ 171 - 0
web/src/components/pages/ShopDetailPage.ts

@@ -169,6 +169,88 @@ export class ShopDetailPage {
         </div>
       </div>
 
+      <!-- Shop Settings -->
+      <div class="card">
+        <div class="card-header">
+          <h3 class="text-lg font-medium text-gray-900 dark:text-white">Shop Settings</h3>
+          <p class="text-sm text-gray-500 dark:text-gray-400">Configure shop identifiers and metadata</p>
+        </div>
+        <div class="card-body">
+          <div class="space-y-6">
+            <!-- Internal ID -->
+            <div>
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
+                Internal ID
+              </label>
+              <div class="flex items-center space-x-2">
+                <input
+                  type="text"
+                  value="${shop.id}"
+                  readonly
+                  class="flex-1 px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-500 dark:text-gray-400 cursor-not-allowed"
+                />
+                <button
+                  type="button"
+                  id="copy-internal-id"
+                  class="btn btn-secondary"
+                  title="Copy to clipboard"
+                >
+                  <div class="w-4 h-4">${getIcon('copy')}</div>
+                </button>
+              </div>
+              <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
+                Auto-generated UUID, cannot be changed
+              </p>
+            </div>
+
+            <!-- Custom ID -->
+            <div>
+              <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
+                Custom ID
+              </label>
+              <div class="flex items-center space-x-2">
+                <input
+                  type="text"
+                  id="custom-id-input"
+                  value="${shop.custom_id || ''}"
+                  placeholder="e.g., 550e8400-e29b-41d4-a716-446655440000"
+                  class="flex-1 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+                />
+                <button
+                  type="button"
+                  id="save-custom-id"
+                  class="btn btn-primary"
+                >
+                  <div class="w-4 h-4 mr-1">${getIcon('save')}</div>
+                  Save
+                </button>
+                ${shop.custom_id ? `
+                  <button
+                    type="button"
+                    id="clear-custom-id"
+                    class="btn btn-secondary"
+                    title="Clear custom ID"
+                  >
+                    <div class="w-4 h-4">${getIcon('delete')}</div>
+                  </button>
+                ` : ''}
+              </div>
+              <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
+                Optional UUID for custom identification. Must be a valid UUID format.
+              </p>
+              ${shop.qdrant_enabled && shop.custom_id ? `
+                <div class="mt-2 flex items-start space-x-2 text-xs text-yellow-600 dark:text-yellow-400">
+                  <div class="w-4 h-4 flex-shrink-0">${getIcon('warning')}</div>
+                  <span>
+                    Custom ID is linked to Qdrant collections and should not be changed while Qdrant is enabled.
+                  </span>
+                </div>
+              ` : ''}
+            </div>
+          </div>
+        </div>
+      </div>
+
       <!-- Content Overview -->
       <div class="card">
         <div class="card-header">
@@ -492,5 +574,94 @@ export class ShopDetailPage {
         this.toastService.error('Error', 'Failed to clear Qdrant errors');
       }
     });
+
+    // Copy internal ID to clipboard
+    this.element.querySelector('#copy-internal-id')?.addEventListener('click', async () => {
+      if (!this.shopDetail) return;
+
+      try {
+        await navigator.clipboard.writeText(this.shopDetail.shop.id);
+        this.toastService.success('Copied', 'Internal ID copied to clipboard');
+      } catch (error) {
+        this.toastService.error('Copy Failed', 'Failed to copy to clipboard');
+      }
+    });
+
+    // Save custom ID
+    this.element.querySelector('#save-custom-id')?.addEventListener('click', async () => {
+      if (!this.shopDetail) return;
+
+      const input = this.element?.querySelector('#custom-id-input') as HTMLInputElement;
+      if (!input) return;
+
+      const newCustomId = input.value.trim();
+
+      // Validate UUID format if not empty
+      const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+      if (newCustomId && !uuidRegex.test(newCustomId)) {
+        this.toastService.error('Invalid Format', 'Custom ID must be a valid UUID format');
+        return;
+      }
+
+      // Warn if changing custom ID while Qdrant is enabled
+      if (this.shopDetail.shop.qdrant_enabled && this.shopDetail.shop.custom_id && newCustomId !== this.shopDetail.shop.custom_id) {
+        const confirmed = confirm(
+          'Warning: Changing the custom ID while Qdrant is enabled may cause issues with existing collections. ' +
+          'It is recommended to disable Qdrant first. Do you want to continue?'
+        );
+        if (!confirmed) return;
+      }
+
+      try {
+        const response = await this.apiService.updateShopCustomId(
+          this.shopId,
+          newCustomId || null
+        );
+
+        if (response.success) {
+          this.toastService.success(
+            'Custom ID Updated',
+            newCustomId ? `Custom ID set to ${newCustomId}` : 'Custom ID removed'
+          );
+          // Reload the page to show updated data
+          await this.loadShopDetail();
+        } else {
+          this.toastService.error('Update Failed', response.error || 'Failed to update custom ID');
+        }
+      } catch (error) {
+        this.toastService.error('Error', 'Failed to update custom ID');
+      }
+    });
+
+    // Clear custom ID
+    this.element.querySelector('#clear-custom-id')?.addEventListener('click', async () => {
+      if (!this.shopDetail) return;
+
+      // Warn if clearing custom ID while Qdrant is enabled
+      if (this.shopDetail.shop.qdrant_enabled) {
+        const confirmed = confirm(
+          'Warning: Removing the custom ID while Qdrant is enabled may cause issues with existing collections. ' +
+          'It is recommended to disable Qdrant first. Do you want to continue?'
+        );
+        if (!confirmed) return;
+      }
+
+      const confirmClear = confirm('Are you sure you want to clear the custom ID?');
+      if (!confirmClear) return;
+
+      try {
+        const response = await this.apiService.updateShopCustomId(this.shopId, null);
+
+        if (response.success) {
+          this.toastService.success('Custom ID Cleared', 'Custom ID has been removed');
+          // Reload the page to show updated data
+          await this.loadShopDetail();
+        } else {
+          this.toastService.error('Clear Failed', response.error || 'Failed to clear custom ID');
+        }
+      } catch (error) {
+        this.toastService.error('Error', 'Failed to clear custom ID');
+      }
+    });
   }
 }

+ 1 - 0
web/src/utils/icons.ts

@@ -46,6 +46,7 @@ export const icons = {
 
   // Copy
   copy: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>',
+  save: '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg>',
 
   // Arrows
   'arrow-left': '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>',