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;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
content-visibility: hidden;
|
||||
}
|
||||
|
||||
.dashboard-tab-pane-inactive.dashboard-tab-pane-capturing {
|
||||
@ -4062,6 +4063,7 @@ body.objectKanbanColumnResizing * {
|
||||
pointer-events: none;
|
||||
transform: translate(-100%, 0);
|
||||
z-index: 0;
|
||||
content-visibility: visible;
|
||||
}
|
||||
|
||||
.dashboard-tab-preview-capture-frame {
|
||||
|
||||
@ -128,9 +128,6 @@
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"electrobun": "1.18.1",
|
||||
"electron": "^38.7.1",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-packager": "^17.1.2",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"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
|
||||
}
|
||||
|
||||
const AppContent = () => {
|
||||
const { themeConfig } = useThemeContext()
|
||||
const Router = getRouter()
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={themeConfig}
|
||||
renderEmpty={() => (
|
||||
const renderEmpty = () => (
|
||||
<div style={{ margin: '32px' }}>
|
||||
<MissingPlaceholder
|
||||
message='No data.'
|
||||
@ -80,8 +73,14 @@ const AppContent = () => {
|
||||
hasBorder={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
)
|
||||
|
||||
const AppContent = () => {
|
||||
const { themeConfig } = useThemeContext()
|
||||
const Router = getRouter()
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={themeConfig} renderEmpty={renderEmpty}>
|
||||
<App>
|
||||
<Router>
|
||||
<ElectronProvider>
|
||||
|
||||
@ -14,7 +14,6 @@ import DeveloperSidebar from './Developer/DeveloperSidebar'
|
||||
import DashboardSidebarSplitter from './common/DashboardSidebarSplitter'
|
||||
import MobileFooterNavigation from './common/MobileFooterNavigation'
|
||||
import { useThemeContext } from './context/ThemeContext'
|
||||
import { MessageProvider } from './context/MessageContext'
|
||||
import { useDashboardObjectToolsContext } from './context/DashboardObjectToolsContext'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { ElectronContext } from './context/ElectronContext'
|
||||
@ -52,7 +51,6 @@ const DashboardLayout = ({ children }) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<MessageProvider>
|
||||
<Layout
|
||||
style={{ height: 'var(--unit-100vh)' }}
|
||||
className={`${isDarkMode ? 'dark-mode' : 'light-mode'} main-layout${isMobile ? ' is-mobile' : ''}`}
|
||||
@ -80,7 +78,6 @@ const DashboardLayout = ({ children }) => {
|
||||
</DashboardSidebarSplitter>
|
||||
{isMobile ? <MobileFooterNavigation /> : null}
|
||||
</Layout>
|
||||
</MessageProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 DashboardTabPreview from './DashboardTabPreview'
|
||||
import classNames from 'classnames'
|
||||
@ -66,7 +66,7 @@ DashboardTabLabel.propTypes = {
|
||||
onClose: PropTypes.func
|
||||
}
|
||||
|
||||
const DashboardTabItem = ({
|
||||
const DashboardTabItem = memo(function DashboardTabItem({
|
||||
tab,
|
||||
tabIndex,
|
||||
isSelected,
|
||||
@ -78,9 +78,9 @@ const DashboardTabItem = ({
|
||||
onKeyDown,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
onClose
|
||||
}) => {
|
||||
const { captureTabPreview } = useNavigationTabs()
|
||||
onClose,
|
||||
captureTabPreview
|
||||
}) {
|
||||
const {
|
||||
src: previewSrc,
|
||||
theme: previewTheme,
|
||||
@ -174,7 +174,9 @@ const DashboardTabItem = ({
|
||||
{tabButton}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
DashboardTabItem.displayName = 'DashboardTabItem'
|
||||
|
||||
DashboardTabItem.propTypes = {
|
||||
tab: PropTypes.shape({
|
||||
@ -193,7 +195,8 @@ DashboardTabItem.propTypes = {
|
||||
onKeyDown: PropTypes.func.isRequired,
|
||||
onDragOver: PropTypes.func.isRequired,
|
||||
onDrop: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func
|
||||
onClose: PropTypes.func,
|
||||
captureTabPreview: PropTypes.func
|
||||
}
|
||||
|
||||
const STICKY_EDGE_INSET = 20
|
||||
@ -546,7 +549,8 @@ const DashboardTabs = () => {
|
||||
reorderTabs,
|
||||
handleTabDragStart,
|
||||
handleTabDragEnd,
|
||||
handleExternalTabDrop
|
||||
handleExternalTabDrop,
|
||||
captureTabPreview
|
||||
} = useNavigationTabs()
|
||||
const { rootRef, listRef, cloneRef, lineRef } = useActiveTabStickyScroll(
|
||||
activeTabId,
|
||||
@ -720,7 +724,8 @@ const DashboardTabs = () => {
|
||||
onKeyDown: handleKeyDown,
|
||||
onDragOver: handleItemDragOver,
|
||||
onDrop: handleItemDrop,
|
||||
onClose: closeTab
|
||||
onClose: closeTab,
|
||||
captureTabPreview
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Descriptions, Card, Flex, Divider, Skeleton } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import ObjectProperty from './ObjectProperty'
|
||||
import { createElement } from 'react'
|
||||
import { createElement, memo } from 'react'
|
||||
import Thumbnail from './Thumbnail'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
|
||||
const ObjectCard = ({
|
||||
const ObjectCard = memo(function ObjectCard({
|
||||
isSkeleton = false,
|
||||
model,
|
||||
modelProperties,
|
||||
@ -16,7 +16,7 @@ const ObjectCard = ({
|
||||
renderActions,
|
||||
lazyLoading = false,
|
||||
cardStyle = 'borderless'
|
||||
}) => {
|
||||
}) {
|
||||
const descriptionItems = []
|
||||
const modelIcon = createElement(model.icon, { style: { fontSize: 24 } })
|
||||
|
||||
@ -169,7 +169,9 @@ const ObjectCard = ({
|
||||
</Flex>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
ObjectCard.displayName = 'ObjectCard'
|
||||
|
||||
ObjectCard.propTypes = {
|
||||
model: PropTypes.object.isRequired,
|
||||
|
||||
@ -14,6 +14,7 @@ import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { useIsNavigationTabActive } from '../context/NavigationTabsContext'
|
||||
import ObjectCard from './ObjectCard'
|
||||
import ObjectKanbanColumn from './ObjectKanbanColumn'
|
||||
import ObjectKanbanHeader from './ObjectKanbanHeader'
|
||||
@ -287,6 +288,7 @@ const ObjectKanban = forwardRef(
|
||||
subscribeToObjectTypeUpdates
|
||||
} = useContext(ApiServerContext)
|
||||
const { token } = useContext(AuthContext)
|
||||
const isTabActive = useIsNavigationTabActive()
|
||||
const columnRefs = useRef({})
|
||||
const headerTrackRef = useRef(null)
|
||||
const skeletonTrackRef = useRef(null)
|
||||
@ -720,7 +722,7 @@ const ObjectKanban = forwardRef(
|
||||
}, [connected, token])
|
||||
|
||||
useEffect(() => {
|
||||
if (connected !== true || !type || !token) return
|
||||
if (connected !== true || !type || !token || !isTabActive) return
|
||||
|
||||
const unsubscribe = subscribeToAllObjectUpdates(
|
||||
type,
|
||||
@ -735,10 +737,10 @@ const ObjectKanban = forwardRef(
|
||||
subscribeToAllObjectUpdatesRef.current = null
|
||||
}
|
||||
}
|
||||
}, [type, connected, subscribeToAllObjectUpdates, token])
|
||||
}, [type, connected, subscribeToAllObjectUpdates, token, isTabActive])
|
||||
|
||||
useEffect(() => {
|
||||
if (connected !== true || !token) return
|
||||
if (connected !== true || !token || !isTabActive) return
|
||||
if (subscribedTypeRef.current === type) return
|
||||
|
||||
const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
|
||||
@ -757,7 +759,7 @@ const ObjectKanban = forwardRef(
|
||||
subscribedTypeRef.current = null
|
||||
}
|
||||
}
|
||||
}, [type, connected, token])
|
||||
}, [type, connected, token, isTabActive])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useEffect,
|
||||
@ -45,6 +46,7 @@ import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import { useActions } from '../context/ActionsContext'
|
||||
import { useIsNavigationTabActive } from '../context/NavigationTabsContext'
|
||||
import ActionsIcon from '../../Icons/ActionsIcon'
|
||||
import FilterIcon from '../../Icons/FilterIcon'
|
||||
import ScrollBox from './ScrollBox'
|
||||
@ -80,6 +82,7 @@ logger.setLevel(config.logLevel)
|
||||
const SCROLL_THRESHOLD = 50
|
||||
const SKELETON_HEIGHT = 49.5
|
||||
const EMPTY_MASTER_FILTER = {}
|
||||
const EMPTY_VISIBLE_COLUMNS = {}
|
||||
|
||||
const getCardColSpan = (containerWidth) => {
|
||||
if (containerWidth >= 2980) return 2
|
||||
@ -291,7 +294,8 @@ EditableRow.propTypes = {
|
||||
onRegister: PropTypes.func
|
||||
}
|
||||
|
||||
const ObjectTable = forwardRef(
|
||||
const ObjectTable = memo(
|
||||
forwardRef(
|
||||
(
|
||||
{
|
||||
type,
|
||||
@ -301,7 +305,7 @@ const ObjectTable = forwardRef(
|
||||
initialPage = 1,
|
||||
viewMode: viewModeProp,
|
||||
cards = false,
|
||||
visibleColumns = {},
|
||||
visibleColumns = EMPTY_VISIBLE_COLUMNS,
|
||||
masterFilter,
|
||||
size = 'middle',
|
||||
onStateChange,
|
||||
@ -335,6 +339,7 @@ const ObjectTable = forwardRef(
|
||||
const { token, userProfile } = useContext(AuthContext)
|
||||
const { isElectron } = useContext(ElectronContext)
|
||||
const { callAction } = useActions()
|
||||
const isTabActive = useIsNavigationTabActive()
|
||||
const resolvedMasterFilter = masterFilter ?? EMPTY_MASTER_FILTER
|
||||
const listViewId = objectListView?.listViewId ?? null
|
||||
const listViewFilter = objectListView?.listViewFilter ?? null
|
||||
@ -517,8 +522,68 @@ const ObjectTable = forwardRef(
|
||||
return {}
|
||||
}, [])
|
||||
|
||||
const rowActions =
|
||||
model.actions?.filter((action) => action.row == true) || []
|
||||
const rowActions = useMemo(
|
||||
() => 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(
|
||||
(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(
|
||||
async (pageNum = 1, filter = null, sorter = null) => {
|
||||
if (filter == null) {
|
||||
@ -1254,7 +1264,7 @@ const ObjectTable = forwardRef(
|
||||
|
||||
// Subscribe to all object updates for this type (list/cards only)
|
||||
useEffect(() => {
|
||||
if (isKanban || connected !== true || !type) return
|
||||
if (isKanban || connected !== true || !type || !isTabActive) return
|
||||
|
||||
const unsubscribe = subscribeToAllObjectUpdates(
|
||||
type,
|
||||
@ -1269,10 +1279,10 @@ const ObjectTable = forwardRef(
|
||||
subscribeToAllObjectUpdatesRef.current = null
|
||||
}
|
||||
}
|
||||
}, [type, connected, subscribeToAllObjectUpdates, isKanban])
|
||||
}, [type, connected, subscribeToAllObjectUpdates, isKanban, isTabActive])
|
||||
|
||||
useEffect(() => {
|
||||
if (isKanban || connected !== true) return
|
||||
if (isKanban || connected !== true || !isTabActive) return
|
||||
if (subscribedTypeRef.current === type) return
|
||||
|
||||
const unsubscribe = subscribeToObjectTypeUpdatesFnRef.current(
|
||||
@ -1291,7 +1301,7 @@ const ObjectTable = forwardRef(
|
||||
subscribedTypeRef.current = null
|
||||
}
|
||||
}
|
||||
}, [type, connected, isKanban])
|
||||
}, [type, connected, isKanban, isTabActive])
|
||||
|
||||
const updateData = useCallback(
|
||||
(id, updatedData) => {
|
||||
@ -1706,7 +1716,8 @@ const ObjectTable = forwardRef(
|
||||
return () => registerPageSorter({})
|
||||
}, [effectiveSorter, registerPageSorter, registerObjectListSorter])
|
||||
|
||||
const getFilterDropdown = ({
|
||||
const getFilterDropdown = useCallback(
|
||||
({
|
||||
setSelectedKeys,
|
||||
selectedKeys,
|
||||
confirm,
|
||||
@ -1727,6 +1738,8 @@ const ObjectTable = forwardRef(
|
||||
filter={effectiveFilter}
|
||||
masterFilter={resolvedMasterFilter}
|
||||
/>
|
||||
),
|
||||
[effectiveFilter, resolvedMasterFilter, type]
|
||||
)
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
@ -1858,9 +1871,47 @@ const ObjectTable = forwardRef(
|
||||
]
|
||||
)
|
||||
|
||||
const modelProperties = getModelProperties(type)
|
||||
// Table columns from model properties
|
||||
const columnsWithSkeleton = [
|
||||
const modelProperties = useMemo(() => getModelProperties(type), [type])
|
||||
|
||||
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:
|
||||
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) => {
|
||||
const prop = modelProperties.find((p) => p.name === colName)
|
||||
if (prop) {
|
||||
// Check if column should be visible based on visibleColumns prop
|
||||
if (
|
||||
Object.keys(visibleColumns).length > 0 &&
|
||||
visibleColumns[prop.name] === false
|
||||
) {
|
||||
return // Skip this column if it's not visible
|
||||
return
|
||||
}
|
||||
|
||||
var fixed = prop.columnFixed || undefined
|
||||
@ -1988,10 +2002,8 @@ const ObjectTable = forwardRef(
|
||||
default:
|
||||
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 columnConfig = {
|
||||
@ -2056,16 +2068,12 @@ const ObjectTable = forwardRef(
|
||||
)
|
||||
}
|
||||
|
||||
columnsWithSkeleton.push(columnConfig)
|
||||
columns.push(columnConfig)
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
showActions &&
|
||||
rowActions.length > 0 &&
|
||||
tableData.some((item) => !item?.isSkeleton)
|
||||
) {
|
||||
columnsWithSkeleton.push({
|
||||
if (showActions && rowActions.length > 0 && hasRealTableRows) {
|
||||
columns.push({
|
||||
title: (
|
||||
<Flex gap='small' align='center' justify='center'>
|
||||
<ActionsIcon />
|
||||
@ -2073,13 +2081,39 @@ const ObjectTable = forwardRef(
|
||||
),
|
||||
key: 'actions',
|
||||
fixed: 'right',
|
||||
width: 20 + rowActions.length * 30, // Adjust width based on number of actions
|
||||
width: 20 + rowActions.length * 30,
|
||||
render: (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
|
||||
const [cardColSpan, setCardColSpan] = useState(24)
|
||||
const cardsContainerNodeRef = useRef(null)
|
||||
@ -2465,6 +2499,7 @@ const ObjectTable = forwardRef(
|
||||
</ObjectTableFilterContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
ObjectTable.displayName = 'ObjectTable'
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
@ -26,6 +27,8 @@ const ActionsContext = createContext()
|
||||
const ActionsProvider = ({ children }) => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const locationRef = useRef(location)
|
||||
locationRef.current = location
|
||||
const { userProfile } = useContext(AuthContext)
|
||||
const [currentObject, setCurrentObject] = useState(null)
|
||||
const [currentObjectType, setCurrentObjectType] = useState(null)
|
||||
@ -38,13 +41,14 @@ const ActionsProvider = ({ children }) => {
|
||||
const actionObjectType = searchParams.get('actionObjectType')
|
||||
|
||||
const clearAction = useCallback(() => {
|
||||
const nextUrl = stripModalActionParams(location.pathname, location.search)
|
||||
if (nextUrl !== location.pathname + location.search) {
|
||||
const loc = locationRef.current
|
||||
const nextUrl = stripModalActionParams(loc.pathname, loc.search)
|
||||
if (nextUrl !== loc.pathname + loc.search) {
|
||||
navigate(nextUrl, { replace: true })
|
||||
}
|
||||
setModalAction(null)
|
||||
lastHandledAction.current = null
|
||||
}, [location.pathname, location.search, navigate])
|
||||
}, [navigate])
|
||||
|
||||
const handleModalOk = useCallback(() => {
|
||||
onModalOk?.()
|
||||
@ -56,16 +60,17 @@ const ActionsProvider = ({ children }) => {
|
||||
setCurrentObject(objectData)
|
||||
setCurrentObjectType(objectType)
|
||||
const model = getModelByName(objectType)
|
||||
const loc = locationRef.current
|
||||
const url = buildActionUrl(
|
||||
model,
|
||||
action,
|
||||
objectData._id,
|
||||
location.pathname,
|
||||
location.search
|
||||
loc.pathname,
|
||||
loc.search
|
||||
)
|
||||
navigate(url)
|
||||
},
|
||||
[location.pathname, location.search, navigate]
|
||||
[navigate]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -187,9 +192,8 @@ const ActionsProvider = ({ children }) => {
|
||||
? {}
|
||||
: modalObjectData
|
||||
|
||||
return (
|
||||
<ActionsContext.Provider
|
||||
value={{
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
currentObject,
|
||||
currentObjectType,
|
||||
setCurrentObject,
|
||||
@ -197,8 +201,12 @@ const ActionsProvider = ({ children }) => {
|
||||
callAction,
|
||||
clearAction,
|
||||
setOnModalOk
|
||||
}}
|
||||
>
|
||||
}),
|
||||
[callAction, clearAction, currentObject, currentObjectType]
|
||||
)
|
||||
|
||||
return (
|
||||
<ActionsContext.Provider value={value}>
|
||||
<Modal
|
||||
open={resolvedModalAction != null}
|
||||
destroyOnHidden={true}
|
||||
|
||||
@ -5,7 +5,8 @@ import {
|
||||
useState,
|
||||
useContext,
|
||||
useRef,
|
||||
useCallback
|
||||
useCallback,
|
||||
useMemo
|
||||
} from 'react'
|
||||
import io from 'socket.io-client'
|
||||
import { message, Modal, Space, Button, Typography, Flex } from 'antd'
|
||||
@ -133,10 +134,244 @@ const getObjectTypeSubscriptionArgs = (filterOrCallback, callback) => {
|
||||
const getObjectEndpoint = (type) =>
|
||||
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 ApiServerProvider = ({ children }) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
token,
|
||||
@ -162,16 +397,12 @@ const ApiServerProvider = ({ children }) => {
|
||||
const subscribedActivityCallbacksRef = useRef(new Map())
|
||||
const subscribedActivityServerSubscriptionsRef = useRef(new Set())
|
||||
const notificationListenersRef = useRef(new Set())
|
||||
const completedLaunchSessionsRef = useRef(new Set())
|
||||
const [connectionIssue, setConnectionIssue] = useState(false)
|
||||
const [reconnectProgress, setReconnectProgress] = useState(0)
|
||||
const [reconnectRemainingMs, setReconnectRemainingMs] = useState(
|
||||
RECONNECT_DELAYS_MS[0]
|
||||
)
|
||||
const [reconnectDelayMs, setReconnectDelayMs] = useState(RECONNECT_DELAYS_MS[0])
|
||||
const [reconnectCycle, setReconnectCycle] = useState(0)
|
||||
const [isReconnecting, setIsReconnecting] = useState(false)
|
||||
const reconnectAttemptRef = useRef(0)
|
||||
const reconnectTimerRef = useRef(null)
|
||||
const reconnectProgressTimerRef = useRef(null)
|
||||
const reconnectScheduledRef = useRef(false)
|
||||
const isReconnectingRef = useRef(false)
|
||||
const hasConnectedOnceRef = useRef(false)
|
||||
@ -349,10 +580,6 @@ const ApiServerProvider = ({ children }) => {
|
||||
clearTimeout(reconnectTimerRef.current)
|
||||
reconnectTimerRef.current = null
|
||||
}
|
||||
if (reconnectProgressTimerRef.current) {
|
||||
clearInterval(reconnectProgressTimerRef.current)
|
||||
reconnectProgressTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const resetReconnectState = useCallback(() => {
|
||||
@ -361,25 +588,16 @@ const ApiServerProvider = ({ children }) => {
|
||||
reconnectScheduledRef.current = false
|
||||
isReconnectingRef.current = false
|
||||
setIsReconnecting(false)
|
||||
setReconnectProgress(0)
|
||||
setReconnectRemainingMs(RECONNECT_DELAYS_MS[0])
|
||||
setReconnectDelayMs(RECONNECT_DELAYS_MS[0])
|
||||
}, [clearReconnectTimers])
|
||||
|
||||
const startReconnectCountdown = useCallback(
|
||||
(delayMs) => {
|
||||
clearReconnectTimers()
|
||||
const startedAt = Date.now()
|
||||
setReconnectRemainingMs(delayMs)
|
||||
setReconnectProgress(0)
|
||||
setReconnectDelayMs(delayMs)
|
||||
setReconnectCycle((current) => current + 1)
|
||||
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(() => {
|
||||
attemptReconnectRef.current?.()
|
||||
}, delayMs)
|
||||
@ -429,7 +647,6 @@ const ApiServerProvider = ({ children }) => {
|
||||
isReconnectingRef.current = true
|
||||
reconnectAttemptRef.current += 1
|
||||
setIsReconnecting(true)
|
||||
setReconnectProgress(100)
|
||||
setConnecting(true)
|
||||
setConnectionIssue(true)
|
||||
|
||||
@ -2769,43 +2986,6 @@ const ApiServerProvider = ({ children }) => {
|
||||
[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 response = await axios.get(
|
||||
`${config.backendUrl}/applaunch/${launchSession}`,
|
||||
@ -2924,36 +3104,8 @@ const ApiServerProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const reconnectSecondsRemaining = Math.max(
|
||||
1,
|
||||
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,
|
||||
const apiMethodsRef = useRef({})
|
||||
apiMethodsRef.current = {
|
||||
getUserSettings,
|
||||
updateUserSettings,
|
||||
setObjectActivity,
|
||||
@ -2981,7 +3133,6 @@ const ApiServerProvider = ({ children }) => {
|
||||
getModelStats,
|
||||
getModelPropertyValues,
|
||||
getModelHistory,
|
||||
fetchLoading,
|
||||
showError,
|
||||
fetchFileContent,
|
||||
fetchFileThumbnail,
|
||||
@ -3025,66 +3176,48 @@ const ApiServerProvider = ({ children }) => {
|
||||
fetchAppUpdateCurrent,
|
||||
fetchWsServerVersion,
|
||||
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 }}
|
||||
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={() => attemptReconnect()}
|
||||
>
|
||||
Reconnect
|
||||
</Button>
|
||||
|
||||
const stableApiRef = useRef(null)
|
||||
if (stableApiRef.current == null) {
|
||||
stableApiRef.current = createStableApiMethods(apiMethodsRef)
|
||||
}
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
...stableApiRef.current,
|
||||
apiServer: socketRef.current,
|
||||
error,
|
||||
connecting,
|
||||
connected,
|
||||
userSettings,
|
||||
userSettingsLoaded,
|
||||
fetchLoading
|
||||
}),
|
||||
[
|
||||
connecting,
|
||||
connected,
|
||||
error,
|
||||
fetchLoading,
|
||||
userSettings,
|
||||
userSettingsLoaded
|
||||
]
|
||||
: false
|
||||
}
|
||||
>
|
||||
{!isReconnecting ? (
|
||||
<Flex vertical gap='middle'>
|
||||
<Text>
|
||||
{isReconnecting
|
||||
? 'Reconnecting to the API server...'
|
||||
: `Lost connection to the API server. Reconnecting in ${reconnectSecondsRemaining} second${
|
||||
reconnectSecondsRemaining === 1 ? '' : 's'
|
||||
}...`}
|
||||
</Text>
|
||||
<ProgressDisplay
|
||||
percent={isReconnecting ? 0 : 100 - reconnectProgress}
|
||||
showInfo={false}
|
||||
status={'exception'}
|
||||
)
|
||||
|
||||
return (
|
||||
<ApiServerContext.Provider value={value}>
|
||||
{contextHolder}
|
||||
<LaunchSessionCompleter
|
||||
completeAppLaunchSession={completeAppLaunchSession}
|
||||
/>
|
||||
{children}
|
||||
<ConnectionIssueModal
|
||||
open={Boolean(token) && authenticated == true && connectionIssue}
|
||||
delayMs={reconnectDelayMs}
|
||||
cycle={reconnectCycle}
|
||||
isReconnecting={isReconnecting}
|
||||
onReconnect={attemptReconnect}
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<Space size={'middle'}>
|
||||
<LoadingOutlined />
|
||||
<Text style={{ margin: 0 }}>Reconnecting, please wait...</Text>
|
||||
</Space>
|
||||
)}
|
||||
</Modal>
|
||||
<Modal
|
||||
title={
|
||||
<Space size={'middle'}>
|
||||
|
||||
@ -5,7 +5,8 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useContext,
|
||||
useRef
|
||||
useRef,
|
||||
useMemo
|
||||
} from 'react'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
@ -112,6 +113,8 @@ const AuthProvider = ({ children }) => {
|
||||
} = useContext(ElectronContext)
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const locationRef = useRef(location)
|
||||
locationRef.current = location
|
||||
const sessionRef = useRef({
|
||||
token,
|
||||
expiresAt,
|
||||
@ -432,9 +435,11 @@ const AuthProvider = ({ children }) => {
|
||||
|
||||
// Login using query parameters
|
||||
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')
|
||||
const loginUrl = `${config.backendUrl}/auth/${redirectType}/login?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||
const loginUrl = `${config.backendUrl}/auth/${redirectType}/login?redirect_uri=${encodeURIComponent(nextRedirectUri)}`
|
||||
if (isElectron) {
|
||||
logger.debug('Opening external url...')
|
||||
openExternalUrl(loginUrl)
|
||||
@ -444,14 +449,7 @@ const AuthProvider = ({ children }) => {
|
||||
window.location.href = loginUrl
|
||||
}
|
||||
},
|
||||
[
|
||||
redirectType,
|
||||
messageApi,
|
||||
openExternalUrl,
|
||||
isElectron,
|
||||
location.search,
|
||||
location.pathname
|
||||
]
|
||||
[redirectType, messageApi, openExternalUrl, isElectron]
|
||||
)
|
||||
|
||||
const getLoginToken = useCallback(
|
||||
@ -461,7 +459,6 @@ const AuthProvider = ({ children }) => {
|
||||
setShowSessionExpiredModal(false)
|
||||
setAuthError(null)
|
||||
try {
|
||||
// Make a call to your backend to check auth status
|
||||
const response = await axios.get(
|
||||
`${config.backendUrl}/auth/${redirectType}/token?code=${code}`
|
||||
)
|
||||
@ -480,7 +477,6 @@ const AuthProvider = ({ children }) => {
|
||||
setAuthenticated(true)
|
||||
setShowUnauthorizedModal(false)
|
||||
|
||||
// Persist session (cookies on web, electron storage on desktop)
|
||||
const persisted = await persistSession({
|
||||
token: nextToken,
|
||||
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')
|
||||
const newSearch = searchParams.toString()
|
||||
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
|
||||
const newPath = loc.pathname + (newSearch ? `?${newSearch}` : '')
|
||||
navigate(newPath, { replace: true })
|
||||
} else {
|
||||
setAuthenticated(false)
|
||||
@ -520,14 +517,7 @@ const AuthProvider = ({ children }) => {
|
||||
setRetreivedTokenFromCookies(true)
|
||||
}
|
||||
},
|
||||
[
|
||||
navigate,
|
||||
location.search,
|
||||
location.pathname,
|
||||
messageApi,
|
||||
persistSession,
|
||||
redirectType
|
||||
]
|
||||
[navigate, messageApi, persistSession, redirectType]
|
||||
)
|
||||
|
||||
// Function to check if the user is logged in
|
||||
@ -805,12 +795,8 @@ const AuthProvider = ({ children }) => {
|
||||
retreivedTokenFromCookies
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
{notificationContextHolder}
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
authenticated,
|
||||
authInitialized: retreivedTokenFromCookies,
|
||||
setUnauthenticated,
|
||||
@ -822,8 +808,26 @@ const AuthProvider = ({ children }) => {
|
||||
setUserProfile,
|
||||
profileImageUrl,
|
||||
logout
|
||||
}}
|
||||
>
|
||||
}),
|
||||
[
|
||||
authenticated,
|
||||
getLoginToken,
|
||||
loading,
|
||||
loginWithSSO,
|
||||
logout,
|
||||
profileImageUrl,
|
||||
retreivedTokenFromCookies,
|
||||
setUnauthenticated,
|
||||
token,
|
||||
userProfile
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
{notificationContextHolder}
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
<Modal
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
@ -28,14 +29,17 @@ export const DashboardObjectToolsProvider = ({ children }) => {
|
||||
setCurrentObjectToolsState(null)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<DashboardObjectToolsContext.Provider
|
||||
value={{
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
currentObjectTools,
|
||||
setCurrentObjectTools,
|
||||
clearCurrentObjectTools
|
||||
}}
|
||||
>
|
||||
}),
|
||||
[clearCurrentObjectTools, currentObjectTools, setCurrentObjectTools]
|
||||
)
|
||||
|
||||
return (
|
||||
<DashboardObjectToolsContext.Provider value={value}>
|
||||
{children}
|
||||
</DashboardObjectToolsContext.Provider>
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { message } from 'antd'
|
||||
import { isElectrobunDesktop } from '../../../electrobun-bridge'
|
||||
@ -12,36 +12,52 @@ const MESSAGE_TOP = isElectrobunDesktop() ? 48 : undefined
|
||||
export const MessageProvider = ({ children }) => {
|
||||
const [msgApi, contextHolder] = message.useMessage({ top: MESSAGE_TOP })
|
||||
|
||||
const showMessage = (type, content, options = {}) => {
|
||||
const showMessage = useCallback(
|
||||
(type, content, options = {}) => {
|
||||
return msgApi.open({
|
||||
type,
|
||||
content,
|
||||
...options
|
||||
})
|
||||
}
|
||||
},
|
||||
[msgApi]
|
||||
)
|
||||
|
||||
const showSuccess = (content, options = {}) =>
|
||||
showMessage('success', content, options)
|
||||
const showInfo = (content, options = {}) =>
|
||||
showMessage('info', content, options)
|
||||
const showWarning = (content, options = {}) =>
|
||||
showMessage('warning', content, options)
|
||||
const showError = (content, options = {}) =>
|
||||
showMessage('error', content, options)
|
||||
const showLoading = (content, options = {}) =>
|
||||
showMessage('loading', content, options)
|
||||
const showSuccess = useCallback(
|
||||
(content, options = {}) => showMessage('success', content, options),
|
||||
[showMessage]
|
||||
)
|
||||
const showInfo = useCallback(
|
||||
(content, options = {}) => showMessage('info', content, options),
|
||||
[showMessage]
|
||||
)
|
||||
const showWarning = useCallback(
|
||||
(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 (
|
||||
<MessageContext.Provider
|
||||
value={{
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
msgApi,
|
||||
showSuccess,
|
||||
showInfo,
|
||||
showWarning,
|
||||
showError,
|
||||
showLoading
|
||||
}}
|
||||
>
|
||||
}),
|
||||
[msgApi, showError, showInfo, showLoading, showSuccess, showWarning]
|
||||
)
|
||||
|
||||
return (
|
||||
<MessageContext.Provider value={value}>
|
||||
{contextHolder}
|
||||
{children}
|
||||
</MessageContext.Provider>
|
||||
|
||||
@ -183,7 +183,6 @@ export const NavigationTabsProvider = ({ children }) => {
|
||||
const tabsRef = useRef(tabs)
|
||||
const activeTabIdRef = useRef(activeTabId)
|
||||
const locationRef = useRef(location)
|
||||
const previousActiveTabIdRef = useRef(null)
|
||||
const isRestoringRef = useRef(null)
|
||||
const tabPaneElsRef = useRef(new Map())
|
||||
const captureQueueRef = useRef(Promise.resolve())
|
||||
@ -471,16 +470,6 @@ export const NavigationTabsProvider = ({ children }) => {
|
||||
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(
|
||||
({ title, modelName, iconKey, location: pageLocation } = {}) => {
|
||||
const activeId = activeTabIdRef.current
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
useContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef
|
||||
} from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
@ -121,7 +122,14 @@ const NotificationProvider = ({ children }) => {
|
||||
|
||||
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(() => {
|
||||
if (!authenticated || !connected) return
|
||||
fetchNotificationsRef.current()
|
||||
@ -134,10 +142,6 @@ const NotificationProvider = ({ children }) => {
|
||||
fetchNotificationsRef.current()
|
||||
}, [notificationCenterVisible])
|
||||
|
||||
useEffect(() => {
|
||||
setNotificationCenterVisible(false)
|
||||
}, [location.pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (!authenticated || !registerNotificationListener) return
|
||||
const handleNotification = (notif) => {
|
||||
@ -169,9 +173,8 @@ const NotificationProvider = ({ children }) => {
|
||||
return unregister
|
||||
}, [authenticated, registerNotificationListener, api, deleteNotification])
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider
|
||||
value={{
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
notificationCenterVisible,
|
||||
toggleNotificationCenter,
|
||||
notifications,
|
||||
@ -182,15 +185,30 @@ const NotificationProvider = ({ children }) => {
|
||||
deleteAllNotifications,
|
||||
unreadCount,
|
||||
notificationsLoading
|
||||
}}
|
||||
>
|
||||
}),
|
||||
[
|
||||
deleteAllNotifications,
|
||||
deleteNotification,
|
||||
fetchNotifications,
|
||||
markAllNotificationsAsRead,
|
||||
markNotificationAsRead,
|
||||
notificationCenterVisible,
|
||||
notifications,
|
||||
notificationsLoading,
|
||||
toggleNotificationCenter,
|
||||
unreadCount
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={value}>
|
||||
{contextHolder}
|
||||
{children}
|
||||
<Drawer
|
||||
title='Notifications'
|
||||
placement='right'
|
||||
width={isMobile ? '100%' : 460}
|
||||
onClose={() => setNotificationCenterVisible(false)}
|
||||
onClose={closeNotificationCenter}
|
||||
open={notificationCenterVisible}
|
||||
>
|
||||
<NotificationCenter
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect
|
||||
} from 'react'
|
||||
@ -88,24 +89,27 @@ export const ThemeProvider = ({ children }) => {
|
||||
)
|
||||
}, [showNavigationLabels])
|
||||
|
||||
const toggleTheme = () => {
|
||||
if (isSystem) {
|
||||
setIsSystem(false)
|
||||
}
|
||||
setIsDarkMode(!isDarkMode)
|
||||
}
|
||||
const toggleTheme = useCallback(() => {
|
||||
setIsSystem((current) => {
|
||||
if (current) return false
|
||||
return current
|
||||
})
|
||||
setIsDarkMode((current) => !current)
|
||||
}, [])
|
||||
|
||||
const toggleSystem = () => {
|
||||
setIsSystem(!isSystem)
|
||||
if (!isSystem) {
|
||||
// When enabling system theme, update to match system preference
|
||||
const toggleSystem = useCallback(() => {
|
||||
setIsSystem((current) => {
|
||||
const next = !current
|
||||
if (next) {
|
||||
setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
}
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const toggleCompact = () => {
|
||||
setIsCompact(!isCompact)
|
||||
}
|
||||
const toggleCompact = useCallback(() => {
|
||||
setIsCompact((current) => !current)
|
||||
}, [])
|
||||
|
||||
const setThemeMode = useCallback((value) => {
|
||||
if (value === 'system') {
|
||||
@ -126,19 +130,63 @@ export const ThemeProvider = ({ children }) => {
|
||||
setShowNavigationLabelsState(Boolean(value))
|
||||
}, [])
|
||||
|
||||
const getThemeAlgorithm = () => {
|
||||
var baseAlgorithm
|
||||
if (isDarkMode == true) {
|
||||
baseAlgorithm = theme.darkAlgorithm
|
||||
} else {
|
||||
baseAlgorithm = theme.defaultAlgorithm
|
||||
}
|
||||
return isCompact ? [theme.compactAlgorithm, baseAlgorithm] : [baseAlgorithm]
|
||||
}
|
||||
const getColors = useCallback(() => COLORS, [])
|
||||
|
||||
const getColors = () => {
|
||||
return COLORS
|
||||
const themeConfig = useMemo(() => {
|
||||
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
|
||||
useEffect(() => {
|
||||
@ -226,41 +274,8 @@ export const ThemeProvider = ({ children }) => {
|
||||
)
|
||||
}, [isDarkMode, primaryColorOverride])
|
||||
|
||||
const themeConfig = {
|
||||
algorithm: getThemeAlgorithm(),
|
||||
token: {
|
||||
...COLORS,
|
||||
colorPrimary:
|
||||
primaryColorOverride == null
|
||||
? COLORS.colorPrimary
|
||||
: primaryColorOverride,
|
||||
borderRadius: '12px'
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
headerBg: isDarkMode ? '#141414' : '#ffffff'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
isDarkMode,
|
||||
toggleTheme,
|
||||
isCompact,
|
||||
toggleCompact,
|
||||
isSystem,
|
||||
toggleSystem,
|
||||
setThemeMode,
|
||||
setDensityMode,
|
||||
showNavigationLabels,
|
||||
setShowNavigationLabels,
|
||||
getColors,
|
||||
setPrimaryColorOverride,
|
||||
themeConfig
|
||||
}}
|
||||
>
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user