feat: mobile support (#831)
This commit is contained in:
parent
7744df1103
commit
83c9d91b88
@ -870,7 +870,7 @@ collections:
|
|||||||
- value: 2
|
- value: 2
|
||||||
label: Another fancy label
|
label: Another fancy label
|
||||||
- value: c
|
- value: c
|
||||||
label: And one more fancy label
|
label: And one more fancy label test test test test test test test
|
||||||
- label: Value and Label With Default
|
- label: Value and Label With Default
|
||||||
name: value_and_label_with_default
|
name: value_and_label_with_default
|
||||||
widget: select
|
widget: select
|
||||||
|
@ -84,9 +84,9 @@
|
|||||||
"@styled-icons/material-rounded": "10.47.0",
|
"@styled-icons/material-rounded": "10.47.0",
|
||||||
"@styled-icons/remix-editor": "10.46.0",
|
"@styled-icons/remix-editor": "10.46.0",
|
||||||
"@styled-icons/simple-icons": "10.46.0",
|
"@styled-icons/simple-icons": "10.46.0",
|
||||||
"@udecode/plate": "21.1.4",
|
"@udecode/plate": "21.3.2",
|
||||||
"@udecode/plate-juice": "21.0.0",
|
"@udecode/plate-juice": "21.3.2",
|
||||||
"@udecode/plate-serializer-md": "21.0.0",
|
"@udecode/plate-serializer-md": "21.3.2",
|
||||||
"@uiw/codemirror-extensions-langs": "4.19.16",
|
"@uiw/codemirror-extensions-langs": "4.19.16",
|
||||||
"@uiw/react-codemirror": "4.19.16",
|
"@uiw/react-codemirror": "4.19.16",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
@ -156,10 +156,10 @@
|
|||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"scheduler": "0.23.0",
|
"scheduler": "0.23.0",
|
||||||
"semaphore": "1.1.0",
|
"semaphore": "1.1.0",
|
||||||
"slate": "0.91.4",
|
"slate": "0.94.1",
|
||||||
"slate-history": "0.86.0",
|
"slate-history": "0.93.0",
|
||||||
"slate-hyperscript": "0.77.0",
|
"slate-hyperscript": "0.77.0",
|
||||||
"slate-react": "0.91.10",
|
"slate-react": "0.95.0",
|
||||||
"stream-browserify": "3.0.0",
|
"stream-browserify": "3.0.0",
|
||||||
"styled-components": "5.3.10",
|
"styled-components": "5.3.10",
|
||||||
"symbol-observable": "4.0.0",
|
"symbol-observable": "4.0.0",
|
||||||
@ -271,6 +271,7 @@
|
|||||||
"tailwindcss": "3.3.1",
|
"tailwindcss": "3.3.1",
|
||||||
"to-string-loader": "1.2.0",
|
"to-string-loader": "1.2.0",
|
||||||
"ts-jest": "29.1.0",
|
"ts-jest": "29.1.0",
|
||||||
|
"ts-node": "10.9.1",
|
||||||
"tsconfig-paths-webpack-plugin": "4.0.1",
|
"tsconfig-paths-webpack-plugin": "4.0.1",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
"webpack": "5.80.0",
|
"webpack": "5.80.0",
|
||||||
|
@ -16,6 +16,7 @@ import addExtensions from './extensions';
|
|||||||
import { getPhrases } from './lib/phrases';
|
import { getPhrases } from './lib/phrases';
|
||||||
import { selectLocale } from './reducers/selectors/config';
|
import { selectLocale } from './reducers/selectors/config';
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
|
import useMeta from './lib/hooks/useMeta';
|
||||||
|
|
||||||
import type { AnyAction } from '@reduxjs/toolkit';
|
import type { AnyAction } from '@reduxjs/toolkit';
|
||||||
import type { ConnectedProps } from 'react-redux';
|
import type { ConnectedProps } from 'react-redux';
|
||||||
@ -45,6 +46,8 @@ import ReactDOM from 'react-dom';
|
|||||||
ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = true;
|
ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = true;
|
||||||
|
|
||||||
const TranslatedApp = ({ locale, config }: AppRootProps) => {
|
const TranslatedApp = ({ locale, config }: AppRootProps) => {
|
||||||
|
useMeta({ name: 'viewport', content: 'width=device-width, initial-scale=1.0' });
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,12 @@ import React from 'react';
|
|||||||
import TopBarProgress from 'react-topbar-progress-indicator';
|
import TopBarProgress from 'react-topbar-progress-indicator';
|
||||||
|
|
||||||
import classNames from '../lib/util/classNames.util';
|
import classNames from '../lib/util/classNames.util';
|
||||||
|
import BottomNavigation from './navbar/BottomNavigation';
|
||||||
import Navbar from './navbar/Navbar';
|
import Navbar from './navbar/Navbar';
|
||||||
import Sidebar from './navbar/Sidebar';
|
import Sidebar from './navbar/Sidebar';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { Breadcrumb } from '../interface';
|
import type { Breadcrumb, Collection } from '../interface';
|
||||||
|
|
||||||
TopBarProgress.config({
|
TopBarProgress.config({
|
||||||
barColors: {
|
barColors: {
|
||||||
@ -25,6 +26,7 @@ interface MainViewProps {
|
|||||||
noMargin?: boolean;
|
noMargin?: boolean;
|
||||||
noScroll?: boolean;
|
noScroll?: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
collection?: Collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MainView = ({
|
const MainView = ({
|
||||||
@ -35,6 +37,7 @@ const MainView = ({
|
|||||||
noMargin = false,
|
noMargin = false,
|
||||||
noScroll = false,
|
noScroll = false,
|
||||||
navbarActions,
|
navbarActions,
|
||||||
|
collection,
|
||||||
}: MainViewProps) => {
|
}: MainViewProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -48,11 +51,12 @@ const MainView = ({
|
|||||||
<div
|
<div
|
||||||
id="main-view"
|
id="main-view"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
showLeftNav ? 'w-main left-64' : 'w-full',
|
showLeftNav ? ' w-full left-0 md:w-main' : 'w-full',
|
||||||
!noMargin && 'px-5 py-4',
|
!noMargin && 'px-5 py-4',
|
||||||
noScroll ? 'overflow-hidden' : 'overflow-y-auto',
|
noScroll ? 'overflow-hidden' : 'overflow-y-auto',
|
||||||
`
|
`
|
||||||
h-main
|
h-main-mobile
|
||||||
|
md:h-main
|
||||||
relative
|
relative
|
||||||
styled-scrollbars
|
styled-scrollbars
|
||||||
`,
|
`,
|
||||||
@ -61,6 +65,7 @@ const MainView = ({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<BottomNavigation collection={collection} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import ViewStyleControl from '../common/view-style/ViewStyleControl';
|
||||||
import FilterControl from './FilterControl';
|
import FilterControl from './FilterControl';
|
||||||
import GroupControl from './GroupControl';
|
import GroupControl from './GroupControl';
|
||||||
|
import MobileCollectionControls from './mobile/MobileCollectionControls';
|
||||||
import SortControl from './SortControl';
|
import SortControl from './SortControl';
|
||||||
import ViewStyleControl from '../common/view-style/ViewStyleControl';
|
|
||||||
|
|
||||||
import type { ViewStyle } from '@staticcms/core/constants/views';
|
import type { ViewStyle } from '@staticcms/core/constants/views';
|
||||||
import type {
|
import type {
|
||||||
@ -12,7 +13,6 @@ import type {
|
|||||||
SortableField,
|
SortableField,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
SortMap,
|
SortMap,
|
||||||
TranslatedProps,
|
|
||||||
ViewFilter,
|
ViewFilter,
|
||||||
ViewGroup,
|
ViewGroup,
|
||||||
} from '@staticcms/core/interface';
|
} from '@staticcms/core/interface';
|
||||||
@ -41,34 +41,69 @@ const CollectionControls = ({
|
|||||||
viewGroups,
|
viewGroups,
|
||||||
onFilterClick,
|
onFilterClick,
|
||||||
onGroupClick,
|
onGroupClick,
|
||||||
t,
|
|
||||||
filter,
|
filter,
|
||||||
group,
|
group,
|
||||||
}: TranslatedProps<CollectionControlsProps>) => {
|
}: CollectionControlsProps) => {
|
||||||
|
const showGroupControl = useMemo(
|
||||||
|
() => Boolean(viewGroups && onGroupClick && group && viewGroups.length > 0),
|
||||||
|
[group, onGroupClick, viewGroups],
|
||||||
|
);
|
||||||
|
|
||||||
|
const showFilterControl = useMemo(
|
||||||
|
() => Boolean(viewFilters && onFilterClick && filter && viewFilters.length > 0),
|
||||||
|
[filter, onFilterClick, viewFilters],
|
||||||
|
);
|
||||||
|
|
||||||
|
const showSortControl = useMemo(
|
||||||
|
() => Boolean(sortableFields && onSortClick && sort && sortableFields.length > 0),
|
||||||
|
[onSortClick, sort, sortableFields],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center relative z-20">
|
<>
|
||||||
<ViewStyleControl viewStyle={viewStyle} onChangeViewStyle={onChangeViewStyle} />
|
<div
|
||||||
{viewGroups && onGroupClick && group
|
className="
|
||||||
? viewGroups.length > 0 && (
|
flex
|
||||||
<GroupControl viewGroups={viewGroups} onGroupClick={onGroupClick} t={t} group={group} />
|
items-center
|
||||||
)
|
relative
|
||||||
: null}
|
z-20
|
||||||
{viewFilters && onFilterClick && filter
|
w-full
|
||||||
? viewFilters.length > 0 && (
|
justify-end
|
||||||
<FilterControl
|
gap-1.5
|
||||||
viewFilters={viewFilters}
|
sm:w-auto
|
||||||
onFilterClick={onFilterClick}
|
sm:justify-normal
|
||||||
t={t}
|
lg:gap-2
|
||||||
filter={filter}
|
flex-[1_0_0%]
|
||||||
/>
|
"
|
||||||
)
|
>
|
||||||
: null}
|
<ViewStyleControl viewStyle={viewStyle} onChangeViewStyle={onChangeViewStyle} />
|
||||||
{sortableFields && onSortClick && sort
|
{showGroupControl || showFilterControl || showFilterControl ? (
|
||||||
? sortableFields.length > 0 && (
|
<MobileCollectionControls
|
||||||
<SortControl fields={sortableFields} sort={sort} onSortClick={onSortClick} />
|
showFilterControl={showFilterControl}
|
||||||
)
|
viewFilters={viewFilters}
|
||||||
: null}
|
onFilterClick={onFilterClick}
|
||||||
</div>
|
filter={filter}
|
||||||
|
showGroupControl={showGroupControl}
|
||||||
|
viewGroups={viewGroups}
|
||||||
|
onGroupClick={onGroupClick}
|
||||||
|
group={group}
|
||||||
|
showSortControl={showSortControl}
|
||||||
|
fields={sortableFields}
|
||||||
|
sort={sort}
|
||||||
|
onSortClick={onSortClick}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{showGroupControl ? (
|
||||||
|
<GroupControl viewGroups={viewGroups} onGroupClick={onGroupClick} group={group} />
|
||||||
|
) : null}
|
||||||
|
{showFilterControl ? (
|
||||||
|
<FilterControl viewFilters={viewFilters} onFilterClick={onFilterClick} filter={filter} />
|
||||||
|
) : null}
|
||||||
|
{showSortControl ? (
|
||||||
|
<SortControl fields={sortableFields} sort={sort} onSortClick={onSortClick} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,26 +11,25 @@ import {
|
|||||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
import { addFileTemplateFields } from '@staticcms/core/lib/widgets/stringTemplate';
|
import { addFileTemplateFields } from '@staticcms/core/lib/widgets/stringTemplate';
|
||||||
import Button from '../common/button/Button';
|
import Button from '../common/button/Button';
|
||||||
|
import useNewEntryUrl from '@staticcms/core/lib/hooks/useNewEntryUrl';
|
||||||
|
|
||||||
import type { Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
|
import type { Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
interface CollectionHeaderProps {
|
interface CollectionHeaderProps {
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
newEntryUrl?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollectionHeader = ({
|
const CollectionHeader: FC<TranslatedProps<CollectionHeaderProps>> = ({ collection, t }) => {
|
||||||
collection,
|
|
||||||
newEntryUrl,
|
|
||||||
t,
|
|
||||||
}: TranslatedProps<CollectionHeaderProps>) => {
|
|
||||||
const collectionLabel = collection.label;
|
const collectionLabel = collection.label;
|
||||||
const collectionLabelSingular = collection.label_singular;
|
const collectionLabelSingular = collection.label_singular;
|
||||||
|
|
||||||
const icon = useIcon(collection.icon);
|
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const filterTerm = useMemo(() => params['*'], [params]);
|
const filterTerm = useMemo(() => params['*'], [params]);
|
||||||
|
const newEntryUrl = useNewEntryUrl(collection, filterTerm);
|
||||||
|
|
||||||
|
const icon = useIcon(collection.icon);
|
||||||
|
|
||||||
const entries = useEntries(collection);
|
const entries = useEntries(collection);
|
||||||
|
|
||||||
const pluralLabel = useMemo(() => {
|
const pluralLabel = useMemo(() => {
|
||||||
@ -65,7 +64,18 @@ const CollectionHeader = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-grow gap-4">
|
<div
|
||||||
|
className="
|
||||||
|
flex
|
||||||
|
flex-grow
|
||||||
|
gap-4
|
||||||
|
justify-normal
|
||||||
|
xs:justify-between
|
||||||
|
sm:justify-normal
|
||||||
|
w-full
|
||||||
|
truncate
|
||||||
|
"
|
||||||
|
>
|
||||||
<h2
|
<h2
|
||||||
className="
|
className="
|
||||||
text-xl
|
text-xl
|
||||||
@ -75,13 +85,25 @@ const CollectionHeader = ({
|
|||||||
text-gray-800
|
text-gray-800
|
||||||
dark:text-gray-300
|
dark:text-gray-300
|
||||||
gap-2
|
gap-2
|
||||||
|
flex-grow
|
||||||
|
w-full
|
||||||
|
md:grow-0
|
||||||
|
md:w-auto
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div className="flex items-center">{icon}</div>
|
<div className="flex items-center">{icon}</div>
|
||||||
{pluralLabel}
|
<div
|
||||||
|
className="
|
||||||
|
w-collection-header
|
||||||
|
flex-grow
|
||||||
|
truncate
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{pluralLabel}
|
||||||
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
{newEntryUrl ? (
|
{newEntryUrl ? (
|
||||||
<Button to={newEntryUrl}>
|
<Button to={newEntryUrl} className="hidden md:flex">
|
||||||
{t('collection.collectionTop.newButton', {
|
{t('collection.collectionTop.newButton', {
|
||||||
collectionLabel: collectionLabelSingular || pluralLabel,
|
collectionLabel: collectionLabelSingular || pluralLabel,
|
||||||
})}
|
})}
|
||||||
@ -92,4 +114,4 @@ const CollectionHeader = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(CollectionHeader);
|
export default translate()(CollectionHeader) as FC<CollectionHeaderProps>;
|
||||||
|
@ -42,7 +42,14 @@ const SingleCollectionPage: FC<SingleCollectionPageProps> = ({
|
|||||||
const breadcrumbs = useBreadcrumbs(collection, filterTerm);
|
const breadcrumbs = useBreadcrumbs(collection, filterTerm);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainView breadcrumbs={breadcrumbs} showQuickCreate showLeftNav noScroll noMargin>
|
<MainView
|
||||||
|
breadcrumbs={breadcrumbs}
|
||||||
|
collection={collection}
|
||||||
|
showQuickCreate
|
||||||
|
showLeftNav
|
||||||
|
noScroll
|
||||||
|
noMargin
|
||||||
|
>
|
||||||
<CollectionView
|
<CollectionView
|
||||||
name={name}
|
name={name}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
@ -136,6 +136,10 @@ const CollectionSearch = ({
|
|||||||
[submitSearch],
|
[submitSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleClick = useCallback((event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -171,6 +175,7 @@ const CollectionSearch = ({
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
value={query}
|
value={query}
|
||||||
onChange={handleQueryChange}
|
onChange={handleQueryChange}
|
||||||
|
onClick={handleClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PopperUnstyled
|
<PopperUnstyled
|
||||||
@ -191,7 +196,7 @@ const CollectionSearch = ({
|
|||||||
ring-opacity-5
|
ring-opacity-5
|
||||||
focus:outline-none
|
focus:outline-none
|
||||||
sm:text-sm
|
sm:text-sm
|
||||||
z-40
|
z-[1300]
|
||||||
dark:bg-slate-700
|
dark:bg-slate-700
|
||||||
dark:shadow-lg
|
dark:shadow-lg
|
||||||
"
|
"
|
||||||
|
@ -9,13 +9,11 @@ import {
|
|||||||
sortByField as sortByFieldAction,
|
sortByField as sortByFieldAction,
|
||||||
} from '@staticcms/core/actions/entries';
|
} from '@staticcms/core/actions/entries';
|
||||||
import { SORT_DIRECTION_ASCENDING } from '@staticcms/core/constants';
|
import { SORT_DIRECTION_ASCENDING } from '@staticcms/core/constants';
|
||||||
import { getNewEntryUrl } from '@staticcms/core/lib/urlHelper';
|
|
||||||
import {
|
import {
|
||||||
selectSortableFields,
|
selectSortableFields,
|
||||||
selectViewFilters,
|
selectViewFilters,
|
||||||
selectViewGroups,
|
selectViewGroups,
|
||||||
} from '@staticcms/core/lib/util/collection.util';
|
} from '@staticcms/core/lib/util/collection.util';
|
||||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
|
||||||
import {
|
import {
|
||||||
selectEntriesFilter,
|
selectEntriesFilter,
|
||||||
selectEntriesGroup,
|
selectEntriesGroup,
|
||||||
@ -42,7 +40,6 @@ import type { ConnectedProps } from 'react-redux';
|
|||||||
const CollectionView = ({
|
const CollectionView = ({
|
||||||
collection,
|
collection,
|
||||||
collections,
|
collections,
|
||||||
collectionName,
|
|
||||||
isSearchResults,
|
isSearchResults,
|
||||||
isSingleSearchResult,
|
isSingleSearchResult,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
@ -67,16 +64,6 @@ const CollectionView = ({
|
|||||||
setPrevCollection(collection);
|
setPrevCollection(collection);
|
||||||
}, [collection]);
|
}, [collection]);
|
||||||
|
|
||||||
const newEntryUrl = useMemo(() => {
|
|
||||||
if (!collectionName || !collection) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'fields' in collection && collection.create
|
|
||||||
? `${getNewEntryUrl(collectionName)}${isNotEmpty(filterTerm) ? `/${filterTerm}` : ''}`
|
|
||||||
: '';
|
|
||||||
}, [collection, collectionName, filterTerm]);
|
|
||||||
|
|
||||||
const searchResultKey = useMemo(
|
const searchResultKey = useMemo(
|
||||||
() => `collection.collectionTop.searchResults${isSingleSearchResult ? 'InCollection' : ''}`,
|
() => `collection.collectionTop.searchResults${isSingleSearchResult ? 'InCollection' : ''}`,
|
||||||
[isSingleSearchResult],
|
[isSingleSearchResult],
|
||||||
@ -196,18 +183,18 @@ const CollectionView = ({
|
|||||||
const collectionDescription = collection?.description;
|
const collectionDescription = collection?.description;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full px-5 pt-4">
|
<div className="flex flex-col h-full px-5 pt-4 overflow-hidden">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4 flex-row gap-4 sm:gap-0">
|
||||||
{isSearchResults ? (
|
{isSearchResults ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<div>{t(searchResultKey, { searchTerm, collection: collection?.label })}</div>
|
<div>{t(searchResultKey, { searchTerm, collection: collection?.label })}</div>
|
||||||
</div>
|
</div>
|
||||||
<CollectionControls viewStyle={viewStyle} onChangeViewStyle={changeViewStyle} t={t} />
|
<CollectionControls viewStyle={viewStyle} onChangeViewStyle={changeViewStyle} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CollectionHeader collection={collection} newEntryUrl={newEntryUrl} />
|
{collection ? <CollectionHeader collection={collection} /> : null}
|
||||||
<CollectionControls
|
<CollectionControls
|
||||||
viewStyle={viewStyle}
|
viewStyle={viewStyle}
|
||||||
onChangeViewStyle={changeViewStyle}
|
onChangeViewStyle={changeViewStyle}
|
||||||
@ -216,7 +203,6 @@ const CollectionView = ({
|
|||||||
sort={sort}
|
sort={sort}
|
||||||
viewFilters={viewFilters ?? []}
|
viewFilters={viewFilters ?? []}
|
||||||
viewGroups={viewGroups ?? []}
|
viewGroups={viewGroups ?? []}
|
||||||
t={t}
|
|
||||||
onFilterClick={onFilterClick}
|
onFilterClick={onFilterClick}
|
||||||
onGroupClick={onGroupClick}
|
onGroupClick={onGroupClick}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
@ -270,7 +256,6 @@ function mapStateToProps(state: RootState, ownProps: TranslatedProps<CollectionV
|
|||||||
filterTerm,
|
filterTerm,
|
||||||
collection,
|
collection,
|
||||||
collections,
|
collections,
|
||||||
collectionName: name,
|
|
||||||
sort,
|
sort,
|
||||||
sortableFields,
|
sortableFields,
|
||||||
viewFilters,
|
viewFilters,
|
||||||
|
@ -6,19 +6,21 @@ import MenuGroup from '../common/menu/MenuGroup';
|
|||||||
import MenuItemButton from '../common/menu/MenuItemButton';
|
import MenuItemButton from '../common/menu/MenuItemButton';
|
||||||
|
|
||||||
import type { FilterMap, TranslatedProps, ViewFilter } from '@staticcms/core/interface';
|
import type { FilterMap, TranslatedProps, ViewFilter } from '@staticcms/core/interface';
|
||||||
import type { MouseEvent } from 'react';
|
import type { FC, MouseEvent } from 'react';
|
||||||
|
|
||||||
interface FilterControlProps {
|
export interface FilterControlProps {
|
||||||
filter: Record<string, FilterMap>;
|
filter: Record<string, FilterMap> | undefined;
|
||||||
viewFilters: ViewFilter[];
|
viewFilters: ViewFilter[] | undefined;
|
||||||
onFilterClick: (viewFilter: ViewFilter) => void;
|
variant?: 'menu' | 'list';
|
||||||
|
onFilterClick: ((viewFilter: ViewFilter) => void) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterControl = ({
|
const FilterControl = ({
|
||||||
viewFilters,
|
filter = {},
|
||||||
t,
|
viewFilters = [],
|
||||||
|
variant = 'menu',
|
||||||
onFilterClick,
|
onFilterClick,
|
||||||
filter,
|
t,
|
||||||
}: TranslatedProps<FilterControlProps>) => {
|
}: TranslatedProps<FilterControlProps>) => {
|
||||||
const anyActive = useMemo(() => Object.keys(filter).some(key => filter[key]?.active), [filter]);
|
const anyActive = useMemo(() => Object.keys(filter).some(key => filter[key]?.active), [filter]);
|
||||||
|
|
||||||
@ -26,15 +28,65 @@ const FilterControl = ({
|
|||||||
(viewFilter: ViewFilter) => (event: MouseEvent) => {
|
(viewFilter: ViewFilter) => (event: MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onFilterClick(viewFilter);
|
onFilterClick?.(viewFilter);
|
||||||
},
|
},
|
||||||
[onFilterClick],
|
[onFilterClick],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (variant === 'list') {
|
||||||
|
return (
|
||||||
|
<div key="filter-by-list" className="flex flex-col gap-2">
|
||||||
|
<h3
|
||||||
|
className="
|
||||||
|
text-lg
|
||||||
|
font-bold
|
||||||
|
text-gray-800
|
||||||
|
dark:text-white
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{t('collection.collectionTop.filterBy')}
|
||||||
|
</h3>
|
||||||
|
{viewFilters.map(viewFilter => {
|
||||||
|
const checked = Boolean(viewFilter.id && filter[viewFilter?.id]?.active) ?? false;
|
||||||
|
const labelId = `filter-list-label-${viewFilter.label}`;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={viewFilter.id}
|
||||||
|
className="
|
||||||
|
ml-1.5
|
||||||
|
font-medium
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300
|
||||||
|
"
|
||||||
|
onClick={handleFilterClick(viewFilter)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
key={`${labelId}-${checked}`}
|
||||||
|
id={labelId}
|
||||||
|
type="checkbox"
|
||||||
|
value=""
|
||||||
|
className=""
|
||||||
|
checked={checked}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
|
||||||
|
{viewFilter.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
|
key="filter-by-menu"
|
||||||
label={t('collection.collectionTop.filterBy')}
|
label={t('collection.collectionTop.filterBy')}
|
||||||
variant={anyActive ? 'contained' : 'outlined'}
|
variant={anyActive ? 'contained' : 'outlined'}
|
||||||
|
rootClassName="hidden lg:block"
|
||||||
>
|
>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
{viewFilters.map(viewFilter => {
|
{viewFilters.map(viewFilter => {
|
||||||
@ -62,4 +114,4 @@ const FilterControl = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(FilterControl);
|
export default translate()(FilterControl) as FC<FilterControlProps>;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Check as CheckIcon } from '@styled-icons/material/Check';
|
import { Check as CheckIcon } from '@styled-icons/material/Check';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
import Menu from '../common/menu/Menu';
|
import Menu from '../common/menu/Menu';
|
||||||
@ -7,31 +7,87 @@ import MenuGroup from '../common/menu/MenuGroup';
|
|||||||
import MenuItemButton from '../common/menu/MenuItemButton';
|
import MenuItemButton from '../common/menu/MenuItemButton';
|
||||||
|
|
||||||
import type { GroupMap, TranslatedProps, ViewGroup } from '@staticcms/core/interface';
|
import type { GroupMap, TranslatedProps, ViewGroup } from '@staticcms/core/interface';
|
||||||
|
import type { FC, MouseEvent } from 'react';
|
||||||
|
|
||||||
interface GroupControlProps {
|
export interface GroupControlProps {
|
||||||
group: Record<string, GroupMap>;
|
group: Record<string, GroupMap> | undefined;
|
||||||
viewGroups: ViewGroup[];
|
viewGroups: ViewGroup[] | undefined;
|
||||||
onGroupClick: (viewGroup: ViewGroup) => void;
|
variant?: 'menu' | 'list';
|
||||||
|
onGroupClick: ((viewGroup: ViewGroup) => void) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupControl = ({
|
const GroupControl = ({
|
||||||
viewGroups,
|
viewGroups = [],
|
||||||
group,
|
group = {},
|
||||||
t,
|
variant = 'menu',
|
||||||
onGroupClick,
|
onGroupClick,
|
||||||
|
t,
|
||||||
}: TranslatedProps<GroupControlProps>) => {
|
}: TranslatedProps<GroupControlProps>) => {
|
||||||
const activeGroup = useMemo(() => Object.values(group).find(f => f.active === true), [group]);
|
const activeGroup = useMemo(() => Object.values(group).find(f => f.active === true), [group]);
|
||||||
|
|
||||||
|
const handleGroupClick = useCallback(
|
||||||
|
(viewGroup: ViewGroup) => (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
onGroupClick?.(viewGroup);
|
||||||
|
},
|
||||||
|
[onGroupClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variant === 'list') {
|
||||||
|
return (
|
||||||
|
<div key="filter-by-list" className="flex flex-col gap-2">
|
||||||
|
<h3
|
||||||
|
className="
|
||||||
|
text-lg
|
||||||
|
font-bold
|
||||||
|
text-gray-800
|
||||||
|
dark:text-white
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{t('collection.collectionTop.groupBy')}
|
||||||
|
</h3>
|
||||||
|
{viewGroups.map(viewGroup => {
|
||||||
|
const active = Boolean(viewGroup.id && group[viewGroup?.id]?.active) ?? false;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={viewGroup.id}
|
||||||
|
className="
|
||||||
|
ml-0.5
|
||||||
|
font-medium
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300
|
||||||
|
"
|
||||||
|
onClick={handleGroupClick(viewGroup)}
|
||||||
|
>
|
||||||
|
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
|
||||||
|
{viewGroup.label}
|
||||||
|
</label>
|
||||||
|
{active ? (
|
||||||
|
<CheckIcon key="checkmark" className="ml-2 w-6 h-6 text-blue-500" />
|
||||||
|
) : (
|
||||||
|
<div key="not-checked" className="ml-2 w-6 h-6" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
label={t('collection.collectionTop.groupBy')}
|
label={t('collection.collectionTop.groupBy')}
|
||||||
variant={activeGroup ? 'contained' : 'outlined'}
|
variant={activeGroup ? 'contained' : 'outlined'}
|
||||||
|
rootClassName="hidden lg:block"
|
||||||
>
|
>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
{viewGroups.map(viewGroup => (
|
{viewGroups.map(viewGroup => (
|
||||||
<MenuItemButton
|
<MenuItemButton
|
||||||
key={viewGroup.id}
|
key={viewGroup.id}
|
||||||
onClick={() => onGroupClick(viewGroup)}
|
onClick={() => onGroupClick?.(viewGroup)}
|
||||||
endIcon={viewGroup.id === activeGroup?.id ? CheckIcon : undefined}
|
endIcon={viewGroup.id === activeGroup?.id ? CheckIcon : undefined}
|
||||||
>
|
>
|
||||||
{viewGroup.label}
|
{viewGroup.label}
|
||||||
@ -42,4 +98,4 @@ const GroupControl = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(GroupControl);
|
export default translate()(GroupControl) as FC<GroupControlProps>;
|
||||||
|
@ -11,6 +11,7 @@ import NavLink from '../navbar/NavLink';
|
|||||||
|
|
||||||
import type { Collection, Entry } from '@staticcms/core/interface';
|
import type { Collection, Entry } from '@staticcms/core/interface';
|
||||||
import type { TreeNodeData } from '@staticcms/core/lib/util/nested.util';
|
import type { TreeNodeData } from '@staticcms/core/lib/util/nested.util';
|
||||||
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
function getNodeTitle(node: TreeNodeData) {
|
function getNodeTitle(node: TreeNodeData) {
|
||||||
const title = node.isRoot
|
const title = node.isRoot
|
||||||
@ -29,6 +30,20 @@ interface TreeNodeProps {
|
|||||||
const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps) => {
|
const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps) => {
|
||||||
const collectionName = collection.name;
|
const collectionName = collection.name;
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(event: MouseEvent | undefined, node: TreeNodeData, expanded: boolean) => {
|
||||||
|
event?.stopPropagation();
|
||||||
|
event?.preventDefault();
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
onToggle({ node, expanded });
|
||||||
|
} else {
|
||||||
|
onToggle({ node, expanded: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onToggle],
|
||||||
|
);
|
||||||
|
|
||||||
const sortedData = sortBy(treeData, getNodeTitle);
|
const sortedData = sortBy(treeData, getNodeTitle);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -50,7 +65,7 @@ const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps)
|
|||||||
<div className={classNames(depth !== 0 && 'ml-8')}>
|
<div className={classNames(depth !== 0 && 'ml-8')}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={to}
|
to={to}
|
||||||
onClick={() => onToggle({ node, expanded: !node.expanded })}
|
onClick={() => handleClick(undefined, node, !node.expanded)}
|
||||||
data-testid={node.path}
|
data-testid={node.path}
|
||||||
icon={<ArticleIcon className={classNames(depth === 0 ? 'h-6 w-6' : 'h-5 w-5')} />}
|
icon={<ArticleIcon className={classNames(depth === 0 ? 'h-6 w-6' : 'h-5 w-5')} />}
|
||||||
>
|
>
|
||||||
@ -58,6 +73,7 @@ const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps)
|
|||||||
<div>{title}</div>
|
<div>{title}</div>
|
||||||
{hasChildren && (
|
{hasChildren && (
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon
|
||||||
|
onClick={event => handleClick(event, node, !node.expanded)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
node.expanded && 'rotate-90 transform',
|
node.expanded && 'rotate-90 transform',
|
||||||
`
|
`
|
||||||
@ -135,7 +151,6 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
|||||||
const entries = useEntries(collection);
|
const entries = useEntries(collection);
|
||||||
|
|
||||||
const [treeData, setTreeData] = useState<TreeNodeData[]>(getTreeData(collection, entries));
|
const [treeData, setTreeData] = useState<TreeNodeData[]>(getTreeData(collection, entries));
|
||||||
const [selected, setSelected] = useState<TreeNodeData | null>(null);
|
|
||||||
const [useFilter, setUseFilter] = useState(true);
|
const [useFilter, setUseFilter] = useState(true);
|
||||||
|
|
||||||
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
|
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
|
||||||
@ -186,22 +201,15 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
|||||||
|
|
||||||
const onToggle = useCallback(
|
const onToggle = useCallback(
|
||||||
({ node, expanded }: { node: TreeNodeData; expanded: boolean }) => {
|
({ node, expanded }: { node: TreeNodeData; expanded: boolean }) => {
|
||||||
if (!selected || selected.path === node.path || expanded) {
|
setTreeData(
|
||||||
setTreeData(
|
updateNode(treeData, node, node => ({
|
||||||
updateNode(treeData, node, node => ({
|
...node,
|
||||||
...node,
|
expanded,
|
||||||
expanded,
|
})),
|
||||||
})),
|
);
|
||||||
);
|
setUseFilter(false);
|
||||||
setSelected(node);
|
|
||||||
setUseFilter(false);
|
|
||||||
} else {
|
|
||||||
// don't collapse non selected nodes when clicked
|
|
||||||
setSelected(node);
|
|
||||||
setUseFilter(false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[selected, treeData],
|
[treeData],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <TreeNode collection={collection} treeData={treeData} onToggle={onToggle} />;
|
return <TreeNode collection={collection} treeData={treeData} onToggle={onToggle} />;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { KeyboardArrowDown as KeyboardArrowDownIcon } from '@styled-icons/material/KeyboardArrowDown';
|
import { KeyboardArrowDown as KeyboardArrowDownIcon } from '@styled-icons/material/KeyboardArrowDown';
|
||||||
import { KeyboardArrowUp as KeyboardArrowUpIcon } from '@styled-icons/material/KeyboardArrowUp';
|
import { KeyboardArrowUp as KeyboardArrowUpIcon } from '@styled-icons/material/KeyboardArrowUp';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -18,6 +18,7 @@ import type {
|
|||||||
SortMap,
|
SortMap,
|
||||||
TranslatedProps,
|
TranslatedProps,
|
||||||
} from '@staticcms/core/interface';
|
} from '@staticcms/core/interface';
|
||||||
|
import type { FC, MouseEvent } from 'react';
|
||||||
|
|
||||||
function nextSortDirection(direction: SortDirection) {
|
function nextSortDirection(direction: SortDirection) {
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
@ -30,13 +31,20 @@ function nextSortDirection(direction: SortDirection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortControlProps {
|
export interface SortControlProps {
|
||||||
fields: SortableField[];
|
fields: SortableField[] | undefined;
|
||||||
onSortClick: (key: string, direction?: SortDirection) => Promise<void>;
|
|
||||||
sort: SortMap | undefined;
|
sort: SortMap | undefined;
|
||||||
|
variant?: 'menu' | 'list';
|
||||||
|
onSortClick: ((key: string, direction?: SortDirection) => Promise<void>) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortControlProps>) => {
|
const SortControl = ({
|
||||||
|
fields = [],
|
||||||
|
sort = {},
|
||||||
|
variant = 'menu',
|
||||||
|
onSortClick,
|
||||||
|
t,
|
||||||
|
}: TranslatedProps<SortControlProps>) => {
|
||||||
const selectedSort = useMemo(() => {
|
const selectedSort = useMemo(() => {
|
||||||
if (!sort) {
|
if (!sort) {
|
||||||
return { key: undefined, direction: undefined };
|
return { key: undefined, direction: undefined };
|
||||||
@ -50,10 +58,68 @@ const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortContr
|
|||||||
return sortValues[0];
|
return sortValues[0];
|
||||||
}, [sort]);
|
}, [sort]);
|
||||||
|
|
||||||
|
const handleSortClick = useCallback(
|
||||||
|
(key: string, direction?: SortDirection) => (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
onSortClick?.(key, direction);
|
||||||
|
},
|
||||||
|
[onSortClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variant === 'list') {
|
||||||
|
return (
|
||||||
|
<div key="filter-by-list" className="flex flex-col gap-2">
|
||||||
|
<h3
|
||||||
|
className="
|
||||||
|
text-lg
|
||||||
|
font-bold
|
||||||
|
text-gray-800
|
||||||
|
dark:text-white
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{t('collection.collectionTop.sortBy')}
|
||||||
|
</h3>
|
||||||
|
{fields.map(field => {
|
||||||
|
const sortDir = sort?.[field.name]?.direction ?? SORT_DIRECTION_NONE;
|
||||||
|
const nextSortDir = nextSortDirection(sortDir);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={field.name}
|
||||||
|
className="
|
||||||
|
ml-0.5
|
||||||
|
font-medium
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
text-gray-800
|
||||||
|
dark:text-gray-300
|
||||||
|
"
|
||||||
|
onClick={handleSortClick(field.name, nextSortDir)}
|
||||||
|
>
|
||||||
|
<label className="ml-2 text-md font-medium text-gray-800 dark:text-gray-300">
|
||||||
|
{field.label ?? field.name}
|
||||||
|
</label>
|
||||||
|
{field.name === selectedSort.key ? (
|
||||||
|
selectedSort.direction === SORT_DIRECTION_ASCENDING ? (
|
||||||
|
<KeyboardArrowUpIcon key="checkmark" className="ml-2 w-6 h-6 text-blue-500" />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowDownIcon key="checkmark" className="ml-2 w-6 h-6 text-blue-500" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div key="not-checked" className="ml-2 w-6 h-6" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
label={t('collection.collectionTop.sortBy')}
|
label={t('collection.collectionTop.sortBy')}
|
||||||
variant={selectedSort.key ? 'contained' : 'outlined'}
|
variant={selectedSort.key ? 'contained' : 'outlined'}
|
||||||
|
rootClassName="hidden lg:block"
|
||||||
>
|
>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
{fields.map(field => {
|
{fields.map(field => {
|
||||||
@ -62,7 +128,7 @@ const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortContr
|
|||||||
return (
|
return (
|
||||||
<MenuItemButton
|
<MenuItemButton
|
||||||
key={field.name}
|
key={field.name}
|
||||||
onClick={() => onSortClick(field.name, nextSortDir)}
|
onClick={handleSortClick(field.name, nextSortDir)}
|
||||||
active={field.name === selectedSort.key}
|
active={field.name === selectedSort.key}
|
||||||
endIcon={
|
endIcon={
|
||||||
field.name === selectedSort.key
|
field.name === selectedSort.key
|
||||||
@ -81,4 +147,4 @@ const SortControl = ({ t, fields, onSortClick, sort }: TranslatedProps<SortContr
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(SortControl);
|
export default translate()(SortControl) as FC<SortControlProps>;
|
||||||
|
@ -81,7 +81,20 @@ const Entries = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>{t('collection.entries.noEntries')}</div>;
|
return (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
py-2
|
||||||
|
px-3
|
||||||
|
rounded-md
|
||||||
|
bg-yellow-300/75
|
||||||
|
dark:bg-yellow-800/75
|
||||||
|
text-sm
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{t('collection.entries.noEntries')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(Entries);
|
export default translate()(Entries);
|
||||||
|
@ -6,10 +6,10 @@ import { loadEntries, traverseCollectionCursor } from '@staticcms/core/actions/e
|
|||||||
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||||
import useGroups from '@staticcms/core/lib/hooks/useGroups';
|
import useGroups from '@staticcms/core/lib/hooks/useGroups';
|
||||||
import { Cursor } from '@staticcms/core/lib/util';
|
import { Cursor } from '@staticcms/core/lib/util';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
|
||||||
import { selectCollectionEntriesCursor } from '@staticcms/core/reducers/selectors/cursors';
|
import { selectCollectionEntriesCursor } from '@staticcms/core/reducers/selectors/cursors';
|
||||||
import { selectEntriesLoaded, selectIsFetching } from '@staticcms/core/reducers/selectors/entries';
|
import { selectEntriesLoaded, selectIsFetching } from '@staticcms/core/reducers/selectors/entries';
|
||||||
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
||||||
|
import Button from '../../common/button/Button';
|
||||||
import Entries from './Entries';
|
import Entries from './Entries';
|
||||||
|
|
||||||
import type { ViewStyle } from '@staticcms/core/constants/views';
|
import type { ViewStyle } from '@staticcms/core/constants/views';
|
||||||
@ -104,39 +104,64 @@ const EntriesCollection = ({
|
|||||||
[collection, dispatch],
|
[collection, dispatch],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState(0);
|
||||||
|
const handleGroupClick = useCallback(
|
||||||
|
(index: number) => () => {
|
||||||
|
setSelectedGroup(index);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
if (groups && groups.length > 0) {
|
if (groups && groups.length > 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{groups.map((group, index) => {
|
<div
|
||||||
const title = getGroupTitle(group, t);
|
className="
|
||||||
return (
|
pb-3
|
||||||
<div key={group.id} id={group.id}>
|
"
|
||||||
<h2
|
>
|
||||||
className={classNames(
|
<div
|
||||||
`
|
className="
|
||||||
px-2
|
-m-1
|
||||||
pt-4
|
"
|
||||||
pb-2
|
>
|
||||||
`,
|
<div
|
||||||
index === 0 && 'pt-0',
|
className="
|
||||||
)}
|
flex
|
||||||
>
|
gap-2
|
||||||
{title}
|
p-1
|
||||||
</h2>
|
overflow-x-auto
|
||||||
<Entries
|
hide-scrollbar
|
||||||
collection={collection}
|
"
|
||||||
entries={getGroupEntries(filteredEntries, group.paths)}
|
>
|
||||||
isFetching={isFetching}
|
{groups.map((group, index) => {
|
||||||
collectionName={collection.label}
|
const title = getGroupTitle(group, t);
|
||||||
viewStyle={viewStyle}
|
return (
|
||||||
cursor={cursor}
|
<Button
|
||||||
handleCursorActions={handleCursorActions}
|
key={index}
|
||||||
page={page}
|
variant={index === selectedGroup ? 'contained' : 'text'}
|
||||||
filterTerm={filterTerm}
|
onClick={handleGroupClick(index)}
|
||||||
/>
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
</div>
|
||||||
|
<Entries
|
||||||
|
key={`entries-with-group-${groups[selectedGroup].id}`}
|
||||||
|
collection={collection}
|
||||||
|
entries={getGroupEntries(filteredEntries, groups[selectedGroup].paths)}
|
||||||
|
isFetching={isFetching}
|
||||||
|
collectionName={collection.label}
|
||||||
|
viewStyle={viewStyle}
|
||||||
|
cursor={cursor}
|
||||||
|
handleCursorActions={handleCursorActions}
|
||||||
|
page={page}
|
||||||
|
filterTerm={filterTerm}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -113,52 +113,64 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
|||||||
|
|
||||||
if (PreviewCardComponent) {
|
if (PreviewCardComponent) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className="h-full w-full relative overflow-visible">
|
||||||
<CardActionArea to={path}>
|
<div className="absolute -inset-1 pr-2">
|
||||||
<PreviewCardComponent
|
<div className="p-1 h-full w-full">
|
||||||
collection={collection}
|
<Card>
|
||||||
fields={fields}
|
<CardActionArea to={path}>
|
||||||
entry={entry}
|
<PreviewCardComponent
|
||||||
widgetFor={widgetFor}
|
collection={collection}
|
||||||
widgetsFor={widgetsFor}
|
fields={fields}
|
||||||
theme={theme}
|
entry={entry}
|
||||||
hasLocalBackup={hasLocalBackup}
|
widgetFor={widgetFor}
|
||||||
/>
|
widgetsFor={widgetsFor}
|
||||||
</CardActionArea>
|
theme={theme}
|
||||||
</Card>
|
hasLocalBackup={hasLocalBackup}
|
||||||
|
/>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="h-full" title={summary}>
|
<div className="h-full w-full relative overflow-visible">
|
||||||
<CardActionArea to={path}>
|
<div className="absolute -inset-1 pr-2">
|
||||||
{image && imageField ? (
|
<div className="p-1 h-full w-full">
|
||||||
<CardMedia
|
<Card className="h-full" title={summary}>
|
||||||
height="140"
|
<CardActionArea to={path}>
|
||||||
image={image}
|
{image && imageField ? (
|
||||||
collection={collection}
|
<CardMedia
|
||||||
field={imageField}
|
height="140"
|
||||||
entry={entry}
|
image={image}
|
||||||
/>
|
collection={collection}
|
||||||
) : null}
|
field={imageField}
|
||||||
<CardContent>
|
entry={entry}
|
||||||
<div className="flex w-full items-center justify-between">
|
/>
|
||||||
<div className="whitespace-nowrap overflow-hidden text-ellipsis">{summary}</div>
|
) : null}
|
||||||
{hasLocalBackup ? (
|
<CardContent>
|
||||||
<InfoIcon
|
<div className="flex w-full items-center justify-between">
|
||||||
className="
|
<div className="truncate">{summary}</div>
|
||||||
w-5
|
{hasLocalBackup ? (
|
||||||
h-5
|
<InfoIcon
|
||||||
text-blue-600
|
className="
|
||||||
dark:text-blue-300
|
w-5
|
||||||
"
|
h-5
|
||||||
title={t('ui.localBackup.hasLocalBackup')}
|
text-blue-600
|
||||||
/>
|
dark:text-blue-300
|
||||||
) : null}
|
"
|
||||||
</div>
|
title={t('ui.localBackup.hasLocalBackup')}
|
||||||
</CardContent>
|
/>
|
||||||
</CardActionArea>
|
) : null}
|
||||||
</Card>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ const CardWrapper = ({
|
|||||||
? style.left ?? COLLECTION_CARD_MARGIN * columnIndex
|
? style.left ?? COLLECTION_CARD_MARGIN * columnIndex
|
||||||
: style.left
|
: style.left
|
||||||
}`,
|
}`,
|
||||||
),
|
) + 4,
|
||||||
[columnIndex, style.left],
|
[columnIndex, style.left],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -138,12 +138,22 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
|||||||
}, [cardHeights, prevCardHeights.length]);
|
}, [cardHeights, prevCardHeights.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full">
|
<div
|
||||||
|
className="
|
||||||
|
relative
|
||||||
|
w-card-grid
|
||||||
|
h-full
|
||||||
|
overflow-hidden
|
||||||
|
-ml-1
|
||||||
|
"
|
||||||
|
>
|
||||||
<AutoSizer onResize={handleResize}>
|
<AutoSizer onResize={handleResize}>
|
||||||
{({ height = 0, width = 0 }) => {
|
{({ height = 0, width = 0 }) => {
|
||||||
|
const calculatedWidth = width - 4;
|
||||||
const columnWidthWithGutter = COLLECTION_CARD_WIDTH + COLLECTION_CARD_MARGIN;
|
const columnWidthWithGutter = COLLECTION_CARD_WIDTH + COLLECTION_CARD_MARGIN;
|
||||||
const columnCount = Math.floor(width / columnWidthWithGutter);
|
const columnCount = Math.max(Math.floor(calculatedWidth / columnWidthWithGutter), 1);
|
||||||
const nonGutterSpace = (width - COLLECTION_CARD_MARGIN * columnCount) / width;
|
const nonGutterSpace =
|
||||||
|
(calculatedWidth - COLLECTION_CARD_MARGIN * columnCount) / calculatedWidth;
|
||||||
const columnWidth = (1 / columnCount) * nonGutterSpace;
|
const columnWidth = (1 / columnCount) * nonGutterSpace;
|
||||||
|
|
||||||
const rowCount = Math.ceil(entryData.length / columnCount);
|
const rowCount = Math.ceil(entryData.length / columnCount);
|
||||||
@ -157,7 +167,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
|||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
width,
|
width: calculatedWidth,
|
||||||
height,
|
height,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -165,8 +175,8 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
|||||||
columnCount={columnCount}
|
columnCount={columnCount}
|
||||||
columnWidth={index =>
|
columnWidth={index =>
|
||||||
index + 1 === columnCount
|
index + 1 === columnCount
|
||||||
? width * columnWidth
|
? calculatedWidth * columnWidth
|
||||||
: width * columnWidth + COLLECTION_CARD_MARGIN
|
: calculatedWidth * columnWidth + COLLECTION_CARD_MARGIN
|
||||||
}
|
}
|
||||||
rowCount={rowCount}
|
rowCount={rowCount}
|
||||||
rowHeight={index => {
|
rowHeight={index => {
|
||||||
@ -189,7 +199,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
|||||||
|
|
||||||
return rowHeight;
|
return rowHeight;
|
||||||
}}
|
}}
|
||||||
width={width}
|
width={calculatedWidth}
|
||||||
height={height}
|
height={height}
|
||||||
itemData={
|
itemData={
|
||||||
{
|
{
|
||||||
@ -203,7 +213,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
|||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`
|
`
|
||||||
overflow-hidden
|
!overflow-x-hidden
|
||||||
overflow-y-auto
|
overflow-y-auto
|
||||||
styled-scrollbars
|
styled-scrollbars
|
||||||
`,
|
`,
|
||||||
|
@ -57,8 +57,8 @@ const EntryListingGrid: FC<EntryListingGridProps> = ({
|
|||||||
}, [handleScroll]);
|
}, [handleScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full overflow-hidden">
|
<div className="relative h-full flex-grow">
|
||||||
<div ref={gridContainerRef} className="relative h-full overflow-hidden">
|
<div ref={gridContainerRef} className="relative h-full">
|
||||||
<EntryListingCardGrid
|
<EntryListingCardGrid
|
||||||
key="grid"
|
key="grid"
|
||||||
entryData={entryData}
|
entryData={entryData}
|
||||||
@ -71,14 +71,14 @@ const EntryListingGrid: FC<EntryListingGridProps> = ({
|
|||||||
<div
|
<div
|
||||||
key="loading"
|
key="loading"
|
||||||
className="
|
className="
|
||||||
absolute
|
absolute
|
||||||
inset-0
|
inset-0
|
||||||
flex
|
flex
|
||||||
items-center
|
items-center
|
||||||
justify-center
|
justify-center
|
||||||
bg-slate-50/50
|
bg-slate-50/50
|
||||||
dark:bg-slate-900/50
|
dark:bg-slate-900/50
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{t('collection.entries.loadingEntries')}
|
{t('collection.entries.loadingEntries')}
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,6 +80,7 @@ const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
|
|||||||
hover:bg-gray-200
|
hover:bg-gray-200
|
||||||
dark:hover:bg-slate-700/70
|
dark:hover:bg-slate-700/70
|
||||||
"
|
"
|
||||||
|
to={path}
|
||||||
>
|
>
|
||||||
{collectionLabel ? (
|
{collectionLabel ? (
|
||||||
<TableCell key="collectionLabel" to={path}>
|
<TableCell key="collectionLabel" to={path}>
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import { FilterList as FilterListIcon } from '@styled-icons/material/FilterList';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import IconButton from '../../common/button/IconButton';
|
||||||
|
import MobileCollectionControlsDrawer from './MobileCollectionControlsDrawer';
|
||||||
|
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import type { FilterControlProps } from '../FilterControl';
|
||||||
|
import type { GroupControlProps } from '../GroupControl';
|
||||||
|
import type { SortControlProps } from '../SortControl';
|
||||||
|
|
||||||
|
export type MobileCollectionControlsProps = Omit<FilterControlProps, 'variant'> &
|
||||||
|
Omit<GroupControlProps, 'variant'> &
|
||||||
|
Omit<SortControlProps, 'variant'> & {
|
||||||
|
showGroupControl: boolean;
|
||||||
|
showFilterControl: boolean;
|
||||||
|
showSortControl: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileCollectionControls: FC<MobileCollectionControlsProps> = props => {
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
const toggleMobileMenu = useCallback(() => {
|
||||||
|
setMobileOpen(old => !old);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton className="flex lg:hidden" variant="text" onClick={toggleMobileMenu}>
|
||||||
|
<FilterListIcon className="w-5 h-5" />
|
||||||
|
</IconButton>
|
||||||
|
<MobileCollectionControlsDrawer
|
||||||
|
{...props}
|
||||||
|
mobileOpen={mobileOpen}
|
||||||
|
onMobileOpenToggle={toggleMobileMenu}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileCollectionControls;
|
@ -0,0 +1,117 @@
|
|||||||
|
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import FilterControl from '../FilterControl';
|
||||||
|
import GroupControl from '../GroupControl';
|
||||||
|
import SortControl from '../SortControl';
|
||||||
|
|
||||||
|
import type { FilterControlProps } from '../FilterControl';
|
||||||
|
import type { GroupControlProps } from '../GroupControl';
|
||||||
|
import type { SortControlProps } from '../SortControl';
|
||||||
|
|
||||||
|
const DRAWER_WIDTH = 240;
|
||||||
|
|
||||||
|
export type MobileCollectionControlsDrawerProps = Omit<FilterControlProps, 'variant'> &
|
||||||
|
Omit<GroupControlProps, 'variant'> &
|
||||||
|
Omit<SortControlProps, 'variant'> & {
|
||||||
|
mobileOpen: boolean;
|
||||||
|
onMobileOpenToggle: () => void;
|
||||||
|
} & {
|
||||||
|
showGroupControl: boolean;
|
||||||
|
showFilterControl: boolean;
|
||||||
|
showSortControl: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileCollectionControlsDrawer = ({
|
||||||
|
mobileOpen,
|
||||||
|
onMobileOpenToggle,
|
||||||
|
showFilterControl,
|
||||||
|
filter,
|
||||||
|
viewFilters,
|
||||||
|
onFilterClick,
|
||||||
|
showGroupControl,
|
||||||
|
group,
|
||||||
|
viewGroups,
|
||||||
|
onGroupClick,
|
||||||
|
showSortControl,
|
||||||
|
sort,
|
||||||
|
fields,
|
||||||
|
onSortClick,
|
||||||
|
}: MobileCollectionControlsDrawerProps) => {
|
||||||
|
const iOS = useMemo(
|
||||||
|
() => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const container = useMemo(
|
||||||
|
() => (typeof window !== 'undefined' ? window.document.body : undefined),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwipeableDrawer
|
||||||
|
disableBackdropTransition={!iOS}
|
||||||
|
disableDiscovery={iOS}
|
||||||
|
container={container}
|
||||||
|
variant="temporary"
|
||||||
|
open={mobileOpen}
|
||||||
|
onOpen={onMobileOpenToggle}
|
||||||
|
onClose={onMobileOpenToggle}
|
||||||
|
anchor="right"
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true, // Better open performance on mobile.
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: '80%',
|
||||||
|
maxWidth: DRAWER_WIDTH,
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: '80%',
|
||||||
|
maxWidth: DRAWER_WIDTH,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={onMobileOpenToggle}
|
||||||
|
className="
|
||||||
|
px-5
|
||||||
|
py-4
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
gap-6
|
||||||
|
h-full
|
||||||
|
w-full
|
||||||
|
overflow-y-auto
|
||||||
|
bg-white
|
||||||
|
dark:bg-slate-800
|
||||||
|
styled-scrollbars
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{showSortControl ? (
|
||||||
|
<SortControl fields={fields} sort={sort} onSortClick={onSortClick} variant="list" />
|
||||||
|
) : null}
|
||||||
|
{showFilterControl ? (
|
||||||
|
<FilterControl
|
||||||
|
viewFilters={viewFilters}
|
||||||
|
onFilterClick={onFilterClick}
|
||||||
|
filter={filter}
|
||||||
|
variant="list"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{showGroupControl ? (
|
||||||
|
<GroupControl
|
||||||
|
viewGroups={viewGroups}
|
||||||
|
onGroupClick={onGroupClick}
|
||||||
|
group={group}
|
||||||
|
variant="list"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</SwipeableDrawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileCollectionControlsDrawer;
|
@ -63,7 +63,8 @@ const Autocomplete = function <T>(
|
|||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
flex
|
flex
|
||||||
items-center
|
flex-col
|
||||||
|
flex-start
|
||||||
text-sm
|
text-sm
|
||||||
font-medium
|
font-medium
|
||||||
relative
|
relative
|
||||||
@ -90,8 +91,8 @@ const Autocomplete = function <T>(
|
|||||||
leading-5
|
leading-5
|
||||||
focus:ring-0
|
focus:ring-0
|
||||||
outline-none
|
outline-none
|
||||||
basis-60
|
|
||||||
flex-grow
|
flex-grow
|
||||||
|
truncate
|
||||||
`,
|
`,
|
||||||
disabled
|
disabled
|
||||||
? `
|
? `
|
||||||
@ -194,7 +195,9 @@ const Autocomplete = function <T>(
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'block truncate',
|
`
|
||||||
|
block
|
||||||
|
`,
|
||||||
selected ? 'font-medium' : 'font-normal',
|
selected ? 'font-medium' : 'font-normal',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -4,13 +4,13 @@ import Button from './Button';
|
|||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { ButtonProps } from './Button';
|
import type { ButtonLinkProps } from './Button';
|
||||||
|
|
||||||
export type IconButtonProps = Omit<ButtonProps, 'children'> & {
|
export type IconButtonProps = Omit<ButtonLinkProps, 'children'> & {
|
||||||
children: FC<{ className?: string }>;
|
children: FC<{ className?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IconButton = ({ children, size = 'medium', className, ...otherProps }: ButtonProps) => {
|
const IconButton = ({ children, size = 'medium', className, ...otherProps }: ButtonLinkProps) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={classNames(size === 'small' && 'px-0.5', size === 'medium' && 'px-1.5', className)}
|
className={classNames(size === 'small' && 'px-0.5', size === 'medium' && 'px-1.5', className)}
|
||||||
|
@ -22,6 +22,10 @@ const CardActionArea = ({ to, children }: CardActionAreaProps) => {
|
|||||||
justify-start
|
justify-start
|
||||||
hover:bg-gray-200
|
hover:bg-gray-200
|
||||||
dark:hover:bg-slate-700/70
|
dark:hover:bg-slate-700/70
|
||||||
|
focus:outline-none
|
||||||
|
focus:ring-4
|
||||||
|
focus:ring-gray-200
|
||||||
|
dark:focus:ring-slate-700
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -52,7 +52,7 @@ const Checkbox: FC<CheckboxProps> = ({ checked, disabled = false, onChange }) =>
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
className="sr-only peer"
|
className="sr-only peer hide-tap"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onClick={handleNoop}
|
onClick={handleNoop}
|
||||||
|
@ -23,6 +23,8 @@ export interface FieldProps {
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
disableClick?: boolean;
|
disableClick?: boolean;
|
||||||
endAdornment?: ReactNode;
|
endAdornment?: ReactNode;
|
||||||
|
rootClassName?: string;
|
||||||
|
wrapperClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Field: FC<FieldProps> = ({
|
const Field: FC<FieldProps> = ({
|
||||||
@ -39,6 +41,8 @@ const Field: FC<FieldProps> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
disableClick = false,
|
disableClick = false,
|
||||||
endAdornment,
|
endAdornment,
|
||||||
|
rootClassName,
|
||||||
|
wrapperClassName,
|
||||||
}) => {
|
}) => {
|
||||||
const finalCursor = useCursor(cursor, disabled);
|
const finalCursor = useCursor(cursor, disabled);
|
||||||
|
|
||||||
@ -104,6 +108,7 @@ const Field: FC<FieldProps> = ({
|
|||||||
focus-within:border-blue-800
|
focus-within:border-blue-800
|
||||||
dark:focus-within:border-blue-100
|
dark:focus-within:border-blue-100
|
||||||
`,
|
`,
|
||||||
|
rootClassName,
|
||||||
!noHightlight &&
|
!noHightlight &&
|
||||||
!disabled &&
|
!disabled &&
|
||||||
`
|
`
|
||||||
@ -118,7 +123,7 @@ const Field: FC<FieldProps> = ({
|
|||||||
finalCursor === 'default' && 'cursor-default',
|
finalCursor === 'default' && 'cursor-default',
|
||||||
!hasErrors && 'group/active',
|
!hasErrors && 'group/active',
|
||||||
),
|
),
|
||||||
[finalCursor, disabled, hasErrors, noHightlight, noPadding],
|
[rootClassName, noHightlight, disabled, noPadding, finalCursor, hasErrors],
|
||||||
);
|
);
|
||||||
|
|
||||||
const wrapperClassNames = useMemo(
|
const wrapperClassNames = useMemo(
|
||||||
@ -129,9 +134,10 @@ const Field: FC<FieldProps> = ({
|
|||||||
flex-col
|
flex-col
|
||||||
w-full
|
w-full
|
||||||
`,
|
`,
|
||||||
|
wrapperClassName,
|
||||||
forSingleList && 'mr-14',
|
forSingleList && 'mr-14',
|
||||||
),
|
),
|
||||||
[forSingleList],
|
[forSingleList, wrapperClassName],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (variant === 'inline') {
|
if (variant === 'inline') {
|
||||||
|
@ -16,9 +16,14 @@ export interface MenuProps {
|
|||||||
color?: BaseBaseProps['color'];
|
color?: BaseBaseProps['color'];
|
||||||
size?: BaseBaseProps['size'];
|
size?: BaseBaseProps['size'];
|
||||||
rounded?: boolean | 'no-padding';
|
rounded?: boolean | 'no-padding';
|
||||||
className?: string;
|
rootClassName?: string;
|
||||||
|
iconClassName?: string;
|
||||||
|
buttonClassName?: string;
|
||||||
|
labelClassName?: string;
|
||||||
children: ReactNode | ReactNode[];
|
children: ReactNode | ReactNode[];
|
||||||
hideDropdownIcon?: boolean;
|
hideDropdownIcon?: boolean;
|
||||||
|
hideDropdownIconOnMobile?: boolean;
|
||||||
|
hideLabel?: boolean;
|
||||||
keepMounted?: boolean;
|
keepMounted?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
@ -31,9 +36,14 @@ const Menu = ({
|
|||||||
color = 'primary',
|
color = 'primary',
|
||||||
size = 'medium',
|
size = 'medium',
|
||||||
rounded = false,
|
rounded = false,
|
||||||
className,
|
rootClassName,
|
||||||
|
iconClassName,
|
||||||
|
buttonClassName,
|
||||||
|
labelClassName,
|
||||||
children,
|
children,
|
||||||
hideDropdownIcon = false,
|
hideDropdownIcon = false,
|
||||||
|
hideDropdownIconOnMobile = false,
|
||||||
|
hideLabel = false,
|
||||||
keepMounted = false,
|
keepMounted = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
'data-testid': dataTestId,
|
'data-testid': dataTestId,
|
||||||
@ -56,16 +66,16 @@ const Menu = ({
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const buttonClassName = useButtonClassNames(variant, color, size, rounded);
|
const calculatedButtonClassName = useButtonClassNames(variant, color, size, rounded);
|
||||||
|
|
||||||
const menuButtonClassNames = useMemo(
|
const menuButtonClassNames = useMemo(
|
||||||
() => classNames(className, buttonClassName),
|
() => classNames(calculatedButtonClassName, buttonClassName, 'whitespace-nowrap'),
|
||||||
[buttonClassName, className],
|
[calculatedButtonClassName, buttonClassName],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClickAwayListener mouseEvent="onMouseDown" touchEvent="onTouchStart" onClickAway={handleClose}>
|
<ClickAwayListener mouseEvent="onMouseDown" touchEvent="onTouchStart" onClickAway={handleClose}>
|
||||||
<div className="flex">
|
<div className={classNames('flex', rootClassName)}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
@ -76,10 +86,26 @@ const Menu = ({
|
|||||||
className={menuButtonClassNames}
|
className={menuButtonClassNames}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{StartIcon ? <StartIcon className="-ml-0.5 mr-1.5 h-5 w-5" /> : null}
|
{StartIcon ? (
|
||||||
{label}
|
<StartIcon
|
||||||
|
className={classNames(
|
||||||
|
`-ml-0.5 h-5 w-5`,
|
||||||
|
!hideLabel && !hideDropdownIcon && 'mr-1.5',
|
||||||
|
hideDropdownIconOnMobile && '!mr-0 md:!mr-1.5',
|
||||||
|
iconClassName,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{!hideLabel ? <div className={labelClassName}>{label}</div> : null}
|
||||||
{!hideDropdownIcon ? (
|
{!hideDropdownIcon ? (
|
||||||
<KeyboardArrowDownIcon className="-mr-0.5 ml-2 h-5 w-5" aria-hidden="true" />
|
<KeyboardArrowDownIcon
|
||||||
|
className={classNames(
|
||||||
|
`-mr-0.5 h-5 w-5`,
|
||||||
|
!hideLabel && 'ml-2',
|
||||||
|
hideDropdownIconOnMobile && '!hidden md:!block',
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
<MenuUnstyled
|
<MenuUnstyled
|
||||||
|
@ -61,6 +61,7 @@ const Pill: FC<PillProps> = ({
|
|||||||
px-3
|
px-3
|
||||||
py-1
|
py-1
|
||||||
rounded-lg
|
rounded-lg
|
||||||
|
truncate
|
||||||
`,
|
`,
|
||||||
noWrap && 'whitespace-nowrap',
|
noWrap && 'whitespace-nowrap',
|
||||||
colorClassNames,
|
colorClassNames,
|
||||||
|
@ -59,7 +59,6 @@ const Select = forwardRef(
|
|||||||
},
|
},
|
||||||
[onOpenChange],
|
[onOpenChange],
|
||||||
);
|
);
|
||||||
const handleButtonClick = useCallback(() => handleOpenChange(!open), [handleOpenChange, open]);
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(_event: MouseEvent | KeyboardEvent | FocusEvent | null, selectedValue: number | string) => {
|
(_event: MouseEvent | KeyboardEvent | FocusEvent | null, selectedValue: number | string) => {
|
||||||
@ -89,8 +88,10 @@ const Select = forwardRef(
|
|||||||
<SelectUnstyled<any>
|
<SelectUnstyled<any>
|
||||||
renderValue={() => {
|
renderValue={() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full">
|
||||||
{label ?? placeholder}
|
<div className="flex flex-start w-select-widget-label">
|
||||||
|
<span className="truncate">{label ?? placeholder}</span>
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
className="
|
className="
|
||||||
pointer-events-none
|
pointer-events-none
|
||||||
@ -118,13 +119,12 @@ const Select = forwardRef(
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
root: {
|
root: {
|
||||||
ref,
|
ref,
|
||||||
onClick: handleButtonClick,
|
|
||||||
className: classNames(
|
className: classNames(
|
||||||
`
|
`
|
||||||
flex
|
flex
|
||||||
@ -160,11 +160,11 @@ const Select = forwardRef(
|
|||||||
ring-opacity-5
|
ring-opacity-5
|
||||||
focus:outline-none
|
focus:outline-none
|
||||||
sm:text-sm
|
sm:text-sm
|
||||||
z-50
|
z-[100]
|
||||||
dark:bg-slate-700
|
dark:bg-slate-700
|
||||||
dark:shadow-lg
|
dark:shadow-lg
|
||||||
`,
|
`,
|
||||||
style: { width },
|
style: { width: ref ? width : 'auto' },
|
||||||
disablePortal: false,
|
disablePortal: false,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
@ -26,6 +26,7 @@ const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCell
|
|||||||
py-3
|
py-3
|
||||||
whitespace-nowrap
|
whitespace-nowrap
|
||||||
"
|
"
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
@ -50,6 +51,8 @@ const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCell
|
|||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
h-[44px]
|
h-[44px]
|
||||||
|
truncate
|
||||||
|
w-full
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
@ -35,6 +35,8 @@ const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
|
|||||||
dark:border-gray-700
|
dark:border-gray-700
|
||||||
dark:bg-slate-800
|
dark:bg-slate-800
|
||||||
text-[14px]
|
text-[14px]
|
||||||
|
truncate
|
||||||
|
w-full
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{typeof children === 'string' && isEmpty(children) ? <> </> : children}
|
{typeof children === 'string' && isEmpty(children) ? <> </> : children}
|
||||||
|
@ -1,15 +1,31 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { KeyboardEvent, ReactNode } from 'react';
|
||||||
|
|
||||||
interface TableRowProps {
|
interface TableRowProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
to?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableRow = ({ children, className }: TableRowProps) => {
|
const TableRow = ({ children, className, to }: TableRowProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent) => {
|
||||||
|
if (!to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter' || event.key === 'Space') {
|
||||||
|
navigate(to);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[navigate, to],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -22,9 +38,14 @@ const TableRow = ({ children, className }: TableRowProps) => {
|
|||||||
hover:bg-slate-50
|
hover:bg-slate-50
|
||||||
dark:bg-slate-800
|
dark:bg-slate-800
|
||||||
dark:hover:bg-slate-700
|
dark:hover:bg-slate-700
|
||||||
|
focus:outline-none
|
||||||
|
focus:bg-gray-100
|
||||||
|
focus:dark:bg-slate-700
|
||||||
`,
|
`,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
tabIndex={to ? 0 : -1}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -19,6 +19,8 @@ export interface BaseTextFieldProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
endAdornment?: ReactNode;
|
endAdornment?: ReactNode;
|
||||||
startAdornment?: ReactNode;
|
startAdornment?: ReactNode;
|
||||||
|
rootClassName?: string;
|
||||||
|
inputClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberTextFieldProps extends BaseTextFieldProps {
|
export interface NumberTextFieldProps extends BaseTextFieldProps {
|
||||||
@ -49,6 +51,8 @@ const TextField: FC<TextFieldProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
startAdornment,
|
startAdornment,
|
||||||
endAdornment,
|
endAdornment,
|
||||||
|
rootClassName,
|
||||||
|
inputClassName,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const finalCursor = useCursor(cursor, disabled);
|
const finalCursor = useCursor(cursor, disabled);
|
||||||
@ -67,10 +71,13 @@ const TextField: FC<TextFieldProps> = ({
|
|||||||
endAdornment={endAdornment}
|
endAdornment={endAdornment}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
root: {
|
root: {
|
||||||
className: `
|
className: classNames(
|
||||||
flex
|
`
|
||||||
w-full
|
flex
|
||||||
`,
|
w-full
|
||||||
|
`,
|
||||||
|
rootClassName,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
ref: inputRef,
|
ref: inputRef,
|
||||||
@ -79,6 +86,7 @@ const TextField: FC<TextFieldProps> = ({
|
|||||||
w-full
|
w-full
|
||||||
text-sm
|
text-sm
|
||||||
`,
|
`,
|
||||||
|
inputClassName,
|
||||||
variant === 'borderless' &&
|
variant === 'borderless' &&
|
||||||
`
|
`
|
||||||
h-6
|
h-6
|
||||||
|
@ -15,7 +15,7 @@ interface ViewStyleControlPros {
|
|||||||
|
|
||||||
const ViewStyleControl = ({ viewStyle, onChangeViewStyle }: ViewStyleControlPros) => {
|
const ViewStyleControl = ({ viewStyle, onChangeViewStyle }: ViewStyleControlPros) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5 mr-1">
|
<div className="flex items-center gap-1.5 lg:mr-1">
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="text"
|
variant="text"
|
||||||
className={classNames(viewStyle === VIEW_STYLE_TABLE && 'text-blue-500 dark:text-blue-500')}
|
className={classNames(viewStyle === VIEW_STYLE_TABLE && 'text-blue-500 dark:text-blue-500')}
|
||||||
|
@ -22,6 +22,7 @@ export default function useWidgetsFor(
|
|||||||
collection: Collection,
|
collection: Collection,
|
||||||
fields: Field[],
|
fields: Field[],
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
|
data: EntryData = entry.data,
|
||||||
): {
|
): {
|
||||||
widgetFor: WidgetFor;
|
widgetFor: WidgetFor;
|
||||||
widgetsFor: WidgetsFor;
|
widgetsFor: WidgetsFor;
|
||||||
@ -35,9 +36,19 @@ export default function useWidgetsFor(
|
|||||||
if (!config) {
|
if (!config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return getWidgetFor(config, collection, name, fields, entry, theme, inferredFields);
|
return getWidgetFor(
|
||||||
|
config,
|
||||||
|
collection,
|
||||||
|
name,
|
||||||
|
fields,
|
||||||
|
entry,
|
||||||
|
theme,
|
||||||
|
inferredFields,
|
||||||
|
fields,
|
||||||
|
data,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[collection, config, entry, fields, inferredFields, theme],
|
[collection, config, data, entry, fields, inferredFields, theme],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,7 +3,7 @@ import { ScrollSyncPane } from 'react-scroll-sync';
|
|||||||
|
|
||||||
import { EDITOR_SIZE_COMPACT } from '@staticcms/core/constants/views';
|
import { EDITOR_SIZE_COMPACT } from '@staticcms/core/constants/views';
|
||||||
import useBreadcrumbs from '@staticcms/core/lib/hooks/useBreadcrumbs';
|
import useBreadcrumbs from '@staticcms/core/lib/hooks/useBreadcrumbs';
|
||||||
import { getI18nInfo, getPreviewEntry, hasI18n } from '@staticcms/core/lib/i18n';
|
import { getI18nInfo, hasI18n } from '@staticcms/core/lib/i18n';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import {
|
import {
|
||||||
getFileFromSlug,
|
getFileFromSlug,
|
||||||
@ -169,6 +169,11 @@ const EditorInterface = ({
|
|||||||
|
|
||||||
const collectHasI18n = hasI18n(collection);
|
const collectHasI18n = hasI18n(collection);
|
||||||
|
|
||||||
|
const [showMobilePreview, setShowMobilePreview] = useState(false);
|
||||||
|
const toggleMobilePreview = useCallback(() => {
|
||||||
|
setShowMobilePreview(old => !old);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const editor = (
|
const editor = (
|
||||||
<div
|
<div
|
||||||
key={defaultLocale}
|
key={defaultLocale}
|
||||||
@ -181,7 +186,13 @@ const EditorInterface = ({
|
|||||||
`
|
`
|
||||||
overflow-y-auto
|
overflow-y-auto
|
||||||
styled-scrollbars
|
styled-scrollbars
|
||||||
h-main
|
h-main-mobile
|
||||||
|
md:h-main
|
||||||
|
`,
|
||||||
|
showMobilePreview &&
|
||||||
|
`
|
||||||
|
hidden
|
||||||
|
lg:block
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -204,10 +215,12 @@ const EditorInterface = ({
|
|||||||
<div
|
<div
|
||||||
key={selectedLocale}
|
key={selectedLocale}
|
||||||
className="
|
className="
|
||||||
|
flex
|
||||||
w-full
|
w-full
|
||||||
overflow-y-auto
|
overflow-y-auto
|
||||||
styled-scrollbars
|
styled-scrollbars
|
||||||
h-main
|
h-main-mobile
|
||||||
|
md:h-main
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<EditorControlPane
|
<EditorControlPane
|
||||||
@ -227,9 +240,36 @@ const EditorInterface = ({
|
|||||||
[collection, entry, fields, fieldsErrors, handleLocaleChange, selectedLocale, submitted, t],
|
[collection, entry, fields, fieldsErrors, handleLocaleChange, selectedLocale, submitted, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const previewEntry = collectHasI18n
|
const mobileLocaleEditor = useMemo(
|
||||||
? getPreviewEntry(entry, selectedLocale[0], defaultLocale)
|
() => (
|
||||||
: entry;
|
<div
|
||||||
|
key={selectedLocale}
|
||||||
|
className="
|
||||||
|
w-full
|
||||||
|
overflow-y-auto
|
||||||
|
styled-scrollbars
|
||||||
|
h-main-mobile
|
||||||
|
flex
|
||||||
|
md:hidden
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<EditorControlPane
|
||||||
|
collection={collection}
|
||||||
|
entry={entry}
|
||||||
|
fields={fields}
|
||||||
|
fieldsErrors={fieldsErrors}
|
||||||
|
locale={selectedLocale}
|
||||||
|
onLocaleChange={handleLocaleChange}
|
||||||
|
allowDefaultLocale
|
||||||
|
submitted={submitted}
|
||||||
|
canChangeLocale
|
||||||
|
hideBorder
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[collection, entry, fields, fieldsErrors, handleLocaleChange, selectedLocale, submitted, t],
|
||||||
|
);
|
||||||
|
|
||||||
const editorWithPreview = (
|
const editorWithPreview = (
|
||||||
<div
|
<div
|
||||||
@ -238,27 +278,31 @@ const EditorInterface = ({
|
|||||||
grid
|
grid
|
||||||
h-full
|
h-full
|
||||||
`,
|
`,
|
||||||
editorSize === EDITOR_SIZE_COMPACT ? 'grid-cols-editor' : 'grid-cols-2',
|
editorSize === EDITOR_SIZE_COMPACT ? 'lg:grid-cols-editor' : 'lg:grid-cols-2',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
||||||
<EditorPreviewPane
|
<EditorPreviewPane
|
||||||
collection={collection}
|
collection={collection}
|
||||||
previewInFrame={previewInFrame}
|
previewInFrame={previewInFrame}
|
||||||
entry={previewEntry}
|
entry={entry}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
editorSize={editorSize}
|
editorSize={editorSize}
|
||||||
|
showMobilePreview={showMobilePreview}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const editorSideBySideLocale = (
|
const editorSideBySideLocale = (
|
||||||
<div className="grid grid-cols-2 h-full">
|
<>
|
||||||
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
<div className="grid-cols-2 h-full hidden lg:grid">
|
||||||
<ScrollSyncPane>
|
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
||||||
<>{editorLocale}</>
|
<ScrollSyncPane>
|
||||||
</ScrollSyncPane>
|
<>{editorLocale}</>
|
||||||
</div>
|
</ScrollSyncPane>
|
||||||
|
</div>
|
||||||
|
{mobileLocaleEditor}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
|
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
|
||||||
@ -296,6 +340,9 @@ const EditorInterface = ({
|
|||||||
toggleScrollSync={handleToggleScrollSync}
|
toggleScrollSync={handleToggleScrollSync}
|
||||||
toggleI18n={handleToggleI18n}
|
toggleI18n={handleToggleI18n}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
|
showMobilePreview={showMobilePreview}
|
||||||
|
onMobilePreviewToggle={toggleMobilePreview}
|
||||||
|
className="flex"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -11,6 +11,7 @@ import React, { useCallback, useMemo } from 'react';
|
|||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
import { deleteLocalBackup, loadEntry } from '@staticcms/core/actions/entries';
|
import { deleteLocalBackup, loadEntry } from '@staticcms/core/actions/entries';
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { selectAllowDeletion } from '@staticcms/core/lib/util/collection.util';
|
import { selectAllowDeletion } from '@staticcms/core/lib/util/collection.util';
|
||||||
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
||||||
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
@ -18,6 +19,7 @@ import confirm from '../common/confirm/Confirm';
|
|||||||
import Menu from '../common/menu/Menu';
|
import Menu from '../common/menu/Menu';
|
||||||
import MenuGroup from '../common/menu/MenuGroup';
|
import MenuGroup from '../common/menu/MenuGroup';
|
||||||
import MenuItemButton from '../common/menu/MenuItemButton';
|
import MenuItemButton from '../common/menu/MenuItemButton';
|
||||||
|
import IconButton from '../common/button/IconButton';
|
||||||
|
|
||||||
import type { Collection, EditorPersistOptions, TranslatedProps } from '@staticcms/core/interface';
|
import type { Collection, EditorPersistOptions, TranslatedProps } from '@staticcms/core/interface';
|
||||||
import type { FC, MouseEventHandler } from 'react';
|
import type { FC, MouseEventHandler } from 'react';
|
||||||
@ -44,6 +46,9 @@ export interface EditorToolbarProps {
|
|||||||
toggleScrollSync: MouseEventHandler;
|
toggleScrollSync: MouseEventHandler;
|
||||||
toggleI18n: MouseEventHandler;
|
toggleI18n: MouseEventHandler;
|
||||||
slug?: string | undefined;
|
slug?: string | undefined;
|
||||||
|
className?: string;
|
||||||
|
showMobilePreview: boolean;
|
||||||
|
onMobilePreviewToggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditorToolbar = ({
|
const EditorToolbar = ({
|
||||||
@ -67,6 +72,9 @@ const EditorToolbar = ({
|
|||||||
toggleScrollSync,
|
toggleScrollSync,
|
||||||
toggleI18n,
|
toggleI18n,
|
||||||
slug,
|
slug,
|
||||||
|
className,
|
||||||
|
showMobilePreview,
|
||||||
|
onMobilePreviewToggle,
|
||||||
}: TranslatedProps<EditorToolbarProps>) => {
|
}: TranslatedProps<EditorToolbarProps>) => {
|
||||||
const canCreate = useMemo(
|
const canCreate = useMemo(
|
||||||
() => ('folder' in collection && collection.create) ?? false,
|
() => ('folder' in collection && collection.create) ?? false,
|
||||||
@ -164,13 +172,22 @@ const EditorToolbar = ({
|
|||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => (
|
() => (
|
||||||
<div className="flex gap-2">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
`
|
||||||
|
flex
|
||||||
|
gap-2
|
||||||
|
`,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{showI18nToggle || showPreviewToggle || canDelete ? (
|
{showI18nToggle || showPreviewToggle || canDelete ? (
|
||||||
<Menu
|
<Menu
|
||||||
key="extra-menu"
|
key="extra-menu"
|
||||||
label={<MoreVertIcon className="w-5 h-5" />}
|
label={<MoreVertIcon className="w-5 h-5" />}
|
||||||
variant="text"
|
variant="text"
|
||||||
className="px-1.5"
|
rootClassName="hidden lg:flex"
|
||||||
|
buttonClassName="px-1.5"
|
||||||
hideDropdownIcon
|
hideDropdownIcon
|
||||||
>
|
>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
@ -215,12 +232,39 @@ const EditorToolbar = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</Menu>
|
</Menu>
|
||||||
) : null}
|
) : null}
|
||||||
|
{showPreviewToggle ? (
|
||||||
|
<IconButton
|
||||||
|
key="show-preview-button"
|
||||||
|
title={t('editor.editorInterface.preview')}
|
||||||
|
variant={showMobilePreview ? 'contained' : 'text'}
|
||||||
|
onClick={onMobilePreviewToggle}
|
||||||
|
className="flex lg:hidden"
|
||||||
|
>
|
||||||
|
<EyeIcon className="w-5 h-5" />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
{canDelete ? (
|
||||||
|
<IconButton
|
||||||
|
key="delete-button"
|
||||||
|
title={t('editor.editorToolbar.deleteEntry')}
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
onClick={onDelete}
|
||||||
|
className="flex lg:hidden"
|
||||||
|
>
|
||||||
|
<TrashIcon className="w-5 h-5" />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
<Menu
|
<Menu
|
||||||
label={
|
label={
|
||||||
isPublished ? t('editor.editorToolbar.published') : t('editor.editorToolbar.publish')
|
isPublished ? t('editor.editorToolbar.published') : t('editor.editorToolbar.publish')
|
||||||
}
|
}
|
||||||
color={isPublished ? 'success' : 'primary'}
|
color={isPublished ? 'success' : 'primary'}
|
||||||
disabled={isLoading || (menuItems.length == 1 && menuItems[0].length === 0)}
|
disabled={isLoading || (menuItems.length == 1 && menuItems[0].length === 0)}
|
||||||
|
startIcon={PublishIcon}
|
||||||
|
iconClassName="flex !md:hidden"
|
||||||
|
labelClassName="hidden md:block"
|
||||||
|
hideDropdownIconOnMobile
|
||||||
>
|
>
|
||||||
{menuItems.map((group, index) => (
|
{menuItems.map((group, index) => (
|
||||||
<MenuGroup key={`menu-group-${index}`}>{group}</MenuGroup>
|
<MenuGroup key={`menu-group-${index}`}>{group}</MenuGroup>
|
||||||
@ -229,6 +273,7 @@ const EditorToolbar = ({
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
|
className,
|
||||||
showI18nToggle,
|
showI18nToggle,
|
||||||
showPreviewToggle,
|
showPreviewToggle,
|
||||||
canDelete,
|
canDelete,
|
||||||
@ -241,6 +286,8 @@ const EditorToolbar = ({
|
|||||||
toggleScrollSync,
|
toggleScrollSync,
|
||||||
scrollSyncActive,
|
scrollSyncActive,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
showMobilePreview,
|
||||||
|
onMobilePreviewToggle,
|
||||||
isPublished,
|
isPublished,
|
||||||
menuItems,
|
menuItems,
|
||||||
],
|
],
|
||||||
|
@ -30,6 +30,7 @@ export interface EditorControlPaneProps {
|
|||||||
hideBorder: boolean;
|
hideBorder: boolean;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
onLocaleChange?: (locale: string) => void;
|
onLocaleChange?: (locale: string) => void;
|
||||||
|
allowDefaultLocale?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditorControlPane = ({
|
const EditorControlPane = ({
|
||||||
@ -43,6 +44,7 @@ const EditorControlPane = ({
|
|||||||
hideBorder,
|
hideBorder,
|
||||||
slug,
|
slug,
|
||||||
onLocaleChange,
|
onLocaleChange,
|
||||||
|
allowDefaultLocale = false,
|
||||||
t,
|
t,
|
||||||
}: TranslatedProps<EditorControlPaneProps>) => {
|
}: TranslatedProps<EditorControlPaneProps>) => {
|
||||||
const pathField = useMemo(
|
const pathField = useMemo(
|
||||||
@ -95,12 +97,13 @@ const EditorControlPane = ({
|
|||||||
flex
|
flex
|
||||||
flex-col
|
flex-col
|
||||||
min-h-full
|
min-h-full
|
||||||
|
w-full
|
||||||
`,
|
`,
|
||||||
!hideBorder &&
|
!hideBorder &&
|
||||||
`
|
`
|
||||||
border-r
|
lg:border-r
|
||||||
border-slate-400
|
border-slate-400
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{i18n?.locales && locale ? (
|
{i18n?.locales && locale ? (
|
||||||
@ -117,6 +120,7 @@ const EditorControlPane = ({
|
|||||||
})}
|
})}
|
||||||
canChangeLocale={canChangeLocale}
|
canChangeLocale={canChangeLocale}
|
||||||
onLocaleChange={onLocaleChange}
|
onLocaleChange={onLocaleChange}
|
||||||
|
allowDefaultLocale={allowDefaultLocale}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -9,6 +9,7 @@ interface LocaleDropdownProps {
|
|||||||
defaultLocale: string;
|
defaultLocale: string;
|
||||||
dropdownText: string;
|
dropdownText: string;
|
||||||
canChangeLocale: boolean;
|
canChangeLocale: boolean;
|
||||||
|
allowDefaultLocale: boolean;
|
||||||
onLocaleChange?: (locale: string) => void;
|
onLocaleChange?: (locale: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ const LocaleDropdown = ({
|
|||||||
defaultLocale,
|
defaultLocale,
|
||||||
dropdownText,
|
dropdownText,
|
||||||
canChangeLocale,
|
canChangeLocale,
|
||||||
|
allowDefaultLocale,
|
||||||
onLocaleChange,
|
onLocaleChange,
|
||||||
}: LocaleDropdownProps) => {
|
}: LocaleDropdownProps) => {
|
||||||
if (!canChangeLocale) {
|
if (!canChangeLocale) {
|
||||||
@ -39,7 +41,7 @@ const LocaleDropdown = ({
|
|||||||
<Menu label={dropdownText}>
|
<Menu label={dropdownText}>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
{locales
|
{locales
|
||||||
.filter(locale => locale !== defaultLocale)
|
.filter(locale => allowDefaultLocale || locale !== defaultLocale)
|
||||||
.map(locale => (
|
.map(locale => (
|
||||||
<MenuItemButton key={locale} onClick={() => onLocaleChange?.(locale)}>
|
<MenuItemButton key={locale} onClick={() => onLocaleChange?.(locale)}>
|
||||||
{locale}
|
{locale}
|
||||||
|
@ -90,6 +90,10 @@ const FrameGlobalStyles = `
|
|||||||
height: 10px; /* Mostly for horizontal scrollbars */
|
height: 10px; /* Mostly for horizontal scrollbars */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.styled-scrollbars::-webkit-scrollbar-corner {
|
||||||
|
background: rgba(0,0,0,0);
|
||||||
|
}
|
||||||
|
|
||||||
.styled-scrollbars::-webkit-scrollbar-thumb {
|
.styled-scrollbars::-webkit-scrollbar-thumb {
|
||||||
/* Foreground */
|
/* Foreground */
|
||||||
background: var(--scrollbar-foreground);
|
background: var(--scrollbar-foreground);
|
||||||
@ -107,10 +111,11 @@ export interface EditorPreviewPaneProps {
|
|||||||
entry: Entry;
|
entry: Entry;
|
||||||
previewInFrame: boolean;
|
previewInFrame: boolean;
|
||||||
editorSize: EditorSize;
|
editorSize: EditorSize;
|
||||||
|
showMobilePreview: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
const EditorPreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
||||||
const { editorSize, entry, collection, fields, previewInFrame, t } = props;
|
const { editorSize, entry, collection, fields, previewInFrame, showMobilePreview, t } = props;
|
||||||
|
|
||||||
const config = useAppSelector(selectConfig);
|
const config = useAppSelector(selectConfig);
|
||||||
|
|
||||||
@ -175,62 +180,67 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`
|
`
|
||||||
h-main
|
h-main-mobile
|
||||||
|
md:h-main
|
||||||
absolute
|
absolute
|
||||||
top-16
|
top-16
|
||||||
right-0
|
right-0
|
||||||
|
w-full
|
||||||
`,
|
`,
|
||||||
editorSize === EDITOR_SIZE_COMPACT ? 'w-preview' : 'w-6/12',
|
editorSize === EDITOR_SIZE_COMPACT ? 'lg:w-preview' : 'lg:w-6/12',
|
||||||
|
!showMobilePreview &&
|
||||||
|
`
|
||||||
|
hidden
|
||||||
|
lg:block
|
||||||
|
`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!entry || !entry.data ? null : (
|
<ErrorBoundary config={config}>
|
||||||
<ErrorBoundary config={config}>
|
{previewInFrame ? (
|
||||||
{previewInFrame ? (
|
<Frame
|
||||||
<Frame
|
key="preview-frame"
|
||||||
key="preview-frame"
|
id="preview-pane"
|
||||||
|
head={previewStyles}
|
||||||
|
initialContent={initialFrameContent}
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
{!collection ? (
|
||||||
|
t('collection.notFound')
|
||||||
|
) : (
|
||||||
|
<PreviewFrameContent
|
||||||
|
key="preview-frame-content"
|
||||||
|
previewComponent={previewComponent}
|
||||||
|
previewProps={{ ...previewProps }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Frame>
|
||||||
|
) : (
|
||||||
|
<ScrollSyncPane key="preview-wrapper-scroll-sync">
|
||||||
|
<div
|
||||||
|
key="preview-wrapper"
|
||||||
id="preview-pane"
|
id="preview-pane"
|
||||||
head={previewStyles}
|
className="
|
||||||
initialContent={initialFrameContent}
|
|
||||||
className="w-full h-full"
|
|
||||||
>
|
|
||||||
{!collection ? (
|
|
||||||
t('collection.notFound')
|
|
||||||
) : (
|
|
||||||
<PreviewFrameContent
|
|
||||||
key="preview-frame-content"
|
|
||||||
previewComponent={previewComponent}
|
|
||||||
previewProps={{ ...previewProps }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Frame>
|
|
||||||
) : (
|
|
||||||
<ScrollSyncPane key="preview-wrapper-scroll-sync">
|
|
||||||
<div
|
|
||||||
key="preview-wrapper"
|
|
||||||
id="preview-pane"
|
|
||||||
className="
|
|
||||||
overflow-y-auto
|
overflow-y-auto
|
||||||
styled-scrollbars
|
styled-scrollbars
|
||||||
h-full
|
h-full
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{!collection ? (
|
{!collection ? (
|
||||||
t('collection.notFound')
|
t('collection.notFound')
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{previewStyles}
|
{previewStyles}
|
||||||
<EditorPreviewContent
|
<EditorPreviewContent
|
||||||
key="preview-wrapper-content"
|
key="preview-wrapper-content"
|
||||||
previewComponent={previewComponent}
|
previewComponent={previewComponent}
|
||||||
previewProps={{ ...previewProps, document, window }}
|
previewProps={{ ...previewProps, document, window }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollSyncPane>
|
</ScrollSyncPane>
|
||||||
)}
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)}
|
|
||||||
</div>,
|
</div>,
|
||||||
element,
|
element,
|
||||||
'preview-content',
|
'preview-content',
|
||||||
@ -240,14 +250,14 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
|||||||
config,
|
config,
|
||||||
editorSize,
|
editorSize,
|
||||||
element,
|
element,
|
||||||
entry,
|
|
||||||
initialFrameContent,
|
initialFrameContent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
previewInFrame,
|
previewInFrame,
|
||||||
previewProps,
|
previewProps,
|
||||||
previewStyles,
|
previewStyles,
|
||||||
|
showMobilePreview,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(PreviewPane) as FC<EditorPreviewPaneProps>;
|
export default translate()(EditorPreviewPane) as FC<EditorPreviewPaneProps>;
|
||||||
|
80
packages/core/src/components/navbar/BottomNavigation.tsx
Normal file
80
packages/core/src/components/navbar/BottomNavigation.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { Add as AddIcon } from '@styled-icons/material/Add';
|
||||||
|
import { Menu as MenuIcon } from '@styled-icons/material/Menu';
|
||||||
|
import { OpenInNew as OpenInNewIcon } from '@styled-icons/material/OpenInNew';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import useNewEntryUrl from '@staticcms/core/lib/hooks/useNewEntryUrl';
|
||||||
|
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
import { selectDisplayUrl } from '@staticcms/core/reducers/selectors/config';
|
||||||
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
|
import IconButton from '../common/button/IconButton';
|
||||||
|
import NavigationDrawer from './NavigationDrawer';
|
||||||
|
import QuickCreate from './QuickCreate';
|
||||||
|
|
||||||
|
import type { Collection } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
export interface BottomNavigationProps {
|
||||||
|
collection: Collection | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BottomNavigation: FC<BottomNavigationProps> = ({ collection }) => {
|
||||||
|
const params = useParams();
|
||||||
|
const filterTerm = useMemo(() => params['*'], [params]);
|
||||||
|
const newEntryUrl = useNewEntryUrl(collection, filterTerm);
|
||||||
|
|
||||||
|
const displayUrl = useAppSelector(selectDisplayUrl);
|
||||||
|
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
const toggleMobileMenu = useCallback(() => {
|
||||||
|
setMobileOpen(old => !old);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
fixed
|
||||||
|
bottom-0
|
||||||
|
left-0
|
||||||
|
right-0
|
||||||
|
h-16
|
||||||
|
shadow-bottom-navigation
|
||||||
|
bg-gray-50
|
||||||
|
dark:bg-gray-800
|
||||||
|
flex
|
||||||
|
px-6
|
||||||
|
py-1
|
||||||
|
md:hidden
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<IconButton variant="text" className="flex-grow" onClick={toggleMobileMenu}>
|
||||||
|
<MenuIcon className="w-6 h-6" />
|
||||||
|
</IconButton>
|
||||||
|
{isNotEmpty(newEntryUrl) ? (
|
||||||
|
<IconButton to={newEntryUrl} variant="text" className="flex-grow">
|
||||||
|
<AddIcon className="w-6 h-6" />
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<QuickCreate
|
||||||
|
key="quick-create"
|
||||||
|
variant="text"
|
||||||
|
rootClassName="flex-grow"
|
||||||
|
buttonClassName="w-full"
|
||||||
|
hideDropdownIcon
|
||||||
|
hideLabel
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{displayUrl ? (
|
||||||
|
<IconButton variant="text" className="flex-grow" href={displayUrl}>
|
||||||
|
<OpenInNewIcon className="h-6 w-6" />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<NavigationDrawer mobileOpen={mobileOpen} onMobileOpenToggle={toggleMobileMenu} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BottomNavigation;
|
142
packages/core/src/components/navbar/Breadcrumbs.tsx
Normal file
142
packages/core/src/components/navbar/Breadcrumbs.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { ArrowBack as ArrowBackIcon } from '@styled-icons/material/ArrowBack';
|
||||||
|
import React, { Fragment, useMemo } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
|
||||||
|
import type { Breadcrumb } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
export interface BreadcrumbsProps {
|
||||||
|
breadcrumbs: Breadcrumb[];
|
||||||
|
inEditor?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Breadcrumbs: FC<BreadcrumbsProps> = ({ breadcrumbs, inEditor = false }) => {
|
||||||
|
const finalNonEditorBreadcrumb = useMemo(() => {
|
||||||
|
const nonEditorBreadcrumbs = breadcrumbs.filter(b => !b.editor);
|
||||||
|
if (nonEditorBreadcrumbs.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonEditorBreadcrumbs[nonEditorBreadcrumbs.length - 1];
|
||||||
|
}, [breadcrumbs]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
flex
|
||||||
|
h-full
|
||||||
|
md:w-auto
|
||||||
|
items-center
|
||||||
|
text-xl
|
||||||
|
font-semibold
|
||||||
|
gap-1
|
||||||
|
text-gray-800
|
||||||
|
dark:text-white
|
||||||
|
flex-grow
|
||||||
|
truncate
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
hidden
|
||||||
|
md:flex
|
||||||
|
overflow-hidden
|
||||||
|
relative
|
||||||
|
flex-grow
|
||||||
|
h-full
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
w-full
|
||||||
|
absolute
|
||||||
|
inset-0
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
gap-1
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{breadcrumbs.map((breadcrumb, index) =>
|
||||||
|
breadcrumb.name ? (
|
||||||
|
<Fragment key={`breadcrumb-${index}`}>
|
||||||
|
{index > 0 ? <span key={`separator-${index}`}>></span> : null}
|
||||||
|
{breadcrumb.to ? (
|
||||||
|
<Link
|
||||||
|
key={`link-${index}`}
|
||||||
|
className={classNames(
|
||||||
|
`
|
||||||
|
hover:text-gray-400
|
||||||
|
dark:hover:text-gray-400
|
||||||
|
overflow-hidden
|
||||||
|
whitespace-nowrap
|
||||||
|
focus:outline-none
|
||||||
|
focus:ring-4
|
||||||
|
focus:ring-gray-200
|
||||||
|
dark:focus:ring-slate-700
|
||||||
|
`,
|
||||||
|
index + 1 === breadcrumbs.length ? 'text-ellipsis' : 'flex-shrink-0',
|
||||||
|
)}
|
||||||
|
to={breadcrumb.to}
|
||||||
|
>
|
||||||
|
{breadcrumb.name}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
key={`text-${index}`}
|
||||||
|
className={classNames(
|
||||||
|
`
|
||||||
|
truncate
|
||||||
|
`,
|
||||||
|
index + 1 === breadcrumbs.length ? 'text-ellipsis' : 'flex-shrink-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{breadcrumb.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{finalNonEditorBreadcrumb ? (
|
||||||
|
finalNonEditorBreadcrumb.to ? (
|
||||||
|
<Link
|
||||||
|
key="final-non-editor-breadcrumb-link"
|
||||||
|
className="
|
||||||
|
flex
|
||||||
|
md:hidden
|
||||||
|
gap-2
|
||||||
|
items-center
|
||||||
|
truncate
|
||||||
|
w-full
|
||||||
|
focus:outline-none
|
||||||
|
focus:ring-4
|
||||||
|
focus:ring-gray-200
|
||||||
|
dark:focus:ring-slate-700
|
||||||
|
"
|
||||||
|
to={finalNonEditorBreadcrumb.to}
|
||||||
|
>
|
||||||
|
{inEditor ? <ArrowBackIcon className="w-6 h-6" /> : null}
|
||||||
|
{finalNonEditorBreadcrumb.name}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
key="final-non-editor-breadcrumb-text"
|
||||||
|
className="
|
||||||
|
block
|
||||||
|
md:hidden
|
||||||
|
truncate
|
||||||
|
w-full
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{finalNonEditorBreadcrumb?.name ?? ''}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Breadcrumbs;
|
@ -1,18 +1,19 @@
|
|||||||
import { OpenInNew as OpenInNewIcon } from '@styled-icons/material/OpenInNew';
|
import { OpenInNew as OpenInNewIcon } from '@styled-icons/material/OpenInNew';
|
||||||
import React, { Fragment, useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { checkBackendStatus } from '@staticcms/core/actions/status';
|
import { checkBackendStatus } from '@staticcms/core/actions/status';
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { selectConfig, selectDisplayUrl } from '@staticcms/core/reducers/selectors/config';
|
import { selectConfig, selectDisplayUrl } from '@staticcms/core/reducers/selectors/config';
|
||||||
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
import Button from '../common/button/Button';
|
import Button from '../common/button/Button';
|
||||||
import { StaticCmsIcon } from '../images/_index';
|
import { StaticCmsIcon } from '../images/_index';
|
||||||
|
import Breadcrumbs from './Breadcrumbs';
|
||||||
import QuickCreate from './QuickCreate';
|
import QuickCreate from './QuickCreate';
|
||||||
import SettingsDropdown from './SettingsDropdown';
|
import SettingsDropdown from './SettingsDropdown';
|
||||||
|
|
||||||
import type { Breadcrumb, TranslatedProps } from '@staticcms/core/interface';
|
import type { Breadcrumb, TranslatedProps } from '@staticcms/core/interface';
|
||||||
import type { ComponentType, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
export interface NavbarProps {
|
export interface NavbarProps {
|
||||||
breadcrumbs?: Breadcrumb[];
|
breadcrumbs?: Breadcrumb[];
|
||||||
@ -40,6 +41,11 @@ const Navbar = ({
|
|||||||
};
|
};
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const inEditor = useMemo(
|
||||||
|
() => Boolean(breadcrumbs.length > 0 && breadcrumbs[breadcrumbs.length - 1].editor),
|
||||||
|
[breadcrumbs],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className="
|
className="
|
||||||
@ -52,49 +58,56 @@ const Navbar = ({
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div key="nav" className="mx-auto pr-2 sm:pr-4 lg:pr-5">
|
<div key="nav" className="mx-auto pr-2 sm:pr-4 lg:pr-5">
|
||||||
<div className="relative flex h-16 items-center justify-between">
|
<div
|
||||||
<div className="flex flex-1 items-center justify-center h-full sm:items-stretch sm:justify-start gap-4">
|
className={classNames(
|
||||||
<div className="flex flex-shrink-0 items-center justify-center bg-slate-500 dark:bg-slate-700 w-16">
|
`
|
||||||
|
relative
|
||||||
|
flex
|
||||||
|
h-16
|
||||||
|
items-center
|
||||||
|
justify-between
|
||||||
|
gap-2
|
||||||
|
`,
|
||||||
|
inEditor && 'pl-3 md:pl-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 h-full items-stretch justify-start gap-2 md:gap-4 truncate">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
`
|
||||||
|
flex-shrink-0
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
w-16
|
||||||
|
bg-slate-500
|
||||||
|
dark:bg-slate-700
|
||||||
|
`,
|
||||||
|
inEditor ? 'hidden md:flex' : 'flex',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{config?.logo_url ? (
|
{config?.logo_url ? (
|
||||||
<div
|
<div
|
||||||
className="inline-flex h-10 w-10 bg-cover bg-no-repeat bg-center object-cover"
|
className="h-10 w-10 bg-cover bg-no-repeat bg-center object-cover"
|
||||||
style={{ backgroundImage: `url('${config.logo_url}')` }}
|
style={{ backgroundImage: `url('${config.logo_url}')` }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StaticCmsIcon className="inline-flex w-10 h-10" />
|
<StaticCmsIcon className="w-10 h-10" />
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex h-full items-center text-xl font-semibold gap-1 text-gray-800 dark:text-white">
|
|
||||||
{breadcrumbs.map((breadcrumb, index) =>
|
|
||||||
breadcrumb.name ? (
|
|
||||||
<Fragment key={`breadcrumb-${index}`}>
|
|
||||||
{index > 0 ? <span key={`separator-${index}`}>></span> : null}
|
|
||||||
{breadcrumb.to ? (
|
|
||||||
<Link
|
|
||||||
key={`link-${index}`}
|
|
||||||
className="hover:text-gray-400 dark:hover:text-gray-400"
|
|
||||||
to={breadcrumb.to}
|
|
||||||
>
|
|
||||||
{breadcrumb.name}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span key={`text-${index}`}>{breadcrumb.name}</span>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
) : null,
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Breadcrumbs breadcrumbs={breadcrumbs} inEditor={inEditor} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3 items-center">
|
||||||
{displayUrl ? (
|
{displayUrl ? (
|
||||||
<Button variant="text" className="flex gap-2" href={displayUrl}>
|
<Button variant="text" className="gap-2 hidden lg:flex" href={displayUrl}>
|
||||||
{displayUrl}
|
<div className="hidden lg:flex">{displayUrl}</div>
|
||||||
<OpenInNewIcon className="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
<OpenInNewIcon className="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{showQuickCreate ? <QuickCreate key="quick-create" /> : null}
|
{showQuickCreate ? (
|
||||||
|
<QuickCreate key="quick-create" rootClassName="hidden md:block" />
|
||||||
|
) : null}
|
||||||
{navbarActions}
|
{navbarActions}
|
||||||
<SettingsDropdown />
|
<SettingsDropdown inEditor={inEditor} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -102,4 +115,4 @@ const Navbar = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(Navbar) as ComponentType<NavbarProps>;
|
export default translate()(Navbar) as FC<NavbarProps>;
|
||||||
|
56
packages/core/src/components/navbar/NavigationDrawer.tsx
Normal file
56
packages/core/src/components/navbar/NavigationDrawer.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import SidebarContent from './SidebarContent';
|
||||||
|
|
||||||
|
const DRAWER_WIDTH = 320;
|
||||||
|
|
||||||
|
interface NavigationDrawerProps {
|
||||||
|
mobileOpen: boolean;
|
||||||
|
onMobileOpenToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavigationDrawer = ({ mobileOpen, onMobileOpenToggle }: NavigationDrawerProps) => {
|
||||||
|
const iOS = useMemo(
|
||||||
|
() => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const container = useMemo(
|
||||||
|
() => (typeof window !== 'undefined' ? window.document.body : undefined),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwipeableDrawer
|
||||||
|
disableBackdropTransition={!iOS}
|
||||||
|
disableDiscovery={iOS}
|
||||||
|
container={container}
|
||||||
|
variant="temporary"
|
||||||
|
open={mobileOpen}
|
||||||
|
onOpen={onMobileOpenToggle}
|
||||||
|
onClose={onMobileOpenToggle}
|
||||||
|
ModalProps={{
|
||||||
|
keepMounted: true, // Better open performance on mobile.
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: '80%',
|
||||||
|
maxWidth: DRAWER_WIDTH,
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
width: '80%',
|
||||||
|
maxWidth: DRAWER_WIDTH,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div onClick={onMobileOpenToggle} className="w-full h-full">
|
||||||
|
<SidebarContent />
|
||||||
|
</div>
|
||||||
|
</SwipeableDrawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavigationDrawer;
|
@ -6,12 +6,19 @@ import { getNewEntryUrl } from '@staticcms/core/lib/urlHelper';
|
|||||||
import { selectCollections } from '@staticcms/core/reducers/selectors/collections';
|
import { selectCollections } from '@staticcms/core/reducers/selectors/collections';
|
||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
import Menu from '../common/menu/Menu';
|
import Menu from '../common/menu/Menu';
|
||||||
import MenuItemLink from '../common/menu/MenuItemLink';
|
|
||||||
import MenuGroup from '../common/menu/MenuGroup';
|
import MenuGroup from '../common/menu/MenuGroup';
|
||||||
|
import MenuItemLink from '../common/menu/MenuItemLink';
|
||||||
|
|
||||||
import type { TranslateProps } from 'react-polyglot';
|
import type { TranslatedProps } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import type { MenuProps } from '../common/menu/Menu';
|
||||||
|
|
||||||
const QuickCreate = ({ t }: TranslateProps) => {
|
export type QuickCreateProps = Pick<
|
||||||
|
MenuProps,
|
||||||
|
'rootClassName' | 'buttonClassName' | 'hideDropdownIcon' | 'hideLabel' | 'variant'
|
||||||
|
>;
|
||||||
|
|
||||||
|
const QuickCreate: FC<TranslatedProps<QuickCreateProps>> = ({ t, ...menuProps }) => {
|
||||||
const collections = useAppSelector(selectCollections);
|
const collections = useAppSelector(selectCollections);
|
||||||
|
|
||||||
const createableCollections = useMemo(
|
const createableCollections = useMemo(
|
||||||
@ -23,7 +30,7 @@ const QuickCreate = ({ t }: TranslateProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu label={t('app.header.quickAdd')} startIcon={AddIcon}>
|
<Menu label={t('app.header.quickAdd')} startIcon={AddIcon} {...menuProps}>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
{createableCollections.map(collection => (
|
{createableCollections.map(collection => (
|
||||||
<MenuItemLink key={collection.name} href={getNewEntryUrl(collection.name)}>
|
<MenuItemLink key={collection.name} href={getNewEntryUrl(collection.name)}>
|
||||||
@ -35,4 +42,4 @@ const QuickCreate = ({ t }: TranslateProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(QuickCreate);
|
export default translate()(QuickCreate) as FC<QuickCreateProps>;
|
||||||
|
@ -13,8 +13,8 @@ import MenuGroup from '../common/menu/MenuGroup';
|
|||||||
import MenuItemButton from '../common/menu/MenuItemButton';
|
import MenuItemButton from '../common/menu/MenuItemButton';
|
||||||
import Switch from '../common/switch/Switch';
|
import Switch from '../common/switch/Switch';
|
||||||
|
|
||||||
import type { ChangeEvent, MouseEvent } from 'react';
|
import type { TranslatedProps } from '@staticcms/core/interface';
|
||||||
import type { TranslateProps } from 'react-polyglot';
|
import type { ChangeEvent, FC, MouseEvent } from 'react';
|
||||||
|
|
||||||
interface AvatarImageProps {
|
interface AvatarImageProps {
|
||||||
imageUrl: string | undefined;
|
imageUrl: string | undefined;
|
||||||
@ -28,7 +28,11 @@ const AvatarImage = ({ imageUrl }: AvatarImageProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsDropdown = ({ t }: TranslateProps) => {
|
export interface SettingsDropdownProps {
|
||||||
|
inEditor: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsDropdown: FC<TranslatedProps<SettingsDropdownProps>> = ({ inEditor, t }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const user = useAppSelector(selectUser);
|
const user = useAppSelector(selectUser);
|
||||||
const [isDarkMode, setIsDarkMode] = useState(document.documentElement.classList.contains('dark'));
|
const [isDarkMode, setIsDarkMode] = useState(document.documentElement.classList.contains('dark'));
|
||||||
@ -84,6 +88,7 @@ const SettingsDropdown = ({ t }: TranslateProps) => {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
rounded={!user?.avatar_url || 'no-padding'}
|
rounded={!user?.avatar_url || 'no-padding'}
|
||||||
hideDropdownIcon
|
hideDropdownIcon
|
||||||
|
rootClassName={inEditor ? 'hidden md:flex' : ''}
|
||||||
>
|
>
|
||||||
<MenuGroup>
|
<MenuGroup>
|
||||||
<MenuItemButton key="dark-mode" onClick={handleToggleDarkMode} startIcon={MoonIcon}>
|
<MenuItemButton key="dark-mode" onClick={handleToggleDarkMode} startIcon={MoonIcon}>
|
||||||
@ -102,4 +107,4 @@ const SettingsDropdown = ({ t }: TranslateProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(SettingsDropdown);
|
export default translate()(SettingsDropdown) as FC<SettingsDropdownProps>;
|
||||||
|
@ -1,139 +1,19 @@
|
|||||||
import { Photo as PhotoIcon } from '@styled-icons/material/Photo';
|
import React from 'react';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
|
||||||
import { translate } from 'react-polyglot';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { getIcon } from '@staticcms/core/lib/hooks/useIcon';
|
|
||||||
import { getAdditionalLinks } from '@staticcms/core/lib/registry';
|
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import { selectCollections } from '@staticcms/core/reducers/selectors/collections';
|
import SidebarContent from './SidebarContent';
|
||||||
import { selectIsSearchEnabled } from '@staticcms/core/reducers/selectors/config';
|
|
||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
|
||||||
import CollectionSearch from '../collections/CollectionSearch';
|
|
||||||
import NestedCollection from '../collections/NestedCollection';
|
|
||||||
import NavLink from './NavLink';
|
|
||||||
|
|
||||||
import type { Collection } from '@staticcms/core/interface';
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { TranslateProps } from 'react-polyglot';
|
|
||||||
|
|
||||||
const Sidebar: FC<TranslateProps> = ({ t }) => {
|
|
||||||
const { name, searchTerm, ...params } = useParams();
|
|
||||||
const filterTerm = useMemo(() => params['*'] ?? '', [params]);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const isSearchEnabled = useAppSelector(selectIsSearchEnabled);
|
|
||||||
const collections = useAppSelector(selectCollections);
|
|
||||||
|
|
||||||
const collection = useMemo(
|
|
||||||
() => (name ? collections[name] : collections[0]) as Collection | undefined,
|
|
||||||
[collections, name],
|
|
||||||
);
|
|
||||||
|
|
||||||
const collectionLinks = useMemo(
|
|
||||||
() =>
|
|
||||||
Object.values(collections)
|
|
||||||
.filter(collection => collection.hide !== true)
|
|
||||||
.map(collection => {
|
|
||||||
const collectionName = collection.name;
|
|
||||||
const icon = getIcon(collection.icon);
|
|
||||||
|
|
||||||
if ('nested' in collection) {
|
|
||||||
return (
|
|
||||||
<NestedCollection
|
|
||||||
key={`nested-${collectionName}`}
|
|
||||||
collection={collection}
|
|
||||||
filterTerm={filterTerm}
|
|
||||||
data-testid={collectionName}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavLink key={collectionName} to={`/collections/${collectionName}`} icon={icon}>
|
|
||||||
{collection.label}
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[collections, filterTerm],
|
|
||||||
);
|
|
||||||
|
|
||||||
const additionalLinks = useMemo(() => getAdditionalLinks(), []);
|
|
||||||
const links = useMemo(
|
|
||||||
() =>
|
|
||||||
Object.values(additionalLinks).map(
|
|
||||||
({ id, title, data, options: { icon: iconName } = {} }) => {
|
|
||||||
const icon = getIcon(iconName);
|
|
||||||
|
|
||||||
return typeof data === 'string' ? (
|
|
||||||
<NavLink key={title} href={data} icon={icon}>
|
|
||||||
{title}
|
|
||||||
</NavLink>
|
|
||||||
) : (
|
|
||||||
<NavLink key={title} to={`/page/${id}`} icon={icon}>
|
|
||||||
{title}
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
[additionalLinks],
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchCollections = useCallback(
|
|
||||||
(query?: string, collection?: string) => {
|
|
||||||
if (!query) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collection) {
|
|
||||||
navigate(`/collections/${collection}/search/${query}`);
|
|
||||||
} else {
|
|
||||||
navigate(`/search/${query}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[navigate],
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const Sidebar: FC = () => {
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={classNames(
|
className={classNames('w-sidebar-expanded', 'h-main-mobile md:h-main hidden md:block')}
|
||||||
'w-sidebar-expanded',
|
|
||||||
'h-main sm:fixed sm:z-20 sm:shadow-sidebar lg:block lg:z-auto lg:shadow-none',
|
|
||||||
)}
|
|
||||||
aria-label="Sidebar"
|
aria-label="Sidebar"
|
||||||
>
|
>
|
||||||
<div
|
<SidebarContent />
|
||||||
className="
|
|
||||||
px-3
|
|
||||||
py-4
|
|
||||||
h-full
|
|
||||||
w-full
|
|
||||||
overflow-y-auto
|
|
||||||
bg-white
|
|
||||||
dark:bg-slate-800
|
|
||||||
styled-scrollbars
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{isSearchEnabled && (
|
|
||||||
<CollectionSearch
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
collections={collections}
|
|
||||||
collection={collection}
|
|
||||||
onSubmit={(query: string, collection?: string) =>
|
|
||||||
searchCollections(query, collection)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{collectionLinks}
|
|
||||||
{links}
|
|
||||||
<NavLink key="Media" to="/media" icon={<PhotoIcon className="h-6 w-6" />}>
|
|
||||||
{t('app.header.media')}
|
|
||||||
</NavLink>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(Sidebar) as FC;
|
export default Sidebar;
|
||||||
|
128
packages/core/src/components/navbar/SidebarContent.tsx
Normal file
128
packages/core/src/components/navbar/SidebarContent.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { Photo as PhotoIcon } from '@styled-icons/material/Photo';
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { translate } from 'react-polyglot';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { getIcon } from '@staticcms/core/lib/hooks/useIcon';
|
||||||
|
import { getAdditionalLinks } from '@staticcms/core/lib/registry';
|
||||||
|
import { selectCollections } from '@staticcms/core/reducers/selectors/collections';
|
||||||
|
import { selectIsSearchEnabled } from '@staticcms/core/reducers/selectors/config';
|
||||||
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
|
import CollectionSearch from '../collections/CollectionSearch';
|
||||||
|
import NestedCollection from '../collections/NestedCollection';
|
||||||
|
import NavLink from './NavLink';
|
||||||
|
|
||||||
|
import type { Collection } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import type { TranslateProps } from 'react-polyglot';
|
||||||
|
|
||||||
|
const SidebarContent: FC<TranslateProps> = ({ t }) => {
|
||||||
|
const { name, searchTerm, ...params } = useParams();
|
||||||
|
const filterTerm = useMemo(() => params['*'] ?? '', [params]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isSearchEnabled = useAppSelector(selectIsSearchEnabled);
|
||||||
|
const collections = useAppSelector(selectCollections);
|
||||||
|
|
||||||
|
const collection = useMemo(
|
||||||
|
() => (name ? collections[name] : collections[0]) as Collection | undefined,
|
||||||
|
[collections, name],
|
||||||
|
);
|
||||||
|
|
||||||
|
const collectionLinks = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(collections)
|
||||||
|
.filter(collection => collection.hide !== true)
|
||||||
|
.map(collection => {
|
||||||
|
const collectionName = collection.name;
|
||||||
|
const icon = getIcon(collection.icon);
|
||||||
|
|
||||||
|
if ('nested' in collection) {
|
||||||
|
return (
|
||||||
|
<NestedCollection
|
||||||
|
key={`nested-${collectionName}`}
|
||||||
|
collection={collection}
|
||||||
|
filterTerm={filterTerm}
|
||||||
|
data-testid={collectionName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink key={collectionName} to={`/collections/${collectionName}`} icon={icon}>
|
||||||
|
{collection.label}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[collections, filterTerm],
|
||||||
|
);
|
||||||
|
|
||||||
|
const additionalLinks = useMemo(() => getAdditionalLinks(), []);
|
||||||
|
const links = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(additionalLinks).map(
|
||||||
|
({ id, title, data, options: { icon: iconName } = {} }) => {
|
||||||
|
const icon = getIcon(iconName);
|
||||||
|
|
||||||
|
return typeof data === 'string' ? (
|
||||||
|
<NavLink key={title} href={data} icon={icon}>
|
||||||
|
{title}
|
||||||
|
</NavLink>
|
||||||
|
) : (
|
||||||
|
<NavLink key={title} to={`/page/${id}`} icon={icon}>
|
||||||
|
{title}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[additionalLinks],
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchCollections = useCallback(
|
||||||
|
(query?: string, collection?: string) => {
|
||||||
|
if (!query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
navigate(`/collections/${collection}/search/${query}`);
|
||||||
|
} else {
|
||||||
|
navigate(`/search/${query}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
px-3
|
||||||
|
py-4
|
||||||
|
h-full
|
||||||
|
w-full
|
||||||
|
overflow-y-auto
|
||||||
|
bg-white
|
||||||
|
dark:bg-slate-800
|
||||||
|
styled-scrollbars
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{isSearchEnabled && (
|
||||||
|
<CollectionSearch
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
collections={collections}
|
||||||
|
collection={collection}
|
||||||
|
onSubmit={(query: string, collection?: string) => searchCollections(query, collection)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{collectionLinks}
|
||||||
|
{links}
|
||||||
|
<NavLink key="Media" to="/media" icon={<PhotoIcon className="h-6 w-6" />}>
|
||||||
|
{t('app.header.media')}
|
||||||
|
</NavLink>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default translate()(SidebarContent) as FC;
|
@ -72,6 +72,9 @@ export const INFERABLE_FIELDS: Record<string, InferredField> = {
|
|||||||
'cover',
|
'cover',
|
||||||
'hero',
|
'hero',
|
||||||
'logo',
|
'logo',
|
||||||
|
'cover_image',
|
||||||
|
'cover-image',
|
||||||
|
'coverimage',
|
||||||
],
|
],
|
||||||
defaultPreview: value => value,
|
defaultPreview: value => value,
|
||||||
fallbackToFirstField: false,
|
fallbackToFirstField: false,
|
||||||
|
@ -1103,6 +1103,7 @@ export interface SvgProps {
|
|||||||
export interface Breadcrumb {
|
export interface Breadcrumb {
|
||||||
name?: string;
|
name?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
|
editor?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaLibraryDisplayURL {
|
export interface MediaLibraryDisplayURL {
|
||||||
|
@ -83,6 +83,7 @@ export default function useBreadcrumbs(
|
|||||||
collectionLabel: collection.label_singular || collection.label,
|
collectionLabel: collection.label_singular || collection.label,
|
||||||
})
|
})
|
||||||
: summary,
|
: summary,
|
||||||
|
editor: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
25
packages/core/src/lib/hooks/useMeta.ts
Normal file
25
packages/core/src/lib/hooks/useMeta.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export interface MetaProps {
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useMeta({ name, content }: MetaProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
const head = document.querySelector('head');
|
||||||
|
if (!head) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = document.createElement('meta');
|
||||||
|
|
||||||
|
meta.setAttribute('name', name);
|
||||||
|
meta.setAttribute('content', content);
|
||||||
|
head.appendChild(meta);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
head.removeChild(meta);
|
||||||
|
};
|
||||||
|
}, [content, name]);
|
||||||
|
}
|
21
packages/core/src/lib/hooks/useNewEntryUrl.ts
Normal file
21
packages/core/src/lib/hooks/useNewEntryUrl.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { getNewEntryUrl } from '../urlHelper';
|
||||||
|
import { isNotEmpty } from '../util/string.util';
|
||||||
|
|
||||||
|
import type { Collection } from '@staticcms/core/interface';
|
||||||
|
|
||||||
|
export default function useNewEntryUrl(
|
||||||
|
collection: Collection | undefined,
|
||||||
|
filterTerm: string | undefined,
|
||||||
|
) {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!collection) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'fields' in collection && collection.create
|
||||||
|
? `${getNewEntryUrl(collection.name)}${isNotEmpty(filterTerm) ? `/${filterTerm}` : ''}`
|
||||||
|
: '';
|
||||||
|
}, [collection, filterTerm]);
|
||||||
|
}
|
@ -69,8 +69,7 @@ export function getLocaleDataPath(locale: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDataPath(locale: string, defaultLocale: string) {
|
export function getDataPath(locale: string, defaultLocale: string) {
|
||||||
const dataPath = locale !== defaultLocale ? getLocaleDataPath(locale) : ['data'];
|
return locale !== defaultLocale ? getLocaleDataPath(locale) : ['data'];
|
||||||
return dataPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFilePath(
|
export function getFilePath(
|
||||||
|
@ -8,13 +8,15 @@
|
|||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
html {
|
html {
|
||||||
@apply overflow-hidden;
|
@apply overflow-hidden
|
||||||
|
w-screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply text-gray-800
|
@apply text-gray-800
|
||||||
dark:text-gray-100
|
dark:text-gray-100
|
||||||
overflow-hidden;
|
overflow-hidden
|
||||||
|
w-screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,7 +31,8 @@
|
|||||||
focus:outline-none
|
focus:outline-none
|
||||||
focus:ring-4
|
focus:ring-4
|
||||||
focus:ring-gray-200
|
focus:ring-gray-200
|
||||||
dark:focus:ring-slate-700;
|
dark:focus:ring-slate-700
|
||||||
|
border;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@ -64,8 +67,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-contained-primary {
|
.btn-contained-primary {
|
||||||
@apply border
|
@apply border-transparent
|
||||||
border-transparent
|
|
||||||
bg-blue-700
|
bg-blue-700
|
||||||
hover:bg-blue-800
|
hover:bg-blue-800
|
||||||
text-white
|
text-white
|
||||||
@ -80,7 +82,6 @@
|
|||||||
.btn-outlined-primary {
|
.btn-outlined-primary {
|
||||||
@apply text-gray-800
|
@apply text-gray-800
|
||||||
bg-transparent
|
bg-transparent
|
||||||
border
|
|
||||||
border-gray-200
|
border-gray-200
|
||||||
hover:bg-slate-100
|
hover:bg-slate-100
|
||||||
hover:border-blue-400
|
hover:border-blue-400
|
||||||
@ -101,6 +102,7 @@
|
|||||||
|
|
||||||
.btn-text-primary {
|
.btn-text-primary {
|
||||||
@apply bg-transparent
|
@apply bg-transparent
|
||||||
|
border-transparent
|
||||||
text-gray-800
|
text-gray-800
|
||||||
hover:text-blue-700
|
hover:text-blue-700
|
||||||
hover:bg-blue-100
|
hover:bg-blue-100
|
||||||
@ -110,13 +112,11 @@
|
|||||||
dark:hover:text-blue-400
|
dark:hover:text-blue-400
|
||||||
dark:hover:bg-slate-700/75
|
dark:hover:bg-slate-700/75
|
||||||
dark:disabled:text-slate-600/75
|
dark:disabled:text-slate-600/75
|
||||||
dark:disabled:border-slate-600/75
|
|
||||||
dark:disabled:hover:bg-transparent;
|
dark:disabled:hover:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-contained-secondary {
|
.btn-contained-secondary {
|
||||||
@apply border
|
@apply font-medium
|
||||||
font-medium
|
|
||||||
text-gray-600
|
text-gray-600
|
||||||
bg-white
|
bg-white
|
||||||
border-gray-200/75
|
border-gray-200/75
|
||||||
@ -140,7 +140,6 @@
|
|||||||
.btn-outlined-secondary {
|
.btn-outlined-secondary {
|
||||||
@apply text-gray-800
|
@apply text-gray-800
|
||||||
bg-transparent
|
bg-transparent
|
||||||
border
|
|
||||||
border-gray-200
|
border-gray-200
|
||||||
hover:bg-gray-100
|
hover:bg-gray-100
|
||||||
hover:text-gray-800
|
hover:text-gray-800
|
||||||
@ -161,6 +160,7 @@
|
|||||||
|
|
||||||
.btn-text-secondary {
|
.btn-text-secondary {
|
||||||
@apply bg-transparent
|
@apply bg-transparent
|
||||||
|
border-transparent
|
||||||
text-gray-800
|
text-gray-800
|
||||||
hover:text-gray-800
|
hover:text-gray-800
|
||||||
hover:bg-gray-100
|
hover:bg-gray-100
|
||||||
@ -170,13 +170,11 @@
|
|||||||
dark:hover:text-white
|
dark:hover:text-white
|
||||||
dark:hover:bg-gray-700
|
dark:hover:bg-gray-700
|
||||||
dark:disabled:text-gray-400/20
|
dark:disabled:text-gray-400/20
|
||||||
dark:disabled:border-gray-600/20
|
|
||||||
dark:disabled:hover:bg-transparent;
|
dark:disabled:hover:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-contained-success {
|
.btn-contained-success {
|
||||||
@apply border
|
@apply border-transparent
|
||||||
border-transparent
|
|
||||||
bg-green-600
|
bg-green-600
|
||||||
hover:bg-green-700
|
hover:bg-green-700
|
||||||
text-white
|
text-white
|
||||||
@ -190,7 +188,6 @@
|
|||||||
|
|
||||||
.btn-outlined-success {
|
.btn-outlined-success {
|
||||||
@apply bg-transparent
|
@apply bg-transparent
|
||||||
border
|
|
||||||
text-green-500
|
text-green-500
|
||||||
border-green-200
|
border-green-200
|
||||||
hover:bg-green-100
|
hover:bg-green-100
|
||||||
@ -210,6 +207,7 @@
|
|||||||
|
|
||||||
.btn-text-success {
|
.btn-text-success {
|
||||||
@apply bg-transparent
|
@apply bg-transparent
|
||||||
|
border-transparent
|
||||||
text-green-500
|
text-green-500
|
||||||
hover:text-green-700
|
hover:text-green-700
|
||||||
hover:bg-green-100
|
hover:bg-green-100
|
||||||
@ -219,13 +217,11 @@
|
|||||||
dark:hover:text-green-500
|
dark:hover:text-green-500
|
||||||
dark:hover:bg-green-700/10
|
dark:hover:bg-green-700/10
|
||||||
dark:disabled:text-slate-600/75
|
dark:disabled:text-slate-600/75
|
||||||
dark:disabled:border-slate-600/75
|
|
||||||
dark:disabled:hover:bg-transparent;
|
dark:disabled:hover:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-contained-error {
|
.btn-contained-error {
|
||||||
@apply border
|
@apply border-transparent
|
||||||
border-transparent
|
|
||||||
bg-red-600
|
bg-red-600
|
||||||
hover:bg-red-700
|
hover:bg-red-700
|
||||||
text-white
|
text-white
|
||||||
@ -239,7 +235,6 @@
|
|||||||
|
|
||||||
.btn-outlined-error {
|
.btn-outlined-error {
|
||||||
@apply bg-transparent
|
@apply bg-transparent
|
||||||
border
|
|
||||||
text-red-500
|
text-red-500
|
||||||
border-red-200
|
border-red-200
|
||||||
hover:bg-red-100
|
hover:bg-red-100
|
||||||
@ -259,6 +254,7 @@
|
|||||||
|
|
||||||
.btn-text-error {
|
.btn-text-error {
|
||||||
@apply bg-transparent
|
@apply bg-transparent
|
||||||
|
border-transparent
|
||||||
text-red-500
|
text-red-500
|
||||||
hover:text-red-700
|
hover:text-red-700
|
||||||
hover:bg-red-100
|
hover:bg-red-100
|
||||||
@ -268,13 +264,11 @@
|
|||||||
dark:hover:text-red-500
|
dark:hover:text-red-500
|
||||||
dark:hover:bg-red-700/10
|
dark:hover:bg-red-700/10
|
||||||
dark:disabled:text-slate-600/75
|
dark:disabled:text-slate-600/75
|
||||||
dark:disabled:border-slate-600/75
|
|
||||||
dark:disabled:hover:bg-transparent;
|
dark:disabled:hover:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-contained-warning {
|
.btn-contained-warning {
|
||||||
@apply border
|
@apply border-transparent
|
||||||
border-transparent
|
|
||||||
bg-yellow-500
|
bg-yellow-500
|
||||||
hover:bg-yellow-600
|
hover:bg-yellow-600
|
||||||
text-white
|
text-white
|
||||||
@ -308,6 +302,7 @@
|
|||||||
|
|
||||||
.btn-text-warning {
|
.btn-text-warning {
|
||||||
@apply bg-transparent
|
@apply bg-transparent
|
||||||
|
border-transparent
|
||||||
text-yellow-500
|
text-yellow-500
|
||||||
hover:text-yellow-600
|
hover:text-yellow-600
|
||||||
hover:bg-yellow-50
|
hover:bg-yellow-50
|
||||||
@ -317,7 +312,6 @@
|
|||||||
dark:hover:text-yellow-500
|
dark:hover:text-yellow-500
|
||||||
dark:hover:bg-yellow-700/10
|
dark:hover:bg-yellow-700/10
|
||||||
dark:disabled:text-slate-600/75
|
dark:disabled:text-slate-600/75
|
||||||
dark:disabled:border-slate-600/75
|
|
||||||
dark:disabled:hover:bg-transparent;
|
dark:disabled:hover:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,6 +373,10 @@
|
|||||||
height: 10px; /* Mostly for horizontal scrollbars */
|
height: 10px; /* Mostly for horizontal scrollbars */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.styled-scrollbars::-webkit-scrollbar-corner {
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
.styled-scrollbars::-webkit-scrollbar-thumb {
|
.styled-scrollbars::-webkit-scrollbar-thumb {
|
||||||
/* Foreground */
|
/* Foreground */
|
||||||
background: var(--scrollbar-foreground);
|
background: var(--scrollbar-foreground);
|
||||||
@ -389,6 +387,20 @@
|
|||||||
background: var(--scrollbar-background);
|
background: var(--scrollbar-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none; /* Safari and Chrome */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-tap {
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
|
@ -249,6 +249,7 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
|
|||||||
{...convertMuiTextFieldProps(props)}
|
{...convertMuiTextFieldProps(props)}
|
||||||
inputRef={ref}
|
inputRef={ref}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
|
inputClassName="truncate"
|
||||||
/>
|
/>
|
||||||
<NowButton
|
<NowButton
|
||||||
key="mobile-date-now"
|
key="mobile-date-now"
|
||||||
@ -282,6 +283,7 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
|
|||||||
forSingleList={forSingleList}
|
forSingleList={forSingleList}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
wrapperClassName="!w-date-widget"
|
||||||
>
|
>
|
||||||
<LocalizationProvider key="localization-provider" dateAdapter={AdapterDateFns}>
|
<LocalizationProvider key="localization-provider" dateAdapter={AdapterDateFns}>
|
||||||
{dateTimePicker}
|
{dateTimePicker}
|
||||||
|
@ -350,7 +350,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
|||||||
if (Array.isArray(internalValue) ? internalValue.length === 0 : isEmpty(internalValue)) {
|
if (Array.isArray(internalValue) ? internalValue.length === 0 : isEmpty(internalValue)) {
|
||||||
return (
|
return (
|
||||||
<div key="selection" className="flex flex-col gap-2 px-3 pt-2 pb-4">
|
<div key="selection" className="flex flex-col gap-2 px-3 pt-2 pb-4">
|
||||||
<div key="controls" className="flex gap-2">
|
<div key="controls" className="flex gap-2 flex-col xs:flex-row">
|
||||||
<Button
|
<Button
|
||||||
buttonRef={uploadButtonRef}
|
buttonRef={uploadButtonRef}
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -388,7 +388,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{renderedImagesLinks}
|
{renderedImagesLinks}
|
||||||
<div key="controls" className="flex gap-2">
|
<div key="controls" className="flex gap-2 flex-col xs:flex-row">
|
||||||
<Button
|
<Button
|
||||||
buttonRef={uploadButtonRef}
|
buttonRef={uploadButtonRef}
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -364,7 +364,7 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
|
|||||||
<Menu
|
<Menu
|
||||||
label={t('editor.editorWidgets.list.addType', { item: label })}
|
label={t('editor.editorWidgets.list.addType', { item: label })}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
className="w-full z-20"
|
buttonClassName="w-full z-20"
|
||||||
data-testid="list-type-add"
|
data-testid="list-type-add"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MDXProvider } from '@mdx-js/react';
|
import { MDXProvider } from '@mdx-js/react';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { VFileMessage } from 'vfile-message';
|
import { VFileMessage } from 'vfile-message';
|
||||||
|
|
||||||
import { withMdxImage } from '@staticcms/core/components/common/image/Image';
|
import { withMdxImage } from '@staticcms/core/components/common/image/Image';
|
||||||
@ -11,20 +11,33 @@ import { processShortcodeConfigToMdx } from './plate/serialization/slate/process
|
|||||||
|
|
||||||
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
|
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
import type { UseMdxState } from './plate/hooks/useMdx';
|
||||||
|
|
||||||
interface FallbackComponentProps {
|
interface MdxComponentProps {
|
||||||
error: string;
|
state: UseMdxState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FallbackComponent({ error }: FallbackComponentProps) {
|
// Create a preview component that can handle errors with try-catch block; for catching invalid JS expressions errors that ErrorBoundary cannot catch.
|
||||||
const message = new VFileMessage(error);
|
const MdxComponent: FC<MdxComponentProps> = ({ state }) => {
|
||||||
message.fatal = true;
|
const Result = useMemo(() => state.file?.result as FC | undefined, [state]);
|
||||||
return (
|
|
||||||
<pre>
|
if (!Result) {
|
||||||
<code>{String(message)}</code>
|
return null;
|
||||||
</pre>
|
}
|
||||||
);
|
|
||||||
}
|
try {
|
||||||
|
return <Result key="result" />;
|
||||||
|
} catch (error) {
|
||||||
|
const message = new VFileMessage(String(error));
|
||||||
|
message.fatal = true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre key="error">
|
||||||
|
<code>{String(message)}</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewProps => {
|
const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewProps => {
|
||||||
const { value, collection, field } = previewProps;
|
const { value, collection, field } = previewProps;
|
||||||
@ -40,7 +53,7 @@ const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewPr
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [state, setValue] = useMdx(`editor-${id}.mdx`, value ?? '');
|
const [state, setValue] = useMdx(`editor-${id}.mdx`, value ?? '');
|
||||||
const [prevValue, setPrevValue] = useState('');
|
const [prevValue, setPrevValue] = useState<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prevValue !== value) {
|
if (prevValue !== value) {
|
||||||
const parsedValue = processShortcodeConfigToMdx(getShortcodes(), value ?? '');
|
const parsedValue = processShortcodeConfigToMdx(getShortcodes(), value ?? '');
|
||||||
@ -49,35 +62,13 @@ const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewPr
|
|||||||
}
|
}
|
||||||
}, [prevValue, setValue, value]);
|
}, [prevValue, setValue, value]);
|
||||||
|
|
||||||
// Create a preview component that can handle errors with try-catch block; for catching invalid JS expressions errors that ErrorBoundary cannot catch.
|
return (
|
||||||
const MdxComponent = useCallback(() => {
|
<div key="markdown-preview">
|
||||||
if (!state.file) {
|
<MDXProvider components={components}>
|
||||||
return null;
|
<MdxComponent state={state} />{' '}
|
||||||
}
|
</MDXProvider>
|
||||||
|
</div>
|
||||||
try {
|
);
|
||||||
return (state.file.result as FC)({});
|
|
||||||
} catch (error) {
|
|
||||||
return <FallbackComponent error={String(error)} />;
|
|
||||||
}
|
|
||||||
}, [state.file]);
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{state.file && state.file.result ? (
|
|
||||||
<MDXProvider components={components}>
|
|
||||||
<MdxComponent />
|
|
||||||
</MDXProvider>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [MdxComponent]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MarkdownPreview;
|
export default MarkdownPreview;
|
||||||
|
@ -280,6 +280,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
...editableProps,
|
...editableProps,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
|
className: '!outline-none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div key="editor-inner-wrapper" ref={innerEditorContainerRef}>
|
<div key="editor-inner-wrapper" ref={innerEditorContainerRef}>
|
||||||
|
@ -40,7 +40,7 @@ const ShortcodeToolbarButton: FC<ShortcodeToolbarButtonProps> = ({ disabled }) =
|
|||||||
keepMounted
|
keepMounted
|
||||||
hideDropdownIcon
|
hideDropdownIcon
|
||||||
variant="text"
|
variant="text"
|
||||||
className="
|
buttonClassName="
|
||||||
py-0.5
|
py-0.5
|
||||||
px-0.5
|
px-0.5
|
||||||
h-6
|
h-6
|
||||||
|
@ -8,6 +8,7 @@ import { VFileMessage } from 'vfile-message';
|
|||||||
|
|
||||||
import useDebouncedCallback from '@staticcms/core/lib/hooks/useDebouncedCallback';
|
import useDebouncedCallback from '@staticcms/core/lib/hooks/useDebouncedCallback';
|
||||||
import flattenListItemParagraphs from '../serialization/slate/flattenListItemParagraphs';
|
import flattenListItemParagraphs from '../serialization/slate/flattenListItemParagraphs';
|
||||||
|
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||||
|
|
||||||
export interface UseMdxState {
|
export interface UseMdxState {
|
||||||
file: VFile | null;
|
file: VFile | null;
|
||||||
@ -49,10 +50,11 @@ export default function useMdx(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setValue = useDebouncedCallback(setValueCallback, 100);
|
const setValue = useDebouncedCallback(setValueCallback, 100);
|
||||||
|
const debouncedState = useDebounce(state, 150);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(input);
|
setValue(input);
|
||||||
}, [input, setValue]);
|
}, [input, setValue]);
|
||||||
|
|
||||||
return [state, setValue];
|
return [debouncedState, setValue];
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ export function getToolbarButtons(
|
|||||||
keepMounted
|
keepMounted
|
||||||
hideDropdownIcon
|
hideDropdownIcon
|
||||||
variant="text"
|
variant="text"
|
||||||
className="
|
buttonClassName="
|
||||||
py-0.5
|
py-0.5
|
||||||
px-0.5
|
px-0.5
|
||||||
h-6
|
h-6
|
||||||
|
@ -322,7 +322,7 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
|
|||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
{Array.isArray(selectedValue) && selectedValue.length > 0 ? (
|
{Array.isArray(selectedValue) && selectedValue.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-2 w-full p-2 pr-0 max-w-fit">
|
<div className="flex flex-wrap gap-2 p-2 pr-0 w-relation-widget-label">
|
||||||
{selectedValue.map(selectValue => {
|
{selectedValue.map(selectValue => {
|
||||||
const option = uniqueOptionsByValue[selectValue];
|
const option = uniqueOptionsByValue[selectValue];
|
||||||
return (
|
return (
|
||||||
|
@ -87,7 +87,14 @@ const UUIDControl: FC<WidgetControlProps<string, UUIDField>> = ({
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TextField type="text" inputRef={ref} value={internalValue} disabled={disabled} readonly />
|
<TextField
|
||||||
|
type="text"
|
||||||
|
inputRef={ref}
|
||||||
|
value={internalValue}
|
||||||
|
disabled={disabled}
|
||||||
|
readonly
|
||||||
|
inputClassName="truncate"
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,13 +4,14 @@ module.exports = {
|
|||||||
extend: {
|
extend: {
|
||||||
height: {
|
height: {
|
||||||
main: "calc(100vh - 64px)",
|
main: "calc(100vh - 64px)",
|
||||||
|
"main-mobile": "calc(100vh - 128px)",
|
||||||
"media-library-dialog": "80vh",
|
"media-library-dialog": "80vh",
|
||||||
"media-card": "240px",
|
"media-card": "240px",
|
||||||
"media-preview-image": "104px",
|
"media-preview-image": "104px",
|
||||||
"media-card-image": "196px",
|
"media-card-image": "196px",
|
||||||
"image-card": "120px",
|
"image-card": "120px",
|
||||||
input: "24px",
|
input: "24px",
|
||||||
"table-full": "calc(100% - 40px)"
|
"table-full": "calc(100% - 40px)",
|
||||||
},
|
},
|
||||||
minHeight: {
|
minHeight: {
|
||||||
8: "2rem",
|
8: "2rem",
|
||||||
@ -26,12 +27,20 @@ module.exports = {
|
|||||||
"media-card": "240px",
|
"media-card": "240px",
|
||||||
"media-preview-image": "126px",
|
"media-preview-image": "126px",
|
||||||
"image-card": "120px",
|
"image-card": "120px",
|
||||||
|
"card-grid": "calc(100% + 8px)",
|
||||||
|
"breadcrumb-title-small": "calc(100vw - 126px)",
|
||||||
|
"breadcrumb-title": "calc(100vw * .4)",
|
||||||
|
"collection-header": "calc(100% - 32px)",
|
||||||
|
"date-widget": "calc(100% - 58px)",
|
||||||
|
"relation-widget-label": "calc(100% - 32px)",
|
||||||
|
"select-widget-label": "calc(100% - 12px)",
|
||||||
},
|
},
|
||||||
maxWidth: {
|
maxWidth: {
|
||||||
"media-search": "400px",
|
"media-search": "400px",
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
sidebar: "0 10px 15px 18px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
|
sidebar: "0 10px 15px 18px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
|
||||||
|
"bottom-navigation": "0 -10px 10px -5px rgb(0 0 0 / 0.15)",
|
||||||
},
|
},
|
||||||
gridTemplateColumns: {
|
gridTemplateColumns: {
|
||||||
editor: "450px auto",
|
editor: "450px auto",
|
||||||
@ -42,5 +51,24 @@ module.exports = {
|
|||||||
sans: ["Inter var", "Inter", "ui-sans-serif", "system-ui", "sans-serif"],
|
sans: ["Inter var", "Inter", "ui-sans-serif", "system-ui", "sans-serif"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
screens: {
|
||||||
|
xs: "480px",
|
||||||
|
// => @media (min-width: 480px) { ... }
|
||||||
|
|
||||||
|
sm: "640px",
|
||||||
|
// => @media (min-width: 640px) { ... }
|
||||||
|
|
||||||
|
md: "768px",
|
||||||
|
// => @media (min-width: 768px) { ... }
|
||||||
|
|
||||||
|
lg: "1024px",
|
||||||
|
// => @media (min-width: 1024px) { ... }
|
||||||
|
|
||||||
|
xl: "1280px",
|
||||||
|
// => @media (min-width: 1280px) { ... }
|
||||||
|
|
||||||
|
"2xl": "1536px",
|
||||||
|
// => @media (min-width: 1536px) { ... }
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user