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:
parent
96b809b9d8
commit
73de7ac8ee
@ -2647,6 +2647,7 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
|
|
||||||
.segmented-nav-item-icon {
|
.segmented-nav-item-icon {
|
||||||
padding-inline: 8px;
|
padding-inline: 8px;
|
||||||
|
min-width: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segmented-nav-item-icon-contents {
|
.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-selected,
|
||||||
.segmented-nav-item:not(.segmented-nav-item-disabled):hover {
|
.segmented-nav-item:not(.segmented-nav-item-disabled):hover {
|
||||||
background: var(--color-button-background);
|
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 {
|
.segmented-nav-item-disabled {
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import EditButtons from './EditButtons'
|
|||||||
import { useObjectListView } from '../context/ObjectListViewContext'
|
import { useObjectListView } from '../context/ObjectListViewContext'
|
||||||
|
|
||||||
const ListViewEditButtons = ({ showStartingDivider = false }) => {
|
const ListViewEditButtons = ({ showStartingDivider = false }) => {
|
||||||
const { isEditing, saving, saveEdits, cancelEdits, startEditing } =
|
const {
|
||||||
useObjectListView()
|
isEditing,
|
||||||
|
initialLoading,
|
||||||
if (!isEditing) {
|
saving,
|
||||||
return null
|
saveEdits,
|
||||||
}
|
cancelEdits,
|
||||||
|
startEditing
|
||||||
|
} = useObjectListView()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex align='center' gap='small'>
|
<Flex align='center' gap='small'>
|
||||||
@ -24,6 +26,7 @@ const ListViewEditButtons = ({ showStartingDivider = false }) => {
|
|||||||
cancelEditing={cancelEdits}
|
cancelEditing={cancelEdits}
|
||||||
startEditing={startEditing}
|
startEditing={startEditing}
|
||||||
formValid={true}
|
formValid={true}
|
||||||
|
disabled={initialLoading}
|
||||||
loading={saving}
|
loading={saving}
|
||||||
requirePermission={false}
|
requirePermission={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
Typography,
|
Typography,
|
||||||
Spin,
|
|
||||||
Divider,
|
Divider,
|
||||||
theme
|
theme
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
@ -18,7 +17,6 @@ import { getModelByName } from '../../../database/ObjectModels'
|
|||||||
import { useObjectListView } from '../context/ObjectListViewContext'
|
import { useObjectListView } from '../context/ObjectListViewContext'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
import EditIcon from '../../Icons/EditIcon'
|
|
||||||
import BinIcon from '../../Icons/BinIcon'
|
import BinIcon from '../../Icons/BinIcon'
|
||||||
import ScrollBox from './ScrollBox'
|
import ScrollBox from './ScrollBox'
|
||||||
import ColorSelector from './ColorSelector'
|
import ColorSelector from './ColorSelector'
|
||||||
@ -154,14 +152,16 @@ const ViewTabLabel = ({
|
|||||||
className='list-view-tabs-label list-view-tabs-label-editing'
|
className='list-view-tabs-label list-view-tabs-label-editing'
|
||||||
onFocusCapture={() => onSelect?.()}
|
onFocusCapture={() => onSelect?.()}
|
||||||
>
|
>
|
||||||
<ColorSelector
|
<div style={{ marginRight: 2 }}>
|
||||||
size='small'
|
<ColorSelector
|
||||||
showText={false}
|
size='small'
|
||||||
required
|
showText={false}
|
||||||
value={view.color || '#3498DB'}
|
required
|
||||||
onChange={(color) => onDraftChange({ color })}
|
value={view.color || '#3498DB'}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onChange={(color) => onDraftChange({ color })}
|
||||||
/>
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ViewTabNameInput
|
<ViewTabNameInput
|
||||||
value={view.name}
|
value={view.name}
|
||||||
onClick={(event) => event.stopPropagation()}
|
onClick={(event) => event.stopPropagation()}
|
||||||
@ -196,7 +196,6 @@ const ListViewTabs = () => {
|
|||||||
activeTabKey,
|
activeTabKey,
|
||||||
initialLoading,
|
initialLoading,
|
||||||
isEditing,
|
isEditing,
|
||||||
startEditing,
|
|
||||||
selectTab,
|
selectTab,
|
||||||
handleTabEdit,
|
handleTabEdit,
|
||||||
updateDraftView,
|
updateDraftView,
|
||||||
@ -207,10 +206,19 @@ const ListViewTabs = () => {
|
|||||||
const model = getModelByName(objectType)
|
const model = getModelByName(objectType)
|
||||||
|
|
||||||
const options = useMemo(() => {
|
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 = {
|
const allOption = {
|
||||||
value: ALL_TAB_KEY,
|
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) => ({
|
const viewOptions = views.map((view) => ({
|
||||||
@ -242,6 +250,7 @@ const ListViewTabs = () => {
|
|||||||
ALL_TAB_KEY,
|
ALL_TAB_KEY,
|
||||||
activeTabKey,
|
activeTabKey,
|
||||||
handleTabEdit,
|
handleTabEdit,
|
||||||
|
initialLoading,
|
||||||
isEditing,
|
isEditing,
|
||||||
model?.icon,
|
model?.icon,
|
||||||
selectTab,
|
selectTab,
|
||||||
@ -256,51 +265,40 @@ const ListViewTabs = () => {
|
|||||||
style={{ minHeight: 0, width: '100%', minWidth: 0 }}
|
style={{ minHeight: 0, width: '100%', minWidth: 0 }}
|
||||||
>
|
>
|
||||||
<Divider type='vertical' style={{ margin: '0 4px', height: '18px' }} />
|
<Divider type='vertical' style={{ margin: '0 4px', height: '18px' }} />
|
||||||
<Spin
|
|
||||||
spinning={initialLoading === true}
|
<Flex
|
||||||
wrapperClassName='list-view-tabs-spin'
|
align='center'
|
||||||
indicator={<LoadingOutlined spin />}
|
gap={4}
|
||||||
|
className={classNames('list-view-tabs', {
|
||||||
|
'list-view-tabs-editing': isEditing
|
||||||
|
})}
|
||||||
|
style={{ flex: 1, minWidth: 0 }}
|
||||||
>
|
>
|
||||||
<Flex
|
<ScrollBox className='list-view-tabs-segmented-wrap' horizontal>
|
||||||
align='center'
|
<Flex
|
||||||
gap={4}
|
align='center'
|
||||||
className={classNames('list-view-tabs', {
|
gap={8}
|
||||||
'list-view-tabs-editing': isEditing
|
className='list-view-tabs-segmented-inner'
|
||||||
})}
|
>
|
||||||
style={{ flex: 1, minWidth: 0 }}
|
<SegmentedNav
|
||||||
>
|
className='list-view-tabs-segmented'
|
||||||
<ScrollBox className='list-view-tabs-segmented-wrap' horizontal>
|
value={activeTabKey}
|
||||||
<Flex
|
options={options}
|
||||||
align='center'
|
onChange={selectTab}
|
||||||
gap={8}
|
onReorder={isEditing ? reorderDraftViews : undefined}
|
||||||
className='list-view-tabs-segmented-inner'
|
/>
|
||||||
>
|
{isEditing && (
|
||||||
<SegmentedNav
|
|
||||||
className='list-view-tabs-segmented'
|
|
||||||
value={activeTabKey}
|
|
||||||
options={options}
|
|
||||||
onChange={selectTab}
|
|
||||||
onReorder={isEditing ? reorderDraftViews : undefined}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
type='text'
|
type='text'
|
||||||
size='small'
|
size='small'
|
||||||
className='list-view-tabs-add-button'
|
className='list-view-tabs-add-button'
|
||||||
onClick={
|
onClick={() => handleTabEdit(null, 'add')}
|
||||||
isEditing ? () => handleTabEdit(null, 'add') : startEditing
|
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
|
||||||
}
|
|
||||||
icon={
|
|
||||||
isEditing ? (
|
|
||||||
<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />
|
|
||||||
) : (
|
|
||||||
<EditIcon style={{ fontSize: 12, marginBottom: 5 }} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
)}
|
||||||
</ScrollBox>
|
</Flex>
|
||||||
</Flex>
|
</ScrollBox>
|
||||||
</Spin>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,15 +37,12 @@ const DEFAULT_VIEW = {
|
|||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const getObjectViewKey = (view) =>
|
export const getObjectViewKey = (view) =>
|
||||||
view?._reference || view?._id || null
|
view?._id != null ? String(view._id) : null
|
||||||
|
|
||||||
const resolveViewFromRef = (views, viewRef) => {
|
const resolveViewFromId = (views, viewId) => {
|
||||||
if (!viewRef) return null
|
if (!viewId) return null
|
||||||
return (
|
return (
|
||||||
views.find(
|
views.find((view) => String(view._id) === String(viewId)) || null
|
||||||
(view) =>
|
|
||||||
view._reference === viewRef || String(view._id) === String(viewRef)
|
|
||||||
) || null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,15 +203,15 @@ export const ObjectListViewProvider = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialLoading || urlSyncedRef.current) return
|
if (initialLoading || urlSyncedRef.current) return
|
||||||
|
|
||||||
const viewRef = getViewFromUrl()
|
const viewId = getViewFromUrl()
|
||||||
if (!viewRef) {
|
if (!viewId) {
|
||||||
urlSyncedRef.current = true
|
urlSyncedRef.current = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const matched =
|
const matched =
|
||||||
resolveViewFromRef(views, viewRef) ||
|
resolveViewFromId(views, viewId) ||
|
||||||
resolveViewFromRef(draftViews, viewRef)
|
resolveViewFromId(draftViews, viewId)
|
||||||
|
|
||||||
if (matched) {
|
if (matched) {
|
||||||
setActiveTabKey(String(matched._id))
|
setActiveTabKey(String(matched._id))
|
||||||
@ -222,7 +219,7 @@ export const ObjectListViewProvider = ({
|
|||||||
return
|
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 (views.length === 0) return
|
||||||
|
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const TableStateContext = createContext()
|
|||||||
|
|
||||||
export const FILTER_URL_PARAM = 'filter'
|
export const FILTER_URL_PARAM = 'filter'
|
||||||
export const SORT_URL_PARAM = 'sort'
|
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 getSessionFilterKey = (scope) => `tableState:${scope}:filter`
|
||||||
const getSessionSortKey = (scope) => `tableState:${scope}:sort`
|
const getSessionSortKey = (scope) => `tableState:${scope}:sort`
|
||||||
@ -137,10 +137,12 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const persistView = useCallback(
|
const persistView = useCallback(
|
||||||
(viewRef) => {
|
(viewId) => {
|
||||||
const next = new URLSearchParams(searchParams)
|
const next = new URLSearchParams(searchParams)
|
||||||
if (viewRef) {
|
// Drop legacy `view` param that previously stored `_reference`
|
||||||
next.set(VIEW_URL_PARAM, viewRef)
|
next.delete('view')
|
||||||
|
if (viewId) {
|
||||||
|
next.set(VIEW_URL_PARAM, String(viewId))
|
||||||
} else {
|
} else {
|
||||||
next.delete(VIEW_URL_PARAM)
|
next.delete(VIEW_URL_PARAM)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -182,7 +182,7 @@ export const ThemeProvider = ({ children }) => {
|
|||||||
)
|
)
|
||||||
root.style.setProperty(
|
root.style.setProperty(
|
||||||
'--color-list-view-tabs-border',
|
'--color-list-view-tabs-border',
|
||||||
isDarkMode ? '#2a2a2a' : '#E7E7E7'
|
isDarkMode ? '#1f1f1f' : '#E7E7E7'
|
||||||
)
|
)
|
||||||
root.style.setProperty(
|
root.style.setProperty(
|
||||||
'--color-button-background',
|
'--color-button-background',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user