Răsfoiți Sursa

refactor: update AccessTokenScopeSelect to use selectedScopes prop and optimize scope handling

reiji-h 1 an în urmă
părinte
comite
357b51057d

+ 1 - 1
apps/app/src/client/components/Me/AccessTokenForm.tsx

@@ -115,7 +115,7 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
               {t('page_me_access_token.scope')}
             </label>
             <AccessTokenScopeSelect
-              watch={watch('scopes')}
+              selectedScopes={watch('scopes')}
               register={register('scopes', {
                 required: t('input_validation.message.required', { param: t('page_me_access_token.scope') }),
               })}

+ 8 - 41
apps/app/src/client/components/Me/AccessTokenScopeSelect.tsx

@@ -1,8 +1,8 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useMemo } from 'react';
 
 import type { UseFormRegisterReturn } from 'react-hook-form';
 
-import { parseScopes } from '~/client/util/scope-util';
+import { extractScopes, getDisabledScopes, parseScopes } from '~/client/util/scope-util';
 import { useIsAdmin } from '~/stores-universal/context';
 
 import type { Scope } from '../../../interfaces/scope';
@@ -16,56 +16,23 @@ import { AccessTokenScopeList } from './AccessTokenScopeList';
 type AccessTokenScopeSelectProps = {
   /** React Hook Form's register function for a field named "scopes" */
   register: UseFormRegisterReturn<'scopes'>;
-  watch: string[];
+  selectedScopes: Scope[];
 };
 
 /**
  * Displays a list of permissions in a recursive, nested checkbox interface.
  */
-export const AccessTokenScopeSelect: React.FC<AccessTokenScopeSelectProps> = ({ register, watch }) => {
+export const AccessTokenScopeSelect: React.FC<AccessTokenScopeSelectProps> = ({ register, selectedScopes }) => {
   const [disabledScopes, setDisabledScopes] = useState<Set<Scope>>(new Set());
   const { data: isAdmin } = useIsAdmin();
-  const ScopesMap = parseScopes({ scopes: SCOPE, isAdmin });
 
-  const extractScopes = (obj: Record<string, any>): string[] => {
-    let result: string[] = [];
-
-    Object.values(obj).forEach((value) => {
-      if (typeof value === 'string') {
-        result.push(value);
-      }
-      else if (typeof value === 'object' && !Array.isArray(value)) {
-        result = result.concat(extractScopes(value));
-      }
-    });
-
-    return result;
-  };
-  const Scopes: string[] = extractScopes(ScopesMap);
+  const ScopesMap = useMemo(() => parseScopes({ scopes: SCOPE, isAdmin }), [isAdmin]);
+  const extractedScopes = useMemo(() => extractScopes(ScopesMap), [ScopesMap]);
 
   useEffect(() => {
-    const selectedScopes = watch || [];
-
-    // Create a set of scopes to disable based on prefixes
-    const disabledSet = new Set<Scope>();
-
-    selectedScopes.forEach((scope) => {
-      // Check if the scope is in the form `xxx:*`
-      if (scope.endsWith(':*')) {
-        // Convert something like `read:*` into the prefix `read:`
-        const prefix = scope.replace(':*', ':');
-
-        // Disable all scopes that start with the prefix (but are not the selected scope itself)
-        Scopes.forEach((s:Scope) => {
-          if (s.startsWith(prefix) && s !== scope) {
-            disabledSet.add(s);
-          }
-        });
-      }
-    });
-
+    const disabledSet = getDisabledScopes(selectedScopes, extractedScopes);
     setDisabledScopes(disabledSet);
-  }, [watch]);
+  }, [selectedScopes, extractedScopes]);
 
   return (
     <div className="border rounded">

+ 54 - 0
apps/app/src/client/util/scope-util.ts

@@ -1,4 +1,5 @@
 import type { Scope } from '~/interfaces/scope';
+import { ALL_SIGN } from '~/interfaces/scope';
 
 // Data structure for the final merged scopes
 interface ScopeMap {
@@ -92,3 +93,56 @@ export function parseScopes({ scopes, isAdmin = false }: { scopes: ScopesInput ;
 
   return result;
 }
+
+/**
+ * Determines which scopes should be disabled based on wildcard selections
+ * @param selectedScopes Array of currently selected scopes
+ * @param availableScopes Array of all available scopes
+ * @returns Set of scopes that should be disabled
+ */
+export function getDisabledScopes(selectedScopes: Scope[], availableScopes: string[]): Set<Scope> {
+  const disabledSet = new Set<Scope>();
+
+
+  // If no selected scopes, return empty set
+  if (!selectedScopes || selectedScopes.length === 0) {
+    return disabledSet;
+  }
+
+  selectedScopes.forEach((scope) => {
+    // Check if the scope is in the form `xxx:*`
+    if (scope.endsWith(`:${ALL_SIGN}`)) {
+      // Convert something like `read:*` into the prefix `read:`
+      const prefix = scope.replace(`:${ALL_SIGN}`, ':');
+
+      // Disable all scopes that start with the prefix (but are not the selected scope itself)
+      availableScopes.forEach((s: Scope) => {
+        if (s.startsWith(prefix) && s !== scope) {
+          disabledSet.add(s);
+        }
+      });
+    }
+  });
+
+  return disabledSet;
+}
+
+/**
+ * Extracts all scope strings from a nested ScopeMap object
+ * @param obj The scope object to extract from
+ * @returns Array of all scope strings
+ */
+export function extractScopes(obj: Record<string, any>): string[] {
+  let result: string[] = [];
+
+  Object.values(obj).forEach((value) => {
+    if (typeof value === 'string') {
+      result.push(value);
+    }
+    else if (typeof value === 'object' && !Array.isArray(value)) {
+      result = result.concat(extractScopes(value));
+    }
+  });
+
+  return result;
+}