feat: folder creation (#696)
This commit is contained in:
parent
bc0331483a
commit
5384b5c7a2
BIN
packages/core/dev-test/assets/uploads/Other Pics/lobby.jpg
Normal file
BIN
packages/core/dev-test/assets/uploads/Other Pics/lobby.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 808 KiB |
BIN
packages/core/dev-test/assets/uploads/Other Pics/moby-dick.jpg
Normal file
BIN
packages/core/dev-test/assets/uploads/Other Pics/moby-dick.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 310 KiB |
@ -2,6 +2,7 @@ backend:
|
||||
name: test-repo
|
||||
site_url: 'https://example.com'
|
||||
media_folder: assets/uploads
|
||||
media_library_folder_support: true
|
||||
locale: en
|
||||
i18n:
|
||||
# Required and can be one of multiple_folders, multiple_files or single_file
|
||||
|
@ -14,11 +14,7 @@
|
||||
'lobby.jpg': {
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
_posts: {
|
||||
assets: {
|
||||
uploads: {
|
||||
'Other Pics': {
|
||||
'moby-dick.jpg': {
|
||||
content: '',
|
||||
},
|
||||
@ -27,6 +23,8 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_posts: {
|
||||
'2015-02-14-this-is-a-post.md': {
|
||||
content:
|
||||
'---\ntitle: This is a YAML front matter post\ndraft: true\nimage: /assets/uploads/moby-dick.jpg\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n',
|
||||
|
@ -241,7 +241,12 @@ function createMediaFileFromAsset({
|
||||
return mediaFile;
|
||||
}
|
||||
|
||||
export function persistMedia(file: File, opts: MediaOptions = {}, currentFolder?: string) {
|
||||
export function persistMedia(
|
||||
file: File,
|
||||
opts: MediaOptions = {},
|
||||
targetFolder?: string,
|
||||
currentFolder?: string,
|
||||
) {
|
||||
const { field } = opts;
|
||||
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
@ -287,7 +292,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}, currentFolder?
|
||||
try {
|
||||
const entry = state.entryDraft.entry;
|
||||
const collection = entry?.collection ? state.collections[entry.collection] : null;
|
||||
const path = selectMediaFilePath(config, collection, entry, fileName, field, currentFolder);
|
||||
const path = selectMediaFilePath(config, collection, entry, fileName, field, targetFolder);
|
||||
const assetProxy = createAssetProxy({
|
||||
file,
|
||||
path,
|
||||
@ -310,7 +315,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}, currentFolder?
|
||||
mediaFile = await backend.persistMedia(config, assetProxy);
|
||||
}
|
||||
|
||||
return dispatch(mediaPersisted(mediaFile));
|
||||
return dispatch(mediaPersisted(mediaFile, currentFolder));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
dispatch(
|
||||
@ -487,10 +492,10 @@ export function mediaPersisting() {
|
||||
return { type: MEDIA_PERSIST_REQUEST } as const;
|
||||
}
|
||||
|
||||
export function mediaPersisted(file: ImplementationMediaFile) {
|
||||
export function mediaPersisted(file: ImplementationMediaFile, currentFolder: string | undefined) {
|
||||
return {
|
||||
type: MEDIA_PERSIST_SUCCESS,
|
||||
payload: { file },
|
||||
payload: { file, currentFolder },
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,11 @@ import attempt from 'lodash/attempt';
|
||||
import isError from 'lodash/isError';
|
||||
import take from 'lodash/take';
|
||||
import unset from 'lodash/unset';
|
||||
import { extname } from 'path';
|
||||
import { basename, dirname } from 'path';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { Cursor, CURSOR_COMPATIBILITY_SYMBOL } from '@staticcms/core/lib/util';
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
|
||||
import type {
|
||||
@ -20,7 +21,7 @@ import type {
|
||||
} from '@staticcms/core/interface';
|
||||
import type AssetProxy from '@staticcms/core/valueObjects/AssetProxy';
|
||||
|
||||
type RepoFile = { path: string; content: string | AssetProxy };
|
||||
type RepoFile = { path: string; content: string | AssetProxy; isDirectory?: boolean };
|
||||
type RepoTree = { [key: string]: RepoFile | RepoTree };
|
||||
|
||||
declare global {
|
||||
@ -83,20 +84,36 @@ export function getFolderFiles(
|
||||
depth: number,
|
||||
files = [] as RepoFile[],
|
||||
path = folder,
|
||||
) {
|
||||
includeFolders?: boolean,
|
||||
): RepoFile[] {
|
||||
if (depth <= 0) {
|
||||
return files;
|
||||
}
|
||||
|
||||
if (includeFolders) {
|
||||
files.unshift({ isDirectory: true, content: '', path });
|
||||
}
|
||||
|
||||
Object.keys(tree[folder] || {}).forEach(key => {
|
||||
if (extname(key)) {
|
||||
const parts = key.split('.');
|
||||
const keyExtension = parts.length > 1 ? parts[parts.length - 1] : '';
|
||||
|
||||
if (isNotEmpty(keyExtension)) {
|
||||
const file = (tree[folder] as RepoTree)[key] as RepoFile;
|
||||
if (!extension || key.endsWith(`.${extension}`)) {
|
||||
files.unshift({ content: file.content, path: `${path}/${key}` });
|
||||
}
|
||||
} else {
|
||||
const subTree = tree[folder] as RepoTree;
|
||||
return getFolderFiles(subTree, key, extension, depth - 1, files, `${path}/${key}`);
|
||||
return getFolderFiles(
|
||||
subTree,
|
||||
key,
|
||||
extension,
|
||||
depth - 1,
|
||||
files,
|
||||
`${path}/${key}`,
|
||||
includeFolders,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -222,18 +239,31 @@ export default class TestBackend implements BackendClass {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getMedia(mediaFolder = this.mediaFolder): Promise<ImplementationMediaFile[]> {
|
||||
async getMedia(
|
||||
mediaFolder = this.mediaFolder,
|
||||
folderSupport?: boolean,
|
||||
): Promise<ImplementationMediaFile[]> {
|
||||
if (!mediaFolder) {
|
||||
return [];
|
||||
}
|
||||
const files = getFolderFiles(window.repoFiles, mediaFolder.split('/')[0], '', 100).filter(f =>
|
||||
f.path.startsWith(mediaFolder),
|
||||
);
|
||||
const files = getFolderFiles(
|
||||
window.repoFiles,
|
||||
mediaFolder.split('/')[0],
|
||||
'',
|
||||
100,
|
||||
undefined,
|
||||
undefined,
|
||||
folderSupport,
|
||||
).filter(f => {
|
||||
return dirname(f.path) === mediaFolder;
|
||||
});
|
||||
|
||||
return files.map(f => ({
|
||||
name: f.path,
|
||||
name: basename(f.path),
|
||||
id: f.path,
|
||||
path: f.path,
|
||||
displayURL: f.path,
|
||||
isDirectory: f.isDirectory ?? false,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -242,7 +272,7 @@ export default class TestBackend implements BackendClass {
|
||||
id: path,
|
||||
displayURL: path,
|
||||
path,
|
||||
name: path,
|
||||
name: basename(path),
|
||||
size: 1,
|
||||
url: path,
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
selectFields,
|
||||
selectTemplateName,
|
||||
} from '@staticcms/core/lib/util/collection.util';
|
||||
import { isNullish } from '@staticcms/core/lib/util/null.util';
|
||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||
import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
@ -120,6 +121,8 @@ const EntryCard = ({
|
||||
value={value}
|
||||
theme={theme}
|
||||
/>
|
||||
) : isNullish(value) ? (
|
||||
''
|
||||
) : (
|
||||
String(value)
|
||||
)}
|
||||
|
@ -8,7 +8,7 @@ import type { CSSProperties, FC, MouseEventHandler, ReactNode, Ref } from 'react
|
||||
|
||||
export interface BaseBaseProps {
|
||||
variant?: 'contained' | 'outlined' | 'text';
|
||||
color?: 'primary' | 'success' | 'error';
|
||||
color?: 'primary' | 'secondary' | 'success' | 'error';
|
||||
size?: 'medium' | 'small';
|
||||
rounded?: boolean | 'no-padding';
|
||||
className?: string;
|
||||
|
@ -8,16 +8,19 @@ const classes: Record<
|
||||
> = {
|
||||
contained: {
|
||||
primary: 'btn-contained-primary',
|
||||
secondary: 'btn-contained-secondary',
|
||||
success: 'btn-contained-success',
|
||||
error: 'btn-contained-error',
|
||||
},
|
||||
outlined: {
|
||||
primary: 'btn-outlined-primary',
|
||||
secondary: 'btn-outlined-secondary',
|
||||
success: 'btn-outlined-success',
|
||||
error: 'btn-outlined-error',
|
||||
},
|
||||
text: {
|
||||
primary: 'btn-text-primary',
|
||||
secondary: 'btn-text-secondary',
|
||||
success: 'btn-text-success',
|
||||
error: 'btn-text-error',
|
||||
},
|
||||
|
@ -7,13 +7,16 @@ import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import type { ChangeEventHandler, FC, MouseEventHandler, Ref } from 'react';
|
||||
|
||||
export interface BaseTextFieldProps {
|
||||
id?: string;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
'data-testid'?: string;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
onClick?: MouseEventHandler<HTMLInputElement>;
|
||||
cursor?: 'default' | 'pointer' | 'text';
|
||||
variant?: 'borderless' | 'contained';
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface NumberTextFieldProps extends BaseTextFieldProps {
|
||||
@ -36,6 +39,7 @@ const TextField: FC<TextFieldProps> = ({
|
||||
type,
|
||||
'data-testid': dataTestId,
|
||||
cursor = 'default',
|
||||
variant = 'borderless',
|
||||
inputRef,
|
||||
readonly,
|
||||
disabled = false,
|
||||
@ -66,17 +70,38 @@ const TextField: FC<TextFieldProps> = ({
|
||||
className: classNames(
|
||||
`
|
||||
w-full
|
||||
text-sm
|
||||
`,
|
||||
variant === 'borderless' &&
|
||||
`
|
||||
h-6
|
||||
px-3
|
||||
bg-transparent
|
||||
outline-none
|
||||
text-sm
|
||||
font-medium
|
||||
text-gray-900
|
||||
disabled:text-gray-300
|
||||
dark:text-gray-100
|
||||
dark:disabled:text-gray-500
|
||||
`,
|
||||
variant === 'contained' &&
|
||||
`
|
||||
bg-gray-50
|
||||
border
|
||||
border-gray-300
|
||||
text-gray-900
|
||||
rounded-lg
|
||||
focus:ring-blue-500
|
||||
focus:border-blue-500
|
||||
block
|
||||
p-2.5
|
||||
dark:bg-gray-700
|
||||
dark:border-gray-600
|
||||
dark:placeholder-gray-400
|
||||
dark:text-white
|
||||
dark:focus:ring-blue-500
|
||||
dark:focus:border-blue-500
|
||||
`,
|
||||
finalCursor === 'pointer' && 'cursor-pointer',
|
||||
finalCursor === 'text' && 'cursor-text',
|
||||
finalCursor === 'default' && 'cursor-default',
|
||||
|
@ -44,7 +44,7 @@ const MediaLibraryModal: FC = () => {
|
||||
>
|
||||
<CloseIcon className="w-5 h-5" />
|
||||
</IconButton>
|
||||
<MediaLibrary canInsert />
|
||||
<MediaLibrary canInsert isDialog />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@ -42,6 +42,7 @@ const CurrentMediaDetails: FC<CurrentMediaDetailsProps> = ({
|
||||
py-4
|
||||
border-b
|
||||
border-gray-200/75
|
||||
dark:border-slate-500/75
|
||||
"
|
||||
>
|
||||
<Image
|
||||
|
@ -0,0 +1,115 @@
|
||||
import { Close as CloseIcon } from '@styled-icons/material/Close';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import Button from '../../common/button/Button';
|
||||
import IconButton from '../../common/button/IconButton';
|
||||
import Modal from '../../common/modal/Modal';
|
||||
import TextField from '../../common/text-field/TextField';
|
||||
|
||||
import type { TranslatedProps } from '@staticcms/core/interface';
|
||||
import type { ChangeEventHandler, FC } from 'react';
|
||||
|
||||
interface FolderCreationDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onCreate: (folderName: string) => void;
|
||||
}
|
||||
|
||||
const FolderCreationDialog: FC<TranslatedProps<FolderCreationDialogProps>> = ({
|
||||
open,
|
||||
onClose,
|
||||
onCreate,
|
||||
t,
|
||||
}) => {
|
||||
const [folderName, setFolderName] = useState('');
|
||||
const handleFolderNameChange: ChangeEventHandler<HTMLInputElement> = useCallback(event => {
|
||||
setFolderName(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
if (isEmpty(folderName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onCreate(folderName);
|
||||
}, [folderName, onCreate]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
className="
|
||||
w-[50%]
|
||||
min-w-[300px]
|
||||
max-w-[600px]
|
||||
"
|
||||
>
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
items-start
|
||||
justify-between
|
||||
px-4
|
||||
pt-4
|
||||
pb-3
|
||||
"
|
||||
>
|
||||
<h3
|
||||
className="
|
||||
text-xl
|
||||
font-semibold
|
||||
text-gray-900
|
||||
dark:text-white
|
||||
"
|
||||
>
|
||||
{t('mediaLibrary.folderSupport.createNewFolder')}
|
||||
</h3>
|
||||
<IconButton variant="text" aria-label="add" onClick={onClose}>
|
||||
<CloseIcon className="w-5 h-5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
px-4
|
||||
py-2
|
||||
"
|
||||
>
|
||||
<TextField
|
||||
id="folder_name"
|
||||
type="text"
|
||||
value={folderName}
|
||||
onChange={handleFolderNameChange}
|
||||
key="mobile-time-input"
|
||||
data-testid="time-input"
|
||||
cursor="pointer"
|
||||
variant="contained"
|
||||
placeholder={t('mediaLibrary.folderSupport.enterFolderName')}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
p-4
|
||||
space-x-2
|
||||
"
|
||||
>
|
||||
<Button variant="text" aria-label="cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
aria-label="create"
|
||||
onClick={handleCreate}
|
||||
disabled={isEmpty(folderName)}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderCreationDialog;
|
@ -1,13 +1,14 @@
|
||||
import { Photo as PhotoIcon } from '@styled-icons/material/Photo';
|
||||
import { ArrowUpward as UpwardIcon } from '@styled-icons/material/ArrowUpward';
|
||||
import { Home as HomeIcon } from '@styled-icons/material/Home';
|
||||
import { CreateNewFolder as NewFolderIcon } from '@styled-icons/material/CreateNewFolder';
|
||||
import { FolderOpen as FolderOpenIcon } from '@styled-icons/material/FolderOpen';
|
||||
import { Home as HomeIcon } from '@styled-icons/material/Home';
|
||||
import { Photo as PhotoIcon } from '@styled-icons/material/Photo';
|
||||
import fuzzy from 'fuzzy';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { dirname } from 'path';
|
||||
import trim from 'lodash/trim';
|
||||
import { dirname, join } from 'path';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { translate } from 'react-polyglot';
|
||||
|
||||
import {
|
||||
closeMediaLibrary,
|
||||
@ -19,20 +20,23 @@ import {
|
||||
} from '@staticcms/core/actions/mediaLibrary';
|
||||
import useMediaFiles from '@staticcms/core/lib/hooks/useMediaFiles';
|
||||
import { fileExtension } from '@staticcms/core/lib/util';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import MediaLibraryCloseEvent from '@staticcms/core/lib/util/events/MediaLibraryCloseEvent';
|
||||
import { selectMediaFilePath, selectMediaFolder } from '@staticcms/core/lib/util/media.util';
|
||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
|
||||
import { selectMediaLibraryState } from '@staticcms/core/reducers/selectors/mediaLibrary';
|
||||
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import alert from '../../common/alert/Alert';
|
||||
import Button from '../../common/button/Button';
|
||||
import IconButton from '../../common/button/IconButton';
|
||||
import confirm from '../../common/confirm/Confirm';
|
||||
import CurrentMediaDetails from './CurrentMediaDetails';
|
||||
import EmptyMessage from './EmptyMessage';
|
||||
import FileUploadButton from './FileUploadButton';
|
||||
import FolderCreationDialog from './FolderCreationDialog';
|
||||
import MediaLibraryCardGrid from './MediaLibraryCardGrid';
|
||||
import MediaLibrarySearch from './MediaLibrarySearch';
|
||||
import { selectMediaFilePath, selectMediaFolder } from '@staticcms/core/lib/util/media.util';
|
||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
|
||||
|
||||
import type { MediaFile, TranslatedProps } from '@staticcms/core/interface';
|
||||
import type { ChangeEvent, FC, KeyboardEvent } from 'react';
|
||||
@ -56,9 +60,14 @@ const IMAGE_EXTENSIONS = [...IMAGE_EXTENSIONS_VIEWABLE];
|
||||
|
||||
interface MediaLibraryProps {
|
||||
canInsert?: boolean;
|
||||
isDialog?: boolean;
|
||||
}
|
||||
|
||||
const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({ canInsert = false, t }) => {
|
||||
const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({
|
||||
canInsert = false,
|
||||
isDialog = false,
|
||||
t,
|
||||
}) => {
|
||||
const [currentFolder, setCurrentFolder] = useState<string | undefined>(undefined);
|
||||
const [selectedFile, setSelectedFile] = useState<MediaFile | null>(null);
|
||||
const [query, setQuery] = useState<string | undefined>(undefined);
|
||||
@ -265,9 +274,13 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({ canInsert = fals
|
||||
|
||||
const handleOpenDirectory = useCallback(
|
||||
(dir: string) => {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newDirectory = selectMediaFilePath(
|
||||
config!,
|
||||
collection!,
|
||||
config,
|
||||
collection,
|
||||
entry,
|
||||
dir,
|
||||
field,
|
||||
@ -281,28 +294,72 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({ canInsert = fals
|
||||
[dispatch, currentFolder, collection, config, entry, field],
|
||||
);
|
||||
|
||||
const handleGoBack = useCallback(
|
||||
(toHome?: boolean) => {
|
||||
const mediaFolder = useMemo(() => {
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
return trim(selectMediaFolder(config, collection, entry, field), '/');
|
||||
}, [collection, config, entry, field]);
|
||||
|
||||
const parentFolder = useMemo(() => {
|
||||
if (!config || !currentFolder) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return dirname(currentFolder);
|
||||
}, [config, currentFolder]);
|
||||
|
||||
const goToFolder = useCallback(
|
||||
(folder: string | undefined) => {
|
||||
setSelectedFile(null);
|
||||
setQuery('');
|
||||
let newDirectory: string | undefined;
|
||||
if (toHome) {
|
||||
setCurrentFolder(undefined);
|
||||
} else {
|
||||
const mediaFolder = trim(selectMediaFolder(config!, collection, entry, field), '/');
|
||||
const dir = dirname(currentFolder!);
|
||||
newDirectory = dir.includes(mediaFolder) && trim(dir, '/') != mediaFolder ? dir : undefined;
|
||||
setCurrentFolder(newDirectory);
|
||||
}
|
||||
dispatch(loadMedia({ currentFolder: newDirectory }));
|
||||
setCurrentFolder(folder);
|
||||
dispatch(loadMedia({ currentFolder: folder }));
|
||||
},
|
||||
[dispatch, config, collection, entry, field, currentFolder],
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleHome = useCallback(() => {
|
||||
goToFolder(undefined);
|
||||
}, [goToFolder]);
|
||||
|
||||
const handleGoBack = useCallback(() => {
|
||||
if (!mediaFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
goToFolder(
|
||||
parentFolder?.includes(mediaFolder) && parentFolder !== mediaFolder
|
||||
? parentFolder
|
||||
: undefined,
|
||||
);
|
||||
}, [goToFolder, mediaFolder, parentFolder]);
|
||||
|
||||
const [folderCreationOpen, setFolderCreationOpen] = useState(false);
|
||||
const handleCreateFolder = useCallback(() => {
|
||||
console.log('[createFolder]');
|
||||
setFolderCreationOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleFolderCreationDialogClose = useCallback(() => {
|
||||
setFolderCreationOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleFolderCreate = useCallback(
|
||||
async (folderName: string) => {
|
||||
const folder = currentFolder ?? mediaFolder;
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFolderCreationOpen(false);
|
||||
const file = new File([''], '.gitkeep', { type: 'text/plain' });
|
||||
await dispatch(
|
||||
persistMedia(file, { field }, join(folder, folderName), currentFolder ?? mediaFolder),
|
||||
);
|
||||
},
|
||||
[currentFolder, dispatch, field, mediaFolder],
|
||||
);
|
||||
|
||||
/**
|
||||
* Stores the public path of the file in the application store, where the
|
||||
* editor field that launched the media library can retrieve it.
|
||||
@ -408,108 +465,154 @@ const MediaLibrary: FC<TranslatedProps<MediaLibraryProps>> = ({ canInsert = fals
|
||||
const hasSelection = hasMedia && !isEmpty(selectedFile);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full">
|
||||
<CurrentMediaDetails
|
||||
collection={collection}
|
||||
field={field}
|
||||
canInsert={canInsert}
|
||||
url={url}
|
||||
alt={alt}
|
||||
insertOptions={insertOptions}
|
||||
onUrlChange={handleURLChange}
|
||||
onAltChange={handleAltChange}
|
||||
/>
|
||||
<div className="flex items-center px-5 pt-4 mb-4">
|
||||
<div className="flex flex-grow gap-4 mr-8">
|
||||
<h2
|
||||
className="
|
||||
text-xl
|
||||
font-semibold
|
||||
flex
|
||||
items-center
|
||||
gap-2
|
||||
text-gray-800
|
||||
dark:text-gray-300
|
||||
"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<PhotoIcon className="w-5 h-5" />
|
||||
</div>
|
||||
{t('app.header.media')}
|
||||
</h2>
|
||||
{config?.media_library_folder_support ? (
|
||||
<div className="flex gap-3 items-center">
|
||||
<Button
|
||||
onClick={() => handleGoBack(true)}
|
||||
title={t('mediaLibrary.folderSupport.goBackToHome')}
|
||||
disabled={!currentFolder}
|
||||
>
|
||||
<HomeIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleGoBack()}
|
||||
title={t('mediaLibrary.folderSupport.goBack')}
|
||||
disabled={!currentFolder}
|
||||
>
|
||||
<UpwardIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<MediaLibrarySearch
|
||||
value={query}
|
||||
onChange={handleSearchChange}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
placeholder={t('mediaLibrary.mediaLibraryModal.search')}
|
||||
disabled={!dynamicSearchActive && !hasFilteredFiles}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3 items-center relative z-20">
|
||||
{config?.media_library_folder_support ? (
|
||||
<Button
|
||||
onClick={() => handleCreateFolder()}
|
||||
title={t('mediaLibrary.folderSupport.onCreateTitle')}
|
||||
>
|
||||
<NewFolderIcon className="h-5 w-5"></NewFolderIcon>
|
||||
</Button>
|
||||
) : null}
|
||||
<FileUploadButton imagesOnly={forImage} onChange={handlePersist} />
|
||||
{canInsert ? (
|
||||
<Button
|
||||
key="choose-selected"
|
||||
color="success"
|
||||
variant="contained"
|
||||
onClick={handleInsert}
|
||||
disabled={!hasSelection}
|
||||
data-testid="choose-selected"
|
||||
>
|
||||
{t('mediaLibrary.mediaLibraryModal.chooseSelected')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{!hasMedia ? (
|
||||
<EmptyMessage content={emptyMessage} />
|
||||
) : (
|
||||
<MediaLibraryCardGrid
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
mediaItems={tableData}
|
||||
isSelectedFile={file => selectedFile?.key === file.key}
|
||||
onAssetSelect={handleAssetSelect}
|
||||
canLoadMore={hasNextPage}
|
||||
onLoadMore={handleLoadMore}
|
||||
onDirectoryOpen={handleOpenDirectory}
|
||||
currentFolder={currentFolder}
|
||||
isPaginating={isPaginating}
|
||||
paginatingMessage={t('mediaLibrary.mediaLibraryModal.loading')}
|
||||
cardDraftText={t('mediaLibrary.mediaLibraryCard.draft')}
|
||||
loadDisplayURL={loadDisplayURL}
|
||||
displayURLs={displayURLs}
|
||||
<>
|
||||
<div className="flex flex-col w-full h-full">
|
||||
<CurrentMediaDetails
|
||||
collection={collection}
|
||||
field={field}
|
||||
onDelete={handleDelete}
|
||||
canInsert={canInsert}
|
||||
url={url}
|
||||
alt={alt}
|
||||
insertOptions={insertOptions}
|
||||
onUrlChange={handleURLChange}
|
||||
onAltChange={handleAltChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
`
|
||||
flex
|
||||
items-center
|
||||
px-5
|
||||
pt-4
|
||||
`,
|
||||
config?.media_library_folder_support &&
|
||||
`
|
||||
pb-4
|
||||
border-b
|
||||
border-gray-200/75
|
||||
dark:border-slate-500/75
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-grow gap-3 mr-8">
|
||||
<h2
|
||||
className="
|
||||
text-xl
|
||||
font-semibold
|
||||
flex
|
||||
items-center
|
||||
gap-2
|
||||
text-gray-800
|
||||
dark:text-gray-300
|
||||
"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<PhotoIcon className="w-5 h-5" />
|
||||
</div>
|
||||
{t('app.header.media')}
|
||||
</h2>
|
||||
<MediaLibrarySearch
|
||||
value={query}
|
||||
onChange={handleSearchChange}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
placeholder={t('mediaLibrary.mediaLibraryModal.search')}
|
||||
disabled={!dynamicSearchActive && !hasFilteredFiles}
|
||||
/>
|
||||
{config?.media_library_folder_support ? (
|
||||
<div className="flex gap-1.5 items-center">
|
||||
<IconButton
|
||||
onClick={handleHome}
|
||||
title={t('mediaLibrary.folderSupport.home')}
|
||||
color="secondary"
|
||||
disabled={!currentFolder}
|
||||
>
|
||||
<HomeIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleGoBack}
|
||||
title={
|
||||
parentFolder
|
||||
? t('mediaLibrary.folderSupport.upToFolder', { folder: parentFolder })
|
||||
: t('mediaLibrary.folderSupport.up')
|
||||
}
|
||||
color="secondary"
|
||||
disabled={!parentFolder}
|
||||
>
|
||||
<UpwardIcon className="h-5 w-5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleCreateFolder}
|
||||
title={t('mediaLibrary.folderSupport.newFolder')}
|
||||
color="secondary"
|
||||
>
|
||||
<NewFolderIcon className="h-5 w-5"></NewFolderIcon>
|
||||
</IconButton>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex gap-3 items-center relative z-20">
|
||||
<FileUploadButton imagesOnly={forImage} onChange={handlePersist} />
|
||||
{canInsert ? (
|
||||
<Button
|
||||
key="choose-selected"
|
||||
color="success"
|
||||
variant="contained"
|
||||
onClick={handleInsert}
|
||||
disabled={!hasSelection}
|
||||
data-testid="choose-selected"
|
||||
>
|
||||
{t('mediaLibrary.mediaLibraryModal.chooseSelected')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{config?.media_library_folder_support ? (
|
||||
<div
|
||||
className="
|
||||
flex
|
||||
gap-2
|
||||
items-center
|
||||
px-5
|
||||
py-4
|
||||
font-bold
|
||||
text-xl
|
||||
"
|
||||
>
|
||||
<FolderOpenIcon className="w-6 h-6" />
|
||||
{currentFolder ?? mediaFolder}
|
||||
</div>
|
||||
) : null}
|
||||
{!hasMedia ? (
|
||||
<EmptyMessage content={emptyMessage} />
|
||||
) : (
|
||||
<MediaLibraryCardGrid
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
mediaItems={tableData}
|
||||
isSelectedFile={file => selectedFile?.key === file.key}
|
||||
onAssetSelect={handleAssetSelect}
|
||||
canLoadMore={hasNextPage}
|
||||
onLoadMore={handleLoadMore}
|
||||
onDirectoryOpen={handleOpenDirectory}
|
||||
currentFolder={currentFolder}
|
||||
isPaginating={isPaginating}
|
||||
paginatingMessage={t('mediaLibrary.mediaLibraryModal.loading')}
|
||||
cardDraftText={t('mediaLibrary.mediaLibraryCard.draft')}
|
||||
loadDisplayURL={loadDisplayURL}
|
||||
displayURLs={displayURLs}
|
||||
collection={collection}
|
||||
field={field}
|
||||
isDialog={isDialog}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<FolderCreationDialog
|
||||
open={folderCreationOpen}
|
||||
onClose={handleFolderCreationDialogClose}
|
||||
onCreate={handleFolderCreate}
|
||||
t={t}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Delete as DeleteIcon } from '@styled-icons/material/Delete';
|
||||
import { Download as DownloadIcon } from '@styled-icons/material/Download';
|
||||
import { FolderOpen as FolderIcon } from '@styled-icons/material/FolderOpen';
|
||||
import { FolderOpen as FolderOpenIcon } from '@styled-icons/material/FolderOpen';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { translate } from 'react-polyglot';
|
||||
|
||||
@ -245,7 +245,7 @@ const MediaLibraryCard = <T extends MediaField, EF extends BaseField = UnknownFi
|
||||
text-5xl
|
||||
"
|
||||
>
|
||||
<FolderIcon className="w-24 h-24" />
|
||||
<FolderOpenIcon className="w-24 h-24" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
|
@ -9,6 +9,9 @@ import {
|
||||
MEDIA_CARD_WIDTH,
|
||||
MEDIA_LIBRARY_PADDING,
|
||||
} from '@staticcms/core/constants/mediaLibrary';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import MediaLibraryCard from './MediaLibraryCard';
|
||||
|
||||
import type {
|
||||
@ -49,6 +52,7 @@ export interface MediaLibraryCardGridProps {
|
||||
displayURLs: MediaLibraryState['displayURLs'];
|
||||
collection?: Collection;
|
||||
field?: Field;
|
||||
isDialog: boolean;
|
||||
onDelete: (file: MediaFile) => void;
|
||||
}
|
||||
|
||||
@ -89,9 +93,7 @@ const CardWrapper = ({
|
||||
);
|
||||
|
||||
const top = useMemo(
|
||||
() =>
|
||||
parseFloat(`${typeof style.top === 'number' ? style.top ?? 0 : style.top}`) +
|
||||
MEDIA_LIBRARY_PADDING,
|
||||
() => parseFloat(`${typeof style.top === 'number' ? style.top ?? 0 : style.top}`),
|
||||
[style.top],
|
||||
);
|
||||
|
||||
@ -134,7 +136,8 @@ const CardWrapper = ({
|
||||
};
|
||||
|
||||
const MediaLibraryCardGrid: FC<MediaLibraryCardGridProps> = props => {
|
||||
const { mediaItems, scrollContainerRef, canLoadMore, onLoadMore } = props;
|
||||
const { mediaItems, scrollContainerRef, canLoadMore, isDialog, onLoadMore } = props;
|
||||
const config = useAppSelector(selectConfig);
|
||||
|
||||
const [version, setVersion] = useState(0);
|
||||
|
||||
@ -154,7 +157,20 @@ const MediaLibraryCardGrid: FC<MediaLibraryCardGridProps> = props => {
|
||||
const rowCount = Math.ceil(mediaItems.length / columnCount);
|
||||
|
||||
return (
|
||||
<div key={version} ref={scrollContainerRef}>
|
||||
<div
|
||||
key={version}
|
||||
className={classNames(
|
||||
`
|
||||
overflow-hidden
|
||||
`,
|
||||
isDialog && 'rounded-b-lg',
|
||||
!config?.media_library_folder_support && 'pt-[20px]',
|
||||
)}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
columnCount={columnCount}
|
||||
columnWidth={index =>
|
||||
@ -163,20 +179,26 @@ const MediaLibraryCardGrid: FC<MediaLibraryCardGridProps> = props => {
|
||||
rowCount={rowCount}
|
||||
rowHeight={() => rowHeightWithGutter}
|
||||
width={width}
|
||||
height={height}
|
||||
height={
|
||||
height - (!config?.media_library_folder_support ? MEDIA_LIBRARY_PADDING : 0)
|
||||
}
|
||||
itemData={
|
||||
{
|
||||
...props,
|
||||
columnCount,
|
||||
} as CardGridItemData
|
||||
}
|
||||
className="
|
||||
px-5
|
||||
py-4
|
||||
overflow-hidden
|
||||
overflow-y-auto
|
||||
styled-scrollbars
|
||||
"
|
||||
outerRef={scrollContainerRef}
|
||||
className={classNames(
|
||||
`
|
||||
px-5
|
||||
pb-2
|
||||
overflow-hidden
|
||||
overflow-y-auto
|
||||
styled-scrollbars
|
||||
`,
|
||||
isDialog && 'styled-scrollbars-secondary',
|
||||
)}
|
||||
style={{ position: 'unset' }}
|
||||
>
|
||||
{CardWrapper}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { dirname } from 'path';
|
||||
import trim from 'lodash/trim';
|
||||
import { basename, dirname } from 'path';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { selectMediaLibraryFiles } from '@staticcms/core/reducers/selectors/mediaLibrary';
|
||||
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import { currentBackend } from '@staticcms/core/backend';
|
||||
import { selectCollection } from '@staticcms/core/reducers/selectors/collections';
|
||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
|
||||
import { selectMediaLibraryFiles } from '@staticcms/core/reducers/selectors/mediaLibrary';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import { selectMediaFolder } from '../util/media.util';
|
||||
import { currentBackend } from '@staticcms/core/backend';
|
||||
|
||||
import type { MediaField, MediaFile } from '@staticcms/core/interface';
|
||||
|
||||
@ -54,16 +54,35 @@ export default function useMediaFiles(field?: MediaField, currentFolder?: string
|
||||
};
|
||||
}, [currentFolder, config, entry]);
|
||||
|
||||
return useMemo(() => {
|
||||
const files = useMemo(() => {
|
||||
if (entry) {
|
||||
const entryFiles = entry.mediaFiles ?? [];
|
||||
if (config) {
|
||||
const mediaFolder = selectMediaFolder(config, collection, entry, field, currentFolder);
|
||||
const entryFolderFiles = entryFiles
|
||||
.filter(f => {
|
||||
if (f.name === '.gitkeep') {
|
||||
const folder = dirname(f.path);
|
||||
return dirname(folder) === mediaFolder;
|
||||
}
|
||||
|
||||
return dirname(f.path) === mediaFolder;
|
||||
})
|
||||
.map(file => ({ key: file.id, ...file }));
|
||||
.map(file => {
|
||||
if (file.name === '.gitkeep') {
|
||||
const folder = dirname(file.path);
|
||||
return {
|
||||
key: folder,
|
||||
id: folder,
|
||||
name: basename(folder),
|
||||
path: folder,
|
||||
isDirectory: true,
|
||||
draft: true,
|
||||
} as MediaFile;
|
||||
}
|
||||
return { key: file.id, ...file };
|
||||
});
|
||||
|
||||
if (currentFolderMediaFiles) {
|
||||
if (entryFiles.length > 0) {
|
||||
const draftFiles = entryFolderFiles.filter(file => file.draft == true);
|
||||
@ -77,4 +96,6 @@ export default function useMediaFiles(field?: MediaField, currentFolder?: string
|
||||
|
||||
return mediaLibraryFiles ?? [];
|
||||
}, [collection, config, currentFolderMediaFiles, entry, field, mediaLibraryFiles, currentFolder]);
|
||||
|
||||
return useMemo(() => files.filter(file => file.name !== '.gitkeep'), [files]);
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
export default function classNames(...classes: (string | undefined | null | false)[]) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
const filteredClasses = classes.filter(Boolean) as string[];
|
||||
|
||||
return filteredClasses
|
||||
.map(value => value.replace(/\n/g, ' ').replace(/[ ]+/g, ' ').trim())
|
||||
.join(' ');
|
||||
}
|
||||
|
@ -294,7 +294,7 @@ export function selectMediaFilePublicPath<EF extends BaseField>(
|
||||
|
||||
export function selectMediaFilePath(
|
||||
config: Config,
|
||||
collection: Collection | null,
|
||||
collection: Collection | null | undefined,
|
||||
entryMap: Entry | null | undefined,
|
||||
mediaPath: string,
|
||||
field: Field | undefined,
|
||||
|
@ -249,10 +249,12 @@ const en: LocalePhrasesRoot = {
|
||||
chooseSelected: 'Choose selected',
|
||||
},
|
||||
folderSupport: {
|
||||
onCreateTitle: 'Create new folder',
|
||||
onCreateBody: 'Please enter a name for the new folder.',
|
||||
goBackToHome: 'Go back to media folder.',
|
||||
goBack: 'Go back to previous folder.',
|
||||
newFolder: 'New folder',
|
||||
createNewFolder: 'Create new folder',
|
||||
enterFolderName: 'Enter folder name...',
|
||||
home: 'Home',
|
||||
up: 'Up',
|
||||
upToFolder: 'Up to %{folder}',
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { basename, dirname } from 'path';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
@ -208,13 +209,41 @@ function mediaLibrary(
|
||||
};
|
||||
|
||||
case MEDIA_PERSIST_SUCCESS: {
|
||||
const { file } = action.payload;
|
||||
const { file, currentFolder } = action.payload;
|
||||
const fileWithKey = { ...file, key: uuid() };
|
||||
const files = state.files as MediaFile[];
|
||||
const updatedFiles = [fileWithKey, ...files];
|
||||
|
||||
const dir = dirname(file.path);
|
||||
if (!currentFolder || dir === currentFolder) {
|
||||
const updatedFiles: MediaFile[] = [fileWithKey, ...files];
|
||||
return {
|
||||
...state,
|
||||
files: updatedFiles,
|
||||
isPersisting: false,
|
||||
};
|
||||
}
|
||||
|
||||
const folder = files.find(otherFile => otherFile.isDirectory && otherFile.path === dir);
|
||||
if (!folder) {
|
||||
const updatedFiles: MediaFile[] = [
|
||||
{
|
||||
name: basename(dir),
|
||||
id: dir,
|
||||
path: dir,
|
||||
isDirectory: true,
|
||||
},
|
||||
...files,
|
||||
];
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: updatedFiles,
|
||||
isPersisting: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: updatedFiles,
|
||||
isPersisting: false,
|
||||
};
|
||||
}
|
||||
|
@ -107,6 +107,66 @@
|
||||
dark:disabled:hover:bg-transparent;
|
||||
}
|
||||
|
||||
.btn-contained-secondary {
|
||||
@apply border
|
||||
font-medium
|
||||
text-gray-600
|
||||
bg-white
|
||||
border-gray-200/75
|
||||
hover:text-gray-700
|
||||
hover:bg-gray-100
|
||||
hover:border-gray-200/50
|
||||
disabled:text-gray-300/75
|
||||
disabled:bg-white/80
|
||||
disabled:border-gray-200/5
|
||||
dark:text-gray-300
|
||||
dark:bg-gray-800
|
||||
dark:border-gray-600/60
|
||||
dark:hover:bg-gray-700
|
||||
dark:hover:text-white
|
||||
dark:hover:border-gray-600/80
|
||||
dark:disabled:text-gray-400/20
|
||||
dark:disabled:bg-gray-700/20
|
||||
dark:disabled:border-gray-600/20;
|
||||
}
|
||||
|
||||
.btn-outlined-secondary {
|
||||
@apply text-gray-900
|
||||
bg-transparent
|
||||
border
|
||||
border-gray-200
|
||||
hover:bg-gray-100
|
||||
hover:text-gray-700
|
||||
hover:border-gray-200/50
|
||||
disabled:text-gray-300/75
|
||||
disabled:border-gray-200/40
|
||||
disabled:hover:bg-transparent
|
||||
dark:bg-transparent
|
||||
dark:text-gray-300
|
||||
dark:border-gray-600/60
|
||||
dark:hover:bg-gray-700
|
||||
dark:hover:text-white
|
||||
dark:hover:border-gray-600/80
|
||||
dark:disabled:text-gray-400/20
|
||||
dark:disabled:border-gray-600/20
|
||||
dark:disabled:hover:bg-transparent;
|
||||
}
|
||||
|
||||
.btn-text-secondary {
|
||||
@apply bg-transparent
|
||||
text-gray-900
|
||||
hover:text-gray-700
|
||||
hover:bg-gray-100
|
||||
disabled:text-gray-300/75
|
||||
disabled:hover:bg-transparent
|
||||
dark:text-gray-300
|
||||
dark:hover:text-white
|
||||
dark:hover:bg-gray-700
|
||||
dark:disabled:text-gray-400/20
|
||||
dark:disabled:border-gray-600/20
|
||||
dark:disabled:hover:bg-transparent;
|
||||
}
|
||||
|
||||
.btn-contained-success {
|
||||
@apply border
|
||||
border-transparent
|
||||
@ -238,11 +298,21 @@
|
||||
--scrollbar-background: rgb(248 250 252);
|
||||
}
|
||||
|
||||
.styled-scrollbars.styled-scrollbars-secondary {
|
||||
--scrollbar-foreground: rgba(100, 116, 139, 0.25);
|
||||
--scrollbar-background: rgb(255 255 255);
|
||||
}
|
||||
|
||||
.dark .styled-scrollbars {
|
||||
--scrollbar-foreground: rgba(30, 41, 59, 0.8);
|
||||
--scrollbar-background: rgb(15 23 42);
|
||||
}
|
||||
|
||||
.dark .styled-scrollbars.styled-scrollbars-secondary {
|
||||
--scrollbar-foreground: rgba(47, 64, 93, 0.8);
|
||||
--scrollbar-background: rgb(30 41 59);
|
||||
}
|
||||
|
||||
.styled-scrollbars {
|
||||
/* Foreground, Background */
|
||||
scrollbar-color: var(--scrollbar-foreground) var(--scrollbar-background);
|
||||
|
@ -2,6 +2,7 @@ backend:
|
||||
name: test-repo
|
||||
site_url: 'https://staticcms.org/'
|
||||
media_folder: assets/uploads
|
||||
media_library_folder_support: true
|
||||
locale: en
|
||||
i18n:
|
||||
# Required and can be one of multiple_folders, multiple_files or single_file
|
||||
|
@ -17,19 +17,17 @@
|
||||
"lobby.jpg": {
|
||||
content: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
_posts: {
|
||||
assets: {
|
||||
uploads: {
|
||||
"moby-dick.jpg": {
|
||||
content: "",
|
||||
'Other Pics': {
|
||||
'moby-dick.jpg': {
|
||||
content: '',
|
||||
},
|
||||
"lobby.jpg": {
|
||||
content: "",
|
||||
'lobby.jpg': {
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_posts: {
|
||||
"2015-02-14-this-is-a-post.md": {
|
||||
content:
|
||||
"---\ntitle: This is a YAML front matter post\nimage: /assets/uploads/moby-dick.jpg\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n",
|
||||
|
Loading…
x
Reference in New Issue
Block a user