feat: add login and logout event, clean up collection page styles (#798)

This commit is contained in:
Daniel Lautzenheiser 2023-05-11 18:52:06 -04:00 committed by GitHub
parent a66068ca03
commit 80a5e11722
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 952 additions and 2887 deletions

View File

@ -387,6 +387,28 @@ collections:
- label: Description - label: Description
name: description name: description
widget: text widget: text
- name: list_with_object_child
label: List With Object Child
widget: list
fields:
- label: Name
name: name
widget: string
hint: First and Last
- label: Description
name: description
widget: text
- label: Object
name: object
widget: object
fields:
- label: Name
name: name
widget: string
hint: First and Last
- label: Description
name: description
widget: text
- name: string_list - name: string_list
label: String List label: String List
widget: list widget: list

View File

@ -184,16 +184,15 @@
var slug = dateString + '-post-number-' + i + '.md'; var slug = dateString + '-post-number-' + i + '.md';
window.repoFiles._posts[slug] = { window.repoFiles._posts[slug] = {
content: content: `---
'---\ntitle: "This is post # ' + title: "This is post # ${i}"
i + draft: ${i % 2 === 0}
`\"\ndraft: ${i % 2 === 0}` + image: /assets/uploads/lobby.jpg
'\nimage: /assets/uploads/lobby.jpg' + date: ${dateString}T00:00:00.000Z
'\ndate: ' + ---
dateString + # The post is number ${i}
'T00:00:00.000Z\n---\n# The post is number ' +
i + ![Static CMS](https://raw.githubusercontent.com/StaticJsCMS/static-cms/main/static-cms-logo.png)
`\n\n![Static CMS](https://raw.githubusercontent.com/StaticJsCMS/static-cms/main/static-cms-logo.png)
# Awesome Editor! # Awesome Editor!

View File

@ -11,15 +11,31 @@ const PostPreview = ({ entry, widgetFor }) => {
); );
}; };
const PostPreviewCard = ({ entry, theme, hasLocalBackup }) => { const PostPreviewCard = ({ entry, theme, hasLocalBackup, widgetFor }) => {
const date = new Date(entry.data.date); const date = new Date(entry.data.date);
const month = date.getMonth() + 1; const month = date.getMonth() + 1;
const day = date.getDate(); const day = date.getDate();
const image = entry.data.image;
return h( return h(
'div', 'div',
{ style: { width: '100%' } }, { style: { width: '100%' } },
h('div', {
style: {
width: '100%',
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px',
overflow: 'hidden',
height: '140px',
backgroundSize: 'cover',
backgroundRepat: 'no-repeat',
backgroundPosition: 'center',
objectFit: 'cover',
backgroundImage: `url('${image}')`,
},
}),
h( h(
'div', 'div',
{ style: { padding: '16px', width: '100%' } }, { style: { padding: '16px', width: '100%' } },
@ -52,7 +68,6 @@ const PostPreviewCard = ({ entry, theme, hasLocalBackup }) => {
fontSize: '14px', fontSize: '14px',
fontWeight: 700, fontWeight: 700,
color: 'rgb(107, 114, 128)', color: 'rgb(107, 114, 128)',
fontSize: '14px',
lineHeight: '18px', lineHeight: '18px',
}, },
}, },
@ -93,7 +108,7 @@ const PostPreviewCard = ({ entry, theme, hasLocalBackup }) => {
justifyContent: 'center', justifyContent: 'center',
textAlign: 'center', textAlign: 'center',
}, },
title: 'Has local backup' title: 'Has local backup',
}, },
'i', 'i',
) )
@ -151,6 +166,8 @@ const PostDraftFieldPreview = ({ value }) => {
cursor: 'pointer', cursor: 'pointer',
borderRadius: '4px', borderRadius: '4px',
fontSize: '14px', fontSize: '14px',
lineHeight: '16px',
height: '20px',
}, },
}, },
value === true ? 'Draft' : 'Published', value === true ? 'Draft' : 'Published',
@ -226,7 +243,7 @@ const CustomPage = () => {
}; };
CMS.registerPreviewTemplate('posts', PostPreview); CMS.registerPreviewTemplate('posts', PostPreview);
CMS.registerPreviewCard('posts', PostPreviewCard); CMS.registerPreviewCard('posts', PostPreviewCard, () => 240);
CMS.registerFieldPreview('posts', 'date', PostDateFieldPreview); CMS.registerFieldPreview('posts', 'date', PostDateFieldPreview);
CMS.registerFieldPreview('posts', 'draft', PostDraftFieldPreview); CMS.registerFieldPreview('posts', 'draft', PostDraftFieldPreview);
CMS.registerPreviewTemplate('general', GeneralPreview); CMS.registerPreviewTemplate('general', GeneralPreview);

View File

@ -150,15 +150,6 @@ export default class BitbucketBackend implements BackendClass {
return AuthenticationPage; return AuthenticationPage;
} }
setUser(user: { token: string }) {
this.token = user.token;
this.api = new API({
requestFunction: this.apiRequestFunction,
branch: this.branch,
repo: this.repo,
});
}
requestFunction = async (req: ApiRequest) => { requestFunction = async (req: ApiRequest) => {
const token = await this.getToken(); const token = await this.getToken();
const authorizedRequest = unsentRequest.withHeaders({ Authorization: `Bearer ${token}` }, req); const authorizedRequest = unsentRequest.withHeaders({ Authorization: `Bearer ${token}` }, req);

View File

@ -1,5 +1,5 @@
import { createTheme, ThemeProvider } from '@mui/material/styles'; import { createTheme, ThemeProvider } from '@mui/material/styles';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { translate } from 'react-polyglot'; import { translate } from 'react-polyglot';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
@ -185,6 +185,17 @@ const App = ({
} }
}, [dispatch]); }, [dispatch]);
const [prevUser, setPrevUser] = useState(user);
useEffect(() => {
if (!prevUser && user) {
invokeEvent('login', {
login: user.login,
name: user.name ?? '',
});
}
setPrevUser(user);
}, [prevUser, user]);
const content = useMemo(() => { const content = useMemo(() => {
if (!user) { if (!user) {
return authenticationPage; return authenticationPage;

View File

@ -13,7 +13,7 @@ const MultiSearchCollectionPage: FC = () => {
const filterTerm = params['*']; const filterTerm = params['*'];
return ( return (
<MainView breadcrumbs={[{ name: 'Search' }]} showQuickCreate showLeftNav> <MainView breadcrumbs={[{ name: 'Search' }]} showQuickCreate showLeftNav noScroll noMargin>
<CollectionView <CollectionView
name={name} name={name}
searchTerm={searchTerm} searchTerm={searchTerm}
@ -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 noScroll> <MainView breadcrumbs={breadcrumbs} showQuickCreate showLeftNav noScroll noMargin>
<CollectionView <CollectionView
name={name} name={name}
searchTerm={searchTerm} searchTerm={searchTerm}

View File

@ -196,7 +196,7 @@ const CollectionView = ({
const collectionDescription = collection?.description; const collectionDescription = collection?.description;
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full px-5 pt-4">
<div className="flex items-center mb-4"> <div className="flex items-center mb-4">
{isSearchResults ? ( {isSearchResults ? (
<> <>

View File

@ -79,7 +79,10 @@ const EntryCard: FC<TranslatedProps<EntryCardProps>> = ({
[collection, entry.slug], [collection, entry.slug],
); );
const PreviewCardComponent = useMemo(() => getPreviewCard(templateName) ?? null, [templateName]); const PreviewCardComponent = useMemo(
() => getPreviewCard(templateName)?.component ?? null,
[templateName],
);
const theme = useAppSelector(selectTheme); const theme = useAppSelector(selectTheme);

View File

@ -152,16 +152,18 @@ const EntryListing: FC<TranslatedProps<EntryListingProps>> = ({
if (viewStyle === VIEW_STYLE_TABLE) { if (viewStyle === VIEW_STYLE_TABLE) {
return ( return (
<EntryListingTable <div className="pb-3 overflow-hidden">
key="table" <EntryListingTable
entryData={entryData} key="table"
isSingleCollectionInList={isSingleCollectionInList} entryData={entryData}
summaryFieldHeaders={summaryFieldHeaders} isSingleCollectionInList={isSingleCollectionInList}
loadNext={handleLoadMore} summaryFieldHeaders={summaryFieldHeaders}
canLoadMore={Boolean(hasMore && handleLoadMore)} loadNext={handleLoadMore}
isLoadingEntries={isLoadingEntries} canLoadMore={Boolean(hasMore && handleLoadMore)}
t={t} isLoadingEntries={isLoadingEntries}
/> t={t}
/>
</div>
); );
} }

View File

@ -1,14 +1,17 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { VariableSizeGrid as Grid } from 'react-window'; import { VariableSizeGrid as Grid } from 'react-window';
import { import {
MEDIA_CARD_HEIGHT, COLLECTION_CARD_HEIGHT,
MEDIA_CARD_MARGIN, COLLECTION_CARD_HEIGHT_WITHOUT_IMAGE,
MEDIA_CARD_WIDTH, COLLECTION_CARD_MARGIN,
MEDIA_LIBRARY_PADDING, COLLECTION_CARD_WIDTH,
} from '@staticcms/core/constants/mediaLibrary'; } from '@staticcms/core/constants/views';
import { getPreviewCard } from '@staticcms/core/lib/registry';
import classNames from '@staticcms/core/lib/util/classNames.util'; import classNames from '@staticcms/core/lib/util/classNames.util';
import { selectTemplateName } from '@staticcms/core/lib/util/collection.util';
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
import EntryCard from './EntryCard'; import EntryCard from './EntryCard';
import type { CollectionEntryData } from '@staticcms/core/interface'; import type { CollectionEntryData } from '@staticcms/core/interface';
@ -40,7 +43,7 @@ const CardWrapper = ({
parseFloat( parseFloat(
`${ `${
typeof style.left === 'number' typeof style.left === 'number'
? style.left ?? MEDIA_CARD_MARGIN * columnIndex ? style.left ?? COLLECTION_CARD_MARGIN * columnIndex
: style.left : style.left
}`, }`,
), ),
@ -66,8 +69,8 @@ const CardWrapper = ({
top, top,
width: style.width, width: style.width,
height: style.height, height: style.height,
paddingRight: `${columnIndex + 1 === columnCount ? 0 : MEDIA_CARD_MARGIN}px`, paddingRight: `${columnIndex + 1 === columnCount ? 0 : COLLECTION_CARD_MARGIN}px`,
paddingBottom: `${MEDIA_CARD_MARGIN}px`, paddingBottom: `${COLLECTION_CARD_MARGIN}px`,
}} }}
> >
<EntryCard <EntryCard
@ -93,14 +96,51 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
setVersion(oldVersion => oldVersion + 1); setVersion(oldVersion => oldVersion + 1);
}, []); }, []);
const getHeightFn = useCallback((data: CollectionEntryData) => {
const templateName = selectTemplateName(data.collection, data.entry.slug);
return getPreviewCard(templateName)?.getHeight ?? null;
}, []);
const getDefaultHeight = useCallback((data?: CollectionEntryData) => {
return isNotNullish(data?.imageFieldName)
? COLLECTION_CARD_HEIGHT
: COLLECTION_CARD_HEIGHT_WITHOUT_IMAGE;
}, []);
const [prevCardHeights, setPrevCardHeight] = useState<number[]>([]);
const cardHeights: number[] = useMemo(() => {
const newCardHeights = [...prevCardHeights];
const startIndex = newCardHeights.length;
const endIndex = entryData.length;
for (let i = startIndex; i < endIndex; i++) {
const data = entryData[i];
const getHeight = getHeightFn(data);
if (getHeight) {
newCardHeights.push(getHeight({ collection: data.collection, entry: data.entry }));
continue;
}
newCardHeights.push(getDefaultHeight(data));
}
return newCardHeights;
}, [entryData, getDefaultHeight, getHeightFn, prevCardHeights]);
useEffect(() => {
if (cardHeights.length !== prevCardHeights.length) {
setPrevCardHeight(cardHeights);
}
}, [cardHeights, prevCardHeights.length]);
return ( return (
<div className="relative w-full h-full"> <div className="relative w-full h-full">
<AutoSizer onResize={handleResize}> <AutoSizer onResize={handleResize}>
{({ height = 0, width = 0 }) => { {({ height = 0, width = 0 }) => {
const columnWidthWithGutter = MEDIA_CARD_WIDTH + MEDIA_CARD_MARGIN; const columnWidthWithGutter = COLLECTION_CARD_WIDTH + COLLECTION_CARD_MARGIN;
const rowHeightWithGutter = MEDIA_CARD_HEIGHT + MEDIA_CARD_MARGIN;
const columnCount = Math.floor(width / columnWidthWithGutter); const columnCount = Math.floor(width / columnWidthWithGutter);
const nonGutterSpace = (width - MEDIA_CARD_MARGIN * columnCount) / width; const nonGutterSpace = (width - COLLECTION_CARD_MARGIN * columnCount) / width;
const columnWidth = (1 / columnCount) * nonGutterSpace; const columnWidth = (1 / columnCount) * nonGutterSpace;
const rowCount = Math.ceil(entryData.length / columnCount); const rowCount = Math.ceil(entryData.length / columnCount);
@ -123,12 +163,15 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
columnWidth={index => columnWidth={index =>
index + 1 === columnCount index + 1 === columnCount
? width * columnWidth ? width * columnWidth
: width * columnWidth + MEDIA_CARD_MARGIN : width * columnWidth + COLLECTION_CARD_MARGIN
} }
rowCount={rowCount} rowCount={rowCount}
rowHeight={() => rowHeightWithGutter} rowHeight={index =>
(cardHeights.length > index ? cardHeights[index] : getDefaultHeight()) +
COLLECTION_CARD_MARGIN
}
width={width} width={width}
height={height - MEDIA_LIBRARY_PADDING} height={height}
itemData={ itemData={
{ {
entryData, entryData,
@ -146,6 +189,7 @@ const EntryListingCardGrid: FC<EntryListingCardGridProps> = ({
`, `,
)} )}
style={{ position: 'unset' }} style={{ position: 'unset' }}
overscanRowCount={5}
> >
{CardWrapper} {CardWrapper}
</Grid> </Grid>

View File

@ -58,7 +58,7 @@ const EntryListingGrid: FC<EntryListingGridProps> = ({
return ( return (
<div className="relative h-full overflow-hidden"> <div className="relative h-full overflow-hidden">
<div ref={gridContainerRef} className="relative h-full overflow-auto styled-scrollbars"> <div ref={gridContainerRef} className="relative h-full overflow-hidden">
<EntryListingCardGrid <EntryListingCardGrid
key="grid" key="grid"
entryData={entryData} entryData={entryData}

View File

@ -69,8 +69,28 @@ const EntryListingTable: FC<EntryListingTableProps> = ({
}, [clientHeight, fetchMoreOnBottomReached, scrollHeight, scrollTop]); }, [clientHeight, fetchMoreOnBottomReached, scrollHeight, scrollTop]);
return ( return (
<div className="relative h-full overflow-hidden"> <div
<div ref={tableContainerRef} className="relative h-full overflow-auto styled-scrollbars"> className="
relative
max-h-full
h-full
overflow-hidden
p-1.5
bg-white
dark:bg-slate-800
rounded-xl
"
>
<div
ref={tableContainerRef}
className="
relative
h-full
overflow-auto
styled-scrollbars
styled-scrollbars-secondary
"
>
<Table <Table
columns={ columns={
!isSingleCollectionInList !isSingleCollectionInList

View File

@ -22,6 +22,7 @@ const Card = ({ children, className }: CardProps) => {
dark:border-gray-700/40 dark:border-gray-700/40
flex flex
flex-col flex-col
h-full
`, `,
className, className,
)} )}

View File

@ -11,18 +11,17 @@ interface TableCellProps {
const TableCell = ({ columns, children }: TableCellProps) => { const TableCell = ({ columns, children }: TableCellProps) => {
return ( return (
<div className="shadow-md"> <div
className="
shadow-md
z-[2]
"
>
<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"> <thead className="text-xs">
<tr> <tr>
{columns.map((column, index) => ( {columns.map((column, index) => (
<TableHeaderCell <TableHeaderCell key={index}>{column}</TableHeaderCell>
key={index}
isFirst={index === 0}
isLast={index + 1 === columns.length}
>
{column}
</TableHeaderCell>
))} ))}
</tr> </tr>
</thead> </thead>

View File

@ -38,12 +38,21 @@ const TableCell = ({ children, emphasis = false, to, shrink = false }: TableCell
<td <td
className={classNames( className={classNames(
!to ? 'px-4 py-3' : 'p-0', !to ? 'px-4 py-3' : 'p-0',
'text-gray-500 dark:text-gray-300', `
text-gray-500
dark:text-gray-300
`,
emphasis && 'font-medium text-gray-900 whitespace-nowrap dark:text-white', emphasis && 'font-medium text-gray-900 whitespace-nowrap dark:text-white',
shrink && 'w-0', shrink && 'w-0',
)} )}
> >
{content} <div
className="
h-[44px]
"
>
{content}
</div>
</td> </td>
); );
}; };

View File

@ -7,37 +7,36 @@ import type { ReactNode } from 'react';
interface TableHeaderCellProps { interface TableHeaderCellProps {
children: ReactNode; children: ReactNode;
isFirst: boolean;
isLast: boolean;
} }
const TableHeaderCell = ({ children, isFirst, isLast }: TableHeaderCellProps) => { const TableHeaderCell = ({ children }: TableHeaderCellProps) => {
return ( return (
<th <th
scope="col" scope="col"
className=" className={classNames(
text-gray-500 `
bg-slate-50 font-bold
dark:text-gray-400 sticky
dark:bg-slate-900 top-0
sticky border-0
top-0 p-0
p-0 `,
" )}
> >
<div <div
className={classNames( className="
` px-4
bg-gray-100 py-3
border-slate-200 text-gray-900
dark:bg-slate-700 border-gray-100
dark:border-gray-700 border-b
px-4 bg-white
py-3 dark:text-white
`, dark:border-gray-700
isFirst && 'rounded-tl-lg', dark:bg-slate-800
isLast && 'rounded-tr-lg', shadow-sm
)} text-[14px]
"
> >
{typeof children === 'string' && isEmpty(children) ? <>&nbsp;</> : children} {typeof children === 'string' && isEmpty(children) ? <>&nbsp;</> : children}
</div> </div>

View File

@ -14,11 +14,14 @@ const TableRow = ({ children, className }: TableRowProps) => {
<tr <tr
className={classNames( className={classNames(
` `
border-b border-t
first:border-t-0
border-gray-100
dark:border-gray-700
bg-white bg-white
hover:bg-slate-50 hover:bg-slate-50
dark:bg-slate-800 dark:bg-slate-800
dark:border-gray-700 dark:hover:bg-slate-700
`, `,
className, className,
)} )}

View File

@ -2,3 +2,9 @@ export const VIEW_STYLE_TABLE = 'table';
export const VIEW_STYLE_GRID = 'grid'; export const VIEW_STYLE_GRID = 'grid';
export type ViewStyle = typeof VIEW_STYLE_TABLE | typeof VIEW_STYLE_GRID; export type ViewStyle = typeof VIEW_STYLE_TABLE | typeof VIEW_STYLE_GRID;
export const COLLECTION_CARD_WIDTH = 240;
export const COLLECTION_CARD_HEIGHT = 204;
export const COLLECTION_CARD_IMAGE_HEIGHT = 140;
export const COLLECTION_CARD_HEIGHT_WITHOUT_IMAGE = 64;
export const COLLECTION_CARD_MARGIN = 10;

View File

@ -887,9 +887,14 @@ export interface BackendInitializer<EF extends BaseField = UnknownField> {
init: (config: Config<EF>, options: BackendInitializerOptions) => BackendClass; init: (config: Config<EF>, options: BackendInitializerOptions) => BackendClass;
} }
export interface AuthorData {
login: string | undefined;
name: string;
}
export interface EventData { export interface EventData {
entry: Entry; entry: Entry;
author: { login: string | undefined; name: string }; author: AuthorData;
} }
export type PreSaveEventHandler<O extends Record<string, unknown> = Record<string, unknown>> = ( export type PreSaveEventHandler<O extends Record<string, unknown> = Record<string, unknown>> = (
@ -906,10 +911,21 @@ export type MountedEventHandler<O extends Record<string, unknown> = Record<strin
options: O, options: O,
) => void | Promise<void>; ) => void | Promise<void>;
export type LoginEventHandler<O extends Record<string, unknown> = Record<string, unknown>> = (
data: AuthorData,
options: O,
) => void | Promise<void>;
export type LogoutEventHandler<O extends Record<string, unknown> = Record<string, unknown>> = (
options: O,
) => void | Promise<void>;
export type EventHandlers<O extends Record<string, unknown> = Record<string, unknown>> = { export type EventHandlers<O extends Record<string, unknown> = Record<string, unknown>> = {
preSave: PreSaveEventHandler<O>; preSave: PreSaveEventHandler<O>;
postSave: PostSaveEventHandler<O>; postSave: PostSaveEventHandler<O>;
mounted: MountedEventHandler<O>; mounted: MountedEventHandler<O>;
login: LoginEventHandler<O>;
logout: LogoutEventHandler<O>;
}; };
export interface EventListener< export interface EventListener<

View File

@ -96,6 +96,10 @@ export function slugFormatter<EF extends BaseField = UnknownField>(
entryData: EntryData, entryData: EntryData,
slugConfig?: Slug, slugConfig?: Slug,
): string { ): string {
if (!('fields' in collection)) {
return '';
}
const slugTemplate = collection.slug || '{{slug}}'; const slugTemplate = collection.slug || '{{slug}}';
const identifierField = selectIdentifier(collection); const identifierField = selectIdentifier(collection);
@ -152,12 +156,12 @@ export function summaryFormatter<EF extends BaseField>(
export function folderFormatter<EF extends BaseField>( export function folderFormatter<EF extends BaseField>(
folderTemplate: string, folderTemplate: string,
entry: Entry | null | undefined, entry: Entry | null | undefined,
collection: Collection<EF>, collection: Collection<EF> | undefined,
defaultFolder: string, defaultFolder: string,
folderKey: string, folderKey: string,
slugConfig?: Slug, slugConfig?: Slug,
) { ) {
if (!entry || !entry.data) { if (!entry || !entry.data || !collection) {
return folderTemplate; return folderTemplate;
} }

View File

@ -2,10 +2,12 @@ import { oneLine } from 'common-tags';
import type { import type {
AdditionalLink, AdditionalLink,
AuthorData,
BackendClass, BackendClass,
BackendInitializer, BackendInitializer,
BackendInitializerOptions, BackendInitializerOptions,
BaseField, BaseField,
Collection,
Config, Config,
CustomIcon, CustomIcon,
Entry, Entry,
@ -14,6 +16,8 @@ import type {
EventListener, EventListener,
FieldPreviewComponent, FieldPreviewComponent,
LocalePhrasesRoot, LocalePhrasesRoot,
LoginEventHandler,
LogoutEventHandler,
MountedEventHandler, MountedEventHandler,
ObjectValue, ObjectValue,
PostSaveEventHandler, PostSaveEventHandler,
@ -30,13 +34,15 @@ import type {
WidgetValueSerializer, WidgetValueSerializer,
} from '../interface'; } from '../interface';
export const allowedEvents = ['mounted', 'preSave', 'postSave'] as const; export const allowedEvents = ['mounted', 'login', 'logout', 'preSave', 'postSave'] as const;
export type AllowedEvent = (typeof allowedEvents)[number]; export type AllowedEvent = (typeof allowedEvents)[number];
type EventHandlerRegistry = { type EventHandlerRegistry = {
preSave: { handler: PreSaveEventHandler; options: Record<string, unknown> }[]; preSave: { handler: PreSaveEventHandler; options: Record<string, unknown> }[];
postSave: { handler: PostSaveEventHandler; options: Record<string, unknown> }[]; postSave: { handler: PostSaveEventHandler; options: Record<string, unknown> }[];
mounted: { handler: MountedEventHandler; options: Record<string, unknown> }[]; mounted: { handler: MountedEventHandler; options: Record<string, unknown> }[];
login: { handler: LoginEventHandler; options: Record<string, unknown> }[];
logout: { handler: LogoutEventHandler; options: Record<string, unknown> }[];
}; };
const eventHandlers = allowedEvents.reduce((acc, e) => { const eventHandlers = allowedEvents.reduce((acc, e) => {
@ -44,10 +50,15 @@ const eventHandlers = allowedEvents.reduce((acc, e) => {
return acc; return acc;
}, {} as EventHandlerRegistry); }, {} as EventHandlerRegistry);
interface CardPreviews {
component: TemplatePreviewCardComponent<ObjectValue>;
getHeight?: (data: { collection: Collection; entry: Entry }) => number;
}
interface Registry { interface Registry {
backends: Record<string, BackendInitializer>; backends: Record<string, BackendInitializer>;
templates: Record<string, TemplatePreviewComponent<ObjectValue>>; templates: Record<string, TemplatePreviewComponent<ObjectValue>>;
cards: Record<string, TemplatePreviewCardComponent<ObjectValue>>; cards: Record<string, CardPreviews>;
fieldPreviews: Record<string, Record<string, FieldPreviewComponent>>; fieldPreviews: Record<string, Record<string, FieldPreviewComponent>>;
widgets: Record<string, Widget>; widgets: Record<string, Widget>;
icons: Record<string, CustomIcon>; icons: Record<string, CustomIcon>;
@ -145,11 +156,15 @@ export function getPreviewTemplate(name: string): TemplatePreviewComponent<Objec
export function registerPreviewCard<T, EF extends BaseField = UnknownField>( export function registerPreviewCard<T, EF extends BaseField = UnknownField>(
name: string, name: string,
component: TemplatePreviewCardComponent<T, EF>, component: TemplatePreviewCardComponent<T, EF>,
getHeight?: () => number,
) { ) {
registry.cards[name] = component as TemplatePreviewCardComponent<ObjectValue>; registry.cards[name] = {
component: component as TemplatePreviewCardComponent<ObjectValue>,
getHeight,
};
} }
export function getPreviewCard(name: string): TemplatePreviewCardComponent<ObjectValue> | null { export function getPreviewCard(name: string): CardPreviews | null {
return registry.cards[name] ?? null; return registry.cards[name] ?? null;
} }
@ -336,19 +351,27 @@ export function registerEventListener<
>({ name, handler }: EventListener<E, O>, options?: O) { >({ name, handler }: EventListener<E, O>, options?: O) {
validateEventName(name); validateEventName(name);
registry.eventHandlers[name].push({ registry.eventHandlers[name].push({
handler: handler as MountedEventHandler & PreSaveEventHandler & PostSaveEventHandler, handler: handler as MountedEventHandler &
LoginEventHandler &
PreSaveEventHandler &
PostSaveEventHandler,
options: options ?? {}, options: options ?? {},
}); });
} }
export async function invokeEvent(name: 'login', data: AuthorData): Promise<void>;
export async function invokeEvent(name: 'logout'): Promise<void>;
export async function invokeEvent(name: 'preSave', data: EventData): Promise<EntryData>; export async function invokeEvent(name: 'preSave', data: EventData): Promise<EntryData>;
export async function invokeEvent(name: 'postSave', data: EventData): Promise<void>; export async function invokeEvent(name: 'postSave', data: EventData): Promise<void>;
export async function invokeEvent(name: 'mounted'): Promise<void>; export async function invokeEvent(name: 'mounted'): Promise<void>;
export async function invokeEvent(name: AllowedEvent, data?: EventData): Promise<void | EntryData> { export async function invokeEvent(
name: AllowedEvent,
data?: EventData | AuthorData,
): Promise<void | EntryData> {
validateEventName(name); validateEventName(name);
if (name === 'mounted') { if (name === 'mounted' || name === 'logout') {
console.info('[StaticCMS] Firing mounted event'); console.info(`[StaticCMS] Firing ${name} event`);
const handlers = registry.eventHandlers[name]; const handlers = registry.eventHandlers[name];
for (const { handler, options } of handlers) { for (const { handler, options } of handlers) {
handler(options); handler(options);
@ -357,11 +380,21 @@ export async function invokeEvent(name: AllowedEvent, data?: EventData): Promise
return; return;
} }
if (name === 'login') {
console.info('[StaticCMS] Firing login event', data);
const handlers = registry.eventHandlers[name];
for (const { handler, options } of handlers) {
handler(data as AuthorData, options);
}
return;
}
if (name === 'postSave') { if (name === 'postSave') {
console.info(`[StaticCMS] Firing post save event`, data); console.info(`[StaticCMS] Firing post save event`, data);
const handlers = registry.eventHandlers[name]; const handlers = registry.eventHandlers[name];
for (const { handler, options } of handlers) { for (const { handler, options } of handlers) {
handler(data!, options); handler(data as EventData, options);
} }
return; return;
@ -371,7 +404,7 @@ export async function invokeEvent(name: AllowedEvent, data?: EventData): Promise
console.info(`[StaticCMS] Firing pre save event`, data); console.info(`[StaticCMS] Firing pre save event`, data);
let _data = { ...data! }; let _data = { ...(data as EventData) };
for (const { handler, options } of handlers) { for (const { handler, options } of handlers) {
const result = await handler(_data, options); const result = await handler(_data, options);
if (_data !== undefined && result !== undefined) { if (_data !== undefined && result !== undefined) {

View File

@ -360,7 +360,7 @@
} }
.dark .styled-scrollbars.styled-scrollbars-secondary { .dark .styled-scrollbars.styled-scrollbars-secondary {
--scrollbar-foreground: rgba(47, 64, 93, 0.8); --scrollbar-foreground: rgba(47, 64, 84, 0.8);
--scrollbar-background: rgb(30 41 59); --scrollbar-background: rgb(30 41 59);
} }
@ -383,3 +383,28 @@
/* Background */ /* Background */
background: var(--scrollbar-background); background: var(--scrollbar-background);
} }
table {
tbody {
tr {
&:last-child {
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
td {
&:first-child {
& > div {
border-bottom-left-radius: 8px;
}
}
&:last-child {
& > div {
border-bottom-right-radius: 8px;
}
}
}
}
}
}
}

View File

@ -60,7 +60,6 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
parentHidden={hidden} parentHidden={hidden}
locale={locale} locale={locale}
i18n={i18n} i18n={i18n}
forList={forList}
forSingleList={forSingleList} forSingleList={forSingleList}
/> />
); );

View File

@ -1,3 +0,0 @@
const { removeModuleScopePlugin } = require("customize-cra");
module.exports = removeModuleScopePlugin();

View File

@ -183,15 +183,15 @@
var slug = dateString + "-post-number-" + i + ".md"; var slug = dateString + "-post-number-" + i + ".md";
window.repoFiles._posts[slug] = { window.repoFiles._posts[slug] = {
content: content: `---
'---\ntitle: "This is post # ' + title: "This is post # ${i}"
i + draft: ${i % 2 === 0}
`\"\ndraft: ${i % 2 === 0}` + image: /assets/uploads/lobby.jpg
"\ndate: " + date: ${dateString}T00:00:00.000Z
dateString + ---
"T00:00:00.000Z\n---\n# The post is number " + # The post is number ${i}
i +
`\n\n![Static CMS](https://raw.githubusercontent.com/StaticJsCMS/static-cms/main/static-cms-logo.png) ![Static CMS](https://raw.githubusercontent.com/StaticJsCMS/static-cms/main/static-cms-logo.png)
# Awesome Editor! # Awesome Editor!
@ -312,5 +312,6 @@ widget: 'markdown',
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body> </body>
</html> </html>

View File

@ -3,8 +3,9 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "react-app-rewired start", "dev": "vite",
"build": "react-app-rewired build" "build": "vite build",
"serve": "vite preview"
}, },
"dependencies": { "dependencies": {
"@babel/eslint-parser": "7.21.3", "@babel/eslint-parser": "7.21.3",
@ -18,9 +19,8 @@
"@babel/core": "7.21.4", "@babel/core": "7.21.4",
"@babel/plugin-syntax-flow": "7.21.4", "@babel/plugin-syntax-flow": "7.21.4",
"@babel/plugin-transform-react-jsx": "7.21.0", "@babel/plugin-transform-react-jsx": "7.21.0",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"copy-webpack-plugin": "11.0.0",
"customize-cra": "1.0.0",
"eslint": "8.39.0", "eslint": "8.39.0",
"eslint-import-resolver-typescript": "3.5.5", "eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-cypress": "2.13.2", "eslint-plugin-cypress": "2.13.2",
@ -32,9 +32,8 @@
"postcss": "8.4.23", "postcss": "8.4.23",
"postcss-scss": "4.0.6", "postcss-scss": "4.0.6",
"prettier": "2.8.8", "prettier": "2.8.8",
"react-app-rewired": "2.2.1", "vite": "4.3.5",
"react-scripts": "5.0.1", "vite-plugin-svgr": "3.2.0",
"typescript": "5.0.4",
"webpack": "5.80.0" "webpack": "5.80.0"
}, },
"browserslist": { "browserslist": {

View File

@ -1,5 +1,7 @@
import cms, { useMediaAsset } from "@staticcms/core"; import cms, { useMediaAsset } from "@staticcms/core";
import "@staticcms/core/dist/main.css";
// Register all the things // Register all the things
cms.init(); cms.init();
@ -18,81 +20,111 @@ const PostPreview = ({ entry, widgetFor }) => {
); );
}; };
const PostPreviewCard = ({ entry, theme }) => { const PostPreviewCard = ({ entry, theme, hasLocalBackup }) => {
const date = new Date(entry.data.date); const date = new Date(entry.data.date);
const month = date.getMonth() + 1; const month = date.getMonth() + 1;
const day = date.getDate(); const day = date.getDate();
return h( const image = entry.data.image;
'div',
{ style: { width: '100%' } }, return (
h( <div style={{ width: "100%" }}>
'div', <div
{ style: { padding: '16px', width: '100%' } }, style={{
h( width: "100%",
'div', borderTopLeftRadius: "8px",
{ borderTopRightRadius: "8px",
style: { overflow: "hidden",
display: 'flex', height: "140px",
width: '100%', backgroundSize: "cover",
justifyContent: 'space-between', backgroundRepat: "no-repeat",
alignItems: 'start', backgroundPosition: "center",
gap: '4px', objectFit: "cover",
color: theme === 'dark' ? 'white' : 'inherit', backgroundImage: `url('${image}')`,
}, }}
}, />
h( <div style={{ padding: "16px", width: "100%" }}>
'div', <div
{ style={{
style: { display: "flex",
display: 'flex', width: "100%",
flexDirection: 'column', justifyContent: "space-between",
alignItems: 'baseline', alignItems: "start",
gap: '4px', gap: "4px",
}, color: theme === "dark" ? "white" : "inherit",
}, }}
h( >
'div', <div
{ style={{
style: { display: "flex",
fontSize: '14px', flexDirection: "column",
alignItems: "baseline",
gap: "4px",
}}
>
<div
style={{
fontSize: "14px",
fontWeight: 700, fontWeight: 700,
color: 'rgb(107, 114, 128)', color: "rgb(107, 114, 128)",
fontSize: '14px', lineHeight: "18px",
lineHeight: '18px', }}
}, >
}, {entry.data.title}
entry.data.title, </div>
), <span style={{ fontSize: "14px" }}>{`${date.getFullYear()}-${month < 10 ? `0${month}` : month}-${
h(
'span',
{ style: { fontSize: '14px' } },
`${date.getFullYear()}-${month < 10 ? `0${month}` : month}-${
day < 10 ? `0${day}` : day day < 10 ? `0${day}` : day
}`, }`}</span>
), </div>
), <div
h( style={{
'div', display: "flex",
{ alignItems: "center",
style: { whiteSpace: "no-wrap",
backgroundColor: entry.data.draft === true ? 'blue' : 'green', gap: "8px",
color: 'white', }}
border: 'none', >
padding: '2px 6px', {hasLocalBackup ? (
textAlign: 'center', <div
textDecoration: 'none', style={{
display: 'inline-block', border: "2px solid rgb(147, 197, 253)",
cursor: 'pointer', borderRadius: "50%",
borderRadius: '4px', color: "rgb(147, 197, 253)",
fontSize: '14px', height: "18px",
}, width: "18px",
}, fontWeight: "bold",
entry.data.draft === true ? 'Draft' : 'Published', fontSize: "11px",
), display: "flex",
), alignItems: "center",
), justifyContent: "center",
textAlign: "center",
}}
title="Has local backup"
>
i
</div>
) : null}
<div
style={{
backgroundColor: entry.data.draft === 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",
}}
>
{entry.data.draft === true ? "Draft" : "Published"}
</div>
</div>
</div>
</div>
</div>
); );
}; };
@ -102,31 +134,29 @@ const PostDateFieldPreview = ({ value }) => {
const month = date.getMonth() + 1; const month = date.getMonth() + 1;
const day = date.getDate(); const day = date.getDate();
return h( return <div>{`${date.getFullYear()}-${month < 10 ? `0${month}` : month}-${day < 10 ? `0${day}` : day}`}</div>;
'div',
{},
`${date.getFullYear()}-${month < 10 ? `0${month}` : month}-${day < 10 ? `0${day}` : day}`,
);
}; };
const PostDraftFieldPreview = ({ value }) => { const PostDraftFieldPreview = ({ value }) => {
return h( return (
'div', <div
{ style={{
style: { backgroundColor: value === true ? "rgb(37 99 235)" : "rgb(22 163 74)",
backgroundColor: value === true ? 'rgb(37 99 235)' : 'rgb(22 163 74)', color: "white",
color: 'white', border: "none",
border: 'none', padding: "2px 6px",
padding: '2px 6px', textAlign: "center",
textAlign: 'center', textDecoration: "none",
textDecoration: 'none', display: "inline-block",
display: 'inline-block', cursor: "pointer",
cursor: 'pointer', borderRadius: "4px",
borderRadius: '4px', fontSize: "14px",
fontSize: '14px', lineHeight: "16px",
}, height: "20px",
}, }}
value === true ? 'Draft' : 'Published', >
{value === true ? "Draft" : "Published"}
</div>
); );
}; };
@ -199,9 +229,9 @@ const CustomPage = () => {
}; };
cms.registerPreviewTemplate("posts", PostPreview); cms.registerPreviewTemplate("posts", PostPreview);
CMS.registerPreviewCard("posts", PostPreviewCard); CMS.registerPreviewCard("posts", PostPreviewCard, () => 240);
CMS.registerFieldPreview('posts', 'date', PostDateFieldPreview); CMS.registerFieldPreview("posts", "date", PostDateFieldPreview);
CMS.registerFieldPreview('posts', 'draft', PostDraftFieldPreview); CMS.registerFieldPreview("posts", "draft", PostDraftFieldPreview);
cms.registerPreviewTemplate("general", GeneralPreview); cms.registerPreviewTemplate("general", GeneralPreview);
cms.registerPreviewTemplate("authors", AuthorsPreview); cms.registerPreviewTemplate("authors", AuthorsPreview);
// Pass the name of a registered control to reuse with a new widget preview. // Pass the name of a registered control to reuse with a new widget preview.

View File

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import "../../core/src/styles/main.css";
import "./cms"; import "./cms";
const root = ReactDOM.createRoot(document.getElementById("root")); const root = ReactDOM.createRoot(document.getElementById("root"));

View File

@ -1,7 +0,0 @@
const baseConfig = require('../../tailwind.base.config');
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['../core/src/**/*.tsx'],
...baseConfig,
};

View File

@ -0,0 +1,17 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgrPlugin from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), svgrPlugin()],
assetsInclude: ["public/**/*"],
optimizeDeps: {
force: true,
include: ["@staticcms/core"],
},
build: {
commonjsOptions: { include: [/core/, /node_modules/] },
outDir: "build",
},
});

View File

@ -92,7 +92,7 @@ CMS.registerEventListener({
}); });
``` ```
Supported events are `mounted`, `preSave` and `postSave`. The `preSave` hook can be used to modify the entry data like so: Supported events are `mounted`, `login`, `preSave` and `postSave`. The `preSave` hook can be used to modify the entry data like so:
```javascript ```javascript
CMS.registerEventListener({ CMS.registerEventListener({

View File

@ -339,10 +339,11 @@ CMS.registerPreviewStyle('.main { color: blue; border: 1px solid gree; }', { raw
### Params ### Params
| Param | Type | Description | | Param | Type | Description |
| --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | --------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | The name of the collection (or file for file collections) which this preview component will be used for<br /><ul><li>Folder collections: Use the name of the collection</li><li>File collections: Use the name of the file</li></ul> | | name | string | The name of the collection (or file for file collections) which this preview component will be used for<br /><ul><li>Folder collections: Use the name of the collection</li><li>File collections: Use the name of the file</li></ul> |
| react_component | [React Function Component](https://reactjs.org/docs/components-and-props.html) | A React functional component that renders a preview card for a given entry in your collection | | component | [React Function Component](https://reactjs.org/docs/components-and-props.html) | A React functional component that renders a preview card for a given entry in your collection |
| getHeight | function | A function that returns the height for your cards. An object containing the current `collection` and `entry` are passed into the function at render. If no `getHeight` function is provided, the height will be `204` if the collection has an image field or `64` if the collection does not have an image field |
The following parameters will be passed to your `react_component` during render: The following parameters will be passed to your `react_component` during render:
@ -412,7 +413,7 @@ const PostPreviewCard = ({ entry, widgetFor }) => {
); );
}; };
CMS.registerPreviewCard('posts', PostPreviewCard); CMS.registerPreviewCard('posts', PostPreviewCard, () => 240);
``` ```
```jsx ```jsx
@ -462,6 +463,8 @@ const PostPreviewCard = ({ entry, widgetFor }) => {
</div> </div>
); );
}; };
CMS.registerPreviewCard('posts', PostPreviewCard, () => 240);
``` ```
```tsx ```tsx
@ -523,7 +526,7 @@ const PostPreviewCard = ({ entry, widgetFor }: TemplatePreviewCardProps<Post>) =
); );
}; };
CMS.registerPreviewTemplate('posts', PostPreview); CMS.registerPreviewCard('posts', PostPreviewCard, () => 240);
``` ```
</CodeTabs> </CodeTabs>

3129
yarn.lock

File diff suppressed because it is too large Load Diff