diff --git a/src/custom/ResourceDetailFormatters/index.ts b/src/custom/ResourceDetailFormatters/index.ts index bd66d708b..c67d58a90 100644 --- a/src/custom/ResourceDetailFormatters/index.ts +++ b/src/custom/ResourceDetailFormatters/index.ts @@ -14,7 +14,7 @@ import { TableDataFormatter, TextWithLinkFormatter } from './Formatter'; -import { useResourceCleanData } from './useResourceCleanData'; +// Note: useResourceCleanData has been moved to src/hooks/data/ import { convertToReadableUnit, extractPodVolumnTables, splitCamelCaseString } from './utils'; export { @@ -35,6 +35,5 @@ export { splitCamelCaseString, StatusFormatter, TableDataFormatter, - TextWithLinkFormatter, - useResourceCleanData + TextWithLinkFormatter }; diff --git a/src/custom/Workspaces/index.ts b/src/custom/Workspaces/index.ts index 52c295604..0f466a3b2 100644 --- a/src/custom/Workspaces/index.ts +++ b/src/custom/Workspaces/index.ts @@ -7,10 +7,7 @@ import WorkspaceEnvironmentSelection from './WorkspaceEnvironmentSelection'; import WorkspaceRecentActivityModal from './WorkspaceRecentActivityModal'; import WorkspaceTeamsTable from './WorkspaceTeamsTable'; import WorkspaceViewsTable from './WorkspaceViewsTable'; -import useDesignAssignment from './hooks/useDesignAssignment'; -import useEnvironmentAssignment from './hooks/useEnvironmentAssignment'; -import useTeamAssignment from './hooks/useTeamAssignment'; -import useViewAssignment from './hooks/useViewsAssignment'; +// Note: Workspace hooks have been moved to src/hooks/workspace/ import { L5DeleteIcon, L5EditIcon } from './styles'; export { @@ -19,10 +16,6 @@ export { EnvironmentTable, L5DeleteIcon, L5EditIcon, - useDesignAssignment, - useEnvironmentAssignment, - useTeamAssignment, - useViewAssignment, WorkspaceCard, WorkspaceContentMoveModal, WorkspaceEnvironmentSelection, diff --git a/src/custom/index.tsx b/src/custom/index.tsx index 8cea123bb..182c10615 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -31,8 +31,7 @@ import { import { FeedbackButton } from './Feedback'; import { FlipCard, FlipCardProps } from './FlipCard'; import { FormatId } from './FormatId'; -import { useWindowDimensions } from './Helpers/Dimension'; -import { useNotificationHandler } from './Helpers/Notification'; +// Note: useWindowDimensions and useNotificationHandler have been moved to src/hooks/ import { ColView, updateVisibleColumns } from './Helpers/ResponsiveColumns/responsive-coulmns.tsx'; import { LearningCard } from './LearningCard'; import { BasicMarkdown, RenderMarkdown } from './Markdown'; @@ -116,8 +115,6 @@ export { UsersTable, VisibilityChipMenu, updateVisibleColumns, - useNotificationHandler, - useWindowDimensions, withErrorBoundary, withSuppressedErrorBoundary }; diff --git a/src/hooks/README.md b/src/hooks/README.md new file mode 100644 index 000000000..4e89c408f --- /dev/null +++ b/src/hooks/README.md @@ -0,0 +1,118 @@ +# Sistent Hooks + +This directory contains all reusable custom React hooks for the Sistent design system. The hooks are organized into logical categories for better maintainability and discoverability. + +## Directory Structure + +``` +src/hooks/ +├── data/ # Data management and API-related hooks +├── ui/ # UI interaction and state management hooks +├── utils/ # General utility hooks +├── workspace/ # Workspace-specific hooks +└── index.ts # Main exports +``` + +## Categories + +### Data Hooks (`./data/`) + +Hooks for managing data, API interactions, and data processing. + +- `useRoomActivity` - WebSocket-based collaboration for room activity tracking +- `useResourceCleanData` - Resource data formatting and processing for Kubernetes resources + +### UI Hooks (`./ui/`) + +Hooks for managing UI state, interactions, and visual components. + +- `useWindowDimensions` - Window dimension tracking with debounced resize handling +- `useNotification` - Notification management using notistack + +### Utility Hooks (`./utils/`) + +General-purpose utility hooks for common patterns. + +- `useDebounce` - Debounce values with configurable delay +- `usePreventPageLeave` - Prevent users from leaving pages with unsaved changes +- `useLocalStorage` - Manage localStorage with React state synchronization +- `useToggle` - Simple boolean state toggle management +- `useTimeout` - Manage timeouts with automatic cleanup + +### Workspace Hooks (`./workspace/`) + +Hooks specific to workspace functionality and management. + +- `useDesignAssignment` - Design assignment management for workspaces +- `useEnvironmentAssignment` - Environment assignment management for workspaces +- `useTeamAssignment` - Team assignment management for workspaces +- `useViewsAssignment` - Views assignment management for workspaces + +## Usage + +All hooks are exported from the main hooks index file and can be imported directly: + +```typescript +import { + useDebounce, + useWindowDimensions, + useNotification, + useRoomActivity +} from '@layer5/sistent'; +``` + +Or import specific categories: + +```typescript +import { useDebounce, useToggle } from '@layer5/sistent'; +``` + +## Adding New Hooks + +When adding new hooks, please follow these guidelines: + +1. **Categorization**: Place hooks in the appropriate directory based on their primary function +2. **Naming**: Use descriptive names starting with "use" following React conventions +3. **TypeScript**: Provide full type definitions for parameters and return values +4. **Documentation**: Include JSDoc comments describing the hook's purpose and usage +5. **Exports**: Add the hook to the appropriate category's index.ts file + +## Examples + +### Basic Usage + +```typescript +import { useDebounce, useToggle } from '@layer5/sistent'; + +function MyComponent() { + const [searchTerm, setSearchTerm] = useState(''); + const [isOpen, toggleOpen] = useToggle(false); + const debouncedSearchTerm = useDebounce(searchTerm, 300); + + // Use debouncedSearchTerm for API calls + // Use toggleOpen for modal state +} +``` + +### Advanced Usage + +```typescript +import { useLocalStorage, usePreventPageLeave } from '@layer5/sistent'; + +function FormComponent() { + const [formData, setFormData] = useLocalStorage('formData', {}); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + + usePreventPageLeave(hasUnsavedChanges, 'You have unsaved changes!'); + + // Form logic here +} +``` + +## Best Practices + +1. **Keep hooks focused**: Each hook should have a single, well-defined responsibility +2. **Use TypeScript**: Always provide type definitions for better developer experience +3. **Handle cleanup**: Ensure proper cleanup of effects, timers, and event listeners +4. **Test thoroughly**: Write comprehensive tests for all hooks +5. **Document edge cases**: Include documentation for error handling and edge cases diff --git a/src/hooks/data/index.ts b/src/hooks/data/index.ts new file mode 100644 index 000000000..315cdba3c --- /dev/null +++ b/src/hooks/data/index.ts @@ -0,0 +1,2 @@ +export { useResourceCleanData } from './useResourceCleanData'; +export { useRoomActivity } from './useRoomActivity'; diff --git a/src/hooks/data/useResourceCleanData.ts b/src/hooks/data/useResourceCleanData.ts new file mode 100644 index 000000000..395601d57 --- /dev/null +++ b/src/hooks/data/useResourceCleanData.ts @@ -0,0 +1,187 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import _ from 'lodash'; +import moment from 'moment'; +import { + GetResourceCleanDataProps, + NumberState +} from '../../custom/ResourceDetailFormatters/types'; + +export const useResourceCleanData = () => { + const structureNumberStates = (parsedStatus: any, parsedSpec: any): NumberState[] => { + const numberStates: NumberState[] = []; + + if (parsedSpec?.priority !== undefined) { + numberStates.push({ + title: 'Priority', + value: parsedSpec.priority, + quantity: '' + }); + } + + if (parsedSpec?.containers) { + numberStates.push({ + title: 'Containers', + value: parsedSpec.containers.length, + quantity: 'total' + }); + } + + if (parsedStatus?.containerStatuses) { + const totalRestarts = parsedStatus.containerStatuses.reduce( + (sum: number, container: { restartCount?: number }) => sum + (container.restartCount || 0), + 0 + ); + numberStates.push({ + title: 'Total Restarts', + value: totalRestarts, + quantity: 'times' + }); + } + + return numberStates; + }; + + const getAge = (creationTimestamp?: string): string | undefined => { + if (!creationTimestamp) return undefined; + const creationTime = moment(creationTimestamp); + const currentTime = moment(); + const ageInHours = currentTime.diff(creationTime, 'hours'); + return ageInHours >= 24 ? `${Math.floor(ageInHours / 24)} days` : `${ageInHours} hours`; + }; + + const getStatus = (attribute: any): string | false => { + if (attribute?.phase) { + return attribute.phase; + } + const readyCondition = attribute?.conditions?.find( + (cond: { type: string }) => cond.type === 'Ready' + ); + return readyCondition ? 'Ready' : false; + }; + + const joinwithEqual = (object: Record | undefined): string[] => { + if (!object) return []; + return Object.entries(object).map(([key, value]) => { + return `${key}=${value}`; + }); + }; + + const getResourceCleanData = ({ + resource, + activeLabels, + dispatchMsgToEditor, + router, + showStatus = true, + container + }: GetResourceCleanDataProps) => { + const parsedStatus = resource?.status?.attribute && JSON.parse(resource?.status?.attribute); + const parsedSpec = resource?.spec?.attribute && JSON.parse(resource?.spec.attribute); + const numberStates = structureNumberStates(parsedStatus, parsedSpec); + const kind = resource?.kind ?? resource?.component?.kind; + const cleanData = { + container: container, + age: getAge(resource?.metadata?.creationTimestamp), + kind: kind, + status: showStatus && getStatus(parsedStatus), + kubeletVersion: parsedStatus?.nodeInfo?.kubeletVersion, + podIP: parsedStatus?.podIP, + hostIP: parsedStatus?.hostIP, + QoSClass: parsedStatus?.qosClass, + size: parsedSpec?.resources?.requests?.storage, + claim: parsedSpec?.claimRef?.name, + claimNamespace: parsedSpec?.claimRef?.namespace, + apiVersion: resource?.apiVersion, + pods: + parsedStatus?.replicas === undefined + ? parsedStatus?.availableReplicas?.toString() + : `${ + parsedStatus?.availableReplicas?.toString() ?? '0' + } / ${parsedStatus?.replicas?.toString()}`, + replicas: + parsedStatus?.readyReplicas !== undefined && + parsedStatus?.replicas !== undefined && + `${parsedStatus?.readyReplicas} / ${parsedStatus?.replicas}`, + strategyType: resource?.configuration?.spec?.strategy?.type, + storageClass: parsedSpec?.storageClassName, + secretType: resource?.type, + serviceType: parsedSpec?.type, + clusterIp: parsedSpec?.clusterIP, + updateStrategy: parsedSpec?.updateStrategy?.type, + externalIp: parsedSpec?.externalIPs, + finalizers: parsedSpec?.finalizers, + accessModes: parsedSpec?.accessModes, + deeplinks: { + links: [ + { nodeName: parsedSpec?.nodeName, label: 'Node' }, + { namespace: resource?.metadata?.namespace, label: 'Namespace' }, + { + serviceAccount: parsedSpec?.serviceAccountName, + label: 'ServiceAccount', + resourceCategory: 'Security' + } + ], + router: router, + dispatchMsgToEditor: dispatchMsgToEditor + }, + selector: parsedSpec?.selector?.matchLabels + ? joinwithEqual(parsedSpec?.selector?.matchLabels) + : joinwithEqual(parsedSpec?.selector), + images: parsedSpec?.template?.spec?.containers?.map((container: { image?: string }) => { + return container?.image; + }), + numberStates: numberStates, + nodeSelector: + joinwithEqual(parsedSpec?.nodeSelector) || + joinwithEqual(parsedSpec?.template?.spec?.nodeSelector), + loadBalancer: parsedStatus?.loadBalancer?.ingress?.map((ingress: { ip?: string }) => { + return ingress?.ip; + }), + rules: parsedSpec?.rules?.map((rule: { host?: string }) => { + return rule?.host; + }), + usage: { + allocatable: parsedStatus?.allocatable, + capacity: parsedStatus?.capacity + }, + configData: resource?.configuration?.data, + capacity: parsedSpec?.capacity?.storage, + totalCapacity: parsedStatus?.capacity, + totalAllocatable: parsedStatus?.allocatable, + conditions: { + ...parsedStatus?.conditions?.map((condition: { type?: string }) => { + return condition?.type; + }) + }, + tolerations: parsedSpec?.tolerations, + podVolumes: parsedSpec?.volumes, + ingressRules: parsedSpec?.rules, + connections: kind === 'Service' && _.omit(parsedSpec, ['selector', 'type']), + labels: { + data: resource?.metadata?.labels?.map((label) => { + const value = label?.value !== undefined ? label?.value : ''; + return `${label?.key}=${value}`; + }), + dispatchMsgToEditor: dispatchMsgToEditor, + activeViewFilters: activeLabels + }, + annotations: resource?.metadata?.annotations?.map((annotation) => { + const value = annotation?.value !== undefined ? annotation?.value : ''; + return `${annotation?.key}=${value}`; + }), + // secret: resource?.data, //TODO: show it when we have the role based access control for secrets + initContainers: parsedSpec?.initContainers && + parsedStatus?.initContainerStatuses && { + spec: parsedSpec?.initContainers, + status: parsedStatus?.initContainerStatuses + }, + containers: parsedSpec?.containers && + parsedStatus?.containerStatuses && { + spec: parsedSpec?.containers, + status: parsedStatus?.containerStatuses + } + }; + return cleanData; + }; + + return { getResourceCleanData, structureNumberStates, getAge, getStatus, joinwithEqual }; +}; diff --git a/src/hooks/useRoomActivity.ts b/src/hooks/data/useRoomActivity.ts similarity index 99% rename from src/hooks/useRoomActivity.ts rename to src/hooks/data/useRoomActivity.ts index 361f1dd20..06227f92b 100644 --- a/src/hooks/useRoomActivity.ts +++ b/src/hooks/data/useRoomActivity.ts @@ -6,7 +6,7 @@ import { MESHERY_CLOUD_STAGING, MESHERY_CLOUD_WS_PROD, MESHERY_CLOUD_WS_STAGING -} from '../constants/constants'; +} from '../../constants/constants'; interface UserProfile { [key: string]: unknown; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 855448bb6..3e54d17c5 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1 +1,11 @@ -export * from './useRoomActivity'; +// Data hooks +export * from './data'; + +// UI hooks +export * from './ui'; + +// Workspace hooks +export * from './workspace'; + +// Utility hooks +export * from './utils'; diff --git a/src/hooks/ui/index.ts b/src/hooks/ui/index.ts new file mode 100644 index 000000000..0c5abf51c --- /dev/null +++ b/src/hooks/ui/index.ts @@ -0,0 +1,2 @@ +export { default as useNotification } from './useNotification'; +export { useWindowDimensions } from './useWindowSize'; diff --git a/src/hooks/ui/useNotification.ts b/src/hooks/ui/useNotification.ts new file mode 100644 index 000000000..446011303 --- /dev/null +++ b/src/hooks/ui/useNotification.ts @@ -0,0 +1,27 @@ +import { OptionsObject, useSnackbar } from 'notistack'; +import React from 'react'; + +type NotificationHandler = (message: string, options?: OptionsObject) => void; + +const useNotificationHandler = (): NotificationHandler => { + const [message, setMessage] = React.useState(''); + const { enqueueSnackbar } = useSnackbar(); + + React.useEffect(() => { + if (message) { + enqueueSnackbar(message); + setMessage(''); + } + }, [message, enqueueSnackbar]); + + const notify: NotificationHandler = (message, options) => { + setMessage(message); + if (options) { + enqueueSnackbar(message, options); + } + }; + + return notify; +}; + +export default useNotificationHandler; diff --git a/src/hooks/ui/useWindowSize.ts b/src/hooks/ui/useWindowSize.ts new file mode 100644 index 000000000..e396e9896 --- /dev/null +++ b/src/hooks/ui/useWindowSize.ts @@ -0,0 +1,56 @@ +import React from 'react'; + +/** + * Returns the width and height of the window. + * + * @returns {WindowDimensions} { width, height } + */ +function getWindowDimensions(): WindowDimensions { + const { innerWidth: width, innerHeight: height } = window; + return { + width, + height + }; +} + +/** + * Custom hook for getting window dimensions. + * + * @returns {WindowDimensions} { width, height } + */ +export function useWindowDimensions(): WindowDimensions { + const [windowDimensions, setWindowDimensions] = React.useState(getWindowDimensions()); + + React.useEffect(() => { + let resizeTimeout: number; + + function handleResize() { + if (resizeTimeout) { + clearTimeout(resizeTimeout); + } + resizeTimeout = window.setTimeout(() => { + setWindowDimensions(getWindowDimensions()); + }, 500); + } + + window.addEventListener('resize', handleResize); + + // Clean up the event listener on unmount + return () => { + window.removeEventListener('resize', handleResize); + if (resizeTimeout) { + clearTimeout(resizeTimeout); + } + }; + }, []); + + return windowDimensions; +} + +/** + * Represents the width and height of the window. + */ +interface WindowDimensions { + width: number; + height: number; +} diff --git a/src/hooks/utils/index.ts b/src/hooks/utils/index.ts new file mode 100644 index 000000000..61b389b93 --- /dev/null +++ b/src/hooks/utils/index.ts @@ -0,0 +1,5 @@ +export { useDebounce } from './useDebounce'; +export { useLocalStorage } from './useLocalStorage'; +export { usePreventPageLeave } from './usePreventPageLeave'; +export { useTimeout } from './useTimeout'; +export { useToggle } from './useToggle'; diff --git a/src/hooks/utils/useDebounce.ts b/src/hooks/utils/useDebounce.ts new file mode 100644 index 000000000..4280c0738 --- /dev/null +++ b/src/hooks/utils/useDebounce.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from 'react'; + +/** + * Custom hook for debouncing values + * @param value - The value to debounce + * @param delay - The debounce delay in milliseconds + * @returns The debounced value + */ +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/src/hooks/utils/useLocalStorage.ts b/src/hooks/utils/useLocalStorage.ts new file mode 100644 index 000000000..05099ca1c --- /dev/null +++ b/src/hooks/utils/useLocalStorage.ts @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; + +/** + * Custom hook for managing localStorage with React state + * @param key - The localStorage key + * @param initialValue - The initial value to use if key doesn't exist + * @returns A tuple of [storedValue, setValue] similar to useState + */ +export function useLocalStorage( + key: string, + initialValue: T +): [T, (value: T | ((val: T) => T)) => void] { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.error(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }); + + const setValue = (value: T | ((val: T) => T)) => { + try { + const valueToStore = value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } catch (error) { + console.error(`Error setting localStorage key "${key}":`, error); + } + }; + + useEffect(() => { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === key && e.newValue) { + try { + setStoredValue(JSON.parse(e.newValue)); + } catch (error) { + console.error(`Error parsing localStorage value for key "${key}":`, error); + } + } + }; + + window.addEventListener('storage', handleStorageChange); + return () => window.removeEventListener('storage', handleStorageChange); + }, [key]); + + return [storedValue, setValue]; +} diff --git a/src/hooks/utils/usePreventPageLeave.ts b/src/hooks/utils/usePreventPageLeave.ts new file mode 100644 index 000000000..6fc3815be --- /dev/null +++ b/src/hooks/utils/usePreventPageLeave.ts @@ -0,0 +1,26 @@ +import { useEffect } from 'react'; + +/** + * Custom hook to prevent user from leaving the page when there are unsaved changes + * @param when - Boolean indicating whether to prevent page leave + * @param message - Optional message to show in the confirmation dialog + */ +export function usePreventPageLeave(when: boolean, message?: string): void { + useEffect(() => { + const handleBeforeUnload = (event: BeforeUnloadEvent) => { + if (when) { + event.preventDefault(); + event.returnValue = message || 'You have unsaved changes. Are you sure you want to leave?'; + return message || 'You have unsaved changes. Are you sure you want to leave?'; + } + }; + + if (when) { + window.addEventListener('beforeunload', handleBeforeUnload); + } + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [when, message]); +} diff --git a/src/hooks/utils/useTimeout.ts b/src/hooks/utils/useTimeout.ts new file mode 100644 index 000000000..ef7e29c1a --- /dev/null +++ b/src/hooks/utils/useTimeout.ts @@ -0,0 +1,27 @@ +import { useEffect, useRef } from 'react'; + +/** + * Custom hook for managing timeouts + * @param callback - The callback function to execute + * @param delay - The delay in milliseconds + */ +export function useTimeout(callback: () => void, delay: number | null): void { + const savedCallback = useRef<() => void>(); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + function tick() { + if (savedCallback.current) { + savedCallback.current(); + } + } + + if (delay !== null) { + const id = setTimeout(tick, delay); + return () => clearTimeout(id); + } + }, [delay]); +} diff --git a/src/hooks/utils/useToggle.ts b/src/hooks/utils/useToggle.ts new file mode 100644 index 000000000..7a7ae7fdd --- /dev/null +++ b/src/hooks/utils/useToggle.ts @@ -0,0 +1,18 @@ +import { useCallback, useState } from 'react'; + +/** + * Custom hook for managing boolean toggle state + * @param initialValue - The initial boolean value + * @returns A tuple of [value, toggleValue, setValue] + */ +export function useToggle( + initialValue: boolean = false +): [boolean, () => void, (value: boolean) => void] { + const [value, setValue] = useState(initialValue); + + const toggleValue = useCallback(() => { + setValue((prev) => !prev); + }, []); + + return [value, toggleValue, setValue]; +} diff --git a/src/hooks/workspace/index.ts b/src/hooks/workspace/index.ts new file mode 100644 index 000000000..4be0ec3fc --- /dev/null +++ b/src/hooks/workspace/index.ts @@ -0,0 +1,4 @@ +export { default as useDesignAssignment } from './useDesignAssignment'; +export { default as useEnvironmentAssignment } from './useEnvironmentAssignment'; +export { default as useTeamAssignment } from './useTeamAssignment'; +export { default as useViewsAssignment } from './useViewsAssignment'; diff --git a/src/hooks/workspace/useDesignAssignment.tsx b/src/hooks/workspace/useDesignAssignment.tsx new file mode 100644 index 000000000..aeb85a9aa --- /dev/null +++ b/src/hooks/workspace/useDesignAssignment.tsx @@ -0,0 +1,160 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useState } from 'react'; +import { Pattern } from '../../custom/CustomCatalog/CustomCard'; +import { withDefaultPageArgs } from '../../custom/PerformersSection/PerformersSection'; +import { AssignmentHookResult } from '../../custom/Workspaces/types'; + +interface AddedAndRemovedDesigns { + addedDesignsIds: string[]; + removedDesignsIds: string[]; +} + +interface useDesignAssignmentProps { + workspaceId: string; + useGetDesignsOfWorkspaceQuery: any; + useAssignDesignToWorkspaceMutation: any; + useUnassignDesignFromWorkspaceMutation: any; + isDesignsVisible?: boolean; +} + +const useDesignAssignment = ({ + workspaceId, + useGetDesignsOfWorkspaceQuery, + useAssignDesignToWorkspaceMutation, + useUnassignDesignFromWorkspaceMutation, + isDesignsVisible +}: useDesignAssignmentProps): AssignmentHookResult => { + const [designsPage, setDesignsPage] = useState(0); + const [designsData, setDesignsData] = useState([]); + const designsPageSize = 25; + const [designsOfWorkspacePage, setDesignsOfWorkspacePage] = useState(0); + const [workspaceDesignsData, setWorkspaceDesignsData] = useState([]); + const [assignDesignModal, setAssignDesignModal] = useState(false); + const [skipDesigns, setSkipDesigns] = useState(true); + const [disableTransferButton, setDisableTransferButton] = useState(true); + const [assignedDesigns, setAssignedDesigns] = useState([]); + + const { data: designs } = useGetDesignsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: designsPage, + pagesize: designsPageSize, + filter: '{"assigned":false}' + }), + { + skip: skipDesigns || !isDesignsVisible + } + ); + + const { data: designsOfWorkspace } = useGetDesignsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: designsOfWorkspacePage, + pagesize: designsPageSize + }), + { + skip: skipDesigns || !isDesignsVisible + } + ); + + const [assignDesignToWorkspace] = useAssignDesignToWorkspaceMutation(); + const [unassignDesignFromWorkspace] = useUnassignDesignFromWorkspaceMutation(); + + useEffect(() => { + const designsDataRtk = designs?.designs ? designs.designs : []; + setDesignsData((prevData) => [...prevData, ...designsDataRtk]); + }, [designs]); + + useEffect(() => { + const designsOfWorkspaceDataRtk = designsOfWorkspace?.designs ? designsOfWorkspace.designs : []; + setWorkspaceDesignsData((prevData) => [...prevData, ...designsOfWorkspaceDataRtk]); + }, [designsOfWorkspace]); + + const handleAssignDesignModal = (e?: React.MouseEvent): void => { + e?.stopPropagation(); + setAssignDesignModal(true); + setSkipDesigns(false); + }; + + const handleAssignDesignModalClose = (e?: React.MouseEvent): void => { + e?.stopPropagation(); + setAssignDesignModal(false); + setSkipDesigns(true); + }; + + const handleAssignablePageDesign = (): void => { + const pagesCount = Math.ceil(Number(designs?.total_count) / designsPageSize); + if (designsPage < pagesCount - 1) { + setDesignsPage((prevDesignsPage) => prevDesignsPage + 1); + } + }; + + const handleAssignedPageDesign = (): void => { + const pagesCount = Math.ceil(Number(designsOfWorkspace?.total_count) / designsPageSize); + if (designsOfWorkspacePage < pagesCount - 1) { + setDesignsOfWorkspacePage((prevPage) => prevPage + 1); + } + }; + + const getAddedAndRemovedDesigns = (allAssignedDesigns: Pattern[]): AddedAndRemovedDesigns => { + const originalDesignsIds = workspaceDesignsData.map((design) => design.id); + const updatedDesignsIds = allAssignedDesigns.map((design) => design.id); + + const addedDesignsIds = updatedDesignsIds.filter((id) => !originalDesignsIds.includes(id)); + const removedDesignsIds = originalDesignsIds.filter((id) => !updatedDesignsIds.includes(id)); + + return { addedDesignsIds, removedDesignsIds }; + }; + + const isDesignsActivityOccurred = (allAssignedDesigns: Pattern[]): boolean => { + const { addedDesignsIds, removedDesignsIds } = getAddedAndRemovedDesigns(allAssignedDesigns); + return addedDesignsIds.length > 0 || removedDesignsIds.length > 0; + }; + + const handleAssignDesigns = async (): Promise => { + const { addedDesignsIds, removedDesignsIds } = getAddedAndRemovedDesigns(assignedDesigns); + + addedDesignsIds.map((id) => + assignDesignToWorkspace({ + workspaceId, + designId: id + }).unwrap() + ); + + removedDesignsIds.map((id) => + unassignDesignFromWorkspace({ + workspaceId, + designId: id + }).unwrap() + ); + + setDesignsData([]); + setWorkspaceDesignsData([]); + setDesignsPage(0); + setDesignsOfWorkspacePage(0); + handleAssignDesignModalClose(); + }; + + const handleAssignDesignsData = (updatedAssignedData: Pattern[]): void => { + const { addedDesignsIds, removedDesignsIds } = getAddedAndRemovedDesigns(updatedAssignedData); + setDisableTransferButton(!(addedDesignsIds.length > 0 || removedDesignsIds.length > 0)); + setAssignedDesigns(updatedAssignedData); + }; + + return { + data: designsData, + workspaceData: workspaceDesignsData, + assignModal: assignDesignModal, + handleAssignModal: handleAssignDesignModal, + handleAssignModalClose: handleAssignDesignModalClose, + handleAssignablePage: handleAssignablePageDesign, + handleAssignedPage: handleAssignedPageDesign, + handleAssign: handleAssignDesigns, + handleAssignData: handleAssignDesignsData, + isActivityOccurred: isDesignsActivityOccurred, + disableTransferButton, + assignedItems: assignedDesigns + }; +}; + +export default useDesignAssignment; diff --git a/src/hooks/workspace/useEnvironmentAssignment.tsx b/src/hooks/workspace/useEnvironmentAssignment.tsx new file mode 100644 index 000000000..70ddf2853 --- /dev/null +++ b/src/hooks/workspace/useEnvironmentAssignment.tsx @@ -0,0 +1,161 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useState } from 'react'; +import { withDefaultPageArgs } from '../../custom/PerformersSection/PerformersSection'; +import { AssignmentHookResult, Environment } from '../../custom/Workspaces/types'; + +interface UseEnvironmentAssignmentProps { + workspaceId: string; + useGetEnvironmentsOfWorkspaceQuery: any; + useAssignEnvironmentToWorkspaceMutation: any; + useUnassignEnvironmentFromWorkspaceMutation: any; + isEnvironmentsVisible?: boolean; +} + +const useEnvironmentAssignment = ({ + workspaceId, + useGetEnvironmentsOfWorkspaceQuery, + useAssignEnvironmentToWorkspaceMutation, + useUnassignEnvironmentFromWorkspaceMutation, + isEnvironmentsVisible +}: UseEnvironmentAssignmentProps): AssignmentHookResult => { + const [environmentsPage, setEnvironmentsPage] = useState(0); + const [environmentsData, setEnvironmentsData] = useState([]); + const environmentsPageSize = 25; + const [environmentsOfWorkspacePage, setEnvironmentsOfWorkspacePage] = useState(0); + const [workspaceEnvironmentsData, setWorkspaceEnvironmentsData] = useState([]); + const [assignEnvironmentModal, setAssignEnvironmentModal] = useState(false); + const [skipEnvironments, setSkipEnvironments] = useState(true); + const [disableTransferButton, setDisableTransferButton] = useState(true); + const [assignedEnvironments, setAssignedEnvironments] = useState([]); + + const { data: environments } = useGetEnvironmentsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: environmentsPage, + pagesize: environmentsPageSize, + filter: '{"assigned":false}' + }), + { + skip: skipEnvironments || !isEnvironmentsVisible + } + ); + + const { data: environmentsOfWorkspace } = useGetEnvironmentsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: environmentsOfWorkspacePage, + pagesize: environmentsPageSize + }), + { + skip: skipEnvironments || !isEnvironmentsVisible + } + ); + + const [assignEnvironmentToWorkspace] = useAssignEnvironmentToWorkspaceMutation(); + const [unassignEnvironmentFromWorkspace] = useUnassignEnvironmentFromWorkspaceMutation(); + + useEffect(() => { + const environmentsDataRtk = environments?.environments ? environments.environments : []; + setEnvironmentsData((prevData) => [...prevData, ...environmentsDataRtk]); + }, [environments]); + + useEffect(() => { + const environmentsOfWorkspaceDataRtk = environmentsOfWorkspace?.environments + ? environmentsOfWorkspace.environments + : []; + setWorkspaceEnvironmentsData((prevData) => [...prevData, ...environmentsOfWorkspaceDataRtk]); + }, [environmentsOfWorkspace]); + + const handleAssignEnvironmentModal = (e?: React.MouseEvent) => { + e?.stopPropagation(); + setAssignEnvironmentModal(true); + setSkipEnvironments(false); + }; + + const handleAssignEnvironmentModalClose = (e?: React.MouseEvent) => { + e?.stopPropagation(); + setAssignEnvironmentModal(false); + setSkipEnvironments(true); + }; + + const handleAssignablePageEnvironment = () => { + const pagesCount = Math.ceil(Number(environments?.total_count) / environmentsPageSize); + if (environmentsPage < pagesCount - 1) { + setEnvironmentsPage((prevEnvironmentsPage) => prevEnvironmentsPage + 1); + } + }; + + const handleAssignedPageEnvironment = () => { + const pagesCount = Math.ceil( + Number(environmentsOfWorkspace?.total_count) / environmentsPageSize + ); + if (environmentsOfWorkspacePage < pagesCount - 1) { + setEnvironmentsOfWorkspacePage((prevPage) => prevPage + 1); + } + }; + + const getAddedAndRemovedEnvironments = (allAssignedEnvironments: Environment[]) => { + const originalEnvironmentsIds = workspaceEnvironmentsData.map((env) => env.id); + const updatedEnvironmentsIds = allAssignedEnvironments.map((env) => env.id); + + const addedEnvironmentsIds = updatedEnvironmentsIds.filter( + (id) => !originalEnvironmentsIds.includes(id) + ); + const removedEnvironmentsIds = originalEnvironmentsIds.filter( + (id) => !updatedEnvironmentsIds.includes(id) + ); + + return { addedEnvironmentsIds, removedEnvironmentsIds }; + }; + + const handleAssignEnvironments = async () => { + const { addedEnvironmentsIds, removedEnvironmentsIds } = + getAddedAndRemovedEnvironments(assignedEnvironments); + + addedEnvironmentsIds.map((id) => + assignEnvironmentToWorkspace({ + workspaceId, + environmentId: id + }).unwrap() + ); + + removedEnvironmentsIds.map((id) => + unassignEnvironmentFromWorkspace({ + workspaceId, + environmentId: id + }).unwrap() + ); + + setEnvironmentsData([]); + setWorkspaceEnvironmentsData([]); + setEnvironmentsPage(0); + setEnvironmentsOfWorkspacePage(0); + handleAssignEnvironmentModalClose(); + }; + + const handleAssignEnvironmentsData = (updatedAssignedData: Environment[]) => { + const { addedEnvironmentsIds, removedEnvironmentsIds } = + getAddedAndRemovedEnvironments(updatedAssignedData); + addedEnvironmentsIds.length > 0 || removedEnvironmentsIds.length > 0 + ? setDisableTransferButton(false) + : setDisableTransferButton(true); + + setAssignedEnvironments(updatedAssignedData); + }; + + return { + data: environmentsData, + workspaceData: workspaceEnvironmentsData, + assignModal: assignEnvironmentModal, + handleAssignModal: handleAssignEnvironmentModal, + handleAssignModalClose: handleAssignEnvironmentModalClose, + handleAssignablePage: handleAssignablePageEnvironment, + handleAssignedPage: handleAssignedPageEnvironment, + handleAssign: handleAssignEnvironments, + handleAssignData: handleAssignEnvironmentsData, + disableTransferButton, + assignedItems: assignedEnvironments + }; +}; + +export default useEnvironmentAssignment; diff --git a/src/hooks/workspace/useTeamAssignment.tsx b/src/hooks/workspace/useTeamAssignment.tsx new file mode 100644 index 000000000..45a77d744 --- /dev/null +++ b/src/hooks/workspace/useTeamAssignment.tsx @@ -0,0 +1,148 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useState } from 'react'; +import { withDefaultPageArgs } from '../../custom/PerformersSection/PerformersSection'; +import { AssignmentHookResult, Team } from '../../custom/Workspaces/types'; + +interface UseTeamAssignmentProps { + workspaceId: string; + useGetTeamsOfWorkspaceQuery: any; + useAssignTeamToWorkspaceMutation: any; + useUnassignTeamFromWorkspaceMutation: any; + isTeamsVisible?: boolean; +} + +const useTeamAssignment = ({ + workspaceId, + useGetTeamsOfWorkspaceQuery, + useAssignTeamToWorkspaceMutation, + useUnassignTeamFromWorkspaceMutation, + isTeamsVisible +}: UseTeamAssignmentProps): AssignmentHookResult => { + const [teamsPage, setTeamsPage] = useState(0); + const [teamsData, setTeamsData] = useState([]); + const teamsPageSize = 25; + const [teamsOfWorkspacePage, setTeamsOfWorkspacePage] = useState(0); + const [workspaceTeamsData, setWorkspaceTeamsData] = useState([]); + const [assignTeamModal, setAssignTeamModal] = useState(false); + const [skipTeams, setSkipTeams] = useState(true); + const [assignTeamToWorkspace] = useAssignTeamToWorkspaceMutation(); + const [unassignTeamFromWorkspace] = useUnassignTeamFromWorkspaceMutation(); + const [disableTransferButton, setDisableTransferButton] = useState(true); + const [assignedTeams, setAssignedTeams] = useState([]); + + const { data: teams } = useGetTeamsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: teamsPage, + pagesize: teamsPageSize, + filter: '{"assigned":false}' + }), + { + skip: skipTeams || !isTeamsVisible + } + ); + + const { data: teamsOfWorkspace } = useGetTeamsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: teamsOfWorkspacePage, + pagesize: teamsPageSize + }), + { + skip: skipTeams || !isTeamsVisible + } + ); + + useEffect(() => { + const teamsDataRtk = teams?.teams ? teams.teams : []; + setTeamsData((prevData) => [...prevData, ...teamsDataRtk]); + }, [teams]); + + useEffect(() => { + const teamsOfWorkspaceDataRtk = teamsOfWorkspace?.teams ? teamsOfWorkspace.teams : []; + setWorkspaceTeamsData((prevData) => [...prevData, ...teamsOfWorkspaceDataRtk]); + }, [teamsOfWorkspace]); + + const handleAssignTeamModal = (e?: React.MouseEvent) => { + e?.stopPropagation(); + setAssignTeamModal(true); + setSkipTeams(false); + }; + + const handleAssignTeamModalClose = (e?: React.MouseEvent) => { + e?.stopPropagation(); + setAssignTeamModal(false); + setSkipTeams(true); + }; + + const handleAssignablePageTeam = () => { + const pagesCount = Math.ceil(Number(teams?.total_count) / teamsPageSize); + if (teamsPage < pagesCount - 1) { + setTeamsPage((prevTeamsPage) => prevTeamsPage + 1); + } + }; + + const handleAssignedPageTeam = () => { + const pagesCount = Math.ceil(Number(teamsOfWorkspace?.total_count) / teamsPageSize); + + if (teamsOfWorkspacePage < pagesCount - 1) { + setTeamsOfWorkspacePage((prevPage) => prevPage + 1); + } + }; + + const handleAssignTeams = () => { + const { addedTeamsIds, removedTeamsIds } = getAddedAndRemovedTeams(assignedTeams); + + addedTeamsIds.map((id) => + assignTeamToWorkspace({ + workspaceId, + teamId: id + }).unwrap() + ); + + removedTeamsIds.map((id) => + unassignTeamFromWorkspace({ + workspaceId, + teamId: id + }).unwrap() + ); + + setTeamsData([]); + setWorkspaceTeamsData([]); + handleAssignTeamModalClose(); + }; + + const getAddedAndRemovedTeams = (allAssignedTeams: Team[]) => { + const originalTeamsIds = workspaceTeamsData.map((team) => team.id); + const updatedTeamsIds = allAssignedTeams.map((team) => team.id); + + const addedTeamsIds = updatedTeamsIds.filter((id) => !originalTeamsIds.includes(id)); + const removedTeamsIds = originalTeamsIds.filter((id) => !updatedTeamsIds.includes(id)); + + return { addedTeamsIds, removedTeamsIds }; + }; + + const handleAssignTeamsData = (updatedAssignedData: Team[]) => { + const { addedTeamsIds, removedTeamsIds } = getAddedAndRemovedTeams(updatedAssignedData); + addedTeamsIds.length > 0 || removedTeamsIds.length > 0 + ? setDisableTransferButton(false) + : setDisableTransferButton(true); + setAssignedTeams(updatedAssignedData); + }; + + return { + data: teamsData, + workspaceData: workspaceTeamsData, + assignModal: assignTeamModal, + handleAssignModal: handleAssignTeamModal, + handleAssignModalClose: handleAssignTeamModalClose, + handleAssignablePage: handleAssignablePageTeam, + handleAssignedPage: handleAssignedPageTeam, + handleAssign: handleAssignTeams, + handleAssignData: handleAssignTeamsData, + disableTransferButton, + assignedItems: assignedTeams + }; +}; + +export default useTeamAssignment; diff --git a/src/hooks/workspace/useViewsAssignment.tsx b/src/hooks/workspace/useViewsAssignment.tsx new file mode 100644 index 000000000..ffe3ceb65 --- /dev/null +++ b/src/hooks/workspace/useViewsAssignment.tsx @@ -0,0 +1,160 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, useState } from 'react'; +import { Pattern } from '../../custom/CustomCatalog/CustomCard'; +import { withDefaultPageArgs } from '../../custom/PerformersSection/PerformersSection'; +import { AssignmentHookResult } from '../../custom/Workspaces/types'; + +interface AddedAndRemovedViews { + addedviewsIds: string[]; + removedviewsIds: string[]; +} + +interface useViewAssignmentProps { + workspaceId: string; + useGetViewsOfWorkspaceQuery: any; + useAssignViewToWorkspaceMutation: any; + useUnassignViewFromWorkspaceMutation: any; + isViewsVisible: boolean; +} + +const useViewAssignment = ({ + workspaceId, + useGetViewsOfWorkspaceQuery, + useAssignViewToWorkspaceMutation, + useUnassignViewFromWorkspaceMutation, + isViewsVisible +}: useViewAssignmentProps): AssignmentHookResult => { + const [viewsPage, setviewsPage] = useState(0); + const [viewsData, setviewsData] = useState([]); + const viewsPageSize = 25; + const [viewsOfWorkspacePage, setviewsOfWorkspacePage] = useState(0); + const [workspaceviewsData, setWorkspaceviewsData] = useState([]); + const [assignviewModal, setAssignviewModal] = useState(false); + const [skipviews, setSkipviews] = useState(true); + const [disableTransferButton, setDisableTransferButton] = useState(true); + const [assignedviews, setAssignedviews] = useState([]); + + const { data: views } = useGetViewsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: viewsPage, + pagesize: viewsPageSize, + filter: '{"assigned":false}' + }), + { + skip: skipviews || !isViewsVisible + } + ); + + const { data: viewsOfWorkspace } = useGetViewsOfWorkspaceQuery( + withDefaultPageArgs({ + workspaceId, + page: viewsOfWorkspacePage, + pagesize: viewsPageSize + }), + { + skip: skipviews || !isViewsVisible + } + ); + + const [assignviewToWorkspace] = useAssignViewToWorkspaceMutation(); + const [unassignviewFromWorkspace] = useUnassignViewFromWorkspaceMutation(); + + useEffect(() => { + const viewsDataRtk = views?.views ? views.views : []; + setviewsData((prevData) => [...prevData, ...viewsDataRtk]); + }, [views]); + + useEffect(() => { + const viewsOfWorkspaceDataRtk = viewsOfWorkspace?.views ? viewsOfWorkspace.views : []; + setWorkspaceviewsData((prevData) => [...prevData, ...viewsOfWorkspaceDataRtk]); + }, [viewsOfWorkspace]); + + const handleAssignviewModal = (e?: React.MouseEvent): void => { + e?.stopPropagation(); + setAssignviewModal(true); + setSkipviews(false); + }; + + const handleAssignviewModalClose = (e?: React.MouseEvent): void => { + e?.stopPropagation(); + setAssignviewModal(false); + setSkipviews(true); + }; + + const handleAssignablePageview = (): void => { + const pagesCount = Math.ceil(Number(views?.total_count) / viewsPageSize); + if (viewsPage < pagesCount - 1) { + setviewsPage((prevviewsPage) => prevviewsPage + 1); + } + }; + + const handleAssignedPageview = (): void => { + const pagesCount = Math.ceil(Number(viewsOfWorkspace?.total_count) / viewsPageSize); + if (viewsOfWorkspacePage < pagesCount - 1) { + setviewsOfWorkspacePage((prevPage) => prevPage + 1); + } + }; + + const getAddedAndRemovedviews = (allAssignedviews: Pattern[]): AddedAndRemovedViews => { + const originalviewsIds = workspaceviewsData.map((view) => view.id); + const updatedviewsIds = allAssignedviews.map((view) => view.id); + + const addedviewsIds = updatedviewsIds.filter((id) => !originalviewsIds.includes(id)); + const removedviewsIds = originalviewsIds.filter((id) => !updatedviewsIds.includes(id)); + + return { addedviewsIds, removedviewsIds }; + }; + + const isViewsActivityOccurred = (allViews: Pattern[]): boolean => { + const { addedviewsIds, removedviewsIds } = getAddedAndRemovedviews(allViews); + return addedviewsIds.length > 0 || removedviewsIds.length > 0; + }; + + const handleAssignviews = async (): Promise => { + const { addedviewsIds, removedviewsIds } = getAddedAndRemovedviews(assignedviews); + + addedviewsIds.map((id) => + assignviewToWorkspace({ + workspaceId, + viewId: id + }).unwrap() + ); + + removedviewsIds.map((id) => + unassignviewFromWorkspace({ + workspaceId, + viewId: id + }).unwrap() + ); + + setviewsData([]); + setWorkspaceviewsData([]); + setviewsPage(0); + setviewsOfWorkspacePage(0); + handleAssignviewModalClose(); + }; + + const handleAssignviewsData = (updatedAssignedData: Pattern[]): void => { + const { addedviewsIds, removedviewsIds } = getAddedAndRemovedviews(updatedAssignedData); + setDisableTransferButton(!(addedviewsIds.length > 0 || removedviewsIds.length > 0)); + setAssignedviews(updatedAssignedData); + }; + + return { + data: viewsData, + workspaceData: workspaceviewsData, + assignModal: assignviewModal, + handleAssignModal: handleAssignviewModal, + handleAssignModalClose: handleAssignviewModalClose, + handleAssignablePage: handleAssignablePageview, + handleAssignedPage: handleAssignedPageview, + handleAssign: handleAssignviews, + isActivityOccurred: isViewsActivityOccurred, + handleAssignData: handleAssignviewsData, + disableTransferButton, + assignedItems: assignedviews + }; +}; + +export default useViewAssignment;