fix: improve media fetching on collection page, add pagination (#788)
This commit is contained in:
parent
e78ebbe65e
commit
92cc4575f8
@ -5,12 +5,12 @@ backend:
|
|||||||
|
|
||||||
media_folder: assets/upload
|
media_folder: assets/upload
|
||||||
public_folder: /assets/upload
|
public_folder: /assets/upload
|
||||||
|
media_library:
|
||||||
|
max_file_size: 100000000
|
||||||
collections:
|
collections:
|
||||||
- name: posts
|
- name: posts
|
||||||
label: Posts
|
label: Posts
|
||||||
label_singular: Post
|
label_singular: Post
|
||||||
media_folder: /assets/posts
|
|
||||||
public_folder: /assets/posts
|
|
||||||
description: >
|
description: >
|
||||||
The description is a great place for tone setting, high level information,
|
The description is a great place for tone setting, high level information,
|
||||||
and editing guidelines that are specific to a collection.
|
and editing guidelines that are specific to a collection.
|
||||||
|
@ -3,10 +3,12 @@ backend:
|
|||||||
branch: main
|
branch: main
|
||||||
repo: static-cms/static-cms-gitlab
|
repo: static-cms/static-cms-gitlab
|
||||||
auth_type: pkce
|
auth_type: pkce
|
||||||
app_id: 91cc479ec663625098d456850c4dc4943fd8462064ebd9693b330e66f9d8f11a
|
app_id: 589ccb30ce31da8ba08e0b1e10b681a10be108e144508c2712573c6e4d60707e
|
||||||
|
|
||||||
media_folder: assets/upload
|
media_folder: assets/upload
|
||||||
public_folder: /assets/upload
|
public_folder: /assets/upload
|
||||||
|
media_library:
|
||||||
|
max_file_size: 100000000
|
||||||
collections:
|
collections:
|
||||||
- name: posts
|
- name: posts
|
||||||
label: Posts
|
label: Posts
|
||||||
@ -17,12 +19,14 @@ collections:
|
|||||||
folder: _posts
|
folder: _posts
|
||||||
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
|
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
|
||||||
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
|
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
|
||||||
|
summary_fields:
|
||||||
|
- title
|
||||||
|
- date
|
||||||
|
- draft
|
||||||
sortable_fields:
|
sortable_fields:
|
||||||
fields:
|
fields:
|
||||||
- title
|
- title
|
||||||
- date
|
- date
|
||||||
default:
|
|
||||||
field: title
|
|
||||||
create: true
|
create: true
|
||||||
view_filters:
|
view_filters:
|
||||||
- label: Posts With Index
|
- label: Posts With Index
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="/static-cms-core.js"></script>
|
<script src="/static-cms-core.js"></script>
|
||||||
<script type="module" src="/index.js"></script>
|
<script type="module" src="/backends/gitlab/index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
197
packages/core/dev-test/backends/gitlab/index.js
Normal file
197
packages/core/dev-test/backends/gitlab/index.js
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
// Register all the things
|
||||||
|
CMS.init();
|
||||||
|
|
||||||
|
const PostPreview = ({ entry, widgetFor }) => {
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{},
|
||||||
|
h('div', { className: 'cover' }, h('h1', {}, entry.data.title), widgetFor('image')),
|
||||||
|
h('p', {}, h('small', {}, 'Written ' + entry.data.date)),
|
||||||
|
h('div', { className: 'text' }, widgetFor('body')),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PostDateFieldPreview = ({ value }) => {
|
||||||
|
const date = new Date(value);
|
||||||
|
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
|
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{},
|
||||||
|
`${date.getFullYear()}-${month < 10 ? `0${month}` : month}-${day < 10 ? `0${day}` : day}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PostDraftFieldPreview = ({ value }) => {
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
backgroundColor: value === true ? 'rgb(37 99 235)' : 'rgb(22 163 74)',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
padding: '2px 6px',
|
||||||
|
textAlign: 'center',
|
||||||
|
textDecoration: 'none',
|
||||||
|
display: 'inline-block',
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value === true ? 'Draft' : 'Published',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GeneralPreview = ({ widgetsFor, entry, collection }) => {
|
||||||
|
const title = entry.data.site_title;
|
||||||
|
const posts = entry.data.posts;
|
||||||
|
const thumb = posts && posts.thumb;
|
||||||
|
|
||||||
|
const thumbUrl = useMediaAsset(thumb, collection, undefined, entry);
|
||||||
|
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{},
|
||||||
|
h('h1', {}, title),
|
||||||
|
h(
|
||||||
|
'dl',
|
||||||
|
{},
|
||||||
|
h('dt', {}, 'Posts on Frontpage'),
|
||||||
|
h('dd', {}, widgetsFor('posts').widgets.front_limit ?? 0),
|
||||||
|
|
||||||
|
h('dt', {}, 'Default Author'),
|
||||||
|
h('dd', {}, widgetsFor('posts').data?.author ?? 'None'),
|
||||||
|
|
||||||
|
h('dt', {}, 'Default Thumbnail'),
|
||||||
|
h('dd', {}, thumb && h('img', { src: thumbUrl })),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthorsPreview = ({ widgetsFor }) => {
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{},
|
||||||
|
h('h1', {}, 'Authors'),
|
||||||
|
widgetsFor('authors').map(function (author, index) {
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{ key: index },
|
||||||
|
h('hr', {}),
|
||||||
|
h('strong', {}, author.data.name),
|
||||||
|
author.widgets.description,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RelationKitchenSinkPostPreview = ({ fieldsMetaData }) => {
|
||||||
|
// When a post is selected from the relation field, all of it's data
|
||||||
|
// will be available in the field's metadata nested under the collection
|
||||||
|
// name, and then further nested under the value specified in `value_field`.
|
||||||
|
// In this case, the post would be nested under "posts" and then under
|
||||||
|
// the title of the selected post, since our `value_field` in the config
|
||||||
|
// is "title".
|
||||||
|
const post = fieldsMetaData && fieldsMetaData.posts.value;
|
||||||
|
const style = { border: '2px solid #ccc', borderRadius: '8px', padding: '20px' };
|
||||||
|
return post
|
||||||
|
? h(
|
||||||
|
'div',
|
||||||
|
{ style: style },
|
||||||
|
h('h2', {}, 'Related Post'),
|
||||||
|
h('h3', {}, post.title),
|
||||||
|
h('img', { src: post.image }),
|
||||||
|
h('p', {}, (post.body ?? '').slice(0, 100) + '...'),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomPage = () => {
|
||||||
|
return h('div', {}, 'I am a custom page!');
|
||||||
|
};
|
||||||
|
|
||||||
|
CMS.registerPreviewTemplate('posts', PostPreview);
|
||||||
|
CMS.registerFieldPreview('posts', 'date', PostDateFieldPreview);
|
||||||
|
CMS.registerFieldPreview('posts', 'draft', PostDraftFieldPreview);
|
||||||
|
CMS.registerPreviewTemplate('general', GeneralPreview);
|
||||||
|
CMS.registerPreviewTemplate('authors', AuthorsPreview);
|
||||||
|
// Pass the name of a registered control to reuse with a new widget preview.
|
||||||
|
CMS.registerWidget('relationKitchenSinkPost', 'relation', RelationKitchenSinkPostPreview);
|
||||||
|
CMS.registerAdditionalLink({
|
||||||
|
id: 'example',
|
||||||
|
title: 'Example.com',
|
||||||
|
data: 'https://example.com',
|
||||||
|
options: {
|
||||||
|
icon: 'page',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
CMS.registerAdditionalLink({
|
||||||
|
id: 'custom-page',
|
||||||
|
title: 'Custom Page',
|
||||||
|
data: CustomPage,
|
||||||
|
options: {
|
||||||
|
icon: 'page',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
CMS.registerShortcode('youtube', {
|
||||||
|
label: 'YouTube',
|
||||||
|
openTag: '[',
|
||||||
|
closeTag: ']',
|
||||||
|
separator: '|',
|
||||||
|
toProps: args => {
|
||||||
|
if (args.length > 0) {
|
||||||
|
return { src: args[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { src: '' };
|
||||||
|
},
|
||||||
|
toArgs: ({ src }) => {
|
||||||
|
return [src];
|
||||||
|
},
|
||||||
|
control: ({ src, onChange, theme }) => {
|
||||||
|
return h('span', {}, [
|
||||||
|
h('input', {
|
||||||
|
key: 'control-input',
|
||||||
|
value: src,
|
||||||
|
onChange: event => {
|
||||||
|
onChange({ src: event.target.value });
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: theme === 'dark' ? 'rgb(30, 41, 59)' : 'white',
|
||||||
|
color: theme === 'dark' ? 'white' : 'black',
|
||||||
|
padding: '4px 8px',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
h(
|
||||||
|
'iframe',
|
||||||
|
{
|
||||||
|
key: 'control-preview',
|
||||||
|
width: '100%',
|
||||||
|
height: '315',
|
||||||
|
src: `https://www.youtube.com/embed/${src}`,
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
preview: ({ src }) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{},
|
||||||
|
h(
|
||||||
|
'iframe',
|
||||||
|
{
|
||||||
|
width: '420',
|
||||||
|
height: '315',
|
||||||
|
src: `https://www.youtube.com/embed/${src}`,
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
@ -146,6 +146,7 @@
|
|||||||
"react-router-dom": "6.10.0",
|
"react-router-dom": "6.10.0",
|
||||||
"react-scroll-sync": "0.11.0",
|
"react-scroll-sync": "0.11.0",
|
||||||
"react-topbar-progress-indicator": "4.1.1",
|
"react-topbar-progress-indicator": "4.1.1",
|
||||||
|
"react-virtual": "2.10.4",
|
||||||
"react-virtualized-auto-sizer": "1.0.15",
|
"react-virtualized-auto-sizer": "1.0.15",
|
||||||
"react-waypoint": "10.3.0",
|
"react-waypoint": "10.3.0",
|
||||||
"react-window": "1.8.9",
|
"react-window": "1.8.9",
|
||||||
|
@ -633,8 +633,7 @@ export async function tryLoadEntry(state: RootState, collection: Collection, slu
|
|||||||
}
|
}
|
||||||
|
|
||||||
const backend = currentBackend(configState.config);
|
const backend = currentBackend(configState.config);
|
||||||
const loadedEntry = await backend.getEntry(state, collection, slug);
|
return backend.getEntry(state, collection, slug);
|
||||||
return loadedEntry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppendAction {
|
interface AppendAction {
|
||||||
|
@ -46,6 +46,7 @@ const MainView = ({
|
|||||||
<div className="flex bg-slate-50 dark:bg-slate-900">
|
<div className="flex bg-slate-50 dark:bg-slate-900">
|
||||||
{showLeftNav ? <Sidebar /> : null}
|
{showLeftNav ? <Sidebar /> : null}
|
||||||
<div
|
<div
|
||||||
|
id="main-view"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
showLeftNav ? 'w-main left-64' : 'w-full',
|
showLeftNav ? 'w-main left-64' : 'w-full',
|
||||||
!noMargin && 'px-5 py-4',
|
!noMargin && 'px-5 py-4',
|
||||||
|
@ -42,7 +42,7 @@ const SingleCollectionPage: FC<SingleCollectionPageProps> = ({
|
|||||||
const breadcrumbs = useBreadcrumbs(collection, filterTerm);
|
const breadcrumbs = useBreadcrumbs(collection, filterTerm);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainView breadcrumbs={breadcrumbs} showQuickCreate showLeftNav>
|
<MainView breadcrumbs={breadcrumbs} showQuickCreate showLeftNav noScroll>
|
||||||
<CollectionView
|
<CollectionView
|
||||||
name={name}
|
name={name}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
@ -196,7 +196,7 @@ const CollectionView = ({
|
|||||||
const collectionDescription = collection?.description;
|
const collectionDescription = collection?.description;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
{isSearchResults ? (
|
{isSearchResults ? (
|
||||||
<>
|
<>
|
||||||
@ -226,7 +226,7 @@ const CollectionView = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{collectionDescription ? (
|
{collectionDescription ? (
|
||||||
<div className="flex flex-grow mb-4">
|
<div className="flex mb-4">
|
||||||
<Card className="flex-grow px-3.5 py-2.5 text-sm">{collectionDescription}</Card>
|
<Card className="flex-grow px-3.5 py-2.5 text-sm">{collectionDescription}</Card>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
|
|
||||||
import Loader from '@staticcms/core/components/common/progress/Loader';
|
import Loader from '@staticcms/core/components/common/progress/Loader';
|
||||||
@ -37,11 +37,14 @@ const Entries = ({
|
|||||||
page,
|
page,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: TranslatedProps<EntriesProps>) => {
|
}: TranslatedProps<EntriesProps>) => {
|
||||||
const loadingMessages = [
|
const loadingMessages = useMemo(
|
||||||
|
() => [
|
||||||
t('collection.entries.loadingEntries'),
|
t('collection.entries.loadingEntries'),
|
||||||
t('collection.entries.cachingEntries'),
|
t('collection.entries.cachingEntries'),
|
||||||
t('collection.entries.longerLoading'),
|
t('collection.entries.longerLoading'),
|
||||||
];
|
],
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
if (isFetching && page === undefined) {
|
if (isFetching && page === undefined) {
|
||||||
return <Loader>{loadingMessages}</Loader>;
|
return <Loader>{loadingMessages}</Loader>;
|
||||||
@ -49,9 +52,7 @@ const Entries = ({
|
|||||||
|
|
||||||
const hasEntries = (entries && entries.length > 0) || cursor?.actions?.has('append_next');
|
const hasEntries = (entries && entries.length > 0) || cursor?.actions?.has('append_next');
|
||||||
if (hasEntries) {
|
if (hasEntries) {
|
||||||
return (
|
return 'collection' in otherProps ? (
|
||||||
<>
|
|
||||||
{'collection' in otherProps ? (
|
|
||||||
<EntryListing
|
<EntryListing
|
||||||
key="collection-listing"
|
key="collection-listing"
|
||||||
collection={otherProps.collection}
|
collection={otherProps.collection}
|
||||||
@ -60,6 +61,7 @@ const Entries = ({
|
|||||||
cursor={cursor}
|
cursor={cursor}
|
||||||
handleCursorActions={handleCursorActions}
|
handleCursorActions={handleCursorActions}
|
||||||
page={page}
|
page={page}
|
||||||
|
isLoadingEntries={isFetching && page !== undefined && entries.length > 0}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EntryListing
|
<EntryListing
|
||||||
@ -70,12 +72,8 @@ const Entries = ({
|
|||||||
cursor={cursor}
|
cursor={cursor}
|
||||||
handleCursorActions={handleCursorActions}
|
handleCursorActions={handleCursorActions}
|
||||||
page={page}
|
page={page}
|
||||||
|
isLoadingEntries={isFetching && page !== undefined && entries.length > 0}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{isFetching && page !== undefined && entries.length > 0 ? (
|
|
||||||
<div>{t('collection.entries.loadingEntries')}</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { Info as InfoIcon } from '@styled-icons/material-outlined/Info';
|
import { Info as InfoIcon } from '@styled-icons/material-outlined/Info';
|
||||||
import get from 'lodash/get';
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { VIEW_STYLE_LIST } from '@staticcms/core/constants/views';
|
import { getPreviewCard } from '@staticcms/core/lib/registry';
|
||||||
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
|
||||||
import { getFieldPreview, getPreviewCard } from '@staticcms/core/lib/registry';
|
|
||||||
import { getEntryBackupKey } from '@staticcms/core/lib/util/backup.util';
|
import { getEntryBackupKey } from '@staticcms/core/lib/util/backup.util';
|
||||||
import {
|
import {
|
||||||
selectEntryCollectionTitle,
|
selectEntryCollectionTitle,
|
||||||
@ -12,7 +9,6 @@ import {
|
|||||||
selectTemplateName,
|
selectTemplateName,
|
||||||
} from '@staticcms/core/lib/util/collection.util';
|
} from '@staticcms/core/lib/util/collection.util';
|
||||||
import localForage from '@staticcms/core/lib/util/localForage';
|
import localForage from '@staticcms/core/lib/util/localForage';
|
||||||
import { isNullish } from '@staticcms/core/lib/util/null.util';
|
|
||||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||||
import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
@ -20,17 +16,13 @@ import Card from '../../common/card/Card';
|
|||||||
import CardActionArea from '../../common/card/CardActionArea';
|
import CardActionArea from '../../common/card/CardActionArea';
|
||||||
import CardContent from '../../common/card/CardContent';
|
import CardContent from '../../common/card/CardContent';
|
||||||
import CardMedia from '../../common/card/CardMedia';
|
import CardMedia from '../../common/card/CardMedia';
|
||||||
import TableCell from '../../common/table/TableCell';
|
|
||||||
import TableRow from '../../common/table/TableRow';
|
|
||||||
import useWidgetsFor from '../../common/widget/useWidgetsFor';
|
import useWidgetsFor from '../../common/widget/useWidgetsFor';
|
||||||
|
|
||||||
import type { ViewStyle } from '@staticcms/core/constants/views';
|
|
||||||
import type {
|
import type {
|
||||||
BackupEntry,
|
BackupEntry,
|
||||||
Collection,
|
Collection,
|
||||||
Entry,
|
Entry,
|
||||||
FileOrImageField,
|
FileOrImageField,
|
||||||
MediaField,
|
|
||||||
TranslatedProps,
|
TranslatedProps,
|
||||||
} from '@staticcms/core/interface';
|
} from '@staticcms/core/interface';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@ -39,18 +31,12 @@ export interface EntryCardProps {
|
|||||||
entry: Entry;
|
entry: Entry;
|
||||||
imageFieldName?: string | null | undefined;
|
imageFieldName?: string | null | undefined;
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
collectionLabel?: string;
|
|
||||||
viewStyle: ViewStyle;
|
|
||||||
summaryFields: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
||||||
collection,
|
collection,
|
||||||
entry,
|
entry,
|
||||||
collectionLabel,
|
|
||||||
viewStyle = VIEW_STYLE_LIST,
|
|
||||||
imageFieldName,
|
imageFieldName,
|
||||||
summaryFields,
|
|
||||||
t,
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
const entryData = entry.data;
|
const entryData = entry.data;
|
||||||
@ -83,7 +69,6 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
|||||||
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
|
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
|
||||||
|
|
||||||
const fields = selectFields(collection, entry.slug);
|
const fields = selectFields(collection, entry.slug);
|
||||||
const imageUrl = useMediaAsset(image, collection as Collection<MediaField>, imageField, entry);
|
|
||||||
|
|
||||||
const config = useAppSelector(selectConfig);
|
const config = useAppSelector(selectConfig);
|
||||||
|
|
||||||
@ -123,66 +108,6 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
|||||||
};
|
};
|
||||||
}, [collection.name, entry.slug]);
|
}, [collection.name, entry.slug]);
|
||||||
|
|
||||||
if (viewStyle === VIEW_STYLE_LIST) {
|
|
||||||
return (
|
|
||||||
<TableRow
|
|
||||||
className="
|
|
||||||
hover:bg-gray-200
|
|
||||||
dark:hover:bg-slate-700/70
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{collectionLabel ? (
|
|
||||||
<TableCell key="collectionLabel" to={path}>
|
|
||||||
{collectionLabel}
|
|
||||||
</TableCell>
|
|
||||||
) : null}
|
|
||||||
{summaryFields.map(fieldName => {
|
|
||||||
if (fieldName === 'summary') {
|
|
||||||
return (
|
|
||||||
<TableCell key={fieldName} to={path}>
|
|
||||||
{summary}
|
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const field = fields.find(f => f.name === fieldName);
|
|
||||||
const value = get(entry.data, fieldName);
|
|
||||||
const FieldPreviewComponent = getFieldPreview(templateName, fieldName);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableCell key={fieldName} to={path}>
|
|
||||||
{field && FieldPreviewComponent ? (
|
|
||||||
<FieldPreviewComponent
|
|
||||||
collection={collection}
|
|
||||||
field={field}
|
|
||||||
value={value}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
) : isNullish(value) ? (
|
|
||||||
''
|
|
||||||
) : (
|
|
||||||
String(value)
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<TableCell key="unsavedChanges" to={path} shrink>
|
|
||||||
{hasLocalBackup ? (
|
|
||||||
<InfoIcon
|
|
||||||
className="
|
|
||||||
w-5
|
|
||||||
h-5
|
|
||||||
text-blue-600
|
|
||||||
dark:text-blue-300
|
|
||||||
"
|
|
||||||
title={t('ui.localBackup.hasLocalBackup')}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PreviewCardComponent) {
|
if (PreviewCardComponent) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@ -202,9 +127,9 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className="h-full">
|
||||||
<CardActionArea to={path}>
|
<CardActionArea to={path}>
|
||||||
{image && imageField ? <CardMedia height="140" image={imageUrl} /> : null}
|
{image && imageField ? <CardMedia height="140" image={image} /> : null}
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div>{summary}</div>
|
<div>{summary}</div>
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { translate } from 'react-polyglot';
|
import { translate } from 'react-polyglot';
|
||||||
import { Waypoint } from 'react-waypoint';
|
|
||||||
|
|
||||||
|
import { VIEW_STYLE_TABLE } from '@staticcms/core/constants/views';
|
||||||
import { selectFields, selectInferredField } from '@staticcms/core/lib/util/collection.util';
|
import { selectFields, selectInferredField } from '@staticcms/core/lib/util/collection.util';
|
||||||
import { toTitleCaseFromKey } from '@staticcms/core/lib/util/string.util';
|
import { toTitleCaseFromKey } from '@staticcms/core/lib/util/string.util';
|
||||||
import Table from '../../common/table/Table';
|
import EntryListingGrid from './EntryListingGrid';
|
||||||
import EntryCard from './EntryCard';
|
import EntryListingTable from './EntryListingTable';
|
||||||
|
|
||||||
import type { ViewStyle } from '@staticcms/core/constants/views';
|
import type { ViewStyle } from '@staticcms/core/constants/views';
|
||||||
import type {
|
import type {
|
||||||
Collection,
|
Collection,
|
||||||
|
CollectionEntryData,
|
||||||
Collections,
|
Collections,
|
||||||
Entry,
|
Entry,
|
||||||
Field,
|
Field,
|
||||||
@ -22,6 +23,7 @@ export interface BaseEntryListingProps {
|
|||||||
entries: Entry[];
|
entries: Entry[];
|
||||||
viewStyle: ViewStyle;
|
viewStyle: ViewStyle;
|
||||||
cursor?: Cursor;
|
cursor?: Cursor;
|
||||||
|
isLoadingEntries: boolean;
|
||||||
handleCursorActions?: (action: string) => void;
|
handleCursorActions?: (action: string) => void;
|
||||||
page?: number;
|
page?: number;
|
||||||
}
|
}
|
||||||
@ -40,9 +42,9 @@ export type EntryListingProps =
|
|||||||
|
|
||||||
const EntryListing: FC<TranslatedProps<EntryListingProps>> = ({
|
const EntryListing: FC<TranslatedProps<EntryListingProps>> = ({
|
||||||
entries,
|
entries,
|
||||||
page,
|
|
||||||
cursor,
|
cursor,
|
||||||
viewStyle,
|
viewStyle,
|
||||||
|
isLoadingEntries,
|
||||||
handleCursorActions,
|
handleCursorActions,
|
||||||
t,
|
t,
|
||||||
...otherProps
|
...otherProps
|
||||||
@ -93,23 +95,22 @@ const EntryListing: FC<TranslatedProps<EntryListingProps>> = ({
|
|||||||
[otherProps],
|
[otherProps],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderedCards = useMemo(() => {
|
const entryData: CollectionEntryData[] = useMemo(() => {
|
||||||
if ('collection' in otherProps) {
|
if ('collection' in otherProps) {
|
||||||
const inferredFields = inferFields(otherProps.collection);
|
const inferredFields = inferFields(otherProps.collection);
|
||||||
return entries.map(entry => (
|
|
||||||
<EntryCard
|
return entries.map(entry => ({
|
||||||
collection={otherProps.collection}
|
collection: otherProps.collection,
|
||||||
imageFieldName={inferredFields.imageField}
|
imageFieldName: inferredFields.imageField,
|
||||||
viewStyle={viewStyle}
|
viewStyle,
|
||||||
entry={entry}
|
entry,
|
||||||
key={entry.slug}
|
key: entry.slug,
|
||||||
summaryFields={summaryFields}
|
summaryFields,
|
||||||
t={t}
|
}));
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries.map(entry => {
|
return entries
|
||||||
|
.map(entry => {
|
||||||
const collectionName = entry.collection;
|
const collectionName = entry.collection;
|
||||||
const collection = Object.values(otherProps.collections).find(
|
const collection = Object.values(otherProps.collections).find(
|
||||||
coll => coll.name === collectionName,
|
coll => coll.name === collectionName,
|
||||||
@ -117,20 +118,20 @@ const EntryListing: FC<TranslatedProps<EntryListingProps>> = ({
|
|||||||
|
|
||||||
const collectionLabel = !isSingleCollectionInList ? collection?.label : undefined;
|
const collectionLabel = !isSingleCollectionInList ? collection?.label : undefined;
|
||||||
const inferredFields = inferFields(collection);
|
const inferredFields = inferFields(collection);
|
||||||
return collection ? (
|
return collection
|
||||||
<EntryCard
|
? {
|
||||||
collection={collection}
|
collection,
|
||||||
entry={entry}
|
entry,
|
||||||
imageFieldName={inferredFields.imageField}
|
imageFieldName: inferredFields.imageField,
|
||||||
viewStyle={viewStyle}
|
viewStyle,
|
||||||
collectionLabel={collectionLabel}
|
collectionLabel,
|
||||||
key={entry.slug}
|
key: entry.slug,
|
||||||
summaryFields={summaryFields}
|
summaryFields,
|
||||||
t={t}
|
}
|
||||||
/>
|
: null;
|
||||||
) : null;
|
})
|
||||||
});
|
.filter(e => e) as CollectionEntryData[];
|
||||||
}, [entries, inferFields, isSingleCollectionInList, otherProps, summaryFields, t, viewStyle]);
|
}, [entries, inferFields, isSingleCollectionInList, otherProps, summaryFields, viewStyle]);
|
||||||
|
|
||||||
const summaryFieldHeaders = useMemo(() => {
|
const summaryFieldHeaders = useMemo(() => {
|
||||||
if ('collection' in otherProps) {
|
if ('collection' in otherProps) {
|
||||||
@ -149,38 +150,30 @@ const EntryListing: FC<TranslatedProps<EntryListingProps>> = ({
|
|||||||
return [];
|
return [];
|
||||||
}, [otherProps, summaryFields]);
|
}, [otherProps, summaryFields]);
|
||||||
|
|
||||||
if (viewStyle === 'VIEW_STYLE_LIST') {
|
if (viewStyle === VIEW_STYLE_TABLE) {
|
||||||
return (
|
return (
|
||||||
<>
|
<EntryListingTable
|
||||||
<Table
|
key="table"
|
||||||
columns={
|
entryData={entryData}
|
||||||
!isSingleCollectionInList
|
isSingleCollectionInList={isSingleCollectionInList}
|
||||||
? ['Collection', ...summaryFieldHeaders, '']
|
summaryFieldHeaders={summaryFieldHeaders}
|
||||||
: [...summaryFieldHeaders, '']
|
loadNext={handleLoadMore}
|
||||||
}
|
canLoadMore={Boolean(hasMore && handleLoadMore)}
|
||||||
>
|
isLoadingEntries={isLoadingEntries}
|
||||||
{renderedCards}
|
t={t}
|
||||||
</Table>
|
/>
|
||||||
{hasMore && handleLoadMore && <Waypoint key={page} onEnter={handleLoadMore} />}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<EntryListingGrid
|
||||||
className="
|
key="grid"
|
||||||
grid
|
entryData={entryData}
|
||||||
gap-4
|
onLoadMore={handleLoadMore}
|
||||||
sm:grid-cols-1
|
canLoadMore={Boolean(hasMore && handleLoadMore)}
|
||||||
md:grid-cols-1
|
isLoadingEntries={isLoadingEntries}
|
||||||
lg:grid-cols-2
|
t={t}
|
||||||
xl:grid-cols-3
|
/>
|
||||||
2xl:grid-cols-4
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{renderedCards}
|
|
||||||
{hasMore && handleLoadMore && <Waypoint key={page} onEnter={handleLoadMore} />}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,160 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
import { VariableSizeGrid as Grid } from 'react-window';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MEDIA_CARD_HEIGHT,
|
||||||
|
MEDIA_CARD_MARGIN,
|
||||||
|
MEDIA_CARD_WIDTH,
|
||||||
|
MEDIA_LIBRARY_PADDING,
|
||||||
|
} from '@staticcms/core/constants/mediaLibrary';
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import EntryCard from './EntryCard';
|
||||||
|
|
||||||
|
import type { CollectionEntryData } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import type { t } from 'react-polyglot';
|
||||||
|
import type { GridChildComponentProps } from 'react-window';
|
||||||
|
|
||||||
|
export interface EntryListingCardGridProps {
|
||||||
|
scrollContainerRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
|
entryData: CollectionEntryData[];
|
||||||
|
onScroll: () => void;
|
||||||
|
t: t;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardGridItemData {
|
||||||
|
columnCount: number;
|
||||||
|
entryData: CollectionEntryData[];
|
||||||
|
t: t;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardWrapper = ({
|
||||||
|
rowIndex,
|
||||||
|
columnIndex,
|
||||||
|
style,
|
||||||
|
data: { columnCount, entryData, t },
|
||||||
|
}: GridChildComponentProps<CardGridItemData>) => {
|
||||||
|
const left = useMemo(
|
||||||
|
() =>
|
||||||
|
parseFloat(
|
||||||
|
`${
|
||||||
|
typeof style.left === 'number'
|
||||||
|
? style.left ?? MEDIA_CARD_MARGIN * columnIndex
|
||||||
|
: style.left
|
||||||
|
}`,
|
||||||
|
),
|
||||||
|
[columnIndex, style.left],
|
||||||
|
);
|
||||||
|
|
||||||
|
const top = useMemo(
|
||||||
|
() => parseFloat(`${typeof style.top === 'number' ? style.top ?? 0 : style.top}`) + 4,
|
||||||
|
[style.top],
|
||||||
|
);
|
||||||
|
|
||||||
|
const index = rowIndex * columnCount + columnIndex;
|
||||||
|
if (index >= entryData.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = entryData[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
width: style.width,
|
||||||
|
height: style.height,
|
||||||
|
paddingRight: `${columnIndex + 1 === columnCount ? 0 : MEDIA_CARD_MARGIN}px`,
|
||||||
|
paddingBottom: `${MEDIA_CARD_MARGIN}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EntryCard
|
||||||
|
key={data.key}
|
||||||
|
collection={data.collection}
|
||||||
|
entry={data.entry}
|
||||||
|
imageFieldName={data.imageFieldName}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
|
||||||
|
entryData,
|
||||||
|
scrollContainerRef,
|
||||||
|
onScroll,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
const [version, setVersion] = useState(0);
|
||||||
|
|
||||||
|
const handleResize = useCallback(() => {
|
||||||
|
setVersion(oldVersion => oldVersion + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
<AutoSizer onResize={handleResize}>
|
||||||
|
{({ height = 0, width = 0 }) => {
|
||||||
|
const columnWidthWithGutter = MEDIA_CARD_WIDTH + MEDIA_CARD_MARGIN;
|
||||||
|
const rowHeightWithGutter = MEDIA_CARD_HEIGHT + MEDIA_CARD_MARGIN;
|
||||||
|
const columnCount = Math.floor(width / columnWidthWithGutter);
|
||||||
|
const nonGutterSpace = (width - MEDIA_CARD_MARGIN * columnCount) / width;
|
||||||
|
const columnWidth = (1 / columnCount) * nonGutterSpace;
|
||||||
|
|
||||||
|
const rowCount = Math.ceil(entryData.length / columnCount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={version}
|
||||||
|
className={classNames(
|
||||||
|
`
|
||||||
|
overflow-hidden
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
columnCount={columnCount}
|
||||||
|
columnWidth={index =>
|
||||||
|
index + 1 === columnCount
|
||||||
|
? width * columnWidth
|
||||||
|
: width * columnWidth + MEDIA_CARD_MARGIN
|
||||||
|
}
|
||||||
|
rowCount={rowCount}
|
||||||
|
rowHeight={() => rowHeightWithGutter}
|
||||||
|
width={width}
|
||||||
|
height={height - MEDIA_LIBRARY_PADDING}
|
||||||
|
itemData={
|
||||||
|
{
|
||||||
|
entryData,
|
||||||
|
columnCount,
|
||||||
|
t,
|
||||||
|
} as CardGridItemData
|
||||||
|
}
|
||||||
|
outerRef={scrollContainerRef}
|
||||||
|
onScroll={onScroll}
|
||||||
|
className={classNames(
|
||||||
|
`
|
||||||
|
overflow-hidden
|
||||||
|
overflow-y-auto
|
||||||
|
styled-scrollbars
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
style={{ position: 'unset' }}
|
||||||
|
>
|
||||||
|
{CardWrapper}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntryListingCardGrid;
|
@ -0,0 +1,90 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||||
|
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
||||||
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
|
import EntryListingCardGrid from './EntryListingCardGrid';
|
||||||
|
|
||||||
|
import type { CollectionEntryData } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import type { t } from 'react-polyglot';
|
||||||
|
|
||||||
|
export interface EntryListingGridProps {
|
||||||
|
entryData: CollectionEntryData[];
|
||||||
|
canLoadMore?: boolean;
|
||||||
|
isLoadingEntries: boolean;
|
||||||
|
onLoadMore: () => void;
|
||||||
|
t: t;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryListingGrid: FC<EntryListingGridProps> = ({
|
||||||
|
entryData,
|
||||||
|
canLoadMore,
|
||||||
|
isLoadingEntries,
|
||||||
|
onLoadMore,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
const gridContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const isFetching = useAppSelector(selectIsFetching);
|
||||||
|
|
||||||
|
const fetchMoreOnBottomReached = useCallback(
|
||||||
|
(scrollHeight?: number, scrollTop?: number, clientHeight?: number) => {
|
||||||
|
if (isNotNullish(scrollHeight) && isNotNullish(scrollTop) && isNotNullish(clientHeight)) {
|
||||||
|
//once the user has scrolled within 300px of the bottom of the table, fetch more data if there is any
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 300 && !isFetching && canLoadMore) {
|
||||||
|
onLoadMore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isFetching, canLoadMore, onLoadMore],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
const { scrollHeight, scrollTop, clientHeight } = gridContainerRef.current ?? {};
|
||||||
|
|
||||||
|
fetchMoreOnBottomReached(scrollHeight, scrollTop, clientHeight);
|
||||||
|
}, [fetchMoreOnBottomReached]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
handleScroll();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full overflow-hidden">
|
||||||
|
<div ref={gridContainerRef} className="relative h-full overflow-auto styled-scrollbars">
|
||||||
|
<EntryListingCardGrid
|
||||||
|
key="grid"
|
||||||
|
entryData={entryData}
|
||||||
|
scrollContainerRef={gridContainerRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isLoadingEntries ? (
|
||||||
|
<div
|
||||||
|
key="loading"
|
||||||
|
className="
|
||||||
|
absolute
|
||||||
|
inset-0
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
bg-slate-50/50
|
||||||
|
dark:bg-slate-900/50
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{t('collection.entries.loadingEntries')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntryListingGrid;
|
@ -0,0 +1,125 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useVirtual } from 'react-virtual';
|
||||||
|
|
||||||
|
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
|
||||||
|
import { selectIsFetching } from '@staticcms/core/reducers/selectors/globalUI';
|
||||||
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
|
import Table from '../../common/table/Table';
|
||||||
|
import EntryRow from './EntryRow';
|
||||||
|
|
||||||
|
import type { CollectionEntryData } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import type { t } from 'react-polyglot';
|
||||||
|
|
||||||
|
export interface EntryListingTableProps {
|
||||||
|
isSingleCollectionInList: boolean;
|
||||||
|
entryData: CollectionEntryData[];
|
||||||
|
summaryFieldHeaders: string[];
|
||||||
|
canLoadMore: boolean;
|
||||||
|
isLoadingEntries: boolean;
|
||||||
|
loadNext: () => void;
|
||||||
|
t: t;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryListingTable: FC<EntryListingTableProps> = ({
|
||||||
|
isSingleCollectionInList,
|
||||||
|
entryData,
|
||||||
|
summaryFieldHeaders,
|
||||||
|
canLoadMore,
|
||||||
|
isLoadingEntries,
|
||||||
|
loadNext,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
const isFetching = useAppSelector(selectIsFetching);
|
||||||
|
|
||||||
|
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const { virtualItems: virtualRows, totalSize } = useVirtual({
|
||||||
|
parentRef: tableContainerRef,
|
||||||
|
size: entryData.length,
|
||||||
|
overscan: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const paddingTop = useMemo(
|
||||||
|
() => (virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0),
|
||||||
|
[virtualRows],
|
||||||
|
);
|
||||||
|
const paddingBottom = useMemo(
|
||||||
|
() =>
|
||||||
|
virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0,
|
||||||
|
[totalSize, virtualRows],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchMoreOnBottomReached = useCallback(
|
||||||
|
(scrollHeight?: number, scrollTop?: number, clientHeight?: number) => {
|
||||||
|
if (isNotNullish(scrollHeight) && isNotNullish(scrollTop) && isNotNullish(clientHeight)) {
|
||||||
|
//once the user has scrolled within 300px of the bottom of the table, fetch more data if there is any
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 300 && !isFetching && canLoadMore) {
|
||||||
|
loadNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isFetching, canLoadMore, loadNext],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { scrollHeight, scrollTop, clientHeight } = tableContainerRef.current ?? {};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMoreOnBottomReached(scrollHeight, scrollTop, clientHeight);
|
||||||
|
}, [clientHeight, fetchMoreOnBottomReached, scrollHeight, scrollTop]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full overflow-hidden">
|
||||||
|
<div ref={tableContainerRef} className="relative h-full overflow-auto styled-scrollbars">
|
||||||
|
<Table
|
||||||
|
columns={
|
||||||
|
!isSingleCollectionInList
|
||||||
|
? ['Collection', ...summaryFieldHeaders, '']
|
||||||
|
: [...summaryFieldHeaders, '']
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{paddingTop > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td style={{ height: `${paddingTop}px` }} />
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{virtualRows.map(virtualRow => {
|
||||||
|
const data = entryData[virtualRow.index];
|
||||||
|
return (
|
||||||
|
<EntryRow
|
||||||
|
key={virtualRow.index}
|
||||||
|
collection={data.collection}
|
||||||
|
entry={data.entry}
|
||||||
|
summaryFields={data.summaryFields}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{paddingBottom > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td style={{ height: `${paddingBottom}px` }} />
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
{isLoadingEntries ? (
|
||||||
|
<div
|
||||||
|
key="loading"
|
||||||
|
className="
|
||||||
|
absolute
|
||||||
|
inset-0
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
justify-center
|
||||||
|
bg-slate-50/50
|
||||||
|
dark:bg-slate-900/50
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{t('collection.entries.loadingEntries')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntryListingTable;
|
136
packages/core/src/components/collections/entries/EntryRow.tsx
Normal file
136
packages/core/src/components/collections/entries/EntryRow.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { Info as InfoIcon } from '@styled-icons/material-outlined/Info';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { getFieldPreview } from '@staticcms/core/lib/registry';
|
||||||
|
import { getEntryBackupKey } from '@staticcms/core/lib/util/backup.util';
|
||||||
|
import {
|
||||||
|
selectEntryCollectionTitle,
|
||||||
|
selectFields,
|
||||||
|
selectTemplateName,
|
||||||
|
} from '@staticcms/core/lib/util/collection.util';
|
||||||
|
import localForage from '@staticcms/core/lib/util/localForage';
|
||||||
|
import { isNullish } from '@staticcms/core/lib/util/null.util';
|
||||||
|
import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
||||||
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
|
import TableCell from '../../common/table/TableCell';
|
||||||
|
import TableRow from '../../common/table/TableRow';
|
||||||
|
|
||||||
|
import type { BackupEntry, Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
export interface EntryRowProps {
|
||||||
|
entry: Entry;
|
||||||
|
collection: Collection;
|
||||||
|
collectionLabel?: string;
|
||||||
|
summaryFields: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EntryRow: FC<TranslatedProps<EntryRowProps>> = ({
|
||||||
|
collection,
|
||||||
|
entry,
|
||||||
|
collectionLabel,
|
||||||
|
summaryFields,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
const path = useMemo(
|
||||||
|
() => `/collections/${collection.name}/entries/${entry.slug}`,
|
||||||
|
[collection.name, entry.slug],
|
||||||
|
);
|
||||||
|
|
||||||
|
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
|
||||||
|
|
||||||
|
const fields = selectFields(collection, entry.slug);
|
||||||
|
|
||||||
|
const templateName = useMemo(
|
||||||
|
() => selectTemplateName(collection, entry.slug),
|
||||||
|
[collection, entry.slug],
|
||||||
|
);
|
||||||
|
|
||||||
|
const theme = useAppSelector(selectTheme);
|
||||||
|
|
||||||
|
const [hasLocalBackup, setHasLocalBackup] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
|
||||||
|
const checkLocalBackup = async () => {
|
||||||
|
const key = getEntryBackupKey(collection.name, entry.slug);
|
||||||
|
const backup = await localForage.getItem<BackupEntry>(key);
|
||||||
|
|
||||||
|
if (alive) {
|
||||||
|
setHasLocalBackup(Boolean(backup));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkLocalBackup();
|
||||||
|
|
||||||
|
// Check again after small delay to ensure we capture the draft just made from the editor
|
||||||
|
setTimeout(() => {
|
||||||
|
checkLocalBackup();
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, [collection.name, entry.slug]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
className="
|
||||||
|
hover:bg-gray-200
|
||||||
|
dark:hover:bg-slate-700/70
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{collectionLabel ? (
|
||||||
|
<TableCell key="collectionLabel" to={path}>
|
||||||
|
{collectionLabel}
|
||||||
|
</TableCell>
|
||||||
|
) : null}
|
||||||
|
{summaryFields.map(fieldName => {
|
||||||
|
if (fieldName === 'summary') {
|
||||||
|
return (
|
||||||
|
<TableCell key={fieldName} to={path}>
|
||||||
|
{summary}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = fields.find(f => f.name === fieldName);
|
||||||
|
const value = get(entry.data, fieldName);
|
||||||
|
const FieldPreviewComponent = getFieldPreview(templateName, fieldName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCell key={fieldName} to={path}>
|
||||||
|
{field && FieldPreviewComponent ? (
|
||||||
|
<FieldPreviewComponent
|
||||||
|
collection={collection}
|
||||||
|
field={field}
|
||||||
|
value={value}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
) : isNullish(value) ? (
|
||||||
|
''
|
||||||
|
) : (
|
||||||
|
String(value)
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<TableCell key="unsavedChanges" to={path} shrink>
|
||||||
|
{hasLocalBackup ? (
|
||||||
|
<InfoIcon
|
||||||
|
className="
|
||||||
|
w-5
|
||||||
|
h-5
|
||||||
|
text-blue-600
|
||||||
|
dark:text-blue-300
|
||||||
|
"
|
||||||
|
title={t('ui.localBackup.hasLocalBackup')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntryRow;
|
@ -20,6 +20,8 @@ const Card = ({ children, className }: CardProps) => {
|
|||||||
shadow-md
|
shadow-md
|
||||||
dark:bg-slate-800
|
dark:bg-slate-800
|
||||||
dark:border-gray-700/40
|
dark:border-gray-700/40
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
`,
|
`,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
@ -17,6 +17,8 @@ const CardActionArea = ({ to, children }: CardActionAreaProps) => {
|
|||||||
w-full
|
w-full
|
||||||
relative
|
relative
|
||||||
flex
|
flex
|
||||||
|
flex-col
|
||||||
|
rounded-lg
|
||||||
justify-start
|
justify-start
|
||||||
hover:bg-gray-200
|
hover:bg-gray-200
|
||||||
dark:hover:bg-slate-700/70
|
dark:hover:bg-slate-700/70
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import Image from '../image/Image';
|
||||||
|
|
||||||
interface CardMediaProps {
|
interface CardMediaProps {
|
||||||
image: string;
|
image: string;
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
@ -9,7 +11,7 @@ interface CardMediaProps {
|
|||||||
|
|
||||||
const CardMedia = ({ image, width, height, alt = '' }: CardMediaProps) => {
|
const CardMedia = ({ image, width, height, alt = '' }: CardMediaProps) => {
|
||||||
return (
|
return (
|
||||||
<img
|
<Image
|
||||||
className="rounded-t-lg bg-cover bg-no-repeat bg-center w-full object-cover"
|
className="rounded-t-lg bg-cover bg-no-repeat bg-center w-full object-cover"
|
||||||
style={{
|
style={{
|
||||||
width: width ? `${width}px` : undefined,
|
width: width ? `${width}px` : undefined,
|
||||||
|
@ -8,11 +8,13 @@ import { useAppSelector } from '@staticcms/core/store/hooks';
|
|||||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
|
||||||
import type { BaseField, Collection, MediaField, UnknownField } from '@staticcms/core/interface';
|
import type { BaseField, Collection, MediaField, UnknownField } from '@staticcms/core/interface';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
export interface ImageProps<EF extends BaseField> {
|
export interface ImageProps<EF extends BaseField> {
|
||||||
src?: string;
|
src?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
collection?: Collection<EF>;
|
collection?: Collection<EF>;
|
||||||
field?: MediaField;
|
field?: MediaField;
|
||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
@ -22,6 +24,7 @@ const Image = <EF extends BaseField = UnknownField>({
|
|||||||
src,
|
src,
|
||||||
alt,
|
alt,
|
||||||
className,
|
className,
|
||||||
|
style,
|
||||||
collection,
|
collection,
|
||||||
field,
|
field,
|
||||||
'data-testid': dataTestId,
|
'data-testid': dataTestId,
|
||||||
@ -52,6 +55,7 @@ const Image = <EF extends BaseField = UnknownField>({
|
|||||||
alt={alt}
|
alt={alt}
|
||||||
data-testid={dataTestId ?? 'image'}
|
data-testid={dataTestId ?? 'image'}
|
||||||
className={classNames('object-cover max-w-full overflow-hidden', className)}
|
className={classNames('object-cover max-w-full overflow-hidden', className)}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,12 +11,18 @@ interface TableCellProps {
|
|||||||
|
|
||||||
const TableCell = ({ columns, children }: TableCellProps) => {
|
const TableCell = ({ columns, children }: TableCellProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-x-auto shadow-md sm:rounded-lg border border-slate-200 dark:border-gray-700">
|
<div className="shadow-md">
|
||||||
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-300">
|
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-300">
|
||||||
<thead className="text-xs text-gray-700 bg-gray-100 dark:bg-slate-700 dark:text-gray-300">
|
<thead className="text-xs">
|
||||||
<tr>
|
<tr>
|
||||||
{columns.map((column, index) => (
|
{columns.map((column, index) => (
|
||||||
<TableHeaderCell key={index}>{column}</TableHeaderCell>
|
<TableHeaderCell
|
||||||
|
key={index}
|
||||||
|
isFirst={index === 0}
|
||||||
|
isLast={index + 1 === columns.length}
|
||||||
|
>
|
||||||
|
{column}
|
||||||
|
</TableHeaderCell>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -1,15 +1,46 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
interface TableHeaderCellProps {
|
interface TableHeaderCellProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
|
const TableHeaderCell = ({ children, isFirst, isLast }: TableHeaderCellProps) => {
|
||||||
return (
|
return (
|
||||||
<th scope="col" className="px-4 py-3 text-gray-500 dark:text-gray-400">
|
<th
|
||||||
{children}
|
scope="col"
|
||||||
|
className="
|
||||||
|
text-gray-500
|
||||||
|
bg-slate-50
|
||||||
|
dark:text-gray-400
|
||||||
|
dark:bg-slate-900
|
||||||
|
sticky
|
||||||
|
top-0
|
||||||
|
p-0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
`
|
||||||
|
bg-gray-100
|
||||||
|
border-slate-200
|
||||||
|
dark:bg-slate-700
|
||||||
|
dark:border-gray-700
|
||||||
|
px-4
|
||||||
|
py-3
|
||||||
|
`,
|
||||||
|
isFirst && 'rounded-tl-lg',
|
||||||
|
isLast && 'rounded-tr-lg',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{typeof children === 'string' && isEmpty(children) ? <> </> : children}
|
||||||
|
</div>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import { Grid as GridIcon } from '@styled-icons/bootstrap/Grid';
|
|||||||
import { TableRows as TableRowsIcon } from '@styled-icons/material-rounded/TableRows';
|
import { TableRows as TableRowsIcon } from '@styled-icons/material-rounded/TableRows';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { VIEW_STYLE_GRID, VIEW_STYLE_LIST } from '@staticcms/core/constants/views';
|
import { VIEW_STYLE_GRID, VIEW_STYLE_TABLE } from '@staticcms/core/constants/views';
|
||||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||||
import IconButton from '../button/IconButton';
|
import IconButton from '../button/IconButton';
|
||||||
|
|
||||||
@ -18,9 +18,9 @@ const ViewStyleControl = ({ viewStyle, onChangeViewStyle }: ViewStyleControlPros
|
|||||||
<div className="flex items-center gap-1.5 mr-1">
|
<div className="flex items-center gap-1.5 mr-1">
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="text"
|
variant="text"
|
||||||
className={classNames(viewStyle === VIEW_STYLE_LIST && 'text-blue-500 dark:text-blue-500')}
|
className={classNames(viewStyle === VIEW_STYLE_TABLE && 'text-blue-500 dark:text-blue-500')}
|
||||||
aria-label="table view"
|
aria-label="table view"
|
||||||
onClick={() => onChangeViewStyle(VIEW_STYLE_LIST)}
|
onClick={() => onChangeViewStyle(VIEW_STYLE_TABLE)}
|
||||||
>
|
>
|
||||||
<TableRowsIcon className="h-5 w-5" />
|
<TableRowsIcon className="h-5 w-5" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -10,13 +10,14 @@ import { Publish as PublishIcon } from '@styled-icons/material/Publish';
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
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 { 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 { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
|
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 confirm from '../common/confirm/Confirm';
|
|
||||||
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
|
||||||
import { deleteLocalBackup, loadEntry } from '@staticcms/core/actions/entries';
|
|
||||||
|
|
||||||
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';
|
||||||
@ -73,6 +74,7 @@ const EditorToolbar = ({
|
|||||||
);
|
);
|
||||||
const canDelete = useMemo(() => selectAllowDeletion(collection), [collection]);
|
const canDelete = useMemo(() => selectAllowDeletion(collection), [collection]);
|
||||||
const isPublished = useMemo(() => !isNewEntry && !hasChanged, [hasChanged, isNewEntry]);
|
const isPublished = useMemo(() => !isNewEntry && !hasChanged, [hasChanged, isNewEntry]);
|
||||||
|
const isLoading = useAppSelector(selectIsFetching);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -185,7 +187,7 @@ const EditorToolbar = ({
|
|||||||
<>
|
<>
|
||||||
<MenuItemButton
|
<MenuItemButton
|
||||||
onClick={togglePreview}
|
onClick={togglePreview}
|
||||||
disabled={i18nActive}
|
disabled={isLoading || i18nActive}
|
||||||
startIcon={EyeIcon}
|
startIcon={EyeIcon}
|
||||||
endIcon={previewActive && !i18nActive ? CheckIcon : undefined}
|
endIcon={previewActive && !i18nActive ? CheckIcon : undefined}
|
||||||
>
|
>
|
||||||
@ -193,7 +195,7 @@ const EditorToolbar = ({
|
|||||||
</MenuItemButton>
|
</MenuItemButton>
|
||||||
<MenuItemButton
|
<MenuItemButton
|
||||||
onClick={toggleScrollSync}
|
onClick={toggleScrollSync}
|
||||||
disabled={i18nActive || !previewActive}
|
disabled={isLoading || i18nActive || !previewActive}
|
||||||
startIcon={HeightIcon}
|
startIcon={HeightIcon}
|
||||||
endIcon={
|
endIcon={
|
||||||
scrollSyncActive && !(i18nActive || !previewActive) ? CheckIcon : undefined
|
scrollSyncActive && !(i18nActive || !previewActive) ? CheckIcon : undefined
|
||||||
@ -218,7 +220,7 @@ const EditorToolbar = ({
|
|||||||
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={menuItems.length == 1 && menuItems[0].length === 0}
|
disabled={isLoading || (menuItems.length == 1 && menuItems[0].length === 0)}
|
||||||
>
|
>
|
||||||
{menuItems.map((group, index) => (
|
{menuItems.map((group, index) => (
|
||||||
<MenuGroup key={`menu-group-${index}`}>{group}</MenuGroup>
|
<MenuGroup key={`menu-group-${index}`}>{group}</MenuGroup>
|
||||||
@ -229,14 +231,15 @@ const EditorToolbar = ({
|
|||||||
[
|
[
|
||||||
showI18nToggle,
|
showI18nToggle,
|
||||||
showPreviewToggle,
|
showPreviewToggle,
|
||||||
|
canDelete,
|
||||||
toggleI18n,
|
toggleI18n,
|
||||||
i18nActive,
|
i18nActive,
|
||||||
t,
|
t,
|
||||||
togglePreview,
|
togglePreview,
|
||||||
|
isLoading,
|
||||||
previewActive,
|
previewActive,
|
||||||
toggleScrollSync,
|
toggleScrollSync,
|
||||||
scrollSyncActive,
|
scrollSyncActive,
|
||||||
canDelete,
|
|
||||||
onDelete,
|
onDelete,
|
||||||
isPublished,
|
isPublished,
|
||||||
menuItems,
|
menuItems,
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import AJV from 'ajv';
|
import AJV from 'ajv';
|
||||||
import select from 'ajv-keywords/dist/keywords/select';
|
import ajvErrors from 'ajv-errors';
|
||||||
import uniqueItemProperties from 'ajv-keywords/dist/keywords/uniqueItemProperties';
|
|
||||||
import instanceOf from 'ajv-keywords/dist/keywords/instanceof';
|
import instanceOf from 'ajv-keywords/dist/keywords/instanceof';
|
||||||
import prohibited from 'ajv-keywords/dist/keywords/prohibited';
|
import prohibited from 'ajv-keywords/dist/keywords/prohibited';
|
||||||
import ajvErrors from 'ajv-errors';
|
import select from 'ajv-keywords/dist/keywords/select';
|
||||||
|
import uniqueItemProperties from 'ajv-keywords/dist/keywords/uniqueItemProperties';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { formatExtensions, frontmatterFormats, extensionFormatters } from '../formats/formats';
|
import { extensionFormatters, formatExtensions, frontmatterFormats } from '../formats/formats';
|
||||||
import { getWidgets } from '../lib/registry';
|
|
||||||
import {
|
import {
|
||||||
I18N_FIELD_DUPLICATE,
|
I18N_FIELD_DUPLICATE,
|
||||||
I18N_FIELD_NONE,
|
I18N_FIELD_NONE,
|
||||||
@ -16,6 +15,7 @@ import {
|
|||||||
I18N_STRUCTURE_MULTIPLE_FOLDERS,
|
I18N_STRUCTURE_MULTIPLE_FOLDERS,
|
||||||
I18N_STRUCTURE_SINGLE_FILE,
|
I18N_STRUCTURE_SINGLE_FILE,
|
||||||
} from '../lib/i18n';
|
} from '../lib/i18n';
|
||||||
|
import { getWidgets } from '../lib/registry';
|
||||||
|
|
||||||
import type { ErrorObject } from 'ajv';
|
import type { ErrorObject } from 'ajv';
|
||||||
import type { Config } from '../interface';
|
import type { Config } from '../interface';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const VIEW_STYLE_LIST = 'VIEW_STYLE_LIST';
|
export const VIEW_STYLE_TABLE = 'table';
|
||||||
export const VIEW_STYLE_GRID = 'VIEW_STYLE_GRID';
|
export const VIEW_STYLE_GRID = 'grid';
|
||||||
|
|
||||||
export type ViewStyle = typeof VIEW_STYLE_LIST | typeof VIEW_STYLE_GRID;
|
export type ViewStyle = typeof VIEW_STYLE_TABLE | typeof VIEW_STYLE_GRID;
|
||||||
|
@ -37,6 +37,7 @@ import type {
|
|||||||
STRIKETHROUGH_TOOLBAR_BUTTON,
|
STRIKETHROUGH_TOOLBAR_BUTTON,
|
||||||
UNORDERED_LIST_TOOLBAR_BUTTON,
|
UNORDERED_LIST_TOOLBAR_BUTTON,
|
||||||
} from './constants/toolbar_buttons';
|
} from './constants/toolbar_buttons';
|
||||||
|
import type { ViewStyle } from './constants/views';
|
||||||
import type { formatExtensions } from './formats/formats';
|
import type { formatExtensions } from './formats/formats';
|
||||||
import type {
|
import type {
|
||||||
I18N_FIELD_DUPLICATE,
|
I18N_FIELD_DUPLICATE,
|
||||||
@ -1087,3 +1088,13 @@ export interface BackupEntry {
|
|||||||
mediaFiles: MediaFile[];
|
mediaFiles: MediaFile[];
|
||||||
i18n?: Record<string, { raw: string }>;
|
i18n?: Record<string, { raw: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CollectionEntryData {
|
||||||
|
collection: Collection;
|
||||||
|
imageFieldName: string | null | undefined;
|
||||||
|
viewStyle: ViewStyle;
|
||||||
|
entry: Entry;
|
||||||
|
key: string;
|
||||||
|
summaryFields: string[];
|
||||||
|
collectionLabel?: string;
|
||||||
|
}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
SORT_ENTRIES_REQUEST,
|
SORT_ENTRIES_REQUEST,
|
||||||
SORT_ENTRIES_SUCCESS,
|
SORT_ENTRIES_SUCCESS,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { VIEW_STYLE_LIST } from '../constants/views';
|
import { VIEW_STYLE_TABLE } from '../constants/views';
|
||||||
import { set } from '../lib/util/object.util';
|
import { set } from '../lib/util/object.util';
|
||||||
|
|
||||||
import type { EntriesAction } from '../actions/entries';
|
import type { EntriesAction } from '../actions/entries';
|
||||||
@ -97,8 +97,8 @@ const loadViewStyle = once(() => {
|
|||||||
return viewStyle;
|
return viewStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(viewStyleKey, VIEW_STYLE_LIST);
|
localStorage.setItem(viewStyleKey, VIEW_STYLE_TABLE);
|
||||||
return VIEW_STYLE_LIST;
|
return VIEW_STYLE_TABLE;
|
||||||
});
|
});
|
||||||
|
|
||||||
function clearViewStyle() {
|
function clearViewStyle() {
|
||||||
|
@ -10,6 +10,7 @@ module.exports = {
|
|||||||
"media-card-image": "196px",
|
"media-card-image": "196px",
|
||||||
"image-card": "120px",
|
"image-card": "120px",
|
||||||
input: "24px",
|
input: "24px",
|
||||||
|
"table-full": "calc(100% - 40px)"
|
||||||
},
|
},
|
||||||
minHeight: {
|
minHeight: {
|
||||||
8: "2rem",
|
8: "2rem",
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -3594,6 +3594,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@reach/observe-rect@^1.1.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
|
||||||
|
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
|
||||||
|
|
||||||
"@react-dnd/asap@^5.0.1":
|
"@react-dnd/asap@^5.0.1":
|
||||||
version "5.0.2"
|
version "5.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
|
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
|
||||||
@ -16401,6 +16406,13 @@ react-use@^17.3.2:
|
|||||||
ts-easing "^0.2.0"
|
ts-easing "^0.2.0"
|
||||||
tslib "^2.1.0"
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
react-virtual@2.10.4:
|
||||||
|
version "2.10.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-virtual/-/react-virtual-2.10.4.tgz#08712f0acd79d7d6f7c4726f05651a13b24d8704"
|
||||||
|
integrity sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==
|
||||||
|
dependencies:
|
||||||
|
"@reach/observe-rect" "^1.1.0"
|
||||||
|
|
||||||
react-virtualized-auto-sizer@1.0.15:
|
react-virtualized-auto-sizer@1.0.15:
|
||||||
version "1.0.15"
|
version "1.0.15"
|
||||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.15.tgz#84558bcab61a625d13ec37876639bb09c5a3ec0b"
|
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.15.tgz#84558bcab61a625d13ec37876639bb09c5a3ec0b"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user