Feature/fix hidden widget (#196)
This commit is contained in:
parent
2d7e661fdb
commit
c4a812a575
@ -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: '<div>Some html!</div>'
|
||||
- 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: '<div>Some html!</div>'
|
||||
- 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: '<div>Some html!</div>'
|
||||
- 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
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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<Entry>, b: fuzzy.FilterResult<Entry>) {
|
||||
if (a.score > b.score) return -1;
|
||||
if (a.score < b.score) return 1;
|
||||
export function sortByScore(a: fuzzy.FilterResult<Entry>, b: fuzzy.FilterResult<Entry>) {
|
||||
if (a.score > b.score) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.score < b.score) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -478,16 +486,16 @@ export class Backend<BC extends BackendClass = BackendClass> {
|
||||
// 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<T extends BaseField = UnknownField>(collection: Collection<T>) {
|
||||
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<BC extends BackendClass = BackendClass> {
|
||||
return { entries: hits, pagination: 1 };
|
||||
}
|
||||
|
||||
async query(
|
||||
collection: Collection,
|
||||
async query<T extends BaseField = UnknownField>(
|
||||
collection: Collection<T>,
|
||||
searchFields: string[],
|
||||
searchTerm: string,
|
||||
file?: string,
|
||||
limit?: number,
|
||||
): Promise<SearchQueryResponse> {
|
||||
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 <T extends BaseField = UnknownField>(config: Config<T>) => {
|
||||
if (backend) {
|
||||
return backend;
|
||||
}
|
||||
|
||||
return (backend = resolveBackend(config));
|
||||
return (backend = resolveBackend(config as Config));
|
||||
};
|
||||
})();
|
||||
|
@ -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 = ({
|
||||
<ControlContainer $isHidden={isHidden}>
|
||||
<>
|
||||
{createElement(widget.control, {
|
||||
key: id,
|
||||
key: `${id}-${version}`,
|
||||
collection,
|
||||
config,
|
||||
entry,
|
||||
|
@ -188,31 +188,29 @@ const EditorControlPane = ({
|
||||
/>
|
||||
</LocaleRowWrapper>
|
||||
) : 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 (
|
||||
<EditorControl
|
||||
key={key}
|
||||
field={field}
|
||||
value={getFieldValue(field, entry, isTranslatable, locale)}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
isDisabled={isDuplicate}
|
||||
isHidden={isHidden}
|
||||
isFieldDuplicate={field => isFieldDuplicate(field, locale, i18n?.defaultLocale)}
|
||||
isFieldHidden={field => isFieldHidden(field, locale, i18n?.defaultLocale)}
|
||||
locale={locale}
|
||||
parentPath=""
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<EditorControl
|
||||
key={key}
|
||||
field={field}
|
||||
value={getFieldValue(field, entry, isTranslatable, locale)}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
isDisabled={isDuplicate}
|
||||
isHidden={isHidden}
|
||||
isFieldDuplicate={field => isFieldDuplicate(field, locale, i18n?.defaultLocale)}
|
||||
isFieldHidden={field => isFieldHidden(field, locale, i18n?.defaultLocale)}
|
||||
locale={locale}
|
||||
parentPath=""
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ControlPaneContainer>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<PreviewContainer>
|
||||
{fields.filter(isVisible).map(field => (
|
||||
{fields.map(field => (
|
||||
<div key={field.name}>{widgetFor(field.name)}</div>
|
||||
))}
|
||||
</PreviewContainer>
|
||||
|
@ -188,7 +188,7 @@ function isReactFragment(value: any): value is ReactFragment {
|
||||
|
||||
function getWidget(
|
||||
config: Config,
|
||||
field: RenderedField,
|
||||
field: RenderedField<Field>,
|
||||
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<string, unknown>)[field.name]
|
||||
: value
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
||||
})();
|
@ -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<string, string> = {}) {
|
||||
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<string, string>;
|
||||
},
|
||||
) {
|
||||
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<SearchResponse> {
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -313,6 +313,7 @@ export type TemplatePreviewComponent<
|
||||
export interface WidgetOptions<T = unknown, F extends BaseField = UnknownField> {
|
||||
validator?: Widget<T, F>['validator'];
|
||||
getValidValue?: Widget<T, F>['getValidValue'];
|
||||
getDefaultValue?: Widget<T, F>['getDefaultValue'];
|
||||
schema?: Widget<T, F>['schema'];
|
||||
}
|
||||
|
||||
@ -321,6 +322,7 @@ export interface Widget<T = unknown, F extends BaseField = UnknownField> {
|
||||
preview?: WidgetPreviewComponent<T, F>;
|
||||
validator: FieldValidationMethod<T, F>;
|
||||
getValidValue: (value: T | undefined | null) => T | undefined | null;
|
||||
getDefaultValue?: (defaultValue: T | undefined | null, field: F) => T;
|
||||
schema?: PropertiesSchema<unknown>;
|
||||
}
|
||||
|
||||
|
@ -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<T extends FileOrImageField | MarkdownField
|
||||
fetchMedia();
|
||||
}, [collection, dispatch, entry, field, url]);
|
||||
|
||||
return assetSource;
|
||||
return isNotEmpty(assetSource) ? assetSource : url;
|
||||
}
|
||||
|
@ -140,6 +140,7 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
||||
schema,
|
||||
validator = () => false,
|
||||
getValidValue = (value: T | null | undefined) => value,
|
||||
getDefaultValue,
|
||||
}: WidgetOptions<T, F> = {},
|
||||
): void {
|
||||
if (Array.isArray(name)) {
|
||||
@ -162,6 +163,7 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
||||
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<T = unknown, F extends BaseField = UnknownField>(
|
||||
options: {
|
||||
validator = () => false,
|
||||
getValidValue = (value: T | undefined | null) => value,
|
||||
getDefaultValue,
|
||||
schema,
|
||||
} = {},
|
||||
} = name;
|
||||
@ -190,6 +193,7 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
||||
preview,
|
||||
validator,
|
||||
getValidValue,
|
||||
getDefaultValue,
|
||||
schema,
|
||||
} as unknown as Widget;
|
||||
} else {
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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<K extends keyof IntegrationHooks>(
|
||||
state: RootState,
|
||||
collection: string | null,
|
||||
hook: K,
|
||||
): IntegrationHooks[K] | false {
|
||||
return fromIntegrations.selectIntegration(state.integrations, collection, hook);
|
||||
}
|
||||
|
@ -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<string, IntegrationHooks>;
|
||||
}
|
||||
|
||||
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<K extends keyof IntegrationHooks>(
|
||||
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;
|
@ -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<WidgetControlProps<boolean, BooleanField>> = ({
|
||||
[onChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof internalValue !== 'boolean') {
|
||||
setInternalValue(false);
|
||||
setTimeout(() => {
|
||||
onChange(false);
|
||||
});
|
||||
}
|
||||
}, [internalValue, onChange]);
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
key="boolean-field-label"
|
||||
|
@ -1,4 +1,5 @@
|
||||
import controlComponent from './BooleanControl';
|
||||
import schema from './schema';
|
||||
|
||||
import type { BooleanField, WidgetParam } from '@staticcms/core/interface';
|
||||
|
||||
@ -6,9 +7,15 @@ const BooleanWidget = (): WidgetParam<boolean, BooleanField> => {
|
||||
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;
|
||||
|
5
core/src/widgets/boolean/schema.ts
Normal file
5
core/src/widgets/boolean/schema.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
properties: {
|
||||
default: { type: 'boolean' },
|
||||
},
|
||||
};
|
@ -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<WidgetControlProps<string | { [key: string]: string }, Cod
|
||||
const valueIsMap = useMemo(() => 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<ProcessedCodeLanguage | null>(null);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const [hasFocus, setHasFocus] = useState(false);
|
||||
@ -105,20 +106,20 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
||||
(newValue: string) => {
|
||||
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<WidgetControlProps<string | { [key: string]: string }, Cod
|
||||
[field.allow_language_selection],
|
||||
);
|
||||
|
||||
const availableLanguages = languages.map(language =>
|
||||
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 (
|
||||
<StyledCodeControlWrapper>
|
||||
@ -168,9 +189,9 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
||||
hideSettings={hideSettings}
|
||||
uniqueId={uniqueId}
|
||||
languages={availableLanguages}
|
||||
language={valueToOption(lang)}
|
||||
language={valueToOption(lang?.label ?? '')}
|
||||
allowLanguageSelection={allowLanguageSelection}
|
||||
onChangeLanguage={newLang => setLang(newLang)}
|
||||
onChangeLanguage={handleSetLanguage}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
|
@ -11,6 +11,29 @@ const CodeWidget = (): WidgetParam<string | { [key: string]: string }, CodeField
|
||||
previewComponent,
|
||||
options: {
|
||||
schema,
|
||||
getDefaultValue: (
|
||||
defaultValue: string | { [key: string]: string } | null | undefined,
|
||||
field: CodeField,
|
||||
) => {
|
||||
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] ?? '',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -7,5 +7,8 @@ export default {
|
||||
type: 'object',
|
||||
properties: { code: { type: 'string' }, lang: { type: 'string' } },
|
||||
},
|
||||
default: {
|
||||
oneOf: [{ type: 'string' }, { type: 'object' }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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<string, ColorField> => {
|
||||
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;
|
||||
|
5
core/src/widgets/colorstring/schema.ts
Normal file
5
core/src/widgets/colorstring/schema.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
properties: {
|
||||
default: { type: 'string' },
|
||||
},
|
||||
};
|
@ -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<WidgetControlProps<string, DateTimeField>> = ({
|
||||
|
||||
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<WidgetControlProps<string, DateTimeField>> = ({
|
||||
}, [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<WidgetControlProps<string, DateTimeField>> = ({
|
||||
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<WidgetControlProps<string, DateTimeField>> = ({
|
||||
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;
|
||||
|
@ -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<string, DateTimeField> => {
|
||||
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);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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' },
|
||||
},
|
||||
};
|
||||
|
@ -1,5 +1,16 @@
|
||||
export default {
|
||||
properties: {
|
||||
allow_multiple: { type: 'boolean' },
|
||||
default: {
|
||||
oneOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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<FileOrImageField>;
|
||||
field: FileOrImageField;
|
||||
entry: Entry;
|
||||
}
|
||||
|
||||
const Image: FC<ImageProps> = ({ src }) => {
|
||||
return <StyledImage key="image" role="presentation" src={src} />;
|
||||
const Image: FC<ImageProps> = ({ value, collection, field, entry }) => {
|
||||
const assetSource = useMediaAsset(value, collection, field, entry);
|
||||
|
||||
return <StyledImage key="image" role="presentation" src={assetSource} />;
|
||||
};
|
||||
|
||||
interface SortableImageButtonsProps {
|
||||
@ -129,34 +136,25 @@ const SortableImageButtons: FC<SortableImageButtonsProps> = ({ onRemove, onRepla
|
||||
|
||||
interface SortableImageProps {
|
||||
itemValue: string;
|
||||
getAsset: GetAssetFunction<FileOrImageField>;
|
||||
collection: Collection<FileOrImageField>;
|
||||
field: FileOrImageField;
|
||||
entry: Entry;
|
||||
onRemove: MouseEventHandler;
|
||||
onReplace: MouseEventHandler;
|
||||
}
|
||||
|
||||
const SortableImage: FC<SortableImageProps> = ({
|
||||
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 (
|
||||
<div>
|
||||
<ImageWrapper key="image-wrapper" $sortable>
|
||||
<Image key="image" src={assetSource} />
|
||||
<Image key="image" value={itemValue} collection={collection} field={field} entry={entry} />
|
||||
</ImageWrapper>
|
||||
<SortableImageButtons
|
||||
key="image-buttons"
|
||||
@ -212,12 +210,13 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
const FileControl: FC<WidgetControlProps<string | string[], FileOrImageField>> = 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 = {}) => {
|
||||
<SortableImage
|
||||
key={`item-${itemValue}`}
|
||||
itemValue={itemValue}
|
||||
getAsset={getAsset}
|
||||
collection={collection}
|
||||
field={field}
|
||||
entry={entry}
|
||||
onRemove={onRemoveOne(index)}
|
||||
onReplace={onReplaceOne(index)}
|
||||
/>
|
||||
@ -385,7 +368,13 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
|
||||
return (
|
||||
<ImageWrapper key="single-image-wrapper">
|
||||
<Image key="single-image" src={assetSource} />
|
||||
<Image
|
||||
key="single-image"
|
||||
value={internalValue}
|
||||
collection={collection}
|
||||
field={field}
|
||||
entry={entry}
|
||||
/>
|
||||
</ImageWrapper>
|
||||
);
|
||||
}
|
||||
@ -403,7 +392,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
||||
}
|
||||
|
||||
return <FileLinks key="single-file-links">{renderFileLink(internalValue)}</FileLinks>;
|
||||
}, [assetSource, field, getAsset, internalValue, onRemoveOne, onReplaceOne, renderFileLink]);
|
||||
}, [collection, entry, field, internalValue, onRemoveOne, onReplaceOne, renderFileLink]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
const subject = forImage ? 'image' : 'file';
|
||||
|
@ -1,5 +1,16 @@
|
||||
export default {
|
||||
properties: {
|
||||
allow_multiple: { type: 'boolean' },
|
||||
default: {
|
||||
oneOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -311,27 +311,34 @@ const ListControl: FC<WidgetControlProps<ObjectValue[], ListField>> = ({
|
||||
<DndContext key="dnd-context" onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={keys}>
|
||||
<StyledSortableList $collapsed={collapsed}>
|
||||
{internalValue.map((item, index) => (
|
||||
<SortableItem
|
||||
index={index}
|
||||
key={keys[index]}
|
||||
id={keys[index]}
|
||||
item={item}
|
||||
valueType={valueType}
|
||||
handleRemove={handleRemove}
|
||||
data-testid={`object-control-${index}`}
|
||||
entry={entry}
|
||||
field={field}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
isFieldDuplicate={isFieldDuplicate}
|
||||
isFieldHidden={isFieldHidden}
|
||||
locale={locale}
|
||||
path={path}
|
||||
value={item as Record<string, ObjectValue>}
|
||||
i18n={i18n}
|
||||
/>
|
||||
))}
|
||||
{internalValue.map((item, index) => {
|
||||
const key = keys[index];
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
index={index}
|
||||
key={key}
|
||||
id={key}
|
||||
item={item}
|
||||
valueType={valueType}
|
||||
handleRemove={handleRemove}
|
||||
data-testid={`object-control-${index}`}
|
||||
entry={entry}
|
||||
field={field}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
isFieldDuplicate={isFieldDuplicate}
|
||||
isFieldHidden={isFieldHidden}
|
||||
locale={locale}
|
||||
path={path}
|
||||
value={item as Record<string, ObjectValue>}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledSortableList>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
@ -6,7 +6,7 @@ import type { MapField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||
import type { FC } from 'react';
|
||||
|
||||
const MapPreview: FC<WidgetPreviewProps<string, MapField>> = ({ value }) => {
|
||||
return <WidgetPreviewContainer>{value ? value.toString() : null}</WidgetPreviewContainer>;
|
||||
return <WidgetPreviewContainer>{value}</WidgetPreviewContainer>;
|
||||
};
|
||||
|
||||
export default MapPreview;
|
||||
|
@ -58,7 +58,7 @@ const MarkdownControl: FC<WidgetControlProps<string, MarkdownField>> = ({
|
||||
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<WidgetControlProps<string, MarkdownField>> = ({
|
||||
|
||||
const [slateValue, loaded] = useMarkdownToSlate(internalValue);
|
||||
|
||||
console.log('[Plate] slateValue', slateValue);
|
||||
// console.log('[Plate] slateValue', slateValue);
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
|
@ -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<string, MarkdownField> => {
|
||||
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;
|
||||
|
@ -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<PlateEditorProps> = ({
|
||||
[components],
|
||||
);
|
||||
|
||||
const id = useUUID();
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<StyledPlateEditor>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<PlateProvider<MdValue>
|
||||
id={id}
|
||||
key="plate-provider"
|
||||
initialValue={initialValue}
|
||||
plugins={plugins}
|
||||
@ -258,6 +262,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
||||
<div key="editor-wrapper" ref={editorContainerRef} style={styles.container}>
|
||||
<Plate
|
||||
key="editor"
|
||||
id={id}
|
||||
editableProps={{
|
||||
...editableProps,
|
||||
onFocus: handleOnFocus,
|
||||
|
5
core/src/widgets/markdown/schema.ts
Normal file
5
core/src/widgets/markdown/schema.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
properties: {
|
||||
default: { type: 'string' },
|
||||
},
|
||||
};
|
@ -4,5 +4,6 @@ export default {
|
||||
value_type: { type: 'string' },
|
||||
min: { type: 'number' },
|
||||
max: { type: 'number' },
|
||||
default: { type: 'number' },
|
||||
},
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import * as fuzzy from 'fuzzy';
|
||||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import TextField from '@mui/material/TextField';
|
||||
@ -6,7 +7,13 @@ import get from 'lodash/get';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { QUERY_SUCCESS } from '@staticcms/core/actions/search';
|
||||
import {
|
||||
currentBackend,
|
||||
expandSearchEntries,
|
||||
getEntryField,
|
||||
mergeExpandedEntries,
|
||||
sortByScore,
|
||||
} from '@staticcms/core/backend';
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import {
|
||||
addFileTemplateFields,
|
||||
@ -14,7 +21,10 @@ import {
|
||||
expandPath,
|
||||
extractTemplateVars,
|
||||
} from '@staticcms/core/lib/widgets/stringTemplate';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import { selectCollection } from '@staticcms/core/reducers/collections';
|
||||
|
||||
import type { FilterOptionsState } from '@mui/material/useAutocomplete';
|
||||
import type {
|
||||
Entry,
|
||||
EntryData,
|
||||
@ -106,11 +116,10 @@ function getSelectedValue(
|
||||
}
|
||||
|
||||
const RelationControl: FC<WidgetControlProps<string | string[], RelationField>> = ({
|
||||
path,
|
||||
value,
|
||||
field,
|
||||
onChange,
|
||||
query,
|
||||
config,
|
||||
locale,
|
||||
label,
|
||||
hasErrors,
|
||||
@ -118,6 +127,12 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
|
||||
const [internalValue, setInternalValue] = useState(value);
|
||||
const [initialOptions, setInitialOptions] = useState<HitOption[]>([]);
|
||||
|
||||
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<WidgetControlProps<string | string[], RelationField>>
|
||||
);
|
||||
|
||||
const [options, setOptions] = useState<HitOption[]>([]);
|
||||
const [entries, setEntries] = useState<Entry[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const valueNotEmpty = useMemo(
|
||||
() => (Array.isArray(internalValue) ? internalValue.length > 0 : isNotEmpty(internalValue)),
|
||||
@ -193,33 +209,45 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
|
||||
[open, valueNotEmpty, options.length],
|
||||
);
|
||||
|
||||
const filterOptions = useCallback(
|
||||
(_options: HitOption[], { inputValue }: FilterOptionsState<HitOption>) => {
|
||||
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<WidgetControlProps<string | string[], RelationField>>
|
||||
disablePortal
|
||||
options={uniqueOptions}
|
||||
fullWidth
|
||||
filterOptions={filterOptions}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
key="relation-control-input"
|
||||
|
@ -9,6 +9,17 @@ export default {
|
||||
max: { type: 'integer' },
|
||||
display_fields: { type: 'array', minItems: 1, items: { type: 'string' } },
|
||||
options_length: { type: 'integer' },
|
||||
default: {
|
||||
oneOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
oneOf: [
|
||||
{
|
||||
|
@ -3,6 +3,18 @@ export default {
|
||||
multiple: { type: 'boolean' },
|
||||
min: { type: 'integer' },
|
||||
max: { type: 'integer' },
|
||||
default: {
|
||||
oneOf: [
|
||||
{ type: 'string' },
|
||||
{ type: 'number' },
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
oneOf: [{ type: 'string' }, { type: 'number' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
type: 'array',
|
||||
items: {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import schema from './schema';
|
||||
import controlComponent from './StringControl';
|
||||
import previewComponent from './StringPreview';
|
||||
|
||||
@ -8,9 +9,16 @@ const StringWidget = (): WidgetParam<string, StringOrTextField> => {
|
||||
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;
|
||||
|
5
core/src/widgets/string/schema.ts
Normal file
5
core/src/widgets/string/schema.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
properties: {
|
||||
default: { type: 'string' },
|
||||
},
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import schema from './schema';
|
||||
import controlComponent from './TextControl';
|
||||
import previewComponent from './TextPreview';
|
||||
|
||||
@ -8,9 +9,12 @@ const TextWidget = (): WidgetParam<string, StringOrTextField> => {
|
||||
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;
|
||||
|
5
core/src/widgets/text/schema.ts
Normal file
5
core/src/widgets/text/schema.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
properties: {
|
||||
default: { type: 'string' },
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user