Enhance Dashboard Components with Loading State and Styling Improvements

- Updated ListViewEditButtons to disable actions during initial loading, improving user experience.
- Refactored ListViewTabs to adjust icon styles based on loading state and removed unnecessary imports for cleaner code.
- Modified ObjectListViewContext to use viewId instead of view reference for better clarity and consistency.
- Updated TableStateContext to reflect changes in view parameter naming, enhancing code maintainability.
- Adjusted App.css for improved styling of segmented navigation items and loading indicators.
This commit is contained in:
Tom Butcher 2026-09-02 21:26:39 +01:00
parent 96b809b9d8
commit 73de7ac8ee
6 changed files with 82 additions and 76 deletions

View File

@ -2647,6 +2647,7 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.segmented-nav-item-icon {
padding-inline: 8px;
min-width: 32px;
}
.segmented-nav-item-icon-contents {
@ -2667,7 +2668,12 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.segmented-nav-item-selected,
.segmented-nav-item:not(.segmented-nav-item-disabled):hover {
background: var(--color-button-background);
border: 1px solid var(--color-button-border);
border: 1px solid
color-mix(
in srgb,
var(--color-button-border) 40%,
var(--color-button-background)
);
}
.segmented-nav-item-disabled {

View File

@ -4,12 +4,14 @@ import EditButtons from './EditButtons'
import { useObjectListView } from '../context/ObjectListViewContext'
const ListViewEditButtons = ({ showStartingDivider = false }) => {
const { isEditing, saving, saveEdits, cancelEdits, startEditing } =
useObjectListView()
if (!isEditing) {
return null
}
const {
isEditing,
initialLoading,
saving,
saveEdits,
cancelEdits,
startEditing
} = useObjectListView()
return (
<Flex align='center' gap='small'>
@ -24,6 +26,7 @@ const ListViewEditButtons = ({ showStartingDivider = false }) => {
cancelEditing={cancelEdits}
startEditing={startEditing}
formValid={true}
disabled={initialLoading}
loading={saving}
requirePermission={false}
/>

View File

@ -7,7 +7,6 @@ import {
Button,
Flex,
Typography,
Spin,
Divider,
theme
} from 'antd'
@ -18,7 +17,6 @@ import { getModelByName } from '../../../database/ObjectModels'
import { useObjectListView } from '../context/ObjectListViewContext'
import classNames from 'classnames'
import PlusIcon from '../../Icons/PlusIcon'
import EditIcon from '../../Icons/EditIcon'
import BinIcon from '../../Icons/BinIcon'
import ScrollBox from './ScrollBox'
import ColorSelector from './ColorSelector'
@ -154,14 +152,16 @@ const ViewTabLabel = ({
className='list-view-tabs-label list-view-tabs-label-editing'
onFocusCapture={() => onSelect?.()}
>
<ColorSelector
size='small'
showText={false}
required
value={view.color || '#3498DB'}
onChange={(color) => onDraftChange({ color })}
onClick={(event) => event.stopPropagation()}
/>
<div style={{ marginRight: 2 }}>
<ColorSelector
size='small'
showText={false}
required
value={view.color || '#3498DB'}
onChange={(color) => onDraftChange({ color })}
onClick={(event) => event.stopPropagation()}
/>
</div>
<ViewTabNameInput
value={view.name}
onClick={(event) => event.stopPropagation()}
@ -196,7 +196,6 @@ const ListViewTabs = () => {
activeTabKey,
initialLoading,
isEditing,
startEditing,
selectTab,
handleTabEdit,
updateDraftView,
@ -207,10 +206,19 @@ const ListViewTabs = () => {
const model = getModelByName(objectType)
const options = useMemo(() => {
const Icon = model?.icon
const Icon = initialLoading ? LoadingOutlined : model?.icon
const styles = initialLoading
? { fontSize: 12, color: 'var(--color-button-border)' }
: { fontSize: 14 }
const allOption = {
value: ALL_TAB_KEY,
icon: Icon ? <Icon style={{ fontSize: 14 }} /> : null
icon: Icon ? (
<Icon
style={styles}
color={'secondary'}
spin={initialLoading || undefined}
/>
) : null
}
const viewOptions = views.map((view) => ({
@ -242,6 +250,7 @@ const ListViewTabs = () => {
ALL_TAB_KEY,
activeTabKey,
handleTabEdit,
initialLoading,
isEditing,
model?.icon,
selectTab,
@ -256,51 +265,40 @@ const ListViewTabs = () => {
style={{ minHeight: 0, width: '100%', minWidth: 0 }}
>
<Divider type='vertical' style={{ margin: '0 4px', height: '18px' }} />
<Spin
spinning={initialLoading === true}
wrapperClassName='list-view-tabs-spin'
indicator={<LoadingOutlined spin />}
<Flex
align='center'
gap={4}
className={classNames('list-view-tabs', {
'list-view-tabs-editing': isEditing
})}
style={{ flex: 1, minWidth: 0 }}
>
<Flex
align='center'
gap={4}
className={classNames('list-view-tabs', {
'list-view-tabs-editing': isEditing
})}
style={{ flex: 1, minWidth: 0 }}
>
<ScrollBox className='list-view-tabs-segmented-wrap' horizontal>
<Flex
align='center'
gap={8}
className='list-view-tabs-segmented-inner'
>
<SegmentedNav
className='list-view-tabs-segmented'
value={activeTabKey}
options={options}
onChange={selectTab}
onReorder={isEditing ? reorderDraftViews : undefined}
/>
<ScrollBox className='list-view-tabs-segmented-wrap' horizontal>
<Flex
align='center'
gap={8}
className='list-view-tabs-segmented-inner'
>
<SegmentedNav
className='list-view-tabs-segmented'
value={activeTabKey}
options={options}
onChange={selectTab}
onReorder={isEditing ? reorderDraftViews : undefined}
/>
{isEditing && (
<Button
type='text'
size='small'
className='list-view-tabs-add-button'
onClick={
isEditing ? () => handleTabEdit(null, 'add') : startEditing
}
icon={
isEditing ? (
<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />
) : (
<EditIcon style={{ fontSize: 12, marginBottom: 5 }} />
)
}
onClick={() => handleTabEdit(null, 'add')}
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
/>
</Flex>
</ScrollBox>
</Flex>
</Spin>
)}
</Flex>
</ScrollBox>
</Flex>
</Flex>
)
}

View File

@ -37,15 +37,12 @@ const DEFAULT_VIEW = {
// eslint-disable-next-line react-refresh/only-export-components
export const getObjectViewKey = (view) =>
view?._reference || view?._id || null
view?._id != null ? String(view._id) : null
const resolveViewFromRef = (views, viewRef) => {
if (!viewRef) return null
const resolveViewFromId = (views, viewId) => {
if (!viewId) return null
return (
views.find(
(view) =>
view._reference === viewRef || String(view._id) === String(viewRef)
) || null
views.find((view) => String(view._id) === String(viewId)) || null
)
}
@ -206,15 +203,15 @@ export const ObjectListViewProvider = ({
useEffect(() => {
if (initialLoading || urlSyncedRef.current) return
const viewRef = getViewFromUrl()
if (!viewRef) {
const viewId = getViewFromUrl()
if (!viewId) {
urlSyncedRef.current = true
return
}
const matched =
resolveViewFromRef(views, viewRef) ||
resolveViewFromRef(draftViews, viewRef)
resolveViewFromId(views, viewId) ||
resolveViewFromId(draftViews, viewId)
if (matched) {
setActiveTabKey(String(matched._id))
@ -222,7 +219,7 @@ export const ObjectListViewProvider = ({
return
}
// Wait until views have loaded before deciding the URL reference is invalid
// Wait until views have loaded before deciding the URL viewId is invalid
if (views.length === 0) return
if (!isEditing) {

View File

@ -12,7 +12,7 @@ const TableStateContext = createContext()
export const FILTER_URL_PARAM = 'filter'
export const SORT_URL_PARAM = 'sort'
export const VIEW_URL_PARAM = 'view'
export const VIEW_URL_PARAM = 'viewId'
const getSessionFilterKey = (scope) => `tableState:${scope}:filter`
const getSessionSortKey = (scope) => `tableState:${scope}:sort`
@ -137,10 +137,12 @@ export const TableStateProvider = ({ children }) => {
)
const persistView = useCallback(
(viewRef) => {
(viewId) => {
const next = new URLSearchParams(searchParams)
if (viewRef) {
next.set(VIEW_URL_PARAM, viewRef)
// Drop legacy `view` param that previously stored `_reference`
next.delete('view')
if (viewId) {
next.set(VIEW_URL_PARAM, String(viewId))
} else {
next.delete(VIEW_URL_PARAM)
}

View File

@ -182,7 +182,7 @@ export const ThemeProvider = ({ children }) => {
)
root.style.setProperty(
'--color-list-view-tabs-border',
isDarkMode ? '#2a2a2a' : '#E7E7E7'
isDarkMode ? '#1f1f1f' : '#E7E7E7'
)
root.style.setProperty(
'--color-button-background',