diff --git a/core/dev-test/config.yml b/core/dev-test/config.yml
index e2e23c48..bbcb5b6a 100644
--- a/core/dev-test/config.yml
+++ b/core/dev-test/config.yml
@@ -98,10 +98,14 @@ collections:
description: Boolean widget
fields:
- name: required
- label: 'Required Validation'
+ label: Required Validation
widget: boolean
+ - name: with_default
+ label: Required With Default
+ widget: boolean
+ default: true
- name: pattern
- label: 'Pattern Validation'
+ label: Pattern Validation
widget: boolean
pattern: ['true', 'Must be true']
required: false
@@ -111,29 +115,58 @@ collections:
description: Code widget
fields:
- name: required
- label: 'Required Validation'
+ label: Required Validation
widget: code
+ - name: with_default
+ label: Required With Default
+ widget: code
+ default: '
Some html!
'
- name: pattern
- label: 'Pattern Validation'
+ label: Pattern Validation
widget: code
pattern: ['.{12,}', 'Must have at least 12 characters']
allow_input: true
required: false
- name: language
- label: 'Language Selection'
+ label: Language Selection
widget: code
allow_language_selection: true
required: false
+ - name: language_with_default
+ label: Language Selection With Default Language
+ widget: code
+ allow_language_selection: true
+ required: false
+ default_language: html
+ - name: language_with_default_language_and_value
+ label: Language Selection With Default Language and Value
+ widget: code
+ allow_language_selection: true
+ required: false
+ default:
+ lang: html
+ code: 'Some html!
'
+ - name: language_with_default_language_and_value_string_default
+ label: Language Selection With Default Language and Value (String Default)
+ widget: code
+ allow_language_selection: true
+ required: false
+ default_language: html
+ default: 'Some html!
'
- name: color
label: Color
file: _widgets/color.json
description: Color widget
fields:
- name: required
- label: 'Required Validation'
+ label: Required Validation
widget: color
+ - name: with_default
+ label: Required With Default
+ widget: color
+ default: '#2121c5'
- name: pattern
- label: 'Pattern Validation'
+ label: Pattern Validation
widget: color
pattern: ['^#([0-9a-fA-F]{3})(?:[0-9a-fA-F]{3})?$', 'Must be a valid hex code']
allow_input: true
@@ -143,6 +176,12 @@ collections:
widget: color
enable_alpha: true
required: false
+ - name: alpha_with_default
+ label: Alpha With Default
+ widget: color
+ enable_alpha: true
+ required: false
+ default: 'rgba(175, 28, 28, 0.65)'
- name: datetime
label: DateTime
file: _widgets/datetime.json
@@ -166,33 +205,59 @@ collections:
date_format: 'MMM d, yyyy'
time_format: 'h:mm aaa'
required: false
+ - name: date_and_time_with_default
+ label: Date and Time With Deafult
+ widget: datetime
+ format: 'MMM d, yyyy h:mm aaa'
+ date_format: 'MMM d, yyyy'
+ time_format: 'h:mm aaa'
+ required: false
+ default: 'Jan 12, 2023 12:00 am'
- name: date
label: Date
widget: datetime
format: 'MMM d, yyyy'
date_format: 'MMM d, yyyy'
required: false
+ - name: date_with_default
+ label: Date With Deafult
+ widget: datetime
+ format: 'MMM d, yyyy'
+ date_format: 'MMM d, yyyy'
+ required: false
+ default: 'Jan 12, 2023'
- name: time
label: Time
widget: datetime
format: 'h:mm aaa'
time_format: 'h:mm aaa'
required: false
+ - name: time_with_default
+ label: Time With Deafult
+ widget: datetime
+ format: 'h:mm aaa'
+ time_format: 'h:mm aaa'
+ required: false
+ default: '12:00 am'
- name: file
label: File
file: _widgets/file.json
description: File widget
fields:
- name: required
- label: 'Required Validation'
+ label: Required Validation
widget: file
+ - name: with_default
+ label: Required With Default
+ widget: file
+ default: /assets/uploads/moby-dick.jpg
- name: pattern
- label: 'Pattern Validation'
+ label: Pattern Validation
widget: file
pattern: ['\.pdf', 'Must be a pdf']
required: false
- name: choose_url
- label: 'Choose URL'
+ label: Choose URL
widget: file
required: false
media_library:
@@ -203,15 +268,19 @@ collections:
description: Image widget
fields:
- name: required
- label: 'Required Validation'
+ label: Required Validation
widget: image
+ - name: with_default
+ label: Required With Default
+ widget: image
+ default: /assets/uploads/moby-dick.jpg
- name: pattern
- label: 'Pattern Validation'
+ label: Pattern Validation
widget: image
pattern: ['\.png', 'Must be a png']
required: false
- name: choose_url
- label: 'Choose URL'
+ label: Choose URL
widget: image
required: false
media_library:
@@ -222,7 +291,7 @@ collections:
description: List widget
fields:
- name: list
- label: List
+ label: Required List
widget: list
fields:
- label: Name
@@ -232,6 +301,32 @@ collections:
- label: Description
name: description
widget: text
+ - name: with_default
+ label: Required With Default
+ widget: list
+ default:
+ - name: Bob Billy
+ description: Some text about bob
+ fields:
+ - label: Name
+ name: name
+ widget: string
+ hint: First and Last
+ - label: Description
+ name: description
+ widget: text
+ - name: optional
+ label: Optional List
+ widget: list
+ required: false
+ fields:
+ - label: Name
+ name: name
+ widget: string
+ hint: First and Last
+ - label: Description
+ name: description
+ widget: text
- name: typed_list
label: Typed List
widget: list
@@ -268,7 +363,60 @@ collections:
widget: datetime
- label: Markdown
name: markdown
+ widget: markdown
+ - label: Type 3 Object
+ name: type_3_object
+ widget: object
+ fields:
+ - label: Image
+ name: image
+ widget: image
+ - label: File
+ name: file
+ widget: file
+ - name: typed_list_with_default
+ label: Typed List With Default
+ widget: list
+ default:
+ - type: type_2_object
+ number: 5
+ select: c
+ datetime: '2022-12-05T20:22:52+0000'
+ markdown: Some ***Markdown*** ~content~ text
+ 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: markdown
- label: Type 3 Object
name: type_3_object
widget: object
@@ -285,10 +433,14 @@ collections:
description: Map widget
fields:
- name: required
- label: 'Required Validation'
+ label: Required Validation
widget: map
+ - name: with_default
+ label: Required With Default
+ widget: map
+ default: '{ "type": "Point", "coordinates": [-73.9852661, 40.7478738] }'
- name: pattern
- label: 'Pattern Validation'
+ label: Pattern Validation
widget: map
pattern: ['\[-([7-9][0-9]|1[0-2][0-9])\.', 'Must be between latitude -70 and -129']
required: false
@@ -298,10 +450,14 @@ collections:
description: Markdown widget
fields:
- name: required
- label: 'Required Validation'
+ label: Required Validation
widget: markdown
+ - name: with_default
+ label: Required With Default
+ widget: markdown
+ default: Default **markdown** value
- name: pattern
- label: 'Pattern Validation'
+ label: Pattern Validation
widget: markdown
pattern: ['# [a-zA-Z0-9]+', 'Must have a header']
required: false
@@ -311,26 +467,30 @@ collections:
description: Number widget
fields:
- name: required
- label: 'Required Validation'
+ label: Required Validation
widget: number
+ - name: with_default
+ label: Required With Default
+ widget: number
+ default: 5
- name: min
- label: 'Min Validation'
+ label: Min Validation
widget: number
min: 5
required: false
- name: max
- label: 'Max Validation'
+ label: Max Validation
widget: number
max: 10
required: false
- name: min_and_max
- label: 'Min and Max Validation'
+ label: Min and Max Validation
widget: number
min: 5
max: 10
required: false
- name: pattern
- label: 'Pattern Validation'
+ label: Pattern Validation
widget: number
pattern: ['[0-9]{3,}', 'Must be at least 3 digits']
required: false
@@ -346,12 +506,28 @@ collections:
- label: Number of posts on frontpage
name: front_limit
widget: number
- - label: Default Author
+ - label: Author
name: author
widget: string
- - label: Default Thumbnail
+ - label: Thumbnail
name: thumb
widget: image
+ - label: Required With Defaults
+ name: with_defaults
+ widget: object
+ fields:
+ - label: Number of posts on frontpage
+ name: front_limit
+ widget: number
+ default: 5
+ - label: Author
+ name: author
+ widget: string
+ default: Bob
+ - label: Thumbnail
+ name: thumb
+ widget: image
+ default: /assets/uploads/moby-dick.jpg
- label: Optional Validation
name: optional
widget: object
@@ -361,11 +537,31 @@ collections:
name: front_limit
widget: number
required: false
- - label: Default Author
+ - label: Author
name: author
widget: string
required: false
- - label: Default Thumbnail
+ - label: Thumbnail
+ name: thumb
+ widget: image
+ required: false
+ - label: With Hidden Field
+ name: hidden_field
+ widget: object
+ required: false
+ fields:
+ - name: layout
+ widget: hidden
+ default: post
+ - label: Number of posts on frontpage
+ name: front_limit
+ widget: number
+ required: false
+ - label: Author
+ name: author
+ widget: string
+ required: false
+ - label: Thumbnail
name: thumb
widget: image
required: false
@@ -385,6 +581,18 @@ collections:
- title
- body
value_field: title
+ - label: Required With Default
+ name: with_default
+ widget: relation
+ collection: posts
+ display_fields:
+ - title
+ - date
+ search_fields:
+ - title
+ - body
+ value_field: title
+ default: This is a YAML front matter post
- label: Optional Validation
name: optional
widget: relation
@@ -410,6 +618,22 @@ collections:
- title
- body
value_field: title
+ - label: Multiple With Default
+ name: multiple_with_default
+ widget: relation
+ multiple: true
+ required: false
+ collection: posts
+ default:
+ - This is a JSON front matter post
+ - This is a YAML front matter post
+ display_fields:
+ - title
+ - date
+ search_fields:
+ - title
+ - body
+ value_field: title
- name: select
label: Select
file: _widgets/select.json
@@ -422,6 +646,14 @@ collections:
- a
- b
- c
+ - label: Required With Default
+ name: with_default
+ widget: select
+ default: b
+ options:
+ - a
+ - b
+ - c
- label: Pattern Validation
name: pattern
widget: select
@@ -431,13 +663,39 @@ collections:
- c
pattern: ['[a-b]', 'Must be a or b']
required: false
+ - label: Number Value
+ name: number
+ widget: select
+ options:
+ - 1
+ - 2
+ - 3
+ - label: Number With Default
+ name: number_with_default
+ widget: select
+ default: 3
+ options:
+ - 1
+ - 2
+ - 3
- label: Value and Label
name: value_and_label
widget: select
options:
- value: a
label: A fancy label
- - value: b
+ - value: 2
+ label: Another fancy label
+ - value: c
+ label: And one more fancy label
+ - label: Value and Label With Default
+ name: value_and_label_with_default
+ widget: select
+ default: 2
+ options:
+ - value: a
+ label: A fancy label
+ - value: 2
label: Another fancy label
- value: c
label: And one more fancy label
@@ -451,6 +709,19 @@ collections:
pattern: ['[a-b]', 'Must be a or b']
multiple: true
required: false
+ - label: Multiple With Default
+ name: multiple_with_default
+ widget: select
+ default:
+ - b
+ - c
+ options:
+ - a
+ - b
+ - c
+ pattern: ['[a-b]', 'Must be a or b']
+ multiple: true
+ required: false
- label: Value and Label Multiple
name: value_and_label_multiple
widget: select
@@ -468,10 +739,14 @@ collections:
description: String widget
fields:
- name: required
- label: 'Required Validation'
+ label: Required Validation
widget: string
+ - name: with_default
+ label: Required With Default
+ widget: string
+ default: Default value
- name: pattern
- label: 'Pattern Validation'
+ label: Pattern Validation
widget: string
pattern: ['.{12,}', 'Must have at least 12 characters']
required: false
@@ -483,6 +758,10 @@ collections:
- name: required
label: 'Required Validation'
widget: text
+ - name: with_default
+ label: Required With Default
+ widget: text
+ default: Default value
- name: pattern
label: 'Pattern Validation'
widget: text
@@ -778,7 +1057,7 @@ collections:
widget: number
- label: Markdown
name: markdown
- widget: text
+ widget: markdown
- label: Datetime
name: datetime
widget: datetime
@@ -817,7 +1096,7 @@ collections:
widget: number
- label: Markdown
name: markdown
- widget: text
+ widget: markdown
- label: Datetime
name: datetime
widget: datetime
@@ -870,7 +1149,7 @@ collections:
widget: datetime
- label: Markdown
name: markdown
- widget: text
+ widget: markdown
- label: Type 3 Object
name: type_3_object
widget: object
diff --git a/core/src/actions/config.ts b/core/src/actions/config.ts
index f768ea89..0907c049 100644
--- a/core/src/actions/config.ts
+++ b/core/src/actions/config.ts
@@ -8,13 +8,11 @@ import { resolveBackend } from '../backend';
import validateConfig from '../constants/configSchema';
import { I18N, I18N_FIELD, I18N_STRUCTURE } from '../lib/i18n';
import { selectDefaultSortableFields } from '../lib/util/collection.util';
-import { getIntegrations, selectIntegration } from '../reducers/integrations';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type {
BaseField,
- Collection,
Config,
Field,
I18nInfo,
@@ -126,12 +124,6 @@ function throwOnMissingDefaultLocale(i18n?: I18nInfo) {
}
}
-function hasIntegration(config: Config, collection: Collection) {
- const integrations = getIntegrations(config);
- const integration = selectIntegration(integrations, collection.name, 'listEntries');
- return !!integration;
-}
-
export function applyDefaults(originalConfig: Config) {
return produce(originalConfig, config => {
config.slug = config.slug || {};
@@ -247,11 +239,7 @@ export function applyDefaults(originalConfig: Config) {
if (!collection.sortable_fields) {
collection.sortable_fields = {
- fields: selectDefaultSortableFields(
- collection,
- backend,
- hasIntegration(config, collection),
- ),
+ fields: selectDefaultSortableFields(collection, backend),
};
}
diff --git a/core/src/actions/entries.ts b/core/src/actions/entries.ts
index 32aa3539..df424670 100644
--- a/core/src/actions/entries.ts
+++ b/core/src/actions/entries.ts
@@ -3,12 +3,11 @@ import isEqual from 'lodash/isEqual';
import { currentBackend } from '../backend';
import { SORT_DIRECTION_ASCENDING } from '../constants';
import ValidationErrorTypes from '../constants/validationErrorTypes';
-import { getSearchIntegrationProvider } from '../integrations';
import { duplicateDefaultI18nFields, hasI18n, I18N_FIELD, serializeI18n } from '../lib/i18n';
import { serializeValues } from '../lib/serializeEntryValues';
import { Cursor } from '../lib/util';
import { selectFields, updateFieldByKey } from '../lib/util/collection.util';
-import { selectIntegration, selectPublishedSlugs } from '../reducers';
+import { selectPublishedSlugs } from '../reducers';
import { selectCollectionEntriesCursor } from '../reducers/cursors';
import { selectEntriesSortFields, selectIsFetching } from '../reducers/entries';
import { navigateToEntry } from '../routing/history';
@@ -277,17 +276,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, integration)
- : backend;
-
- if (!provider) {
- return [];
- }
-
- const entries = await provider.listAllEntries(collection);
- return entries;
+ return backend.listAllEntries(collection);
}
export function sortByField(
@@ -680,14 +669,6 @@ 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, integration)
- : backend;
-
- if (!provider) {
- throw new Error('Provider not found');
- }
const append = !!(page && !isNaN(page) && page > 0);
dispatch(entriesLoading(collection));
@@ -701,8 +682,8 @@ export function loadEntries(collection: Collection, page = 0) {
entries: Entry[];
} = await (loadAllEntries
? // nested collections require all entries to construct the tree
- provider.listAllEntries(collection).then((entries: Entry[]) => ({ entries }))
- : provider.listEntries(collection, page));
+ backend.listAllEntries(collection).then((entries: Entry[]) => ({ entries }))
+ : backend.listEntries(collection));
const cleanResponse = {
...response,
diff --git a/core/src/actions/search.ts b/core/src/actions/search.ts
index eba5e056..676bc1e9 100644
--- a/core/src/actions/search.ts
+++ b/core/src/actions/search.ts
@@ -1,8 +1,6 @@
import isEqual from 'lodash/isEqual';
import { currentBackend } from '../backend';
-import { getSearchIntegrationProvider } from '../integrations';
-import { selectIntegration } from '../reducers';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
@@ -103,42 +101,25 @@ export function searchEntries(searchTerm: string, searchCollections: string[], p
const backend = currentBackend(configState.config);
const allCollections = searchCollections || Object.keys(state.collections);
- const collections = allCollections.filter(collection =>
- selectIntegration(state, collection, 'search'),
- );
- const integration = selectIntegration(state, collections[0], 'search');
// avoid duplicate searches
if (
search.isFetching &&
search.term === searchTerm &&
- isEqual(allCollections, search.collections) &&
- // if an integration doesn't exist, 'page' is not used
- (search.page === page || !integration)
+ isEqual(allCollections, search.collections)
) {
return;
}
dispatch(searchingEntries(searchTerm, allCollections, page));
- const searchPromise = integration
- ? getSearchIntegrationProvider(state.integrations, integration)?.search(
- collections,
- searchTerm,
- page,
- )
- : backend.search(
- Object.entries(state.collections)
- .filter(([key, _value]) => allCollections.indexOf(key) !== -1)
- .map(([_key, value]) => value),
- searchTerm,
- );
-
try {
- const response = await searchPromise;
- if (!response) {
- return dispatch(searchFailure(new Error(`No integration found for name "${integration}"`)));
- }
+ const response = await backend.search(
+ Object.entries(state.collections)
+ .filter(([key, _value]) => allCollections.indexOf(key) !== -1)
+ .map(([_key, value]) => value),
+ searchTerm,
+ );
return dispatch(searchSuccess(response.entries, page));
} catch (error: unknown) {
@@ -170,7 +151,6 @@ export function query(
}
const backend = currentBackend(configState.config);
- const integration = selectIntegration(state, collectionName, 'search');
const collection = Object.values(state.collections).find(
collection => collection.name === collectionName,
);
@@ -178,16 +158,14 @@ export function query(
return dispatch(queryFailure(new Error('Collection not found')));
}
- const queryPromise = integration
- ? getSearchIntegrationProvider(state.integrations, integration)?.searchBy(
- JSON.stringify(searchFields.map(f => `data.${f}`)),
- collectionName,
- searchTerm,
- )
- : backend.query(collection, searchFields, searchTerm, file, limit);
-
try {
- const response: SearchQueryResponse = await queryPromise;
+ const response: SearchQueryResponse = await backend.query(
+ collection,
+ searchFields,
+ searchTerm,
+ file,
+ limit,
+ );
return dispatch(querySuccess(namespace, response.hits));
} catch (error: unknown) {
console.error(error);
diff --git a/core/src/backend.ts b/core/src/backend.ts
index 01a9463e..56a42be8 100644
--- a/core/src/backend.ts
+++ b/core/src/backend.ts
@@ -46,6 +46,7 @@ import createEntry from './valueObjects/createEntry';
import type {
BackendClass,
BackendInitializer,
+ BaseField,
Collection,
CollectionFile,
Config,
@@ -60,6 +61,7 @@ import type {
ImplementationEntry,
SearchQueryResponse,
SearchResponse,
+ UnknownField,
User,
} from './interface';
import type { AllowedEvent } from './lib/registry';
@@ -109,7 +111,7 @@ function getEntryBackupKey(collectionName?: string, slug?: string) {
return `${baseKey}.${collectionName}${suffix}`;
}
-function getEntryField(field: string, entry: Entry): string {
+export function getEntryField(field: string, entry: Entry): string {
const value = get(entry.data, field);
if (value) {
return String(value);
@@ -210,9 +212,15 @@ export function mergeExpandedEntries(entries: (Entry & { field: string })[]) {
return Object.values(merged);
}
-function sortByScore(a: fuzzy.FilterResult, b: fuzzy.FilterResult) {
- if (a.score > b.score) return -1;
- if (a.score < b.score) return 1;
+export function sortByScore(a: fuzzy.FilterResult, b: fuzzy.FilterResult) {
+ if (a.score > b.score) {
+ return -1;
+ }
+
+ if (a.score < b.score) {
+ return 1;
+ }
+
return 0;
}
@@ -478,16 +486,16 @@ export class Backend {
// repeats the process. Once there is no available "next" action, it
// returns all the collected entries. Used to retrieve all entries
// for local searches and queries.
- async listAllEntries(collection: Collection) {
+ async listAllEntries(collection: Collection) {
if ('folder' in collection && collection.folder && this.implementation.allEntriesByFolder) {
- const depth = collectionDepth(collection);
- const extension = selectFolderEntryExtension(collection);
+ const depth = collectionDepth(collection as Collection);
+ const extension = selectFolderEntryExtension(collection as Collection);
return this.implementation
.allEntriesByFolder(collection.folder as string, extension, depth)
- .then(entries => this.processEntries(entries, collection));
+ .then(entries => this.processEntries(entries, collection as Collection));
}
- const response = await this.listEntries(collection);
+ const response = await this.listEntries(collection as Collection);
const { entries } = response;
let { cursor } = response;
while (cursor && cursor.actions?.has('next')) {
@@ -557,14 +565,14 @@ export class Backend {
return { entries: hits, pagination: 1 };
}
- async query(
- collection: Collection,
+ async query(
+ collection: Collection,
searchFields: string[],
searchTerm: string,
file?: string,
limit?: number,
): Promise {
- let entries = await this.listAllEntries(collection);
+ let entries = await this.listAllEntries(collection as Collection);
if (file) {
entries = entries.filter(e => e.slug === file);
}
@@ -1014,11 +1022,11 @@ export function resolveBackend(config?: Config) {
export const currentBackend = (function () {
let backend: Backend;
- return (config: Config) => {
+ return (config: Config) => {
if (backend) {
return backend;
}
- return (backend = resolveBackend(config));
+ return (backend = resolveBackend(config as Config));
};
})();
diff --git a/core/src/components/Editor/EditorControlPane/EditorControl.tsx b/core/src/components/Editor/EditorControlPane/EditorControl.tsx
index 3345fd85..1d4d6f6f 100644
--- a/core/src/components/Editor/EditorControlPane/EditorControl.tsx
+++ b/core/src/components/Editor/EditorControlPane/EditorControl.tsx
@@ -23,6 +23,7 @@ import useMemoCompare from '@staticcms/core/lib/hooks/useMemoCompare';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { resolveWidget } from '@staticcms/core/lib/registry';
import { getFieldLabel } from '@staticcms/core/lib/util/field.util';
+import { isNotNullish } from '@staticcms/core/lib/util/null.util';
import { validate } from '@staticcms/core/lib/util/validation.util';
import { selectFieldErrors } from '@staticcms/core/reducers/entryDraft';
import { selectIsLoadingAsset } from '@staticcms/core/reducers/medias';
@@ -213,8 +214,32 @@ const EditorControl = ({
const finalValue = useMemoCompare(value, isEqual);
+ const [version, setVersion] = useState(0);
+ useEffect(() => {
+ if (isNotNullish(finalValue)) {
+ return;
+ }
+
+ if ('default' in field && isNotNullish(!field.default)) {
+ if (widget.getDefaultValue) {
+ handleChangeDraftField(
+ widget.getDefaultValue(field.default, field as unknown as UnknownField),
+ );
+ } else {
+ handleChangeDraftField(field.default);
+ }
+ setVersion(version => version + 1);
+ return;
+ }
+
+ if (widget.getDefaultValue) {
+ handleChangeDraftField(widget.getDefaultValue(null, field as unknown as UnknownField));
+ setVersion(version => version + 1);
+ }
+ }, [field, finalValue, handleChangeDraftField, widget]);
+
return useMemo(() => {
- if (!collection || !entry || !config) {
+ if (!collection || !entry || !config || field.widget === 'hidden') {
return null;
}
@@ -222,7 +247,7 @@ const EditorControl = ({
<>
{createElement(widget.control, {
- key: id,
+ key: `${id}-${version}`,
collection,
config,
entry,
diff --git a/core/src/components/Editor/EditorControlPane/EditorControlPane.tsx b/core/src/components/Editor/EditorControlPane/EditorControlPane.tsx
index a42cbb7d..045f7de5 100644
--- a/core/src/components/Editor/EditorControlPane/EditorControlPane.tsx
+++ b/core/src/components/Editor/EditorControlPane/EditorControlPane.tsx
@@ -188,31 +188,29 @@ const EditorControlPane = ({
/>
) : null}
- {fields
- .filter(f => f.widget !== 'hidden')
- .map(field => {
- const isTranslatable = isFieldTranslatable(field, locale, i18n?.defaultLocale);
- const isDuplicate = isFieldDuplicate(field, locale, i18n?.defaultLocale);
- const isHidden = isFieldHidden(field, locale, i18n?.defaultLocale);
- const key = i18n ? `field-${locale}_${field.name}` : `field-${field.name}`;
+ {fields.map(field => {
+ const isTranslatable = isFieldTranslatable(field, locale, i18n?.defaultLocale);
+ const isDuplicate = isFieldDuplicate(field, locale, i18n?.defaultLocale);
+ const isHidden = isFieldHidden(field, locale, i18n?.defaultLocale);
+ const key = i18n ? `field-${locale}_${field.name}` : `field-${field.name}`;
- return (
- isFieldDuplicate(field, locale, i18n?.defaultLocale)}
- isFieldHidden={field => isFieldHidden(field, locale, i18n?.defaultLocale)}
- locale={locale}
- parentPath=""
- i18n={i18n}
- />
- );
- })}
+ return (
+ isFieldDuplicate(field, locale, i18n?.defaultLocale)}
+ isFieldHidden={field => isFieldHidden(field, locale, i18n?.defaultLocale)}
+ locale={locale}
+ parentPath=""
+ i18n={i18n}
+ />
+ );
+ })}
);
};
diff --git a/core/src/components/Editor/EditorPreviewPane/EditorPreview.tsx b/core/src/components/Editor/EditorPreviewPane/EditorPreview.tsx
index 19d712f9..86306f03 100644
--- a/core/src/components/Editor/EditorPreviewPane/EditorPreview.tsx
+++ b/core/src/components/Editor/EditorPreviewPane/EditorPreview.tsx
@@ -1,11 +1,7 @@
-import React from 'react';
import { styled } from '@mui/material/styles';
+import React from 'react';
-import type { Field, TemplatePreviewProps } from '@staticcms/core/interface';
-
-function isVisible(field: Field) {
- return field.widget !== 'hidden';
-}
+import type { TemplatePreviewProps } from '@staticcms/core/interface';
const PreviewContainer = styled('div')`
overflow-y: auto;
@@ -21,7 +17,7 @@ const Preview = ({ collection, fields, widgetFor }: TemplatePreviewProps) => {
return (
- {fields.filter(isVisible).map(field => (
+ {fields.map(field => (
{widgetFor(field.name)}
))}
diff --git a/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx b/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx
index 6439c6bc..aca7b0ce 100644
--- a/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx
+++ b/core/src/components/Editor/EditorPreviewPane/EditorPreviewPane.tsx
@@ -188,7 +188,7 @@ function isReactFragment(value: any): value is ReactFragment {
function getWidget(
config: Config,
- field: RenderedField,
+ field: RenderedField,
collection: Collection,
value: ValueOrNestedValue | ReactNode,
entry: Entry,
@@ -202,6 +202,10 @@ function getWidget(
const widget = resolveWidget(field.widget);
const key = idx ? field.name + '_' + idx : field.name;
+ if (field.widget === 'hidden' || !widget.preview) {
+ return null;
+ }
+
/**
* Use an HOC to provide conditional updates for all previews.
*/
@@ -214,7 +218,12 @@ function getWidget(
config={config}
collection={collection}
value={
- value && typeof value === 'object' && !isJsxElement(value) && !isReactFragment(value)
+ value &&
+ typeof value === 'object' &&
+ !Array.isArray(value) &&
+ field.name in value &&
+ !isJsxElement(value) &&
+ !isReactFragment(value)
? (value as Record)[field.name]
: value
}
diff --git a/core/src/extensions.ts b/core/src/extensions.ts
index 1a1928dd..10887345 100644
--- a/core/src/extensions.ts
+++ b/core/src/extensions.ts
@@ -35,21 +35,21 @@ export default function addExtensions() {
registerBackend('test-repo', TestBackend);
registerBackend('proxy', ProxyBackend);
registerWidget([
- StringWidget(),
- NumberWidget(),
- TextWidget(),
- ImageWidget(),
- FileWidget(),
- SelectWidget(),
- MarkdownWidget(),
- ListWidget(),
- ObjectWidget(),
- RelationWidget(),
BooleanWidget(),
- MapWidget(),
- DateTimeWidget(),
CodeWidget(),
ColorStringWidget(),
+ DateTimeWidget(),
+ FileWidget(),
+ ImageWidget(),
+ ListWidget(),
+ MapWidget(),
+ MarkdownWidget(),
+ NumberWidget(),
+ ObjectWidget(),
+ RelationWidget(),
+ SelectWidget(),
+ StringWidget(),
+ TextWidget(),
]);
Object.keys(locales).forEach(locale => {
diff --git a/core/src/integrations/index.ts b/core/src/integrations/index.ts
deleted file mode 100644
index 077420e6..00000000
--- a/core/src/integrations/index.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import Algolia from './providers/algolia/implementation';
-
-import type { AlgoliaConfig, SearchIntegrationProvider } from '../interface';
-
-interface IntegrationsConfig {
- providers?: {
- algolia?: AlgoliaConfig;
- };
-}
-
-interface Integrations {
- algolia?: Algolia;
-}
-
-export function resolveIntegrations(config: IntegrationsConfig | undefined) {
- const integrationInstances: Integrations = {};
-
- if (config?.providers?.algolia) {
- integrationInstances.algolia = new Algolia(config.providers.algolia);
- }
-
- return integrationInstances;
-}
-
-export const getSearchIntegrationProvider = (function () {
- let integrations: Integrations = {};
-
- return (config: IntegrationsConfig | undefined, provider: SearchIntegrationProvider) => {
- if (provider in (config?.providers ?? {}))
- if (integrations) {
- return integrations[provider];
- } else {
- integrations = resolveIntegrations(config);
- return integrations[provider];
- }
- };
-})();
diff --git a/core/src/integrations/providers/algolia/implementation.ts b/core/src/integrations/providers/algolia/implementation.ts
deleted file mode 100644
index ef8b0a8c..00000000
--- a/core/src/integrations/providers/algolia/implementation.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-import flatten from 'lodash/flatten';
-
-import { unsentRequest } from '@staticcms/core/lib/util';
-import { selectEntrySlug } from '@staticcms/core/lib/util/collection.util';
-import createEntry from '@staticcms/core/valueObjects/createEntry';
-
-import type { AlgoliaConfig, Collection, Entry, SearchResponse } from '@staticcms/core/interface';
-
-const { fetchWithTimeout: fetch } = unsentRequest;
-
-function getSlug(path: string): string {
- return (
- path
- .split('/')
- .pop()
- ?.replace(/\.[^.]+$/, '') ?? path
- );
-}
-
-interface EntriesCache {
- collection: Collection | null;
- page: number | null;
- entries: Entry[];
-}
-
-interface AlgoliaHits {
- nbPages?: number;
- page: number;
- hits: {
- path: string;
- slug: string;
- data: unknown;
- }[];
-}
-
-interface AlgoliaSearchResponse {
- results: AlgoliaHits[];
-}
-
-export default class Algolia {
- private applicationID: string;
- private apiKey: string;
- private indexPrefix: string;
- private searchURL: string;
- private entriesCache: EntriesCache;
-
- constructor(config: AlgoliaConfig) {
- if (config.applicationID == null || config.apiKey == null) {
- throw 'The Algolia search integration needs the credentials (applicationID and apiKey) in the integration configuration.';
- }
-
- this.applicationID = config.applicationID;
- this.apiKey = config.apiKey;
-
- const prefix = config.indexPrefix;
- this.indexPrefix = prefix ? `${prefix}-` : '';
-
- this.searchURL = `https://${this.applicationID}-dsn.algolia.net/1`;
-
- this.entriesCache = {
- collection: null,
- page: null,
- entries: [],
- };
- }
-
- requestHeaders(headers = {}) {
- return {
- 'X-Algolia-API-Key': this.apiKey,
- 'X-Algolia-Application-Id': this.applicationID,
- 'Content-Type': 'application/json',
- ...headers,
- };
- }
-
- 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;
- }
-
- request(
- path: string,
- options: RequestInit & {
- params?: Record;
- },
- ) {
- const headers = this.requestHeaders(options.headers || {});
- const url = this.urlFor(path, options.params);
- return fetch(url, { ...options, headers }).then(response => {
- const contentType = response.headers.get('Content-Type');
- if (contentType && contentType.match(/json/)) {
- return this.parseJsonResponse(response);
- }
-
- return response.text();
- });
- }
-
- search(collections: string[], searchTerm: string, page: number): Promise {
- const searchCollections = collections.map(collection => ({
- indexName: `${this.indexPrefix}${collection}`,
- params: `query=${searchTerm}&page=${page}`,
- }));
-
- return this.request(`${this.searchURL}/indexes/*/queries`, {
- method: 'POST',
- body: JSON.stringify({ requests: searchCollections }),
- }).then((response: AlgoliaSearchResponse) => {
- const entries = response.results.map((result, index) =>
- result.hits.map(hit => {
- const slug = getSlug(hit.path);
- return createEntry(collections[index], slug, hit.path, { data: hit.data, partial: true });
- }),
- );
-
- return { entries: flatten(entries), pagination: page };
- });
- }
-
- searchBy(field: string, collection: string, query: string) {
- return this.request(`${this.searchURL}/indexes/${this.indexPrefix}${collection}`, {
- params: {
- restrictSearchableAttributes: field,
- query,
- },
- });
- }
-
- listEntries(collection: Collection, page: number) {
- if (this.entriesCache.collection === collection && this.entriesCache.page === page) {
- return Promise.resolve({ page: this.entriesCache.page, entries: this.entriesCache.entries });
- } else {
- return this.request(`${this.searchURL}/indexes/${this.indexPrefix}${collection.name}`, {
- params: { page: `${page}` },
- }).then((response: AlgoliaHits) => {
- const entries = response.hits.map(hit => {
- const slug = selectEntrySlug(collection, hit.path);
- return createEntry(collection.name, slug, hit.path, {
- data: hit.data,
- partial: true,
- });
- });
- this.entriesCache = { collection, page: response.page, entries };
- return { entries, page: response.page };
- });
- }
- }
-
- async listAllEntries(collection: Collection) {
- const params = {
- hitsPerPage: '1000',
- };
- let response: AlgoliaHits = await this.request(
- `${this.searchURL}/indexes/${this.indexPrefix}${collection.name}`,
- { params },
- );
- let { nbPages = 0, hits, page } = response;
- page = page + 1;
- while (page < nbPages) {
- response = await this.request(
- `${this.searchURL}/indexes/${this.indexPrefix}${collection.name}`,
- {
- params: { ...params, page: `${page}` },
- },
- );
- hits = [...hits, ...response.hits];
- page = page + 1;
- }
- const entries = hits.map(hit => {
- const slug = selectEntrySlug(collection, hit.path);
- return createEntry(collection.name, slug, hit.path, {
- data: hit.data,
- partial: true,
- });
- });
-
- return entries;
- }
-
- getEntry(collection: Collection, slug: string) {
- return this.searchBy('slug', collection.name, slug).then((response: AlgoliaHits) => {
- const entry = response.hits.filter(hit => hit.slug === slug)[0];
- return createEntry(collection.name, slug, entry.path, {
- data: entry.data,
- partial: true,
- });
- });
- }
-}
diff --git a/core/src/interface.ts b/core/src/interface.ts
index 483fa2bd..f0d17b07 100644
--- a/core/src/interface.ts
+++ b/core/src/interface.ts
@@ -313,6 +313,7 @@ export type TemplatePreviewComponent<
export interface WidgetOptions {
validator?: Widget['validator'];
getValidValue?: Widget['getValidValue'];
+ getDefaultValue?: Widget['getDefaultValue'];
schema?: Widget['schema'];
}
@@ -321,6 +322,7 @@ export interface Widget {
preview?: WidgetPreviewComponent;
validator: FieldValidationMethod;
getValidValue: (value: T | undefined | null) => T | undefined | null;
+ getDefaultValue?: (defaultValue: T | undefined | null, field: F) => T;
schema?: PropertiesSchema;
}
diff --git a/core/src/lib/hooks/useMediaAsset.ts b/core/src/lib/hooks/useMediaAsset.ts
index 3b5e18f0..df17b025 100644
--- a/core/src/lib/hooks/useMediaAsset.ts
+++ b/core/src/lib/hooks/useMediaAsset.ts
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { getAsset } from '@staticcms/core/actions/media';
import { useAppDispatch } from '@staticcms/core/store/hooks';
+import { isNotEmpty } from '../util/string.util';
import type { Collection, Entry, FileOrImageField, MarkdownField } from '@staticcms/core/interface';
@@ -23,5 +24,5 @@ export default function useMediaAsset(
schema,
validator = () => false,
getValidValue = (value: T | null | undefined) => value,
+ getDefaultValue,
}: WidgetOptions = {},
): void {
if (Array.isArray(name)) {
@@ -162,6 +163,7 @@ export function registerWidget(
preview: preview as Widget['preview'],
validator: validator as Widget['validator'],
getValidValue: getValidValue as Widget['getValidValue'],
+ getDefaultValue: getDefaultValue as Widget['getDefaultValue'],
schema,
};
}
@@ -173,6 +175,7 @@ export function registerWidget(
options: {
validator = () => false,
getValidValue = (value: T | undefined | null) => value,
+ getDefaultValue,
schema,
} = {},
} = name;
@@ -190,6 +193,7 @@ export function registerWidget(
preview,
validator,
getValidValue,
+ getDefaultValue,
schema,
} as unknown as Widget;
} else {
diff --git a/core/src/lib/util/collection.util.ts b/core/src/lib/util/collection.util.ts
index e9577430..ed22cb5c 100644
--- a/core/src/lib/util/collection.util.ts
+++ b/core/src/lib/util/collection.util.ts
@@ -136,21 +136,17 @@ export function selectEntryCollectionTitle(collection: Collection, entry: Entry)
return result;
}
-export function selectDefaultSortableFields(
- collection: Collection,
- backend: Backend,
- hasIntegration: boolean,
-) {
+export function selectDefaultSortableFields(collection: Collection, backend: Backend) {
let defaultSortable = SORTABLE_FIELDS.map((type: string) => {
const field = selectInferedField(collection, type);
- if (backend.isGitBackend() && type === 'author' && !field && !hasIntegration) {
+ if (backend.isGitBackend() && type === 'author' && !field) {
// default to commit author if not author field is found
return COMMIT_AUTHOR;
}
return field;
}).filter(Boolean);
- if (backend.isGitBackend() && !hasIntegration) {
+ if (backend.isGitBackend()) {
// always have commit date by default
defaultSortable = [COMMIT_DATE, ...defaultSortable];
}
diff --git a/core/src/reducers/collections.ts b/core/src/reducers/collections.ts
index 62285c46..67996f7f 100644
--- a/core/src/reducers/collections.ts
+++ b/core/src/reducers/collections.ts
@@ -2,6 +2,7 @@ import { CONFIG_SUCCESS } from '../actions/config';
import type { ConfigAction } from '../actions/config';
import type { Collection, Collections } from '../interface';
+import type { RootState } from '../store';
export type CollectionsState = Collections;
@@ -25,3 +26,7 @@ function collections(
}
export default collections;
+
+export const selectCollection = (collectionName: string) => (state: RootState) => {
+ return Object.values(state.collections).find(collection => collection.name === collectionName);
+};
diff --git a/core/src/reducers/index.ts b/core/src/reducers/index.ts
index 10f3b13c..0ee6a90d 100644
--- a/core/src/reducers/index.ts
+++ b/core/src/reducers/index.ts
@@ -5,7 +5,6 @@ import cursors from './cursors';
import entries, * as fromEntries from './entries';
import entryDraft from './entryDraft';
import globalUI from './globalUI';
-import integrations, * as fromIntegrations from './integrations';
import mediaLibrary from './mediaLibrary';
import medias from './medias';
import scroll from './scroll';
@@ -14,7 +13,6 @@ import status from './status';
import type { Collection } from '../interface';
import type { RootState } from '../store';
-import type { IntegrationHooks } from './integrations';
const reducers = {
auth,
@@ -24,7 +22,6 @@ const reducers = {
entries,
entryDraft,
globalUI,
- integrations,
mediaLibrary,
medias,
scroll,
@@ -55,11 +52,3 @@ export function selectSearchedEntries(state: RootState, availableCollections: st
.filter(entryId => availableCollections.indexOf(entryId!.collection) !== -1)
.map(entryId => fromEntries.selectEntry(state.entries, entryId!.collection, entryId!.slug));
}
-
-export function selectIntegration(
- state: RootState,
- collection: string | null,
- hook: K,
-): IntegrationHooks[K] | false {
- return fromIntegrations.selectIntegration(state.integrations, collection, hook);
-}
diff --git a/core/src/reducers/integrations.ts b/core/src/reducers/integrations.ts
deleted file mode 100644
index 0f79a239..00000000
--- a/core/src/reducers/integrations.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import get from 'lodash/get';
-
-import { CONFIG_SUCCESS } from '../actions/config';
-
-import type { ConfigAction } from '../actions/config';
-import type { AlgoliaConfig, Config, SearchIntegrationProvider } from '../interface';
-
-export interface IntegrationHooks {
- search?: SearchIntegrationProvider;
- listEntries?: SearchIntegrationProvider;
-}
-
-export interface IntegrationsState {
- providers: {
- algolia?: AlgoliaConfig;
- };
- hooks: IntegrationHooks;
- collectionHooks: Record;
-}
-
-export function getIntegrations(config: Config): IntegrationsState {
- const integrations = config.integrations || [];
- const newState = integrations.reduce(
- (acc, integration) => {
- const { collections, ...providerData } = integration;
- const integrationCollections =
- collections === '*' ? config.collections.map(collection => collection.name) : collections;
-
- if (providerData.provider === 'algolia') {
- acc.providers[providerData.provider] = providerData;
-
- if (!collections) {
- providerData.hooks.forEach(hook => (acc.hooks[hook] = providerData.provider));
- return acc;
- }
-
- integrationCollections?.forEach(collection => {
- providerData.hooks.forEach(
- hook => (acc.collectionHooks[collection][hook] = providerData.provider),
- );
- });
- }
- return acc;
- },
- { providers: {}, hooks: {} } as IntegrationsState,
- );
-
- return newState;
-}
-
-const defaultState: IntegrationsState = { providers: {}, hooks: {}, collectionHooks: {} };
-
-function integrations(
- state: IntegrationsState = defaultState,
- action: ConfigAction,
-): IntegrationsState {
- switch (action.type) {
- case CONFIG_SUCCESS: {
- return getIntegrations(action.payload);
- }
- default:
- return state;
- }
-}
-
-export function selectIntegration(
- state: IntegrationsState,
- collection: string | null,
- hook: string,
-): IntegrationHooks[K] | false {
- return collection
- ? get(state, ['collectionHooks', collection, hook], false)
- : get(state, ['hooks', hook], false);
-}
-
-export default integrations;
diff --git a/core/src/widgets/boolean/BooleanControl.tsx b/core/src/widgets/boolean/BooleanControl.tsx
index cb0690a6..c3b1b743 100644
--- a/core/src/widgets/boolean/BooleanControl.tsx
+++ b/core/src/widgets/boolean/BooleanControl.tsx
@@ -1,7 +1,7 @@
import { red } from '@mui/material/colors';
import FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useState } from 'react';
import type { BooleanField, WidgetControlProps } from '@staticcms/core/interface';
import type { ChangeEvent, FC } from 'react';
@@ -22,15 +22,6 @@ const BooleanControl: FC> = ({
[onChange],
);
- useEffect(() => {
- if (typeof internalValue !== 'boolean') {
- setInternalValue(false);
- setTimeout(() => {
- onChange(false);
- });
- }
- }, [internalValue, onChange]);
-
return (
=> {
return {
name: 'boolean',
controlComponent,
+ options: {
+ schema,
+ getDefaultValue: (defaultValue: boolean | undefined | null) => {
+ return typeof defaultValue === 'boolean' ? defaultValue : false;
+ },
+ },
};
};
-export { controlComponent as BooleanControl };
+export { controlComponent as BooleanControl, schema as BooleanSchema };
export default BooleanWidget;
diff --git a/core/src/widgets/boolean/schema.ts b/core/src/widgets/boolean/schema.ts
new file mode 100644
index 00000000..858da99c
--- /dev/null
+++ b/core/src/widgets/boolean/schema.ts
@@ -0,0 +1,5 @@
+export default {
+ properties: {
+ default: { type: 'boolean' },
+ },
+};
diff --git a/core/src/widgets/code/CodeControl.tsx b/core/src/widgets/code/CodeControl.tsx
index cb71536c..db4ee761 100644
--- a/core/src/widgets/code/CodeControl.tsx
+++ b/core/src/widgets/code/CodeControl.tsx
@@ -1,17 +1,22 @@
import { styled } from '@mui/material/styles';
import { loadLanguage } from '@uiw/codemirror-extensions-langs';
import CodeMirror from '@uiw/react-codemirror';
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar';
import Outline from '@staticcms/core/components/UI/Outline';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
+import { isEmpty } from '@staticcms/core/lib/util/string.util';
import transientOptions from '@staticcms/core/lib/util/transientOptions';
import languages from './data/languages';
import SettingsButton from './SettingsButton';
import SettingsPane from './SettingsPane';
-import type { CodeField, WidgetControlProps } from '@staticcms/core/interface';
+import type {
+ CodeField,
+ ProcessedCodeLanguage,
+ WidgetControlProps,
+} from '@staticcms/core/interface';
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
import type { FC } from 'react';
@@ -73,11 +78,7 @@ const CodeControl: FC Boolean(!field.output_code_only), [field.output_code_only]);
const [internalValue, setInternalValue] = useState(value ?? '');
- const [lang, setLang] = useState(() => {
- return valueIsMap && typeof internalValue !== 'string'
- ? internalValue && internalValue[keys.lang]
- : field.default_language ?? '';
- });
+ const [lang, setLang] = useState(null);
const [collapsed, setCollapsed] = useState(false);
const [hasFocus, setHasFocus] = useState(false);
@@ -105,20 +106,20 @@ const CodeControl: FC {
if (valueIsMap) {
handleOnChange({
- ...(typeof internalValue !== 'string' ? internalValue : {}),
+ lang: lang?.label ?? '',
code: newValue,
});
}
handleOnChange(newValue);
},
- [handleOnChange, internalValue, valueIsMap],
+ [handleOnChange, lang?.label, valueIsMap],
);
const loadedLangExtension = useMemo(() => {
if (!lang) {
return null;
}
- return loadLanguage(lang as LanguageName);
+ return loadLanguage(lang.codemirror_mode as LanguageName);
}, [lang]);
const extensions = useMemo(() => {
@@ -154,9 +155,29 @@ const CodeControl: FC
- valueToOption({ name: language.codemirror_mode, label: language.label }),
- );
+ const availableLanguages = languages.map(language => valueToOption(language.label));
+
+ const handleSetLanguage = useCallback((langIdentifier: string) => {
+ const language = languages.find(language => language.identifiers.includes(langIdentifier));
+ if (language) {
+ setLang(language);
+ }
+ }, []);
+
+ useEffect(() => {
+ let langIdentifier: string;
+ if (typeof internalValue !== 'string') {
+ langIdentifier = internalValue[keys.lang];
+ } else {
+ langIdentifier = internalValue;
+ }
+
+ if (isEmpty(langIdentifier)) {
+ return;
+ }
+
+ handleSetLanguage(langIdentifier);
+ }, [field.default_language, handleSetLanguage, internalValue, keys.lang, valueIsMap]);
return (
@@ -168,9 +189,9 @@ const CodeControl: FC setLang(newLang)}
+ onChangeLanguage={handleSetLanguage}
/>
)
) : null}
diff --git a/core/src/widgets/code/index.ts b/core/src/widgets/code/index.ts
index 83884866..d3514392 100644
--- a/core/src/widgets/code/index.ts
+++ b/core/src/widgets/code/index.ts
@@ -11,6 +11,29 @@ const CodeWidget = (): WidgetParam {
+ if (field.output_code_only) {
+ return String(defaultValue);
+ }
+
+ const langKey = field.keys?.['lang'] ?? 'lang';
+ const codeKey = field.keys?.['code'] ?? 'code';
+
+ if (typeof defaultValue === 'string') {
+ return {
+ [langKey]: field.default_language ?? '',
+ [codeKey]: defaultValue,
+ };
+ }
+
+ return {
+ [langKey]: field.default_language ?? defaultValue?.[langKey] ?? '',
+ [codeKey]: defaultValue?.[codeKey] ?? '',
+ };
+ },
},
};
};
diff --git a/core/src/widgets/code/schema.ts b/core/src/widgets/code/schema.ts
index 204c641e..e5575750 100644
--- a/core/src/widgets/code/schema.ts
+++ b/core/src/widgets/code/schema.ts
@@ -7,5 +7,8 @@ export default {
type: 'object',
properties: { code: { type: 'string' }, lang: { type: 'string' } },
},
+ default: {
+ oneOf: [{ type: 'string' }, { type: 'object' }],
+ },
},
};
diff --git a/core/src/widgets/colorstring/index.ts b/core/src/widgets/colorstring/index.ts
index 97e1b3d2..2608dd24 100644
--- a/core/src/widgets/colorstring/index.ts
+++ b/core/src/widgets/colorstring/index.ts
@@ -1,5 +1,6 @@
import controlComponent from './ColorControl';
import previewComponent from './ColorPreview';
+import schema from './schema';
import type { ColorField, WidgetParam } from '@staticcms/core/interface';
@@ -8,9 +9,16 @@ const ColorWidget = (): WidgetParam => {
name: 'color',
controlComponent,
previewComponent,
+ options: {
+ schema,
+ },
};
};
-export { controlComponent as ColorControl, previewComponent as ColorPreview };
+export {
+ controlComponent as ColorControl,
+ previewComponent as ColorPreview,
+ schema as ColorSchema,
+};
export default ColorWidget;
diff --git a/core/src/widgets/colorstring/schema.ts b/core/src/widgets/colorstring/schema.ts
new file mode 100644
index 00000000..c1d7002a
--- /dev/null
+++ b/core/src/widgets/colorstring/schema.ts
@@ -0,0 +1,5 @@
+export default {
+ properties: {
+ default: { type: 'string' },
+ },
+};
diff --git a/core/src/widgets/datetime/DateTimeControl.tsx b/core/src/widgets/datetime/DateTimeControl.tsx
index e5ae011d..e3c0fef4 100644
--- a/core/src/widgets/datetime/DateTimeControl.tsx
+++ b/core/src/widgets/datetime/DateTimeControl.tsx
@@ -11,13 +11,18 @@ import formatDate from 'date-fns/format';
import formatISO from 'date-fns/formatISO';
import parse from 'date-fns/parse';
import parseISO from 'date-fns/parseISO';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import type { DateTimeField, TranslatedProps, WidgetControlProps } from '@staticcms/core/interface';
import type { FC, MouseEvent } from 'react';
+export function localToUTC(dateTime: Date, timezoneOffset: number) {
+ const utcFromLocal = new Date(dateTime.getTime() - timezoneOffset);
+ return utcFromLocal;
+}
+
const StyledNowButton = styled('div')`
width: fit-content;
`;
@@ -77,14 +82,6 @@ const DateTimeControl: FC> = ({
const timezoneOffset = useMemo(() => new Date().getTimezoneOffset() * 60000, []);
- const localToUTC = useCallback(
- (dateTime: Date) => {
- const utcFromLocal = new Date(dateTime.getTime() - timezoneOffset);
- return utcFromLocal;
- },
- [timezoneOffset],
- );
-
const inputFormat = useMemo(() => {
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
const formatParts: string[] = [];
@@ -105,13 +102,13 @@ const DateTimeControl: FC> = ({
}, [dateFormat, timeFormat]);
const defaultValue = useMemo(() => {
- const today = field.picker_utc ? localToUTC(new Date()) : new Date();
+ const today = field.picker_utc ? localToUTC(new Date(), timezoneOffset) : new Date();
return field.default === undefined
? format
? formatDate(today, format)
: formatDate(today, inputFormat)
: field.default;
- }, [field.default, field.picker_utc, format, inputFormat, localToUTC]);
+ }, [field.default, field.picker_utc, format, inputFormat, timezoneOffset]);
const [internalValue, setInternalValue] = useState(value);
@@ -138,7 +135,7 @@ const DateTimeControl: FC> = ({
return;
}
- const adjustedValue = field.picker_utc ? localToUTC(datetime) : datetime;
+ const adjustedValue = field.picker_utc ? localToUTC(datetime, timezoneOffset) : datetime;
let formattedValue: string;
if (format) {
@@ -149,20 +146,9 @@ const DateTimeControl: FC> = ({
setInternalValue(formattedValue);
onChange(formattedValue);
},
- [defaultValue, field.picker_utc, format, localToUTC, onChange],
+ [defaultValue, field.picker_utc, format, onChange, timezoneOffset],
);
- useEffect(() => {
- if (isNotEmpty(internalValue)) {
- return;
- }
-
- setInternalValue(defaultValue);
- setTimeout(() => {
- onChange(defaultValue);
- });
- }, [defaultValue, internalValue, onChange]);
-
const dateTimePicker = useMemo(() => {
if (!internalValue) {
return null;
diff --git a/core/src/widgets/datetime/index.tsx b/core/src/widgets/datetime/index.tsx
index a84a160f..61bc9b6f 100644
--- a/core/src/widgets/datetime/index.tsx
+++ b/core/src/widgets/datetime/index.tsx
@@ -1,4 +1,8 @@
-import controlComponent from './DateTimeControl';
+import formatDate from 'date-fns/format';
+
+import { isNotNullish } from '@staticcms/core/lib/util/null.util';
+import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
+import controlComponent, { localToUTC } from './DateTimeControl';
import previewComponent from './DateTimePreview';
import schema from './schema';
@@ -11,6 +15,38 @@ const DateTimeWidget = (): WidgetParam => {
previewComponent,
options: {
schema,
+ getDefaultValue: (defaultValue: string | null | undefined, field: DateTimeField) => {
+ if (isNotNullish(defaultValue)) {
+ return defaultValue;
+ }
+
+ const timezoneOffset = new Date().getTimezoneOffset() * 60000;
+ const format = field.format;
+
+ // dateFormat and timeFormat are strictly for modifying input field with the date/time pickers
+ const dateFormat: string | boolean = field.date_format ?? true;
+ // show time-picker? false hides it, true shows it using default format
+ const timeFormat: string | boolean = field.time_format ?? true;
+
+ let inputFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
+ if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
+ const formatParts: string[] = [];
+ if (typeof dateFormat === 'string' && isNotEmpty(dateFormat)) {
+ formatParts.push(dateFormat);
+ }
+
+ if (typeof timeFormat === 'string' && isNotEmpty(timeFormat)) {
+ formatParts.push(timeFormat);
+ }
+
+ if (formatParts.length > 0) {
+ inputFormat = formatParts.join(' ');
+ }
+ }
+
+ const today = field.picker_utc ? localToUTC(new Date(), timezoneOffset) : new Date();
+ return format ? formatDate(today, format) : formatDate(today, inputFormat);
+ },
},
};
};
diff --git a/core/src/widgets/datetime/schema.ts b/core/src/widgets/datetime/schema.ts
index 8e32e7ab..8b92e769 100644
--- a/core/src/widgets/datetime/schema.ts
+++ b/core/src/widgets/datetime/schema.ts
@@ -4,5 +4,6 @@ export default {
date_format: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
time_format: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
picker_utc: { type: 'boolean' },
+ default: { type: 'string' },
},
};
diff --git a/core/src/widgets/file/schema.ts b/core/src/widgets/file/schema.ts
index 15dbf814..433db5ba 100644
--- a/core/src/widgets/file/schema.ts
+++ b/core/src/widgets/file/schema.ts
@@ -1,5 +1,16 @@
export default {
properties: {
allow_multiple: { type: 'boolean' },
+ default: {
+ oneOf: [
+ { type: 'string' },
+ {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ ],
+ },
},
};
diff --git a/core/src/widgets/file/withFileControl.tsx b/core/src/widgets/file/withFileControl.tsx
index e21613e7..a5180f38 100644
--- a/core/src/widgets/file/withFileControl.tsx
+++ b/core/src/widgets/file/withFileControl.tsx
@@ -8,14 +8,16 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar';
import Outline from '@staticcms/core/components/UI/Outline';
import { borders, effects, lengths, shadows } from '@staticcms/core/components/UI/styles';
+import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { basename, transientOptions } from '@staticcms/core/lib/util';
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import type {
+ Collection,
+ Entry,
FileOrImageField,
- GetAssetFunction,
WidgetControlProps,
} from '@staticcms/core/interface';
import type { FC, MouseEvent, MouseEventHandler } from 'react';
@@ -102,11 +104,16 @@ const StyledImage = styled('img')`
`;
interface ImageProps {
- src: string;
+ value: string;
+ collection: Collection;
+ field: FileOrImageField;
+ entry: Entry;
}
-const Image: FC = ({ src }) => {
- return ;
+const Image: FC = ({ value, collection, field, entry }) => {
+ const assetSource = useMediaAsset(value, collection, field, entry);
+
+ return ;
};
interface SortableImageButtonsProps {
@@ -129,34 +136,25 @@ const SortableImageButtons: FC = ({ onRemove, onRepla
interface SortableImageProps {
itemValue: string;
- getAsset: GetAssetFunction;
+ collection: Collection;
field: FileOrImageField;
+ entry: Entry;
onRemove: MouseEventHandler;
onReplace: MouseEventHandler;
}
const SortableImage: FC = ({
itemValue,
- getAsset,
+ collection,
field,
+ entry,
onRemove,
onReplace,
}: SortableImageProps) => {
- const [assetSource, setAssetSource] = useState('');
- useEffect(() => {
- const getImage = async () => {
- const asset = (await getAsset(itemValue, field))?.toString() ?? '';
- setAssetSource(asset);
- };
-
- getImage();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [itemValue]);
-
return (
-
+
{
const FileControl: FC> = memo(
({
value,
+ collection,
field,
+ entry,
onChange,
openMediaLibrary,
clearMediaControl,
removeMediaControl,
- getAsset,
hasErrors,
t,
}) => {
@@ -343,23 +342,6 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
);
}, []);
- const [assetSource, setAssetSource] = useState('');
- useEffect(() => {
- if (Array.isArray(internalValue)) {
- return;
- }
-
- const getImage = async () => {
- const newValue = (await getAsset(internalValue, field))?.toString() ?? '';
- if (newValue !== internalValue) {
- setAssetSource(newValue);
- }
- };
-
- getImage();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [internalValue]);
-
const renderedImagesLinks = useMemo(() => {
if (forImage) {
if (!internalValue) {
@@ -373,8 +355,9 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
@@ -385,7 +368,13 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
return (
-
+
);
}
@@ -403,7 +392,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
}
return {renderFileLink(internalValue)};
- }, [assetSource, field, getAsset, internalValue, onRemoveOne, onReplaceOne, renderFileLink]);
+ }, [collection, entry, field, internalValue, onRemoveOne, onReplaceOne, renderFileLink]);
const content = useMemo(() => {
const subject = forImage ? 'image' : 'file';
diff --git a/core/src/widgets/image/schema.ts b/core/src/widgets/image/schema.ts
index 15dbf814..433db5ba 100644
--- a/core/src/widgets/image/schema.ts
+++ b/core/src/widgets/image/schema.ts
@@ -1,5 +1,16 @@
export default {
properties: {
allow_multiple: { type: 'boolean' },
+ default: {
+ oneOf: [
+ { type: 'string' },
+ {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ ],
+ },
},
};
diff --git a/core/src/widgets/list/ListControl.tsx b/core/src/widgets/list/ListControl.tsx
index 101dfd44..270a1f7f 100644
--- a/core/src/widgets/list/ListControl.tsx
+++ b/core/src/widgets/list/ListControl.tsx
@@ -311,27 +311,34 @@ const ListControl: FC> = ({
- {internalValue.map((item, index) => (
- }
- i18n={i18n}
- />
- ))}
+ {internalValue.map((item, index) => {
+ const key = keys[index];
+ if (!key) {
+ return null;
+ }
+
+ return (
+ }
+ i18n={i18n}
+ />
+ );
+ })}
diff --git a/core/src/widgets/map/MapPreview.tsx b/core/src/widgets/map/MapPreview.tsx
index ad6d3404..6937ba28 100644
--- a/core/src/widgets/map/MapPreview.tsx
+++ b/core/src/widgets/map/MapPreview.tsx
@@ -6,7 +6,7 @@ import type { MapField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { FC } from 'react';
const MapPreview: FC> = ({ value }) => {
- return {value ? value.toString() : null};
+ return {value};
};
export default MapPreview;
diff --git a/core/src/widgets/markdown/MarkdownControl.tsx b/core/src/widgets/markdown/MarkdownControl.tsx
index 2c1a79d2..894c36d2 100644
--- a/core/src/widgets/markdown/MarkdownControl.tsx
+++ b/core/src/widgets/markdown/MarkdownControl.tsx
@@ -58,7 +58,7 @@ const MarkdownControl: FC> = ({
const handleOnChange = useCallback(
(slateValue: MdValue) => {
const newValue = slateValue.map(v => serialize(v as BlockType | LeafType)).join('\n');
- console.log('[Plate] slateValue', slateValue, 'newMarkdownValue', newValue);
+ // console.log('[Plate] slateValue', slateValue, 'newMarkdownValue', newValue);
if (newValue !== internalValue) {
setInternalValue(newValue);
onChange(newValue);
@@ -73,7 +73,7 @@ const MarkdownControl: FC> = ({
const [slateValue, loaded] = useMarkdownToSlate(internalValue);
- console.log('[Plate] slateValue', slateValue);
+ // console.log('[Plate] slateValue', slateValue);
return useMemo(
() => (
diff --git a/core/src/widgets/markdown/index.ts b/core/src/widgets/markdown/index.ts
index eae41944..e05fd927 100644
--- a/core/src/widgets/markdown/index.ts
+++ b/core/src/widgets/markdown/index.ts
@@ -1,5 +1,6 @@
import controlComponent from './MarkdownControl';
import previewComponent from './MarkdownPreview';
+import schema from './schema';
import type { MarkdownField, WidgetParam } from '@staticcms/core/interface';
@@ -8,10 +9,17 @@ const MarkdownWidget = (): WidgetParam => {
name: 'markdown',
controlComponent,
previewComponent,
+ options: {
+ schema,
+ },
};
};
export * from './plate';
-export { controlComponent as MarkdownControl, previewComponent as MarkdownPreview };
+export {
+ controlComponent as MarkdownControl,
+ previewComponent as MarkdownPreview,
+ schema as MarkdownSchema,
+};
export default MarkdownWidget;
diff --git a/core/src/widgets/markdown/plate/PlateEditor.tsx b/core/src/widgets/markdown/plate/PlateEditor.tsx
index fa285cfa..9981a43c 100644
--- a/core/src/widgets/markdown/plate/PlateEditor.tsx
+++ b/core/src/widgets/markdown/plate/PlateEditor.tsx
@@ -55,6 +55,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
+import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { BalloonToolbar } from './components/balloon-toolbar';
import { BlockquoteElement } from './components/nodes/blockquote';
import { CodeBlockElement } from './components/nodes/code-block';
@@ -236,11 +237,14 @@ const PlateEditor: FC = ({
[components],
);
+ const id = useUUID();
+
return useMemo(
() => (
+ id={id}
key="plate-provider"
initialValue={initialValue}
plugins={plugins}
@@ -258,6 +262,7 @@ const PlateEditor: FC = ({
> = ({
- path,
value,
field,
onChange,
- query,
+ config,
locale,
label,
hasErrors,
@@ -118,6 +127,12 @@ const RelationControl: FC>
const [internalValue, setInternalValue] = useState(value);
const [initialOptions, setInitialOptions] = useState([]);
+ const searchCollectionSelector = useMemo(
+ () => selectCollection(field.collection),
+ [field.collection],
+ );
+ const searchCollection = useAppSelector(searchCollectionSelector);
+
const isMultiple = useMemo(() => {
return field.multiple ?? false;
}, [field.multiple]);
@@ -183,6 +198,7 @@ const RelationControl: FC>
);
const [options, setOptions] = useState([]);
+ const [entries, setEntries] = useState([]);
const [open, setOpen] = useState(false);
const valueNotEmpty = useMemo(
() => (Array.isArray(internalValue) ? internalValue.length > 0 : isNotEmpty(internalValue)),
@@ -193,33 +209,45 @@ const RelationControl: FC>
[open, valueNotEmpty, options.length],
);
+ const filterOptions = useCallback(
+ (_options: HitOption[], { inputValue }: FilterOptionsState) => {
+ const searchFields = field.search_fields;
+ const limit = field.options_length || 20;
+ const expandedEntries = expandSearchEntries(entries, searchFields);
+
+ let hits = fuzzy
+ .filter(inputValue, expandedEntries, {
+ extract: entry => {
+ return getEntryField(entry.field, entry);
+ },
+ })
+ .sort(sortByScore)
+ .map(f => f.original);
+
+ if (limit !== undefined && limit > 0) {
+ hits = hits.slice(0, limit);
+ }
+
+ return parseHitOptions(mergeExpandedEntries(hits));
+ },
+ [entries, field.options_length, field.search_fields, parseHitOptions],
+ );
+
useEffect(() => {
- let alive = true;
- if (!loading) {
- return undefined;
+ if (!loading || !searchCollection) {
+ return;
}
- (async () => {
- const collection = field.collection;
- const optionsLength = field.options_length || 20;
- const searchFieldsArray = field.search_fields;
- const file = field.file;
+ const getOptions = async () => {
+ const backend = currentBackend(config);
- const response = await query(path, collection, searchFieldsArray, '', file, optionsLength);
- if (alive) {
- if (response?.type === QUERY_SUCCESS) {
- const hits = response.payload.hits ?? [];
- const options = parseHitOptions(hits);
- setOptions(uniqOptions(initialOptions, options));
- }
- }
- })();
-
- return () => {
- alive = false;
+ const options = await backend.listAllEntries(searchCollection);
+ setEntries(options);
+ setOptions(parseHitOptions(options));
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [field.collection, field.file, field.options_length, field.search_fields, loading]);
+
+ getOptions();
+ }, [searchCollection, config, loading, parseHitOptions]);
const uniqueOptions = uniqOptions(initialOptions, options);
const selectedValue = getSelectedValue(internalValue, uniqueOptions, isMultiple);
@@ -230,6 +258,7 @@ const RelationControl: FC>
disablePortal
options={uniqueOptions}
fullWidth
+ filterOptions={filterOptions}
renderInput={params => (
=> {
name: 'string',
controlComponent,
previewComponent,
+ options: {
+ schema,
+ },
};
};
-export { controlComponent as StringControl, previewComponent as StringPreview };
+export {
+ controlComponent as StringControl,
+ previewComponent as StringPreview,
+ schema as StringSchema,
+};
export default StringWidget;
diff --git a/core/src/widgets/string/schema.ts b/core/src/widgets/string/schema.ts
new file mode 100644
index 00000000..c1d7002a
--- /dev/null
+++ b/core/src/widgets/string/schema.ts
@@ -0,0 +1,5 @@
+export default {
+ properties: {
+ default: { type: 'string' },
+ },
+};
diff --git a/core/src/widgets/text/index.ts b/core/src/widgets/text/index.ts
index 3505b6a6..1e3b3993 100644
--- a/core/src/widgets/text/index.ts
+++ b/core/src/widgets/text/index.ts
@@ -1,3 +1,4 @@
+import schema from './schema';
import controlComponent from './TextControl';
import previewComponent from './TextPreview';
@@ -8,9 +9,12 @@ const TextWidget = (): WidgetParam => {
name: 'text',
controlComponent,
previewComponent,
+ options: {
+ schema,
+ },
};
};
-export { controlComponent as TextControl, previewComponent as TextPreview };
+export { controlComponent as TextControl, previewComponent as TextPreview, schema as TextSchema };
export default TextWidget;
diff --git a/core/src/widgets/text/schema.ts b/core/src/widgets/text/schema.ts
new file mode 100644
index 00000000..c1d7002a
--- /dev/null
+++ b/core/src/widgets/text/schema.ts
@@ -0,0 +1,5 @@
+export default {
+ properties: {
+ default: { type: 'string' },
+ },
+};