diff --git a/core/dev-test/backends/azure/config.yml b/core/dev-test/backends/azure/config.yml deleted file mode 100644 index 926817bd..00000000 --- a/core/dev-test/backends/azure/config.yml +++ /dev/null @@ -1,471 +0,0 @@ -backend: - name: azure - branch: master - repo: organization/project/repo # replace with actual path - tenant_id: tenantId # replace with your tenantId - app_id: appId # replace with your appId - -media_folder: static/media -public_folder: /media -collections: - - name: posts - label: Posts - label_singular: Post - description: > - The description is a great place for tone setting, high level information, - and editing guidelines that are specific to a collection. - folder: _posts - slug: '{{year}}-{{month}}-{{day}}-{{slug}}' - summary: '{{title}} -- {{year}}/{{month}}/{{day}}' - sortable_fields: - fields: - - title - - date - default: - field: title - create: true - view_filters: - - label: Posts With Index - field: title - pattern: 'This is post #' - - label: Posts Without Index - field: title - pattern: front matter post - - label: Drafts - field: draft - pattern: true - view_groups: - - label: Year - field: date - pattern: '\d{4}' - - label: Drafts - field: draft - fields: - - label: Title - name: title - widget: string - - label: Draft - name: draft - widget: boolean - default: false - - label: Publish Date - name: date - widget: datetime - date_format: yyyy-MM-dd - time_format: 'HH:mm' - format: 'yyyy-MM-dd HH:mm' - - label: Cover Image - name: image - widget: image - required: false - - label: Body - name: body - widget: markdown - hint: Main content goes here. - - name: faq - label: FAQ - folder: _faqs - create: true - fields: - - label: Question - name: title - widget: string - - label: Answer - name: body - widget: markdown - - name: posts - label: Posts - label_singular: Post - widget: list - summary: '{{fields.post | split(''|'', ''$1'')}}' - fields: - - label: Related Post - name: post - widget: relationKitchenSinkPost - collection: posts - display_fields: - - title - - date - search_fields: - - title - - body - value_field: '{{title}}|{{date}}' - - name: settings - label: Settings - delete: false - editor: - preview: false - files: - - name: general - label: Site Settings - file: _data/settings.json - description: General Site Settings - fields: - - label: Number of posts on frontpage - name: front_limit - widget: number - min: 1 - max: 10 - - label: Global title - name: site_title - widget: string - - label: Post Settings - name: posts - widget: object - fields: - - label: Number of posts on frontpage - name: front_limit - widget: number - min: 1 - max: 10 - - label: Default Author - name: author - widget: string - - label: Default Thumbnail - name: thumb - widget: image - required: false - - name: authors - label: Authors - file: _data/authors.yml - description: Author descriptions - fields: - - name: authors - label: Authors - label_singular: Author - widget: list - fields: - - label: Name - name: name - widget: string - hint: First and Last - - label: Description - name: description - widget: text - - name: kitchenSink - label: Kitchen Sink - folder: _sink - create: true - fields: - - label: Related Post - name: post - widget: relationKitchenSinkPost - collection: posts - display_fields: - - title - - date - search_fields: - - title - - body - value_field: title - - label: Title - name: title - widget: string - - label: Boolean - name: boolean - widget: boolean - default: true - - label: Map - name: map - widget: map - - label: Text - name: text - widget: text - hint: 'Plain text, not markdown' - - label: Number - name: number - widget: number - hint: To infinity and beyond! - - label: Markdown - name: markdown - widget: markdown - - label: Datetime - name: datetime - widget: datetime - - label: Color - name: color - widget: color - - label: Color string editable and alpha enabled - name: colorEditable - widget: color - enable_alpha: true - allow_input: true - - label: Image - name: image - widget: image - - label: File - name: file - widget: file - - label: Select - name: select - widget: select - options: - - a - - b - - c - - label: Select multiple - name: select_multiple - widget: select - options: - - a - - b - - c - multiple: true - - label: Select numeric - name: select_numeric - widget: select - options: - - label: One - value: 1 - - label: Two - value: 2 - - label: Three - value: 3 - - label: Hidden - name: hidden - widget: hidden - default: hidden - - label: Object - name: object - widget: object - collapsed: true - fields: - - label: Related Post - name: post - widget: relationKitchenSinkPost - collection: posts - search_fields: - - title - - body - value_field: title - - label: String - name: string - widget: string - - label: Boolean - name: boolean - widget: boolean - default: false - - label: Text - name: text - widget: text - - label: Number - name: number - widget: number - - label: Markdown - name: markdown - widget: markdown - - label: Datetime - name: datetime - widget: datetime - - label: Image - name: image - widget: image - - label: File - name: file - widget: file - - label: Select - name: select - widget: select - options: - - a - - b - - c - - label: List - name: list - widget: list - fields: - - label: String - name: string - widget: string - - label: Boolean - name: boolean - widget: boolean - - label: Text - name: text - widget: text - - label: Number - name: number - widget: number - - label: Markdown - name: markdown - widget: markdown - - label: Datetime - name: datetime - widget: datetime - - label: Image - name: image - widget: image - - label: File - name: file - widget: file - - label: Select - name: select - widget: select - options: - - a - - b - - c - - label: Object - name: object - widget: object - fields: - - label: String - name: string - widget: string - - label: Boolean - name: boolean - widget: boolean - - label: Text - name: text - widget: text - - label: Number - name: number - widget: number - - label: Markdown - name: markdown - widget: markdown - - label: Datetime - name: datetime - widget: datetime - - label: Image - name: image - widget: image - - label: File - name: file - widget: file - - label: Select - name: select - widget: select - options: - - a - - b - - c - - label: List - name: list - widget: list - fields: - - label: Related Post - name: post - widget: relationKitchenSinkPost - collection: posts - search_fields: - - title - - body - value_field: title - - label: String - name: string - widget: string - - label: Boolean - name: boolean - widget: boolean - - label: Text - name: text - widget: text - - label: Number - name: number - widget: number - - label: Markdown - name: markdown - widget: text - - label: Datetime - name: datetime - widget: datetime - - label: Image - name: image - widget: image - - label: File - name: file - widget: file - - label: Select - name: select - widget: select - options: - - a - - b - - c - - label: Hidden - name: hidden - widget: hidden - default: hidden - - label: Object - name: object - widget: object - fields: - - label: String - name: string - widget: string - - label: Boolean - name: boolean - widget: boolean - - label: Text - name: text - widget: text - - label: Number - name: number - widget: number - - label: Markdown - name: markdown - widget: text - - label: Datetime - name: datetime - widget: datetime - - label: Image - name: image - widget: image - - label: File - name: file - widget: file - - label: Select - name: select - widget: select - options: - - a - - b - - c - - label: Typed List - name: typed_list - widget: list - types: - - label: Type 1 Object - name: type_1_object - widget: object - fields: - - label: String - name: string - widget: string - - label: Boolean - name: boolean - widget: boolean - - label: Text - name: text - widget: text - - label: Type 2 Object - name: type_2_object - widget: object - fields: - - label: Number - name: number - widget: number - - label: Select - name: select - widget: select - options: - - a - - b - - c - - label: Datetime - name: datetime - widget: datetime - - label: Markdown - name: markdown - widget: text - - label: Type 3 Object - name: type_3_object - widget: object - fields: - - label: Image - name: image - widget: image - - label: File - name: file - widget: file diff --git a/core/dev-test/backends/azure/index.html b/core/dev-test/backends/azure/index.html deleted file mode 100644 index 4c589562..00000000 --- a/core/dev-test/backends/azure/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Static CMS - Azure Development Test - - - - - - diff --git a/core/dev-test/config.yml b/core/dev-test/config.yml index 19eeebe1..48acca61 100644 --- a/core/dev-test/config.yml +++ b/core/dev-test/config.yml @@ -497,6 +497,8 @@ collections: label: Site Settings file: _data/settings.json description: General Site Settings + editor: + preview: true fields: - label: Number of posts on frontpage name: front_limit @@ -526,6 +528,8 @@ collections: label: Authors file: _data/authors.yml description: Author descriptions + editor: + preview: true fields: - name: authors label: Authors diff --git a/core/dev-test/index.js b/core/dev-test/index.js index 74b83982..948e4454 100644 --- a/core/dev-test/index.js +++ b/core/dev-test/index.js @@ -1,98 +1,104 @@ // Register all the things CMS.init(); -const PostPreview = createClass({ - render: function () { - var entry = this.props.entry; - return h( - 'div', +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 GeneralPreview = ({ widgetsFor, getAsset, entry }) => { + const title = entry.data.site_title; + const posts = entry.data.posts; + const thumb = posts && posts.thumb; + + const [thumbUrl, setThumbUrl] = useState(''); + + useEffect(() => { + let alive = true; + + const loadThumb = async () => { + const thumbAsset = await getAsset(thumb); + if (alive) { + setThumbUrl(thumbAsset.toString()); + } + }; + + loadThumb(); + + return () => { + alive = false; + }; + }, [thumb]); + + return h( + 'div', + {}, + h('h1', {}, title), + h( + 'dl', {}, - h( + 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', - { className: 'cover' }, - h('h1', {}, entry.data.title), - this.props.widgetFor('image'), - ), - h('p', {}, h('small', {}, 'Written ' + entry.data.date)), - h('div', { className: 'text' }, this.props.widgetFor('body')), - ); - }, -}); + { key: index }, + h('hr', {}), + h('strong', {}, author.data.name), + author.widgets.description, + ); + }), + ); +}; -// TODO Hook this back up, getAsset returns a promise now -const GeneralPreview = createClass({ - render: function () { - const entry = this.props.entry; - const title = entry.data.site_title; - const posts = entry.data.posts; - const thumb = posts && posts.thumb; +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; +}; - return h( - 'div', - {}, - h('h1', {}, title), - h( - 'dl', - {}, - h('dt', {}, 'Posts on Frontpage'), - h('dd', {}, this.props.widgetsFor('posts').widgets.front_limit || 0), - - h('dt', {}, 'Default Author'), - h('dd', {}, this.props.widgetsFor('posts').data.author || 'None'), - - h('dt', {}, 'Default Thumbnail'), - h('dd', {}, thumb && h('img', { src: this.props.getAsset(thumb).toString() })), - ), - ); - }, -}); - -const AuthorsPreview = createClass({ - render: function () { - return h( - 'div', - {}, - h('h1', {}, 'Authors'), - this.props.widgetsFor('authors').map(function (author, index) { - return h( - 'div', - { key: index }, - h('hr', {}), - h('strong', {}, author.data.name), - author.widgets.description, - ); - }), - ); - }, -}); - -const RelationKitchenSinkPostPreview = createClass({ - render: function () { - // 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 { value, fieldsMetaData } = this.props; - 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.registerPreviewStyle('.toastui-editor-contents h1 { color: blue }', { raw: true }); CMS.registerPreviewTemplate('posts', PostPreview); -// CMS.registerPreviewTemplate('general', GeneralPreview); +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); @@ -104,3 +110,11 @@ CMS.registerAdditionalLink({ icon: 'page', }, }); +CMS.registerAdditionalLink({ + id: 'custom-page', + title: 'Custom Page', + data: CustomPage, + options: { + icon: 'page', + }, +}); diff --git a/core/src/actions/entries.ts b/core/src/actions/entries.ts index 0fe996d6..2d84abc9 100644 --- a/core/src/actions/entries.ts +++ b/core/src/actions/entries.ts @@ -68,7 +68,6 @@ export const DRAFT_CREATE_EMPTY = 'DRAFT_CREATE_EMPTY'; export const DRAFT_DISCARD = 'DRAFT_DISCARD'; export const DRAFT_CHANGE_FIELD = 'DRAFT_CHANGE_FIELD'; export const DRAFT_VALIDATION_ERRORS = 'DRAFT_VALIDATION_ERRORS'; -export const DRAFT_CLEAR_ERRORS = 'DRAFT_CLEAR_ERRORS'; export const DRAFT_LOCAL_BACKUP_RETRIEVED = 'DRAFT_LOCAL_BACKUP_RETRIEVED'; export const DRAFT_LOCAL_BACKUP_DELETE = 'DRAFT_LOCAL_BACKUP_DELETE'; export const DRAFT_CREATE_FROM_LOCAL_BACKUP = 'DRAFT_CREATE_FROM_LOCAL_BACKUP'; @@ -279,7 +278,7 @@ async function getAllEntries(state: RootState, collection: Collection) { const backend = currentBackend(configState.config); const integration = selectIntegration(state, collection.name, 'listEntries'); const provider = integration - ? getSearchIntegrationProvider(state.integrations, backend.getToken, integration) + ? getSearchIntegrationProvider(state.integrations, integration) : backend; if (!provider) { @@ -497,10 +496,6 @@ export function changeDraftFieldValidation(path: string, errors: FieldError[]) { } as const; } -export function clearFieldErrors() { - return { type: DRAFT_CLEAR_ERRORS } as const; -} - export function localBackupRetrieved(entry: Entry) { return { type: DRAFT_LOCAL_BACKUP_RETRIEVED, @@ -688,7 +683,7 @@ export function loadEntries(collection: Collection, page = 0) { const backend = currentBackend(configState.config); const integration = selectIntegration(state, collection.name, 'listEntries'); const provider = integration - ? getSearchIntegrationProvider(state.integrations, backend.getToken, integration) + ? getSearchIntegrationProvider(state.integrations, integration) : backend; if (!provider) { @@ -1142,7 +1137,6 @@ export type EntriesAction = ReturnType< | typeof discardDraft | typeof changeDraftField | typeof changeDraftFieldValidation - | typeof clearFieldErrors | typeof localBackupRetrieved | typeof loadLocalBackup | typeof deleteDraftLocalBackup diff --git a/core/src/actions/mediaLibrary.ts b/core/src/actions/mediaLibrary.ts index fff37f9b..81a7a9c4 100644 --- a/core/src/actions/mediaLibrary.ts +++ b/core/src/actions/mediaLibrary.ts @@ -1,9 +1,8 @@ import { currentBackend } from '../backend'; import confirm from '../components/UI/Confirm'; -import { getMediaIntegrationProvider } from '../integrations'; import { sanitizeSlug } from '../lib/urlHelper'; import { basename, getBlobSHA } from '../lib/util'; -import { selectIntegration } from '../reducers'; +import { selectMediaFilePath, selectMediaFilePublicPath } from '../lib/util/media.util'; import { selectEditingDraft } from '../reducers/entries'; import { selectMediaDisplayURL, selectMediaFiles } from '../reducers/mediaLibrary'; import { addSnackbar } from '../store/slices/snackbars'; @@ -11,13 +10,12 @@ import { createAssetProxy } from '../valueObjects/AssetProxy'; import { addDraftEntryMediaFile, removeDraftEntryMediaFile } from './entries'; import { addAsset, removeAsset } from './media'; import { waitUntilWithTimeout } from './waitUntil'; -import { selectMediaFilePath, selectMediaFilePublicPath } from '../lib/util/media.util'; import type { AnyAction } from 'redux'; import type { ThunkDispatch } from 'redux-thunk'; import type { - Field, DisplayURLState, + Field, ImplementationMediaFile, MediaFile, MediaLibraryInstance, @@ -78,7 +76,6 @@ export function openMediaLibrary( payload: { controlID?: string; forImage?: boolean; - privateUpload?: boolean; value?: string | string[]; allowMultiple?: boolean; replaceIndex?: number; @@ -134,10 +131,8 @@ export function removeInsertedMedia(controlID: string) { return { type: MEDIA_REMOVE_INSERTED, payload: { controlID } } as const; } -export function loadMedia( - opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {}, -) { - const { delay = 0, query = '', page = 1, privateUpload = false } = opts; +export function loadMedia(opts: { delay?: number; query?: string; page?: number } = {}) { + const { delay = 0, page = 1 } = opts; return async (dispatch: ThunkDispatch, getState: () => RootState) => { const state = getState(); const config = state.config.config; @@ -146,32 +141,6 @@ export function loadMedia( } const backend = currentBackend(config); - const integration = selectIntegration(state, null, 'assetStore'); - if (integration) { - const provider = getMediaIntegrationProvider( - state.integrations, - backend.getToken, - integration, - ); - if (!provider) { - throw new Error('Provider not found'); - } - - dispatch(mediaLoading(page)); - try { - const files = await provider.retrieve(query, page, privateUpload); - const mediaLoadedOpts = { - page, - canPaginate: true, - dynamicSearch: true, - dynamicSearchQuery: query, - privateUpload, - }; - return dispatch(mediaLoaded(files, mediaLoadedOpts)); - } catch (error) { - return dispatch(mediaLoadFailed({ privateUpload })); - } - } dispatch(mediaLoading(page)); function loadFunction() { @@ -225,7 +194,7 @@ function createMediaFileFromAsset({ } export function persistMedia(file: File, opts: MediaOptions = {}) { - const { privateUpload, field } = opts; + const { field } = opts; return async (dispatch: ThunkDispatch, getState: () => RootState) => { const state = getState(); const config = state.config.config; @@ -234,7 +203,6 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { } const backend = currentBackend(config); - const integration = selectIntegration(state, null, 'assetStore'); const files: MediaFile[] = selectMediaFiles(state, field); const fileName = sanitizeSlug(file.name.toLowerCase(), config.slug); const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName); @@ -247,7 +215,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { * expect file names to be unique. If an asset store is in use, file names * may not be unique, so we forego this check. */ - if (!integration && existingFile) { + if (existingFile) { if ( !(await confirm({ title: 'mediaLibrary.mediaLibrary.alreadyExistsTitle', @@ -260,60 +228,28 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { ) { return; } else { - await dispatch(deleteMedia(existingFile, { privateUpload })); + await dispatch(deleteMedia(existingFile)); } } - if (integration || !editingDraft) { + if (!editingDraft) { dispatch(mediaPersisting()); } try { - let assetProxy: AssetProxy; - if (integration) { - try { - const provider = getMediaIntegrationProvider( - state.integrations, - backend.getToken, - integration, - ); - if (!provider) { - throw new Error('Provider not found'); - } - - const response = await provider.upload(file, privateUpload); - assetProxy = createAssetProxy({ - url: response.asset.url, - path: response.asset.url, - }); - } catch (error) { - assetProxy = createAssetProxy({ - file, - path: fileName, - }); - } - } else if (privateUpload) { - console.error('The Private Upload option is only available for Asset Store Integration') - throw new Error('The Private Upload option is only available for Asset Store Integration'); - } else { - const entry = state.entryDraft.entry; - const collection = entry?.collection ? state.collections[entry.collection] : null; - const path = selectMediaFilePath(config, collection, entry, fileName, field); - assetProxy = createAssetProxy({ - file, - path, - field, - }); - } + const entry = state.entryDraft.entry; + const collection = entry?.collection ? state.collections[entry.collection] : null; + const path = selectMediaFilePath(config, collection, entry, fileName, field); + const assetProxy = createAssetProxy({ + file, + path, + field, + }); dispatch(addAsset(assetProxy)); let mediaFile: ImplementationMediaFile; - if (integration) { - const id = await getBlobSHA(file); - // integration assets are persisted immediately, thus draft is false - mediaFile = createMediaFileFromAsset({ id, file, assetProxy, draft: false }); - } else if (editingDraft) { + if (editingDraft) { const id = await getBlobSHA(file); mediaFile = createMediaFileFromAsset({ id, @@ -326,7 +262,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { mediaFile = await backend.persistMedia(config, assetProxy); } - return dispatch(mediaPersisted(mediaFile, { privateUpload })); + return dispatch(mediaPersisted(mediaFile)); } catch (error) { console.error(error); dispatch( @@ -340,13 +276,12 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { }, }), ); - return dispatch(mediaPersistFailed({ privateUpload })); + return dispatch(mediaPersistFailed()); } }; } -export function deleteMedia(file: MediaFile, opts: MediaOptions = {}) { - const { privateUpload } = opts; +export function deleteMedia(file: MediaFile) { return async (dispatch: ThunkDispatch, getState: () => RootState) => { const state = getState(); const config = state.config.config; @@ -355,42 +290,6 @@ export function deleteMedia(file: MediaFile, opts: MediaOptions = {}) { } const backend = currentBackend(config); - const integration = selectIntegration(state, null, 'assetStore'); - if (integration) { - const provider = getMediaIntegrationProvider( - state.integrations, - backend.getToken, - integration, - ); - if (!provider) { - throw new Error('Provider not found'); - } - - dispatch(mediaDeleting()); - - try { - await provider.delete(file.id); - return dispatch(mediaDeleted(file, { privateUpload })); - } catch (error: unknown) { - console.error(error); - - if (error instanceof Error) { - dispatch( - addSnackbar({ - type: 'error', - message: { - key: 'ui.toast.onFailToDeleteMedia', - options: { - details: error.message, - }, - }, - }), - ); - } - - return dispatch(mediaDeleteFailed({ privateUpload })); - } - } try { if (file.draft) { @@ -490,7 +389,6 @@ export function loadMediaDisplayURL(file: MediaFile) { function mediaLibraryOpened(payload: { controlID?: string; forImage?: boolean; - privateUpload?: boolean; value?: string | string[]; replaceIndex?: number; allowMultiple?: boolean; @@ -516,7 +414,6 @@ export function mediaLoading(page: number) { } export interface MediaOptions { - privateUpload?: boolean; field?: Field; page?: number; canPaginate?: boolean; @@ -531,43 +428,38 @@ export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions } as const; } -export function mediaLoadFailed(opts: MediaOptions = {}) { - const { privateUpload } = opts; - return { type: MEDIA_LOAD_FAILURE, payload: { privateUpload } } as const; +export function mediaLoadFailed() { + return { type: MEDIA_LOAD_FAILURE } as const; } export function mediaPersisting() { return { type: MEDIA_PERSIST_REQUEST } as const; } -export function mediaPersisted(file: ImplementationMediaFile, opts: MediaOptions = {}) { - const { privateUpload } = opts; +export function mediaPersisted(file: ImplementationMediaFile) { return { type: MEDIA_PERSIST_SUCCESS, - payload: { file, privateUpload }, + payload: { file }, } as const; } -export function mediaPersistFailed(opts: MediaOptions = {}) { - const { privateUpload } = opts; - return { type: MEDIA_PERSIST_FAILURE, payload: { privateUpload } } as const; +export function mediaPersistFailed() { + return { type: MEDIA_PERSIST_FAILURE } as const; } export function mediaDeleting() { return { type: MEDIA_DELETE_REQUEST } as const; } -export function mediaDeleted(file: MediaFile, opts: MediaOptions = {}) { - const { privateUpload } = opts; +export function mediaDeleted(file: MediaFile) { return { type: MEDIA_DELETE_SUCCESS, - payload: { file, privateUpload }, + payload: { file }, } as const; } -export function mediaDeleteFailed(opts: MediaOptions = {}) { - const { privateUpload } = opts; - return { type: MEDIA_DELETE_FAILURE, payload: { privateUpload } } as const; +export function mediaDeleteFailed() { + return { type: MEDIA_DELETE_FAILURE } as const; } export function mediaDisplayURLRequest(key: string) { diff --git a/core/src/actions/search.ts b/core/src/actions/search.ts index 0c61f1e3..dd725f5d 100644 --- a/core/src/actions/search.ts +++ b/core/src/actions/search.ts @@ -122,7 +122,7 @@ export function searchEntries(searchTerm: string, searchCollections: string[], p dispatch(searchingEntries(searchTerm, allCollections, page)); const searchPromise = integration - ? getSearchIntegrationProvider(state.integrations, backend.getToken, integration)?.search( + ? getSearchIntegrationProvider(state.integrations, integration)?.search( collections, searchTerm, page, @@ -179,7 +179,7 @@ export function query( } const queryPromise = integration - ? getSearchIntegrationProvider(state.integrations, backend.getToken, integration)?.searchBy( + ? getSearchIntegrationProvider(state.integrations, integration)?.searchBy( JSON.stringify(searchFields.map(f => `data.${f}`)), collectionName, searchTerm, diff --git a/core/src/backend.ts b/core/src/backend.ts index 4eb89b80..1ddb088b 100644 --- a/core/src/backend.ts +++ b/core/src/backend.ts @@ -41,9 +41,8 @@ import { } from './lib/util/collection.util'; import { selectMediaFilePath } from './lib/util/media.util'; import { set } from './lib/util/object.util'; -import { selectIntegration } from './reducers/integrations'; -import { createEntry } from './valueObjects/Entry'; import { dateParsers, expandPath, extractTemplateVars } from './lib/widgets/stringTemplate'; +import { createEntry } from './valueObjects/Entry'; import type { BackendClass, @@ -778,9 +777,8 @@ export class Backend { throw new Error('Config not loaded'); } - const integration = selectIntegration(state.integrations, null, 'assetStore'); const mediaFolders = selectMediaFolders(configState.config, collection, entry); - if (mediaFolders.length > 0 && !integration) { + if (mediaFolders.length > 0) { const files = await Promise.all( mediaFolders.map(folder => this.implementation.getMedia(folder)), ); diff --git a/core/src/backends/azure/API.ts b/core/src/backends/azure/API.ts deleted file mode 100644 index 1a6c4a1b..00000000 --- a/core/src/backends/azure/API.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { Base64 } from 'js-base64'; -import partial from 'lodash/partial'; -import result from 'lodash/result'; -import trim from 'lodash/trim'; -import trimStart from 'lodash/trimStart'; -import { basename, dirname } from 'path'; - -import { - APIError, - localForage, - readFile, - readFileMetadata, - requestWithBackoff, - responseParser, - unsentRequest, -} from '../../lib/util'; - -import type { DataFile, PersistOptions } from '../../interface'; -import type { ApiRequest } from '../../lib/util'; -import type { ApiRequestObject } from '../../lib/util/API'; -import type AssetProxy from '../../valueObjects/AssetProxy'; - -export const API_NAME = 'Azure DevOps'; - -const API_VERSION = 'api-version'; - -type AzureUser = { - coreAttributes?: { - Avatar?: { value?: { value?: string } }; - DisplayName?: { value?: string }; - EmailAddress?: { value?: string }; - }; -}; - -type AzureGitItem = { - objectId: string; - gitObjectType: AzureObjectType; - path: string; -}; - -// This does not match Azure documentation, but it is what comes back from some calls -// PullRequest as an example is documented as returning PullRequest[], but it actually -// returns that inside of this value prop in the json -interface AzureArray { - value: T[]; -} - -enum AzureCommitChangeType { - ADD = 'add', - DELETE = 'delete', - RENAME = 'rename', - EDIT = 'edit', -} - -enum AzureItemContentType { - BASE64 = 'base64encoded', -} - -enum AzureObjectType { - BLOB = 'blob', - TREE = 'tree', -} - -type AzureRef = { - name: string; - objectId: string; -}; - -type AzureCommit = { - author: { - date: string; - email: string; - name: string; - }; -}; - -function getChangeItem(item: AzureCommitItem) { - switch (item.action) { - case AzureCommitChangeType.ADD: - return { - changeType: AzureCommitChangeType.ADD, - item: { path: item.path }, - newContent: { - content: item.base64Content, - contentType: AzureItemContentType.BASE64, - }, - }; - case AzureCommitChangeType.EDIT: - return { - changeType: AzureCommitChangeType.EDIT, - item: { path: item.path }, - newContent: { - content: item.base64Content, - contentType: AzureItemContentType.BASE64, - }, - }; - case AzureCommitChangeType.DELETE: - return { - changeType: AzureCommitChangeType.DELETE, - item: { path: item.path }, - }; - case AzureCommitChangeType.RENAME: - return { - changeType: AzureCommitChangeType.RENAME, - item: { path: item.path }, - sourceServerItem: item.oldPath, - }; - default: - return {}; - } -} - -type AzureCommitItem = { - action: AzureCommitChangeType; - base64Content?: string; - text?: string; - path: string; - oldPath?: string; -}; - -interface AzureApiConfig { - apiRoot: string; - repo: { org: string; project: string; repoName: string }; - branch: string; - apiVersion: string; -} - -export default class API { - apiVersion: string; - token: string; - branch: string; - endpointUrl: string; - - constructor(config: AzureApiConfig, token: string) { - const { repo } = config; - const apiRoot = trim(config.apiRoot, '/'); - this.endpointUrl = `${apiRoot}/${repo.org}/${repo.project}/_apis/git/repositories/${repo.repoName}`; - this.token = token; - this.branch = config.branch; - this.apiVersion = config.apiVersion; - } - - withHeaders = (req: ApiRequest) => { - const withHeaders = unsentRequest.withHeaders( - { - Authorization: `Bearer ${this.token}`, - 'Content-Type': 'application/json; charset=utf-8', - }, - req, - ); - return withHeaders; - }; - - withAzureFeatures = (req: ApiRequestObject) => { - if (API_VERSION in (req.params ?? {})) { - return req; - } - - const withParams = unsentRequest.withParams( - { - [API_VERSION]: `${this.apiVersion}`, - }, - req, - ); - - return withParams; - }; - - buildRequest = (req: ApiRequest) => { - const withHeaders = this.withHeaders(req); - const withAzureFeatures = this.withAzureFeatures(withHeaders); - if ('cache' in withAzureFeatures) { - return withAzureFeatures; - } else { - const withNoCache = unsentRequest.withNoCache(withAzureFeatures); - return withNoCache; - } - }; - - request = (req: ApiRequest): Promise => { - try { - return requestWithBackoff(this, req); - } catch (error: unknown) { - if (error instanceof Error) { - throw new APIError(error.message, null, API_NAME); - } - - throw new APIError('Unknown api error', null, API_NAME); - } - }; - - responseToJSON = responseParser({ format: 'json', apiName: API_NAME }); - responseToBlob = responseParser({ format: 'blob', apiName: API_NAME }); - responseToText = responseParser({ format: 'text', apiName: API_NAME }); - - requestJSON = (req: ApiRequest) => this.request(req).then(this.responseToJSON) as Promise; - requestText = (req: ApiRequest) => this.request(req).then(this.responseToText) as Promise; - - toBase64 = (str: string) => Promise.resolve(Base64.encode(str)); - fromBase64 = (str: string) => Base64.decode(str); - - branchToRef = (branch: string): string => `refs/heads/${branch}`; - refToBranch = (ref: string): string => ref.slice('refs/heads/'.length); - - user = async () => { - const result = await this.requestJSON({ - url: 'https://app.vssps.visualstudio.com/_apis/profile/profiles/me', - params: { [API_VERSION]: '6.1-preview.2' }, - }); - - const name = result.coreAttributes?.DisplayName?.value; - const email = result.coreAttributes?.EmailAddress?.value; - const url = result.coreAttributes?.Avatar?.value?.value; - const user = { - name: name || email || '', - avatar_url: `data:image/png;base64,${url}`, - email, - }; - return user; - }; - - async readFileMetadata( - path: string, - sha: string | null | undefined, - { branch = this.branch } = {}, - ) { - const fetchFileMetadata = async () => { - try { - const { value } = await this.requestJSON>({ - url: `${this.endpointUrl}/commits/`, - params: { - 'searchCriteria.itemPath': path, - 'searchCriteria.itemVersion.version': branch, - 'searchCriteria.$top': '1', - }, - }); - const [commit] = value; - - return { - author: commit.author.name || commit.author.email, - updatedOn: commit.author.date, - }; - } catch (error) { - return { author: '', updatedOn: '' }; - } - }; - - const fileMetadata = await readFileMetadata(sha, fetchFileMetadata, localForage); - return fileMetadata; - } - - readFile = ( - path: string, - sha?: string | null, - { parseText = true, branch = this.branch } = {}, - ) => { - const fetchContent = () => { - return this.request({ - url: `${this.endpointUrl}/items/`, - params: { version: branch, path }, - cache: 'no-store', - }).then(parseText ? this.responseToText : this.responseToBlob); - }; - - return readFile(sha, fetchContent, localForage, parseText); - }; - - listFiles = async (path: string, recursive: boolean, branch = this.branch) => { - try { - const { value: items } = await this.requestJSON>({ - url: `${this.endpointUrl}/items/`, - params: { - version: branch, - scopePath: path, - recursionLevel: recursive ? 'full' : 'oneLevel', - }, - }); - - const files = items - .filter(item => item.gitObjectType === AzureObjectType.BLOB) - .map(file => ({ - id: file.objectId, - path: trimStart(file.path, '/'), - name: basename(file.path), - })); - return files; - } catch (err: any) { - if (err && err.status === 404) { - console.info('This 404 was expected and handled appropriately.'); - return []; - } else { - throw err; - } - } - }; - - async getRef(branch: string = this.branch) { - const { value: refs } = await this.requestJSON>({ - url: `${this.endpointUrl}/refs`, - params: { - $top: '1', // There's only one ref, so keep the payload small - filter: 'heads/' + branch, - }, - }); - - return refs.find(b => b.name == this.branchToRef(branch))!; - } - - async uploadAndCommit( - items: AzureCommitItem[], - comment: string, - branch: string, - newBranch: boolean, - ) { - const ref = await this.getRef(newBranch ? this.branch : branch); - - const refUpdate = [ - { - name: this.branchToRef(branch), - oldObjectId: ref.objectId, - }, - ]; - - const changes = items.map(item => getChangeItem(item)); - const commits = [{ comment, changes }]; - const push = { - refUpdates: refUpdate, - commits, - }; - - return this.requestJSON({ - url: `${this.endpointUrl}/pushes`, - method: 'POST', - body: JSON.stringify(push), - }); - } - - async getCommitItems(files: { path: string; newPath?: string }[], branch: string) { - const items = await Promise.all( - files.map(async file => { - const [base64Content, fileExists] = await Promise.all([ - result(file, 'toBase64', partial(this.toBase64, (file as DataFile).raw)), - this.isFileExists(file.path, branch), - ]); - - const path = file.newPath || file.path; - const oldPath = file.path; - const renameOrEdit = - path !== oldPath ? AzureCommitChangeType.RENAME : AzureCommitChangeType.EDIT; - - const action = fileExists ? renameOrEdit : AzureCommitChangeType.ADD; - return { - action, - base64Content, - path, - oldPath, - } as AzureCommitItem; - }), - ); - - // move children - for (const item of items.filter(i => i.oldPath && i.action === AzureCommitChangeType.RENAME)) { - const sourceDir = dirname(item.oldPath as string); - const destDir = dirname(item.path); - const children = await this.listFiles(sourceDir, true, branch); - children - .filter(file => file.path !== item.oldPath) - .forEach(file => { - items.push({ - action: AzureCommitChangeType.RENAME, - path: file.path.replace(sourceDir, destDir), - oldPath: file.path, - }); - }); - } - - return items; - } - - async persistFiles(dataFiles: DataFile[], mediaFiles: AssetProxy[], options: PersistOptions) { - const files = [...dataFiles, ...mediaFiles]; - const items = await this.getCommitItems(files, this.branch); - - return this.uploadAndCommit(items, options.commitMessage, this.branch, true); - } - - async deleteFiles(paths: string[], comment: string) { - const ref = await this.getRef(this.branch); - const refUpdate = { - name: ref.name, - oldObjectId: ref.objectId, - }; - - const changes = paths.map(path => - getChangeItem({ action: AzureCommitChangeType.DELETE, path }), - ); - const commits = [{ comment, changes }]; - const push = { - refUpdates: [refUpdate], - commits, - }; - - return this.requestJSON({ - url: `${this.endpointUrl}/pushes`, - method: 'POST', - body: JSON.stringify(push), - }); - } - - async isFileExists(path: string, branch: string) { - try { - await this.requestText({ - url: `${this.endpointUrl}/items/`, - params: { version: branch, path }, - cache: 'no-store', - }); - return true; - } catch (error) { - if (error instanceof APIError && error.status === 404) { - return false; - } - throw error; - } - } -} diff --git a/core/src/backends/azure/AuthenticationPage.tsx b/core/src/backends/azure/AuthenticationPage.tsx deleted file mode 100644 index bc7c9b3e..00000000 --- a/core/src/backends/azure/AuthenticationPage.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import alert from '../../components/UI/Alert'; -import AuthenticationPage from '../../components/UI/AuthenticationPage'; -import Icon from '../../components/UI/Icon'; -import { ImplicitAuthenticator } from '../../lib/auth'; - -import type { MouseEvent } from 'react'; -import type { AuthenticationPageProps, TranslatedProps } from '../../interface'; - -const AzureAuthenticationPage = ({ - inProgress = false, - config, - clearHash, - onLogin, - t, -}: TranslatedProps) => { - const [loginError, setLoginError] = useState(null); - - const auth = useMemo( - () => - new ImplicitAuthenticator({ - base_url: `https://login.microsoftonline.com/${config.backend.tenant_id}`, - auth_endpoint: 'oauth2/authorize', - app_id: config.backend.app_id, - clearHash, - }), - [clearHash, config.backend.app_id, config.backend.tenant_id], - ); - - useEffect(() => { - // Complete implicit authentication if we were redirected back to from the provider. - auth.completeAuth((err, data) => { - if (err) { - alert({ - title: 'auth.errors.authTitle', - body: { key: 'auth.errors.authBody', options: { details: err.toString() } }, - }); - return; - } else if (data) { - onLogin(data); - } - }); - }, [auth, onLogin]); - - const handleLogin = useCallback( - (e: MouseEvent) => { - e.preventDefault(); - auth.authenticate( - { - scope: 'vso.code_full,user.read', - resource: '499b84ac-1321-427f-aa17-267ca6975798', - prompt: 'select_account', - }, - (err, data) => { - if (err) { - setLoginError(err.toString()); - } else if (data) { - onLogin(data); - } - }, - ); - }, - [auth, onLogin], - ); - - return ( - } - buttonContent={inProgress ? t('auth.loggingIn') : t('auth.loginWithAzure')} - t={t} - /> - ); -}; - -export default AzureAuthenticationPage; diff --git a/core/src/backends/azure/implementation.ts b/core/src/backends/azure/implementation.ts deleted file mode 100644 index 5e001af9..00000000 --- a/core/src/backends/azure/implementation.ts +++ /dev/null @@ -1,265 +0,0 @@ -import trim from 'lodash/trim'; -import trimStart from 'lodash/trimStart'; -import semaphore from 'semaphore'; - -import { BackendClass } from '../../interface'; -import { - asyncLock, - basename, - entriesByFiles, - entriesByFolder, - filterByExtension, - getBlobSHA, - getMediaAsBlob, - getMediaDisplayURL, -} from '../../lib/util'; -import API, { API_NAME } from './API'; -import AuthenticationPage from './AuthenticationPage'; - -import type { Semaphore } from 'semaphore'; -import type { - BackendEntry, - BackendInitializerOptions, - Config, - Credentials, - DisplayURL, - ImplementationEntry, - ImplementationFile, - ImplementationMediaFile, - PersistOptions, - User, -} from '../../interface'; -import type { AsyncLock, Cursor } from '../../lib/util'; -import type AssetProxy from '../../valueObjects/AssetProxy'; - -const MAX_CONCURRENT_DOWNLOADS = 10; - -function parseAzureRepo(config: Config) { - const { repo } = config.backend; - - if (typeof repo !== 'string') { - throw new Error('The Azure backend needs a "repo" in the backend configuration.'); - } - - const parts = repo.split('/'); - if (parts.length !== 3) { - throw new Error('The Azure backend must be in a the format of {org}/{project}/{repo}'); - } - - const [org, project, repoName] = parts; - return { - org, - project, - repoName, - }; -} - -export default class Azure extends BackendClass { - lock: AsyncLock; - api?: API; - options: BackendInitializerOptions; - repo: { - org: string; - project: string; - repoName: string; - }; - branch: string; - apiRoot: string; - apiVersion: string; - token: string | null; - mediaFolder: string; - - _mediaDisplayURLSem?: Semaphore; - - constructor(config: Config, options: BackendInitializerOptions) { - super(config, options); - this.options = { - ...options, - }; - - this.repo = parseAzureRepo(config); - this.branch = config.backend.branch || 'main'; - this.apiRoot = config.backend.api_root || 'https://dev.azure.com'; - this.apiVersion = config.backend.api_version || '6.1-preview'; - this.token = ''; - this.mediaFolder = trim(config.media_folder, '/'); - this.lock = asyncLock(); - } - - isGitBackend() { - return true; - } - - async status(): Promise<{ - auth: { status: boolean }; - api: { status: boolean; statusPage: string }; - }> { - const auth = - (await this.api!.user() - .then(user => !!user) - .catch(e => { - console.warn('Failed getting Azure user', e); - return false; - })) || false; - - return { auth: { status: auth }, api: { status: true, statusPage: '' } }; - } - - authComponent() { - return AuthenticationPage; - } - - restoreUser(user: User) { - return this.authenticate(user); - } - - async authenticate(state: Credentials) { - this.token = state.token as string; - this.api = new API( - { - apiRoot: this.apiRoot, - apiVersion: this.apiVersion, - repo: this.repo, - branch: this.branch, - }, - this.token, - ); - - const user = await this.api.user(); - return { token: state.token as string, ...user }; - } - - /** - * Log the user out by forgetting their access token. - * TODO: *Actual* logout by redirecting to: - * https://login.microsoftonline.com/{tenantId}/oauth2/logout?client_id={clientId}&post_logout_redirect_uri={baseUrl} - */ - logout() { - this.token = null; - return; - } - - getToken() { - return Promise.resolve(this.token); - } - - async entriesByFolder(folder: string, extension: string, depth: number) { - const listFiles = async () => { - const files = await this.api!.listFiles(folder, depth > 1); - const filtered = files.filter(file => filterByExtension({ path: file.path }, extension)); - return filtered.map(file => ({ - id: file.id, - path: file.path, - })); - }; - - const entries = await entriesByFolder( - listFiles, - this.api!.readFile.bind(this.api!), - this.api!.readFileMetadata.bind(this.api), - API_NAME, - ); - return entries; - } - - entriesByFiles(files: ImplementationFile[]) { - return entriesByFiles( - files, - this.api!.readFile.bind(this.api!), - this.api!.readFileMetadata.bind(this.api), - API_NAME, - ); - } - - async getEntry(path: string) { - const data = (await this.api!.readFile(path)) as string; - return { - file: { path }, - data, - }; - } - - async getMedia() { - const files = await this.api!.listFiles(this.mediaFolder, false); - const mediaFiles = await Promise.all( - files.map(async ({ id, path, name }) => { - const blobUrl = await this.getMediaDisplayURL({ id, path }); - return { id, name, displayURL: blobUrl, path }; - }), - ); - return mediaFiles; - } - - getMediaDisplayURL(displayURL: DisplayURL) { - this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS); - return getMediaDisplayURL( - displayURL, - this.api!.readFile.bind(this.api!), - this._mediaDisplayURLSem, - ); - } - - async getMediaFile(path: string) { - const name = basename(path); - const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!)); - const fileObj = new File([blob], name); - const url = URL.createObjectURL(fileObj); - const id = await getBlobSHA(blob); - - return { - id, - displayURL: url, - path, - name, - size: fileObj.size, - file: fileObj, - url, - }; - } - - async persistEntry(entry: BackendEntry, options: PersistOptions): Promise { - const mediaFiles: AssetProxy[] = entry.assets; - await this.api!.persistFiles(entry.dataFiles, mediaFiles, options); - } - - async persistMedia( - mediaFile: AssetProxy, - options: PersistOptions, - ): Promise { - const fileObj = mediaFile.fileObj as File; - - const [id] = await Promise.all([ - getBlobSHA(fileObj), - this.api!.persistFiles([], [mediaFile], options), - ]); - - const { path } = mediaFile; - const url = URL.createObjectURL(fileObj); - - return { - displayURL: url, - path: trimStart(path, '/'), - name: fileObj!.name, - size: fileObj!.size, - file: fileObj, - url, - id: id as string, - }; - } - - async deleteFiles(paths: string[], commitMessage: string) { - await this.api!.deleteFiles(paths, commitMessage); - } - - traverseCursor(): Promise<{ entries: ImplementationEntry[]; cursor: Cursor }> { - throw new Error('Not supported'); - } - - allEntriesByFolder( - _folder: string, - _extension: string, - _depth: number, - ): Promise { - throw new Error('Not supported'); - } -} diff --git a/core/src/backends/azure/index.ts b/core/src/backends/azure/index.ts deleted file mode 100644 index 50d38206..00000000 --- a/core/src/backends/azure/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as AzureBackend } from './implementation'; -export { default as API } from './API'; -export { default as AuthenticationPage } from './AuthenticationPage'; diff --git a/core/src/backends/index.tsx b/core/src/backends/index.tsx index 3eca0a40..2d788ebc 100644 --- a/core/src/backends/index.tsx +++ b/core/src/backends/index.tsx @@ -1,4 +1,3 @@ -export { AzureBackend } from './azure'; export { BitbucketBackend } from './bitbucket'; export { GitGatewayBackend } from './git-gateway'; export { GitHubBackend } from './github'; diff --git a/core/src/components/App/App.tsx b/core/src/components/App/App.tsx index 23b14de6..4185f707 100644 --- a/core/src/components/App/App.tsx +++ b/core/src/components/App/App.tsx @@ -16,6 +16,7 @@ import { history } from '../../routing/history'; import CollectionRoute from '../Collection/CollectionRoute'; import EditorRoute from '../Editor/EditorRoute'; import MediaLibrary from '../MediaLibrary/MediaLibrary'; +import Page from '../page/Page'; import Snackbars from '../snackbar/Snackbars'; import { Alert } from '../UI/Alert'; import { Confirm } from '../UI/Confirm'; @@ -212,6 +213,7 @@ const App = ({ element={} /> } /> + } /> } /> {useMediaLibrary ? : null} diff --git a/core/src/components/Collection/Sidebar.tsx b/core/src/components/Collection/Sidebar.tsx index 588304b8..478bce7d 100644 --- a/core/src/components/Collection/Sidebar.tsx +++ b/core/src/components/Collection/Sidebar.tsx @@ -57,9 +57,9 @@ const Sidebar = ({ const iconName = collection.icon; let icon: ReactNode = ; if (iconName) { - const storedIcon = getIcon(iconName); - if (storedIcon) { - icon = storedIcon(); + const StoredIcon = getIcon(iconName); + if (StoredIcon) { + icon = ; } } @@ -99,9 +99,9 @@ const Sidebar = ({ Object.values(additionalLinks).map(({ id, title, data, options: { iconName } = {} }) => { let icon: ReactNode = ; if (iconName) { - const storedIcon = getIcon(iconName); - if (storedIcon) { - icon = storedIcon(); + const StoredIcon = getIcon(iconName); + if (StoredIcon) { + icon = ; } } diff --git a/core/src/components/Editor/EditorControlPane/EditorControl.tsx b/core/src/components/Editor/EditorControlPane/EditorControl.tsx index 3c2fefe5..4c3547ca 100644 --- a/core/src/components/Editor/EditorControlPane/EditorControl.tsx +++ b/core/src/components/Editor/EditorControlPane/EditorControl.tsx @@ -7,8 +7,6 @@ import { connect } from 'react-redux'; import { changeDraftField as changeDraftFieldAction, changeDraftFieldValidation as changeDraftFieldValidationAction, - clearFieldErrors as clearFieldErrorsAction, - tryLoadEntry, } from '../../../actions/entries'; import { getAsset as getAssetAction } from '../../../actions/media'; import { @@ -17,7 +15,7 @@ import { removeInsertedMedia as removeInsertedMediaAction, removeMediaControl as removeMediaControlAction, } from '../../../actions/mediaLibrary'; -import { clearSearch as clearSearchAction, query as queryAction } from '../../../actions/search'; +import { query as queryAction } from '../../../actions/search'; import { borders, colors, lengths, transitions } from '../../../components/UI/styles'; import { transientOptions } from '../../../lib'; import { resolveWidget } from '../../../lib/registry'; @@ -37,7 +35,6 @@ import type { Widget, } from '../../../interface'; import type { RootState } from '../../../store'; -import type { EditorControlPaneProps } from './EditorControlPane'; /** * This is a necessary bridge as we are still passing classnames to widgets @@ -133,9 +130,7 @@ const ControlHint = styled( const EditorControl = ({ className, - clearFieldErrors, clearMediaControl, - clearSearch, collection, config: configState, entry, @@ -144,11 +139,9 @@ const EditorControl = ({ submitted, getAsset, isDisabled, - isFetching, isFieldDuplicate, isFieldHidden, isHidden = false, - loadEntry, locale, mediaPaths, changeDraftFieldValidation, @@ -210,8 +203,6 @@ const EditorControl = ({ <> {React.createElement(widget.control, { key: `field_${path}`, - clearFieldErrors, - clearSearch, collection, config, entry, @@ -220,11 +211,9 @@ const EditorControl = ({ submitted, getAsset: handleGetAsset, isDisabled: isDisabled ?? false, - isFetching, isFieldDuplicate, isFieldHidden, label: getFieldLabel(field, t), - loadEntry, locale, mediaPaths, onChange: handleChangeDraftField, @@ -264,7 +253,6 @@ const EditorControl = ({ interface EditorControlOwnProps { className?: string; - clearFieldErrors: EditorControlPaneProps['clearFieldErrors']; field: Field; fieldsErrors: FieldsErrors; submitted: boolean; @@ -285,25 +273,13 @@ function mapStateToProps(state: RootState, ownProps: EditorControlOwnProps) { const collection = entryDraft.entry ? collections[entryDraft.entry.collection] : null; const isLoadingAsset = selectIsLoadingAsset(state.medias); - async function loadEntry(collectionName: string, slug: string) { - const targetCollection = collections[collectionName]; - if (targetCollection) { - const loadedEntry = await tryLoadEntry(state, targetCollection, slug); - return loadedEntry; - } else { - throw new Error(`Can't find collection '${collectionName}'`); - } - } - return { ...ownProps, mediaPaths: state.mediaLibrary.controlMedia, - isFetching: state.search.isFetching, config: state.config, entry, collection, isLoadingAsset, - loadEntry, }; } @@ -315,8 +291,6 @@ const mapDispatchToProps = { removeMediaControl: removeMediaControlAction, removeInsertedMedia: removeInsertedMediaAction, query: queryAction, - clearSearch: clearSearchAction, - clearFieldErrors: clearFieldErrorsAction, getAsset: getAssetAction, }; diff --git a/core/src/components/Editor/EditorControlPane/EditorControlPane.tsx b/core/src/components/Editor/EditorControlPane/EditorControlPane.tsx index 1c65eb2d..ed4bbfbb 100644 --- a/core/src/components/Editor/EditorControlPane/EditorControlPane.tsx +++ b/core/src/components/Editor/EditorControlPane/EditorControlPane.tsx @@ -6,10 +6,7 @@ import get from 'lodash/get'; import React, { useCallback, useMemo } from 'react'; import { connect } from 'react-redux'; -import { - changeDraftField as changeDraftFieldAction, - clearFieldErrors as clearFieldErrorsAction, -} from '../../../actions/entries'; +import { changeDraftField as changeDraftFieldAction } from '../../../actions/entries'; import confirm from '../../../components/UI/Confirm'; import { getI18nInfo, @@ -115,7 +112,6 @@ const EditorControlPane = ({ changeDraftField, locale, onLocaleChange, - clearFieldErrors, t, }: TranslatedProps) => { const i18n = useMemo(() => { @@ -211,7 +207,6 @@ const EditorControlPane = ({ isFieldDuplicate={field => isFieldDuplicate(field, locale, i18n?.defaultLocale)} isFieldHidden={field => isFieldHidden(field, locale, i18n?.defaultLocale)} locale={locale} - clearFieldErrors={clearFieldErrors} parentPath="" i18n={i18n} /> @@ -239,7 +234,6 @@ function mapStateToProps(_state: RootState, ownProps: EditorControlPaneOwnProps) const mapDispatchToProps = { changeDraftField: changeDraftFieldAction, - clearFieldErrors: clearFieldErrorsAction, }; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx b/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx index d9ae5ace..cbde8fcc 100644 --- a/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx +++ b/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx @@ -212,16 +212,11 @@ function getWidget( config={config} collection={collection} value={ - value && - !widget.allowMapValue && - typeof value === 'object' && - !isJsxElement(value) && - !isReactFragment(value) + value && typeof value === 'object' && !isJsxElement(value) && !isReactFragment(value) ? (value as Record)[field.name] : value } entry={entry} - resolveWidget={resolveWidget} /> ); } diff --git a/core/src/components/MediaLibrary/EmptyMessage.tsx b/core/src/components/MediaLibrary/EmptyMessage.tsx index 56c76e87..ce111bc4 100644 --- a/core/src/components/MediaLibrary/EmptyMessage.tsx +++ b/core/src/components/MediaLibrary/EmptyMessage.tsx @@ -1,35 +1,21 @@ import { styled } from '@mui/material/styles'; import React from 'react'; -import { colors } from '../../components/UI/styles'; -import { transientOptions } from '../../lib'; - -interface EmptyMessageContainerProps { - $isPrivate: boolean; -} - -const EmptyMessageContainer = styled( - 'div', - transientOptions, -)( - ({ $isPrivate }) => ` - height: 100%; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - ${$isPrivate ? `color: ${colors.textFieldBorder};` : ''} - `, -); +const EmptyMessageContainer = styled('div')` + height: 100%; + width: 100%; + display: flex; + justify-content: center; + align-items: center; +`; interface EmptyMessageProps { content: string; - isPrivate?: boolean; } -const EmptyMessage = ({ content, isPrivate = false }: EmptyMessageProps) => { +const EmptyMessage = ({ content }: EmptyMessageProps) => { return ( - +

