ソースを参照

fix: handle store_sync_config as object instead of array #100

Supabase returns store_sync_config as a single object (not an array)
due to the UNIQUE constraint on store_id. This fix:

- Adds getSyncConfig() helper to handle both array and object formats
- Updates all accesses to store_sync_config to use the helper
- Fixes optimistic UI updates to maintain object format
- Prevents the switch from reverting after toggle

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

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 5 ヶ月 前
コミット
dd7e832a44
1 ファイル変更57 行追加39 行削除
  1. 57 39
      shopcall.ai-main/src/components/IntegrationsContent.tsx

+ 57 - 39
shopcall.ai-main/src/components/IntegrationsContent.tsx

@@ -41,15 +41,33 @@ interface ConnectedStore {
     connectedAt?: string;
     [key: string]: any;
   };
+  // Note: Supabase returns this as an object (not array) due to UNIQUE constraint on store_id
   store_sync_config?: {
     enabled: boolean;
     sync_frequency: string;
     products_access_policy?: 'sync' | 'api_only' | 'not_allowed';
     customers_access_policy?: 'sync' | 'api_only' | 'not_allowed';
     orders_access_policy?: 'sync' | 'api_only' | 'not_allowed';
+    last_sync_at?: string;
+  } | {
+    enabled: boolean;
+    sync_frequency: string;
+    products_access_policy?: 'sync' | 'api_only' | 'not_allowed';
+    customers_access_policy?: 'sync' | 'api_only' | 'not_allowed';
+    orders_access_policy?: 'sync' | 'api_only' | 'not_allowed';
+    last_sync_at?: string;
   }[];
 }
 
