diff --git a/package.json b/package.json index f71481e..c59d0ba 100644 --- a/package.json +++ b/package.json @@ -15,42 +15,43 @@ "dependencies": { "@phenixrts/sdk": "2025.2.2", "@reduxjs/toolkit": "2.9.0", - "@techniker-me/pcast-api": "2025.1.5", + "@techniker-me/pcast-api": "2025.1.8", "@techniker-me/tools": "2025.0.16", "moment": "2.30.1", + "phenix-edge-auth": "1.2.7", "phenix-web-proto": "2020.0.3", "react": "19.1.1", "react-dom": "19.1.1", "react-redux": "9.2.0", - "react-router-dom": "7.8.2", + "react-router-dom": "7.9.1", "redux-persist": "6.0.0", "styled-components": "6.1.19", - "uuid": "11.1.0" + "uuid": "13.0.0" }, "devDependencies": { - "@eslint/js": "9.34.0", + "@eslint/js": "9.35.0", "@fortawesome/fontawesome-svg-core": "7.0.1", "@fortawesome/free-regular-svg-icons": "7.0.1", "@fortawesome/free-solid-svg-icons": "7.0.1", "@fortawesome/react-fontawesome": "3.0.2", - "@types/node": "24.3.0", - "@types/react": "19.1.12", + "@types/node": "24.5.0", + "@types/react": "19.1.13", "@types/react-dom": "19.1.9", "@vitejs/plugin-react-swc": "4.0.1", "babel-plugin-styled-components": "2.1.4", "babel-plugin-transform-amd-to-commonjs": "1.6.0", - "eslint": "9.34.0", + "eslint": "9.35.0", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.2.0", "eslint-plugin-react-refresh": "0.4.20", - "globals": "16.3.0", + "globals": "16.4.0", "prettier": "3.6.2", "react-datepicker": "8.7.0", "react-toastify": "11.0.5", "typescript": "5.9.2", - "typescript-eslint": "8.42.0", + "typescript-eslint": "8.44.0", "typescript-plugin-styled-components": "3.0.0", - "vite": "7.1.4", + "vite": "7.1.5", "vite-plugin-babel": "1.3.2", "vite-plugin-commonjs": "0.10.4" } diff --git a/src/views/ChannelList/ChannelList.tsx b/src/views/ChannelList/ChannelList.tsx index 1b6f325..7a76865 100644 --- a/src/views/ChannelList/ChannelList.tsx +++ b/src/views/ChannelList/ChannelList.tsx @@ -1,11 +1,10 @@ /** * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. */ -import React, {useEffect, useRef, useState, useCallback} from 'react'; +import React, {useEffect, useState} from 'react'; import {channelListErrorMessages} from 'constants/index'; -import {ITableSortSearch} from 'interfaces/tableProps'; -import {AppDispatch, useAppDispatch, useAppSelector} from 'store'; +import {useAppDispatch, useAppSelector} from 'store'; import {selectChannelList, selectChannelsLoading, selectChannelsError, listChannels} from 'store/action/channels'; import {LoadingWheel as Loader} from 'components/loaders'; @@ -14,31 +13,12 @@ import {TableHeaderKey, ITableWithPaginationHeader} from 'components/table'; import {TableWithPagination} from 'components'; import {Error} from 'components/error-renderer/style'; -import {columns} from './columns-config'; +import {createColumnsWithContext} from './columns-data'; import {CreateChannelModal} from './create-channel'; -const POLLING_INTERVAL = 5000; // 5 seconds -const ChannelListLoading = () => ( -
- -
-); - -const ChannelListError = ({error, dispatch}: {error: string, dispatch: AppDispatch}) => ( - - - {(channelListErrorMessages as Record)[error] || error} - - - -); - - export const ChannelList = (): React.JSX.Element => { const dispatch = useAppDispatch(); - const interval = useRef(null); + const [hasLoaded, setHasLoaded] = useState(false); // Redux state const channels = useAppSelector(selectChannelList); @@ -48,80 +28,57 @@ export const ChannelList = (): React.JSX.Element => { // Local state const [isCreateChannelModalOpened, setCreateChannelModalOpened] = useState(false); - // Memoized columns to prevent unnecessary re-renders - const channelsColumns = React.useMemo(() => ({...columns}), []); + // Create columns with React context for channel name navigation + const columns = React.useMemo(() => createColumnsWithContext(), []); // Load channels on component mount useEffect(() => { dispatch(listChannels()); }, [dispatch]); - // // Set up polling for channel updates - // useEffect(() => { - // if (interval.current) { - // clearInterval(interval.current); - // } + // Screen header configuration + const screenHeader: ITableWithPaginationHeader = { + [TableHeaderKey.Search]: {}, + [TableHeaderKey.AddRow]: { + openAddRowModal: () => setCreateChannelModalOpened(true) + } + }; - // // Only start polling if we have channels and not currently fetching - // if (channels.length > 0 && !isFetching) { - // interval.current = setInterval(() => { - // dispatch(listChannels()); - // }, POLLING_INTERVAL); - // } + // Handle loading state + if (isFetching && !hasLoaded) { + setHasLoaded(true); + return ( +
+ +
+ ); + } - // return () => { - // if (interval.current) { - // clearInterval(interval.current); - // } - // }; - // }, [dispatch, channels.length, isFetching]); - - // Memoized screen header to prevent unnecessary re-renders - const screenHeader: ITableWithPaginationHeader = React.useMemo( - () => ({ - [TableHeaderKey.Search]: {}, - [TableHeaderKey.AddRow]: { - openAddRowModal: () => { - setCreateChannelModalOpened(true); - } - } - }), - [] - ); - - // Callback for handling search and sort changes (no-op since TableWithPagination handles internally) - const changeScreenProps = useCallback((_data: Partial) => { - // TableWithPagination handles search and sort internally - // This is kept for compatibility with the component interface - }, []); - - // Memoized callback for refreshing channel list - const refreshChannelList = useCallback(async (): Promise => { - await dispatch(listChannels()); - }, [dispatch]); - - // Early return for error state + // Handle error state if (error) { - return ; + return ( + + + {(channelListErrorMessages as Record)[error] || error} + + + + ); } - if (isFetching) { - return ; - } - - return (<> + return ( - - {isCreateChannelModalOpened && } + + {isCreateChannelModalOpened && ( + { + await dispatch(listChannels()); + }} + setCreateChannelModalOpened={setCreateChannelModalOpened} + /> + )} - ); }; diff --git a/src/views/ChannelList/columns-config.tsx b/src/views/ChannelList/columns-config.tsx deleted file mode 100644 index c6482c3..0000000 --- a/src/views/ChannelList/columns-config.tsx +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. - */ - -export {columns} from './columns-data'; diff --git a/src/views/ChannelList/columns-data.tsx b/src/views/ChannelList/columns-data.tsx index 58a19cd..df4949b 100644 --- a/src/views/ChannelList/columns-data.tsx +++ b/src/views/ChannelList/columns-data.tsx @@ -1,14 +1,50 @@ /** * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. */ +import React from 'react'; import {faEllipsisV} from '@fortawesome/free-solid-svg-icons'; -import {CellType, ColumnsType} from 'components/table'; +import {CellType, ColumnsType, DataRowType} from 'components/table'; import {ChannelIconMenu} from 'components/channel-icon-menu'; import {IconMenuPosition} from 'components/icon-menu/types'; import {ChannelPublishingStateIndicator} from './channel-publishing-state-indicator'; import {theme} from 'theme'; +import {useAppDispatch} from 'store'; +import {setSelectedChannel} from 'store/slices/Channels.slice'; +import {useNavigate} from 'react-router-dom'; +import styled from 'styled-components'; -export const columns: ColumnsType = { +const ChannelNameLink = styled.a` + color: ${theme.colors.linkBlue}; + text-decoration: none; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +`; + +// Component that needs to be rendered within a React context +const ChannelNameCellComponent = (row?: DataRowType) => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + if (row?.channelId && row?.name) { + dispatch(setSelectedChannel(row as any)); + navigate(`/channels/${row.channelId}`); + } + }; + + return ( + + {row?.name || 'N/A'} + + ); +}; + +// Factory function to create columns with React context +export const createColumnsWithContext = (): ColumnsType => ({ indicator: { title: '', hasBorder: false, @@ -18,8 +54,8 @@ export const columns: ColumnsType = { }, name: { title: 'Channel Name', - type: CellType.Link, - textCell: {propName: 'name'}, + type: CellType.Component, + renderCell: ChannelNameCellComponent, thStyle: { textAlign: 'left', paddingLeft: 16 @@ -60,4 +96,4 @@ export const columns: ColumnsType = { } } } -}; +}); diff --git a/src/views/LoginForm/LoginForm.tsx b/src/views/LoginForm/LoginForm.tsx index 1345280..ecece68 100644 --- a/src/views/LoginForm/LoginForm.tsx +++ b/src/views/LoginForm/LoginForm.tsx @@ -38,7 +38,7 @@ export const LoginForm: FC = () => { useEffect(() => { if (isAuthenticated) { - navigate('/channels', {replace: true}); + navigate(`/${applicationId}/channels`, {replace: true}); } }, [isAuthenticated, navigate]); @@ -59,7 +59,6 @@ export const LoginForm: FC = () => { }; const handleInputChange = (setter: (value: string) => void) => (e: React.ChangeEvent) => { - // Clear error when user starts typing if (error) { dispatch(setError(null)); }