{content}

); diff --git a/core/src/components/MediaLibrary/MediaLibrary.tsx b/core/src/components/MediaLibrary/MediaLibrary.tsx index 37f868a0..f441bd9f 100644 --- a/core/src/components/MediaLibrary/MediaLibrary.tsx +++ b/core/src/components/MediaLibrary/MediaLibrary.tsx @@ -53,7 +53,6 @@ const MediaLibrary = ({ isDeleting, hasNextPage, isPaginating, - privateUpload = false, config, loadMedia, dynamicSearchQuery, @@ -69,7 +68,6 @@ const MediaLibrary = ({ const [query, setQuery] = useState(undefined); const [prevIsVisible, setPrevIsVisible] = useState(false); - const [prevPrivateUpload, setPrevPrivateUpload] = useState(false); useEffect(() => { loadMedia(); @@ -85,14 +83,10 @@ const MediaLibrary = ({ }, [isVisible, prevIsVisible]); useEffect(() => { - setPrevPrivateUpload(privateUpload); - }, [privateUpload]); - - useEffect(() => { - if (!prevIsVisible && isVisible && !prevPrivateUpload && privateUpload) { - loadMedia({ privateUpload }); + if (!prevIsVisible && isVisible) { + loadMedia(); } - }, [isVisible, loadMedia, prevIsVisible, prevPrivateUpload, privateUpload]); + }, [isVisible, loadMedia, prevIsVisible]); const loadDisplayURL = useCallback( (file: MediaFile) => { @@ -208,7 +202,7 @@ const MediaLibrary = ({ }, }); } else { - await persistMedia(file, { privateUpload, field }); + await persistMedia(file, { field }); setSelectedFile(files[0] as unknown as MediaFile); @@ -219,7 +213,7 @@ const MediaLibrary = ({ event.target.value = ''; } }, - [config.max_file_size, field, persistMedia, privateUpload], + [config.max_file_size, field, persistMedia], ); /** @@ -251,11 +245,11 @@ const MediaLibrary = ({ } const file = files.find(file => selectedFile?.key === file.key); if (file) { - deleteMedia(file, { privateUpload }).then(() => { + deleteMedia(file).then(() => { setSelectedFile(null); }); } - }, [deleteMedia, files, privateUpload, selectedFile?.key]); + }, [deleteMedia, files, selectedFile?.key]); /** * Downloads the selected file. @@ -286,8 +280,8 @@ const MediaLibrary = ({ }, [displayURLs, selectedFile]); const handleLoadMore = useCallback(() => { - loadMedia({ query: dynamicSearchQuery, page: (page ?? 0) + 1, privateUpload }); - }, [dynamicSearchQuery, loadMedia, page, privateUpload]); + loadMedia({ query: dynamicSearchQuery, page: (page ?? 0) + 1 }); + }, [dynamicSearchQuery, loadMedia, page]); /** * Executes media library search for implementations that support dynamic @@ -299,11 +293,11 @@ const MediaLibrary = ({ const handleSearchKeyDown = useCallback( async (event: KeyboardEvent) => { if (event.key === 'Enter' && dynamicSearch) { - await loadMedia({ query, privateUpload }); + await loadMedia({ query }); scrollToTop(); } }, - [dynamicSearch, loadMedia, privateUpload, query], + [dynamicSearch, loadMedia, query], ); /** @@ -344,7 +338,6 @@ const MediaLibrary = ({ isDeleting={isDeleting} hasNextPage={hasNextPage} isPaginating={isPaginating} - privateUpload={privateUpload} query={query} selectedFile={selectedFile} handleFilter={filterImages} @@ -382,7 +375,6 @@ function mapStateToProps(state: RootState) { isLoading: mediaLibrary.isLoading, isPersisting: mediaLibrary.isPersisting, isDeleting: mediaLibrary.isDeleting, - privateUpload: mediaLibrary.privateUpload, config: mediaLibrary.config, page: mediaLibrary.page, hasNextPage: mediaLibrary.hasNextPage, diff --git a/core/src/components/MediaLibrary/MediaLibraryCard.tsx b/core/src/components/MediaLibrary/MediaLibraryCard.tsx index 662ba188..a2b580d6 100644 --- a/core/src/components/MediaLibrary/MediaLibraryCard.tsx +++ b/core/src/components/MediaLibrary/MediaLibraryCard.tsx @@ -1,8 +1,8 @@ import { styled } from '@mui/material/styles'; import React, { useEffect, useMemo } from 'react'; -import { transientOptions } from '../../lib'; import { borders, colors, effects, lengths, shadows } from '../../components/UI/styles'; +import { transientOptions } from '../../lib'; import type { MediaLibraryDisplayURL } from '../../reducers/mediaLibrary'; @@ -13,14 +13,13 @@ interface CardProps { $height: string; $margin: string; $isSelected: boolean; - $isPrivate: boolean; } const Card = styled( 'div', transientOptions, )( - ({ $width, $height, $margin, $isSelected, $isPrivate }) => ` + ({ $width, $height, $margin, $isSelected }) => ` width: ${$width}; height: ${$height}; margin: ${$margin}; @@ -29,7 +28,6 @@ const Card = styled( border-radius: ${lengths.borderRadius}; cursor: pointer; overflow: hidden; - ${$isPrivate ? `background-color: ${colors.textFieldBorder};` : ''} &:focus { outline: none; @@ -86,7 +84,6 @@ interface MediaLibraryCardProps { width: string; height: string; margin: string; - isPrivate?: boolean; type?: string; isViewableImage: boolean; loadDisplayURL: () => void; @@ -102,7 +99,6 @@ const MediaLibraryCard = ({ width, height, margin, - isPrivate = false, type, isViewableImage, isDraft, @@ -122,7 +118,6 @@ const MediaLibraryCard = ({ $width={width} $height={height} $margin={margin} - $isPrivate={isPrivate} onClick={onClick} tabIndex={-1} > diff --git a/core/src/components/MediaLibrary/MediaLibraryCardGrid.tsx b/core/src/components/MediaLibrary/MediaLibraryCardGrid.tsx index f35e91db..2db0462f 100644 --- a/core/src/components/MediaLibrary/MediaLibraryCardGrid.tsx +++ b/core/src/components/MediaLibrary/MediaLibraryCardGrid.tsx @@ -4,13 +4,11 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { Waypoint } from 'react-waypoint'; import { FixedSizeGrid as Grid } from 'react-window'; -import { transientOptions } from '../../lib'; -import { colors } from '../../components/UI/styles'; import MediaLibraryCard from './MediaLibraryCard'; import type { GridChildComponentProps } from 'react-window'; -import type { MediaLibraryDisplayURL, MediaLibraryState } from '../../reducers/mediaLibrary'; import type { MediaFile } from '../../interface'; +import type { MediaLibraryDisplayURL, MediaLibraryState } from '../../reducers/mediaLibrary'; export interface MediaLibraryCardItem { displayURL?: MediaLibraryDisplayURL; @@ -37,7 +35,6 @@ export interface MediaLibraryCardGridProps { cardHeight: string; cardMargin: string; loadDisplayURL: (asset: MediaFile) => void; - isPrivate?: boolean; displayURLs: MediaLibraryState['displayURLs']; } @@ -57,7 +54,6 @@ const CardWrapper = ({ cardDraftText, cardWidth, cardHeight, - isPrivate, displayURLs, loadDisplayURL, columnCount, @@ -90,7 +86,6 @@ const CardWrapper = ({ width={cardWidth} height={cardHeight} margin={'0px'} - isPrivate={isPrivate} displayURL={displayURLs[file.id] ?? (file.url ? { url: file.url } : {})} loadDisplayURL={() => loadDisplayURL(file)} type={file.type} @@ -168,19 +163,6 @@ const VirtualizedGrid = (props: MediaLibraryCardGridProps) => { ); }; -interface PaginatingMessageProps { - $isPrivate: boolean; -} - -const PaginatingMessage = styled( - 'h1', - transientOptions, -)( - ({ $isPrivate }) => ` - ${$isPrivate ? `color: ${colors.textFieldBorder};` : ''} - `, -); - const PaginatedGrid = ({ setScrollContainerRef, mediaItems, @@ -190,7 +172,6 @@ const PaginatedGrid = ({ cardWidth, cardHeight, cardMargin, - isPrivate = false, displayURLs, loadDisplayURL, canLoadMore, @@ -212,7 +193,6 @@ const PaginatedGrid = ({ width={cardWidth} height={cardHeight} margin={cardMargin} - isPrivate={isPrivate} displayURL={displayURLs[file.id] ?? (file.url ? { url: file.url } : {})} loadDisplayURL={() => loadDisplayURL(file)} type={file.type} @@ -221,9 +201,7 @@ const PaginatedGrid = ({ ))} {!canLoadMore ? null : } - {!isPaginating ? null : ( - {paginatingMessage} - )} + {!isPaginating ? null :

{paginatingMessage}

} ); }; diff --git a/core/src/components/MediaLibrary/MediaLibraryModal.tsx b/core/src/components/MediaLibrary/MediaLibraryModal.tsx index a0001de1..e5ee6e46 100644 --- a/core/src/components/MediaLibrary/MediaLibraryModal.tsx +++ b/core/src/components/MediaLibrary/MediaLibraryModal.tsx @@ -1,14 +1,12 @@ -import { styled } from '@mui/material/styles'; import CloseIcon from '@mui/icons-material/Close'; import Dialog from '@mui/material/Dialog'; import DialogContent from '@mui/material/DialogContent'; import Fab from '@mui/material/Fab'; +import { styled } from '@mui/material/styles'; import isEmpty from 'lodash/isEmpty'; import React from 'react'; import { translate } from 'react-polyglot'; -import { transientOptions } from '../../lib'; -import { colors, colorsRaw } from '../../components/UI/styles'; import EmptyMessage from './EmptyMessage'; import MediaLibraryCardGrid from './MediaLibraryCardGrid'; import MediaLibraryTop from './MediaLibraryTop'; @@ -37,60 +35,41 @@ const cardMargin = `10px`; */ const cardOutsideWidth = `300px`; -interface StyledModalProps { - $isPrivate: boolean; -} +const StyledModal = styled(Dialog)` + .MuiDialog-paper { + display: flex; + flex-direction: column; + overflow: visible; + height: 80%; + width: calc(${cardOutsideWidth} + 20px); + max-width: calc(${cardOutsideWidth} + 20px); -const StyledModal = styled( - Dialog, - transientOptions, -)( - ({ $isPrivate }) => ` - .MuiDialog-paper { - display: flex; - flex-direction: column; - overflow: visible; - height: 80%; - width: calc(${cardOutsideWidth} + 20px); - max-width: calc(${cardOutsideWidth} + 20px); - ${$isPrivate ? `background-color: ${colorsRaw.grayDark};` : ''} - - @media (min-width: 800px) { - width: calc(${cardOutsideWidth} * 2 + 20px); - max-width: calc(${cardOutsideWidth} * 2 + 20px); - } - - @media (min-width: 1120px) { - width: calc(${cardOutsideWidth} * 3 + 20px); - max-width: calc(${cardOutsideWidth} * 3 + 20px); - } - - @media (min-width: 1440px) { - width: calc(${cardOutsideWidth} * 4 + 20px); - max-width: calc(${cardOutsideWidth} * 4 + 20px); - } - - @media (min-width: 1760px) { - width: calc(${cardOutsideWidth} * 5 + 20px); - max-width: calc(${cardOutsideWidth} * 5 + 20px); - } - - @media (min-width: 2080px) { - width: calc(${cardOutsideWidth} * 6 + 20px); - max-width: calc(${cardOutsideWidth} * 6 + 20px); - } - - h1 { - ${$isPrivate && `color: ${colors.textFieldBorder};`} - } - - button:disabled, - label[disabled] { - ${$isPrivate ? 'background-color: rgba(217, 217, 217, 0.15);' : ''} - } + @media (min-width: 800px) { + width: calc(${cardOutsideWidth} * 2 + 20px); + max-width: calc(${cardOutsideWidth} * 2 + 20px); } - `, -); + + @media (min-width: 1120px) { + width: calc(${cardOutsideWidth} * 3 + 20px); + max-width: calc(${cardOutsideWidth} * 3 + 20px); + } + + @media (min-width: 1440px) { + width: calc(${cardOutsideWidth} * 4 + 20px); + max-width: calc(${cardOutsideWidth} * 4 + 20px); + } + + @media (min-width: 1760px) { + width: calc(${cardOutsideWidth} * 5 + 20px); + max-width: calc(${cardOutsideWidth} * 5 + 20px); + } + + @media (min-width: 2080px) { + width: calc(${cardOutsideWidth} * 6 + 20px); + max-width: calc(${cardOutsideWidth} * 6 + 20px); + } + } +`; interface MediaLibraryModalProps { isVisible?: boolean; @@ -104,7 +83,6 @@ interface MediaLibraryModalProps { isDeleting?: boolean; hasNextPage?: boolean; isPaginating?: boolean; - privateUpload?: boolean; query?: string; selectedFile?: MediaFile; handleFilter: (files: MediaFile[]) => MediaFile[]; @@ -136,7 +114,6 @@ const MediaLibraryModal = ({ isDeleting, hasNextPage, isPaginating, - privateUpload = false, query, selectedFile, handleFilter, @@ -175,14 +152,13 @@ const MediaLibraryModal = ({ const hasSelection = hasMedia && !isEmpty(selectedFile); return ( - + - {!shouldShowEmptyMessage ? null : ( - - )} + {!shouldShowEmptyMessage ? null : } diff --git a/core/src/components/MediaLibrary/MediaLibraryTop.tsx b/core/src/components/MediaLibrary/MediaLibraryTop.tsx index a4c8e722..f5312683 100644 --- a/core/src/components/MediaLibrary/MediaLibraryTop.tsx +++ b/core/src/components/MediaLibrary/MediaLibraryTop.tsx @@ -29,7 +29,6 @@ const StyledDialogTitle = styled(DialogTitle)` export interface MediaLibraryTopProps { onClose: () => void; - privateUpload?: boolean; forImage?: boolean; onDownload: () => void; onUpload: (event: ChangeEvent | DragEvent) => void; @@ -62,7 +61,6 @@ const MediaLibraryTop = ({ isPersisting, isDeleting, selectedFile, - privateUpload, }: TranslatedProps) => { const shouldShowButtonLoader = isPersisting || isDeleting; const uploadEnabled = !shouldShowButtonLoader; @@ -80,11 +78,9 @@ const MediaLibraryTop = ({ return ( - {`${privateUpload ? t('mediaLibrary.mediaLibraryModal.private') : ''}${ - forImage - ? t('mediaLibrary.mediaLibraryModal.images') - : t('mediaLibrary.mediaLibraryModal.mediaAssets') - }`} + {forImage + ? t('mediaLibrary.mediaLibraryModal.images') + : t('mediaLibrary.mediaLibraryModal.mediaAssets')} - - - \ No newline at end of file diff --git a/core/src/components/page/Page.tsx b/core/src/components/page/Page.tsx index 1f522509..ea69ad8c 100644 --- a/core/src/components/page/Page.tsx +++ b/core/src/components/page/Page.tsx @@ -4,23 +4,19 @@ import { translate } from 'react-polyglot'; import { connect } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { lengths } from '../../components/UI/styles'; import { getAdditionalLink } from '../../lib/registry'; +import MainView from '../App/MainView'; import Sidebar from '../Collection/Sidebar'; import type { ComponentType } from 'react'; import type { ConnectedProps } from 'react-redux'; import type { RootState } from '../../store'; -const StylePage = styled('div')` - margin: ${lengths.pageMargin}; -`; - const StyledPageContent = styled('div')` width: 100%; display: flex; + flex-direction: column; align-items: center; - justify-content: center; `; const Page = ({ collections, isSearchEnabled, searchTerm, filterTerm }: PageProps) => { @@ -51,7 +47,7 @@ const Page = ({ collections, isSearchEnabled, searchTerm, filterTerm }: PageProp }, [Content]); return ( - + {pageContent} - + ); }; diff --git a/core/src/constants/configSchema.tsx b/core/src/constants/configSchema.tsx index bef62ea4..a2baf9f5 100644 --- a/core/src/constants/configSchema.tsx +++ b/core/src/constants/configSchema.tsx @@ -202,8 +202,6 @@ function getConfigSchema() { label_singular: { type: 'string' }, description: { type: 'string' }, file: { type: 'string' }, - preview_path: { type: 'string' }, - preview_path_date_field: { type: 'string' }, editor: { type: 'object', properties: { @@ -220,8 +218,6 @@ function getConfigSchema() { summary: { type: 'string' }, slug: { type: 'string' }, path: { type: 'string' }, - preview_path: { type: 'string' }, - preview_path_date_field: { type: 'string' }, create: { type: 'boolean' }, publish: { type: 'boolean' }, hide: { type: 'boolean' }, diff --git a/core/src/extensions.ts b/core/src/extensions.ts index 8ce3c12a..21680915 100644 --- a/core/src/extensions.ts +++ b/core/src/extensions.ts @@ -1,5 +1,4 @@ import { - AzureBackend, BitbucketBackend, GitGatewayBackend, GitHubBackend, @@ -30,7 +29,6 @@ import { export function addExtensions() { // Register all the things registerBackend('git-gateway', GitGatewayBackend); - registerBackend('azure', AzureBackend); registerBackend('github', GitHubBackend); registerBackend('gitlab', GitLabBackend); registerBackend('bitbucket', BitbucketBackend); diff --git a/core/src/index.ts b/core/src/index.ts index d81d7ad4..a4f3b05a 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -18,6 +18,10 @@ export const CMS = { if (typeof window !== 'undefined') { window.CMS = CMS; window.createClass = window.createClass || createReactClass; + window.useState = window.useState || React.useState; + window.useMemo = window.useMemo || React.useMemo; + window.useEffect = window.useEffect || React.useEffect; + window.useCallback = window.useCallback || React.useCallback; window.h = window.h || React.createElement; } diff --git a/core/src/integrations/index.ts b/core/src/integrations/index.ts index c1abebfd..1a75d96e 100644 --- a/core/src/integrations/index.ts +++ b/core/src/integrations/index.ts @@ -1,28 +1,19 @@ import Algolia from './providers/algolia/implementation'; -import AssetStore from './providers/assetStore/implementation'; -import type { - AlgoliaConfig, - AssetStoreConfig, - MediaIntegrationProvider, - SearchIntegrationProvider, -} from '../interface'; +import type { AlgoliaConfig, SearchIntegrationProvider } from '../interface'; interface IntegrationsConfig { providers?: { algolia?: AlgoliaConfig; - assetStore?: AssetStoreConfig; }; } interface Integrations { algolia?: Algolia; - assetStore?: AssetStore; } export function resolveIntegrations( config: IntegrationsConfig | undefined, - getToken: () => Promise, ) { const integrationInstances: Integrations = {}; @@ -30,10 +21,6 @@ export function resolveIntegrations( integrationInstances.algolia = new Algolia(config.providers.algolia); } - if (config?.providers?.['assetStore']) { - integrationInstances['assetStore'] = new AssetStore(config.providers['assetStore'], getToken); - } - return integrationInstances; } @@ -42,32 +29,13 @@ export const getSearchIntegrationProvider = (function () { return ( config: IntegrationsConfig | undefined, - getToken: () => Promise, provider: SearchIntegrationProvider, ) => { if (provider in (config?.providers ?? {})) if (integrations) { return integrations[provider]; } else { - integrations = resolveIntegrations(config, getToken); - return integrations[provider]; - } - }; -})(); - -export const getMediaIntegrationProvider = (function () { - let integrations: Integrations = {}; - - return ( - config: IntegrationsConfig | undefined, - getToken: () => Promise, - provider: MediaIntegrationProvider, - ) => { - if (provider in (config?.providers ?? {})) - if (integrations) { - return integrations[provider]; - } else { - integrations = resolveIntegrations(config, getToken); + integrations = resolveIntegrations(config); return integrations[provider]; } }; diff --git a/core/src/integrations/providers/assetStore/implementation.ts b/core/src/integrations/providers/assetStore/implementation.ts deleted file mode 100644 index 98ef8fa5..00000000 --- a/core/src/integrations/providers/assetStore/implementation.ts +++ /dev/null @@ -1,168 +0,0 @@ -import pickBy from 'lodash/pickBy'; -import trimEnd from 'lodash/trimEnd'; - -import { unsentRequest } from '../../../lib/util'; -import { addParams } from '../../../lib/urlHelper'; - -import type { AssetStoreConfig } from '../../../interface'; - -const { fetchWithTimeout: fetch } = unsentRequest; - -interface AssetStoreResponse { - id: string; - name: string; - size: number; - url: string; -} - -export default class AssetStore { - private shouldConfirmUpload: boolean; - private getSignedFormURL: string; - private getToken: () => Promise; - - constructor(config: AssetStoreConfig, getToken: () => Promise) { - if (config.getSignedFormURL == null) { - throw 'The AssetStore integration needs the getSignedFormURL in the integration configuration.'; - } - this.getToken = getToken; - - this.shouldConfirmUpload = config.shouldConfirmUpload ?? false; - this.getSignedFormURL = trimEnd(config.getSignedFormURL, '/'); - } - - parseJsonResponse(response: Response) { - return response.json().then(json => { - if (!response.ok) { - return Promise.reject(json); - } - - return json; - }); - } - - urlFor(path: string, optionParams: Record = {}) { - const params = []; - for (const key in optionParams) { - params.push(`${key}=${encodeURIComponent(optionParams[key])}`); - } - if (params.length) { - path += `?${params.join('&')}`; - } - return path; - } - - requestHeaders(headers = {}) { - return { - ...headers, - }; - } - - confirmRequest(assetID: string) { - this.getToken().then(token => - this.request(`${this.getSignedFormURL}/${assetID}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ state: 'uploaded' }), - }), - ); - } - - async request( - path: string, - options: RequestInit & { - params?: Record; - }, - ) { - const headers = this.requestHeaders(options.headers || {}); - const url = this.urlFor(path, options.params); - const response = await fetch(url, { ...options, headers }); - const contentType = response.headers.get('Content-Type'); - const isJson = contentType && contentType.match(/json/); - const content = isJson ? await this.parseJsonResponse(response) : response.text(); - return content; - } - - async retrieve(query: string, page: number, privateUpload: boolean) { - const params = pickBy( - { search: query, page: `${page}`, filter: privateUpload ? 'private' : 'public' }, - val => !!val, - ); - const url = addParams(this.getSignedFormURL, params); - const token = await this.getToken(); - const headers = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }; - const response: AssetStoreResponse[] = await this.request(url, { headers }); - const files = response.map(({ id, name, size, url }) => { - return { id, name, size, displayURL: url, url, path: url }; - }); - return files; - } - - delete(assetID: string) { - const url = `${this.getSignedFormURL}/${assetID}`; - return this.getToken().then(token => - this.request(url, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - }), - ); - } - - async upload(file: File, privateUpload = false) { - const fileData: { - name: string; - size: number; - content_type?: string; - visibility?: 'private'; - } = { - name: file.name, - size: file.size, - }; - if (file.type) { - fileData.content_type = file.type; - } - - if (privateUpload) { - fileData.visibility = 'private'; - } - - try { - const token = await this.getToken(); - const response = await this.request(this.getSignedFormURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(fileData), - }); - const formURL = response.form.url; - const formFields = response.form.fields; - const { id, name, size, url } = response.asset; - - const formData = new FormData(); - Object.keys(formFields).forEach(key => formData.append(key, formFields[key])); - formData.append('file', file, file.name); - - await this.request(formURL, { method: 'POST', body: formData }); - - if (this.shouldConfirmUpload) { - await this.confirmRequest(id); - } - - const asset = { id, name, size, displayURL: url, url, path: url }; - return { success: true, asset }; - } catch (error) { - console.error(error); - throw error; - } - } -} diff --git a/core/src/interface.ts b/core/src/interface.ts index 25071da8..961628c9 100644 --- a/core/src/interface.ts +++ b/core/src/interface.ts @@ -4,7 +4,7 @@ import type { } from '@toast-ui/editor/types/editor'; import type { ToolbarItemOptions as MarkdownToolbarItemOptions } from '@toast-ui/editor/types/ui'; import type { PropertiesSchema } from 'ajv/dist/types/json-schema'; -import type { ComponentType, ReactNode } from 'react'; +import type { FunctionComponent, ComponentType, ReactNode } from 'react'; import type { t, TranslateProps as ReactPolyglotTranslateProps } from 'react-polyglot'; import type { MediaFile as BackendMediaFile } from './backend'; import type { EditorControlProps } from './components/Editor/EditorControlPane/EditorControl'; @@ -15,12 +15,6 @@ import type Cursor from './lib/util/Cursor'; import type AssetProxy from './valueObjects/AssetProxy'; import type { MediaHolder } from './widgets/markdown/hooks/useMedia'; -export interface SlugConfig { - encoding: string; - clean_accents: boolean; - sanitize_replacement: string; -} - export interface Pages { [collection: string]: { isFetching?: boolean; page?: number; ids: string[] }; } @@ -68,6 +62,7 @@ export type ValueOrNestedValue = | number | boolean | string[] + | (string | number)[] | null | undefined | ObjectValue @@ -176,8 +171,6 @@ export interface Collection { isFetching?: boolean; media_folder?: string; public_folder?: string; - preview_path?: string; - preview_path_date_field?: string; summary?: string; filter?: FilterRule; type: 'file_based_collection' | 'folder_based_collection'; @@ -224,15 +217,11 @@ export interface DisplayURLState { err?: Error; } -export type Hook = string | boolean; - export type TranslatedProps = T & ReactPolyglotTranslateProps; export type GetAssetFunction = (path: string, field?: Field) => Promise; export interface WidgetControlProps { - clearFieldErrors: EditorControlProps['clearFieldErrors']; - clearSearch: EditorControlProps['clearSearch']; collection: Collection; config: Config; entry: Entry; @@ -242,11 +231,9 @@ export interface WidgetControlProps { forList: boolean; getAsset: GetAssetFunction; isDisabled: boolean; - isFetching: boolean; isFieldDuplicate: EditorControlProps['isFieldDuplicate']; isFieldHidden: EditorControlProps['isFieldHidden']; label: string; - loadEntry: EditorControlProps['loadEntry']; locale: string | undefined; mediaPaths: Record; onChange: (value: T | null | undefined) => void; @@ -268,7 +255,6 @@ export interface WidgetPreviewProps { entry: Entry; field: RenderedField; getAsset: GetAssetFunction; - resolveWidget: (name: string) => Widget; value: T | undefined | null; } @@ -305,7 +291,6 @@ export interface WidgetOptions { validator?: Widget['validator']; getValidValue?: Widget['getValidValue']; schema?: Widget['schema']; - allowMapValue?: boolean; } export interface Widget { @@ -314,7 +299,6 @@ export interface Widget { validator: FieldValidationMethod; getValidValue: (value: T | undefined | null) => T | undefined | null; schema?: PropertiesSchema; - allowMapValue?: boolean; } export interface WidgetParam { @@ -367,8 +351,6 @@ export interface BackendEntry { assets: AssetProxy[]; } -export type DeleteOptions = {}; - export interface Credentials { token: string | {}; refresh_token?: string; @@ -446,7 +428,7 @@ export interface LocalePhrasesRoot { } export type LocalePhrases = string | { [property: string]: LocalePhrases }; -export type CustomIcon = () => JSX.Element; +export type CustomIcon = FunctionComponent; export type WidgetValueSerializer = { serialize: (value: ValueOrNestedValue) => ValueOrNestedValue; @@ -473,33 +455,10 @@ export interface MediaLibraryInternalOptions { export type MediaLibrary = MediaLibraryExternalLibrary | MediaLibraryInternalOptions; -export type BackendType = - | 'azure' - | 'git-gateway' - | 'github' - | 'gitlab' - | 'bitbucket' - | 'test-repo' - | 'proxy'; +export type BackendType = 'git-gateway' | 'github' | 'gitlab' | 'bitbucket' | 'test-repo' | 'proxy'; export type MapWidgetType = 'Point' | 'LineString' | 'Polygon'; -export type MarkdownWidgetButton = - | 'bold' - | 'italic' - | 'code' - | 'link' - | 'heading-one' - | 'heading-two' - | 'heading-three' - | 'heading-four' - | 'heading-five' - | 'heading-six' - | 'quote' - | 'code-block' - | 'bulleted-list' - | 'numbered-list'; - export interface SelectWidgetOptionObject { label: string; value: string; @@ -567,12 +526,10 @@ export interface FileOrImageField extends BaseField { media_library?: MediaLibrary; media_folder?: string; public_folder?: string; - private?: boolean; } export interface ObjectField extends BaseField { widget: 'object'; - default?: ObjectValue; collapsed?: boolean; summary?: string; @@ -626,9 +583,9 @@ export interface NumberField extends BaseField { export interface SelectField extends BaseField { widget: 'select'; - default?: string | string[]; + default?: string | number | (string | number)[]; - options: string[] | SelectWidgetOptionObject[]; + options: (string | number)[] | SelectWidgetOptionObject[]; multiple?: boolean; min?: number; max?: number; @@ -708,12 +665,9 @@ export interface SortableFields { export interface Backend { name: BackendType; - auth_scope?: AuthScope; repo?: string; branch?: string; api_root?: string; - api_version?: string; - tenant_id?: string; site_domain?: string; base_url?: string; auth_endpoint?: string; @@ -725,6 +679,7 @@ export interface Backend { use_large_media_transforms_in_media_library?: boolean; identity_url?: string; gateway_url?: string; + auth_scope?: AuthScope; commit_messages?: { create?: string; update?: string; @@ -784,23 +739,27 @@ export interface EventData { author: { login: string | undefined; name: string }; } +export type EventListenerOptions = Record; + +export type EventListenerHandler = ( + data: EventData, + options: EventListenerOptions, +) => Promise; + export interface EventListener { name: AllowedEvent; - handler: ( - data: EventData, - options: Record, - ) => Promise; + handler: EventListenerHandler; } -export type EventListenerOptions = Record; +export interface AdditionalLinkOptions { + iconName?: string; +} export interface AdditionalLink { id: string; title: string; - data: string | (() => JSX.Element); - options?: { - iconName?: string; - }; + data: string | FunctionComponent; + options?: AdditionalLinkOptions; } export interface AuthenticationPageProps { @@ -816,11 +775,9 @@ export interface AuthenticationPageProps { export type Integration = { collections?: '*' | string[]; -} & (AlgoliaIntegration | AssetStoreIntegration); +} & AlgoliaIntegration; -export type IntegrationProvider = Integration['provider']; export type SearchIntegrationProvider = 'algolia'; -export type MediaIntegrationProvider = 'assetStore'; export interface AlgoliaIntegration extends AlgoliaConfig { provider: 'algolia'; @@ -833,16 +790,6 @@ export interface AlgoliaConfig { indexPrefix?: string; } -export interface AssetStoreIntegration extends AssetStoreConfig { - provider: 'assetStore'; -} - -export interface AssetStoreConfig { - hooks: ['assetStore']; - shouldConfirmUpload?: boolean; - getSignedFormURL: string; -} - export interface SearchResponse { entries: Entry[]; pagination: number; diff --git a/core/src/lib/registry.ts b/core/src/lib/registry.ts index 73135e5c..ea0f4fd2 100644 --- a/core/src/lib/registry.ts +++ b/core/src/lib/registry.ts @@ -171,7 +171,6 @@ export function registerWidget( validator = () => false, getValidValue = (value: T | undefined | null) => value, schema, - allowMapValue, } = {}, } = name; if (registry.widgets[widgetName]) { @@ -189,7 +188,6 @@ export function registerWidget( validator: validator as Widget['validator'], getValidValue: getValidValue as Widget['getValidValue'], schema, - allowMapValue, }; } else { console.error('`registerWidget` failed, called with incorrect arguments.'); @@ -360,6 +358,7 @@ export function getAdditionalLinks(): Record { } export function getAdditionalLink(id: string): AdditionalLink | undefined { + console.log('additionalLinks', registry.additionalLinks); return registry.additionalLinks[id]; } diff --git a/core/src/locales/bg/index.ts b/core/src/locales/bg/index.ts index 0c89cf63..8fb2980d 100644 --- a/core/src/locales/bg/index.ts +++ b/core/src/locales/bg/index.ts @@ -5,7 +5,6 @@ const bg: LocalePhrasesRoot = { login: 'Вход', loggingIn: 'Влизане...', loginWithNetlifyIdentity: 'Вход с Netlify Identity', - loginWithAzure: 'Вход с Azure', loginWithBitbucket: 'Вход с Bitbucket', loginWithGitHub: 'Вход с GitHub', loginWithGitLab: 'Вход с GitLab', @@ -208,7 +207,6 @@ const bg: LocalePhrasesRoot = { noResults: 'Няма резултати.', noAssetsFound: 'Няма намерени ресурси.', noImagesFound: 'Няма намерени изображения.', - private: 'Частен ', images: 'Изображения', mediaAssets: 'Медийни ресурси', search: 'Търсене...', diff --git a/core/src/locales/ca/index.ts b/core/src/locales/ca/index.ts index 74fccc61..650d1ba1 100644 --- a/core/src/locales/ca/index.ts +++ b/core/src/locales/ca/index.ts @@ -206,7 +206,6 @@ const ca: LocalePhrasesRoot = { noResults: 'Sense resultats.', noAssetsFound: 'Arxius no trobats.', noImagesFound: 'Imatges no trobades.', - private: 'Privat', images: 'Imatges', mediaAssets: 'Arxius multimèdia', search: 'Buscar...', diff --git a/core/src/locales/cs/index.ts b/core/src/locales/cs/index.ts index 7f85918a..a50326e9 100644 --- a/core/src/locales/cs/index.ts +++ b/core/src/locales/cs/index.ts @@ -5,7 +5,6 @@ const cs: LocalePhrasesRoot = { login: 'Přihlásit', loggingIn: 'Přihlašování…', loginWithNetlifyIdentity: 'Přihlásit pomocí Netlify Identity', - loginWithAzure: 'Přihlásit pomocí Azure', loginWithBitbucket: 'Přihlásit pomocí Bitbucket', loginWithGitHub: 'Přihlásit pomocí GitHub', loginWithGitLab: 'Přihlásit pomocí GitLab', @@ -206,7 +205,6 @@ const cs: LocalePhrasesRoot = { noResults: 'Nic nenalezeno.', noAssetsFound: 'Média nenalezena.', noImagesFound: 'Obrázky nenalezeny.', - private: 'Soukromé ', images: 'Obrázky', mediaAssets: 'Média', search: 'Hledat…', diff --git a/core/src/locales/da/index.ts b/core/src/locales/da/index.ts index 8677535c..d7934946 100644 --- a/core/src/locales/da/index.ts +++ b/core/src/locales/da/index.ts @@ -5,7 +5,6 @@ const da: LocalePhrasesRoot = { login: 'Log ind', loggingIn: 'Logger ind...', loginWithNetlifyIdentity: 'Log ind med Netlify Identity', - loginWithAzure: 'Log ing med Azure', loginWithBitbucket: 'Log ind med Bitbucket', loginWithGitHub: 'Log ind med GitHub', loginWithGitLab: 'Log ind med GitLab', @@ -192,7 +191,6 @@ const da: LocalePhrasesRoot = { noResults: 'Ingen resultater.', noAssetsFound: 'Ingen elementer fundet.', noImagesFound: 'Ingen billeder fundet.', - private: 'Privat ', images: 'Billeder', mediaAssets: 'Medie elementer', search: 'Søg...', diff --git a/core/src/locales/de/index.ts b/core/src/locales/de/index.ts index 660721bb..582d9f6e 100644 --- a/core/src/locales/de/index.ts +++ b/core/src/locales/de/index.ts @@ -5,7 +5,6 @@ const de: LocalePhrasesRoot = { login: 'Login', loggingIn: 'Sie werden eingeloggt...', loginWithNetlifyIdentity: 'Mit Netlify Identity einloggen', - loginWithAzure: 'Mit Azure einloggen', loginWithBitbucket: 'Mit Bitbucket einloggen', loginWithGitHub: 'Mit GitHub einloggen', loginWithGitLab: 'Mit GitLab einloggen', @@ -220,7 +219,6 @@ const de: LocalePhrasesRoot = { noResults: 'Keine Egebnisse.', noAssetsFound: 'Keine Medien gefunden.', noImagesFound: 'Keine Bilder gefunden.', - private: 'Privat ', images: 'Bilder', mediaAssets: 'Medien', search: 'Suchen...', diff --git a/core/src/locales/en/index.ts b/core/src/locales/en/index.ts index 01115c39..a0d0547e 100644 --- a/core/src/locales/en/index.ts +++ b/core/src/locales/en/index.ts @@ -5,7 +5,6 @@ const en: LocalePhrasesRoot = { login: 'Login', loggingIn: 'Logging in...', loginWithNetlifyIdentity: 'Login with Netlify Identity', - loginWithAzure: 'Login with Azure', loginWithBitbucket: 'Login with Bitbucket', loginWithGitHub: 'Login with GitHub', loginWithGitLab: 'Login with GitLab', @@ -240,7 +239,6 @@ const en: LocalePhrasesRoot = { noResults: 'No results.', noAssetsFound: 'No assets found.', noImagesFound: 'No images found.', - private: 'Private ', images: 'Images', mediaAssets: 'Media assets', search: 'Search...', diff --git a/core/src/locales/es/index.ts b/core/src/locales/es/index.ts index d96e834a..a94819f2 100644 --- a/core/src/locales/es/index.ts +++ b/core/src/locales/es/index.ts @@ -170,7 +170,6 @@ const es: LocalePhrasesRoot = { noResults: 'Sin resultados.', noAssetsFound: 'Archivos no encontrados.', noImagesFound: 'Imágenes no encontradas.', - private: 'Privado ', images: 'Imágenes', mediaAssets: 'Archivos multimedia', search: 'Buscar...', diff --git a/core/src/locales/fr/index.ts b/core/src/locales/fr/index.ts index 58cfef35..62575767 100644 --- a/core/src/locales/fr/index.ts +++ b/core/src/locales/fr/index.ts @@ -5,7 +5,6 @@ const fr: LocalePhrasesRoot = { login: 'Se connecter', loggingIn: 'Connexion en cours...', loginWithNetlifyIdentity: 'Se connecter avec Netlify Identity', - loginWithAzure: 'Se connecter avec Azure', loginWithBitbucket: 'Se connecter avec Bitbucket', loginWithGitHub: 'Se connecter avec GitHub', loginWithGitLab: 'Se connecter avec GitLab', @@ -216,7 +215,6 @@ const fr: LocalePhrasesRoot = { noResults: 'Aucun résultat.', noAssetsFound: 'Aucune ressource trouvée.', noImagesFound: 'Aucune image trouvée.', - private: 'Privé ', images: 'Images', mediaAssets: 'Ressources', search: 'Recherche...', diff --git a/core/src/locales/gr/index.ts b/core/src/locales/gr/index.ts index debafb11..55902b8e 100644 --- a/core/src/locales/gr/index.ts +++ b/core/src/locales/gr/index.ts @@ -149,7 +149,6 @@ const gr: LocalePhrasesRoot = { noResults: 'Χωρίς αποτελέσματα.', noAssetsFound: 'Δεν βρέθηκαν αρχεία.', noImagesFound: 'Δεν βρέθηκαν εικόνες.', - private: 'Ιδιωτικό', images: 'Εικόνες', mediaAssets: 'Αρχεία πολυμέσων', search: 'Αναζήτηση...', diff --git a/core/src/locales/he/index.ts b/core/src/locales/he/index.ts index d5c2eaee..9aa3299d 100644 --- a/core/src/locales/he/index.ts +++ b/core/src/locales/he/index.ts @@ -5,7 +5,6 @@ const he: LocalePhrasesRoot = { login: 'התחברות', loggingIn: 'התחברות...', loginWithNetlifyIdentity: 'התחברות עם Netlify Identity', - loginWithAzure: 'התחברות עם Azure', loginWithBitbucket: 'התחברות עם Bitbucket', loginWithGitHub: 'התחברות עם GitHub', loginWithGitLab: 'התחברות עם GitLab', @@ -216,7 +215,6 @@ const he: LocalePhrasesRoot = { noResults: 'לא נמצאו תוצאות.', noAssetsFound: 'לא נמצאו קבצים.', noImagesFound: 'לא נמצאו תמונות.', - private: 'פרטי ', images: 'תמונות', mediaAssets: 'קבצי מדיה', search: 'חיפוש...', diff --git a/core/src/locales/hr/index.ts b/core/src/locales/hr/index.ts index 3f6cd4f8..45e75e53 100644 --- a/core/src/locales/hr/index.ts +++ b/core/src/locales/hr/index.ts @@ -5,7 +5,6 @@ const hr: LocalePhrasesRoot = { login: 'Prijava', loggingIn: 'Prijava u tijeku...', loginWithNetlifyIdentity: 'Prijava sa Netlify računom', - loginWithAzure: 'Prijava za Azure računom', loginWithBitbucket: 'Prijava sa Bitbucket računom', loginWithGitHub: 'Prijava sa GitHub računom', loginWithGitLab: 'Prijava sa GitLab računom', @@ -195,7 +194,6 @@ const hr: LocalePhrasesRoot = { noResults: 'Nema rezultata.', noAssetsFound: 'Sredstva nisu pronađena.', noImagesFound: 'Slike nisu pronađene.', - private: 'Privatno ', images: 'Slike', mediaAssets: 'Medijska sredstva', search: 'Pretraži...', diff --git a/core/src/locales/hu/index.ts b/core/src/locales/hu/index.ts index 091af09e..41df0651 100644 --- a/core/src/locales/hu/index.ts +++ b/core/src/locales/hu/index.ts @@ -135,7 +135,6 @@ const hu: LocalePhrasesRoot = { noResults: 'Nincs találat.', noAssetsFound: 'Nem található tartalom.', noImagesFound: 'Nem található kép.', - private: 'Privát ', images: 'Képek', mediaAssets: 'Média tartalmak', search: 'Keresés...', diff --git a/core/src/locales/it/index.ts b/core/src/locales/it/index.ts index 70a31e16..8fd7db0a 100644 --- a/core/src/locales/it/index.ts +++ b/core/src/locales/it/index.ts @@ -146,7 +146,6 @@ const it: LocalePhrasesRoot = { noResults: 'Nessun risultato.', noAssetsFound: 'Nessun assets trovato.', noImagesFound: 'Nessuna immagine trovata.', - private: 'Privato ', images: 'Immagini', mediaAssets: 'Media assets', search: 'Cerca...', diff --git a/core/src/locales/ja/index.ts b/core/src/locales/ja/index.ts index 96637a21..30eb91fd 100644 --- a/core/src/locales/ja/index.ts +++ b/core/src/locales/ja/index.ts @@ -5,7 +5,6 @@ const ja: LocalePhrasesRoot = { login: 'ログイン', loggingIn: 'ログインしています...', loginWithNetlifyIdentity: 'Netlify Identity でログインする', - loginWithAzure: 'Azure でログインする', loginWithBitbucket: 'Bitbucket でログインする', loginWithGitHub: 'GitHub でログインする', loginWithGitLab: 'GitLab でログインする', @@ -213,7 +212,6 @@ const ja: LocalePhrasesRoot = { noResults: 'データがありません。', noAssetsFound: 'データがありません。', noImagesFound: 'データがありません。', - private: 'プライベート', images: '画像', mediaAssets: 'メディア', search: '検索', diff --git a/core/src/locales/ko/index.ts b/core/src/locales/ko/index.ts index 843164f0..b6377d73 100644 --- a/core/src/locales/ko/index.ts +++ b/core/src/locales/ko/index.ts @@ -177,7 +177,6 @@ const ko: LocalePhrasesRoot = { noResults: '일치 항목 없음.', noAssetsFound: '발견된 에셋 없음.', noImagesFound: '발견된 이미지 없음.', - private: '개인 ', images: '이미지', mediaAssets: '미디어 에셋', search: '검색...', diff --git a/core/src/locales/lt/index.ts b/core/src/locales/lt/index.ts index 5d131bf6..ad6dbc9e 100644 --- a/core/src/locales/lt/index.ts +++ b/core/src/locales/lt/index.ts @@ -5,7 +5,6 @@ const lt: LocalePhrasesRoot = { login: 'Prisijungti', loggingIn: 'Prisijungiama...', loginWithNetlifyIdentity: 'Prisijungti su Netlify Identity', - loginWithAzure: 'Prisijungti su Azure', loginWithBitbucket: 'Prisijungti su Bitbucket', loginWithGitHub: 'Prisijungti su GitHub', loginWithGitLab: 'Prisijungti su GitLab', @@ -197,7 +196,6 @@ const lt: LocalePhrasesRoot = { noResults: 'Nėra rezultatų.', noAssetsFound: 'Turinio nerasta.', noImagesFound: 'Vaizdų nerasta.', - private: 'Privatu ', images: 'Vaizdai', mediaAssets: 'Medijos turinys', search: 'Paieška...', diff --git a/core/src/locales/nb_no/index.ts b/core/src/locales/nb_no/index.ts index 18468203..8a2351df 100644 --- a/core/src/locales/nb_no/index.ts +++ b/core/src/locales/nb_no/index.ts @@ -166,7 +166,6 @@ const nb_no: LocalePhrasesRoot = { noResults: 'Ingen resultater.', noAssetsFound: 'Ingen elementer funnet.', noImagesFound: 'Ingen bilder funnet.', - private: 'Privat ', images: 'Bilder', mediaAssets: 'Mediebibliotek', search: 'Søk...', diff --git a/core/src/locales/nl/index.ts b/core/src/locales/nl/index.ts index 19f10d06..a83344fb 100644 --- a/core/src/locales/nl/index.ts +++ b/core/src/locales/nl/index.ts @@ -5,7 +5,6 @@ const nl: LocalePhrasesRoot = { login: 'Inloggen', loggingIn: 'Inloggen...', loginWithNetlifyIdentity: 'Inloggen met Netlify Identity', - loginWithAzure: 'Inloggen met Azure', loginWithBitbucket: 'Inloggen met Bitbucket', loginWithGitHub: 'Inloggen met GitHub', loginWithGitLab: 'Inloggen met GitLab', @@ -212,7 +211,6 @@ const nl: LocalePhrasesRoot = { noResults: 'Geen resultaten.', noAssetsFound: 'Geen media gevonden.', noImagesFound: 'Geen afbeeldingen gevonden.', - private: 'Privé', images: 'Afbeeldingen', mediaAssets: 'Media', search: 'Zoeken...', diff --git a/core/src/locales/nn_no/index.ts b/core/src/locales/nn_no/index.ts index 242233a7..5f8ee45a 100644 --- a/core/src/locales/nn_no/index.ts +++ b/core/src/locales/nn_no/index.ts @@ -167,7 +167,6 @@ const nn_no: LocalePhrasesRoot = { noResults: 'Ingen resultat.', noAssetsFound: 'Ingen elementer funne.', noImagesFound: 'Ingen bilete funne.', - private: 'Privat ', images: 'Bileter', mediaAssets: 'Mediebibliotek', search: 'Søk...', diff --git a/core/src/locales/pl/index.ts b/core/src/locales/pl/index.ts index 34124dbc..57dace61 100644 --- a/core/src/locales/pl/index.ts +++ b/core/src/locales/pl/index.ts @@ -5,7 +5,6 @@ const pl: LocalePhrasesRoot = { login: 'Zaloguj się', loggingIn: 'Logowanie...', loginWithNetlifyIdentity: 'Zaloguj przez konto Netlify', - loginWithAzure: 'Zaloguj przez konto Azure', loginWithBitbucket: 'Zaloguj przez Bitbucket', loginWithGitHub: 'Zaloguj przez GitHub', loginWithGitLab: 'Zaloguj przez GitLab', @@ -217,7 +216,6 @@ const pl: LocalePhrasesRoot = { noResults: 'Brak wyników.', noAssetsFound: 'Nie znaleziono żadnych zasobów.', noImagesFound: 'Nie znaleziono żadnych obrazów.', - private: 'Prywatne ', images: 'Obrazy', mediaAssets: 'Zasoby multimedialne', search: 'Szukaj...', diff --git a/core/src/locales/pt/index.ts b/core/src/locales/pt/index.ts index 1165be0e..f0770438 100644 --- a/core/src/locales/pt/index.ts +++ b/core/src/locales/pt/index.ts @@ -5,7 +5,6 @@ const pt: LocalePhrasesRoot = { login: 'Entrar', loggingIn: 'Entrando...', loginWithNetlifyIdentity: 'Entrar com o Netlify Identity', - loginWithAzure: 'Entrar com o Azure', loginWithBitbucket: 'Entrar com o Bitbucket', loginWithGitHub: 'Entrar com o GitHub', loginWithGitLab: 'Entrar com o GitLab', @@ -219,7 +218,6 @@ const pt: LocalePhrasesRoot = { noResults: 'Nenhum resultado.', noAssetsFound: 'Nenhum recurso encontrado.', noImagesFound: 'Nenhuma imagem encontrada.', - private: 'Privado ', images: 'Imagens', mediaAssets: 'Recursos de mídia', search: 'Pesquisar...', diff --git a/core/src/locales/ro/index.ts b/core/src/locales/ro/index.ts index be79667b..41910bde 100644 --- a/core/src/locales/ro/index.ts +++ b/core/src/locales/ro/index.ts @@ -5,7 +5,6 @@ const ro: LocalePhrasesRoot = { login: 'Autentifică-te', loggingIn: 'Te autentificăm...', loginWithNetlifyIdentity: 'Autentifică-te cu Netlify Identity', - loginWithAzure: 'Autentifică-te cu Azure', loginWithBitbucket: 'Autentifică-te cu Bitbucket', loginWithGitHub: 'Autentifică-te cu GitHub', loginWithGitLab: 'Autentifică-te cu GitLab', @@ -211,7 +210,6 @@ const ro: LocalePhrasesRoot = { noResults: 'Nu sunt rezultate.', noAssetsFound: 'Nu s-au găsit fișiere.', noImagesFound: 'Nu s-au găsit imagini.', - private: 'Privat ', images: 'Imagini', mediaAssets: 'Fișiere media', search: 'Caută...', diff --git a/core/src/locales/ru/index.ts b/core/src/locales/ru/index.ts index 987d8aaa..9f972a59 100644 --- a/core/src/locales/ru/index.ts +++ b/core/src/locales/ru/index.ts @@ -5,7 +5,6 @@ const ru: LocalePhrasesRoot = { login: 'Войти', loggingIn: 'Вхожу...', loginWithNetlifyIdentity: 'Войти через Netlify Identity', - loginWithAzure: 'Войти через Azure', loginWithBitbucket: 'Войти через Bitbucket', loginWithGitHub: 'Войти через GitHub', loginWithGitLab: 'Войти через GitLab', @@ -207,7 +206,6 @@ const ru: LocalePhrasesRoot = { noResults: 'Нет результатов.', noAssetsFound: 'Ресурсы не найдены.', noImagesFound: 'Изображения не найдены.', - private: 'Приватные ', images: 'Изображения', mediaAssets: 'Медиаресурсы', search: 'Идёт поиск…', diff --git a/core/src/locales/sv/index.ts b/core/src/locales/sv/index.ts index 29b6d7c2..87d50df8 100644 --- a/core/src/locales/sv/index.ts +++ b/core/src/locales/sv/index.ts @@ -5,7 +5,6 @@ const sv: LocalePhrasesRoot = { login: 'Logga in', loggingIn: 'Loggar in...', loginWithNetlifyIdentity: 'Logga in med Netlify Identity', - loginWithAzure: 'Logga in med Azure', loginWithBitbucket: 'Logga in med Bitbucket', loginWithGitHub: 'Logga in med GitHub', loginWithGitLab: 'Logga in med GitLab', @@ -211,7 +210,6 @@ const sv: LocalePhrasesRoot = { noResults: 'Inga resultat.', noAssetsFound: 'Hittade inga mediaobjekt.', noImagesFound: 'Hittade inga bilder.', - private: 'Privat ', images: 'Bilder', mediaAssets: 'Mediaobjekt', search: 'Sök...', diff --git a/core/src/locales/th/index.ts b/core/src/locales/th/index.ts index cb6394b9..99564c1c 100644 --- a/core/src/locales/th/index.ts +++ b/core/src/locales/th/index.ts @@ -178,7 +178,6 @@ const th: LocalePhrasesRoot = { noResults: 'ไม่มีผลลัพธ์', noAssetsFound: 'ไม่พบข้อมูล', noImagesFound: 'ไม่พบรูปภาพ', - private: 'ส่วนตัว ', images: 'รูปภาพ', mediaAssets: 'ข้อมูลมีเดีย', search: 'ค้นหา...', diff --git a/core/src/locales/tr/index.ts b/core/src/locales/tr/index.ts index b2a5347a..6d43c06e 100644 --- a/core/src/locales/tr/index.ts +++ b/core/src/locales/tr/index.ts @@ -5,7 +5,6 @@ const tr: LocalePhrasesRoot = { login: 'Giriş', loggingIn: 'Giriş yapılıyor..', loginWithNetlifyIdentity: 'Netlify Identity ile Giriş', - loginWithAzure: 'Azure ile Giriş', loginWithBitbucket: 'Bitbucket ile Giriş', loginWithGitHub: 'GitHub ile Giriş', loginWithGitLab: 'GitLab ile Giriş', @@ -223,7 +222,6 @@ const tr: LocalePhrasesRoot = { noResults: 'Sonuç yok.', noAssetsFound: 'Hiçbir dosya bulunamadı.', noImagesFound: 'Resim bulunamadı.', - private: 'Özel ', images: 'Görseller', mediaAssets: 'Medya dosyaları', search: 'Ara...', diff --git a/core/src/locales/uk/index.ts b/core/src/locales/uk/index.ts index 68ae75c3..274ddf99 100644 --- a/core/src/locales/uk/index.ts +++ b/core/src/locales/uk/index.ts @@ -125,7 +125,6 @@ const uk: LocalePhrasesRoot = { noResults: 'Результати відсутні.', noAssetsFound: 'Матеріали відсутні.', noImagesFound: 'Зображення відсутні.', - private: 'Private ', images: 'Зображення', mediaAssets: 'Медіа матеріали', search: 'Пошук...', diff --git a/core/src/locales/vi/index.ts b/core/src/locales/vi/index.ts index b02841fb..7d234ca8 100644 --- a/core/src/locales/vi/index.ts +++ b/core/src/locales/vi/index.ts @@ -175,7 +175,6 @@ const vi: LocalePhrasesRoot = { noResults: 'Không có kết quả.', noAssetsFound: 'Không tìm thấy tập tin nào.', noImagesFound: 'Không tìm thấy hình nào.', - private: 'Riêng tư ', images: 'Hình ảnh', mediaAssets: 'Tập tin', search: 'Tìm kiếm...', diff --git a/core/src/locales/zh_Hans/index.ts b/core/src/locales/zh_Hans/index.ts index 1722bfd2..a0e09cf5 100644 --- a/core/src/locales/zh_Hans/index.ts +++ b/core/src/locales/zh_Hans/index.ts @@ -5,7 +5,6 @@ const zh_Hans: LocalePhrasesRoot = { login: '登录', loggingIn: '正在登录...', loginWithNetlifyIdentity: '使用 Netlify Identity 登录', - loginWithAzure: '使用 Azure 登录', loginWithBitbucket: '使用 Bitbucket 登录', loginWithGitHub: '使用 GitHub 登录', loginWithGitLab: '使用 GitLab 登录', @@ -207,7 +206,6 @@ const zh_Hans: LocalePhrasesRoot = { noResults: '暂无结果', noAssetsFound: '未找到资源', noImagesFound: '未找到图片', - private: '私有', images: '图片', mediaAssets: '媒体资源', search: '搜索...', diff --git a/core/src/locales/zh_Hant/index.ts b/core/src/locales/zh_Hant/index.ts index 50cad6ed..177f0384 100644 --- a/core/src/locales/zh_Hant/index.ts +++ b/core/src/locales/zh_Hant/index.ts @@ -185,7 +185,6 @@ const zh_Hant: LocalePhrasesRoot = { noResults: '沒有結果', noAssetsFound: '沒有發現媒體資產。', noImagesFound: '沒有發現影像。', - private: '私人', images: '影像', mediaAssets: '媒體資產', search: '搜尋中...', diff --git a/core/src/reducers/entryDraft.ts b/core/src/reducers/entryDraft.ts index 5fe2f74d..0a301983 100644 --- a/core/src/reducers/entryDraft.ts +++ b/core/src/reducers/entryDraft.ts @@ -5,7 +5,6 @@ import { v4 as uuid } from 'uuid'; import { ADD_DRAFT_ENTRY_MEDIA_FILE, DRAFT_CHANGE_FIELD, - DRAFT_CLEAR_ERRORS, DRAFT_CREATE_DUPLICATE_FROM_ENTRY, DRAFT_CREATE_EMPTY, DRAFT_CREATE_FROM_ENTRY, @@ -176,13 +175,6 @@ function entryDraftReducer( }; } - case DRAFT_CLEAR_ERRORS: { - return { - ...state, - fieldsErrors: {}, - }; - } - case ENTRY_PERSIST_REQUEST: { if (!state.entry) { return state; diff --git a/core/src/reducers/index.ts b/core/src/reducers/index.ts index 5871b061..10f3b13c 100644 --- a/core/src/reducers/index.ts +++ b/core/src/reducers/index.ts @@ -18,18 +18,18 @@ import type { IntegrationHooks } from './integrations'; const reducers = { auth, - config, collections, - search, - integrations, - entries, + config, cursors, + entries, entryDraft, - medias, - mediaLibrary, globalUI, - status, + integrations, + mediaLibrary, + medias, scroll, + search, + status, }; export default reducers; diff --git a/core/src/reducers/integrations.ts b/core/src/reducers/integrations.ts index 3fc80b0b..b532bcac 100644 --- a/core/src/reducers/integrations.ts +++ b/core/src/reducers/integrations.ts @@ -5,22 +5,18 @@ import { CONFIG_SUCCESS } from '../actions/config'; import type { ConfigAction } from '../actions/config'; import type { AlgoliaConfig, - AssetStoreConfig, Config, - MediaIntegrationProvider, SearchIntegrationProvider, } from '../interface'; export interface IntegrationHooks { search?: SearchIntegrationProvider; listEntries?: SearchIntegrationProvider; - assetStore?: MediaIntegrationProvider; } export interface IntegrationsState { providers: { algolia?: AlgoliaConfig; - assetStore?: AssetStoreConfig; }; hooks: IntegrationHooks; collectionHooks: Record; @@ -47,8 +43,6 @@ export function getIntegrations(config: Config): IntegrationsState { hook => (acc.collectionHooks[collection][hook] = providerData.provider), ); }); - } else if (providerData.provider === 'assetStore') { - acc.providers[providerData.provider] = providerData; } return acc; }, diff --git a/core/src/reducers/mediaLibrary.ts b/core/src/reducers/mediaLibrary.ts index b1db0021..8dab1de1 100644 --- a/core/src/reducers/mediaLibrary.ts +++ b/core/src/reducers/mediaLibrary.ts @@ -21,12 +21,11 @@ import { MEDIA_PERSIST_SUCCESS, MEDIA_REMOVE_INSERTED, } from '../actions/mediaLibrary'; -import { selectIntegration } from './'; -import { selectEditingDraft } from './entries'; import { selectMediaFolder } from '../lib/util/media.util'; +import { selectEditingDraft } from './entries'; import type { MediaLibraryAction } from '../actions/mediaLibrary'; -import type { Field, DisplayURLState, MediaFile, MediaLibraryInstance } from '../interface'; +import type { DisplayURLState, Field, MediaFile, MediaLibraryInstance } from '../interface'; import type { RootState } from '../store'; export interface MediaLibraryDisplayURL { @@ -49,7 +48,6 @@ export type MediaLibraryState = { value?: string | string[]; replaceIndex?: number; canInsert?: boolean; - privateUpload?: boolean; isLoading?: boolean; dynamicSearch?: boolean; dynamicSearchActive?: boolean; @@ -82,26 +80,8 @@ function mediaLibrary( }; case MEDIA_LIBRARY_OPEN: { - const { controlID, forImage, privateUpload, config, field, value, replaceIndex } = - action.payload; + const { controlID, forImage, config, field, value, replaceIndex } = action.payload; const libConfig = config || {}; - const privateUploadChanged = state.privateUpload !== privateUpload; - if (privateUploadChanged) { - return { - ...state, - isVisible: true, - forImage, - controlID, - canInsert: Boolean(controlID), - privateUpload, - config: libConfig, - controlMedia: {}, - displayURLs: {}, - field, - value, - replaceIndex, - }; - } return { ...state, @@ -109,7 +89,6 @@ function mediaLibrary( forImage: Boolean(forImage), controlID, canInsert: !!controlID, - privateUpload: Boolean(privateUpload), config: libConfig, field, value, @@ -180,19 +159,7 @@ function mediaLibrary( }; case MEDIA_LOAD_SUCCESS: { - const { - files = [], - page, - canPaginate, - dynamicSearch, - dynamicSearchQuery, - privateUpload, - } = action.payload; - const privateUploadChanged = state.privateUpload !== privateUpload; - - if (privateUploadChanged) { - return state; - } + const { files = [], page, canPaginate, dynamicSearch, dynamicSearchQuery } = action.payload; const filesWithKeys = files.map(file => ({ ...file, key: uuid() })); return { @@ -210,11 +177,6 @@ function mediaLibrary( } case MEDIA_LOAD_FAILURE: { - const privateUploadChanged = state.privateUpload !== action.payload.privateUpload; - if (privateUploadChanged) { - return state; - } - return { ...state, isLoading: false, @@ -228,12 +190,7 @@ function mediaLibrary( }; case MEDIA_PERSIST_SUCCESS: { - const { file, privateUpload } = action.payload; - const privateUploadChanged = state.privateUpload !== privateUpload; - if (privateUploadChanged) { - return state; - } - + const { file } = action.payload; const fileWithKey = { ...file, key: uuid() }; const files = state.files as MediaFile[]; const updatedFiles = [fileWithKey, ...files]; @@ -245,11 +202,6 @@ function mediaLibrary( } case MEDIA_PERSIST_FAILURE: { - const privateUploadChanged = state.privateUpload !== action.payload.privateUpload; - if (privateUploadChanged) { - return state; - } - return { ...state, isPersisting: false, @@ -263,12 +215,8 @@ function mediaLibrary( }; case MEDIA_DELETE_SUCCESS: { - const { file, privateUpload } = action.payload; + const { file } = action.payload; const { key, id } = file; - const privateUploadChanged = state.privateUpload !== privateUpload; - if (privateUploadChanged) { - return state; - } const files = state.files as MediaFile[]; const updatedFiles = files.filter(file => (key ? file.key !== key : file.id !== id)); @@ -288,11 +236,6 @@ function mediaLibrary( } case MEDIA_DELETE_FAILURE: { - const privateUploadChanged = state.privateUpload !== action.payload.privateUpload; - if (privateUploadChanged) { - return state; - } - return { ...state, isDeleting: false, @@ -347,10 +290,9 @@ function mediaLibrary( export function selectMediaFiles(state: RootState, field?: Field): MediaFile[] { const { mediaLibrary, entryDraft } = state; const editingDraft = selectEditingDraft(entryDraft); - const integration = selectIntegration(state, null, 'assetStore'); let files: MediaFile[] = []; - if (editingDraft && !integration) { + if (editingDraft) { const entryFiles = entryDraft?.entry?.mediaFiles ?? []; const entry = entryDraft['entry']; const collection = entry?.collection ? state.collections[entry.collection] : null; diff --git a/core/src/types/global.d.ts b/core/src/types/global.d.ts index 8cfb28f0..edc352e7 100644 --- a/core/src/types/global.d.ts +++ b/core/src/types/global.d.ts @@ -3,14 +3,21 @@ export {}; import type { Config } from '../interface'; import type CmsAPI from '../index'; import type createReactClass from 'create-react-class'; -import type { createElement } from 'react'; +import type { createElement, useEffect, useState, useMemo, useCallback } from 'react'; declare global { interface Window { CMS?: CmsAPI; CMS_CONFIG?: Config; CMS_ENV?: string; + /** + * @deprecated Should use react functional components instead + */ createClass: createReactClass; h: createElement; + useState: useState; + useMemo: useMemo; + useEffect: useEffect; + useCallback: useCallback; } } diff --git a/core/src/widgets/code/index.ts b/core/src/widgets/code/index.ts index 9e21f30a..f5dfc999 100644 --- a/core/src/widgets/code/index.ts +++ b/core/src/widgets/code/index.ts @@ -11,7 +11,6 @@ const CodeWidget = (): WidgetParam) => { - const [internalValue, setInternalValue] = useState(value ?? ''); - const { format, dateFormat, timeFormat } = useMemo(() => { const format = field.format; @@ -75,18 +75,7 @@ const DateTimeControl = ({ }; }, [field.date_format, field.format, field.time_format]); - const dateValue = useMemo( - () => (format ? parse(internalValue, format, new Date()) : parseISO(internalValue)), - [format, internalValue], - ); - - const timezoneOffset = useMemo(() => dateValue.getTimezoneOffset() * 60000, [dateValue]); - - const utcDate = useMemo(() => { - const dateTime = new Date(dateValue); - const utcFromLocal = new Date(dateTime.getTime() + timezoneOffset); - return utcFromLocal; - }, [dateValue, timezoneOffset]); + const timezoneOffset = useMemo(() => new Date().getTimezoneOffset() * 60000, []); const localToUTC = useCallback( (dateTime: Date) => { @@ -105,6 +94,20 @@ const DateTimeControl = ({ : field.default; }, [field.default, field.picker_utc, format, localToUTC]); + const [internalValue, setInternalValue] = useState(value ?? defaultValue); + + const dateValue = useMemo( + () => + format ? parse(internalValue, format, new Date()) ?? defaultValue : parseISO(internalValue), + [defaultValue, format, internalValue], + ); + + const utcDate = useMemo(() => { + const dateTime = new Date(dateValue); + const utcFromLocal = new Date(dateTime.getTime() + timezoneOffset) ?? defaultValue; + return utcFromLocal; + }, [dateValue, defaultValue, timezoneOffset]); + const handleChange = useCallback( (datetime: Date | null) => { if (datetime === null) { @@ -127,27 +130,16 @@ const DateTimeControl = ({ [defaultValue, field.picker_utc, format, localToUTC, onChange], ); - useEffect(() => { - /** - * Set the current date as default value if no value is provided and default is absent. An - * empty default string means the value is intentionally blank. - */ - if (internalValue === undefined) { - setTimeout(() => { - setInternalValue(defaultValue); - onChange(defaultValue); - }, 0); - } - }, [defaultValue, handleChange, internalValue, onChange]); - const dateTimePicker = useMemo(() => { if (dateFormat && !timeFormat) { + const inputDateFormat = typeof dateFormat === 'string' ? dateFormat : 'MMM d, yyyy'; + return ( ( ( 0) { + inputFormat = formatParts.join(' '); + } } return ( @@ -220,7 +216,7 @@ const DateTimeControl = ({ key="mobile-date-time-picker" inputFormat={inputFormat} label={label} - value={field.picker_utc ? utcDate : dateValue} + value={formatDate(field.picker_utc ? utcDate : dateValue, inputFormat)} onChange={handleChange} renderInput={params => ( , - | 'clearFieldErrors' | 'entry' | 'field' | 'fieldsErrors' @@ -98,7 +97,6 @@ interface ListItemProps const ListItem = ({ index, - clearFieldErrors, entry, field, fieldsErrors, @@ -201,7 +199,6 @@ const ListItem = ({ key={index} field={objectField} value={value} - clearFieldErrors={clearFieldErrors} fieldsErrors={fieldsErrors} submitted={submitted} parentPath={path} diff --git a/core/src/widgets/markdown/MarkdownControl.tsx b/core/src/widgets/markdown/MarkdownControl.tsx index e0faebd1..8fcfaab9 100644 --- a/core/src/widgets/markdown/MarkdownControl.tsx +++ b/core/src/widgets/markdown/MarkdownControl.tsx @@ -86,7 +86,6 @@ const MarkdownControl = ({ openMediaLibrary({ controlID, forImage, - privateUpload: false, allowMultiple: false, field, config: 'config' in mediaLibraryFieldOptions ? mediaLibraryFieldOptions.config : undefined, diff --git a/core/src/widgets/object/ObjectControl.tsx b/core/src/widgets/object/ObjectControl.tsx index 8d2d5ed4..acd1b46e 100644 --- a/core/src/widgets/object/ObjectControl.tsx +++ b/core/src/widgets/object/ObjectControl.tsx @@ -47,7 +47,6 @@ const StyledNoFieldsMessage = styled('div')` `; const ObjectControl = ({ - clearFieldErrors, field, fieldsErrors, submitted, @@ -89,7 +88,6 @@ const ObjectControl = ({ key={index} field={field} value={fieldValue} - clearFieldErrors={clearFieldErrors} fieldsErrors={fieldsErrors} submitted={submitted} parentPath={path} @@ -104,7 +102,6 @@ const ObjectControl = ({ }) ?? null ); }, [ - clearFieldErrors, fieldsErrors, i18n, isFieldDuplicate, diff --git a/core/src/widgets/select/SelectControl.tsx b/core/src/widgets/select/SelectControl.tsx index 0152fe18..29369d86 100644 --- a/core/src/widgets/select/SelectControl.tsx +++ b/core/src/widgets/select/SelectControl.tsx @@ -34,7 +34,7 @@ const SelectControl = ({ }: WidgetControlProps) => { const [internalValue, setInternalValue] = useState(value); - const fieldOptions: (string | Option)[] = useMemo(() => field.options, [field.options]); + const fieldOptions: (string | number | Option)[] = useMemo(() => field.options, [field.options]); const isMultiple = useMemo(() => field.multiple ?? false, [field.multiple]); const options = useMemo( diff --git a/website/content/docs/add-to-your-site-bundling.mdx b/website/content/docs/add-to-your-site-bundling.mdx index 2278eec0..92fa2fab 100644 --- a/website/content/docs/add-to-your-site-bundling.mdx +++ b/website/content/docs/add-to-your-site-bundling.mdx @@ -40,7 +40,7 @@ For GitHub and GitLab repositories, you can start your Static CMS `config.yml` f ```yaml backend: - title: git-gateway + name: git-gateway branch: main # Branch to update (optional; defaults to main) ``` @@ -92,18 +92,18 @@ Given this example, our `collections` settings would look like this in your Stat ```yaml collections: - - title: 'blog' # Used in routes, e.g., /admin/collections/blog + - name: 'blog' # Used in routes, e.g., /admin/collections/blog label: 'Blog' # Used in the UI folder: '_posts/blog' # The path to the folder where the documents are stored create: true # Allow users to create new documents in this collection slug: '{{year}}-{{month}}-{{day}}-{{slug}}' # Filename template, e.g., YYYY-MM-DD-title.md fields: # The fields for each document, usually in front matter - - { label: 'Layout', title: 'layout', widget: 'hidden', default: 'blog' } - - { label: 'Title', title: 'title', widget: 'string' } - - { label: 'Publish Date', title: 'date', widget: 'datetime' } - - { label: 'Featured Image', title: 'thumbnail', widget: 'image' } - - { label: 'Rating (scale of 1-5)', title: 'rating', widget: 'number' } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { label: 'Featured Image', name: 'thumbnail', widget: 'image' } + - { label: 'Rating (scale of 1-5)', name: 'rating', widget: 'number' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` Let's break that down: @@ -119,7 +119,7 @@ Let's break that down: As described above, the `widget` property specifies a built-in or custom UI widget for a given field. When a content editor enters a value into a widget, that value is saved in the document front matter as the value for the `name` specified for that field. A full listing of available widgets can be found in the [Widgets doc](/docs/widgets). -Based on this example, you can go through the post types in your site and add the appropriate settings to your Static CMS `config.yml` file. Each post type should be listed as a separate node under the `collections` field. See the [Collections reference doc](/docs/configuration-options/#collections) for more configuration options. +Based on this example, you can go through the post types in your site and add the appropriate settings to your Static CMS `config.yml` file. Each post type should be listed as a separate node under the `collections` field. See the [Collections reference doc](/docs/collection-overview) for more configuration options. ### Filter @@ -127,14 +127,14 @@ The entries for any collection can be filtered based on the value of a single fi ```yaml collections: - - title: 'posts' + - name: 'posts' label: 'Post' folder: '_posts' filter: field: language value: en fields: - - { label: 'Language', title: 'language' } + - { label: 'Language', name: 'language' } ``` ## Authentication diff --git a/website/content/docs/add-to-your-site-cdn.mdx b/website/content/docs/add-to-your-site-cdn.mdx index 30390511..5dff3480 100644 --- a/website/content/docs/add-to-your-site-cdn.mdx +++ b/website/content/docs/add-to-your-site-cdn.mdx @@ -4,7 +4,7 @@ title: CDN Hosting weight: 4 --- -This tutorial guides you through the steps for adding Static CMS via a public CDN to a site that's built with a common [static site generator](https://www.staticgen.com/), like Jekyll, Nest, Hugo, Hexo, or Gatsby. Alternatively, you can [start from a template](/docs/start-with-a-template) or dive right into [configuration options](/docs/configuration-options). +This tutorial guides you through the steps for adding Static CMS via a public CDN to a site that's built with a common [static site generator](https://www.staticgen.com/), like Jekyll, Next, Hugo, Hexo, or Gatsby. Alternatively, you can [start from a template](/docs/start-with-a-template) or dive right into [configuration options](/docs/configuration-options). ## App File Structure @@ -35,7 +35,7 @@ admin └ config.yml ``` -The first file, `admin/index.html`, is the entry point for the Static CMS admin interface. This means that users navigate to `yoursite.com/admin/` to access it. On the code side, it's a basic HTML starter page that loads the Static CMS JavaScript file from a public CDN. The second file, `admin/config.yml`, is the heart of your Static CMS installation, and a bit more complex. The [Configuration](#configuration) section covers the details. +The first file, `admin/index.html`, is the entry point for the Static CMS admin interface. This means that users navigate to `yoursite.com/admin/` to access it. On the code side, it's a basic HTML starter page that loads the Static CMS JavaScript file from a public CDN and initializes it. The second file, `admin/config.yml`, is the heart of your Static CMS installation, and a bit more complex. The [Configuration](#configuration) section covers the details. In this example, we pull the `admin/index.html` file from a public CDN. @@ -67,15 +67,15 @@ Configuration is different for every site, so we'll break it down into parts. Ad We're using [Netlify](https://www.netlify.com) for our hosting and authentication in this tutorial, so backend configuration is fairly straightforward. -For GitHub and GitLab repositories, you can start your Static CMS `config.yml` file with these lines: +For GitHub repositories, you can start your Static CMS `config.yml` file with these lines: ```yaml backend: - title: git-gateway + name: git-gateway branch: main # Branch to update (optional; defaults to main) ``` -_(For Bitbucket repositories, use the [Bitbucket backend](/docs/bitbucket-backend) instructions instead.)_ +_(For GitLab repositories, use [GitLab backend](/docs/gitlab-backend) and for Bitbucket repositories, use [Bitbucket backend](/docs/bitbucket-backend).)_ The configuration above specifies your backend protocol and your publication branch. Git Gateway is an open source API that acts as a proxy between authenticated users of your site and your site repo. (We'll get to the details of that in the [Authentication section](#authentication) below.) If you leave out the `branch` declaration, it defaults to `main`. @@ -123,18 +123,18 @@ Given this example, our `collections` settings would look like this in your Stat ```yaml collections: - - title: 'blog' # Used in routes, e.g., /admin/collections/blog + - name: 'blog' # Used in routes, e.g., /admin/collections/blog label: 'Blog' # Used in the UI folder: '_posts/blog' # The path to the folder where the documents are stored create: true # Allow users to create new documents in this collection slug: '{{year}}-{{month}}-{{day}}-{{slug}}' # Filename template, e.g., YYYY-MM-DD-title.md fields: # The fields for each document, usually in front matter - - { label: 'Layout', title: 'layout', widget: 'hidden', default: 'blog' } - - { label: 'Title', title: 'title', widget: 'string' } - - { label: 'Publish Date', title: 'date', widget: 'datetime' } - - { label: 'Featured Image', title: 'thumbnail', widget: 'image' } - - { label: 'Rating (scale of 1-5)', title: 'rating', widget: 'number' } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { label: 'Featured Image', name: 'thumbnail', widget: 'image' } + - { label: 'Rating (scale of 1-5)', name: 'rating', widget: 'number' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` Let's break that down: @@ -150,7 +150,7 @@ Let's break that down: As described above, the `widget` property specifies a built-in or custom UI widget for a given field. When a content editor enters a value into a widget, that value is saved in the document front matter as the value for the `name` specified for that field. A full listing of available widgets can be found in the [Widgets doc](/docs/widgets). -Based on this example, you can go through the post types in your site and add the appropriate settings to your Static CMS `config.yml` file. Each post type should be listed as a separate node under the `collections` field. See the [Collections reference doc](/docs/configuration-options/#collections) for more configuration options. +Based on this example, you can go through the post types in your site and add the appropriate settings to your Static CMS `config.yml` file. Each post type should be listed as a separate node under the `collections` field. See the [Collections reference doc](/docs/collection-overview) for more configuration options. ## Authentication diff --git a/website/content/docs/add-to-your-site.mdx b/website/content/docs/add-to-your-site.mdx index 0367e193..c54a4804 100644 --- a/website/content/docs/add-to-your-site.mdx +++ b/website/content/docs/add-to-your-site.mdx @@ -4,6 +4,9 @@ title: Add to Your Site weight: 3 --- -You can adapt Static CMS to a wide variety of projects. It works with any content written in markdown, JSON, YAML, or TOML files, stored in a repo on [GitHub](https://github.com/), [GitLab](https://gitlab.com/), [Bitbucket](https://bitbucket.org) or [Azure](https://azure.microsoft.com/en-us/products/devops/repos/). You can also create your own custom backend. +You can adapt Static CMS to a wide variety of projects. It works with any content written in markdown, JSON, YAML, or TOML files, stored in a repo on [GitHub](https://github.com/), [GitLab](https://gitlab.com/) or [Bitbucket](https://bitbucket.org). You can also create your own custom backend. -You can add Static CMS to your site in two different ways: [CDN hosting](/docs/add-to-your-site-cdn) or [bundling directly into your app](/docs/add-to-your-site-bundling). +You can add Static CMS to your site in two different ways: + +- [CDN hosting](/docs/add-to-your-site-cdn) +- [bundling directly into your app](/docs/add-to-your-site-bundling) diff --git a/website/content/docs/additional-links.mdx b/website/content/docs/additional-links.mdx new file mode 100644 index 00000000..14043a83 --- /dev/null +++ b/website/content/docs/additional-links.mdx @@ -0,0 +1,66 @@ +--- +group: Customization +title: Custom Links & Pages +weight: 60 +--- + +The Static CMS exposes a `window.CMS` global object that you can use to register external links or links custom pages, via `registerAdditionalLink`. The links are displayed at the bottom of the navigation menu in the order they are registered. + +### React Components Inline + +The `registerPreviewTemplate` requires you to provide a React component. If you have a build process in place for your project, it is possible to integrate with this build process. + +However, although possible, it may be cumbersome or even impractical to add a React build phase. For this reason, Static CMS exposes some constructs globally to allow you to create components inline: `h` (alias for React.createElement) as well some basic hooks (`useState`, `useMemo`, `useEffect`, `useCallback`). + +**NOTE**: `createClass` is still provided, allowing for the creation of react class components. However it has now been deprecated and will be removed in `v2.0.0`. + +## Params + +`registerAdditionalLink` takes an `AdditionalLink` object with the following properties: + +| Param | Type | Description | +| ------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| id | string | Unique identifier for the link | +| title | string | Display text for the link | +| data | string
\| [React Function Component](https://reactjs.org/docs/components-and-props.html) |
  • `string` - The href for the link
  • `React Function Component` - A react component to render on the route `/page/[id]`
| +| options | object | _Optional_. See [Options](#options) | + +### Options + +Available options for each additional link are: + +| Param | Type | Description | +| -------- | ------ | --------------------------------------------------------------------------------------- | +| iconName | string | The name of a custom registered icon to display. See [Custom Icons](/docs/custom-icons) | + +## Examples + +### External Links + +```js +CMS.registerAdditionalLink({ + id: 'example', + title: 'Example.com', + data: 'https://example.com', + options: { + icon: 'page', + }, +}); +``` + +### Custom Page + +```js +const CustomPage = () => { + return h('div', {}, 'I am a custom page!'); +}; + +CMS.registerAdditionalLink({ + id: 'custom-page', + title: 'Custom Page', + data: CustomPage, + options: { + icon: 'page', + }, +}); +``` diff --git a/website/content/docs/architecture.mdx b/website/content/docs/architecture.mdx deleted file mode 100644 index 96682a52..00000000 --- a/website/content/docs/architecture.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -group: Contributing -title: Architecture -weight: 200 ---- - -Static CMS is a React application, using Redux for state management with immutable data structures (immutable.js). - -The core abstractions for content editing are `collections`, `entries`, and `widgets`. - -Each `collection` represents a collection of entries. This can either be a collection of similar entries with the same structure, or a set of entries where each has its own structure. - -The structure of an entry is defined as a series of fields, each with a `name`, a `label`, and a `widget`. - -The `widget` determines the UI widget that the content editor will use when editing this field of an entry, as well as how the content of the field is presented in the editing preview. - -Entries are loaded and persisted through a `backend` that will typically represent a `git` repository. - -## State shape / reducers -**Auth:** Keeps track of the logged state and the current user. - -**Config:** Holds the environment configuration (backend type, available collections and fields). - -**Collections:** List of available collections and their fields information. - -**Entries:** Entries for each field. - -**EntryDraft:** Reused for each entry that is edited or created. It holds the entry's temporary data until it's persisted on the backend. - -## Selectors -Selectors are functions defined within reducers used to compute derived data from the Redux store. The available selectors are: - -**selectEntry:** Selects a single entry, given the collection and a slug. - -**selectEntries:** Selects all entries for a given collection. - -**getAsset:** Selects a single AssetProxy object for the given path. - -## Value Objects -**AssetProxy:** AssetProxy is a Value Object that holds information regarding an asset file (for example, an image), whether it's persisted online or held locally in cache. - -For a file persisted online, the AssetProxy only keeps information about its URI. For local files, the AssetProxy will keep a reference to the actual File object while generating the expected final URIs and on-demand blobs for local preview. - -The AssetProxy object can be used directly inside a media tag (such as ``), as it will always return something that can be used by the media tag to render correctly (either the URI for the online file or a single-use blob). - -## Components structure and Workflows -Components are separated into two main categories: Container components and Presentational components. - -### Entry Editing -For either updating an existing entry or creating a new one, the `EntryEditor` is used and the flow is the same: - -* When mounted, the `EntryPage` container component dispatches the `createDraft` action, setting the `entryDraft` state to a blank state (in case of a new entry) or to a copy of the selected entry (in case of an edit). -* The `EntryPage` will also render widgets for each field type in the given entry. -* Widgets are used for editing entry fields. There are different widgets for different field types, and they are always defined in a pair containing a `control` component and a `preview` component. The control component is responsible for presenting the user with the appropriate interface for manipulating the current field value. The preview component is responsible for displaying the value with the appropriate styling. - -#### Widget components implementation -The control component receives one (1) callback as a prop: `onChange`. - -* onChange (required): Should be called when the users changes the current value. It will ultimately end up updating the EntryDraft object in the Redux Store, thus updating the preview component. -* addAsset & onRemoveAsset (optionals): Should be invoked with an `AssetProxy` value object if the field accepts file uploads for media (images, for example). `addAsset` will get the current media stored in the Redux state tree while `onRemoveAsset` will remove it. AssetProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree. - -Both control and preview widgets receive a `getAsset` selector via props. Displaying the media (or its URI) for the user should always be done via `getAsset`, as it returns an AssetProxy that can return the correct value for both medias already persisted on the server and cached media not yet uploaded. - -The actual persistence of the content and medias inserted into the control component is delegated to the backend implementation. The backend will be called with the updated values and a list of assetProxy objects for each field of the entry, and should return a promise that can resolve into the persisted entry object and the list of the persisted media URIs. diff --git a/website/content/docs/azure-backend.mdx b/website/content/docs/azure-backend.mdx deleted file mode 100644 index 12b597c4..00000000 --- a/website/content/docs/azure-backend.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -group: Accounts -title: Azure -weight: 20 ---- -For repositories stored on Azure, the `azure` backend allows CMS users to log in directly with their Azure account. Note that all users must have write access to your content repository for this to work. - -## Authentication - -In order to get Static CMS working with Azure DevOps, you need a Tenant Id and an Application Id. - -1. If you do not have an Azure account, [create one here](https://azure.microsoft.com/en-us/free/?WT.mc_id=A261C142F) and make sure to have a credit card linked to the account. -2. If you do not have an Azure Active Directory Tenant Id, [set one up here](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-create-new-tenant). -3. [Register an application with Azure AD](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app). Configure it as a Single tenant Web application and add a redirect URI (e.g. `http://localhost:8080/`) -4. Add the `Azure DevOps->user_impersonation` [permission](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-permissions-to-access-your-web-api) for the created application. -5. [Grant admin consent](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-configure-app-access-web-apis#admin-consent-button) for the application. -6. Under `Authentication->Implicit grant` enable [Access tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) for the application and click `Save`. -7. Verify your Azure DevOps organization is connected to the same directory as your tenant under: `https://dev.azure.com//_settings/organizationAad` -8. Add the following lines to your Static CMS `config.yml` file: - -```yaml -backend: - title: azure - repo: organization/project/repo # replace with actual path - tenant_id: tenantId # replace with your tenantId - app_id: appId # replace with your appId -``` - -## Limitations - -1. Pagination is not supported so some endpoints might return missing data - -2. Nested collection are partially supported as Azure doesn't allow [renaming and editing](https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pushes/create?view=azure-devops-rest-6.1&source=docs#rename-a-file) in a single operation diff --git a/website/content/docs/backends-overview.mdx b/website/content/docs/backends-overview.mdx index a21dc938..d64f8345 100644 --- a/website/content/docs/backends-overview.mdx +++ b/website/content/docs/backends-overview.mdx @@ -1,5 +1,5 @@ --- -group: Accounts +group: Backends title: Overview weight: 1 --- @@ -8,17 +8,18 @@ A backend is JavaScript code that allows Static CMS to communicate with a servic ## Backend Configuration -Individual backends should provide their own configuration documentation, but there are some configuration options that are common to multiple backends. A full reference is below. Note that these are properties of the `backend` field, and should be nested under that field. +Individual backends provide their own configuration documentation, but there are some configuration options that are common to multiple backends. A full reference is below. Note that these are properties of the `backend` field, and should be nested under that field. -| Field | Default | Description | -| --------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| `repo` | none | **Required** for `github`, `gitlab`, and `bitbucket` backends; ignored by `git-gateway`. Follows the pattern `[org-or-username]/[repo-name]`. | -| `branch` | `main` | The branch where published content is stored. All CMS commits and PRs are made to this branch. | -| `api_root` | `https://api.github.com` (GitHub), `https://gitlab.com/api/v4` (GitLab), or `https://api.bitbucket.org/2.0` (Bitbucket) | The API endpoint. Only necessary in certain cases, like with GitHub Enterprise or self-hosted GitLab. | -| `site_domain` | `location.hostname` (or `cms.netlify.com` when on `localhost`) | Sets the `site_id` query param sent to the API endpoint. Non-Netlify auth setups will often need to set this for local development to work properly. | -| `base_url` | `https://api.netlify.com` (GitHub, Bitbucket) or `https://gitlab.com` (GitLab) | OAuth client hostname (just the base domain, no path). **Required** when using an external OAuth server or self-hosted GitLab. | -| `auth_endpoint` | `auth` (GitHub, Bitbucket) or `oauth/authorize` (GitLab) | Path to append to `base_url` for authentication requests. Optional. | +| Name | Type | Default | Description | +| ------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | 'git-gateway'
\| 'github'
\| 'gitlab'
\| 'bitbucket'
\| 'test-repo'
\| 'proxy' | | The backend git provider | +| repo | string | | Required for `github`, `gitlab`, and `bitbucket` backends. Ignored by `git-gateway`. Follows the pattern `[org-or-username]/[repo-name]` | +| branch | string | `main` | _Optional_. The branch where published content is stored. All CMS commits and PRs are made to this branch | +| api_root | string | GitHub
`https://api.github.com`

GitLab
`https://gitlab.com/api/v4`

Bitbucket
`https://api.bitbucket.org/2.0` | _Optional_. The API endpoint. Only necessary in certain cases, like with GitHub Enterprise or self-hosted GitLab | +| site_domain | string | `location.hostname`

On `localhost`
`cms.netlify.com` | _Optional_. Sets the `site_id` query param sent to the API endpoint. Non-Netlify auth setups will often need to set this for local development to work properly | +| base_url | string | GitHub or Bitbucket
`https://api.netlify.com`

GitLab
`https://gitlab.com` | _Optional_. OAuth client hostname (just the base domain, no path). **Required** when using an external OAuth server or self-hosted GitLab | +| auth_endpoint | string | GitHub or Bitbucket
`auth`

GitLab
`oauth/authorize` | _Optional_. Path to append to `base_url` for authentication requests. | ## Creating a New Backend -Anyone can write a backend, but we don't yet have a finalized and documented API. If you would like to write your own backend for a service that does not have one currently, we recommend using the [GitHub backend](https://github.com/StaticJsCMS/static-cms/tree/main/src/backends/github) as a reference for API and best practices. +Anyone can write a backend, but the API is not yet finalized and documented. If you would like to write your own backend for a service that does not have one currently, Static CMS recommends using the [GitHub backend](https://github.com/StaticJsCMS/static-cms/tree/main/core/src/backends/github) as a reference for API and best practices. diff --git a/website/content/docs/beta-features.mdx b/website/content/docs/beta-features.mdx index 546f2769..c324874a 100644 --- a/website/content/docs/beta-features.mdx +++ b/website/content/docs/beta-features.mdx @@ -1,56 +1,13 @@ --- -group: Configuration -title: Beta Features! +group: Intro +title: Beta Features weight: 200 --- -We run new functionality in an open beta format from time to time. That means that this functionality is totally available for use, and we *think* it might be ready for primetime, but it could break or change without notice. + +Static CMS runs new functionality in an open beta format from time to time. That means that this functionality is totally available for use, an it might be ready for primetime, but it could break or change without notice. **Use these features at your own risk.** -## Working with a Local Git Repository - -You can connect Static CMS to a local Git repository, instead of working with a live repo. - -1. Navigate to a local Git repository configured with the CMS. -2. Add the top-level property `local_backend` configuration to your `config.yml`: - -```yaml -backend: - title: git-gateway - -# when using the default proxy server port -local_backend: true -``` - -3. Run `npx @staticcms/proxy-server` from the root directory of the above repository. - - * If the default port (8081) is in use, the proxy server won't start and you will see an error message. In this case, follow [these steps](#configure-the-@staticcms/proxy-server-port-number) before proceeding. -4. Start your local development server (e.g. run `gatsby develop`). -5. Open `http://localhost:/admin` to verify that your can administer your content locally. Replace `` with the port of your local development server. For example Gatsby's default port is `8000` - -**Note:** `@staticcms/proxy-server` runs an unauthenticated express server. As any client can send requests to the server, it should only be used for local development. - -### Configure the Static CMS proxy server port number - -1. Create a `.env` file in the project's root folder and define the PORT you'd like the proxy server to use - -```ini -PORT=8082 -``` - -2. Update the `local_backend` object in `config.yml` and specify a `url` property to use your custom port number - -```yaml -backend: - title: git-gateway - -local_backend: - # when using a custom proxy server port - url: http://localhost:8082/api/v1 - # when accessing the local site from a host other than 'localhost' or '127.0.0.1' - allowed_hosts: ['192.168.0.1'] -``` - ## i18n Support The CMS can provide a side by side interface for authoring content in multiple languages. @@ -78,7 +35,7 @@ i18n: ```yaml collections: - - title: i18n_content + - name: i18n_content # same as the top level, but all fields are optional and defaults to the top level # can also be a boolean to accept the top level defaults i18n: true @@ -88,20 +45,20 @@ When using a file collection, you must also enable i18n for each individual file ```yaml collections: - - title: pages + - name: pages label: Pages # Configure i18n for this collection. i18n: structure: single_file locales: [en, de, fr] files: - - title: about + - name: about label: About Page file: site/content/about.yml # Enable i18n for this file. i18n: true fields: - - { label: Title, title: title, widget: string, i18n: true } + - { label: Title, name: title, widget: string, i18n: true } ``` ### Field level configuration @@ -109,17 +66,17 @@ collections: ```yaml fields: - label: Title - title: title + name: title widget: string # same as 'i18n: translate'. Allows translation of the title field i18n: true - label: Date - title: date + name: date widget: datetime # The date field will be duplicated from the default locale. i18n: duplicate - label: Body - title: body + name: body # The markdown field will be omitted from the translation. widget: markdown ``` @@ -132,22 +89,22 @@ i18n: locales: [en, de, fr] collections: - - title: posts + - name: posts label: Posts folder: content/posts create: true i18n: true fields: - label: Title - title: title + name: title widget: string i18n: true - label: Date - title: date + name: date widget: datetime i18n: duplicate - label: Body - title: body + name: body widget: markdown ``` @@ -159,96 +116,29 @@ collections: ```yaml - label: 'Object' - title: 'object' + name: 'object' widget: 'object' i18n: true fields: - - { label: 'String', title: 'string', widget: 'string', i18n: true } - - { label: 'Date', title: 'date', widget: 'datetime', i18n: duplicate } - - { label: 'Boolean', title: 'boolean', widget: 'boolean', i18n: duplicate } + - { label: 'String', name: 'string', widget: 'string', i18n: true } + - { label: 'Date', name: 'date', widget: 'datetime', i18n: duplicate } + - { label: 'Boolean', name: 'boolean', widget: 'boolean', i18n: duplicate } - { label: 'Object', - title: 'object', + name: 'object', widget: 'object', i18n: true, - field: { label: 'String', title: 'string', widget: 'string', i18n: duplicate }, + field: { label: 'String', name: 'string', widget: 'string', i18n: duplicate }, } ``` ## Folder Collections Path -By default the CMS stores folder collection content under the folder specified in the collection setting. +See [Folder Collections Path](/docs/collection-types#folder-collections-path). -For example configuring `folder: posts` for a collection will save the content under `posts/post-title.md`. +## Nested Collections -You can now specify an additional `path` template (similar to the `slug` template) to control the content destination. - -This allows saving content in subfolders, e.g. configuring `path: '{{year}}/{{slug}}'` will save the content under `posts/2019/post-title.md`. - -## Folder Collections Media and Public Folder - -By default the CMS stores media files for all collections under a global `media_folder` directory as specified in the configuration. - -When using the global `media_folder` directory any entry field that points to a media file will use the absolute path to the published file as designated by the `public_folder` configuration. - -For example configuring: - -```yaml -media_folder: static/media -public_folder: /media -``` - -And saving an entry with an image named `image.png` will result in the image being saved under `static/media/image.png` and relevant entry fields populated with the value of `/media/image.png`. - -Some static site generators (e.g. Gatsby) work best when using relative image paths. - -This can now be achieved using a per collection `media_folder` configuration which specifies a relative media folder for the collection. - -For example, the following configuration will result in media files being saved in the same directory as the entry, and the image field being populated with the relative path to the image. - -```yaml -media_folder: static/media -public_folder: /media -collections: - - title: posts - label: Posts - label_singular: 'Post' - folder: content/posts - path: '{{slug}}/index' - media_folder: '' - public_folder: '' - fields: - - label: Title - title: title - widget: string - - label: 'Cover Image' - title: 'image' - widget: 'image' -``` - -More specifically, saving an entry with a title of `example post` with an image named `image.png` will result in a directory structure of: - -```bash -content - posts - example-post - index.md - image.png -``` - -And for the image field being populated with a value of `image.png`. - -**Note: When specifying a `path` on a folder collection, `media_folder` defaults to an empty string.** - -**Available template tags:** - -Supports all of the [`slug` templates](/docs/configuration-options#slug) and: - -* `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`. -* `{{filename}}` The file name without the extension part. -* `{{extension}}` The file extension. -* `{{media_folder}}` The global `media_folder`. -* `{{public_folder}}` The global `public_folder`. +Seed [Nested Collections](/docs/collection-types#nested-collections). ## List Widget: Variable Types @@ -264,9 +154,9 @@ To use variable types in the list widget, update your field configuration as fol ### Additional list widget options -* `types`: a nested list of object widgets. All widgets must be of type `object`. Every object widget may define different set of fields. -* `typeKey`: the name of the field that will be added to every item in list representing the name of the object widget that item belongs to. Ignored if `types` is not defined. Default is `type`. -* `summary`: allows customization of a collapsed list item object in a similar way to a [collection summary](/docs/configuration-options/?#summary) +- `types`: a nested list of object widgets. All widgets must be of type `object`. Every object widget may define different set of fields. +- `typeKey`: the name of the field that will be added to every item in list representing the name of the object widget that item belongs to. Ignored if `types` is not defined. Default is `type`. +- `summary`: allows customization of a collapsed list item object in a similar way to a [collection summary](/docs/configuration-options/?#summary) ### Example Configuration @@ -275,27 +165,27 @@ either a "carousel" or a "spotlight". Each type has a unique name and set of fie ```yaml - label: 'Home Section' - title: 'sections' + name: 'sections' widget: 'list' types: - label: 'Carousel' - title: 'carousel' + name: 'carousel' widget: object summary: '{{fields.header}}' fields: - - { label: Header, title: header, widget: string, default: 'Image Gallery' } - - { label: Template, title: template, widget: string, default: 'carousel.html' } + - { label: Header, name: header, widget: string, default: 'Image Gallery' } + - { label: Template, name: template, widget: string, default: 'carousel.html' } - label: Images - title: images + name: images widget: list - field: { label: Image, title: image, widget: image } + field: { label: Image, name: image, widget: image } - label: 'Spotlight' - title: 'spotlight' + name: 'spotlight' widget: object fields: - - { label: Header, title: header, widget: string, default: 'Spotlight' } - - { label: Template, title: template, widget: string, default: 'spotlight.html' } - - { label: Text, title: text, widget: text, default: 'Hello World' } + - { label: Header, name: header, widget: string, default: 'Spotlight' } + - { label: Template, name: template, widget: string, default: 'spotlight.html' } + - { label: Text, name: text, widget: text, default: 'Hello World' } ``` ### Example Output @@ -367,7 +257,7 @@ init() init({ config: { backend: { - title: 'git-gateway', + name: 'git-gateway', }, }, }) @@ -383,17 +273,17 @@ init({ init({ config: { backend: { - title: 'git-gateway', + name: 'git-gateway', }, load_config_file: false, media_folder: "static/images/uploads", public_folder: "/images/uploads", collections: [ - { label: "Blog", title: "blog", folder: "_posts/blog", create: true, fields: [ - { label: "Title", title: "title", widget: "string" }, - { label: "Publish Date", title: "date", widget: "datetime" }, - { label: "Featured Image", title: "thumbnail", widget: "image" }, - { label: "Body", title: "body", widget: "markdown" }, + { label: "Blog", name: "blog", folder: "_posts/blog", create: true, fields: [ + { label: "Title", name: "title", widget: "string" }, + { label: "Publish Date", name: "date", widget: "datetime" }, + { label: "Featured Image", name: "thumbnail", widget: "image" }, + { label: "Body", name: "body", widget: "markdown" }, ]}, ], }, @@ -422,22 +312,22 @@ backend: Static CMS generates the following commit types: -| Commit type | When is it triggered? | Available template tags | -| --------------- | ---------------------------------------- | ----------------------------------------------------------- | -| `create` | A new entry is created | `slug`, `path`, `collection`, `author-login`, `author-name` | -| `update` | An existing entry is changed | `slug`, `path`, `collection`, `author-login`, `author-name` | -| `delete` | An existing entry is deleted | `slug`, `path`, `collection`, `author-login`, `author-name` | -| `uploadMedia` | A media file is uploaded | `path`, `author-login`, `author-name` | -| `deleteMedia` | A media file is deleted | `path`, `author-login`, `author-name` | +| Commit type | When is it triggered? | Available template tags | +| ------------- | ---------------------------- | ----------------------------------------------------------- | +| `create` | A new entry is created | `slug`, `path`, `collection`, `author-login`, `author-name` | +| `update` | An existing entry is changed | `slug`, `path`, `collection`, `author-login`, `author-name` | +| `delete` | An existing entry is deleted | `slug`, `path`, `collection`, `author-login`, `author-name` | +| `uploadMedia` | A media file is uploaded | `path`, `author-login`, `author-name` | +| `deleteMedia` | A media file is deleted | `path`, `author-login`, `author-name` | Template tags produce the following output: -* `{{slug}}`: the url-safe filename of the entry changed -* `{{collection}}`: the name of the collection containing the entry changed -* `{{path}}`: the full path to the file changed -* `{{message}}`: the relevant message based on the current change (e.g. the `create` message when an entry is created) -* `{{author-login}}`: the login/username of the author -* `{{author-name}}`: the full name of the author (might be empty based on the user's profile) +- `{{slug}}`: the url-safe filename of the entry changed +- `{{collection}}`: the name of the collection containing the entry changed +- `{{path}}`: the full path to the file changed +- `{{message}}`: the relevant message based on the current change (e.g. the `create` message when an entry is created) +- `{{author-login}}`: the login/username of the author +- `{{author-name}}`: the full name of the author (might be empty based on the user's profile) ## Image widget file size limit @@ -447,7 +337,7 @@ Example config: ```yaml - label: 'Featured Image' - title: 'thumbnail' + name: 'thumbnail' widget: 'image' default: '/uploads/chocolate-dogecoin.jpg' media_library: @@ -463,18 +353,18 @@ Example config: ```yaml collections: - - title: 'posts' + - name: 'posts' label: 'Posts' folder: '_posts' summary: "{{title | upper}} - {{date | date('YYYY-MM-DD')}} – {{body | truncate(20, '***')}}" fields: - - { label: 'Title', title: 'title', widget: 'string' } - - { label: 'Publish Date', title: 'date', widget: 'datetime' } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` The above config will transform the title field to uppercase and format the date field using `YYYY-MM-DD` format. -Available transformations are `upper`, `lower`, `date('')`, `default('defaultValue')`, `ternary('valueForTrue','valueForFalse')` and `truncate()`/`truncate(, '')` +Available transformations are `upper`, `lower`, `date('')`, `default('defaultValue')`, `ternary('valueForTrue','valueForFalse')` and `truncate()`/`truncate(, '')` ## Registering to CMS Events @@ -484,7 +374,7 @@ Example usage: ```javascript CMS.registerEventListener({ - title: 'prePublish', + name: 'prePublish', handler: ({ author, entry }) => console.info(JSON.stringify({ author, data: entry.data })), }); ``` @@ -493,7 +383,7 @@ Supported events are `prePublish`, `postPublish`, `preSave` and `postSave`. The ```javascript CMS.registerEventListener({ - title: 'preSave', + name: 'preSave', handler: ({ entry }) => { return entry.data.set('title', 'new title'); }, @@ -508,23 +398,23 @@ For example given the configuration: ```yaml collections: - - title: posts + - name: posts label: Posts folder: content/posts create: true fields: - label: Title - title: title + name: title widget: string - label: Object - title: object + name: object widget: object fields: - label: Title - title: title + name: title widget: string - label: body - title: body + name: body widget: markdown ``` @@ -534,45 +424,3 @@ will open the editor for a new post with the `title` field populated with `first with `second` and the markdown `body` field with `# content`. **Note:** URL Encoding might be required for certain values (e.g. in the previous example the value for `body` is URL encoded). - -## Nested Collections - -Allows a folder collection to show a nested structure of entries and edit the locations of the entries. - -Example configuration: - -```yaml -collections: - - title: pages - label: Pages - label_singular: 'Page' - folder: content/pages - create: true - # adding a nested object will show the collection folder structure - nested: - depth: 100 # max depth to show in the collection tree - summary: '{{title}}' # optional summary for a tree node, defaults to the inferred title field - fields: - - label: Title - title: title - widget: string - - label: Body - title: body - widget: markdown -``` - -Nested collections expect the following directory structure: - -```bash -content -└── pages - ├── authors - │ ├── author-1 - │ │ └── index.md - │ └── index.md - ├── index.md - └── posts - ├── hello-world - │ └── index.md - └── index.md -``` diff --git a/website/content/docs/bitbucket-backend.mdx b/website/content/docs/bitbucket-backend.mdx index a26aaefc..7ae7f47f 100644 --- a/website/content/docs/bitbucket-backend.mdx +++ b/website/content/docs/bitbucket-backend.mdx @@ -1,8 +1,11 @@ --- -group: Accounts +group: Backends title: Bitbucket -weight: 20 +weight: 30 --- + +- **Name**: `bitbucket` + For repositories stored on Bitbucket, the `bitbucket` backend allows CMS users to log in directly with their Bitbucket account. Note that all users must have write access to your content repository for this to work. ## Authentication @@ -12,8 +15,8 @@ To enable Bitbucket authentication it: 1. Follow the authentication provider setup steps in the [Netlify docs](https://www.netlify.com/docs/authentication-providers/#using-an-authentication-provider). 2. Add the following lines to your Static CMS `config.yml` file: - ```yaml - backend: - title: bitbucket - repo: owner-name/repo-name # Path to your Bitbucket repository - ``` +```yaml +backend: + name: bitbucket + repo: owner-name/repo-name # Path to your Bitbucket repository +``` diff --git a/website/content/docs/cloudinary.mdx b/website/content/docs/cloudinary.mdx index 273fa1a3..3ba36a7a 100644 --- a/website/content/docs/cloudinary.mdx +++ b/website/content/docs/cloudinary.mdx @@ -3,6 +3,9 @@ group: Media title: Cloudinary weight: 10 --- + +## Beta Feature. Use at your own risk + Cloudinary is a digital asset management platform with a broad feature set, including support for responsive image generation and url based image transformation. They also provide a powerful media library UI for managing assets, and tools for organizing your assets into a hierarchy. The Cloudinary media library integration for Static CMS uses Cloudinary's own media library interface within Static CMS. To get started, you'll need a Cloudinary account and Static CMS 2.3.0 or greater. @@ -13,32 +16,33 @@ You can [sign up for Cloudinary](https://cloudinary.com/users/register/free) for ![Cloudinary console screenshot](/img/cloudinary-console-details.webp) -## Connecting Cloudinary to Static CMS +## Connecting Cloudinary To use the Cloudinary media library within Static CMS, you'll need to update your Static CMS configuration file with the information from your Cloudinary account: ```yaml media_library: - title: cloudinary + name: cloudinary config: - cloud_title: your_cloud_name + cloud_name: your_cloud_name api_key: your_api_key ``` -**Note:** The user must be logged in to the Cloudinary account connected to the `api_key` used in your Static CMS configuration. +**Note:** The user must be logged in to the Cloudinary account connected to the `api_key` used in your Static CMS configuration. ### Security Considerations + Although this setup exposes the `cloud_name` and `api_key` publicly via the `/admin/config.yml` endpoint, this information is not sensitive. Any integration of the Cloudinary media library requires this information to be exposed publicly. To use this library or use the restricted Cloudinary API endpoints, the user must have access to the Cloudinary account login details or the `api_secret` associated with the `cloud_name` and `api_key`. ## Static CMS configuration options The following options are specific to the Static CMS integration for Cloudinary: -* **`output_filename_only`**: _(default: `false`)_\ +- **`output_filename_only`**: _(default: `false`)_\ By default, the value provided for a selected image is a complete URL for the asset on Cloudinary's CDN. Setting `output_filename_only` to `true` will instead produce just the filename (e.g. `image.jpg`). This should be `true` if you will be directly embedding cloudinary transformation urls in page templates. Refer to [Inserting Cloudinary URL in page templates](#inserting-cloudinary-url-in-page-templates). -* **`use_transformations`**: _(default: `true`)_\ +- **`use_transformations`**: _(default: `true`)_\ If `true`, uses derived url when available (the url will have image transformation segments included). Has no effect if `output_filename_only` is set to `true`. -* **`use_secure_url`**: _(default: `true`)_\ +- **`use_secure_url`**: _(default: `true`)_\ Controls whether an `http` or `https` URL is provided. Has no effect if `output_filename_only` is set to `true`. ## Cloudinary configuration options @@ -47,20 +51,21 @@ The following options are used to configure the media library. All options are l ### Authentication -* `cloud_name` -* `api_key` +- `cloud_name` +- `api_key` ### Media library behavior -* `default_transformations` _\- only the first [image transformation](#image-transformations) is used, be sure to use the `SDK Parameter` column transformation names from the_ [_transformation reference_](https://cloudinary.com/documentation/image_transformation_reference) -* `max_files` _\- has no impact on images inside the [markdown widget](/docs/widgets/#markdown)_. Refer to [media library documentation](https://cloudinary.com/documentation/media_library_widget#3_set_the_configuration_options) for details on this property -* `multiple` _\- has no impact on images inside the [markdown widget](/docs/widgets/#markdown)_. Refer to [media library documentation](https://cloudinary.com/documentation/media_library_widget#3_set_the_configuration_options) for details on this property +- `default_transformations` _\- only the first [image transformation](#image-transformations) is used, be sure to use the `SDK Parameter` column transformation names from the_ [_transformation reference_](https://cloudinary.com/documentation/image_transformation_reference) +- `max_files` _\- has no impact on images inside the [markdown widget](/docs/widgets/#markdown)_. Refer to [media library documentation](https://cloudinary.com/documentation/media_library_widget#3_set_the_configuration_options) for details on this property +- `multiple` _\- has no impact on images inside the [markdown widget](/docs/widgets/#markdown)_. Refer to [media library documentation](https://cloudinary.com/documentation/media_library_widget#3_set_the_configuration_options) for details on this property ## Image transformations The Cloudinary integration allows images to be transformed in two ways: directly within Static CMS via [Cloudinary's Media Library](#transforming-images-via-media-library), and separately from the CMS via Cloudinary's [dynamic URL's](https://cloudinary.com/documentation/image_transformations#delivering_media_assets_using_dynamic_urls) by [inserting cloudinary urls](#inserting-cloudinary-url-in-page-templates). -### Transforming images via Media Library +### Transforming Images + If you transform and insert images from within the Cloudinary media library, the transformed image URL will be output by default. This gives the editor complete freedom to make changes to the image output. There are two ways to configure image transformation via media library - [globally](#global-configuration) and per [field](#field-configuration). Global options will be overridden by field options. @@ -73,7 +78,7 @@ instance of the Cloudinary widget. ```yaml # global media_library: - title: cloudinary + name: cloudinary output_filename_only: false config: default_transformations: @@ -92,43 +97,45 @@ For example: ```yaml # field fields: # The fields each document in this collection have -- label: 'Cover Image' - title: 'image' - widget: 'image' - required: false - tagtitle: '' - media_library: - config: - default_transformations: - - fetch_format: auto - width: 300 - quality: auto - crop: fill - effect: grayscale + - label: 'Cover Image' + name: 'image' + widget: 'image' + required: false + tagtitle: '' + media_library: + config: + default_transformations: + - fetch_format: auto + width: 300 + quality: auto + crop: fill + effect: grayscale ``` ## Inserting Cloudinary URL in page templates If you prefer to provide direction so that images are transformed in a specific way, or dynamically retrieve images based on viewport size, you can do so by providing your own base Cloudinary URL and only storing the asset filenames in your content: -* Either globally or for specific fields, configure the Cloudinary extension to only output the asset filename +- Either globally or for specific fields, configure the Cloudinary extension to only output the asset filename ```yaml # global media_library: - title: cloudinary + name: cloudinary output_filename_only: true # field media_library: - title: cloudinary + name: cloudinary output_filename_only: true ``` -* Provide a dynamic URL in the site template +- Provide a dynamic URL in the site template ```handlebars {{! handlebars example }} - + ``` Your dynamic URL can be formed conditionally to provide any desired transformations - please see Cloudinary's [image transformation reference](https://cloudinary.com/documentation/image_transformation_reference) for available transformations. diff --git a/website/content/docs/collection-overview.mdx b/website/content/docs/collection-overview.mdx new file mode 100644 index 00000000..b59b2f3e --- /dev/null +++ b/website/content/docs/collection-overview.mdx @@ -0,0 +1,217 @@ +--- +group: Collections +title: Collections Configuration +weight: 9 +--- + +`collections` accepts a list of collection objects, each with the following options + +| Name | Type | Default | Description | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | | Unique identifier for the collection, used as the key when referenced in other contexts (like the [relation widget](/docs/widgets/#relation)) | +| identifier_field | string | `'title'` | _Optional_. See [identifier_field](#identifier_field) below | +| label | string | `name` | _Optional_. Label for the collection in the editor UI | +| label_singular | string | `label` | _Optional_. singular label for certain elements in the editor | +| icon | string | | Unique name of icon to use in main menu. See [Custom Icons](/docs/custom-icons) | +| description | string | | _Optional_. Text displayed below the label when viewing a collection | +| files or folder | [Collection Files](/docs/collection-types#file-collections)
\| [Collection Folder](/docs/collection-types#folder-collections) | | **Requires one of these**: Specifies the collection type and location; details in [Collection Types](/docs/collection-types) | +| filter | string | | _Optional_. Filter for [Folder Collections](/docs/collection-types#folder-collections) | +| create | string | `false` | **For [Folder Collections](/docs/collection-types#folder-collections) only**
`true` - Allows users to create new items in the collection | +| hide | string | `false` | `true` hides a collection in the CMS UI. Useful when using the relation widget to hide referenced collections | +| delete | string | `true` | `false` prevents users from deleting items in a collection | +| extension | string | | See [extension](#extension-and-format) below | +| format | string | | See [format](#extension-and-format) below | +| frontmatter_delimiter | string | | See [frontmatter_delimiter](#frontmatter_delimiter) below | +| slug | string | | See [slug](#slug) below | +| fields (required) | string | | See [fields](#fields) below | +| editor | string | | See [editor](#editor) below | +| summary | string | | See [summary](#summary) below | +| sortable_fields | string | | See [sortable_fields](#sortable_fields) below | +| view_filters | string | | See [view_filters](#view_filters) below | +| view_groups | string | | See [view_groups](#view_groups) below | + +## `identifier_field` + +Static CMS expects every entry to provide a field named `"title"` that serves as an identifier for the entry. The identifier field serves as an entry's title when viewing a list of entries, and is used in [slug](#slug) creation. If you would like to use a field other than `"title"` as the identifier, you can set `identifier_field` to the name of the other field. + +### Example + +```yaml +collections: + - name: posts + identifier_field: name +``` + +## `extension` and `format` + +These settings determine how collection files are parsed and saved. Both are optional—Static CMS will attempt to infer your settings based on existing items in the collection. If your collection is empty, or you'd like more control, you can set these fields explicitly. + +`extension` determines the file extension searched for when finding existing entries in a folder collection and it determines the file extension used to save new collection items. It accepts the following values: `yml`, `yaml`, `toml`, `json`, `md`, `markdown`, `html`. + +You may also specify a custom `extension` not included in the list above, as long as the collection files can be parsed and saved in one of the supported formats below. + +`format` determines how collection files are parsed and saved. It will be inferred if the `extension` field or existing collection file extensions match one of the supported extensions above. It accepts the following values: + +- `yml` or `yaml`: parses and saves files as YAML-formatted data files; saves with `yml` extension by default +- `toml`: parses and saves files as TOML-formatted data files; saves with `toml` extension by default +- `json`: parses and saves files as JSON-formatted data files; saves with `json` extension by default +- `frontmatter`: parses files and saves files with data frontmatter followed by an unparsed body text (edited using a `body` field); saves with `md` extension by default; default for collections that can't be inferred. Collections with `frontmatter` format (either inferred or explicitly set) can parse files with frontmatter in YAML, TOML, or JSON format. However, they will be saved with YAML frontmatter. +- `yaml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as YAML, followed by unparsed body text. The default delimiter for this option is `---`. +- `toml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as TOML, followed by unparsed body text. The default delimiter for this option is `+++`. +- `json-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved as JSON, followed by unparsed body text. The default delimiter for this option is `{` `}`. + +## `frontmatter_delimiter` + +If you have an explicit frontmatter format declared, this option allows you to specify a custom delimiter like `~~~`. If you need different beginning and ending delimiters, you can use an array like `["(", ")"]`. + +## `slug` + +For folder collections where users can create new items, the `slug` option specifies a template for generating new filenames based on a file's creation date and `title` field. (This means that all collections with `create: true` must have a `title` field (a different field can be used via [`identifier_field`](#identifier_field)). + +The slug template can also reference a field value by name, eg. `{{title}}`. If a field name conflicts with a built in template tag name - for example, if you have a field named `slug`, and would like to reference that field via `{{slug}}`, you can do so by adding the explicit `fields.` prefix, eg. `{{fields.slug}}`. + +### Available Template Tags + +- Any field can be referenced by wrapping the field name in double curly braces, eg. `{{author}}` +- `{{slug}}`: a url-safe version of the `title` field (or identifier field) for the file +- `{{year}}`: 4-digit year of the file creation date +- `{{month}}`: 2-digit month of the file creation date +- `{{day}}`: 2-digit day of the month of the file creation date +- `{{hour}}`: 2-digit hour of the file creation date +- `{{minute}}`: 2-digit minute of the file creation date +- `{{second}}`: 2-digit second of the file creation date + +### Examples + +#### Basic Example + +```yaml +slug: '{{year}}-{{month}}-{{day}}_{{slug}}' +``` + +#### Field Names + +```yaml +slug: '{{year}}-{{month}}-{{day}}_{{title}}_{{some_other_field}}' +``` + +#### Field Name That Conflicts With Template Tag + +```yaml +slug: '{{year}}-{{month}}-{{day}}_{{fields.slug}}' +``` + +## `fields` + +The `fields` option maps editor UI widgets to field-value pairs in the saved file. The order of the fields in your Static CMS `config.yml` file determines their order in the editor UI and in the saved file. + +`fields` accepts a list of widgets. See [widgets](/docs/widgets) for more details. + +In files with frontmatter, one field should be named `body`. This special field represents the section of the document (usually markdown) that comes after the frontmatter. + +### Example + +```yaml +fields: + - label: "Title" + name: "title" + widget: "string" + pattern: ['.{20,}', "Must have at least 20 characters"] + - {label: "Layout", name: "layout", widget: "hidden", default: "blog"} + - {label: "Featured Image", name: "thumbnail", widget: "image", required: false} + - {label: "Body", name: "body", widget: "markdown"} + comment: 'This is a multiline\ncomment' +``` + +## `editor` + +This setting changes options for the editor view of a collection or a file inside a files collection. It has the following options: + +| Name | Type | Default | Description | +| ------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------ | +| preview | boolean | `true` | Set to `false` to disable the preview pane for this collection or file | +| frame | boolean | `true` |
  • `true` - Previews render in a frame
  • `false` - Previews render directly in your app
| + +### Example + +```yaml +editor: + preview: false + frame: false +``` + +**Note**: Setting this as a top level configuration will set the default for all collections + +## `summary` + +This setting allows the customization of the collection list view. Similar to the `slug` field, a string with templates can be used to include values of different fields, e.g. `{{title}}`. This option over-rides the default of `title` field and `identifier_field`. + +### Available Template Tags + +Template tags are the same as those for [slug](#slug), with the following additions: + +- `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`. +- `{{filename}}` The file name without the extension part. +- `{{extension}}` The file extension. +- `{{commit_date}}` The file commit date on supported backends (git based backends). +- `{{commit_author}}` The file author date on supported backends (git based backends). + +### Example + +```yaml +summary: 'Version: {{version}} - {{title}}' +``` + +## `sortable_fields` + +An optional list of sort fields to show in the UI. + +Defaults to inferring `title`, `date`, `author` and `description` fields and will also show `Update On` sort field in git based backends. + +When `author` field can't be inferred commit author will be used. + +### Example + +```yaml +# use dot notation for nested fields +sortable_fields: ['commit_date', 'title', 'commit_author', 'language.en'] +``` + +### `view_filters` + +An optional list of predefined view filters to show in the UI. + +Defaults to an empty list. + +### Example + +```yaml +view_filters: + - label: "Alice's and Bob's Posts" + field: author + pattern: 'Alice|Bob' + - label: 'Posts published in 2020' + field: date + pattern: '2020' + - label: Drafts + field: draft + pattern: true +``` + +## `view_groups` + +An optional list of predefined view groups to show in the UI. + +Defaults to an empty list. + +### Example + +```yaml +view_groups: + - label: Year + field: date + # groups items based on the value matched by the pattern + pattern: \d{4} + - label: Drafts + field: draft +``` diff --git a/website/content/docs/collection-types.mdx b/website/content/docs/collection-types.mdx index 63471d00..08e55b04 100644 --- a/website/content/docs/collection-types.mdx +++ b/website/content/docs/collection-types.mdx @@ -14,7 +14,7 @@ Folder collections represent one or more files with the same format, fields, and Unlike file collections, folder collections have the option to allow editors to create new items in the collection. This is set by the boolean `create` field. -**Note:** Folder collections must have at least one field with the name `title` for creating new entry slugs. That field should use the default `string` widget. The `label` for the field can be any string value. If you wish to use a different field as your identifier, set `identifier_field` to the field name. See the [Collections reference doc](/docs/configuration-options/#collections) for details on how collections and fields are configured. If you forget to add this field, you will get an error that your collection "must have a field that is a valid entry identifier". +**Note:** Folder collections must have at least one field with the name `title` for creating new entry slugs. That field should use the default `string` widget. The `label` for the field can be any string value. If you wish to use a different field as your identifier, set `identifier_field` to the field name. See the [Collections reference doc](/docs/collection-overview) for details on how collections and fields are configured. If you forget to add this field, you will get an error that your collection "must have a field that is a valid entry identifier". ### Examples @@ -115,7 +115,82 @@ collections: widget: markdown ``` -### Nested Collections (Beta) +### Folder Collections Path Beta Feature. Use at your own risk + +By default the CMS stores folder collection content under the folder specified in the collection setting. + +For example configuring `folder: posts` for a collection will save the content under `posts/post-title.md`. + +You can now specify an additional `path` template (similar to the `slug` template) to control the content destination. + +This allows saving content in subfolders, e.g. configuring `path: '{{year}}/{{slug}}'` will save the content under `posts/2019/post-title.md`. + +#### Media and Public Folder + +By default the CMS stores media files for all collections under a global `media_folder` directory as specified in the configuration. + +When using the global `media_folder` directory any entry field that points to a media file will use the absolute path to the published file as designated by the `public_folder` configuration. + +For example configuring: + +```yaml +media_folder: static/media +public_folder: /media +``` + +And saving an entry with an image named `image.png` will result in the image being saved under `static/media/image.png` and relevant entry fields populated with the value of `/media/image.png`. + +Some static site generators (e.g. Gatsby) work best when using relative image paths. + +This can now be achieved using a per collection `media_folder` configuration which specifies a relative media folder for the collection. + +For example, the following configuration will result in media files being saved in the same directory as the entry, and the image field being populated with the relative path to the image. + +```yaml +media_folder: static/media +public_folder: /media +collections: + - name: posts + label: Posts + label_singular: 'Post' + folder: content/posts + path: '{{slug}}/index' + media_folder: '' + public_folder: '' + fields: + - label: Title + name: title + widget: string + - label: 'Cover Image' + name: 'image' + widget: 'image' +``` + +More specifically, saving an entry with a title of `example post` with an image named `image.png` will result in a directory structure of: + +```bash +content + posts + example-post + index.md + image.png +``` + +And for the image field being populated with a value of `image.png`. + +**Note: When specifying a `path` on a folder collection, `media_folder` defaults to an empty string.** + +##### Available Template Tags + +Supports all of the [`slug` templates](/docs/configuration-options#slug) and: + +* `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`. +* `{{filename}}` The file name without the extension part. +* `{{extension}}` The file extension. +* `{{media_folder}}` The global `media_folder`. +* `{{public_folder}}` The global `public_folder`. + +### Nested Collections Beta Feature. Use at your own risk [Nested collections](/docs/beta-features/#nested-collections) is a beta feature that allows a folder collection to show a nested structure of entries and edit the locations of the entries. This feature is useful when you have a complex folder structure and may not want to create separate collections for every directory. As it is in beta, please use with discretion. @@ -140,7 +215,7 @@ collections: fields: - name: title label: Title - widget: string + widget: string - name: intro label: Intro widget: markdown diff --git a/website/content/docs/configuration-options.mdx b/website/content/docs/configuration-options.mdx index 67d3cc3e..dd7f3172 100644 --- a/website/content/docs/configuration-options.mdx +++ b/website/content/docs/configuration-options.mdx @@ -1,9 +1,8 @@ --- -group: Configuration +group: Intro title: Configuration Options -weight: 10 +weight: 180 --- -## Configuration File All configuration options for Static CMS are specified in a `config.yml` file, in the folder where you access the editor UI (usually in the `/admin` folder). @@ -14,7 +13,7 @@ Alternatively, you can specify a custom config file using a link tag: ``` -To see working configuration examples, you can [start from a template](/docs/start-with-a-template) or check out the [CMS demo site](https://cms-demo.netlify.com). (No login required: click the login button and the CMS will open.) You can refer to the demo [configuration code](https://github.com/StaticJsCMS/static-cms/blob/main/dev-test/config.yml) to see how each option was configured. +To see working configuration examples, you can [start from a template](/docs/start-with-a-template) or check out the [CMS demo site](https://static-cms-demo.netlify.app). (No login required: click the login button and the CMS will open.) You can refer to the demo [configuration code](https://github.com/StaticJsCMS/static-cms/blob/main/core/dev-test/config.yml) to see how each option was configured. You can find details about all configuration options below. Note that [YAML syntax](https://en.wikipedia.org/wiki/YAML#Basic_components) allows lists and objects to be written in block or inline style, and the code samples below include a mix of both. @@ -24,7 +23,7 @@ You can find details about all configuration options below. Note that [YAML synt The `backend` option specifies how to access the content for your site, including authentication. Full details and code samples can be found in [Backends](/docs/backends-overview). -**Note**: no matter where you access Static CMS — whether running locally, in a staging environment, or in your published site — it will always fetch and commit files in your hosted repository (for example, on GitHub), on the branch you configured in your Static CMS config.yml file. This means that content fetched in the admin UI will match the content in the repository, which may be different from your locally running site. It also means that content saved using the admin UI will save directly to the hosted repository, even if you're running the UI locally or in staging. If you want to have your local CMS write to a local repository, try the `local_backend` setting, [currently in beta](/docs/beta-features/#working-with-a-local-git-repository). +**Note**: no matter where you access Static CMS — whether running locally, in a staging environment, or in your published site — it will always fetch and commit files in your hosted repository (for example, on GitHub), on the branch you configured in your Static CMS config.yml file. This means that content fetched in the admin UI will match the content in the repository, which may be different from your locally running site. It also means that content saved using the admin UI will save directly to the hosted repository, even if you're running the UI locally or in staging. If you want to have your local CMS write to a local repository, [try the local_backend setting](/docs/local-backend). ## Media and Public Folders @@ -60,7 +59,7 @@ Media library integrations are configured via the `media_library` property, and ```yaml media_library: - title: uploadcare + name: uploadcare config: publicKey: demopublickey ``` @@ -99,9 +98,7 @@ logo_url: https://your-site.com/images/logo.svg ## Locale -The CMS locale. - -Defaults to `en`. +The CMS locale. Defaults to `en`. Other languages than English must be registered manually. @@ -127,8 +124,7 @@ When a translation for the selected locale is missing the English one will be us ## Search -The search functionally requires loading all collection(s) entries, which can exhaust rate limits on large repositories. -It can be disabled by setting the top level `search` property to `false`. +The search functionally requires loading all collection(s) entries, which can exhaust rate limits on large repositories. It can be disabled by setting the top level `search` property to `false`. Defaults to `true` @@ -167,217 +163,4 @@ slug: The `collections` setting is the heart of your Static CMS configuration, as it determines how content types and editor fields in the UI generate files and content in your repository. Each collection you configure displays in the left sidebar of the Content page of the editor UI, in the order they are entered into your Static CMS `config.yml` file. -`collections` accepts a list of collection objects, each with the following options: - -* `name` (required): unique identifier for the collection, used as the key when referenced in other contexts (like the [relation widget](/docs/widgets/#relation)) -* `identifier_field`: see detailed description below -* `label`: label for the collection in the editor UI; defaults to the value of `name` -* `label_singular`: singular label for certain elements in the editor; defaults to the value of `label` -* `description`: optional text, displayed below the label when viewing a collection -* `files` or `folder` (requires one of these): specifies the collection type and location; details in [Collection Types](/docs/collection-types) -* `filter`: optional filter for `folder` collections; details in [Collection Types](/docs/collection-types) -* `create`: for `folder` collections only; `true` allows users to create new items in the collection; defaults to `false` -* `hide`: `true` hides a collection in the CMS UI; defaults to `false`. Useful when using the relation widget to hide referenced collections. -* `delete`: `false` prevents users from deleting items in a collection; defaults to `true` -* `extension`: see detailed description below -* `format`: see detailed description below -* `frontmatter_delimiter`: see detailed description under `format` -* `slug`: see detailed description below -* `preview_path`: see detailed description below -* `preview_path_date_field`: see detailed description below -* `fields` (required): see detailed description below -* `editor`: see detailed description below -* `summary`: see detailed description below -* `sortable_fields`: see detailed description below -* `view_filters`: see detailed description below -* `view_groups`: see detailed description below - -The last few options require more detailed information. - -### `identifier_field` - -Static CMS expects every entry to provide a field named `"title"` that serves as an identifier for the entry. The identifier field serves as an entry's title when viewing a list of entries, and is used in [slug](#slug) creation. If you would like to use a field other than `"title"` as the identifier, you can set `identifier_field` to the name of the other field. - -**Example** - -```yaml -collections: - - title: posts - identifier_field: name -``` - -### `extension` and `format` - -These settings determine how collection files are parsed and saved. Both are optional—Static CMS will attempt to infer your settings based on existing items in the collection. If your collection is empty, or you'd like more control, you can set these fields explicitly. - -`extension` determines the file extension searched for when finding existing entries in a folder collection and it determines the file extension used to save new collection items. It accepts the following values: `yml`, `yaml`, `toml`, `json`, `md`, `markdown`, `html`. - -You may also specify a custom `extension` not included in the list above, as long as the collection files can be parsed and saved in one of the supported formats below. - -`format` determines how collection files are parsed and saved. It will be inferred if the `extension` field or existing collection file extensions match one of the supported extensions above. It accepts the following values: - -* `yml` or `yaml`: parses and saves files as YAML-formatted data files; saves with `yml` extension by default -* `toml`: parses and saves files as TOML-formatted data files; saves with `toml` extension by default -* `json`: parses and saves files as JSON-formatted data files; saves with `json` extension by default -* `frontmatter`: parses files and saves files with data frontmatter followed by an unparsed body text (edited using a `body` field); saves with `md` extension by default; default for collections that can't be inferred. Collections with `frontmatter` format (either inferred or explicitly set) can parse files with frontmatter in YAML, TOML, or JSON format. However, they will be saved with YAML frontmatter. -* `yaml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as YAML, followed by unparsed body text. The default delimiter for this option is `---`. -* `toml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as TOML, followed by unparsed body text. The default delimiter for this option is `+++`. -* `json-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved as JSON, followed by unparsed body text. The default delimiter for this option is `{` `}`. - -### `frontmatter_delimiter` - -If you have an explicit frontmatter format declared, this option allows you to specify a custom delimiter like `~~~`. If you need different beginning and ending delimiters, you can use an array like `["(", ")"]`. - -### `slug` - -For folder collections where users can create new items, the `slug` option specifies a template for generating new filenames based on a file's creation date and `title` field. (This means that all collections with `create: true` must have a `title` field (a different field can be used via [`identifier_field`](#identifier_field)). - -The slug template can also reference a field value by name, eg. `{{title}}`. If a field name conflicts with a built in template tag name - for example, if you have a field named `slug`, and would like to reference that field via `{{slug}}`, you can do so by adding the explicit `fields.` prefix, eg. `{{fields.slug}}`. - -**Available template tags:** - -* Any field can be referenced by wrapping the field name in double curly braces, eg. `{{author}}` -* `{{slug}}`: a url-safe version of the `title` field (or identifier field) for the file -* `{{year}}`: 4-digit year of the file creation date -* `{{month}}`: 2-digit month of the file creation date -* `{{day}}`: 2-digit day of the month of the file creation date -* `{{hour}}`: 2-digit hour of the file creation date -* `{{minute}}`: 2-digit minute of the file creation date -* `{{second}}`: 2-digit second of the file creation date - -**Example:** - -```yaml -slug: "{{year}}-{{month}}-{{day}}_{{slug}}" -``` - -**Example using field names:** - -```yaml -slug: "{{year}}-{{month}}-{{day}}_{{title}}_{{some_other_field}}" -``` - -**Example using field name that conflicts with a template tag:** - -```yaml -slug: "{{year}}-{{month}}-{{day}}_{{fields.slug}}" -``` - -### `fields` - -The `fields` option maps editor UI widgets to field-value pairs in the saved file. The order of the fields in your Static CMS `config.yml` file determines their order in the editor UI and in the saved file. - -`fields` accepts a list of collection objects, each with the following options: - -* `name` (required): unique identifier for the field, used as the key when referenced in other contexts (like the [relation widget](/docs/widgets/#relation)) -* `label`: label for the field in the editor UI; defaults to the value of `name` -* `widget`: defines editor UI and inputs and file field data types; details in [Widgets](/docs/widgets) -* `default`: specify a default value for a field; available for most widget types (see [Widgets](/docs/widgets) for details on each widget type). Please note that field default value only works for folder collection type. -* `required`: specify as `false` to make a field optional; defaults to `true` -* `pattern`: add field validation by specifying a list with a regex pattern and an error message; more extensive validation can be achieved with [custom widgets](/docs/custom-widgets/#advanced-field-validation) -* `comment`: optional comment to add before the field (only supported for `yaml`) - -In files with frontmatter, one field should be named `body`. This special field represents the section of the document (usually markdown) that comes after the frontmatter. - -**Example:** - -```yaml -fields: - - label: "Title" - title: "title" - widget: "string" - pattern: ['.{20,}', "Must have at least 20 characters"] - - {label: "Layout", title: "layout", widget: "hidden", default: "blog"} - - {label: "Featured Image", title: "thumbnail", widget: "image", required: false} - - {label: "Body", title: "body", widget: "markdown"} - comment: 'This is a multiline\ncomment' -``` - -### `editor` - -This setting changes options for the editor view of a collection or a file inside a files collection. It has one option so far: - -* `preview`: set to `false` to disable the preview pane for this collection or file; defaults to `true` - -**Example:** - -```yaml - editor: - preview: false -``` - -**Note**: Setting this as a top level configuration will set the default for all collections - -### `summary` - -This setting allows the customization of the collection list view. Similar to the `slug` field, a string with templates can be used to include values of different fields, e.g. `{{title}}`. This option over-rides the default of `title` field and `identifier_field`. - -**Available template tags:** - -Template tags are the same as those for [slug](#slug), with the following additions: - -* `{{dirname}}` The path to the file's parent directory, relative to the collection's `folder`. -* `{{filename}}` The file name without the extension part. -* `{{extension}}` The file extension. -* `{{commit_date}}` The file commit date on supported backends (git based backends). -* `{{commit_author}}` The file author date on supported backends (git based backends). - -**Example** - -```yaml - summary: "Version: {{version}} - {{title}}" -``` - -### `sortable_fields` - -An optional list of sort fields to show in the UI. - -Defaults to inferring `title`, `date`, `author` and `description` fields and will also show `Update On` sort field in git based backends. - -When `author` field can't be inferred commit author will be used. - -**Example** - -```yaml - # use dot notation for nested fields - sortable_fields: ['commit_date', 'title', 'commit_author', 'language.en'] -``` - -### `view_filters` - -An optional list of predefined view filters to show in the UI. - -Defaults to an empty list. - -**Example** - -```yaml - view_filters: - - label: "Alice's and Bob's Posts" - field: author - pattern: 'Alice|Bob' - - label: 'Posts published in 2020' - field: date - pattern: '2020' - - label: Drafts - field: draft - pattern: true -``` - -### `view_groups` - -An optional list of predefined view groups to show in the UI. - -Defaults to an empty list. - -**Example** - -```yaml - view_groups: - - label: Year - field: date - # groups items based on the value matched by the pattern - pattern: \d{4} - - label: Drafts - field: draft -``` +`collections` accepts a list of collection objects. See [Collections](/docs/collection-overview) for details. \ No newline at end of file diff --git a/website/content/docs/contributor-guide.mdx b/website/content/docs/contributor-guide.mdx index 017a54ab..9244e0fc 100644 --- a/website/content/docs/contributor-guide.mdx +++ b/website/content/docs/contributor-guide.mdx @@ -23,10 +23,10 @@ If you have a GitHub account, you can file an [issue](https://github.com/StaticJ When filing an issue, it is important to remember the [Code of Conduct](https://github.com/StaticJsCMS/static-cms/blob/main/CODE_OF_CONDUCT.md). ## Improve existing content -If you are able to offer up a change to existing content, we welcome this. Once you've forked the repo, and changed the content, you would file a pull request (PR). The repo [Contributing file](https://github.com/StaticJsCMS/static-cms/blob/main/CONTRIBUTING.md) lays out the correct format for PRs. +If you are able to offer up a change to existing content, it is welcome. Once you've forked the repo, and changed the content, you would file a pull request (PR). The repo [Contributing file](https://github.com/StaticJsCMS/static-cms/blob/main/CONTRIBUTING.md) lays out the correct format for PRs. ## Other places to get involved -While we work on building this page (and you can help!), here are some links with more information about getting involved: +Here are some links with more information about getting involved: * [Setup instructions and Contribution Guidelines](https://github.com/StaticJsCMS/static-cms/blob/main/CONTRIBUTING.md) * [Join our Community Chat](/chat) diff --git a/website/content/docs/custom-icons.mdx b/website/content/docs/custom-icons.mdx new file mode 100644 index 00000000..15e195c2 --- /dev/null +++ b/website/content/docs/custom-icons.mdx @@ -0,0 +1,27 @@ +--- +group: Customization +title: Adding Custom Icons +weight: 100 +--- + +The Static CMS exposes a `window.CMS` global object that you can use to register custom icons via `registerIcon`. The same object is also the default export if you import Static CMS as an npm module. + +Custom icons can be used with [Collections](/docs/collection-overview) or [Custom Links & Pages](/docs/additional-links) + +## Params + +| Param | Type | Description | +| ----- | ------------------------------------------------------------------------------ | -------------------------------------------------- | +| name | string | A unique name for the icon | +| name | [React Function Component](https://reactjs.org/docs/components-and-props.html) | A React functional component that renders the icon | + +## Example + +This example uses Font Awesome to supply the icon. + +```js +import { faHouse } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +cmsApp.registerIcon('house', ); +``` diff --git a/website/content/docs/custom-previews.mdx b/website/content/docs/custom-previews.mdx new file mode 100644 index 00000000..21b89576 --- /dev/null +++ b/website/content/docs/custom-previews.mdx @@ -0,0 +1,157 @@ +--- +group: Customization +title: Creating Custom Previews +weight: 50 +--- + +The Static CMS exposes a `window.CMS` global object that you can use to register custom previews for an entire collection (or file within a file collection) via `registerPreviewTemplate`. + +### React Components Inline + +The `registerPreviewTemplate` requires you to provide a React component. If you have a build process in place for your project, it is possible to integrate with this build process. + +However, although possible, it may be cumbersome or even impractical to add a React build phase. For this reason, Static CMS exposes some constructs globally to allow you to create components inline: `h` (alias for React.createElement) as well some basic hooks (`useState`, `useMemo`, `useEffect`, `useCallback`). + +**NOTE**: `createClass` is still provided, allowing for the creation of react class components. However it has now been deprecated and will be removed in `v2.0.0`. + +## Params + +| Param | Type | Description | +| --------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| name | string | The name of the collection (or file for file collections) which this preview component will be used for
  • Folder collections: Use the name of the collection
  • File collections: Use the name of the file
| +| react_component | [React Function Component](https://reactjs.org/docs/components-and-props.html) | A React functional component that renders the collection data. | + +The following parameters will be passed to your `react_component` during render: + +| Param | Type | Description | +| ---------- | -------------- | ---------------------------------------------------------------------------------------------------- | +| entry | object | Object with a `data` field that contains the current value of all widgets in the editor | +| document | Document | The document object the preview is within. If rendered with a frame, it will be the frame's document | +| window | Window | The window object the preview is within. If rendered with a frame, it will be the frame's window | +| getAsset | Async function | Function that given a url returns (as a promise) a loaded asset | +| widgetFor | Function | Given a field name, returns the rendered preview of that field's widget and value | +| widgetsFor | Function | Given a field name, returns the rendered previews of that field's nested child widgets and values | + +### Example + +```html + + +``` + +### Lists and Objects + +The API for accessing the individual fields of list- and object-type entries is similar to the API for accessing fields in standard entries, but there are a few key differences. Access to these nested fields is facilitated through the `widgetsFor` function, which is passed to the preview template component during render. + +**List Example:** + +```html + +``` + +**Object Example:** + +```html + +``` diff --git a/website/content/docs/custom-widgets.mdx b/website/content/docs/custom-widgets.mdx index d4e7c72d..ffa960ef 100644 --- a/website/content/docs/custom-widgets.mdx +++ b/website/content/docs/custom-widgets.mdx @@ -1,20 +1,20 @@ --- -group: Widgets +group: Customization title: Creating Custom Widgets -weight: 50 +weight: 40 --- -The Static CMS exposes a `window.CMS` a global object that you can use to register custom widgets, previews, and editor plugins. The same object is also the default export if you import Static CMS as an npm module. The available widget extension methods are: -* **registerWidget:** registers a custom widget. -* **registerEditorComponent:** adds a block component to the Markdown editor. +The Static CMS exposes a `window.CMS` global object that you can use to register custom widgets via `registerWidget`. The same object is also the default export if you import Static CMS as an npm module. -### Writing React Components inline +### React Components Inline -The `registerWidget` requires you to provide a React component. If you have a build process in place for your project, it is possible to integrate with this build process. +The `registerPreviewTemplate` requires you to provide a React component. If you have a build process in place for your project, it is possible to integrate with this build process. -However, although possible, it may be cumbersome or even impractical to add a React build phase. For this reason, Static CMS exposes two constructs globally to allow you to create components inline: `createClass` and `h` (alias for React.createElement). +However, although possible, it may be cumbersome or even impractical to add a React build phase. For this reason, Static CMS exposes some constructs globally to allow you to create components inline: `h` (alias for React.createElement) as well some basic hooks (`useState`, `useMemo`, `useEffect`, `useCallback`). -## `registerWidget` +**NOTE**: `createClass` is still provided, allowing for the creation of react class components. However it has now been deprecated and will be removed in `v2.0.0`. + +## Register Widget Register a custom widget. @@ -27,58 +27,139 @@ import CMS from '@staticcms/core'; CMS.registerWidget(name, control, [preview], [schema]); ``` -**Params:** +### Params -| Param | Type | Description | -| ----------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | `string` | Widget name, allows this widget to be used via the field `widget` property in config | -| `control` | `React.Component` or `string` |
  • React component that renders the control, receives the following props:
    • **value:** Current field value
    • **field:** Immutable map of current field configuration
    • **forID:** Unique identifier for the field
    • **classNameWrapper:** class name to apply CMS styling to the field
    • **onChange:** Callback function to update the field value
  • Name of a registered widget whose control should be used (includes built in widgets).
| -| [`preview`] | `React.Component`, optional | Renders the widget preview, receives the following props:
  • **value:** Current preview value
  • **field:** Immutable map of current field configuration
  • **getAsset:** Function for retrieving an asset url for image/file fields
  • **entry:** Immutable Record of all entry data
| -| [`schema`] | `JSON Schema object`, optional | Enforces a schema for the widget's field configuration | +| Param | Type | Description | +| ------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | Widget name, allows this widget to be used via the field `widget` property in config | +| control | [React Function Component](https://reactjs.org/docs/components-and-props.html)
\| string |
  • `React Function Component` - The react component that renders the control. See [Control Component](#control-component)
  • `string` - Name of a registered widget whose control should be used (includes built in widgets).
| +| preview | [React Function Component](https://reactjs.org/docs/components-and-props.html) | _Optional_. Renders the widget preview. See [Preview Component](#preview-component) | +| options | object | _Optional_. Widget options. See [Options](#options) | -**Example:** +### Control Component + +The react component that renders the control. It receives the following props: + +| Param | Type | Description | +| ------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | +| label | string | The label for the widget | +| value | An valid widget value | The current value of the widget | +| onChange | function | Function to be called when the value changes. Accepts a valid widget value | +| field | object | The field configuration for the current widget. See [Widget Options](/docs/widgets#common-widget-options) | +| collection | object | The collection configuration for the current widget. See [Collections](/docs/collection-overview) | +| config | object | The current Static CMS config. See [configuration options](/docs/configuration-options) | +| entry | object | Object with a `data` field that contains the current value of all widgets in the editor | +| path | string | `.` separated string donating the path to the current widget within the entry | +| hasErrors | boolean | Specifies if there are validation errors with the current widget | +| fieldsErrors | object | Key/value object of field names mapping to validation errors | +| isDisabled | boolean | Specifies if the widget control should be disabled | +| submitted | boolean | Specifies if a save attempt has been made in the editor session | +| forList | boolean | Specifices if the widget is within a `list` widget | +| isFieldDuplicate | function | Function that given a field configuration, returns if that field is a duplicate | +| isFieldHidden | function | Function that given a field configuration, returns if that field is hidden | +| getAsset | Async function | Function that given a url returns (as a promise) a loaded asset | +| locale | string
\| undefined | The current locale of the editor | +| mediaPaths | object | Key/value object of control IDs (passed to the media library) mapping to media paths | +| clearMediaControl | function | Clears a control ID's value from the internal store | +| openMediaLibrary | function | Opens the media library popup. See [Open Media Library](#open-media-library) | +| removeInsertedMedia | function | Removes draft media for a give control ID | +| removeMediaControl | function | Clears a control ID completely from the internal store | +| query | function | Runs a search on another collection. See [Query](#query) | +| i18n | object | The current i18n settings | +| t | function | Translates a given key to the current locale | + +#### Open Media Library + +`openMediaLibrary` allows you to open up the media library popup. It accepts the following props: + +| Param | Type | Default | Description | +| ------------- | --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ | +| controlID | string | | _Optional_ A unique identifier to which the uploaded media will be linked | +| forImage | boolean | `false` | _Optional_ If `true`, restricts upload to image files only | +| value | string
list of strings | | _Optional_ The current selected media value | +| allowMultiple | boolean | | _Optional_ Allow multiple files or images to be uploaded at once. Only used on media libraries that support multi upload | +| replaceIndex | number | | _Optional_ The index of the image in an list. Ignored if ` allowMultiple` is `false` | +| config | object | | _Optional_ Media library config options. Available options depend on the media library being used | +| field | object | | _Optional_ The current field configuration | + +#### Query + +`query` allows you to search the entries of a given collection. It accepts the following props: + +| Param | Type | Default | Description | +| -------------- | --------------- | ------- | -------------------------------------------------------------------------------------- | +| namespace | string | | Unique identifier for search | +| collectionName | string | | The collection to be searched | +| searchFields | list of strings | | The Fields to be searched within the target collection | +| searchTerm | string | | The term to search with | +| file | string | | _Optional_ The file in a file collection to search. Ignored on folder collections | +| limit | string | | _Optional_ The number of results to return. If not specified, all results are returned | + +### Preview Component + +The react component that renders the preview. It receives the following props: + +| Param | Type | Description | +| ---------- | --------------------- | --------------------------------------------------------------------------------------------------------- | +| value | An valid widget value | The current value of the widget | +| field | object | The field configuration for the current widget. See [Widget Options](/docs/widgets#common-widget-options) | +| collection | object | The collection configuration for the current widget. See [Collections](/docs/collection-overview) | +| config | object | The current Static CMS config. See [configuration options](/docs/configuration-options) | +| entry | object | Object with a `data` field that contains the current value of all widgets in the editor | +| getAsset | Async function | Function that given a url returns (as a promise) a loaded asset | + +### Options + +Register widget takes an optional object of options. These options include: + +| Param | Type | Description | +| ------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| validator | function | _Optional_. Validates the value of the widget | +| getValidValue | string | _Optional_. Given the current value, returns a valid value. See [Advanced field validation](#advanced-field-validation) | +| schema | JSON Schema object | _Optional_. Enforces a schema for the widget's field configuration | + +### Example `admin/index.html` ```html - + ``` @@ -86,478 +167,71 @@ CMS.registerWidget('categories', CategoriesControl, CategoriesPreview, schema); ```yml collections: - - title: posts + - name: posts label: Posts folder: content/posts fields: - - title: title + - name: title label: Title widget: string - - title: categories + - name: categories label: Categories widget: categories separator: __ ``` -## `registerEditorComponent` - -Register a block level component for the Markdown editor: - -```js -CMS.registerEditorComponent(definition) -``` - -**Params** - -* **definition:** The component definition; must specify: id, label, fields, patterns, fromBlock, toBlock, toPreview - -> Additional properties are optional and will be passed to the underlying widget control (object widget by default). For example, adding a `collapsed: true` property will collapse the widget by default. - -**Example:** - -```html - - -``` - -**Result:** - -![youtube-widget](/img/youtube-widget.webp) - ## Advanced field validation All widget fields, including those for built-in widgets, [include basic validation](/docs/widgets/#common-widget-options) capability using the `required` and `pattern` options. -With custom widgets, the widget control can also optionally implement an `isValid` method to perform custom validations, in addition to presence and pattern. The `isValid` method will be automatically called, and it can return either a boolean value, an object with an error message or a promise. Examples: +With custom widgets, the widget can also optionally pass in a `validator` method to perform custom validations, in addition to presence and pattern. The `validator` function will be automatically called, and it can return either a `boolean` value, an `object` with a type and error message or a promise. -**Boolean** -No errors: +### Examples + +#### No Errors ```javascript - isValid = () => { - // Do internal validation - return true; - }; -``` - -Existing error: - -```javascript - isValid = () => { - // Do internal validation - return false; - }; -``` - -**Object with `error` (useful for returning custom error messages)** -Existing error: - -```javascript - isValid = () => { - // Do internal validation - return { error: { message: 'Your error message.' } }; - }; -``` - -**Promise** -You can also return a promise from `isValid`. While the promise is pending, the widget will be marked as "in error". When the promise resolves, the error is automatically cleared. - -```javascript - isValid = () => { - return this.existingPromise; - }; -``` - -**Note:** Do not create a promise inside `isValid` - `isValid` is called right before trying to persist. This means that even if a previous promise was already resolved, when the user hits 'save', `isValid` will be called again. If it returns a new promise, it will be immediately marked as "in error" until the new promise resolves. - -## Writing custom widgets as a separate package - -Widgets are inputs for the Static CMS editor interface. It's a React component that receives user input and outputs a serialized value. Those are the only rules - the component can be extremely simple, like text input, or extremely complicated, like a full-blown markdown editor. They can make calls to external services, and generally do anything that JavaScript can do. - -For writing custom widgets as a separate package you should follow these steps: - -1. Create a directory - - ```javascript - mkdir my-custom-widget - ``` -2. Navigate to the directory - - ```javascript - cd my-custom-widget - ``` -3. For setting up a new npm package run this command: - - ```javascript - npm init - ``` -4. Answer the questions in the command line questionnaire. -5. In order to build React components, we need to set up a build step. We'll be using Webpack. Please run the following commands to install the required dependencies: - -```javascript - npm install --save-dev @staticcms/core babel-loader@7 babel-core babel-plugin-transform-class-properties babel-plugin-transform-export-extensions babel-plugin-transform-object-rest-spread babel-preset-env babel-preset-react cross-env css-loader html-webpack-plugin react source-map-loader style-loader webpack webpack-cli webpack-serve -``` - -```javascript - npm install --save prop-types -``` - -And you should manually add "**peerDependencies**" and "**scripts**" as shown below. - -Here is the content of `package.json` that you will have at the end: - -```javascript -{ - "name": "static-cms-widget-starter", - "description": "A boilerplate for creating Static CMS widgets.", - "author": "name of developer", - "keywords": [ - "simple", - "static-cms", - "cms", - "widget", - "starter", - "boilerplate" - ], - "version": "0.0.1", - "homepage": "https://github.com/StaticJsCMS/static-cms-widget-starter", - "license": "MIT", - "main": "dist/main.js", - "devDependencies": { - "@staticcms/core": "^0.1.0", - "babel-loader": "^7.1.4", - "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-export-extensions": "^6.22.0", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-preset-env": "^1.6.1", - "babel-preset-react": "^6.24.1", - "cross-env": "^5.1.4", - "css-loader": "^0.28.11", - "html-webpack-plugin": "^3.2.0", - "react": "^16.3.2", - "source-map-loader": "^0.2.3", - "style-loader": "^0.20.3", - "webpack": "^4.6.0", - "webpack-cli": "^2.0.14", - "webpack-serve": "^0.3.1" - }, - "dependencies": { - "prop-types": "^15.6.1" - }, - "peerDependencies": { - "react": "^16" - }, - "scripts": { - "start": "webpack-serve --static public --open" - } -} -``` - -5. Create a Webpack configuration file with this content: - - `webpack.config.js` - - ```javascript - const path = require('path') - const HtmlWebpackPlugin = require('html-webpack-plugin') - - const developmentConfig = { - mode: 'development', - entry: './dev/index.js', - output: { - path: path.resolve(__dirname, 'public'), - }, - optimization: { minimize: false }, - module: { - rules: [ - { - test: /\.js$/, - loader: 'source-map-loader', - enforce: 'pre', - }, - { - test: /\.jsx?$/, - exclude: /node_modules/, - loader: 'babel-loader', - }, - { - test: /\.css$/, - use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], - }, - ], - }, - plugins: [ - new HtmlWebpackPlugin(), - ], - devtool: 'eval-source-map', - } - - const productionConfig = { - mode: 'production', - module: { - rules: [ - { - test: /\.jsx?$/, - loader: 'babel-loader', - }, - ], - }, - devtool: 'source-map', - } - - module.exports = process.env.NODE_ENV === 'production' ? productionConfig : developmentConfig - ``` -6. The `.babelrc` file is our local configuration for our code in the project. You should create it under the root of the application repo. It will affect all files that Babel processes. So, create a `.babelrc` file under the main project with this content: - -```javascript -{ - "presets": [ - "react", - "env", - ], - "plugins": [ - "transform-export-extensions", - "transform-class-properties", - "transform-object-rest-spread", - ], -} -``` - -7. Create a `src` directory with the files `Control.js`, `Preview.js` and `index.js` - -`src/Control.js` - -```javascript - import PropTypes from 'prop-types'; - import React from 'react'; - - export default class Control extends React.Component { - static propTypes = { - onChange: PropTypes.func.isRequired, - forID: PropTypes.string, - value: PropTypes.node, - classNameWrapper: PropTypes.string.isRequired, - } - - static defaultProps = { - value: '', - } - - render() { - const { - forID, - value, - onChange, - classNameWrapper, - } = this.props; - - return ( - onChange(e.target.value)} - /> - ); - } - } -``` - -`src/Preview.js` - -```javascript -import PropTypes from 'prop-types'; -import React from 'react'; - -export default function Preview({ value }) { - return
{ value }
; -} - -Preview.propTypes = { - value: PropTypes.node, +const validator = () => { + // Do internal validation + return true; }; ``` -`src/index.js` +#### Has Error ```javascript -import Control from './Control' -import Preview from './Preview' - -if (typeof window !== 'undefined') { - window.Control = Control - window.Preview = Preview -} - -export { Control, Preview } +const validator = () => { + // Do internal validation + return false; +}; ``` -8. Now you need to set up the locale example site. - Under the main project, create a `dev` directory with the files `bootstrap.js` and `index.js` - -`bootstrap.js` +#### Error With Type ```javascript -window.CMS_MANUAL_INIT = true +const validator = () => { + // Do internal validation + return { type: 'custom-error' }; +}; ``` -`index.js` +#### Error With Type and Message + +_Useful for returning custom error messages_ ```javascript -import './bootstrap.js' -import CMS, { init } from '@staticcms/core' -import { Control, Preview } from '/docs/src' - -const config = { -backend: { - title: 'test-repo', - login: false, -}, -media_folder: 'assets', -collections: [{ - title: 'test', - label: 'Test', - files: [{ - file: 'test.yml', - title: 'test', - label: 'Test', - fields: [ - { title: 'test_widget', label: 'Test Widget', widget: 'test'}, - ], - }], -}], -} - -CMS.registerWidget('test', Control, Preview) - -init({ config }) +const validator = () => { + // Do internal validation + return { type: 'custom-error', message: 'Your error message.' }; +}; ``` -### [](https://github.com/StaticJsCMS/static-cms-widget-starter#development)Development +#### Promise -To run a copy of Static CMS with your widget for development, use the start script: +You can also return a promise from `validator`. The promise can return `boolean` value, an `object` with a type and error message or a promise. ```javascript -npm start -``` - -Your widget source is in the `src` directory, where there are separate files for the `Control` and `Preview` components. - -### [](https://github.com/StaticJsCMS/static-cms-widget-starter#production--publishing)Production & Publishing - -You'll want to take a few steps before publishing a production built package to npm: - -1. Customize `package.json` with details for your specific widget, e.g. name, description, author, version, etc. - - ```json - { - "name": "static-cms-widget-starter", - "description": "A boilerplate for creating Static CMS widgets.", - "author": "name of developer", - "keywords": [ - "simple", - "static-cms", - "cms", - "widget", - "starter", - "boilerplate" - ], - "version": "0.0.1", - // ... rest - } - ``` -2. For discoverability, ensure that your package name follows the pattern `static-cms-widget-`. -3. Delete this `README.md`, rename `README_TEMPLATE.md` to `README.md`, and update the new file for your specific widget. -4. Rename the exports in `src/index.js`. For example, if your widget is `static-cms-widget-awesome`, you would do: - -```javascript -if (typeof window !== 'undefined') { - window.AwesomeControl = Control - window.AwesomePreview = Preview -} - -export { Control as AwesomeControl, Preview as AwesomePreview } -``` - -5. Optional: customize the component and file names in `src`. -6. If you haven't already, push your repo to your GitHub account so the source available to other developers. -7. Create a production build, which will be output to `dist`: - -```javascript -npm run build -``` - -8. Finally, if you're sure things are tested and working, publish! - -```javascript -npm publish +const validator = () => { + return this.existingPromise; +}; ``` diff --git a/website/content/docs/customization-overview.mdx b/website/content/docs/customization-overview.mdx new file mode 100644 index 00000000..06ff6574 --- /dev/null +++ b/website/content/docs/customization-overview.mdx @@ -0,0 +1,13 @@ +--- +group: Customization +title: Overview +weight: 1 +--- + +The Static CMS exposes a `window.CMS` global object that you can use to customize your CMS. The same object is also the default export if you import Static CMS as an npm module. Available options are: + +- Register [custom widgets](/docs/custom-widgets) +- Register [custom previews](/docs/custom-previews) +- Register [editor customizations](/docs/widget-markdown#customization) +- Register [additional menu links or custom pages](/docs/additional-links) +- Register [custom icons](/docs/custom-icons) diff --git a/website/content/docs/customization.mdx b/website/content/docs/customization.mdx deleted file mode 100644 index 5c0b50c5..00000000 --- a/website/content/docs/customization.mdx +++ /dev/null @@ -1,147 +0,0 @@ ---- -group: Customization -title: Creating Custom Previews -weight: 50 ---- - -The Static CMS exposes a `window.CMS` global object that you can use to register custom widgets, previews and editor plugins. The available customization methods are: - -- **registerPreviewTemplate:** Registers a template for a collection. - -### React Components inline interaction - -Static CMS is a collection of React components and exposes two constructs globally to allow you to create components inline: `createClass` and `h` (alias for React.createElement). - -## `registerPreviewTemplate` - -Registers a template for a folder collection or an individual file in a file collection. - -`CMS.registerPreviewTemplate(name, react_component);` - -**Params:** - -- title: The name of the collection (or file for file collections) which this preview component will be used for. - - Folder collections: Use the name of the collection - - File collections: Use the name of the file -- react_component: A React component that renders the collection data. Six props will be passed to your component during render: - - - entry: Immutable collection containing the entry data. - - widgetFor: Returns the appropriate widget preview component for a given field. - - [widgetsFor](#lists-and-objects): Returns an array of objects with widgets and associated field data. For use with list and object type entries. - - getAsset: Returns the correct filePath or in-memory preview for uploaded images. - **Example:** - - ```html - - - ``` - - - document: The preview pane iframe's [document instance](https://github.com/ryanseddon/react-frame-component/tree/9f8f06e1d3fc40da7122f0a57c62f7dec306e6cb#accessing-the-iframes-window-and-document). - - window: The preview pane iframe's [window instance](https://github.com/ryanseddon/react-frame-component/tree/9f8f06e1d3fc40da7122f0a57c62f7dec306e6cb#accessing-the-iframes-window-and-document). - - ### Lists and Objects - - The API for accessing the individual fields of list- and object-type entries is similar to the API for accessing fields in standard entries, but there are a few key differences. Access to these nested fields is facilitated through the `widgetsFor` function, which is passed to the preview template component during render. - **Note**: as is often the case with the Static CMS API, arrays and objects are created with Immutable.js. If some of the methods that we use are unfamiliar, such as `getIn`, check out [their docs](https://facebook.github.io/immutable-js/docs/#/) to get a better understanding. - **List Example:** - - ```html - - ``` - - **Object Example:** - - ```html - - ``` diff --git a/website/content/docs/docusaurus.mdx b/website/content/docs/docusaurus.mdx index 1c82d78d..a0791ad7 100644 --- a/website/content/docs/docusaurus.mdx +++ b/website/content/docs/docusaurus.mdx @@ -95,7 +95,7 @@ Your website is now deployed. Netlify provides you with a randomly generated dom - foo - bar authors: - - title: Garrison McMullen + - name: Garrison McMullen title: Instruction Writer url: https://github.com/garrison0 image_url: https://avatars.githubusercontent.com/u/4089393?v=4 @@ -127,7 +127,7 @@ Your website is now deployed. Netlify provides you with a randomly generated dom - + ``` @@ -135,7 +135,7 @@ Your website is now deployed. Netlify provides you with a randomly generated dom 4. Edit `config.yml` to look like this: ```yaml backend: - title: github + name: github branch: main repo: /my-website @@ -144,7 +144,7 @@ Your website is now deployed. Netlify provides you with a randomly generated dom public_folder: "/img/" # The src attribute for uploaded media will begin with /images/uploads collections: - - title: blog + - name: blog label: "blog" folder: blog identifier_field: title @@ -153,20 +153,20 @@ Your website is now deployed. Netlify provides you with a randomly generated dom create: true slug: "{{year}}-{{month}}-{{day}}-{{slug}}" # Filename template, e.g., YYYY-MM-DD-title.md fields: - - { title: title, label: Title, widget: string } - - { title: body, label: Body, widget: markdown } - - { title: slug, label: Slug, widget: string } + - { name: title, label: Title, widget: string } + - { name: body, label: Body, widget: markdown } + - { name: slug, label: Slug, widget: string } - label: "Tags" - title: "tags" + name: "tags" widget: "list" - label: "Authors" - title: "authors" + name: "authors" widget: "list" fields: - - { title: name, label: Name, widget: string } - - { title: title, label: Title, widget: string } - - { title: url, label: URL, widget: string } - - { title: imageUrl, label: ImageURL, widget: string } + - { name: name, label: Name, widget: string } + - { name: title, label: Title, widget: string } + - { name: url, label: URL, widget: string } + - { name: imageUrl, label: ImageURL, widget: string } ``` `config.yml` specifies what kind of content your blog posts have. The content specification enables Static CMS to edit existing posts and create new ones with the same format. To learn more, read about Static CMS' [](/docs/configuration-options/)[Configuration options](/docs/configuration-options/). diff --git a/website/content/docs/examples.mdx b/website/content/docs/examples.mdx index cc5ab191..a8b2c02c 100644 --- a/website/content/docs/examples.mdx +++ b/website/content/docs/examples.mdx @@ -6,5 +6,8 @@ weight: 110 Do you have a great, open source example? Submit a pull request to this page! -Example | Tools | Type | Source | More info | ---- | --- | --- | --- | --- +
+ +| Example | Tools | Type | Source | More info | +| ---------------------------------------------------------------- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| [St Joseph Catholic Church](https://stjosephchurchbluffton.org/) | Next, React | Website | [saint-joseph-catholic-church-site](https://github.com/SaintJosephCatholicChurch/saint-joseph-catholic-church-site) | [README](https://github.com/SaintJosephCatholicChurch/saint-joseph-catholic-church-site/blob/main/README.md) | diff --git a/website/content/docs/external-oauth-clients.mdx b/website/content/docs/external-oauth-clients.mdx deleted file mode 100644 index 9f8b8648..00000000 --- a/website/content/docs/external-oauth-clients.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -group: Accounts -title: External OAuth Clients -weight: 60 ---- -If you would like to facilitate your own OAuth authentication rather than use Netlify's service or a client side flow like implicit or PKCE, you can use one of the community-maintained projects below. Feel free to hit the "Edit this page" button if you'd like to add yours! - -| Author | Supported Git hosts | Language(s)/Platform(s) | Link | -| ------------------------------------------------------------ | --------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [@vencax](https://github.com/vencax) | GitHub, GitHub Enterprise | Node.js | [Repo](https://github.com/vencax/netlify-cms-github-oauth-provider) | -| [@igk1972](https://github.com/igk1972) | GitHub, GitHub Enterprise | Go | [Repo](https://github.com/igk1972/netlify-cms-oauth-provider-go) | -| [@davidejones](https://github.com/davidejones) | GitHub, GitHub Enterprise | Python | [Repo](https://github.com/davidejones/netlify-cms-oauth-provider-python) | -| [@marcelkornblum](https://github.com/marcelkornblum) | GitHub, GitHub Enterprise | Google AppEngine with Python | [Repo](https://github.com/signal-noise/netlify-cms-oauth-provider-python-appengine) | -| [@marksteele](https://github.com/marksteele) | GitHub, GitHub Enterprise | Serverless | [Repo](https://github.com/marksteele/netlify-serverless-oauth2-backend), [Blog](https://www.control-alt-del.org/blog/serverless-blog-howto/) | -| [@Herohtar](https://github.com/Herohtar) | GitHub, GitHub Enterprise | Firebase Cloud Function | [Repo](https://github.com/Herohtar/netlify-cms-oauth-firebase) | -| [@abcalderon3](https://github.com/abcalderon3) | GitHub, GitHub Enterprise | Google Cloud Function with Python | [Repo](https://github.com/abcalderon3/netlify-cms-oauth-client-cloud-function) | -| [@TSV-Zorneding-1920](https://github.com/TSV-Zorneding-1920) | GitHub, GitHub Enterprise | PHP | [Repo](https://github.com/TSV-Zorneding-1920/netlify-cms-oauth-provider-php) | -| [@bericp1](https://github.com/bericp1) | GitHub, GitHub Enterprise | Node.js, Vercel Serverless | [Repo](https://github.com/bericp1/netlify-cms-oauth-provider-node) | -| [@mcdeck](https://github.com/mcdeck) | GitHub, GitHub Enterprise, GitLab | PHP | [Repo](https://github.com/mcdeck/netlify-cms-oauth-provider-php), [Blog](https://www.van-porten.de/blog/2021/01/netlify-auth-provider/) | -| [@deepbass](https://github.com/deepbass) | GitHub, GitHub Enterprise | Node.js Azure Functions | [Repo](https://github.com/deepbass/serverless-cms-azure), [Blog](https://www.danielbass.dev/building-a-serverless-cms-on-azure-with-netlify-cms-and-gatsby/) | -| [@adrian-ub](https://github.com/adrian-ub) | GitHub, GitLab | TypeScript | [Repo](https://github.com/ublabs/netlify-cms-oauth) | -| [@hatappo](https://github.com/hatappo) | GitHub | ClojureScript, Firebase Functions | [Repo](https://github.com/hatappo/netlifycms-oauth-server) | - -Check each project's documentation for instructions on installation and usage. diff --git a/website/content/docs/gatsby.mdx b/website/content/docs/gatsby.mdx index 2f252134..eed57cc4 100644 --- a/website/content/docs/gatsby.mdx +++ b/website/content/docs/gatsby.mdx @@ -60,14 +60,14 @@ In your `config.yml` file paste the following configuration: ```yml backend: - title: git-gateway + name: git-gateway branch: main # Branch to update (optional; defaults to main) media_folder: static/img public_folder: /img collections: - - title: 'blog' + - name: 'blog' label: 'Blog' folder: 'content/blog' create: true @@ -78,10 +78,10 @@ collections: editor: preview: false fields: - - { label: 'Title', title: 'title', widget: 'string' } - - { label: 'Publish Date', title: 'date', widget: 'datetime' } - - { label: 'Description', title: 'description', widget: 'string' } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { label: 'Description', name: 'description', widget: 'string' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` **Note:** The above configuration allows assets to be stored relative to their content. Therefore posts would be stored in the format below as it is in `gatsby-starter-blog`. diff --git a/website/content/docs/git-gateway-backend.mdx b/website/content/docs/git-gateway-backend.mdx index f5ef8c18..48aa67ba 100644 --- a/website/content/docs/git-gateway-backend.mdx +++ b/website/content/docs/git-gateway-backend.mdx @@ -1,10 +1,12 @@ --- -group: Accounts +group: Backends title: Git Gateway weight: 10 --- -[Git Gateway](https://github.com/netlify/git-gateway) is a Netlify open source project that allows you to add editors to your site CMS without giving them direct write access to your GitHub or GitLab repository. (For Bitbucket repositories, use the [Bitbucket backend](/docs/bitbucket-backend/) instead.) +- **Name**: `git-gateway` + +[Git Gateway](https://github.com/netlify/git-gateway) is a Netlify open source project that allows you to add content editors to your site CMS without giving them direct write access to your GitHub or GitLab repository. (For Bitbucket repositories, use the [Bitbucket backend](/docs/bitbucket-backend/) instead.) ## Git Gateway with Netlify @@ -17,7 +19,7 @@ To use it in your own project stored on GitHub or GitLab, follow these steps: ```yaml backend: - title: git-gateway + name: git-gateway ``` ## Reconnect after Changing Repository Permissions diff --git a/website/content/docs/github-backend.mdx b/website/content/docs/github-backend.mdx index 2daee030..c1f9e573 100644 --- a/website/content/docs/github-backend.mdx +++ b/website/content/docs/github-backend.mdx @@ -1,8 +1,11 @@ --- title: GitHub -group: Accounts -weight: 30 +group: Backends +weight: 20 --- + +- **Name**: `github` + For repositories stored on GitHub, the `github` backend allows CMS users to log in directly with their GitHub account. Note that all users must have push access to your content repository for this to work. ## Authentication @@ -16,7 +19,7 @@ To enable basic GitHub authentication: ```yaml backend: - title: github + name: github repo: owner-name/repo-name # Path to your GitHub repository # optional, defaults to main # branch: main @@ -24,4 +27,4 @@ backend: ## Git Large File Storage (LFS) -Please note that the GitHub backend **does not** support [git-lfs](https://git-lfs.github.com/), see [this issue](https://github.com/StaticJsCMS/static-cms/issues/1206) for more information. +Please note that the GitHub backend **does not** support [git-lfs](https://git-lfs.github.com/). diff --git a/website/content/docs/gitlab-backend.mdx b/website/content/docs/gitlab-backend.mdx index 28e451ae..ef74c28d 100644 --- a/website/content/docs/gitlab-backend.mdx +++ b/website/content/docs/gitlab-backend.mdx @@ -1,8 +1,11 @@ --- -group: Accounts +group: Backends title: GitLab weight: 40 --- + +- **Name**: `gitlab` + For repositories stored on GitLab, the `gitlab` backend allows CMS users to log in directly with their GitLab account. Note that all users must have push access to your content repository for this to work. **Note:** GitLab default branch is protected by default, thus typically requires `maintainer` permissions in order for users to have push access. @@ -14,23 +17,40 @@ With GitLab's PKCE authorization, users can authenticate with GitLab directly fr 1. Follow the [GitLab docs](https://docs.gitlab.com/ee/integration/oauth_provider.html#adding-an-application-through-the-profile) to add your Static CMS instance as an OAuth application and uncheck the **Confidential** checkbox. For the **Redirect URI**, enter the address where you access Static CMS, for example, `https://www.mysite.com/admin/`. For scope, select `api`. 2. GitLab gives you an **Application ID**. Copy this ID and enter it in your Static CMS `config.yml` file, along with the following settings: - ```yaml - backend: - title: gitlab - repo: owner-name/repo-name # Path to your GitLab repository - auth_type: pkce # Required for pkce - app_id: your-app-id # Application ID from your GitLab settings - ``` +| Name | Type | Default | Description | +| --------- | ------ | ------- | ---------------------------------------- | +| auth_type | 'pkce' | | The authorization method | +| app_id | string | | Application ID from your GitLab settings | - You can also use PKCE Authorization with a self-hosted GitLab instance. This requires adding `api_root`, `base_url`, and `auth_endpoint` fields: +### Example - ```yaml - backend: - title: gitlab - repo: owner-name/repo-name # Path to your GitLab repository - auth_type: pkce # Required for pkce - app_id: your-app-id # Application ID from your GitLab settings - api_root: https://my-hosted-gitlab-instance.com/api/v4 - base_url: https://my-hosted-gitlab-instance.com - auth_endpoint: oauth/authorize - ``` +```yaml +backend: + name: gitlab + repo: owner-name/repo-name # Path to your GitLab repository + auth_type: pkce # Required for pkce + app_id: your-app-id # Application ID from your GitLab settings +``` + +### Self-Hosted GitLab Instance + +You can also use PKCE Authorization with a self-hosted GitLab instance. This requires adding `api_root`, `base_url`, and `auth_endpoint` fields: + +| Name | Type | Default | Description | +| ------------- | ------ | ------- | ------------------------------------- | +| api_root | string | | Root API url for self-hosted instance | +| base_url | string | | Root url for self-hosted instance | +| auth_endpoint | string | | Auth endpoint on self-hosted instance | + +#### Example + +```yaml +backend: + name: gitlab + repo: owner-name/repo-name # Path to your GitLab repository + auth_type: pkce # Required for pkce + app_id: your-app-id # Application ID from your GitLab settings + api_root: https://my-hosted-gitlab-instance.com/api/v4 + base_url: https://my-hosted-gitlab-instance.com + auth_endpoint: oauth/authorize +``` diff --git a/website/content/docs/gridsome.mdx b/website/content/docs/gridsome.mdx index 08fb27c7..262f7285 100644 --- a/website/content/docs/gridsome.mdx +++ b/website/content/docs/gridsome.mdx @@ -105,23 +105,23 @@ Your `config.yml` for GitHub should look like this: ```yml backend: - title: git-gateway + name: git-gateway branch: main # Branch to update (optional; defaults to main) media_folder: "static/uploads" public_folder: "/uploads" collections: - - title: "posts" + - name: "posts" label: "Posts" folder: "posts" create: true slug: "{{slug}}" fields: - - {label: "Title", title: "title", widget: "string"} - - {label: "Excerpt", title: "excerpt", widget: "string"} - - {label: "Publish Date", title: "date", widget: "datetime"} - - {label: "Body", title: "body", widget: "markdown"} + - {label: "Title", name: "title", widget: "string"} + - {label: "Excerpt", name: "excerpt", widget: "string"} + - {label: "Publish Date", name: "date", widget: "datetime"} + - {label: "Body", name: "body", widget: "markdown"} ``` ## Push to GitHub diff --git a/website/content/docs/hugo.mdx b/website/content/docs/hugo.mdx index f02b1bdf..5c13fccd 100644 --- a/website/content/docs/hugo.mdx +++ b/website/content/docs/hugo.mdx @@ -76,7 +76,7 @@ In Hugo, static files that don't need to be processed by the build commands live - + ``` @@ -85,12 +85,12 @@ In the `config.yml` file, you can add this basic configuration — you can custo ```yaml backend: - title: git-gateway + name: git-gateway branch: main # Branch to update (optional; defaults to main) media_folder: static/img public_folder: /img collections: - - title: 'blog' + - name: 'blog' label: 'Blog' folder: 'content/blog' create: true @@ -98,10 +98,10 @@ collections: editor: preview: false fields: - - { label: 'Title', title: 'title', widget: 'string' } - - { label: 'Publish Date', title: 'date', widget: 'datetime' } - - { label: 'Description', title: 'description', widget: 'string' } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { label: 'Description', name: 'description', widget: 'string' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` **Note:** You won't be able to access the CMS just yet — you still need to deploy the project with **Netlify** and authenticate with **Netlify Identity**. You'll handle this in the next few steps of this guide. diff --git a/website/content/docs/intro.mdx b/website/content/docs/intro.mdx index bb5d7e50..2f9f5b1a 100644 --- a/website/content/docs/intro.mdx +++ b/website/content/docs/intro.mdx @@ -4,20 +4,20 @@ title: Overview weight: 1 --- -Static CMS is an open source content management system for your Git workflow that enables you to provide editors with a friendly UI and intuitive workflows. You can use it with any static site generator to create faster, more flexible web projects. Content is stored in your Git repository alongside your code for easier versioning, multi-channel publishing, and the option to handle content updates directly in Git. +Static CMS is an open source content management system for your Git workflow that enables you to provide editors with a friendly UI. You can use it with any static site generator to create faster, more flexible web projects. Content is stored in your Git repository alongside your code for easier versioning and the option to handle content updates directly in Git. -At its core, Static CMS is an open-source React app that acts as a wrapper for the Git workflow, using the GitHub, GitLab, or Bitbucket API. This provides many advantages, including: +At its core, Static CMS is an open-source React app that acts as a wrapper for the Git workflow, using the GitHub, GitLab, or Bitbucket APIs. This provides many advantages, including: - **Fast, web-based UI:** With rich-text editing, real-time preview, and drag-and-drop media uploads. - **Platform agnostic:** Works with most static site generators. -- **Easy installation:** Add two files to your site and hook up the backend by including those files in your build process or linking to our Content Delivery Network (CDN). +- **Easy installation:** Add two files to your site, hook up the backend by including those files in your build process or linking to our Content Delivery Network (CDN) and call `CMS.init();`. - **Modern authentication:** Using GitHub, GitLab, or Bitbucket and JSON web tokens. - **Flexible content types:** Specify an unlimited number of content types with custom fields. -- **Fully extensible:** Create custom-styled previews, UI widgets, and editor plugins. +- **Fully extensible:** Create custom-styled previews, UI widgets, editor plugins, pages, etc. ## Find out more -- Get a feel for the UI in the [demo site](https://cms-demo.netlify.com). (No login required. Click the login button to go straight to the CMS editor UI.) +- Get a feel for the UI in the [demo site](https://static-cms-demo.netlify.app). (No login required. Click the login button to go straight to the CMS editor UI.) - [Start with a template](/docs/start-with-a-template/) to make a Static CMS-enabled site of your own. - Configure your existing site by following a [tutorial](/docs/add-to-your-site/) or checking [configuration options](/docs/configuration-options). - Ask questions and share ideas in the Static CMS [community chat](/chat). diff --git a/website/content/docs/jekyll.mdx b/website/content/docs/jekyll.mdx index 4316fcf0..2dd0fef9 100644 --- a/website/content/docs/jekyll.mdx +++ b/website/content/docs/jekyll.mdx @@ -36,7 +36,7 @@ Create a file `admin/index.html` in the root of your repo - it should look like - + ``` @@ -49,15 +49,15 @@ Create a file `admin/config.yml` in the root of your repo - it should look like # config.yml backend: - title: git-gateway + name: git-gateway branch: main # Branch to update (optional; defaults to main) media_folder: 'assets/uploads' collections: - - title: 'blog' + - name: 'blog' label: 'Blog' folder: '_posts/' fields: - - { title: Title } + - { name: Title } ``` ### Enable authentication for CMS users @@ -74,7 +74,7 @@ We'll start by updating the `blog` collection. Blogging is baked into Jekyll, an ```yaml collections: - - title: 'blog' + - name: 'blog' label: 'Blog' folder: '_posts/' create: true @@ -82,10 +82,10 @@ collections: editor: preview: false fields: - - { label: 'Layout', title: 'layout', widget: 'hidden', default: 'post' } - - { label: 'Title', title: 'title', widget: 'string' } - - { label: 'Publish Date', title: 'date', widget: 'datetime' } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'post' } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` A few things to note. @@ -176,18 +176,18 @@ then update `_layouts/author.html`, `_layouts/post.html` and `staff.html` accord Next, copy and paste the following into the collections array in `config.yml` below the `blog` collection. ```yaml -- title: 'authors' +- name: 'authors' label: 'Authors' folder: '_authors/' create: true editor: preview: false fields: - - { label: 'Layout', title: 'layout', widget: 'hidden', default: 'author' } - - { label: 'Short Name', title: 'name', widget: 'string' } - - { label: 'Display Name', title: 'display_name', widget: 'string' } - - { label: 'Position', title: 'position', widget: 'string' } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'author' } + - { label: 'Short Name', name: 'name', widget: 'string' } + - { label: 'Display Name', name: 'display_name', widget: 'string' } + - { label: 'Position', name: 'position', widget: 'string' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` Now that we have the `authors` collection configured, we can add an `author` field to the `blog` collection. We'll use the [relation widget](/docs/widgets/#relation) to define the relationship between blog posts and authors. @@ -195,19 +195,19 @@ Now that we have the `authors` collection configured, we can add an `author` fie ```yaml # updated fields in blog collection configuration fields: - - { label: 'Layout', title: 'layout', widget: 'hidden', default: 'post' } - - { label: 'Title', title: 'title', widget: 'string' } - - { label: 'Publish Date', title: 'date', widget: 'datetime' } + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'post' } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } - { label: 'Author', - title: 'author', + name: 'author', widget: 'relation', collection: 'authors', display_fields: [display_name], search_fields: [display_name], value_field: 'name', } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` With that configuration added, you should be able to select the author for a post from a dropdown. @@ -219,18 +219,18 @@ Our Jekyll blog includes an About page. It would nice to be able to edit that pa Copy and paste the following into the collections array in `config.yml` ```yaml -- title: 'pages' +- name: 'pages' label: 'Pages' editor: preview: false files: - label: 'About Page' - title: 'about' + name: 'about' file: 'about.md' fields: - - { label: 'Title', title: 'title', widget: 'hidden', default: 'about' } - - { label: 'Layout', title: 'layout', widget: 'hidden', default: 'about' } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Title', name: 'title', widget: 'hidden', default: 'about' } + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'about' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` ### Navigation @@ -279,21 +279,21 @@ You'll need to update `_includes/navigation.html` accordingly. `{% for item in s Finally, add the following to the collections array in `config.yml` ```yaml -- title: 'config' +- name: 'config' label: 'Config' editor: preview: false files: - label: 'Navigation' - title: 'navigation' + name: 'navigation' file: '_data/navigation.yml' fields: - label: 'Navigation Items' - title: 'items' + name: 'items' widget: 'list' fields: - - { label: Name, title: name, widget: string } - - { label: Link, title: link, widget: string } + - { label: Name, name: name, widget: string } + - { label: Link, name: link, widget: string } ``` Now you can add, rename, and rearrange the navigation items on your blog. diff --git a/website/content/docs/local-backend.mdx b/website/content/docs/local-backend.mdx new file mode 100644 index 00000000..caa8f249 --- /dev/null +++ b/website/content/docs/local-backend.mdx @@ -0,0 +1,58 @@ +--- +group: Backends +title: Local Backend +weight: 50 +--- + +The local backend allows you to use Static CMS with a local git repository, instead of working with a live repo, regardless of backend provider. It will read and write file from your local file system inside your local git repository. You will still need to manually commit and push any files you have changed or added after completing the edits. + +## Configuration + +| Name | Type | Default | Description | +| ------------- | --------------------------------------------- | ------- | ----------------------------------------------------------------------------------- | +| local_backend | boolean
\| [Proxy Config](#configure-proxy-server-port) | `false` | Activates the local backend for Static CMS, overriding other backend configurations | + +### Example + +```yaml +backend: + name: git-gateway + +# when using the default proxy server port +local_backend: true +``` + +## Usage + +1. Run `npx @staticcms/proxy-server` from the root directory of the above repository. + - If the default port (8081) is in use, the proxy server won't start and you will see an error message. In this case, follow [these steps](#configure-the-@staticcms/proxy-server-port-number) before proceeding. +2. Start your local development server (e.g. run `gatsby develop`). +3. Open `http://localhost:/admin` to verify that your can administer your content locally. Replace `` with the port of your local development server. For example Gatsby's default port is `8000` + +**Note:** `@staticcms/proxy-server` runs an unauthenticated express server. As any client can send requests to the server, it should only be used for local development. + +### Configure Proxy Server Port + +| Name | Type | Default | Description | +| ------------- | ------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------- | +| url | string | `http://localhost:8080/api/v1` | URL for proxy server | +| allowed_hosts | list of hosts | `['localhost', '127.0.0.1']` | Whitelist of allowed hosts when accessing the local site from a host other than 'localhost' or '127.0.0.1' | + +1. Create a `.env` file in the project's root folder and define the PORT you'd like the proxy server to use. + +```ini +PORT=8082 +``` + +2. Update the `local_backend` object in `config.yml` and specify a `url` property to use your custom port number + +```yaml +backend: + name: git-gateway + +local_backend: + # when using a custom proxy server port + url: http://localhost:8082/api/v1 + # when accessing the local site from a host other than 'localhost' or '127.0.0.1' + allowed_hosts: ['192.168.0.1'] +``` diff --git a/website/content/docs/middleman.mdx b/website/content/docs/middleman.mdx index 3e7aafaf..9c88ca55 100644 --- a/website/content/docs/middleman.mdx +++ b/website/content/docs/middleman.mdx @@ -115,7 +115,7 @@ In the newly created `index.html` we add scripts for Static CMS and the Netlify - + @@ -134,7 +134,7 @@ media_folder: source/images/uploads public_folder: /images/uploads collections: - - title: blog + - name: blog label: Blog folder: source/posts/ extension: .html.md @@ -142,9 +142,9 @@ collections: create: true slug: '{{year}}-{{month}}-{{day}}-{{title}}' fields: - - {label: Title, title: title, widget: string} - - {label: Publish Date, title: date, widget: datetime} - - {label: Body, title: body, widget: markdown} + - {label: Title, name: title, widget: string} + - {label: Publish Date, name: date, widget: datetime} + - {label: Body, name: body, widget: markdown} ``` ### Push to GitHub diff --git a/website/content/docs/netlify-cms-migration-guide.mdx b/website/content/docs/netlify-cms-migration-guide.mdx new file mode 100644 index 00000000..5bbb51c9 --- /dev/null +++ b/website/content/docs/netlify-cms-migration-guide.mdx @@ -0,0 +1,7 @@ +--- +group: Intro +title: Netlify CMS Migration Guide +weight: 190 +--- + +Guide coming soon on how to migrate your site from Netlify CMS to Static CMS! diff --git a/website/content/docs/netlify-large-media.mdx b/website/content/docs/netlify-large-media.mdx index 47c6d77e..57a1cb18 100644 --- a/website/content/docs/netlify-large-media.mdx +++ b/website/content/docs/netlify-large-media.mdx @@ -4,16 +4,17 @@ title: Netlify Large Media weight: 20 --- +## Beta Feature. Use at your own risk + [Netlify Large Media](https://www.netlify.com/features/large-media/) is a [Git LFS](https://git-lfs.github.com/) implementation for repositories connected to Netlify sites. This means that you can use Git to work with large asset files like images, audio, and video, without bloating your repository. It does this by replacing the asset files in your repository with text pointer files, then uploading the assets to the Netlify Large Media storage service. -If you have a Netlify site with Large Media enabled, Static CMS (version 2.6.0 and above) will handle Large Media asset files seamlessly, in the same way as files stored directly in the repository. +If you have a Netlify site with Large Media enabled, Static CMS will handle Large Media asset files seamlessly, in the same way as files stored directly in the repository. ## Requirements To use Netlify Large Media with Static CMS, you will need to do the following: -- [Upgrade Static CMS](/docs/update-the-cms-version/) to version 2.6.0 or above. -- Configure Static CMS to use the [Git Gateway backend with Netlify Identity](/docs/git-gateway-backend/#git-gateway-with-netlify-identity). +- Configure Static CMS to use the [Git Gateway backend with Netlify Identity](/docs/git-gateway-backend). - Configure the Netlify site and connected repository to use Large Media, following the [Large Media docs on Netlify](https://www.netlify.com/docs/large-media/). When these are complete, you can use Static CMS as normal, and the configured asset files will automatically be handled by Netlify Large Media. @@ -24,17 +25,8 @@ All JPEG, PNG, and GIF files that are handled with Netlify Large Media also have You can learn more about this feature in [Netlify's image transformation docs](https://www.netlify.com/docs/image-transformation/). -### Transformation control for media gallery thumbnails +### Media Gallery Thumbnails In repositories enabled with Netlify Large Media, Static CMS will use the image transformation query parameters to load thumbnail-sized images for the media gallery view. This makes images in the media gallery load significantly faster. **Note:** When using this option all tracked file types have to be imported into Large Media. For example if you track `*.jpg` but still have jpg-files that are not imported into Large Media the backend will throw an error. Check the [netlify docs](https://docs.netlify.com/large-media/setup/#migrate-files-from-git-history) on how to add previously committed files to Large Media. - -You can disable the automatic image transformations with the `use_large_media_transforms_in_media_library` configuration setting, nested under `backend` in the CMS `config.yml` file: - -```yaml -backend: - title: git-gateway - ## Set to false to prevent transforming images in media gallery view - use_large_media_transforms_in_media_library: false -``` diff --git a/website/content/docs/nextjs.mdx b/website/content/docs/nextjs.mdx index 3e4f28ae..8e791aa8 100644 --- a/website/content/docs/nextjs.mdx +++ b/website/content/docs/nextjs.mdx @@ -155,7 +155,7 @@ Paste HTML for Static CMS into your `public/admin/index.html` file (check out th - + ``` @@ -166,27 +166,27 @@ Paste the following configuration into your `public/admin/config.yml` file: ```yaml backend: - title: git-gateway + name: git-gateway branch: main # Branch to update (optional; defaults to main) media_folder: public/img public_folder: img collections: - - title: "pages" + - name: "pages" label: "Pages" files: - label: "Home" - title: "home" + name: "home" file: "content/home.md" fields: - - { label: "Title", title: "title", widget: "string"} - - { label: "Publish Date", title: "date", widget: "datetime" } - - { label: "Body", title: "body", widget: "markdown"} + - { label: "Title", name: "title", widget: "string"} + - { label: "Publish Date", name: "date", widget: "datetime" } + - { label: "Body", name: "body", widget: "markdown"} - label: 'Cats' - title: "cats" + name: "cats" widget: list fields: - - { label: "Name", title: "name", widget: "string"} - - { label: "Description", title: "description", widget: "text"} + - { label: "Name", name: "name", widget: "string"} + - { label: "Description", name: "description", widget: "text"} ``` Awesome! Static CMS should now be available at `localhost:3000/admin/index.html`. Unfortunately we can't edit our content just yet. First we need to move our code into a git repository, and create a new Netlify site. diff --git a/website/content/docs/nuxt.mdx b/website/content/docs/nuxt.mdx index 33ef84f8..d5526484 100644 --- a/website/content/docs/nuxt.mdx +++ b/website/content/docs/nuxt.mdx @@ -33,7 +33,7 @@ In the `static/` directory, create a new directory `admin/`. Inside that directo - + ``` @@ -42,14 +42,14 @@ For your `static/admin/config.yml` file, you can put in a basic starter config: ```yaml backend: - title: git-gateway + name: git-gateway branch: main # Branch to update (optional; defaults to main) media_folder: static/img public_folder: /img collections: - - title: 'blog' + - name: 'blog' label: 'Blog' folder: 'content/blog' format: 'frontmatter' @@ -58,10 +58,10 @@ collections: editor: preview: false fields: - - { label: 'Title', title: 'title', widget: 'string' } - - { label: 'Publish Date', title: 'date', widget: 'datetime' } - - { label: 'Description', title: 'description', widget: 'string' } - - { label: 'Body', title: 'body', widget: 'markdown' } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { label: 'Description', name: 'description', widget: 'string' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` You can build whatever collections and content modeling you want. The important thing to note is the `format: 'frontmatter'` value on each collection. This is important for consuming content in Nuxt with the [nuxt/content](https://content.nuxtjs.org) module. diff --git a/website/content/docs/start-with-a-template.mdx b/website/content/docs/start-with-a-template.mdx index e5711774..48588267 100644 --- a/website/content/docs/start-with-a-template.mdx +++ b/website/content/docs/start-with-a-template.mdx @@ -225,9 +225,9 @@ You can add Static CMS [to an existing site](/docs/add-to-your-site/), but the q
-After clicking one of those buttons, authenticate with GitHub or GitLab and choose a repository name. Netlify then automatically creates a clone of the repository in your GitHub or GitLab account. Next, it builds and deploys the new site on Netlify, bringing you to the site dashboard after completing the build. +After clicking one of those buttons, authenticate with GitHub and choose a repository name. Netlify then automatically creates a clone of the repository in your GitHub account. Next, it builds and deploys the new site on Netlify, bringing you to the site dashboard after completing the build. -**Note for Bitbucket users:** Static CMS supports Bitbucket repositories, but Bitbucket's permissions won't work with the Deploy to Netlify buttons above. You can still set up a repository manually, or follow the [tutorial](/docs/add-to-your-site) for adding Static CMS to an existing site. +**Note for GitLab and Bitbucket users:** Static CMS supports GitLab and Bitbucket repositories, but won't work with the Deploy to Netlify buttons above without additional configuration (See [GitLab](/docs/gitlab-backend) or [Bitbucket](/docs/bitbucket-backend) respectively). ## Access Static CMS diff --git a/website/content/docs/test-backend.mdx b/website/content/docs/test-backend.mdx index e5cf88c5..9763ed69 100644 --- a/website/content/docs/test-backend.mdx +++ b/website/content/docs/test-backend.mdx @@ -1,16 +1,20 @@ --- -group: Accounts -title: Test -weight: 50 +group: Backends +title: Test Backend +weight: 60 --- -You can use the `test-repo` backend to try out Static CMS without connecting to a Git repo. With this backend, you can write and publish content normally, but any changes will disappear when you reload the page. This backend powers the Static CMS [demo site](https://cms-demo.netlify.com/). +- **Name**: `gitlab` + +You can use the `test-repo` backend to try out Static CMS without connecting to a Git repo. With this backend, you can write and publish content normally, but any changes will disappear when you reload the page. This backend powers the Static CMS [demo site](https://static-cms-demo.netlify.app/). **Note:** The `test-repo` backend can't access your local file system, nor does it connect to a Git repo, thus you won't see any existing files while using it. -To enable this backend, add the following lines to your Static CMS `config.yml` file: +To enable this backend, set your backend name to `test-repo` in your Static CMS `config.yml` file. + +## Example ```yaml backend: - title: test-repo + name: test-repo ``` diff --git a/website/content/docs/releases.mdx b/website/content/docs/updating-your-cms.mdx similarity index 92% rename from website/content/docs/releases.mdx rename to website/content/docs/updating-your-cms.mdx index 59a0fd1a..f7149928 100644 --- a/website/content/docs/releases.mdx +++ b/website/content/docs/updating-your-cms.mdx @@ -1,20 +1,16 @@ --- group: Intro -title: Releases +title: Updating Your CMS weight: 10 --- - - -## Update the CMS Version - The update procedure for your CMS depends upon the method you used to install Static CMS. -### Package Manager +## Package Manager If you are using a package manager like Yarn or NPM, use their standard procedure to update. This is how both the Hugo and Gatsby starters are set up. -### CDN +## CDN If you are using the CMS through a CDN like Unpkg, then that depends on the version tag you are using. You can find the version tag in the `/admin/index.html` file of your site. diff --git a/website/content/docs/uploadcare.mdx b/website/content/docs/uploadcare.mdx index cbe9999f..d5895684 100644 --- a/website/content/docs/uploadcare.mdx +++ b/website/content/docs/uploadcare.mdx @@ -3,6 +3,9 @@ group: Media title: Uploadcare weight: 30 --- + +## Beta Feature. Use at your own risk + Uploadcare is a sleek service that allows you to upload files without worrying about maintaining a growing collection — more of an asset store than a library. Just upload when you need to, and the files are hosted on their CDN. They provide image processing controls from simple cropping and rotation to filters and face detection, and a lot more. You can check out Uploadcare's full feature set on their [website](https://uploadcare.com/). The Uploadcare media library integration for Static CMS allows you to use Uploadcare as your media handler within the CMS itself. It's available by default as of our 2.1.0 release, and works in tandem with the existing file and image widgets, so using it only requires creating an Uploadcare account and updating your Static CMS configuration. @@ -25,7 +28,7 @@ Your `config.yml` should now include something like this (except with a real API ```yaml media_library: - title: uploadcare + name: uploadcare config: publicKey: YOUR_UPLOADCARE_PUBLIC_KEY ``` @@ -51,7 +54,7 @@ Configuration can also be provided for individual fields that use the media libr ```yaml ... fields: - title: cover + name: cover label: Cover Image widget: image media_library: @@ -69,7 +72,7 @@ There are several settings that control the behavior of integration with the wid ```yaml media_library: - title: uploadcare + name: uploadcare config: publicKey: YOUR_UPLOADCARE_PUBLIC_KEY settings: diff --git a/website/content/docs/widget-boolean.mdx b/website/content/docs/widget-boolean.mdx index b4bf90ca..ce58e1a9 100644 --- a/website/content/docs/widget-boolean.mdx +++ b/website/content/docs/widget-boolean.mdx @@ -4,8 +4,6 @@ title: Boolean weight: 10 --- -## Overview - - **Name**: `boolean` - **UI**: Toggle switch - **Data type**: `boolean` diff --git a/website/content/docs/widget-code.mdx b/website/content/docs/widget-code.mdx index b7a881cc..c7a0f67c 100644 --- a/website/content/docs/widget-code.mdx +++ b/website/content/docs/widget-code.mdx @@ -3,7 +3,6 @@ group: Widgets title: Code weight: 11 --- -## Overview - **Name**: `code` - **UI**: Codemirror editor @@ -15,12 +14,13 @@ The code widget provides a code editor (powered by [Codemirror](https://codemirr For common options, see [Common widget options](/docs/widgets#common-widget-options). -| Name | Type | Default | Description | -| ------------------------ | ------- | -------------------------------- | -------------------------------------------------------------------- | -| default_language | string | | _Optional_. Default language to use | -| allow_language_selection | boolean | `false` | _Optional_. Allows language syntax to be changed | -| keys | boolean | `{ code: 'code', lang: 'lang' }` | _Optional_. Sets key names for code and lang if outputting an object | -| output_code_only | string | `true` | _Optional_. Set to `true` to output the string value only | +| Name | Type | Default | Description | +| ------------------------ | ------- | -------------------------------- | ------------------------------------------------------------------------------------ | +| default_language | string | | _Optional_. Default language to use | +| allow_language_selection | boolean | `false` | _Optional_. Allows language syntax to be changed | +| keys | boolean | `{ code: 'code', lang: 'lang' }` | _Optional_. Sets key names for code and lang if outputting an object | +| output_code_only | string | `true` | _Optional_. Set to `true` to output the string value only | +| code_mirror_config | boolean | `false` | _Optional_. Config options for [codemiror](https://codemirror.net/5/doc/manual.html) | ## Example diff --git a/website/content/docs/widget-color.mdx b/website/content/docs/widget-color.mdx index 10994305..c68c31d3 100644 --- a/website/content/docs/widget-color.mdx +++ b/website/content/docs/widget-color.mdx @@ -3,7 +3,6 @@ group: Widgets title: Color weight: 12 --- -## Overview - **Name**: `color` - **UI**: Color picker diff --git a/website/content/docs/widget-datetime.mdx b/website/content/docs/widget-datetime.mdx index f2e155e1..9092b413 100644 --- a/website/content/docs/widget-datetime.mdx +++ b/website/content/docs/widget-datetime.mdx @@ -3,7 +3,6 @@ group: Widgets title: DateTime weight: 13 --- -## Overview - **Name**: `datetime` - **UI**: Datetime picker diff --git a/website/content/docs/widget-file.mdx b/website/content/docs/widget-file.mdx index 7f43fc10..00cb28fa 100644 --- a/website/content/docs/widget-file.mdx +++ b/website/content/docs/widget-file.mdx @@ -3,7 +3,6 @@ group: Widgets title: File weight: 14 --- -## Overview - **Name:** `file` - **UI:** File picker button opens media gallery @@ -26,7 +25,7 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti | Name | Type | Default | Description | | -------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| allow_multiple | boolean | `true` | _Optional_. When set to `false`, prevents multiple selection for any media library extension, but must be supported by the extension in use | +| allow_multiple | boolean | `true` | _Optional_. When set to `false`, prevents multiple selection for any media library extension, but must be supported by the extension in use | | config | string | `{}` | _Optional_. A configuration object that will be passed directly to the media library being used - available options are determined by the library | | choose_url | string
\| boolean | `true` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden | diff --git a/website/content/docs/widget-hidden.mdx b/website/content/docs/widget-hidden.mdx index 62a5e59d..2f37565f 100644 --- a/website/content/docs/widget-hidden.mdx +++ b/website/content/docs/widget-hidden.mdx @@ -3,7 +3,6 @@ group: Widgets title: Hidden weight: 15 --- -## Overview - **Name:** `hidden` - **UI:** None diff --git a/website/content/docs/widget-image.mdx b/website/content/docs/widget-image.mdx index 1b57a88d..77350357 100644 --- a/website/content/docs/widget-image.mdx +++ b/website/content/docs/widget-image.mdx @@ -3,7 +3,6 @@ group: Widgets title: Image weight: 16 --- -## Overview - **Name:** `image` - **UI:** File picker button opens media gallery allowing image files (jpg, jpeg, webp, gif, png, bmp, tiff, svg) only; displays selected image thumbnail diff --git a/website/content/docs/widget-list.mdx b/website/content/docs/widget-list.mdx index 34db22d4..0dc40286 100644 --- a/website/content/docs/widget-list.mdx +++ b/website/content/docs/widget-list.mdx @@ -4,8 +4,6 @@ title: List weight: 17 --- -## Overview - - **Name:** `list` - **UI:** The list widget contains a repeatable child widget, with controls for adding, deleting, and re-ordering the repeated widgets. - **Data type:** List of widget values @@ -24,8 +22,8 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti | summary | string | | _Optional_. The label displayed on collapsed entries | | label_singular | string | `label` | _Optional_. The text to show on the add button | | fields | list of widgets | [] | _Optional_. A nested list of multiple widget fields to be included in each repeatable iteration | -| max | number | | _Optional_. Maximum number of items in the list | | min | number | | _Optional_. Minimum number of items in the list | +| max | number | | _Optional_. Maximum number of items in the list | | add_to_top | boolean | `false` | _Optional_.
  • `true` - New entries will be added to the top of the list
  • `false` - New entries will be added to the bottom of the list
| | types | list of object widgets | | _Optional_. A nested list of object widgets representing the available types for items in the list. Takes priority over `fields`. | | type_key | string | `'type'` | _Optional_. The name of the field that will be added to every item in list representing the name of the object widget that item belongs to. Ignored if `types` is not defined | diff --git a/website/content/docs/widget-map.mdx b/website/content/docs/widget-map.mdx index 617452bb..abb95679 100644 --- a/website/content/docs/widget-map.mdx +++ b/website/content/docs/widget-map.mdx @@ -4,8 +4,6 @@ title: Map weight: 18 --- -## Overview - - **Name:** `map` - **UI:** Interactive map - **Data type:** `GeoJSON string`` @@ -20,7 +18,8 @@ For common options, see [Common widget options](/docs/widgets#common-widget-opti | -------- | ---------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------- | | default | string | `''` | _Optional_. The default value for the field. Accepts a GeoJSON string containing a single geometry | | decimals | number | `7` | _Optional_. Precision of saved coordinates | -| default | 'Point'
\| 'LineString'
\| 'Polygon' | `'Point'` | _Optional_. Data type | +| type | 'Point'
\| 'LineString'
\| 'Polygon' | `'Point'` | _Optional_. Data type | +| height | string | `'400px'` | _Optional_. Height of map element | ## Example diff --git a/website/content/docs/widget-markdown.mdx b/website/content/docs/widget-markdown.mdx index f7269190..38c894af 100644 --- a/website/content/docs/widget-markdown.mdx +++ b/website/content/docs/widget-markdown.mdx @@ -4,23 +4,32 @@ title: Markdown weight: 19 --- -## Overview - - **Name:** `markdown` -- **UI:** [Toast UI Editor](https://ui.toast.com/tui-editor) +- **UI:** [Toast UI Editor](https://ui.toast.com/tui-editor) ([Docs](https://nhn.github.io/tui.editor/latest/)) - **Data type:** `markdown string` The markdown widget provides a full fledged text editor allowing users to format text with features such as headings and blockquotes. Users can change their editing view with a handy toggle button. _Please note:_ If you want to use your markdown editor to fill a markdown file contents after its frontmatter, you'll have to name the field `body` so the CMS recognizes it and saves the file accordingly. -## Widget options +## Widget Options For common options, see [Common widget options](/docs/widgets#common-widget-options). -| Name | Type | Default | Description | -| ------- | ------ | ------- | --------------------------------------------------------------------- | -| default | string | `''` | _Optional_. The default value for the field. Accepts markdown content | +| Name | Type | Default | Description | +| ------------- | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| default | string | `''` | _Optional_. The default value for the field. Accepts markdown content | +| media_library | Media Library Options | `{}` | _Optional_. Media library settings to apply when a media library is opened by the current widget. See [Media Library Options](#media-library-options) | +| media_folder | string | | _Optional_. Specifies the folder path where uploaded files should be saved, relative to the base of the repo | +| public_folder | string | | _Optional_. Specifies the folder path where the files uploaded by the media library will be accessed, relative to the base of the built site | + +### Media Library Options + +| Name | Type | Default | Description | +| -------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| allow_multiple | boolean | `true` | _Optional_. When set to `false`, prevents multiple selection for any media library extension, but must be supported by the extension in use | +| config | string | `{}` | _Optional_. A configuration object that will be passed directly to the media library being used - available options are determined by the library | +| choose_url | string
\| boolean | `true` | _Optional_. When set to `false`, the "Insert from URL" button will be hidden | ## Example @@ -35,3 +44,63 @@ This would render as: ![Markdown widget example](/img/widgets-markdown.webp) _Please note:_ The markdown widget outputs a raw markdown string. Your static site generator may or may not render the markdown to HTML automatically. Consult with your static site generator's documentation for more information about rendering markdown. + +## Customization + +Several customization options are available for the markdown editor. You can register the options by calling `setMarkdownEditorOptions` (also available on the global `window.CMS`). + +### Available Options + +| Name | Type | Default | Description | +| --------------- | ---------------------------- | ----------------------------------- | --------------------------------------------------------------------------- | +| initialEditType | 'markdown'
\| 'wysiwyg' | `'wysiwyg'` | _Optional_. Sets which editor view that is active when the editor is loaded | +| height | string | `'600px'` | _Optional_. Specify the height of the editor | +| toolbarItems | factory of list of strings | See [Toolbar Items](#toolbar-items) | _Optional_. See [Toolbar Items](#toolbar-items) | +| plugins | list of plugin factories | | _Optional_. See [Plugins](#plugins) | + +### Toolbar Items + +`toolbarItems` accepts a factory function that returns a list of toolbar buttons for the editor. See the [ToastUI Editor toolbar docs](https://github.com/nhn/tui.editor/blob/master/docs/en/toolbar.md). + +#### Default Value + +```js +[ + ['heading', 'bold', 'italic', 'strike'], + ['hr', 'quote'], + ['ul', 'ol', 'task', 'indent', 'outdent'], + ['table', imageToolbarButton, 'link'], + ['code', 'codeblock'], +]; +``` + +#### Factory Props + +| Name | Type | Description | +| ------------------ | ------------------ | ----------------------------------------------------------- | +| imageToolbarButton | ToolbarItemOptions | An image insert button tied into Static CMS's media library | + +### Plugins + +`plugins` accepts a list of factory functions that returns a plugin for the editor. See the [ToastUI Editor plugins docs](https://github.com/nhn/tui.editor/blob/master/docs/en/plugin.md). + +#### Default Value + +```js +[imagePlugin]; +``` + +#### Factory Props + +| Name | Type | Description | +| ------ | ----------------------- | ---------------------------------------------------------------------------------------------- | +| config | Config | The current Static CMS config. See [configuration options](/docs/configuration-options) | +| field | MarkdownField | The field configuration for the current Markdown widget. See [Widget Options](#widget-options) | +| media | MediaHolder | See [Media Holder](#media-holder) | +| mode | 'editor'
\| 'preview' | Specifies if your plugin is running in the markdown editor or the markdown preview | + +##### Media Holder + +Media holder is a javascript class that holds the loaded media assets (images or files) that are present in the markdown content. It exposes a method called `getMedia` that takes a `url` and returns the loaded image or file as an blob asset. + +This is utilized by the `imagePlugin` to be able to render images present in the markdown that are currently only available in backend or are not yet persisted to the backend. diff --git a/website/content/docs/widget-number.mdx b/website/content/docs/widget-number.mdx index a0058c3e..45803b98 100644 --- a/website/content/docs/widget-number.mdx +++ b/website/content/docs/widget-number.mdx @@ -4,8 +4,6 @@ title: Number weight: 20 --- -## Overview - - **Name:** `number` - **UI:** HTML [number input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number) - **Data type:** `string` or `number`. Configured by `value_type` option diff --git a/website/content/docs/widget-object.mdx b/website/content/docs/widget-object.mdx index 81b7c402..95fb0af9 100644 --- a/website/content/docs/widget-object.mdx +++ b/website/content/docs/widget-object.mdx @@ -4,8 +4,6 @@ title: Object weight: 21 --- -## Overview - - **Name:** `object` - **UI:** a field containing one or more child widgets - **Data type:** Object of child widget values diff --git a/website/content/docs/widget-relation.mdx b/website/content/docs/widget-relation.mdx index 5b7d7e13..4d67a9ee 100644 --- a/website/content/docs/widget-relation.mdx +++ b/website/content/docs/widget-relation.mdx @@ -4,8 +4,6 @@ title: Relation weight: 22 --- -## Overview - - **Name:** `relation` - **UI:** Text input with search result dropdown - **Data type:** Data type of the value pulled from the related collection item diff --git a/website/content/docs/widget-select.mdx b/website/content/docs/widget-select.mdx index c5ccf9a6..24625fc2 100644 --- a/website/content/docs/widget-select.mdx +++ b/website/content/docs/widget-select.mdx @@ -4,8 +4,6 @@ title: Select weight: 23 --- -## Overview - - **Name:** `select` - **UI:** Select input - **Data type:** `string`, `number`, `list of strings` or `list of numbers` @@ -59,7 +57,7 @@ align: 2 ### Options As Objects ```yaml -title: airport-code +name: airport-code label: City widget: select options: diff --git a/website/content/docs/widget-string.mdx b/website/content/docs/widget-string.mdx index 2fe7348d..87b43b6f 100644 --- a/website/content/docs/widget-string.mdx +++ b/website/content/docs/widget-string.mdx @@ -4,8 +4,6 @@ title: String weight: 24 --- -## Overview - - **Name:** `string` - **UI:** Text input - **Data type:** `string` diff --git a/website/content/docs/widget-text.mdx b/website/content/docs/widget-text.mdx index f5f49906..cd7dbe2a 100644 --- a/website/content/docs/widget-text.mdx +++ b/website/content/docs/widget-text.mdx @@ -4,8 +4,6 @@ title: Text weight: 25 --- -## Overview - - **Name:** `text` - **UI:** Textarea - **Data type:** `string` diff --git a/website/content/docs/widgets.mdx b/website/content/docs/widgets.mdx index 4f9bb695..3d4ac0e4 100644 --- a/website/content/docs/widgets.mdx +++ b/website/content/docs/widgets.mdx @@ -8,22 +8,43 @@ Widgets define the data type and interface for entry fields. Static CMS comes wi Widgets are specified as collection fields in the Static CMS `config.yml` file. Note that [YAML syntax](https://en.wikipedia.org/wiki/YAML#Basic_components) allows lists and objects to be written in block or inline style, and the code samples below include a mix of both. -To see working examples of all of the built-in widgets, try making a 'Kitchen Sink' collection item on the [CMS demo site](https://static-cms-demo.netlify.com). (No login required: click the login button and the CMS will open.) You can refer to the demo [configuration code](https://github.com/StaticJsCMS/static-cms/blob/main/dev-test/config.yml) to see how each field was configured. +To see working examples of all of the built-in widgets, try making a 'Kitchen Sink' collection item on the [CMS demo site](https://static-static-cms-demo.netlify.app). (No login required: click the login button and the CMS will open.) You can refer to the demo [configuration code](https://github.com/StaticJsCMS/static-cms/blob/main/dev-test/config.yml) to see how each field was configured. + +## Available Widgets + +| Name | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| [Boolean](/docs/widget-boolean) | The boolean widget translates a toggle switch input to a true or false value | +| [Code](/docs/widget-code) | The code widget provides a code editor (powered by Codemirror) with optional syntax awareness | +| [Color](/docs/widget-color) | The color widget translates a color picker to a color string | +| [Datetime](/docs/widget-datetime) | The datetime widget translates a datetime picker to a datetime string | +| [File](/docs/widget-file) | The file widget allows editors to upload a file or select an existing one from the media library | +| [Hidden](/docs/widget-hidden) | Hidden widgets do not display in the UI | +| [Image](/docs/widget-image) | The file widget allows editors to upload a file or select an existing one from the media library | +| [List](/docs/widget-list) | The list widget allows you to create a repeatable item in the UI which saves as a list of widget values | +| [Map](/docs/widget-map) | The map widget allows you to edit spatial data using an interactive map | +| [Markdown](/docs/widget-markdown) | The markdown widget provides a full fledged text editor allowing users to format text with features such as headings and blockquotes | +| [Number](/docs/widget-number) | The number widget uses an HTML number input | +| [Object](/docs/widget-object) | The object widget allows you to group multiple widgets together, nested under a single field | +| [Relation](/docs/widget-relation) | The relation widget allows you to reference items from another collection | +| [Select](/docs/widget-select) | The select widget allows you to pick a string value from a dropdown menu | +| [String](/docs/widget-string) | The string widget translates a basic text input to a string value | +| [Text](/docs/widget-text) | The text widget takes a multiline text field and saves it as a string | ## Common widget options The following options are available on all fields: -| Name | Type | Default | Description | -| ------------- | ----------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | string | | The name of the field | -| widget | string | `'string'` | _Optional_. The type of widget to render for the field | -| label | string | `name` | _Optional_. The display name of the field | -| required | boolean | `true` | _Optional_. Specify as `false` to make a field optional | -| hint | string | | _Optional_. Adds helper text directly below a widget. Useful for including instructions. Accepts markdown for bold, italic, strikethrough, and links. | -| pattern | string | | _Optional_. Adds field validation by specifying a list with a [regex pattern](https://regexr.com/) and an error message; more extensive validation can be achieved with [custom widgets](/docs/custom-widgets/#advanced-field-validation) | -| i18n | boolean
\|'translate'
\|'duplicate'
\|'none' | | _Optional_. Beta Feature
  • `translate` - Allows translation of the field
  • `duplicate` - Duplicates the value from the default locale
  • `true` - Accept parent values as default
  • `none` or `false` - Exclude field from translations
| -| comment | string | | _Optional_. Adds comment before the field (only supported for yaml) | +| Name | Type | Default | Description | +| -------- | ----------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | | The name of the field | +| widget | string | `'string'` | _Optional_. The type of widget to render for the field | +| label | string | `name` | _Optional_. The display name of the field | +| required | boolean | `true` | _Optional_. Specify as `false` to make a field optional | +| hint | string | | _Optional_. Adds helper text directly below a widget. Useful for including instructions. Accepts markdown for bold, italic, strikethrough, and links. | +| pattern | string | | _Optional_. Adds field validation by specifying a list with a [regex pattern](https://regexr.com/) and an error message; more extensive validation can be achieved with [custom widgets](/docs/custom-widgets/#advanced-field-validation) | +| i18n | boolean
\|'translate'
\|'duplicate'
\|'none' | | _Optional_. Beta Feature. Use at your own risk
  • `translate` - Allows translation of the field
  • `duplicate` - Duplicates the value from the default locale
  • `true` - Accept parent values as default
  • `none` or `false` - Exclude field from translations
| +| comment | string | | _Optional_. Adds comment before the field (only supported for yaml) | ### Example diff --git a/website/content/docs/writing-style-guide.mdx b/website/content/docs/writing-style-guide.mdx index 792f2066..b9209f0c 100644 --- a/website/content/docs/writing-style-guide.mdx +++ b/website/content/docs/writing-style-guide.mdx @@ -116,7 +116,7 @@ _____ > Do: Set the value of the `replicas` field to 2. -> Don't: Set the value of the `replicas` field to 2. +> Don't: Set the value of the `replicas` field to `2`. ## Code snippet formatting diff --git a/website/content/menu.json b/website/content/menu.json index 2d4f5129..64255897 100644 --- a/website/content/menu.json +++ b/website/content/menu.json @@ -6,12 +6,8 @@ "title": "Intro to Static CMS" }, { - "name": "Accounts", - "title": "Account Settings" - }, - { - "name": "Configuration", - "title": "Configuring your Site" + "name": "Backends", + "title": "Backends" }, { "name": "Collections", diff --git a/website/src/components/docs/DocsContent.tsx b/website/src/components/docs/DocsContent.tsx index ee2ec9ac..9efc2553 100644 --- a/website/src/components/docs/DocsContent.tsx +++ b/website/src/components/docs/DocsContent.tsx @@ -53,13 +53,17 @@ const DocsContent = styled('div')( font-weight: 500; margin-top: 28px; margin-bottom: 12px; + position: relative; + } + + & h3 { + color: ${theme.palette.text.primary}; } - & h3, & h4, & h5, & h6 { - color: ${theme.palette.text.primary}; + color: ${theme.palette.text.secondary}; } ${theme.breakpoints.up('lg')} { @@ -159,19 +163,6 @@ const DocsContent = styled('div')( margin-top: 0; } - & table thead tr th, - & table thead tr td { - white-space: nowrap; - } - - & table tbody tr td { - white-space: nowrap; - } - - & table tbody tr td:last-child { - white-space: normal; - } - & pre { display: block; line-height: 1.25rem; @@ -273,13 +264,6 @@ const DocsContent = styled('div')( } ${theme.breakpoints.down('md')} { - & h2, - & h3, - & h4, - & h5 { - position: relative; - } - & h2::before { display: block; } diff --git a/website/src/components/docs/components/Anchor.tsx b/website/src/components/docs/components/Anchor.tsx new file mode 100644 index 00000000..8d7463fb --- /dev/null +++ b/website/src/components/docs/components/Anchor.tsx @@ -0,0 +1,61 @@ +import Link from 'next/link'; +import { useMemo } from 'react'; + +import type { DetailedHTMLProps, AnchorHTMLAttributes, ReactNode } from 'react'; + +enum LinkType { + SAME_SITE, + SAME_PAGE, + EXTERNAL, +} + +interface AnchorProps + extends DetailedHTMLProps, HTMLAnchorElement> { + children?: ReactNode; +} + +const Anchor = ({ href = '', children = '' }: AnchorProps) => { + const type: LinkType = useMemo(() => { + if (href.startsWith('#')) { + return LinkType.SAME_PAGE; + } + + if (href.startsWith('/') || href.startsWith('.')) { + return LinkType.SAME_SITE; + } + + return LinkType.EXTERNAL; + }, [href]); + + if (type === LinkType.SAME_PAGE) { + return ( + { + e.preventDefault(); + document.querySelector(href)?.scrollIntoView({ + behavior: 'smooth', + }); + }} + > + {children} + + ); + } + + if (type === LinkType.SAME_SITE) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +}; + +export default Anchor; diff --git a/website/src/components/docs/components/Header2.tsx b/website/src/components/docs/components/Header2.tsx deleted file mode 100644 index a2f86b30..00000000 --- a/website/src/components/docs/components/Header2.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import LinkIcon from '@mui/icons-material/Link'; -import { styled, useTheme } from '@mui/material/styles'; -import Typography from '@mui/material/Typography'; -import Link from 'next/link'; - -import type { ReactNode } from 'react'; - -const StyledLink = styled('a')( - ({ theme }) => ` - position: absolute; - margin-left: -28px; - top: -1px; - font-weight: 300; - color: ${theme.palette.secondary.main}; - transform: rotateZ(-45deg); - - ${theme.breakpoints.down('sm')} { - margin-left: -22px; - } - `, -); - -function getAnchor(text: string) { - return text - .toLowerCase() - .replace(/[^a-z0-9 ]/g, '') - .replace(/[ ]/g, '-'); -} - -interface Header2Props { - children?: ReactNode; -} - -const Header2 = ({ children = '' }: Header2Props) => { - const anchor = getAnchor(String(children)); - const link = `#${anchor}`; - const theme = useTheme(); - return ( - - - - - - - {children} - - ); -}; - -export default Header2; diff --git a/website/src/components/docs/components/Header3.tsx b/website/src/components/docs/components/Header3.tsx deleted file mode 100644 index 0506782d..00000000 --- a/website/src/components/docs/components/Header3.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import LinkIcon from '@mui/icons-material/Link'; -import { styled, useTheme } from '@mui/material/styles'; -import Typography from '@mui/material/Typography'; -import Link from 'next/link'; - -import type { ReactNode } from 'react'; - -const StyledLink = styled('a')( - ({ theme }) => ` - position: absolute; - margin-left: -28px; - top: 0; - font-weight: 300; - color: ${theme.palette.text.primary}; - transform: rotateZ(-45deg); - - ${theme.breakpoints.down('sm')} { - margin-left: -22px; - top: -1px; - } - `, -); - -function getAnchor(text: string) { - return text - .toLowerCase() - .replace(/[^a-z0-9 ]/g, '') - .replace(/[ ]/g, '-'); -} - -interface Header3Props { - children?: ReactNode; -} - -const Header3 = ({ children = '' }: Header3Props) => { - const anchor = getAnchor(String(children)); - const link = `#${anchor}`; - const theme = useTheme(); - return ( - - - - - - - {children} - - ); -}; - -export default Header3; diff --git a/website/src/components/docs/components/headers/Header2.tsx b/website/src/components/docs/components/headers/Header2.tsx new file mode 100644 index 00000000..ae3c0095 --- /dev/null +++ b/website/src/components/docs/components/headers/Header2.tsx @@ -0,0 +1,13 @@ +import LinkedHeader from './components/LinkedHeader'; + +import type { ReactNode } from 'react'; + +interface Header2Props { + children?: ReactNode; +} + +const Header2 = ({ children }: Header2Props) => { + return {children}; +}; + +export default Header2; diff --git a/website/src/components/docs/components/headers/Header3.tsx b/website/src/components/docs/components/headers/Header3.tsx new file mode 100644 index 00000000..80aca626 --- /dev/null +++ b/website/src/components/docs/components/headers/Header3.tsx @@ -0,0 +1,13 @@ +import LinkedHeader from './components/LinkedHeader'; + +import type { ReactNode } from 'react'; + +interface Header3Props { + children?: ReactNode; +} + +const Header3 = ({ children }: Header3Props) => { + return {children}; +}; + +export default Header3; diff --git a/website/src/components/docs/components/headers/Header4.tsx b/website/src/components/docs/components/headers/Header4.tsx new file mode 100644 index 00000000..3bd0a4d9 --- /dev/null +++ b/website/src/components/docs/components/headers/Header4.tsx @@ -0,0 +1,13 @@ +import LinkedHeader from './components/LinkedHeader'; + +import type { ReactNode } from 'react'; + +interface Header4Props { + children?: ReactNode; +} + +const Header4 = ({ children }: Header4Props) => { + return {children}; +}; + +export default Header4; diff --git a/website/src/components/docs/components/headers/Header5.tsx b/website/src/components/docs/components/headers/Header5.tsx new file mode 100644 index 00000000..11f58b35 --- /dev/null +++ b/website/src/components/docs/components/headers/Header5.tsx @@ -0,0 +1,13 @@ +import LinkedHeader from './components/LinkedHeader'; + +import type { ReactNode } from 'react'; + +interface Header5Props { + children?: ReactNode; +} + +const Header5 = ({ children }: Header5Props) => { + return {children}; +}; + +export default Header5; diff --git a/website/src/components/docs/components/headers/Header6.tsx b/website/src/components/docs/components/headers/Header6.tsx new file mode 100644 index 00000000..9aadebad --- /dev/null +++ b/website/src/components/docs/components/headers/Header6.tsx @@ -0,0 +1,13 @@ +import LinkedHeader from './components/LinkedHeader'; + +import type { ReactNode } from 'react'; + +interface Header6Props { + children?: ReactNode; +} + +const Header6 = ({ children }: Header6Props) => { + return {children}; +}; + +export default Header6; diff --git a/website/src/components/docs/components/headers/components/AnchorLinkIcon.tsx b/website/src/components/docs/components/headers/components/AnchorLinkIcon.tsx new file mode 100644 index 00000000..1fe440ed --- /dev/null +++ b/website/src/components/docs/components/headers/components/AnchorLinkIcon.tsx @@ -0,0 +1,45 @@ +import LinkIcon from '@mui/icons-material/Link'; +import { useTheme } from '@mui/material/styles'; +import { useMemo } from 'react'; + +interface AnchorLinkIconProps { + variant: 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; +} + +const AnchorLinkIcon = ({ variant }: AnchorLinkIconProps) => { + const theme = useTheme(); + + const color = useMemo(() => { + if (variant === 'h2') { + return theme.palette.secondary.main; + } + + if (variant === 'h3') { + return theme.palette.text.primary; + } + + return theme.palette.text.secondary; + }, [ + theme.palette.secondary.main, + theme.palette.text.primary, + theme.palette.text.secondary, + variant, + ]); + + return ( + + ); +}; + +export default AnchorLinkIcon; diff --git a/website/src/components/docs/components/headers/components/LinkedHeader.tsx b/website/src/components/docs/components/headers/components/LinkedHeader.tsx new file mode 100644 index 00000000..9f3bc6b8 --- /dev/null +++ b/website/src/components/docs/components/headers/components/LinkedHeader.tsx @@ -0,0 +1,63 @@ +import { styled, useTheme } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; +import Link from 'next/link'; +import { useMemo } from 'react'; + +import { useNodeText } from '../../../../../util/node.util'; +import { isNotEmpty } from '../../../../../util/string.util'; +import useAnchor from '../hooks/useAnchor'; +import AnchorLinkIcon from './AnchorLinkIcon'; + +import type { ReactNode } from 'react'; + +const StyledLink = styled('a')( + ({ theme }) => ` + position: absolute; + margin-left: -28px; + top: 0; + font-weight: 300; + color: ${theme.palette.text.primary}; + transform: rotateZ(-45deg); + + ${theme.breakpoints.down('sm')} { + margin-left: -22px; + top: -1px; + } + `, +); + +interface Header3Props { + variant: 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; + children?: ReactNode; +} + +const Header3 = ({ variant, children = '' }: Header3Props) => { + const textContent = useNodeText(children); + const anchor = useAnchor(textContent); + const link = useMemo(() => `#${anchor}`, [anchor]); + const theme = useTheme(); + const hasText = useMemo(() => isNotEmpty(textContent), [textContent]); + return ( + + {hasText ? ( + + + + + + ) : null} + {children} + + ); +}; + +export default Header3; diff --git a/website/src/components/docs/components/headers/hooks/useAnchor.ts b/website/src/components/docs/components/headers/hooks/useAnchor.ts new file mode 100644 index 00000000..23f24e9d --- /dev/null +++ b/website/src/components/docs/components/headers/hooks/useAnchor.ts @@ -0,0 +1,9 @@ +const useAnchor = (text: string) => { + return text + .trim() + .toLowerCase() + .replace(/[^a-z0-9 ]/g, '') + .replace(/[ ]/g, '-'); +}; + +export default useAnchor; diff --git a/website/src/components/docs/components/table/Table.tsx b/website/src/components/docs/components/table/Table.tsx index d98d98f9..fbb88d9d 100644 --- a/website/src/components/docs/components/table/Table.tsx +++ b/website/src/components/docs/components/table/Table.tsx @@ -10,14 +10,35 @@ const StyledTableContainer = styled(TableContainer)( color: ${theme.palette.text.secondary}; } - & td:nth-of-type(2) { + & td:nth-of-type(2):not(:last-of-type) { color: ${theme.palette.mode === 'light' ? '#751365' : '#ffb6ec'}; } - & td:last-of-type { + & thead tr th, + & thead tr td { + white-space: nowrap; + } + + & tbody tr td { + white-space: nowrap; + } + + & tbody tr td:last-of-type { + white-space: normal; + } + + .non-props-table + & tbody tr td { + white-space: normal; + } + + & tbody tr td:last-of-type { min-width: 200px; } + + .non-props-table + & tbody tr td:last-of-type { + min-width: unset; + } `, ); diff --git a/website/src/components/docs/table_of_contents/DocsTableOfContents.tsx b/website/src/components/docs/table_of_contents/DocsTableOfContents.tsx index 34a325ed..f5adbfbd 100644 --- a/website/src/components/docs/table_of_contents/DocsTableOfContents.tsx +++ b/website/src/components/docs/table_of_contents/DocsTableOfContents.tsx @@ -2,6 +2,7 @@ import { styled } from '@mui/material/styles'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState } from 'react'; +import { isNotEmpty } from '../../../util/string.util'; import DocsHeadings from './DocsHeadings'; export interface Heading { @@ -19,7 +20,7 @@ const getNestedHeadings = (headingElements: HTMLHeadingElement[]) => { headingElements.forEach(heading => { const { innerText: title, id } = heading; - if (heading.nodeName === 'H2') { + if (heading.nodeName === 'H1' || heading.nodeName === 'H2') { nestedHeadings.push({ id, title, items: [] }); } else if (heading.nodeName === 'H3' && nestedHeadings.length > 0) { nestedHeadings[nestedHeadings.length - 1].items.push({ @@ -38,7 +39,7 @@ const useHeadingsData = () => { useEffect(() => { const headingElements = Array.from( - document.querySelectorAll('main h2, main h3'), + document.querySelectorAll('main h1, main h2, main h3'), ); // Created a list of headings, with H3s nested @@ -53,7 +54,9 @@ const useIntersectionObserver = (setActiveId: (activeId: string) => void) => { const headingElementsRef = useRef>({}); const { asPath } = useRouter(); useEffect(() => { - const headingElements = Array.from(document.querySelectorAll('h2, h3')); + const headingElements = Array.from( + document.querySelectorAll('main h1, main h2, main h3'), + ); if (headingElementsRef.current) { headingElementsRef.current = {}; @@ -71,7 +74,7 @@ const useIntersectionObserver = (setActiveId: (activeId: string) => void) => { const headingElement = ( headingElementsRef.current as Record )[key]; - if (headingElement.isIntersecting) { + if (headingElement.isIntersecting && isNotEmpty(headingElement.target.textContent)) { visibleHeadings.push(headingElement); } }); diff --git a/website/src/pages/docs/[doc].tsx b/website/src/pages/docs/[doc].tsx index 0254e4da..43f72e65 100644 --- a/website/src/pages/docs/[doc].tsx +++ b/website/src/pages/docs/[doc].tsx @@ -4,9 +4,13 @@ import { MDXRemote } from 'next-mdx-remote'; import { serialize } from 'next-mdx-remote/serialize'; import remarkGfm from 'remark-gfm'; +import Anchor from '../../components/docs/components/Anchor'; import Blockquote from '../../components/docs/components/Blockquote'; -import Header2 from '../../components/docs/components/Header2'; -import Header3 from '../../components/docs/components/Header3'; +import Header2 from '../../components/docs/components/headers/Header2'; +import Header3 from '../../components/docs/components/headers/Header3'; +import Header4 from '../../components/docs/components/headers/Header4'; +import Header5 from '../../components/docs/components/headers/Header5'; +import Header6 from '../../components/docs/components/headers/Header6'; import DocsTable from '../../components/docs/components/table/Table'; import TableBody from '../../components/docs/components/table/TableBody'; import TableBodyCell from '../../components/docs/components/table/TableBodyCell'; @@ -95,12 +99,16 @@ const Docs = ({ docsGroups, title, slug, description = '', source }: DocsProps) components={{ h2: Header2, h3: Header3, + h4: Header4, + h5: Header5, + h6: Header6, blockquote: Blockquote, table: DocsTable, thead: TableHead, tbody: TableBody, th: TableHeaderCell, td: TableBodyCell, + a: Anchor, }} /> @@ -145,8 +153,6 @@ export const getStaticProps: GetStaticProps = async ({ params }): Promise<{ prop }, }); - console.log(docsGroups.flatMap(group => group.links.flatMap(link => link.title))); - return { props: { docsGroups, diff --git a/website/src/util/node.util.ts b/website/src/util/node.util.ts new file mode 100644 index 00000000..25c71e8f --- /dev/null +++ b/website/src/util/node.util.ts @@ -0,0 +1,23 @@ +import { useMemo } from 'react'; + +import { isNotEmpty } from './string.util'; + +import type { ReactNode } from 'react'; + +export const getNodeText = (node: ReactNode): string => { + if (['string', 'number'].includes(typeof node)) { + return `${node}`; + } + + if (node instanceof Array) { + return node.map(getNodeText).filter(isNotEmpty).join(''); + } + + if (typeof node === 'object' && node && 'props' in node) { + return getNodeText(node.props.children); + } + + return ''; +}; + +export const useNodeText = (node: ReactNode) => useMemo(() => getNodeText(node), [node]);