Big performance increase.
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
This commit is contained in:
parent
24a3a03c65
commit
e446098a76
@ -4053,6 +4053,7 @@ body.objectKanbanColumnResizing * {
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
content-visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-pane-inactive.dashboard-tab-pane-capturing {
|
.dashboard-tab-pane-inactive.dashboard-tab-pane-capturing {
|
||||||
@ -4062,6 +4063,7 @@ body.objectKanbanColumnResizing * {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translate(-100%, 0);
|
transform: translate(-100%, 0);
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
content-visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tab-preview-capture-frame {
|
.dashboard-tab-preview-capture-frame {
|
||||||
|
|||||||
@ -128,9 +128,6 @@
|
|||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"electrobun": "1.18.1",
|
"electrobun": "1.18.1",
|
||||||
"electron": "^38.7.1",
|
|
||||||
"electron-builder": "^26.0.12",
|
|
||||||
"electron-packager": "^17.1.2",
|
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.34.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.4",
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
|||||||
2153
pnpm-lock.yaml
generated
2153
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
19
src/App.jsx
19
src/App.jsx
@ -65,14 +65,7 @@ const getRouter = () => {
|
|||||||
return BrowserRouter
|
return BrowserRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppContent = () => {
|
const renderEmpty = () => (
|
||||||
const { themeConfig } = useThemeContext()
|
|
||||||
const Router = getRouter()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigProvider
|
|
||||||
theme={themeConfig}
|
|
||||||
renderEmpty={() => (
|
|
||||||
<div style={{ margin: '32px' }}>
|
<div style={{ margin: '32px' }}>
|
||||||
<MissingPlaceholder
|
<MissingPlaceholder
|
||||||
message='No data.'
|
message='No data.'
|
||||||
@ -80,8 +73,14 @@ const AppContent = () => {
|
|||||||
hasBorder={false}
|
hasBorder={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
>
|
|
||||||
|
const AppContent = () => {
|
||||||
|
const { themeConfig } = useThemeContext()
|
||||||
|
const Router = getRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider theme={themeConfig} renderEmpty={renderEmpty}>
|
||||||
<App>
|
<App>
|
||||||
<Router>
|
<Router>
|
||||||
<ElectronProvider>
|
<ElectronProvider>
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import DeveloperSidebar from './Developer/DeveloperSidebar'
|
|||||||
import DashboardSidebarSplitter from './common/DashboardSidebarSplitter'
|
import DashboardSidebarSplitter from './common/DashboardSidebarSplitter'
|
||||||
import MobileFooterNavigation from './common/MobileFooterNavigation'
|
import MobileFooterNavigation from './common/MobileFooterNavigation'
|
||||||
import { useThemeContext } from './context/ThemeContext'
|
import { useThemeContext } from './context/ThemeContext'
|
||||||
import { MessageProvider } from './context/MessageContext'
|
|
||||||
import { useDashboardObjectToolsContext } from './context/DashboardObjectToolsContext'
|
import { useDashboardObjectToolsContext } from './context/DashboardObjectToolsContext'
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import { ElectronContext } from './context/ElectronContext'
|
import { ElectronContext } from './context/ElectronContext'
|
||||||
@ -52,7 +51,6 @@ const DashboardLayout = ({ children }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageProvider>
|
|
||||||
<Layout
|
<Layout
|
||||||
style={{ height: 'var(--unit-100vh)' }}
|
style={{ height: 'var(--unit-100vh)' }}
|
||||||
className={`${isDarkMode ? 'dark-mode' : 'light-mode'} main-layout${isMobile ? ' is-mobile' : ''}`}
|
className={`${isDarkMode ? 'dark-mode' : 'light-mode'} main-layout${isMobile ? ' is-mobile' : ''}`}
|
||||||
@ -80,7 +78,6 @@ const DashboardLayout = ({ children }) => {
|
|||||||
</DashboardSidebarSplitter>
|
</DashboardSidebarSplitter>
|
||||||
{isMobile ? <MobileFooterNavigation /> : null}
|
{isMobile ? <MobileFooterNavigation /> : null}
|
||||||
</Layout>
|
</Layout>
|
||||||
</MessageProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState, memo } from 'react'
|
||||||
import { Button, Flex, Popover, Typography } from 'antd'
|
import { Button, Flex, Popover, Typography } from 'antd'
|
||||||
import DashboardTabPreview from './DashboardTabPreview'
|
import DashboardTabPreview from './DashboardTabPreview'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
@ -66,7 +66,7 @@ DashboardTabLabel.propTypes = {
|
|||||||
onClose: PropTypes.func
|
onClose: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardTabItem = ({
|
const DashboardTabItem = memo(function DashboardTabItem({
|
||||||
tab,
|
tab,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
isSelected,
|
isSelected,
|
||||||
@ -78,9 +78,9 @@ const DashboardTabItem = ({
|
|||||||
onKeyDown,
|
onKeyDown,
|
||||||
onDragOver,
|
onDragOver,
|
||||||
onDrop,
|
onDrop,
|
||||||
onClose
|
onClose,
|
||||||
}) => {
|
captureTabPreview
|
||||||
const { captureTabPreview } = useNavigationTabs()
|
}) {
|
||||||
const {
|
const {
|
||||||
src: previewSrc,
|
src: previewSrc,
|
||||||
theme: previewTheme,
|
theme: previewTheme,
|
||||||
@ -174,7 +174,9 @@ const DashboardTabItem = ({
|
|||||||
{tabButton}
|
{tabButton}
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
DashboardTabItem.displayName = 'DashboardTabItem'
|
||||||
|
|
||||||
DashboardTabItem.propTypes = {
|
DashboardTabItem.propTypes = {
|
||||||
tab: PropTypes.shape({
|
tab: PropTypes.shape({
|
||||||
@ -193,7 +195,8 @@ DashboardTabItem.propTypes = {
|
|||||||
onKeyDown: PropTypes.func.isRequired,
|
onKeyDown: PropTypes.func.isRequired,
|
||||||
onDragOver: PropTypes.func.isRequired,
|
onDragOver: PropTypes.func.isRequired,
|
||||||
onDrop: PropTypes.func.isRequired,
|
onDrop: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func
|
onClose: PropTypes.func,
|
||||||
|
captureTabPreview: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
const STICKY_EDGE_INSET = 20
|
const STICKY_EDGE_INSET = 20
|
||||||
@ -546,7 +549,8 @@ const DashboardTabs = () => {
|
|||||||
reorderTabs,
|
reorderTabs,
|
||||||
handleTabDragStart,
|
handleTabDragStart,
|
||||||
handleTabDragEnd,
|
handleTabDragEnd,
|
||||||
handleExternalTabDrop
|
handleExternalTabDrop,
|
||||||
|
captureTabPreview
|
||||||
} = useNavigationTabs()
|
} = useNavigationTabs()
|
||||||
const { rootRef, listRef, cloneRef, lineRef } = useActiveTabStickyScroll(
|
const { rootRef, listRef, cloneRef, lineRef } = useActiveTabStickyScroll(
|
||||||
activeTabId,
|
activeTabId,
|
||||||
@ -720,7 +724,8 @@ const DashboardTabs = () => {
|
|||||||
onKeyDown: handleKeyDown,
|
onKeyDown: handleKeyDown,
|
||||||
onDragOver: handleItemDragOver,
|
onDragOver: handleItemDragOver,
|
||||||
onDrop: handleItemDrop,
|
onDrop: handleItemDrop,
|
||||||
onClose: closeTab
|
onClose: closeTab,
|
||||||
|
captureTabPreview
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Descriptions, Card, Flex, Divider, Skeleton } from 'antd'
|
import { Descriptions, Card, Flex, Divider, Skeleton } from 'antd'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import { createElement } from 'react'
|
import { createElement, memo } from 'react'
|
||||||
import Thumbnail from './Thumbnail'
|
import Thumbnail from './Thumbnail'
|
||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
const ObjectCard = ({
|
const ObjectCard = memo(function ObjectCard({
|
||||||
isSkeleton = false,
|
isSkeleton = false,
|
||||||
model,
|
model,
|
||||||
modelProperties,
|
modelProperties,
|
||||||
@ -16,7 +16,7 @@ const ObjectCard = ({
|
|||||||
renderActions,
|
renderActions,
|
||||||
lazyLoading = false,
|
lazyLoading = false,
|
||||||
cardStyle = 'borderless'
|
cardStyle = 'borderless'
|
||||||
}) => {
|
}) {
|
||||||
const descriptionItems = []
|
const descriptionItems = []
|
||||||
const modelIcon = createElement(model.icon, { style: { fontSize: 24 } })
|
const modelIcon = createElement(model.icon, { style: { fontSize: 24 } })
|
||||||
|
|
||||||
@ -169,7 +169,9 @@ const ObjectCard = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
ObjectCard.displayName = 'ObjectCard'
|
||||||
|
|
||||||
ObjectCard.propTypes = {
|
ObjectCard.propTypes = {
|
||||||
model: PropTypes.object.isRequired,
|
model: PropTypes.object.isRequired,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import PropTypes from 'prop-types'
|
|||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
|
import { useIsNavigationTabActive } from '../context/NavigationTabsContext'
|
||||||
import ObjectCard from './ObjectCard'
|
import ObjectCard from './ObjectCard'
|
||||||
import ObjectKanbanColumn from './ObjectKanbanColumn'
|
import ObjectKanbanColumn from './ObjectKanbanColumn'
|
||||||
import ObjectKanbanHeader from './ObjectKanbanHeader'
|
import ObjectKanbanHeader from './ObjectKanbanHeader'
|
||||||
@ -287,6 +288,7 @@ const ObjectKanban = forwardRef(
|
|||||||
subscribeToObjectTypeUpdates
|
subscribeToObjectTypeUpdates
|
||||||
} = useContext(ApiServerContext)
|
} = useContext(ApiServerContext)
|
||||||
const { token } = useContext(AuthContext)
|
const { token } = useContext(AuthContext)
|
||||||
|
const isTabActive = useIsNavigationTabActive()
|
||||||
const columnRefs = useRef({})
|
const columnRefs = useRef({})
|
||||||
const headerTrackRef = useRef(null)
|
const headerTrackRef = useRef(null)
|
||||||
const skeletonTrackRef = useRef(null)
|
const skeletonTrackRef = useRef(null)
|
||||||
@ -720,7 +722,7 @@ const ObjectKanban = forwardRef(
|
|||||||
}, [connected, token])
|
}, [connected, token])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connected !== true || !type || !token) return
|
if (connected !== true || !type || !token || !isTabActive) return
|
||||||
|
|
||||||
const unsubscribe = subscribeToAllObjectUpdates(
|
const unsubscribe = subscribeToAllObjectUpdates(
|
||||||
type,
|
type,
|
||||||
@ -735,10 +737,10 @@ const ObjectKanban = forwardRef(
|
|||||||
subscribeToAllObjectUpdatesRef.current = null
|
subscribeToAllObjectUpdatesRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [type, connected, subscribeToAllObjectUpdates, token])
|
}, [type, connected, subscribeToAllObjectUpdates, token, isTabActive])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connected !== true || !token) return
|
if (connected !== true || !token || !isTabActive) return
|
||||||
if (subscribedTypeRef.current === type) return
|
if (subscribedTypeRef.current === type) return
|
||||||
|
|
||||||
const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
|
const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
|
||||||
@ -757,7 +759,7 @@ const ObjectKanban = forwardRef(
|
|||||||
subscribedTypeRef.current = null
|
subscribedTypeRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [type, connected, token])
|
}, [type, connected, token, isTabActive])
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
memo,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -45,6 +46,7 @@ import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
|||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
import { ElectronContext } from '../context/ElectronContext'
|
import { ElectronContext } from '../context/ElectronContext'
|
||||||
import { useActions } from '../context/ActionsContext'
|
import { useActions } from '../context/ActionsContext'
|
||||||
|
import { useIsNavigationTabActive } from '../context/NavigationTabsContext'
|
||||||
import ActionsIcon from '../../Icons/ActionsIcon'
|
import ActionsIcon from '../../Icons/ActionsIcon'
|
||||||
import FilterIcon from '../../Icons/FilterIcon'
|
import FilterIcon from '../../Icons/FilterIcon'
|
||||||
import ScrollBox from './ScrollBox'
|
import ScrollBox from './ScrollBox'
|
||||||
@ -80,6 +82,7 @@ logger.setLevel(config.logLevel)
|
|||||||
const SCROLL_THRESHOLD = 50
|
const SCROLL_THRESHOLD = 50
|
||||||
const SKELETON_HEIGHT = 49.5
|
const SKELETON_HEIGHT = 49.5
|
||||||
const EMPTY_MASTER_FILTER = {}
|
const EMPTY_MASTER_FILTER = {}
|
||||||
|
const EMPTY_VISIBLE_COLUMNS = {}
|
||||||
|
|
||||||
const getCardColSpan = (containerWidth) => {
|
const getCardColSpan = (containerWidth) => {
|
||||||
if (containerWidth >= 2980) return 2
|
if (containerWidth >= 2980) return 2
|
||||||
@ -291,7 +294,8 @@ EditableRow.propTypes = {
|
|||||||
onRegister: PropTypes.func
|
onRegister: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
const ObjectTable = forwardRef(
|
const ObjectTable = memo(
|
||||||
|
forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
type,
|
type,
|
||||||
@ -301,7 +305,7 @@ const ObjectTable = forwardRef(
|
|||||||
initialPage = 1,
|
initialPage = 1,
|
||||||
viewMode: viewModeProp,
|
viewMode: viewModeProp,
|
||||||
cards = false,
|
cards = false,
|
||||||
visibleColumns = {},
|
visibleColumns = EMPTY_VISIBLE_COLUMNS,
|
||||||
masterFilter,
|
masterFilter,
|
||||||
size = 'middle',
|
size = 'middle',
|
||||||
onStateChange,
|
onStateChange,
|
||||||
@ -335,6 +339,7 @@ const ObjectTable = forwardRef(
|
|||||||
const { token, userProfile } = useContext(AuthContext)
|
const { token, userProfile } = useContext(AuthContext)
|
||||||
const { isElectron } = useContext(ElectronContext)
|
const { isElectron } = useContext(ElectronContext)
|
||||||
const { callAction } = useActions()
|
const { callAction } = useActions()
|
||||||
|
const isTabActive = useIsNavigationTabActive()
|
||||||
const resolvedMasterFilter = masterFilter ?? EMPTY_MASTER_FILTER
|
const resolvedMasterFilter = masterFilter ?? EMPTY_MASTER_FILTER
|
||||||
const listViewId = objectListView?.listViewId ?? null
|
const listViewId = objectListView?.listViewId ?? null
|
||||||
const listViewFilter = objectListView?.listViewFilter ?? null
|
const listViewFilter = objectListView?.listViewFilter ?? null
|
||||||
@ -517,8 +522,68 @@ const ObjectTable = forwardRef(
|
|||||||
return {}
|
return {}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const rowActions =
|
const rowActions = useMemo(
|
||||||
model.actions?.filter((action) => action.row == true) || []
|
() => model.actions?.filter((action) => action.row == true) || [],
|
||||||
|
[model]
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderActions = useCallback(
|
||||||
|
(objectData, actionsDisabled = false) => {
|
||||||
|
return (
|
||||||
|
<Flex gap='small' align='center' justify='center'>
|
||||||
|
{rowActions.map((action, index) => {
|
||||||
|
const denied = !hasActionPermission(userProfile, model, action.name)
|
||||||
|
var disabled = denied
|
||||||
|
if (action.disabled) {
|
||||||
|
if (typeof action.disabled === 'function') {
|
||||||
|
disabled =
|
||||||
|
denied ||
|
||||||
|
action.disabled({
|
||||||
|
...objectData,
|
||||||
|
_user: userProfile
|
||||||
|
}) ||
|
||||||
|
actionsDisabled
|
||||||
|
} else {
|
||||||
|
disabled = denied || action.disabled || actionsDisabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip key={index} title={action.label}>
|
||||||
|
<Button
|
||||||
|
icon={
|
||||||
|
action.icon ? (
|
||||||
|
createElement(action.icon)
|
||||||
|
) : (
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={disabled || objectData?.isSkeleton}
|
||||||
|
type={'text'}
|
||||||
|
size={'small'}
|
||||||
|
danger={action?.danger || false}
|
||||||
|
onClick={() => {
|
||||||
|
if (denied) return
|
||||||
|
if (
|
||||||
|
onRowAction &&
|
||||||
|
onRowAction(action, objectData) !== false
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callAction(
|
||||||
|
action,
|
||||||
|
{ ...objectData, _user: userProfile },
|
||||||
|
type
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[callAction, model, onRowAction, rowActions, type, userProfile]
|
||||||
|
)
|
||||||
|
|
||||||
const createSkeletonData = useCallback(
|
const createSkeletonData = useCallback(
|
||||||
(pageNum) => {
|
(pageNum) => {
|
||||||
@ -602,61 +667,6 @@ const ObjectTable = forwardRef(
|
|||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const renderActions = (objectData, actionsDisabled = false) => {
|
|
||||||
return (
|
|
||||||
<Flex gap='small' align='center' justify='center'>
|
|
||||||
{rowActions.map((action, index) => {
|
|
||||||
const denied = !hasActionPermission(userProfile, model, action.name)
|
|
||||||
var disabled = denied
|
|
||||||
if (action.disabled) {
|
|
||||||
if (typeof action.disabled === 'function') {
|
|
||||||
disabled =
|
|
||||||
denied ||
|
|
||||||
action.disabled({
|
|
||||||
...objectData,
|
|
||||||
_user: userProfile
|
|
||||||
}) ||
|
|
||||||
actionsDisabled
|
|
||||||
} else {
|
|
||||||
disabled = denied || action.disabled || actionsDisabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Tooltip key={index} title={action.label}>
|
|
||||||
<Button
|
|
||||||
icon={
|
|
||||||
action.icon ? (
|
|
||||||
createElement(action.icon)
|
|
||||||
) : (
|
|
||||||
<QuestionCircleIcon />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={disabled || objectData?.isSkeleton}
|
|
||||||
type={'text'}
|
|
||||||
size={'small'}
|
|
||||||
danger={action?.danger || false}
|
|
||||||
onClick={() => {
|
|
||||||
if (denied) return
|
|
||||||
if (
|
|
||||||
onRowAction &&
|
|
||||||
onRowAction(action, objectData) !== false
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
callAction(
|
|
||||||
action,
|
|
||||||
{ ...objectData, _user: userProfile },
|
|
||||||
type
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchPage = useCallback(
|
const fetchPage = useCallback(
|
||||||
async (pageNum = 1, filter = null, sorter = null) => {
|
async (pageNum = 1, filter = null, sorter = null) => {
|
||||||
if (filter == null) {
|
if (filter == null) {
|
||||||
@ -1254,7 +1264,7 @@ const ObjectTable = forwardRef(
|
|||||||
|
|
||||||
// Subscribe to all object updates for this type (list/cards only)
|
// Subscribe to all object updates for this type (list/cards only)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isKanban || connected !== true || !type) return
|
if (isKanban || connected !== true || !type || !isTabActive) return
|
||||||
|
|
||||||
const unsubscribe = subscribeToAllObjectUpdates(
|
const unsubscribe = subscribeToAllObjectUpdates(
|
||||||
type,
|
type,
|
||||||
@ -1269,10 +1279,10 @@ const ObjectTable = forwardRef(
|
|||||||
subscribeToAllObjectUpdatesRef.current = null
|
subscribeToAllObjectUpdatesRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [type, connected, subscribeToAllObjectUpdates, isKanban])
|
}, [type, connected, subscribeToAllObjectUpdates, isKanban, isTabActive])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isKanban || connected !== true) return
|
if (isKanban || connected !== true || !isTabActive) return
|
||||||
if (subscribedTypeRef.current === type) return
|
if (subscribedTypeRef.current === type) return
|
||||||
|
|
||||||
const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
|
const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
|
||||||
@ -1291,7 +1301,7 @@ const ObjectTable = forwardRef(
|
|||||||
subscribedTypeRef.current = null
|
subscribedTypeRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [type, connected, isKanban])
|
}, [type, connected, isKanban, isTabActive])
|
||||||
|
|
||||||
const updateData = useCallback(
|
const updateData = useCallback(
|
||||||
(id, updatedData) => {
|
(id, updatedData) => {
|
||||||
@ -1706,7 +1716,8 @@ const ObjectTable = forwardRef(
|
|||||||
return () => registerPageSorter({})
|
return () => registerPageSorter({})
|
||||||
}, [effectiveSorter, registerPageSorter, registerObjectListSorter])
|
}, [effectiveSorter, registerPageSorter, registerObjectListSorter])
|
||||||
|
|
||||||
const getFilterDropdown = ({
|
const getFilterDropdown = useCallback(
|
||||||
|
({
|
||||||
setSelectedKeys,
|
setSelectedKeys,
|
||||||
selectedKeys,
|
selectedKeys,
|
||||||
confirm,
|
confirm,
|
||||||
@ -1727,6 +1738,8 @@ const ObjectTable = forwardRef(
|
|||||||
filter={effectiveFilter}
|
filter={effectiveFilter}
|
||||||
masterFilter={resolvedMasterFilter}
|
masterFilter={resolvedMasterFilter}
|
||||||
/>
|
/>
|
||||||
|
),
|
||||||
|
[effectiveFilter, resolvedMasterFilter, type]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
@ -1858,9 +1871,47 @@ const ObjectTable = forwardRef(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const modelProperties = getModelProperties(type)
|
const modelProperties = useMemo(() => getModelProperties(type), [type])
|
||||||
// Table columns from model properties
|
|
||||||
const columnsWithSkeleton = [
|
useEffect(() => {
|
||||||
|
pagesRef.current = pages
|
||||||
|
}, [pages])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!expandHeight || isCards || isKanban) return
|
||||||
|
|
||||||
|
const findAntTableBody = (element) => {
|
||||||
|
if (!element) return null
|
||||||
|
if (element.classList?.contains('ant-table-body')) return element
|
||||||
|
for (const child of element.children) {
|
||||||
|
const found = findAntTableBody(child)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = tableRef.current?.nativeElement ?? tableRef.current
|
||||||
|
const tableBody = findAntTableBody(root)
|
||||||
|
if (!tableBody) return
|
||||||
|
|
||||||
|
tableBody.style.minHeight = adjustedScrollHeight
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tableBody.style.minHeight = ''
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
expandHeight,
|
||||||
|
adjustedScrollHeight,
|
||||||
|
isCards,
|
||||||
|
isKanban,
|
||||||
|
loading,
|
||||||
|
tableData
|
||||||
|
])
|
||||||
|
|
||||||
|
const hasRealTableRows = tableData.some((item) => !item?.isSkeleton)
|
||||||
|
|
||||||
|
const columnsWithSkeleton = useMemo(() => {
|
||||||
|
const columns = [
|
||||||
{
|
{
|
||||||
title:
|
title:
|
||||||
loading || lazyLoading ? (
|
loading || lazyLoading ? (
|
||||||
@ -1919,51 +1970,14 @@ const ObjectTable = forwardRef(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
pagesRef.current = pages
|
|
||||||
}, [pages])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!expandHeight || isCards || isKanban) return
|
|
||||||
|
|
||||||
const findAntTableBody = (element) => {
|
|
||||||
if (!element) return null
|
|
||||||
if (element.classList?.contains('ant-table-body')) return element
|
|
||||||
for (const child of element.children) {
|
|
||||||
const found = findAntTableBody(child)
|
|
||||||
if (found) return found
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = tableRef.current?.nativeElement ?? tableRef.current
|
|
||||||
const tableBody = findAntTableBody(root)
|
|
||||||
if (!tableBody) return
|
|
||||||
|
|
||||||
tableBody.style.minHeight = adjustedScrollHeight
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
tableBody.style.minHeight = ''
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
expandHeight,
|
|
||||||
adjustedScrollHeight,
|
|
||||||
isCards,
|
|
||||||
isKanban,
|
|
||||||
loading,
|
|
||||||
tableData
|
|
||||||
])
|
|
||||||
|
|
||||||
// Add columns in the order specified by model.columns
|
|
||||||
model.columns.forEach((colName) => {
|
model.columns.forEach((colName) => {
|
||||||
const prop = modelProperties.find((p) => p.name === colName)
|
const prop = modelProperties.find((p) => p.name === colName)
|
||||||
if (prop) {
|
if (prop) {
|
||||||
// Check if column should be visible based on visibleColumns prop
|
|
||||||
if (
|
if (
|
||||||
Object.keys(visibleColumns).length > 0 &&
|
Object.keys(visibleColumns).length > 0 &&
|
||||||
visibleColumns[prop.name] === false
|
visibleColumns[prop.name] === false
|
||||||
) {
|
) {
|
||||||
return // Skip this column if it's not visible
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var fixed = prop.columnFixed || undefined
|
var fixed = prop.columnFixed || undefined
|
||||||
@ -1988,10 +2002,8 @@ const ObjectTable = forwardRef(
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Check if this property should be filterable based on model.filters
|
|
||||||
const isFilterable = model.filters && model.filters.includes(prop.name)
|
|
||||||
|
|
||||||
// Check if this property should be sortable based on model.sorters
|
const isFilterable = model.filters && model.filters.includes(prop.name)
|
||||||
const isSortable = model.sorters && model.sorters.includes(prop.name)
|
const isSortable = model.sorters && model.sorters.includes(prop.name)
|
||||||
|
|
||||||
const columnConfig = {
|
const columnConfig = {
|
||||||
@ -2056,16 +2068,12 @@ const ObjectTable = forwardRef(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
columnsWithSkeleton.push(columnConfig)
|
columns.push(columnConfig)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (
|
if (showActions && rowActions.length > 0 && hasRealTableRows) {
|
||||||
showActions &&
|
columns.push({
|
||||||
rowActions.length > 0 &&
|
|
||||||
tableData.some((item) => !item?.isSkeleton)
|
|
||||||
) {
|
|
||||||
columnsWithSkeleton.push({
|
|
||||||
title: (
|
title: (
|
||||||
<Flex gap='small' align='center' justify='center'>
|
<Flex gap='small' align='center' justify='center'>
|
||||||
<ActionsIcon />
|
<ActionsIcon />
|
||||||
@ -2073,13 +2081,39 @@ const ObjectTable = forwardRef(
|
|||||||
),
|
),
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
width: 20 + rowActions.length * 30, // Adjust width based on number of actions
|
width: 20 + rowActions.length * 30,
|
||||||
render: (record) => {
|
render: (record) => {
|
||||||
return renderActions(record)
|
return renderActions(record)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return columns
|
||||||
|
}, [
|
||||||
|
bulkSelection,
|
||||||
|
canBulkSelect,
|
||||||
|
effectiveFilter,
|
||||||
|
getFilterDropdown,
|
||||||
|
hasRealTableRows,
|
||||||
|
isCards,
|
||||||
|
isEditing,
|
||||||
|
isMobile,
|
||||||
|
lazyLoading,
|
||||||
|
loading,
|
||||||
|
model,
|
||||||
|
modelProperties,
|
||||||
|
renderActions,
|
||||||
|
resolvedMasterFilter,
|
||||||
|
rowActions,
|
||||||
|
selectedRowIdSet,
|
||||||
|
selectedRowIds,
|
||||||
|
showActions,
|
||||||
|
tableSorter,
|
||||||
|
toggleRowSelection,
|
||||||
|
toggleSelectAll,
|
||||||
|
visibleColumns
|
||||||
|
])
|
||||||
|
|
||||||
// Card view rendering
|
// Card view rendering
|
||||||
const [cardColSpan, setCardColSpan] = useState(24)
|
const [cardColSpan, setCardColSpan] = useState(24)
|
||||||
const cardsContainerNodeRef = useRef(null)
|
const cardsContainerNodeRef = useRef(null)
|
||||||
@ -2465,6 +2499,7 @@ const ObjectTable = forwardRef(
|
|||||||
</ObjectTableFilterContext.Provider>
|
</ObjectTableFilterContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
ObjectTable.displayName = 'ObjectTable'
|
ObjectTable.displayName = 'ObjectTable'
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react'
|
} from 'react'
|
||||||
@ -26,6 +27,8 @@ const ActionsContext = createContext()
|
|||||||
const ActionsProvider = ({ children }) => {
|
const ActionsProvider = ({ children }) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const locationRef = useRef(location)
|
||||||
|
locationRef.current = location
|
||||||
const { userProfile } = useContext(AuthContext)
|
const { userProfile } = useContext(AuthContext)
|
||||||
const [currentObject, setCurrentObject] = useState(null)
|
const [currentObject, setCurrentObject] = useState(null)
|
||||||
const [currentObjectType, setCurrentObjectType] = useState(null)
|
const [currentObjectType, setCurrentObjectType] = useState(null)
|
||||||
@ -38,13 +41,14 @@ const ActionsProvider = ({ children }) => {
|
|||||||
const actionObjectType = searchParams.get('actionObjectType')
|
const actionObjectType = searchParams.get('actionObjectType')
|
||||||
|
|
||||||
const clearAction = useCallback(() => {
|
const clearAction = useCallback(() => {
|
||||||
const nextUrl = stripModalActionParams(location.pathname, location.search)
|
const loc = locationRef.current
|
||||||
if (nextUrl !== location.pathname + location.search) {
|
const nextUrl = stripModalActionParams(loc.pathname, loc.search)
|
||||||
|
if (nextUrl !== loc.pathname + loc.search) {
|
||||||
navigate(nextUrl, { replace: true })
|
navigate(nextUrl, { replace: true })
|
||||||
}
|
}
|
||||||
setModalAction(null)
|
setModalAction(null)
|
||||||
lastHandledAction.current = null
|
lastHandledAction.current = null
|
||||||
}, [location.pathname, location.search, navigate])
|
}, [navigate])
|
||||||
|
|
||||||
const handleModalOk = useCallback(() => {
|
const handleModalOk = useCallback(() => {
|
||||||
onModalOk?.()
|
onModalOk?.()
|
||||||
@ -56,16 +60,17 @@ const ActionsProvider = ({ children }) => {
|
|||||||
setCurrentObject(objectData)
|
setCurrentObject(objectData)
|
||||||
setCurrentObjectType(objectType)
|
setCurrentObjectType(objectType)
|
||||||
const model = getModelByName(objectType)
|
const model = getModelByName(objectType)
|
||||||
|
const loc = locationRef.current
|
||||||
const url = buildActionUrl(
|
const url = buildActionUrl(
|
||||||
model,
|
model,
|
||||||
action,
|
action,
|
||||||
objectData._id,
|
objectData._id,
|
||||||
location.pathname,
|
loc.pathname,
|
||||||
location.search
|
loc.search
|
||||||
)
|
)
|
||||||
navigate(url)
|
navigate(url)
|
||||||
},
|
},
|
||||||
[location.pathname, location.search, navigate]
|
[navigate]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -187,9 +192,8 @@ const ActionsProvider = ({ children }) => {
|
|||||||
? {}
|
? {}
|
||||||
: modalObjectData
|
: modalObjectData
|
||||||
|
|
||||||
return (
|
const value = useMemo(
|
||||||
<ActionsContext.Provider
|
() => ({
|
||||||
value={{
|
|
||||||
currentObject,
|
currentObject,
|
||||||
currentObjectType,
|
currentObjectType,
|
||||||
setCurrentObject,
|
setCurrentObject,
|
||||||
@ -197,8 +201,12 @@ const ActionsProvider = ({ children }) => {
|
|||||||
callAction,
|
callAction,
|
||||||
clearAction,
|
clearAction,
|
||||||
setOnModalOk
|
setOnModalOk
|
||||||
}}
|
}),
|
||||||
>
|
[callAction, clearAction, currentObject, currentObjectType]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionsContext.Provider value={value}>
|
||||||
<Modal
|
<Modal
|
||||||
open={resolvedModalAction != null}
|
open={resolvedModalAction != null}
|
||||||
destroyOnHidden={true}
|
destroyOnHidden={true}
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
useContext,
|
useContext,
|
||||||
useRef,
|
useRef,
|
||||||
useCallback
|
useCallback,
|
||||||
|
useMemo
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import io from 'socket.io-client'
|
import io from 'socket.io-client'
|
||||||
import { message, Modal, Space, Button, Typography, Flex } from 'antd'
|
import { message, Modal, Space, Button, Typography, Flex } from 'antd'
|
||||||
@ -133,10 +134,244 @@ const getObjectTypeSubscriptionArgs = (filterOrCallback, callback) => {
|
|||||||
const getObjectEndpoint = (type) =>
|
const getObjectEndpoint = (type) =>
|
||||||
getModelByName(type)?.endpoint || `${type.toLowerCase()}s`
|
getModelByName(type)?.endpoint || `${type.toLowerCase()}s`
|
||||||
|
|
||||||
|
const formatFileName = (name) => {
|
||||||
|
if (!name || typeof name !== 'string') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = name.replace(/[^a-zA-Z0-9.\-_\s]/g, '')
|
||||||
|
const normalized = cleaned.trim().replace(/\s+/g, '_')
|
||||||
|
return normalized.slice(0, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_METHOD_KEYS = [
|
||||||
|
'getUserSettings',
|
||||||
|
'updateUserSettings',
|
||||||
|
'setObjectActivity',
|
||||||
|
'clearObjectActivity',
|
||||||
|
'fetchObjectActivities',
|
||||||
|
'updateObject',
|
||||||
|
'updateMultipleObjects',
|
||||||
|
'createObject',
|
||||||
|
'getObjectFunction',
|
||||||
|
'sendObjectFunction',
|
||||||
|
'deleteObject',
|
||||||
|
'deleteObjects',
|
||||||
|
'subscribeToObjectUpdates',
|
||||||
|
'subscribeToAllObjectUpdates',
|
||||||
|
'subscribeToObjectEvent',
|
||||||
|
'subscribeToObjectTypeUpdates',
|
||||||
|
'subscribeToObjectActivity',
|
||||||
|
'subscribeToModelStats',
|
||||||
|
'fetchObject',
|
||||||
|
'fetchObjects',
|
||||||
|
'getObjectNeighbors',
|
||||||
|
'fetchObjectsByProperty',
|
||||||
|
'searchObjects',
|
||||||
|
'fetchSpotlightData',
|
||||||
|
'getModelStats',
|
||||||
|
'getModelPropertyValues',
|
||||||
|
'getModelHistory',
|
||||||
|
'showError',
|
||||||
|
'fetchFileContent',
|
||||||
|
'fetchFileThumbnail',
|
||||||
|
'exportToExcel',
|
||||||
|
'exportToCsv',
|
||||||
|
'fetchTemplatePreview',
|
||||||
|
'fetchTemplateIntellisense',
|
||||||
|
'fetchTemplateFormat',
|
||||||
|
'fetchTemplatePDF',
|
||||||
|
'fetchTemplateDownload',
|
||||||
|
'fetchNotes',
|
||||||
|
'downloadTemplate',
|
||||||
|
'downloadTemplatePDF',
|
||||||
|
'fetchHostOTP',
|
||||||
|
'sendObjectAction',
|
||||||
|
'uploadFile',
|
||||||
|
'flushFile',
|
||||||
|
'formatFileName',
|
||||||
|
'createUserNotifier',
|
||||||
|
'deleteUserNotifier',
|
||||||
|
'fetchUserNotifiersForObject',
|
||||||
|
'fetchAllUserNotifiersForObject',
|
||||||
|
'toggleUserNotifier',
|
||||||
|
'editUserNotifier',
|
||||||
|
'fetchObjectViews',
|
||||||
|
'createObjectView',
|
||||||
|
'updateObjectView',
|
||||||
|
'deleteObjectView',
|
||||||
|
'fetchNotificationsApi',
|
||||||
|
'markNotificationAsReadApi',
|
||||||
|
'markAllNotificationsAsReadApi',
|
||||||
|
'deleteNotificationApi',
|
||||||
|
'deleteAllNotificationsApi',
|
||||||
|
'registerNotificationListener',
|
||||||
|
'unregisterNotificationListener',
|
||||||
|
'getMarketplaceAuthUrl',
|
||||||
|
'refreshMarketplaceAuth',
|
||||||
|
'completeAppLaunchSession',
|
||||||
|
'getAppLaunchSession',
|
||||||
|
'fetchAppUpdateBranches',
|
||||||
|
'fetchAppUpdateCurrent',
|
||||||
|
'fetchWsServerVersion',
|
||||||
|
'fetchApiServerVersion'
|
||||||
|
]
|
||||||
|
|
||||||
|
const createStableApiMethods = (methodsRef) => {
|
||||||
|
const bound = {}
|
||||||
|
for (const key of API_METHOD_KEYS) {
|
||||||
|
bound[key] = (...args) => methodsRef.current[key]?.(...args)
|
||||||
|
}
|
||||||
|
return bound
|
||||||
|
}
|
||||||
|
|
||||||
|
const LaunchSessionCompleter = ({ completeAppLaunchSession }) => {
|
||||||
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { token, authenticated } = useContext(AuthContext)
|
||||||
|
const completedLaunchSessionsRef = useRef(new Set())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const launchSession = new URLSearchParams(location.search).get(
|
||||||
|
'launchSession'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
authenticated !== true ||
|
||||||
|
!token ||
|
||||||
|
!launchSession ||
|
||||||
|
completedLaunchSessionsRef.current.has(launchSession)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
completedLaunchSessionsRef.current.add(launchSession)
|
||||||
|
|
||||||
|
completeAppLaunchSession(launchSession)
|
||||||
|
.then(() => {
|
||||||
|
const searchParams = new URLSearchParams(location.search)
|
||||||
|
searchParams.delete('launchSession')
|
||||||
|
const newSearch = searchParams.toString()
|
||||||
|
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
||||||
|
navigate(newPath, { replace: true })
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error('Failed to complete app launch session:', err)
|
||||||
|
completedLaunchSessionsRef.current.delete(launchSession)
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
token,
|
||||||
|
authenticated,
|
||||||
|
completeAppLaunchSession,
|
||||||
|
location.search,
|
||||||
|
location.pathname,
|
||||||
|
navigate
|
||||||
|
])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchSessionCompleter.propTypes = {
|
||||||
|
completeAppLaunchSession: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConnectionIssueModal = ({
|
||||||
|
open,
|
||||||
|
delayMs,
|
||||||
|
cycle,
|
||||||
|
isReconnecting,
|
||||||
|
onReconnect
|
||||||
|
}) => {
|
||||||
|
const [reconnectRemainingMs, setReconnectRemainingMs] = useState(delayMs)
|
||||||
|
const [reconnectProgress, setReconnectProgress] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || isReconnecting) return undefined
|
||||||
|
|
||||||
|
const startedAt = Date.now()
|
||||||
|
setReconnectRemainingMs(delayMs)
|
||||||
|
setReconnectProgress(0)
|
||||||
|
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
const elapsed = Date.now() - startedAt
|
||||||
|
const remaining = Math.max(0, delayMs - elapsed)
|
||||||
|
setReconnectRemainingMs(remaining)
|
||||||
|
setReconnectProgress(Math.min(100, (elapsed / delayMs) * 100))
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId)
|
||||||
|
}, [open, isReconnecting, delayMs, cycle])
|
||||||
|
|
||||||
|
const reconnectSecondsRemaining = Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(reconnectRemainingMs / 1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
!isReconnecting ? (
|
||||||
|
<Space size={'middle'}>
|
||||||
|
<ExclamationOctagonIcon />
|
||||||
|
Connection Lost
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
open={open}
|
||||||
|
style={{ maxWidth: !isReconnecting ? 480 : 260 }}
|
||||||
|
zIndex={3000}
|
||||||
|
closable={false}
|
||||||
|
height={isReconnecting ? 20 : undefined}
|
||||||
|
centered
|
||||||
|
className={isReconnecting ? 'loading-modal' : undefined}
|
||||||
|
maskClosable={false}
|
||||||
|
getContainer={() => document.body}
|
||||||
|
footer={
|
||||||
|
!isReconnecting
|
||||||
|
? [
|
||||||
|
<Button key='reconnect' loading={isReconnecting} onClick={onReconnect}>
|
||||||
|
Reconnect
|
||||||
|
</Button>
|
||||||
|
]
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!isReconnecting ? (
|
||||||
|
<Flex vertical gap='middle'>
|
||||||
|
<Text>
|
||||||
|
{`Lost connection to the API server. Reconnecting in ${reconnectSecondsRemaining} second${
|
||||||
|
reconnectSecondsRemaining === 1 ? '' : 's'
|
||||||
|
}...`}
|
||||||
|
</Text>
|
||||||
|
<ProgressDisplay
|
||||||
|
percent={100 - reconnectProgress}
|
||||||
|
showInfo={false}
|
||||||
|
status={'exception'}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Space size={'middle'}>
|
||||||
|
<LoadingOutlined />
|
||||||
|
<Text style={{ margin: 0 }}>Reconnecting, please wait...</Text>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionIssueModal.propTypes = {
|
||||||
|
open: PropTypes.bool,
|
||||||
|
delayMs: PropTypes.number,
|
||||||
|
cycle: PropTypes.number,
|
||||||
|
isReconnecting: PropTypes.bool,
|
||||||
|
onReconnect: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
const ApiServerContext = createContext()
|
const ApiServerContext = createContext()
|
||||||
|
|
||||||
const ApiServerProvider = ({ children }) => {
|
const ApiServerProvider = ({ children }) => {
|
||||||
const location = useLocation()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const {
|
const {
|
||||||
token,
|
token,
|
||||||
@ -162,16 +397,12 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
const subscribedActivityCallbacksRef = useRef(new Map())
|
const subscribedActivityCallbacksRef = useRef(new Map())
|
||||||
const subscribedActivityServerSubscriptionsRef = useRef(new Set())
|
const subscribedActivityServerSubscriptionsRef = useRef(new Set())
|
||||||
const notificationListenersRef = useRef(new Set())
|
const notificationListenersRef = useRef(new Set())
|
||||||
const completedLaunchSessionsRef = useRef(new Set())
|
|
||||||
const [connectionIssue, setConnectionIssue] = useState(false)
|
const [connectionIssue, setConnectionIssue] = useState(false)
|
||||||
const [reconnectProgress, setReconnectProgress] = useState(0)
|
const [reconnectDelayMs, setReconnectDelayMs] = useState(RECONNECT_DELAYS_MS[0])
|
||||||
const [reconnectRemainingMs, setReconnectRemainingMs] = useState(
|
const [reconnectCycle, setReconnectCycle] = useState(0)
|
||||||
RECONNECT_DELAYS_MS[0]
|
|
||||||
)
|
|
||||||
const [isReconnecting, setIsReconnecting] = useState(false)
|
const [isReconnecting, setIsReconnecting] = useState(false)
|
||||||
const reconnectAttemptRef = useRef(0)
|
const reconnectAttemptRef = useRef(0)
|
||||||
const reconnectTimerRef = useRef(null)
|
const reconnectTimerRef = useRef(null)
|
||||||
const reconnectProgressTimerRef = useRef(null)
|
|
||||||
const reconnectScheduledRef = useRef(false)
|
const reconnectScheduledRef = useRef(false)
|
||||||
const isReconnectingRef = useRef(false)
|
const isReconnectingRef = useRef(false)
|
||||||
const hasConnectedOnceRef = useRef(false)
|
const hasConnectedOnceRef = useRef(false)
|
||||||
@ -349,10 +580,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
clearTimeout(reconnectTimerRef.current)
|
clearTimeout(reconnectTimerRef.current)
|
||||||
reconnectTimerRef.current = null
|
reconnectTimerRef.current = null
|
||||||
}
|
}
|
||||||
if (reconnectProgressTimerRef.current) {
|
|
||||||
clearInterval(reconnectProgressTimerRef.current)
|
|
||||||
reconnectProgressTimerRef.current = null
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const resetReconnectState = useCallback(() => {
|
const resetReconnectState = useCallback(() => {
|
||||||
@ -361,25 +588,16 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
reconnectScheduledRef.current = false
|
reconnectScheduledRef.current = false
|
||||||
isReconnectingRef.current = false
|
isReconnectingRef.current = false
|
||||||
setIsReconnecting(false)
|
setIsReconnecting(false)
|
||||||
setReconnectProgress(0)
|
setReconnectDelayMs(RECONNECT_DELAYS_MS[0])
|
||||||
setReconnectRemainingMs(RECONNECT_DELAYS_MS[0])
|
|
||||||
}, [clearReconnectTimers])
|
}, [clearReconnectTimers])
|
||||||
|
|
||||||
const startReconnectCountdown = useCallback(
|
const startReconnectCountdown = useCallback(
|
||||||
(delayMs) => {
|
(delayMs) => {
|
||||||
clearReconnectTimers()
|
clearReconnectTimers()
|
||||||
const startedAt = Date.now()
|
setReconnectDelayMs(delayMs)
|
||||||
setReconnectRemainingMs(delayMs)
|
setReconnectCycle((current) => current + 1)
|
||||||
setReconnectProgress(0)
|
|
||||||
setIsReconnecting(false)
|
setIsReconnecting(false)
|
||||||
|
|
||||||
reconnectProgressTimerRef.current = setInterval(() => {
|
|
||||||
const elapsed = Date.now() - startedAt
|
|
||||||
const remaining = Math.max(0, delayMs - elapsed)
|
|
||||||
setReconnectRemainingMs(remaining)
|
|
||||||
setReconnectProgress(Math.min(100, (elapsed / delayMs) * 100))
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
reconnectTimerRef.current = setTimeout(() => {
|
reconnectTimerRef.current = setTimeout(() => {
|
||||||
attemptReconnectRef.current?.()
|
attemptReconnectRef.current?.()
|
||||||
}, delayMs)
|
}, delayMs)
|
||||||
@ -429,7 +647,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
isReconnectingRef.current = true
|
isReconnectingRef.current = true
|
||||||
reconnectAttemptRef.current += 1
|
reconnectAttemptRef.current += 1
|
||||||
setIsReconnecting(true)
|
setIsReconnecting(true)
|
||||||
setReconnectProgress(100)
|
|
||||||
setConnecting(true)
|
setConnecting(true)
|
||||||
setConnectionIssue(true)
|
setConnectionIssue(true)
|
||||||
|
|
||||||
@ -2769,43 +2986,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
[token]
|
[token]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const launchSession = new URLSearchParams(location.search).get(
|
|
||||||
'launchSession'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
authenticated !== true ||
|
|
||||||
!token ||
|
|
||||||
!launchSession ||
|
|
||||||
completedLaunchSessionsRef.current.has(launchSession)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
completedLaunchSessionsRef.current.add(launchSession)
|
|
||||||
|
|
||||||
completeAppLaunchSession(launchSession)
|
|
||||||
.then(() => {
|
|
||||||
const searchParams = new URLSearchParams(location.search)
|
|
||||||
searchParams.delete('launchSession')
|
|
||||||
const newSearch = searchParams.toString()
|
|
||||||
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
|
||||||
navigate(newPath, { replace: true })
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error('Failed to complete app launch session:', err)
|
|
||||||
completedLaunchSessionsRef.current.delete(launchSession)
|
|
||||||
})
|
|
||||||
}, [
|
|
||||||
token,
|
|
||||||
authenticated,
|
|
||||||
completeAppLaunchSession,
|
|
||||||
location.search,
|
|
||||||
location.pathname,
|
|
||||||
navigate
|
|
||||||
])
|
|
||||||
|
|
||||||
const getAppLaunchSession = useCallback(async (launchSession) => {
|
const getAppLaunchSession = useCallback(async (launchSession) => {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`${config.backendUrl}/applaunch/${launchSession}`,
|
`${config.backendUrl}/applaunch/${launchSession}`,
|
||||||
@ -2924,36 +3104,8 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reconnectSecondsRemaining = Math.max(
|
const apiMethodsRef = useRef({})
|
||||||
1,
|
apiMethodsRef.current = {
|
||||||
Math.ceil(reconnectRemainingMs / 1000)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sanitize a string so it is safe to use as a filename on most file systems
|
|
||||||
const formatFileName = (name) => {
|
|
||||||
if (!name || typeof name !== 'string') {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove characters that are problematic on most common file systems
|
|
||||||
const cleaned = name.replace(/[^a-zA-Z0-9.\-_\s]/g, '')
|
|
||||||
|
|
||||||
// Normalize whitespace to single underscores
|
|
||||||
const normalized = cleaned.trim().replace(/\s+/g, '_')
|
|
||||||
|
|
||||||
// Most file systems limit filenames to 255 characters
|
|
||||||
return normalized.slice(0, 255)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ApiServerContext.Provider
|
|
||||||
value={{
|
|
||||||
apiServer: socketRef.current,
|
|
||||||
error,
|
|
||||||
connecting,
|
|
||||||
connected,
|
|
||||||
userSettings,
|
|
||||||
userSettingsLoaded,
|
|
||||||
getUserSettings,
|
getUserSettings,
|
||||||
updateUserSettings,
|
updateUserSettings,
|
||||||
setObjectActivity,
|
setObjectActivity,
|
||||||
@ -2981,7 +3133,6 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
getModelStats,
|
getModelStats,
|
||||||
getModelPropertyValues,
|
getModelPropertyValues,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
fetchLoading,
|
|
||||||
showError,
|
showError,
|
||||||
fetchFileContent,
|
fetchFileContent,
|
||||||
fetchFileThumbnail,
|
fetchFileThumbnail,
|
||||||
@ -3025,66 +3176,48 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
fetchAppUpdateCurrent,
|
fetchAppUpdateCurrent,
|
||||||
fetchWsServerVersion,
|
fetchWsServerVersion,
|
||||||
fetchApiServerVersion
|
fetchApiServerVersion
|
||||||
}}
|
|
||||||
>
|
|
||||||
{contextHolder}
|
|
||||||
{children}
|
|
||||||
<Modal
|
|
||||||
title={
|
|
||||||
!isReconnecting ? (
|
|
||||||
<Space size={'middle'}>
|
|
||||||
<ExclamationOctagonIcon />
|
|
||||||
Connection Lost
|
|
||||||
</Space>
|
|
||||||
) : (
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
open={Boolean(token) && authenticated == true && connectionIssue}
|
|
||||||
style={{ maxWidth: !isReconnecting ? 480 : 260 }}
|
const stableApiRef = useRef(null)
|
||||||
zIndex={3000}
|
if (stableApiRef.current == null) {
|
||||||
closable={false}
|
stableApiRef.current = createStableApiMethods(apiMethodsRef)
|
||||||
height={isReconnecting ? 20 : undefined}
|
}
|
||||||
centered
|
|
||||||
className={isReconnecting ? 'loading-modal' : undefined}
|
const value = useMemo(
|
||||||
maskClosable={false}
|
() => ({
|
||||||
getContainer={() => document.body}
|
...stableApiRef.current,
|
||||||
footer={
|
apiServer: socketRef.current,
|
||||||
!isReconnecting
|
error,
|
||||||
? [
|
connecting,
|
||||||
<Button
|
connected,
|
||||||
key='reconnect'
|
userSettings,
|
||||||
loading={isReconnecting}
|
userSettingsLoaded,
|
||||||
onClick={() => attemptReconnect()}
|
fetchLoading
|
||||||
>
|
}),
|
||||||
Reconnect
|
[
|
||||||
</Button>
|
connecting,
|
||||||
|
connected,
|
||||||
|
error,
|
||||||
|
fetchLoading,
|
||||||
|
userSettings,
|
||||||
|
userSettingsLoaded
|
||||||
]
|
]
|
||||||
: false
|
)
|
||||||
}
|
|
||||||
>
|
return (
|
||||||
{!isReconnecting ? (
|
<ApiServerContext.Provider value={value}>
|
||||||
<Flex vertical gap='middle'>
|
{contextHolder}
|
||||||
<Text>
|
<LaunchSessionCompleter
|
||||||
{isReconnecting
|
completeAppLaunchSession={completeAppLaunchSession}
|
||||||
? 'Reconnecting to the API server...'
|
/>
|
||||||
: `Lost connection to the API server. Reconnecting in ${reconnectSecondsRemaining} second${
|
{children}
|
||||||
reconnectSecondsRemaining === 1 ? '' : 's'
|
<ConnectionIssueModal
|
||||||
}...`}
|
open={Boolean(token) && authenticated == true && connectionIssue}
|
||||||
</Text>
|
delayMs={reconnectDelayMs}
|
||||||
<ProgressDisplay
|
cycle={reconnectCycle}
|
||||||
percent={isReconnecting ? 0 : 100 - reconnectProgress}
|
isReconnecting={isReconnecting}
|
||||||
showInfo={false}
|
onReconnect={attemptReconnect}
|
||||||
status={'exception'}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
|
||||||
) : (
|
|
||||||
<Space size={'middle'}>
|
|
||||||
<LoadingOutlined />
|
|
||||||
<Text style={{ margin: 0 }}>Reconnecting, please wait...</Text>
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<Space size={'middle'}>
|
<Space size={'middle'}>
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useContext,
|
useContext,
|
||||||
useRef
|
useRef,
|
||||||
|
useMemo
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {
|
import {
|
||||||
@ -112,6 +113,8 @@ const AuthProvider = ({ children }) => {
|
|||||||
} = useContext(ElectronContext)
|
} = useContext(ElectronContext)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const locationRef = useRef(location)
|
||||||
|
locationRef.current = location
|
||||||
const sessionRef = useRef({
|
const sessionRef = useRef({
|
||||||
token,
|
token,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
@ -432,9 +435,11 @@ const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
// Login using query parameters
|
// Login using query parameters
|
||||||
const loginWithSSO = useCallback(
|
const loginWithSSO = useCallback(
|
||||||
(redirectUri = location.pathname + location.search) => {
|
(redirectUri) => {
|
||||||
|
const loc = locationRef.current
|
||||||
|
const nextRedirectUri = redirectUri || loc.pathname + loc.search
|
||||||
messageApi.info('Logging in with tombutcher.work')
|
messageApi.info('Logging in with tombutcher.work')
|
||||||
const loginUrl = `${config.backendUrl}/auth/${redirectType}/login?redirect_uri=${encodeURIComponent(redirectUri)}`
|
const loginUrl = `${config.backendUrl}/auth/${redirectType}/login?redirect_uri=${encodeURIComponent(nextRedirectUri)}`
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
logger.debug('Opening external url...')
|
logger.debug('Opening external url...')
|
||||||
openExternalUrl(loginUrl)
|
openExternalUrl(loginUrl)
|
||||||
@ -444,14 +449,7 @@ const AuthProvider = ({ children }) => {
|
|||||||
window.location.href = loginUrl
|
window.location.href = loginUrl
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[redirectType, messageApi, openExternalUrl, isElectron]
|
||||||
redirectType,
|
|
||||||
messageApi,
|
|
||||||
openExternalUrl,
|
|
||||||
isElectron,
|
|
||||||
location.search,
|
|
||||||
location.pathname
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const getLoginToken = useCallback(
|
const getLoginToken = useCallback(
|
||||||
@ -461,7 +459,6 @@ const AuthProvider = ({ children }) => {
|
|||||||
setShowSessionExpiredModal(false)
|
setShowSessionExpiredModal(false)
|
||||||
setAuthError(null)
|
setAuthError(null)
|
||||||
try {
|
try {
|
||||||
// Make a call to your backend to check auth status
|
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`${config.backendUrl}/auth/${redirectType}/token?code=${code}`
|
`${config.backendUrl}/auth/${redirectType}/token?code=${code}`
|
||||||
)
|
)
|
||||||
@ -480,7 +477,6 @@ const AuthProvider = ({ children }) => {
|
|||||||
setAuthenticated(true)
|
setAuthenticated(true)
|
||||||
setShowUnauthorizedModal(false)
|
setShowUnauthorizedModal(false)
|
||||||
|
|
||||||
// Persist session (cookies on web, electron storage on desktop)
|
|
||||||
const persisted = await persistSession({
|
const persisted = await persistSession({
|
||||||
token: nextToken,
|
token: nextToken,
|
||||||
expiresAt: nextExpiresAt,
|
expiresAt: nextExpiresAt,
|
||||||
@ -492,10 +488,11 @@ const AuthProvider = ({ children }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(location.search)
|
const loc = locationRef.current
|
||||||
|
const searchParams = new URLSearchParams(loc.search)
|
||||||
searchParams.delete('authCode')
|
searchParams.delete('authCode')
|
||||||
const newSearch = searchParams.toString()
|
const newSearch = searchParams.toString()
|
||||||
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
const newPath = loc.pathname + (newSearch ? `?${newSearch}` : '')
|
||||||
navigate(newPath, { replace: true })
|
navigate(newPath, { replace: true })
|
||||||
} else {
|
} else {
|
||||||
setAuthenticated(false)
|
setAuthenticated(false)
|
||||||
@ -520,14 +517,7 @@ const AuthProvider = ({ children }) => {
|
|||||||
setRetreivedTokenFromCookies(true)
|
setRetreivedTokenFromCookies(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[navigate, messageApi, persistSession, redirectType]
|
||||||
navigate,
|
|
||||||
location.search,
|
|
||||||
location.pathname,
|
|
||||||
messageApi,
|
|
||||||
persistSession,
|
|
||||||
redirectType
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Function to check if the user is logged in
|
// Function to check if the user is logged in
|
||||||
@ -805,12 +795,8 @@ const AuthProvider = ({ children }) => {
|
|||||||
retreivedTokenFromCookies
|
retreivedTokenFromCookies
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
const value = useMemo(
|
||||||
<>
|
() => ({
|
||||||
{contextHolder}
|
|
||||||
{notificationContextHolder}
|
|
||||||
<AuthContext.Provider
|
|
||||||
value={{
|
|
||||||
authenticated,
|
authenticated,
|
||||||
authInitialized: retreivedTokenFromCookies,
|
authInitialized: retreivedTokenFromCookies,
|
||||||
setUnauthenticated,
|
setUnauthenticated,
|
||||||
@ -822,8 +808,26 @@ const AuthProvider = ({ children }) => {
|
|||||||
setUserProfile,
|
setUserProfile,
|
||||||
profileImageUrl,
|
profileImageUrl,
|
||||||
logout
|
logout
|
||||||
}}
|
}),
|
||||||
>
|
[
|
||||||
|
authenticated,
|
||||||
|
getLoginToken,
|
||||||
|
loading,
|
||||||
|
loginWithSSO,
|
||||||
|
logout,
|
||||||
|
profileImageUrl,
|
||||||
|
retreivedTokenFromCookies,
|
||||||
|
setUnauthenticated,
|
||||||
|
token,
|
||||||
|
userProfile
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{contextHolder}
|
||||||
|
{notificationContextHolder}
|
||||||
|
<AuthContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react'
|
} from 'react'
|
||||||
@ -28,14 +29,17 @@ export const DashboardObjectToolsProvider = ({ children }) => {
|
|||||||
setCurrentObjectToolsState(null)
|
setCurrentObjectToolsState(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
const value = useMemo(
|
||||||
<DashboardObjectToolsContext.Provider
|
() => ({
|
||||||
value={{
|
|
||||||
currentObjectTools,
|
currentObjectTools,
|
||||||
setCurrentObjectTools,
|
setCurrentObjectTools,
|
||||||
clearCurrentObjectTools
|
clearCurrentObjectTools
|
||||||
}}
|
}),
|
||||||
>
|
[clearCurrentObjectTools, currentObjectTools, setCurrentObjectTools]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardObjectToolsContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</DashboardObjectToolsContext.Provider>
|
</DashboardObjectToolsContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { createContext, useContext } from 'react'
|
import { createContext, useCallback, useContext, useMemo } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { message } from 'antd'
|
import { message } from 'antd'
|
||||||
import { isElectrobunDesktop } from '../../../electrobun-bridge'
|
import { isElectrobunDesktop } from '../../../electrobun-bridge'
|
||||||
@ -12,36 +12,52 @@ const MESSAGE_TOP = isElectrobunDesktop() ? 48 : undefined
|
|||||||
export const MessageProvider = ({ children }) => {
|
export const MessageProvider = ({ children }) => {
|
||||||
const [msgApi, contextHolder] = message.useMessage({ top: MESSAGE_TOP })
|
const [msgApi, contextHolder] = message.useMessage({ top: MESSAGE_TOP })
|
||||||
|
|
||||||
const showMessage = (type, content, options = {}) => {
|
const showMessage = useCallback(
|
||||||
|
(type, content, options = {}) => {
|
||||||
return msgApi.open({
|
return msgApi.open({
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
...options
|
...options
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
[msgApi]
|
||||||
|
)
|
||||||
|
|
||||||
const showSuccess = (content, options = {}) =>
|
const showSuccess = useCallback(
|
||||||
showMessage('success', content, options)
|
(content, options = {}) => showMessage('success', content, options),
|
||||||
const showInfo = (content, options = {}) =>
|
[showMessage]
|
||||||
showMessage('info', content, options)
|
)
|
||||||
const showWarning = (content, options = {}) =>
|
const showInfo = useCallback(
|
||||||
showMessage('warning', content, options)
|
(content, options = {}) => showMessage('info', content, options),
|
||||||
const showError = (content, options = {}) =>
|
[showMessage]
|
||||||
showMessage('error', content, options)
|
)
|
||||||
const showLoading = (content, options = {}) =>
|
const showWarning = useCallback(
|
||||||
showMessage('loading', content, options)
|
(content, options = {}) => showMessage('warning', content, options),
|
||||||
|
[showMessage]
|
||||||
|
)
|
||||||
|
const showError = useCallback(
|
||||||
|
(content, options = {}) => showMessage('error', content, options),
|
||||||
|
[showMessage]
|
||||||
|
)
|
||||||
|
const showLoading = useCallback(
|
||||||
|
(content, options = {}) => showMessage('loading', content, options),
|
||||||
|
[showMessage]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
const value = useMemo(
|
||||||
<MessageContext.Provider
|
() => ({
|
||||||
value={{
|
|
||||||
msgApi,
|
msgApi,
|
||||||
showSuccess,
|
showSuccess,
|
||||||
showInfo,
|
showInfo,
|
||||||
showWarning,
|
showWarning,
|
||||||
showError,
|
showError,
|
||||||
showLoading
|
showLoading
|
||||||
}}
|
}),
|
||||||
>
|
[msgApi, showError, showInfo, showLoading, showSuccess, showWarning]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageContext.Provider value={value}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
{children}
|
{children}
|
||||||
</MessageContext.Provider>
|
</MessageContext.Provider>
|
||||||
|
|||||||
@ -183,7 +183,6 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
const tabsRef = useRef(tabs)
|
const tabsRef = useRef(tabs)
|
||||||
const activeTabIdRef = useRef(activeTabId)
|
const activeTabIdRef = useRef(activeTabId)
|
||||||
const locationRef = useRef(location)
|
const locationRef = useRef(location)
|
||||||
const previousActiveTabIdRef = useRef(null)
|
|
||||||
const isRestoringRef = useRef(null)
|
const isRestoringRef = useRef(null)
|
||||||
const tabPaneElsRef = useRef(new Map())
|
const tabPaneElsRef = useRef(new Map())
|
||||||
const captureQueueRef = useRef(Promise.resolve())
|
const captureQueueRef = useRef(Promise.resolve())
|
||||||
@ -471,16 +470,6 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
return next
|
return next
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const previousId = previousActiveTabIdRef.current
|
|
||||||
previousActiveTabIdRef.current = activeTabId
|
|
||||||
if (!hydrated || !previousId || !activeTabId || previousId === activeTabId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!tabsRef.current.some((tab) => tab.id === previousId)) return
|
|
||||||
void captureTabPreview(previousId)
|
|
||||||
}, [activeTabId, captureTabPreview, hydrated])
|
|
||||||
|
|
||||||
const setTabPage = useCallback(
|
const setTabPage = useCallback(
|
||||||
({ title, modelName, iconKey, location: pageLocation } = {}) => {
|
({ title, modelName, iconKey, location: pageLocation } = {}) => {
|
||||||
const activeId = activeTabIdRef.current
|
const activeId = activeTabIdRef.current
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef
|
useRef
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
@ -121,7 +122,14 @@ const NotificationProvider = ({ children }) => {
|
|||||||
|
|
||||||
const unreadCount = notifications.filter((n) => !n.read).length
|
const unreadCount = notifications.filter((n) => !n.read).length
|
||||||
|
|
||||||
// Initial load / when we become authenticated and connected
|
const closeNotificationCenter = useCallback(() => {
|
||||||
|
setNotificationCenterVisible(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
closeNotificationCenter()
|
||||||
|
}, [location.pathname, closeNotificationCenter])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authenticated || !connected) return
|
if (!authenticated || !connected) return
|
||||||
fetchNotificationsRef.current()
|
fetchNotificationsRef.current()
|
||||||
@ -134,10 +142,6 @@ const NotificationProvider = ({ children }) => {
|
|||||||
fetchNotificationsRef.current()
|
fetchNotificationsRef.current()
|
||||||
}, [notificationCenterVisible])
|
}, [notificationCenterVisible])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNotificationCenterVisible(false)
|
|
||||||
}, [location.pathname])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authenticated || !registerNotificationListener) return
|
if (!authenticated || !registerNotificationListener) return
|
||||||
const handleNotification = (notif) => {
|
const handleNotification = (notif) => {
|
||||||
@ -169,9 +173,8 @@ const NotificationProvider = ({ children }) => {
|
|||||||
return unregister
|
return unregister
|
||||||
}, [authenticated, registerNotificationListener, api, deleteNotification])
|
}, [authenticated, registerNotificationListener, api, deleteNotification])
|
||||||
|
|
||||||
return (
|
const value = useMemo(
|
||||||
<NotificationContext.Provider
|
() => ({
|
||||||
value={{
|
|
||||||
notificationCenterVisible,
|
notificationCenterVisible,
|
||||||
toggleNotificationCenter,
|
toggleNotificationCenter,
|
||||||
notifications,
|
notifications,
|
||||||
@ -182,15 +185,30 @@ const NotificationProvider = ({ children }) => {
|
|||||||
deleteAllNotifications,
|
deleteAllNotifications,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
notificationsLoading
|
notificationsLoading
|
||||||
}}
|
}),
|
||||||
>
|
[
|
||||||
|
deleteAllNotifications,
|
||||||
|
deleteNotification,
|
||||||
|
fetchNotifications,
|
||||||
|
markAllNotificationsAsRead,
|
||||||
|
markNotificationAsRead,
|
||||||
|
notificationCenterVisible,
|
||||||
|
notifications,
|
||||||
|
notificationsLoading,
|
||||||
|
toggleNotificationCenter,
|
||||||
|
unreadCount
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationContext.Provider value={value}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
{children}
|
{children}
|
||||||
<Drawer
|
<Drawer
|
||||||
title='Notifications'
|
title='Notifications'
|
||||||
placement='right'
|
placement='right'
|
||||||
width={isMobile ? '100%' : 460}
|
width={isMobile ? '100%' : 460}
|
||||||
onClose={() => setNotificationCenterVisible(false)}
|
onClose={closeNotificationCenter}
|
||||||
open={notificationCenterVisible}
|
open={notificationCenterVisible}
|
||||||
>
|
>
|
||||||
<NotificationCenter
|
<NotificationCenter
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {
|
|||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
useEffect
|
useEffect
|
||||||
} from 'react'
|
} from 'react'
|
||||||
@ -88,24 +89,27 @@ export const ThemeProvider = ({ children }) => {
|
|||||||
)
|
)
|
||||||
}, [showNavigationLabels])
|
}, [showNavigationLabels])
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = useCallback(() => {
|
||||||
if (isSystem) {
|
setIsSystem((current) => {
|
||||||
setIsSystem(false)
|
if (current) return false
|
||||||
}
|
return current
|
||||||
setIsDarkMode(!isDarkMode)
|
})
|
||||||
}
|
setIsDarkMode((current) => !current)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const toggleSystem = () => {
|
const toggleSystem = useCallback(() => {
|
||||||
setIsSystem(!isSystem)
|
setIsSystem((current) => {
|
||||||
if (!isSystem) {
|
const next = !current
|
||||||
// When enabling system theme, update to match system preference
|
if (next) {
|
||||||
setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
|
setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
}
|
}
|
||||||
}
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const toggleCompact = () => {
|
const toggleCompact = useCallback(() => {
|
||||||
setIsCompact(!isCompact)
|
setIsCompact((current) => !current)
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const setThemeMode = useCallback((value) => {
|
const setThemeMode = useCallback((value) => {
|
||||||
if (value === 'system') {
|
if (value === 'system') {
|
||||||
@ -126,19 +130,63 @@ export const ThemeProvider = ({ children }) => {
|
|||||||
setShowNavigationLabelsState(Boolean(value))
|
setShowNavigationLabelsState(Boolean(value))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const getThemeAlgorithm = () => {
|
const getColors = useCallback(() => COLORS, [])
|
||||||
var baseAlgorithm
|
|
||||||
if (isDarkMode == true) {
|
|
||||||
baseAlgorithm = theme.darkAlgorithm
|
|
||||||
} else {
|
|
||||||
baseAlgorithm = theme.defaultAlgorithm
|
|
||||||
}
|
|
||||||
return isCompact ? [theme.compactAlgorithm, baseAlgorithm] : [baseAlgorithm]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getColors = () => {
|
const themeConfig = useMemo(() => {
|
||||||
return COLORS
|
const baseAlgorithm = isDarkMode
|
||||||
|
? theme.darkAlgorithm
|
||||||
|
: theme.defaultAlgorithm
|
||||||
|
return {
|
||||||
|
algorithm: isCompact
|
||||||
|
? [theme.compactAlgorithm, baseAlgorithm]
|
||||||
|
: [baseAlgorithm],
|
||||||
|
token: {
|
||||||
|
...COLORS,
|
||||||
|
colorPrimary:
|
||||||
|
primaryColorOverride == null
|
||||||
|
? COLORS.colorPrimary
|
||||||
|
: primaryColorOverride,
|
||||||
|
borderRadius: '12px'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Layout: {
|
||||||
|
headerBg: isDarkMode ? '#141414' : '#ffffff'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isDarkMode, isCompact, primaryColorOverride])
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
isDarkMode,
|
||||||
|
toggleTheme,
|
||||||
|
isCompact,
|
||||||
|
toggleCompact,
|
||||||
|
isSystem,
|
||||||
|
toggleSystem,
|
||||||
|
setThemeMode,
|
||||||
|
setDensityMode,
|
||||||
|
showNavigationLabels,
|
||||||
|
setShowNavigationLabels,
|
||||||
|
getColors,
|
||||||
|
setPrimaryColorOverride,
|
||||||
|
themeConfig
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
getColors,
|
||||||
|
isCompact,
|
||||||
|
isDarkMode,
|
||||||
|
isSystem,
|
||||||
|
setDensityMode,
|
||||||
|
setShowNavigationLabels,
|
||||||
|
setThemeMode,
|
||||||
|
showNavigationLabels,
|
||||||
|
themeConfig,
|
||||||
|
toggleCompact,
|
||||||
|
toggleSystem,
|
||||||
|
toggleTheme
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
// Set CSS custom properties for theme colors
|
// Set CSS custom properties for theme colors
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -226,41 +274,8 @@ export const ThemeProvider = ({ children }) => {
|
|||||||
)
|
)
|
||||||
}, [isDarkMode, primaryColorOverride])
|
}, [isDarkMode, primaryColorOverride])
|
||||||
|
|
||||||
const themeConfig = {
|
|
||||||
algorithm: getThemeAlgorithm(),
|
|
||||||
token: {
|
|
||||||
...COLORS,
|
|
||||||
colorPrimary:
|
|
||||||
primaryColorOverride == null
|
|
||||||
? COLORS.colorPrimary
|
|
||||||
: primaryColorOverride,
|
|
||||||
borderRadius: '12px'
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Layout: {
|
|
||||||
headerBg: isDarkMode ? '#141414' : '#ffffff'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider
|
<ThemeContext.Provider value={value}>
|
||||||
value={{
|
|
||||||
isDarkMode,
|
|
||||||
toggleTheme,
|
|
||||||
isCompact,
|
|
||||||
toggleCompact,
|
|
||||||
isSystem,
|
|
||||||
toggleSystem,
|
|
||||||
setThemeMode,
|
|
||||||
setDensityMode,
|
|
||||||
showNavigationLabels,
|
|
||||||
setShowNavigationLabels,
|
|
||||||
getColors,
|
|
||||||
setPrimaryColorOverride,
|
|
||||||
themeConfig
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user