+// Helper function to get sync config from store (handles both array and object formats)
+function getSyncConfig(store: ConnectedStore) {
+  if (!store.store_sync_config) return null;
+  if (Array.isArray(store.store_sync_config)) {
+    return store.store_sync_config[0] || null;
+  }
+  return store.store_sync_config;
+}
+
 export function IntegrationsContent() {
   const { t } = useTranslation();
   const [connectedShops, setConnectedShops] = useState<ConnectedStore[]>([]);
@@ -85,8 +103,9 @@ export function IntegrationsContent() {
         const stores = data.stores || [];
         // Debug: Log the sync config for each store
         stores.forEach((store: ConnectedStore) => {
+          const syncConfig = getSyncConfig(store);
           console.log(`[DEBUG] Store ${store.store_name}:`, {
-            enabled: store.store_sync_config?.[0]?.enabled,
+            enabled: syncConfig?.enabled,
             sync_config: store.store_sync_config
           });
         });
@@ -348,19 +367,17 @@ export function IntegrationsContent() {
     console.log('[DEBUG] Toggle called:', { storeId, storeName, currentEnabled, newEnabled });
 
     // Optimistic UI update - immediately update the local state
+    // Note: Supabase returns store_sync_config as an object (not array) due to UNIQUE constraint
     setConnectedShops(prev => prev.map(shop => {
       if (shop.id === storeId) {
-        // Ensure store_sync_config is always an array with proper structure
-        const currentConfig = Array.isArray(shop.store_sync_config) && shop.store_sync_config.length > 0
-          ? shop.store_sync_config[0]
-          : { enabled: false, sync_frequency: 'hourly' };
+        const currentConfig = getSyncConfig(shop) || { enabled: false, sync_frequency: 'hourly' };
 
         return {
           ...shop,
-          store_sync_config: [{
+          store_sync_config: {
             ...currentConfig,
             enabled: newEnabled
-          }]
+          }
         };
       }
       return shop;
@@ -404,17 +421,14 @@ export function IntegrationsContent() {
       // Revert the optimistic update on error
       setConnectedShops(prev => prev.map(shop => {
         if (shop.id === storeId) {
-          // Ensure store_sync_config is always an array with proper structure
-          const existingConfig = Array.isArray(shop.store_sync_config) && shop.store_sync_config.length > 0
-            ? shop.store_sync_config[0]
-            : { enabled: false, sync_frequency: 'hourly' };
+          const existingConfig = getSyncConfig(shop) || { enabled: false, sync_frequency: 'hourly' };
 
           return {
             ...shop,
-            store_sync_config: [{
+            store_sync_config: {
               ...existingConfig,
               enabled: currentEnabled // Revert to original value
-            }]
+            }
           };
         }
         return shop;
@@ -482,7 +496,7 @@ export function IntegrationsContent() {
 
   const getDataAccessBadges = (shop: ConnectedStore) => {
     // Use the store_sync_config policies instead of the old data_access_permissions
-    const syncConfig = shop.store_sync_config?.[0];
+    const syncConfig = getSyncConfig(shop);
     const productsPolicy = syncConfig?.products_access_policy || 'sync';
     const customersPolicy = syncConfig?.customers_access_policy || 'api_only';
     const ordersPolicy = syncConfig?.orders_access_policy || 'api_only';
@@ -711,7 +725,9 @@ export function IntegrationsContent() {
             </Card>
           ) : (
             <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
-              {connectedShops.map((shop) => (
+              {connectedShops.map((shop) => {
+                const shopSyncConfig = getSyncConfig(shop);
+                return (
                 <Card key={shop.id} className="bg-slate-800 border-slate-700 hover:border-slate-600 transition-all">
                   <CardContent className="p-5">
                     {/* Header Section */}
@@ -770,9 +786,9 @@ export function IntegrationsContent() {
                         <span className="text-xs text-slate-400">{t('integrations.syncStatus')}</span>
                         {getSyncStatusBadge(shop)}
                       </div>
-                      {shop.store_sync_config?.[0]?.last_sync_at && shop.sync_status === 'completed' && (
+                      {shopSyncConfig?.last_sync_at && shop.sync_status === 'completed' && (
                         <div className="text-xs text-slate-500">
-                          {t('integrations.lastSync')}: {new Date(shop.store_sync_config[0].last_sync_at).toLocaleString()}
+                          {t('integrations.lastSync')}: {new Date(shopSyncConfig.last_sync_at).toLocaleString()}
                         </div>
                       )}
                       {shop.sync_error && (
@@ -792,11 +808,11 @@ export function IntegrationsContent() {
                       <div className="text-right">
                         <div className="text-xs text-slate-400 mb-1">{t('integrations.autoSync')}</div>
                         <Switch
-                          checked={shop.store_sync_config?.[0]?.enabled ?? false}
+                          checked={shopSyncConfig?.enabled ?? false}
                           onCheckedChange={() => handleToggleStoreEnabled(
                             shop.id,
                             shop.store_name || 'this store',
-                            shop.store_sync_config?.[0]?.enabled ?? false
+                            shopSyncConfig?.enabled ?? false
                           )}
                           disabled={shop.sync_status === 'syncing'}
                           title={shop.sync_status === 'syncing' ? 'Cannot change while syncing' : 'Toggle background sync'}
@@ -861,7 +877,7 @@ export function IntegrationsContent() {
                     </Button>
                   </CardContent>
                 </Card>
-              ))}
+              );})}
             </div>
           )}
         </div>
@@ -921,7 +937,9 @@ export function IntegrationsContent() {
               {selectedStore && t('integrations.dataAccessDialog.description', { storeName: selectedStore.store_name || 'this store' })}
             </DialogDescription>
           </DialogHeader>
-          {selectedStore && (
+          {selectedStore && (() => {
+            const selectedSyncConfig = getSyncConfig(selectedStore);
+            return (
             <DataAccessSettings
               storeId={selectedStore.id}
               storeName={selectedStore.store_name || 'this store'}
@@ -930,30 +948,30 @@ export function IntegrationsContent() {
                 allow_order_access: true,
                 allow_product_access: true
               }}
-              currentPolicies={selectedStore.store_sync_config?.[0] ? {
-                products_access_policy: selectedStore.store_sync_config[0].products_access_policy || 'sync',
-                customers_access_policy: selectedStore.store_sync_config[0].customers_access_policy || 'api_only',
-                orders_access_policy: selectedStore.store_sync_config[0].orders_access_policy || 'api_only'
+              currentPolicies={selectedSyncConfig ? {
+                products_access_policy: selectedSyncConfig.products_access_policy || 'sync',
+                customers_access_policy: selectedSyncConfig.customers_access_policy || 'api_only',
+                orders_access_policy: selectedSyncConfig.orders_access_policy || 'api_only'
               } : undefined}
               onPermissionsUpdated={handlePermissionsUpdated}
               onPoliciesUpdated={(newPolicies) => {
                 // Update the store in the list with the new policies
-                setConnectedShops(prev => prev.map(shop =>
-                  shop.id === selectedStore?.id
-                    ? {
-                        ...shop,
-                        store_sync_config: [{
-                          ...(shop.store_sync_config?.[0] || { enabled: true, sync_frequency: 'hourly' }),
-                          products_access_policy: newPolicies.products_access_policy,
-                          customers_access_policy: newPolicies.customers_access_policy,
-                          orders_access_policy: newPolicies.orders_access_policy
-                        }]
-                      }
-                    : shop
-                ));
+                setConnectedShops(prev => prev.map(shop => {
+                  if (shop.id !== selectedStore?.id) return shop;
+                  const currentConfig = getSyncConfig(shop) || { enabled: true, sync_frequency: 'hourly' };
+                  return {
+                    ...shop,
+                    store_sync_config: {
+                      ...currentConfig,
+                      products_access_policy: newPolicies.products_access_policy,
+                      customers_access_policy: newPolicies.customers_access_policy,
+                      orders_access_policy: newPolicies.orders_access_policy
+                    }
+                  };
+                }));
               }}
             />
-          )}
+          );})()}
           <div className="flex justify-end pt-4 border-t border-slate-700">
             <Button
               variant="outline"