Feature/fix hidden widget (#196)
This commit is contained in:
parent
2d7e661fdb
commit
c4a812a575
@ -98,10 +98,14 @@ collections:
|
|||||||
description: Boolean widget
|
description: Boolean widget
|
||||||
fields:
|
fields:
|
||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: Required Validation
|
||||||
widget: boolean
|
widget: boolean
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: boolean
|
||||||
|
default: true
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: Pattern Validation
|
||||||
widget: boolean
|
widget: boolean
|
||||||
pattern: ['true', 'Must be true']
|
pattern: ['true', 'Must be true']
|
||||||
required: false
|
required: false
|
||||||
@ -111,29 +115,58 @@ collections:
|
|||||||
description: Code widget
|
description: Code widget
|
||||||
fields:
|
fields:
|
||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: Required Validation
|
||||||
widget: code
|
widget: code
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: code
|
||||||
|
default: '<div>Some html!</div>'
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: Pattern Validation
|
||||||
widget: code
|
widget: code
|
||||||
pattern: ['.{12,}', 'Must have at least 12 characters']
|
pattern: ['.{12,}', 'Must have at least 12 characters']
|
||||||
allow_input: true
|
allow_input: true
|
||||||
required: false
|
required: false
|
||||||
- name: language
|
- name: language
|
||||||
label: 'Language Selection'
|
label: Language Selection
|
||||||
widget: code
|
widget: code
|
||||||
allow_language_selection: true
|
allow_language_selection: true
|
||||||
required: false
|
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
|
- name: color
|
||||||
label: Color
|
label: Color
|
||||||
file: _widgets/color.json
|
file: _widgets/color.json
|
||||||
description: Color widget
|
description: Color widget
|
||||||
fields:
|
fields:
|
||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: Required Validation
|
||||||
widget: color
|
widget: color
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: color
|
||||||
|
default: '#2121c5'
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: Pattern Validation
|
||||||
widget: color
|
widget: color
|
||||||
pattern: ['^#([0-9a-fA-F]{3})(?:[0-9a-fA-F]{3})?$', 'Must be a valid hex code']
|
pattern: ['^#([0-9a-fA-F]{3})(?:[0-9a-fA-F]{3})?$', 'Must be a valid hex code']
|
||||||
allow_input: true
|
allow_input: true
|
||||||
@ -143,6 +176,12 @@ collections:
|
|||||||
widget: color
|
widget: color
|
||||||
enable_alpha: true
|
enable_alpha: true
|
||||||
required: false
|
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
|
- name: datetime
|
||||||
label: DateTime
|
label: DateTime
|
||||||
file: _widgets/datetime.json
|
file: _widgets/datetime.json
|
||||||
@ -166,33 +205,59 @@ collections:
|
|||||||
date_format: 'MMM d, yyyy'
|
date_format: 'MMM d, yyyy'
|
||||||
time_format: 'h:mm aaa'
|
time_format: 'h:mm aaa'
|
||||||
required: false
|
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
|
- name: date
|
||||||
label: Date
|
label: Date
|
||||||
widget: datetime
|
widget: datetime
|
||||||
format: 'MMM d, yyyy'
|
format: 'MMM d, yyyy'
|
||||||
date_format: 'MMM d, yyyy'
|
date_format: 'MMM d, yyyy'
|
||||||
required: false
|
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
|
- name: time
|
||||||
label: Time
|
label: Time
|
||||||
widget: datetime
|
widget: datetime
|
||||||
format: 'h:mm aaa'
|
format: 'h:mm aaa'
|
||||||
time_format: 'h:mm aaa'
|
time_format: 'h:mm aaa'
|
||||||
required: false
|
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
|
- name: file
|
||||||
label: File
|
label: File
|
||||||
file: _widgets/file.json
|
file: _widgets/file.json
|
||||||
description: File widget
|
description: File widget
|
||||||
fields:
|
fields:
|
||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: Required Validation
|
||||||
widget: file
|
widget: file
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: file
|
||||||
|
default: /assets/uploads/moby-dick.jpg
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: Pattern Validation
|
||||||
widget: file
|
widget: file
|
||||||
pattern: ['\.pdf', 'Must be a pdf']
|
pattern: ['\.pdf', 'Must be a pdf']
|
||||||
required: false
|
required: false
|
||||||
- name: choose_url
|
- name: choose_url
|
||||||
label: 'Choose URL'
|
label: Choose URL
|
||||||
widget: file
|
widget: file
|
||||||
required: false
|
required: false
|
||||||
media_library:
|
media_library:
|
||||||
@ -203,15 +268,19 @@ collections:
|
|||||||
description: Image widget
|
description: Image widget
|
||||||
fields:
|
fields:
|
||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: Required Validation
|
||||||
widget: image
|
widget: image
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: image
|
||||||
|
default: /assets/uploads/moby-dick.jpg
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: Pattern Validation
|
||||||
widget: image
|
widget: image
|
||||||
pattern: ['\.png', 'Must be a png']
|
pattern: ['\.png', 'Must be a png']
|
||||||
required: false
|
required: false
|
||||||
- name: choose_url
|
- name: choose_url
|
||||||
label: 'Choose URL'
|
label: Choose URL
|
||||||
widget: image
|
widget: image
|
||||||
required: false
|
required: false
|
||||||
media_library:
|
media_library:
|
||||||
@ -222,7 +291,7 @@ collections:
|
|||||||
description: List widget
|
description: List widget
|
||||||
fields:
|
fields:
|
||||||
- name: list
|
- name: list
|
||||||
label: List
|
label: Required List
|
||||||
widget: list
|
widget: list
|
||||||
fields:
|
fields:
|
||||||
- label: Name
|
- label: Name
|
||||||
@ -232,6 +301,32 @@ collections:
|
|||||||
- label: Description
|
- label: Description
|
||||||
name: description
|
name: description
|
||||||
widget: text
|
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
|
- name: typed_list
|
||||||
label: Typed List
|
label: Typed List
|
||||||
widget: list
|
widget: list
|
||||||
@ -268,7 +363,60 @@ collections:
|
|||||||
widget: datetime
|
widget: datetime
|
||||||
- label: Markdown
|
- label: Markdown
|
||||||
name: 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
|
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
|
- label: Type 3 Object
|
||||||
name: type_3_object
|
name: type_3_object
|
||||||
widget: object
|
widget: object
|
||||||
@ -285,10 +433,14 @@ collections:
|
|||||||
description: Map widget
|
description: Map widget
|
||||||
fields:
|
fields:
|
||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: Required Validation
|
||||||
widget: map
|
widget: map
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: map
|
||||||
|
default: '{ "type": "Point", "coordinates": [-73.9852661, 40.7478738] }'
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: Pattern Validation
|
||||||
widget: map
|
widget: map
|
||||||
pattern: ['\[-([7-9][0-9]|1[0-2][0-9])\.', 'Must be between latitude -70 and -129']
|
pattern: ['\[-([7-9][0-9]|1[0-2][0-9])\.', 'Must be between latitude -70 and -129']
|
||||||
required: false
|
required: false
|
||||||
@ -298,10 +450,14 @@ collections:
|
|||||||
description: Markdown widget
|
description: Markdown widget
|
||||||
fields:
|
fields:
|
||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: Required Validation
|
||||||
widget: markdown
|
widget: markdown
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: markdown
|
||||||
|
default: Default **markdown** value
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: Pattern Validation
|
||||||
widget: markdown
|
widget: markdown
|
||||||
pattern: ['# [a-zA-Z0-9]+', 'Must have a header']
|
pattern: ['# [a-zA-Z0-9]+', 'Must have a header']
|
||||||
required: false
|
required: false
|
||||||
@ -311,26 +467,30 @@ collections:
|
|||||||
description: Number widget
|
description: Number widget
|
||||||
fields:
|
fields:
|
||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: Required Validation
|
||||||
widget: number
|
widget: number
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: number
|
||||||
|
default: 5
|
||||||
- name: min
|
- name: min
|
||||||
label: 'Min Validation'
|
label: Min Validation
|
||||||
widget: number
|
widget: number
|
||||||
min: 5
|
min: 5
|
||||||
required: false
|
required: false
|
||||||
- name: max
|
- name: max
|
||||||
label: 'Max Validation'
|
label: Max Validation
|
||||||
widget: number
|
widget: number
|
||||||
max: 10
|
max: 10
|
||||||
required: false
|
required: false
|
||||||
- name: min_and_max
|
- name: min_and_max
|
||||||
label: 'Min and Max Validation'
|
label: Min and Max Validation
|
||||||
widget: number
|
widget: number
|
||||||
min: 5
|
min: 5
|
||||||
max: 10
|
max: 10
|
||||||
required: false
|
required: false
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: Pattern Validation
|
||||||
widget: number
|
widget: number
|
||||||
pattern: ['[0-9]{3,}', 'Must be at least 3 digits']
|
pattern: ['[0-9]{3,}', 'Must be at least 3 digits']
|
||||||
required: false
|
required: false
|
||||||
@ -346,12 +506,28 @@ collections:
|
|||||||
- label: Number of posts on frontpage
|
- label: Number of posts on frontpage
|
||||||
name: front_limit
|
name: front_limit
|
||||||
widget: number
|
widget: number
|
||||||
- label: Default Author
|
- label: Author
|
||||||
name: author
|
name: author
|
||||||
widget: string
|
widget: string
|
||||||
- label: Default Thumbnail
|
- label: Thumbnail
|
||||||
name: thumb
|
name: thumb
|
||||||
widget: image
|
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
|
- label: Optional Validation
|
||||||
name: optional
|
name: optional
|
||||||
widget: object
|
widget: object
|
||||||
@ -361,11 +537,31 @@ collections:
|
|||||||
name: front_limit
|
name: front_limit
|
||||||
widget: number
|
widget: number
|
||||||
required: false
|
required: false
|
||||||
- label: Default Author
|
- label: Author
|
||||||
name: author
|
name: author
|
||||||
widget: string
|
widget: string
|
||||||
required: false
|
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
|
name: thumb
|
||||||
widget: image
|
widget: image
|
||||||
required: false
|
required: false
|
||||||
@ -385,6 +581,18 @@ collections:
|
|||||||
- title
|
- title
|
||||||
- body
|
- body
|
||||||
value_field: title
|
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
|
- label: Optional Validation
|
||||||
name: optional
|
name: optional
|
||||||
widget: relation
|
widget: relation
|
||||||
@ -410,6 +618,22 @@ collections:
|
|||||||
- title
|
- title
|
||||||
- body
|
- body
|
||||||
value_field: title
|
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
|
- name: select
|
||||||
label: Select
|
label: Select
|
||||||
file: _widgets/select.json
|
file: _widgets/select.json
|
||||||
@ -422,6 +646,14 @@ collections:
|
|||||||
- a
|
- a
|
||||||
- b
|
- b
|
||||||
- c
|
- c
|
||||||
|
- label: Required With Default
|
||||||
|
name: with_default
|
||||||
|
widget: select
|
||||||
|
default: b
|
||||||
|
options:
|
||||||
|
- a
|
||||||
|
- b
|
||||||
|
- c
|
||||||
- label: Pattern Validation
|
- label: Pattern Validation
|
||||||
name: pattern
|
name: pattern
|
||||||
widget: select
|
widget: select
|
||||||
@ -431,13 +663,39 @@ collections:
|
|||||||
- c
|
- c
|
||||||
pattern: ['[a-b]', 'Must be a or b']
|
pattern: ['[a-b]', 'Must be a or b']
|
||||||
required: false
|
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
|
- label: Value and Label
|
||||||
name: value_and_label
|
name: value_and_label
|
||||||
widget: select
|
widget: select
|
||||||
options:
|
options:
|
||||||
- value: a
|
- value: a
|
||||||
label: A fancy label
|
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
|
label: Another fancy label
|
||||||
- value: c
|
- value: c
|
||||||
label: And one more fancy label
|
label: And one more fancy label
|
||||||
@ -451,6 +709,19 @@ collections:
|
|||||||
pattern: ['[a-b]', 'Must be a or b']
|
pattern: ['[a-b]', 'Must be a or b']
|
||||||
multiple: true
|
multiple: true
|
||||||
required: false
|
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
|
- label: Value and Label Multiple
|
||||||
name: value_and_label_multiple
|
name: value_and_label_multiple
|
||||||
widget: select
|
widget: select
|
||||||
@ -468,10 +739,14 @@ collections:
|
|||||||
description: String widget
|
description: String widget
|
||||||
fields:
|
fields:
|
||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: Required Validation
|
||||||
widget: string
|
widget: string
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: string
|
||||||
|
default: Default value
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: Pattern Validation
|
||||||
widget: string
|
widget: string
|
||||||
pattern: ['.{12,}', 'Must have at least 12 characters']
|
pattern: ['.{12,}', 'Must have at least 12 characters']
|
||||||
required: false
|
required: false
|
||||||
@ -483,6 +758,10 @@ collections:
|
|||||||
- name: required
|
- name: required
|
||||||
label: 'Required Validation'
|
label: 'Required Validation'
|
||||||
widget: text
|
widget: text
|
||||||
|
- name: with_default
|
||||||
|
label: Required With Default
|
||||||
|
widget: text
|
||||||
|
default: Default value
|
||||||
- name: pattern
|
- name: pattern
|
||||||
label: 'Pattern Validation'
|
label: 'Pattern Validation'
|
||||||
widget: text
|
widget: text
|
||||||
@ -778,7 +1057,7 @@ collections:
|
|||||||
widget: number
|
widget: number
|
||||||
- label: Markdown
|
- label: Markdown
|
||||||
name: markdown
|
name: markdown
|
||||||
widget: text
|
widget: markdown
|
||||||
- label: Datetime
|
- label: Datetime
|
||||||
name: datetime
|
name: datetime
|
||||||
widget: datetime
|
widget: datetime
|
||||||
@ -817,7 +1096,7 @@ collections:
|
|||||||
widget: number
|
widget: number
|
||||||
- label: Markdown
|
- label: Markdown
|
||||||
name: markdown
|
name: markdown
|
||||||
widget: text
|
widget: markdown
|
||||||
- label: Datetime
|
- label: Datetime
|
||||||
name: datetime
|
name: datetime
|
||||||
widget: datetime
|
widget: datetime
|
||||||
@ -870,7 +1149,7 @@ collections:
|
|||||||
widget: datetime
|
widget: datetime
|
||||||
- label: Markdown
|
- label: Markdown
|
||||||
name: markdown
|
name: markdown
|
||||||
widget: text
|
widget: markdown
|
||||||
- label: Type 3 Object
|
- label: Type 3 Object
|
||||||
name: type_3_object
|
name: type_3_object
|
||||||
widget: object
|
widget: object
|
||||||
|
@ -8,13 +8,11 @@ import { resolveBackend } from '../backend';
|
|||||||
import validateConfig from '../constants/configSchema';
|
import validateConfig from '../constants/configSchema';
|
||||||
import { I18N, I18N_FIELD, I18N_STRUCTURE } from '../lib/i18n';
|
import { I18N, I18N_FIELD, I18N_STRUCTURE } from '../lib/i18n';
|
||||||
import { selectDefaultSortableFields } from '../lib/util/collection.util';
|
import { selectDefaultSortableFields } from '../lib/util/collection.util';
|
||||||
import { getIntegrations, selectIntegration } from '../reducers/integrations';
|
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { ThunkDispatch } from 'redux-thunk';
|
import type { ThunkDispatch } from 'redux-thunk';
|
||||||
import type {
|
import type {
|
||||||
BaseField,
|
BaseField,
|
||||||
Collection,
|
|
||||||
Config,
|
Config,
|
||||||
Field,
|
Field,
|
||||||
I18nInfo,
|
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) {
|
export function applyDefaults(originalConfig: Config) {
|
||||||
return produce(originalConfig, config => {
|
return produce(originalConfig, config => {
|
||||||
config.slug = config.slug || {};
|
config.slug = config.slug || {};
|
||||||
@ -247,11 +239,7 @@ export function applyDefaults(originalConfig: Config) {
|
|||||||
|
|
||||||
if (!collection.sortable_fields) {
|
if (!collection.sortable_fields) {
|
||||||
collection.sortable_fields = {
|
collection.sortable_fields = {
|
||||||
fields: selectDefaultSortableFields(
|
fields: selectDefaultSortableFields(collection, backend),
|
||||||
collection,
|
|
||||||
backend,
|
|
||||||
hasIntegration(config, collection),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,12 +3,11 @@ import isEqual from 'lodash/isEqual';
|
|||||||
import { currentBackend } from '../backend';
|
import { currentBackend } from '../backend';
|
||||||
import { SORT_DIRECTION_ASCENDING } from '../constants';
|
import { SORT_DIRECTION_ASCENDING } from '../constants';
|
||||||
import ValidationErrorTypes from '../constants/validationErrorTypes';
|
import ValidationErrorTypes from '../constants/validationErrorTypes';
|
||||||
import { getSearchIntegrationProvider } from '../integrations';
|
|
||||||
import { duplicateDefaultI18nFields, hasI18n, I18N_FIELD, serializeI18n } from '../lib/i18n';
|
import { duplicateDefaultI18nFields, hasI18n, I18N_FIELD, serializeI18n } from '../lib/i18n';
|
||||||
import { serializeValues } from '../lib/serializeEntryValues';
|
import { serializeValues } from '../lib/serializeEntryValues';
|
||||||
import { Cursor } from '../lib/util';
|
import { Cursor } from '../lib/util';
|
||||||
import { selectFields, updateFieldByKey } from '../lib/util/collection.util';
|
import { selectFields, updateFieldByKey } from '../lib/util/collection.util';
|
||||||
import { selectIntegration, selectPublishedSlugs } from '../reducers';
|
import { selectPublishedSlugs } from '../reducers';
|
||||||
import { selectCollectionEntriesCursor } from '../reducers/cursors';
|
import { selectCollectionEntriesCursor } from '../reducers/cursors';
|
||||||
import { selectEntriesSortFields, selectIsFetching } from '../reducers/entries';
|
import { selectEntriesSortFields, selectIsFetching } from '../reducers/entries';
|
||||||
import { navigateToEntry } from '../routing/history';
|
import { navigateToEntry } from '../routing/history';
|
||||||
@ -277,17 +276,7 @@ async function getAllEntries(state: RootState, collection: Collection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const backend = currentBackend(configState.config);
|
const backend = currentBackend(configState.config);
|
||||||
const integration = selectIntegration(state, collection.name, 'listEntries');
|
return backend.listAllEntries(collection);
|
||||||
const provider = integration
|
|
||||||
? getSearchIntegrationProvider(state.integrations, integration)
|
|
||||||
: backend;
|
|
||||||
|
|
||||||
if (!provider) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await provider.listAllEntries(collection);
|
|
||||||
return entries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortByField(
|
export function sortByField(
|
||||||
@ -680,14 +669,6 @@ export function loadEntries(collection: Collection, page = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const backend = currentBackend(configState.config);
|
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);
|
const append = !!(page && !isNaN(page) && page > 0);
|
||||||
dispatch(entriesLoading(collection));
|
dispatch(entriesLoading(collection));
|
||||||
@ -701,8 +682,8 @@ export function loadEntries(collection: Collection, page = 0) {
|
|||||||
entries: Entry[];
|
entries: Entry[];
|
||||||
} = await (loadAllEntries
|
} = await (loadAllEntries
|
||||||
? // nested collections require all entries to construct the tree
|
? // nested collections require all entries to construct the tree
|
||||||
provider.listAllEntries(collection).then((entries: Entry[]) => ({ entries }))
|
backend.listAllEntries(collection).then((entries: Entry[]) => ({ entries }))
|
||||||
: provider.listEntries(collection, page));
|
: backend.listEntries(collection));
|
||||||
|
|
||||||
const cleanResponse = {
|
const cleanResponse = {
|
||||||
...response,
|
...response,
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
import { currentBackend } from '../backend';
|
import { currentBackend } from '../backend';
|
||||||
import { getSearchIntegrationProvider } from '../integrations';
|
|
||||||
import { selectIntegration } from '../reducers';
|
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { ThunkDispatch } from 'redux-thunk';
|
import type { ThunkDispatch } from 'redux-thunk';
|
||||||
@ -103,43 +101,26 @@ export function searchEntries(searchTerm: string, searchCollections: string[], p
|
|||||||
|
|
||||||
const backend = currentBackend(configState.config);
|
const backend = currentBackend(configState.config);
|
||||||
const allCollections = searchCollections || Object.keys(state.collections);
|
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
|
// avoid duplicate searches
|
||||||
if (
|
if (
|
||||||
search.isFetching &&
|
search.isFetching &&
|
||||||
search.term === searchTerm &&
|
search.term === searchTerm &&
|
||||||
isEqual(allCollections, search.collections) &&
|
isEqual(allCollections, search.collections)
|
||||||
// if an integration doesn't exist, 'page' is not used
|
|
||||||
(search.page === page || !integration)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(searchingEntries(searchTerm, allCollections, page));
|
dispatch(searchingEntries(searchTerm, allCollections, page));
|
||||||
|
|
||||||
const searchPromise = integration
|
try {
|
||||||
? getSearchIntegrationProvider(state.integrations, integration)?.search(
|
const response = await backend.search(
|
||||||
collections,
|
|
||||||
searchTerm,
|
|
||||||
page,
|
|
||||||
)
|
|
||||||
: backend.search(
|
|
||||||
Object.entries(state.collections)
|
Object.entries(state.collections)
|
||||||
.filter(([key, _value]) => allCollections.indexOf(key) !== -1)
|
.filter(([key, _value]) => allCollections.indexOf(key) !== -1)
|
||||||
.map(([_key, value]) => value),
|
.map(([_key, value]) => value),
|
||||||
searchTerm,
|
searchTerm,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await searchPromise;
|
|
||||||
if (!response) {
|
|
||||||
return dispatch(searchFailure(new Error(`No integration found for name "${integration}"`)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return dispatch(searchSuccess(response.entries, page));
|
return dispatch(searchSuccess(response.entries, page));
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -170,7 +151,6 @@ export function query(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const backend = currentBackend(configState.config);
|
const backend = currentBackend(configState.config);
|
||||||
const integration = selectIntegration(state, collectionName, 'search');
|
|
||||||
const collection = Object.values(state.collections).find(
|
const collection = Object.values(state.collections).find(
|
||||||
collection => collection.name === collectionName,
|
collection => collection.name === collectionName,
|
||||||
);
|
);
|
||||||
@ -178,16 +158,14 @@ export function query(
|
|||||||
return dispatch(queryFailure(new Error('Collection not found')));
|
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 {
|
try {
|
||||||
const response: SearchQueryResponse = await queryPromise;
|
const response: SearchQueryResponse = await backend.query(
|
||||||
|
collection,
|
||||||
|
searchFields,
|
||||||
|
searchTerm,
|
||||||
|
file,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
return dispatch(querySuccess(namespace, response.hits));
|
return dispatch(querySuccess(namespace, response.hits));
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -46,6 +46,7 @@ import createEntry from './valueObjects/createEntry';
|
|||||||
import type {
|
import type {
|
||||||
BackendClass,
|
BackendClass,
|
||||||
BackendInitializer,
|
BackendInitializer,
|
||||||
|
BaseField,
|
||||||
Collection,
|
Collection,
|
||||||
CollectionFile,
|
CollectionFile,
|
||||||
Config,
|
Config,
|
||||||
@ -60,6 +61,7 @@ import type {
|
|||||||
ImplementationEntry,
|
ImplementationEntry,
|
||||||
SearchQueryResponse,
|
SearchQueryResponse,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
|
UnknownField,
|
||||||
User,
|
User,
|
||||||
} from './interface';
|
} from './interface';
|
||||||
import type { AllowedEvent } from './lib/registry';
|
import type { AllowedEvent } from './lib/registry';
|
||||||
@ -109,7 +111,7 @@ function getEntryBackupKey(collectionName?: string, slug?: string) {
|
|||||||
return `${baseKey}.${collectionName}${suffix}`;
|
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);
|
const value = get(entry.data, field);
|
||||||
if (value) {
|
if (value) {
|
||||||
return String(value);
|
return String(value);
|
||||||
@ -210,9 +212,15 @@ export function mergeExpandedEntries(entries: (Entry & { field: string })[]) {
|
|||||||
return Object.values(merged);
|
return Object.values(merged);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByScore(a: fuzzy.FilterResult<Entry>, b: fuzzy.FilterResult<Entry>) {
|
export function sortByScore(a: fuzzy.FilterResult<Entry>, b: fuzzy.FilterResult<Entry>) {
|
||||||
if (a.score > b.score) return -1;
|
if (a.score > b.score) {
|
||||||
if (a.score < b.score) return 1;
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.score < b.score) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -478,16 +486,16 @@ export class Backend<BC extends BackendClass = BackendClass> {
|
|||||||
// repeats the process. Once there is no available "next" action, it
|
// repeats the process. Once there is no available "next" action, it
|
||||||
// returns all the collected entries. Used to retrieve all entries
|
// returns all the collected entries. Used to retrieve all entries
|
||||||
// for local searches and queries.
|
// 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) {
|
if ('folder' in collection && collection.folder && this.implementation.allEntriesByFolder) {
|
||||||
const depth = collectionDepth(collection);
|
const depth = collectionDepth(collection as Collection);
|
||||||
const extension = selectFolderEntryExtension(collection);
|
const extension = selectFolderEntryExtension(collection as Collection);
|
||||||
return this.implementation
|
return this.implementation
|
||||||
.allEntriesByFolder(collection.folder as string, extension, depth)
|
.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;
|
const { entries } = response;
|
||||||
let { cursor } = response;
|
let { cursor } = response;
|
||||||
while (cursor && cursor.actions?.has('next')) {
|
while (cursor && cursor.actions?.has('next')) {
|
||||||
@ -557,14 +565,14 @@ export class Backend<BC extends BackendClass = BackendClass> {
|
|||||||
return { entries: hits, pagination: 1 };
|
return { entries: hits, pagination: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
async query(
|
async query<T extends BaseField = UnknownField>(
|
||||||
collection: Collection,
|
collection: Collection<T>,
|
||||||
searchFields: string[],
|
searchFields: string[],
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
file?: string,
|
file?: string,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
): Promise<SearchQueryResponse> {
|
): Promise<SearchQueryResponse> {
|
||||||
let entries = await this.listAllEntries(collection);
|
let entries = await this.listAllEntries(collection as Collection);
|
||||||
if (file) {
|
if (file) {
|
||||||
entries = entries.filter(e => e.slug === file);
|
entries = entries.filter(e => e.slug === file);
|
||||||
}
|
}
|
||||||
@ -1014,11 +1022,11 @@ export function resolveBackend(config?: Config) {
|
|||||||
export const currentBackend = (function () {
|
export const currentBackend = (function () {
|
||||||
let backend: Backend;
|
let backend: Backend;
|
||||||
|
|
||||||
return (config: Config) => {
|
return <T extends BaseField = UnknownField>(config: Config<T>) => {
|
||||||
if (backend) {
|
if (backend) {
|
||||||
return 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 useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||||
import { resolveWidget } from '@staticcms/core/lib/registry';
|
import { resolveWidget } from '@staticcms/core/lib/registry';
|
||||||
import { getFieldLabel } from '@staticcms/core/lib/util/field.util';
|
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 { validate } from '@staticcms/core/lib/util/validation.util';
|
||||||
import { selectFieldErrors } from '@staticcms/core/reducers/entryDraft';
|
import { selectFieldErrors } from '@staticcms/core/reducers/entryDraft';
|
||||||
import { selectIsLoadingAsset } from '@staticcms/core/reducers/medias';
|
import { selectIsLoadingAsset } from '@staticcms/core/reducers/medias';
|
||||||
@ -213,8 +214,32 @@ const EditorControl = ({
|
|||||||
|
|
||||||
const finalValue = useMemoCompare(value, isEqual);
|
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(() => {
|
return useMemo(() => {
|
||||||
if (!collection || !entry || !config) {
|
if (!collection || !entry || !config || field.widget === 'hidden') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,7 +247,7 @@ const EditorControl = ({
|
|||||||
<ControlContainer $isHidden={isHidden}>
|
<ControlContainer $isHidden={isHidden}>
|
||||||
<>
|
<>
|
||||||
{createElement(widget.control, {
|
{createElement(widget.control, {
|
||||||
key: id,
|
key: `${id}-${version}`,
|
||||||
collection,
|
collection,
|
||||||
config,
|
config,
|
||||||
entry,
|
entry,
|
||||||
|
@ -188,9 +188,7 @@ const EditorControlPane = ({
|
|||||||
/>
|
/>
|
||||||
</LocaleRowWrapper>
|
</LocaleRowWrapper>
|
||||||
) : null}
|
) : null}
|
||||||
{fields
|
{fields.map(field => {
|
||||||
.filter(f => f.widget !== 'hidden')
|
|
||||||
.map(field => {
|
|
||||||
const isTranslatable = isFieldTranslatable(field, locale, i18n?.defaultLocale);
|
const isTranslatable = isFieldTranslatable(field, locale, i18n?.defaultLocale);
|
||||||
const isDuplicate = isFieldDuplicate(field, locale, i18n?.defaultLocale);
|
const isDuplicate = isFieldDuplicate(field, locale, i18n?.defaultLocale);
|
||||||
const isHidden = isFieldHidden(field, locale, i18n?.defaultLocale);
|
const isHidden = isFieldHidden(field, locale, i18n?.defaultLocale);
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import React from 'react';
|
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import type { Field, TemplatePreviewProps } from '@staticcms/core/interface';
|
import type { TemplatePreviewProps } from '@staticcms/core/interface';
|
||||||
|
|
||||||
function isVisible(field: Field) {
|
|
||||||
return field.widget !== 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
const PreviewContainer = styled('div')`
|
const PreviewContainer = styled('div')`
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -21,7 +17,7 @@ const Preview = ({ collection, fields, widgetFor }: TemplatePreviewProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PreviewContainer>
|
<PreviewContainer>
|
||||||
{fields.filter(isVisible).map(field => (
|
{fields.map(field => (
|
||||||
<div key={field.name}>{widgetFor(field.name)}</div>
|
<div key={field.name}>{widgetFor(field.name)}</div>
|
||||||
))}
|
))}
|
||||||
</PreviewContainer>
|
</PreviewContainer>
|
||||||
|
@ -188,7 +188,7 @@ function isReactFragment(value: any): value is ReactFragment {
|
|||||||
|
|
||||||
function getWidget(
|
function getWidget(
|
||||||
config: Config,
|
config: Config,
|
||||||
field: RenderedField,
|
field: RenderedField<Field>,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
value: ValueOrNestedValue | ReactNode,
|
value: ValueOrNestedValue | ReactNode,
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
@ -202,6 +202,10 @@ function getWidget(
|
|||||||
const widget = resolveWidget(field.widget);
|
const widget = resolveWidget(field.widget);
|
||||||
const key = idx ? field.name + '_' + idx : field.name;
|
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.
|
* Use an HOC to provide conditional updates for all previews.
|
||||||
*/
|
*/
|
||||||
@ -214,7 +218,12 @@ function getWidget(
|
|||||||
config={config}
|
config={config}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
value={
|
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 as Record<string, unknown>)[field.name]
|
||||||
: value
|
: value
|
||||||
}
|
}
|
||||||
|
@ -35,21 +35,21 @@ export default function addExtensions() {
|
|||||||
registerBackend('test-repo', TestBackend);
|
registerBackend('test-repo', TestBackend);
|
||||||
registerBackend('proxy', ProxyBackend);
|
registerBackend('proxy', ProxyBackend);
|
||||||
registerWidget([
|
registerWidget([
|
||||||
StringWidget(),
|
|
||||||
NumberWidget(),
|
|
||||||
TextWidget(),
|
|
||||||
ImageWidget(),
|
|
||||||
FileWidget(),
|
|
||||||
SelectWidget(),
|
|
||||||
MarkdownWidget(),
|
|
||||||
ListWidget(),
|
|
||||||
ObjectWidget(),
|
|
||||||
RelationWidget(),
|
|
||||||
BooleanWidget(),
|
BooleanWidget(),
|
||||||
MapWidget(),
|
|
||||||
DateTimeWidget(),
|
|
||||||
CodeWidget(),
|
CodeWidget(),
|
||||||
ColorStringWidget(),
|
ColorStringWidget(),
|
||||||
|
DateTimeWidget(),
|
||||||
|
FileWidget(),
|
||||||
|
ImageWidget(),
|
||||||
|
ListWidget(),
|
||||||
|
MapWidget(),
|
||||||
|
MarkdownWidget(),
|
||||||
|
NumberWidget(),
|
||||||
|
ObjectWidget(),
|
||||||
|
RelationWidget(),
|
||||||
|
SelectWidget(),
|
||||||
|
StringWidget(),
|
||||||
|
TextWidget(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Object.keys(locales).forEach(locale => {
|
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> {
|
export interface WidgetOptions<T = unknown, F extends BaseField = UnknownField> {
|
||||||
validator?: Widget<T, F>['validator'];
|
validator?: Widget<T, F>['validator'];
|
||||||
getValidValue?: Widget<T, F>['getValidValue'];
|
getValidValue?: Widget<T, F>['getValidValue'];
|
||||||
|
getDefaultValue?: Widget<T, F>['getDefaultValue'];
|
||||||
schema?: Widget<T, F>['schema'];
|
schema?: Widget<T, F>['schema'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,6 +322,7 @@ export interface Widget<T = unknown, F extends BaseField = UnknownField> {
|
|||||||
preview?: WidgetPreviewComponent<T, F>;
|
preview?: WidgetPreviewComponent<T, F>;
|
||||||
validator: FieldValidationMethod<T, F>;
|
validator: FieldValidationMethod<T, F>;
|
||||||
getValidValue: (value: T | undefined | null) => T | undefined | null;
|
getValidValue: (value: T | undefined | null) => T | undefined | null;
|
||||||
|
getDefaultValue?: (defaultValue: T | undefined | null, field: F) => T;
|
||||||
schema?: PropertiesSchema<unknown>;
|
schema?: PropertiesSchema<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { getAsset } from '@staticcms/core/actions/media';
|
import { getAsset } from '@staticcms/core/actions/media';
|
||||||
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
||||||
|
import { isNotEmpty } from '../util/string.util';
|
||||||
|
|
||||||
import type { Collection, Entry, FileOrImageField, MarkdownField } from '@staticcms/core/interface';
|
import type { Collection, Entry, FileOrImageField, MarkdownField } from '@staticcms/core/interface';
|
||||||
|
|
||||||
@ -23,5 +24,5 @@ export default function useMediaAsset<T extends FileOrImageField | MarkdownField
|
|||||||
fetchMedia();
|
fetchMedia();
|
||||||
}, [collection, dispatch, entry, field, url]);
|
}, [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,
|
schema,
|
||||||
validator = () => false,
|
validator = () => false,
|
||||||
getValidValue = (value: T | null | undefined) => value,
|
getValidValue = (value: T | null | undefined) => value,
|
||||||
|
getDefaultValue,
|
||||||
}: WidgetOptions<T, F> = {},
|
}: WidgetOptions<T, F> = {},
|
||||||
): void {
|
): void {
|
||||||
if (Array.isArray(name)) {
|
if (Array.isArray(name)) {
|
||||||
@ -162,6 +163,7 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
|||||||
preview: preview as Widget['preview'],
|
preview: preview as Widget['preview'],
|
||||||
validator: validator as Widget['validator'],
|
validator: validator as Widget['validator'],
|
||||||
getValidValue: getValidValue as Widget['getValidValue'],
|
getValidValue: getValidValue as Widget['getValidValue'],
|
||||||
|
getDefaultValue: getDefaultValue as Widget['getDefaultValue'],
|
||||||
schema,
|
schema,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -173,6 +175,7 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
|||||||
options: {
|
options: {
|
||||||
validator = () => false,
|
validator = () => false,
|
||||||
getValidValue = (value: T | undefined | null) => value,
|
getValidValue = (value: T | undefined | null) => value,
|
||||||
|
getDefaultValue,
|
||||||
schema,
|
schema,
|
||||||
} = {},
|
} = {},
|
||||||
} = name;
|
} = name;
|
||||||
@ -190,6 +193,7 @@ export function registerWidget<T = unknown, F extends BaseField = UnknownField>(
|
|||||||
preview,
|
preview,
|
||||||
validator,
|
validator,
|
||||||
getValidValue,
|
getValidValue,
|
||||||
|
getDefaultValue,
|
||||||
schema,
|
schema,
|
||||||
} as unknown as Widget;
|
} as unknown as Widget;
|
||||||
} else {
|
} else {
|
||||||
|
@ -136,21 +136,17 @@ export function selectEntryCollectionTitle(collection: Collection, entry: Entry)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectDefaultSortableFields(
|
export function selectDefaultSortableFields(collection: Collection, backend: Backend) {
|
||||||
collection: Collection,
|
|
||||||
backend: Backend,
|
|
||||||
hasIntegration: boolean,
|
|
||||||
) {
|
|
||||||
let defaultSortable = SORTABLE_FIELDS.map((type: string) => {
|
let defaultSortable = SORTABLE_FIELDS.map((type: string) => {
|
||||||
const field = selectInferedField(collection, type);
|
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
|
// default to commit author if not author field is found
|
||||||
return COMMIT_AUTHOR;
|
return COMMIT_AUTHOR;
|
||||||
}
|
}
|
||||||
return field;
|
return field;
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
if (backend.isGitBackend() && !hasIntegration) {
|
if (backend.isGitBackend()) {
|
||||||
// always have commit date by default
|
// always have commit date by default
|
||||||
defaultSortable = [COMMIT_DATE, ...defaultSortable];
|
defaultSortable = [COMMIT_DATE, ...defaultSortable];
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { CONFIG_SUCCESS } from '../actions/config';
|
|||||||
|
|
||||||
import type { ConfigAction } from '../actions/config';
|
import type { ConfigAction } from '../actions/config';
|
||||||
import type { Collection, Collections } from '../interface';
|
import type { Collection, Collections } from '../interface';
|
||||||
|
import type { RootState } from '../store';
|
||||||
|
|
||||||
export type CollectionsState = Collections;
|
export type CollectionsState = Collections;
|
||||||
|
|
||||||
@ -25,3 +26,7 @@ function collections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default 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 entries, * as fromEntries from './entries';
|
||||||
import entryDraft from './entryDraft';
|
import entryDraft from './entryDraft';
|
||||||
import globalUI from './globalUI';
|
import globalUI from './globalUI';
|
||||||
import integrations, * as fromIntegrations from './integrations';
|
|
||||||
import mediaLibrary from './mediaLibrary';
|
import mediaLibrary from './mediaLibrary';
|
||||||
import medias from './medias';
|
import medias from './medias';
|
||||||
import scroll from './scroll';
|
import scroll from './scroll';
|
||||||
@ -14,7 +13,6 @@ import status from './status';
|
|||||||
|
|
||||||
import type { Collection } from '../interface';
|
import type { Collection } from '../interface';
|
||||||
import type { RootState } from '../store';
|
import type { RootState } from '../store';
|
||||||
import type { IntegrationHooks } from './integrations';
|
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
auth,
|
auth,
|
||||||
@ -24,7 +22,6 @@ const reducers = {
|
|||||||
entries,
|
entries,
|
||||||
entryDraft,
|
entryDraft,
|
||||||
globalUI,
|
globalUI,
|
||||||
integrations,
|
|
||||||
mediaLibrary,
|
mediaLibrary,
|
||||||
medias,
|
medias,
|
||||||
scroll,
|
scroll,
|
||||||
@ -55,11 +52,3 @@ export function selectSearchedEntries(state: RootState, availableCollections: st
|
|||||||
.filter(entryId => availableCollections.indexOf(entryId!.collection) !== -1)
|
.filter(entryId => availableCollections.indexOf(entryId!.collection) !== -1)
|
||||||
.map(entryId => fromEntries.selectEntry(state.entries, entryId!.collection, entryId!.slug));
|
.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 { red } from '@mui/material/colors';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
import Switch from '@mui/material/Switch';
|
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 { BooleanField, WidgetControlProps } from '@staticcms/core/interface';
|
||||||
import type { ChangeEvent, FC } from 'react';
|
import type { ChangeEvent, FC } from 'react';
|
||||||
@ -22,15 +22,6 @@ const BooleanControl: FC<WidgetControlProps<boolean, BooleanField>> = ({
|
|||||||
[onChange],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof internalValue !== 'boolean') {
|
|
||||||
setInternalValue(false);
|
|
||||||
setTimeout(() => {
|
|
||||||
onChange(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [internalValue, onChange]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key="boolean-field-label"
|
key="boolean-field-label"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import controlComponent from './BooleanControl';
|
import controlComponent from './BooleanControl';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
import type { BooleanField, WidgetParam } from '@staticcms/core/interface';
|
import type { BooleanField, WidgetParam } from '@staticcms/core/interface';
|
||||||
|
|
||||||
@ -6,9 +7,15 @@ const BooleanWidget = (): WidgetParam<boolean, BooleanField> => {
|
|||||||
return {
|
return {
|
||||||
name: 'boolean',
|
name: 'boolean',
|
||||||
controlComponent,
|
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;
|
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 { styled } from '@mui/material/styles';
|
||||||
import { loadLanguage } from '@uiw/codemirror-extensions-langs';
|
import { loadLanguage } from '@uiw/codemirror-extensions-langs';
|
||||||
import CodeMirror from '@uiw/react-codemirror';
|
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 ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar';
|
||||||
import Outline from '@staticcms/core/components/UI/Outline';
|
import Outline from '@staticcms/core/components/UI/Outline';
|
||||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
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 transientOptions from '@staticcms/core/lib/util/transientOptions';
|
||||||
import languages from './data/languages';
|
import languages from './data/languages';
|
||||||
import SettingsButton from './SettingsButton';
|
import SettingsButton from './SettingsButton';
|
||||||
import SettingsPane from './SettingsPane';
|
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 { LanguageName } from '@uiw/codemirror-extensions-langs';
|
||||||
import type { FC } from 'react';
|
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 valueIsMap = useMemo(() => Boolean(!field.output_code_only), [field.output_code_only]);
|
||||||
|
|
||||||
const [internalValue, setInternalValue] = useState(value ?? '');
|
const [internalValue, setInternalValue] = useState(value ?? '');
|
||||||
const [lang, setLang] = useState(() => {
|
const [lang, setLang] = useState<ProcessedCodeLanguage | null>(null);
|
||||||
return valueIsMap && typeof internalValue !== 'string'
|
|
||||||
? internalValue && internalValue[keys.lang]
|
|
||||||
: field.default_language ?? '';
|
|
||||||
});
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
const [hasFocus, setHasFocus] = useState(false);
|
const [hasFocus, setHasFocus] = useState(false);
|
||||||
@ -105,20 +106,20 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
|||||||
(newValue: string) => {
|
(newValue: string) => {
|
||||||
if (valueIsMap) {
|
if (valueIsMap) {
|
||||||
handleOnChange({
|
handleOnChange({
|
||||||
...(typeof internalValue !== 'string' ? internalValue : {}),
|
lang: lang?.label ?? '',
|
||||||
code: newValue,
|
code: newValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleOnChange(newValue);
|
handleOnChange(newValue);
|
||||||
},
|
},
|
||||||
[handleOnChange, internalValue, valueIsMap],
|
[handleOnChange, lang?.label, valueIsMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadedLangExtension = useMemo(() => {
|
const loadedLangExtension = useMemo(() => {
|
||||||
if (!lang) {
|
if (!lang) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return loadLanguage(lang as LanguageName);
|
return loadLanguage(lang.codemirror_mode as LanguageName);
|
||||||
}, [lang]);
|
}, [lang]);
|
||||||
|
|
||||||
const extensions = useMemo(() => {
|
const extensions = useMemo(() => {
|
||||||
@ -154,9 +155,29 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
|||||||
[field.allow_language_selection],
|
[field.allow_language_selection],
|
||||||
);
|
);
|
||||||
|
|
||||||
const availableLanguages = languages.map(language =>
|
const availableLanguages = languages.map(language => valueToOption(language.label));
|
||||||
valueToOption({ name: language.codemirror_mode, label: 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 (
|
return (
|
||||||
<StyledCodeControlWrapper>
|
<StyledCodeControlWrapper>
|
||||||
@ -168,9 +189,9 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
|
|||||||
hideSettings={hideSettings}
|
hideSettings={hideSettings}
|
||||||
uniqueId={uniqueId}
|
uniqueId={uniqueId}
|
||||||
languages={availableLanguages}
|
languages={availableLanguages}
|
||||||
language={valueToOption(lang)}
|
language={valueToOption(lang?.label ?? '')}
|
||||||
allowLanguageSelection={allowLanguageSelection}
|
allowLanguageSelection={allowLanguageSelection}
|
||||||
onChangeLanguage={newLang => setLang(newLang)}
|
onChangeLanguage={handleSetLanguage}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -11,6 +11,29 @@ const CodeWidget = (): WidgetParam<string | { [key: string]: string }, CodeField
|
|||||||
previewComponent,
|
previewComponent,
|
||||||
options: {
|
options: {
|
||||||
schema,
|
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',
|
type: 'object',
|
||||||
properties: { code: { type: 'string' }, lang: { type: 'string' } },
|
properties: { code: { type: 'string' }, lang: { type: 'string' } },
|
||||||
},
|
},
|
||||||
|
default: {
|
||||||
|
oneOf: [{ type: 'string' }, { type: 'object' }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import controlComponent from './ColorControl';
|
import controlComponent from './ColorControl';
|
||||||
import previewComponent from './ColorPreview';
|
import previewComponent from './ColorPreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
import type { ColorField, WidgetParam } from '@staticcms/core/interface';
|
import type { ColorField, WidgetParam } from '@staticcms/core/interface';
|
||||||
|
|
||||||
@ -8,9 +9,16 @@ const ColorWidget = (): WidgetParam<string, ColorField> => {
|
|||||||
name: 'color',
|
name: 'color',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
options: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export { controlComponent as ColorControl, previewComponent as ColorPreview };
|
export {
|
||||||
|
controlComponent as ColorControl,
|
||||||
|
previewComponent as ColorPreview,
|
||||||
|
schema as ColorSchema,
|
||||||
|
};
|
||||||
|
|
||||||
export default ColorWidget;
|
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 formatISO from 'date-fns/formatISO';
|
||||||
import parse from 'date-fns/parse';
|
import parse from 'date-fns/parse';
|
||||||
import parseISO from 'date-fns/parseISO';
|
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 { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
|
||||||
import type { DateTimeField, TranslatedProps, WidgetControlProps } from '@staticcms/core/interface';
|
import type { DateTimeField, TranslatedProps, WidgetControlProps } from '@staticcms/core/interface';
|
||||||
import type { FC, MouseEvent } from 'react';
|
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')`
|
const StyledNowButton = styled('div')`
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
`;
|
`;
|
||||||
@ -77,14 +82,6 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
|||||||
|
|
||||||
const timezoneOffset = useMemo(() => new Date().getTimezoneOffset() * 60000, []);
|
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(() => {
|
const inputFormat = useMemo(() => {
|
||||||
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
|
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
|
||||||
const formatParts: string[] = [];
|
const formatParts: string[] = [];
|
||||||
@ -105,13 +102,13 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
|||||||
}, [dateFormat, timeFormat]);
|
}, [dateFormat, timeFormat]);
|
||||||
|
|
||||||
const defaultValue = useMemo(() => {
|
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
|
return field.default === undefined
|
||||||
? format
|
? format
|
||||||
? formatDate(today, format)
|
? formatDate(today, format)
|
||||||
: formatDate(today, inputFormat)
|
: formatDate(today, inputFormat)
|
||||||
: field.default;
|
: field.default;
|
||||||
}, [field.default, field.picker_utc, format, inputFormat, localToUTC]);
|
}, [field.default, field.picker_utc, format, inputFormat, timezoneOffset]);
|
||||||
|
|
||||||
const [internalValue, setInternalValue] = useState(value);
|
const [internalValue, setInternalValue] = useState(value);
|
||||||
|
|
||||||
@ -138,7 +135,7 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const adjustedValue = field.picker_utc ? localToUTC(datetime) : datetime;
|
const adjustedValue = field.picker_utc ? localToUTC(datetime, timezoneOffset) : datetime;
|
||||||
|
|
||||||
let formattedValue: string;
|
let formattedValue: string;
|
||||||
if (format) {
|
if (format) {
|
||||||
@ -149,20 +146,9 @@ const DateTimeControl: FC<WidgetControlProps<string, DateTimeField>> = ({
|
|||||||
setInternalValue(formattedValue);
|
setInternalValue(formattedValue);
|
||||||
onChange(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(() => {
|
const dateTimePicker = useMemo(() => {
|
||||||
if (!internalValue) {
|
if (!internalValue) {
|
||||||
return null;
|
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 previewComponent from './DateTimePreview';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
|
|
||||||
@ -11,6 +15,38 @@ const DateTimeWidget = (): WidgetParam<string, DateTimeField> => {
|
|||||||
previewComponent,
|
previewComponent,
|
||||||
options: {
|
options: {
|
||||||
schema,
|
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' }] },
|
date_format: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
|
||||||
time_format: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
|
time_format: { oneOf: [{ type: 'string' }, { type: 'boolean' }] },
|
||||||
picker_utc: { type: 'boolean' },
|
picker_utc: { type: 'boolean' },
|
||||||
|
default: { type: 'string' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
export default {
|
export default {
|
||||||
properties: {
|
properties: {
|
||||||
allow_multiple: { type: 'boolean' },
|
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 ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar';
|
||||||
import Outline from '@staticcms/core/components/UI/Outline';
|
import Outline from '@staticcms/core/components/UI/Outline';
|
||||||
import { borders, effects, lengths, shadows } from '@staticcms/core/components/UI/styles';
|
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 useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
||||||
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||||
import { basename, transientOptions } from '@staticcms/core/lib/util';
|
import { basename, transientOptions } from '@staticcms/core/lib/util';
|
||||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
Collection,
|
||||||
|
Entry,
|
||||||
FileOrImageField,
|
FileOrImageField,
|
||||||
GetAssetFunction,
|
|
||||||
WidgetControlProps,
|
WidgetControlProps,
|
||||||
} from '@staticcms/core/interface';
|
} from '@staticcms/core/interface';
|
||||||
import type { FC, MouseEvent, MouseEventHandler } from 'react';
|
import type { FC, MouseEvent, MouseEventHandler } from 'react';
|
||||||
@ -102,11 +104,16 @@ const StyledImage = styled('img')`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
interface ImageProps {
|
interface ImageProps {
|
||||||
src: string;
|
value: string;
|
||||||
|
collection: Collection<FileOrImageField>;
|
||||||
|
field: FileOrImageField;
|
||||||
|
entry: Entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Image: FC<ImageProps> = ({ src }) => {
|
const Image: FC<ImageProps> = ({ value, collection, field, entry }) => {
|
||||||
return <StyledImage key="image" role="presentation" src={src} />;
|
const assetSource = useMediaAsset(value, collection, field, entry);
|
||||||
|
|
||||||
|
return <StyledImage key="image" role="presentation" src={assetSource} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SortableImageButtonsProps {
|
interface SortableImageButtonsProps {
|
||||||
@ -129,34 +136,25 @@ const SortableImageButtons: FC<SortableImageButtonsProps> = ({ onRemove, onRepla
|
|||||||
|
|
||||||
interface SortableImageProps {
|
interface SortableImageProps {
|
||||||
itemValue: string;
|
itemValue: string;
|
||||||
getAsset: GetAssetFunction<FileOrImageField>;
|
collection: Collection<FileOrImageField>;
|
||||||
field: FileOrImageField;
|
field: FileOrImageField;
|
||||||
|
entry: Entry;
|
||||||
onRemove: MouseEventHandler;
|
onRemove: MouseEventHandler;
|
||||||
onReplace: MouseEventHandler;
|
onReplace: MouseEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortableImage: FC<SortableImageProps> = ({
|
const SortableImage: FC<SortableImageProps> = ({
|
||||||
itemValue,
|
itemValue,
|
||||||
getAsset,
|
collection,
|
||||||
field,
|
field,
|
||||||
|
entry,
|
||||||
onRemove,
|
onRemove,
|
||||||
onReplace,
|
onReplace,
|
||||||
}: SortableImageProps) => {
|
}: 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ImageWrapper key="image-wrapper" $sortable>
|
<ImageWrapper key="image-wrapper" $sortable>
|
||||||
<Image key="image" src={assetSource} />
|
<Image key="image" value={itemValue} collection={collection} field={field} entry={entry} />
|
||||||
</ImageWrapper>
|
</ImageWrapper>
|
||||||
<SortableImageButtons
|
<SortableImageButtons
|
||||||
key="image-buttons"
|
key="image-buttons"
|
||||||
@ -212,12 +210,13 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
|||||||
const FileControl: FC<WidgetControlProps<string | string[], FileOrImageField>> = memo(
|
const FileControl: FC<WidgetControlProps<string | string[], FileOrImageField>> = memo(
|
||||||
({
|
({
|
||||||
value,
|
value,
|
||||||
|
collection,
|
||||||
field,
|
field,
|
||||||
|
entry,
|
||||||
onChange,
|
onChange,
|
||||||
openMediaLibrary,
|
openMediaLibrary,
|
||||||
clearMediaControl,
|
clearMediaControl,
|
||||||
removeMediaControl,
|
removeMediaControl,
|
||||||
getAsset,
|
|
||||||
hasErrors,
|
hasErrors,
|
||||||
t,
|
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(() => {
|
const renderedImagesLinks = useMemo(() => {
|
||||||
if (forImage) {
|
if (forImage) {
|
||||||
if (!internalValue) {
|
if (!internalValue) {
|
||||||
@ -373,8 +355,9 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
|||||||
<SortableImage
|
<SortableImage
|
||||||
key={`item-${itemValue}`}
|
key={`item-${itemValue}`}
|
||||||
itemValue={itemValue}
|
itemValue={itemValue}
|
||||||
getAsset={getAsset}
|
collection={collection}
|
||||||
field={field}
|
field={field}
|
||||||
|
entry={entry}
|
||||||
onRemove={onRemoveOne(index)}
|
onRemove={onRemoveOne(index)}
|
||||||
onReplace={onReplaceOne(index)}
|
onReplace={onReplaceOne(index)}
|
||||||
/>
|
/>
|
||||||
@ -385,7 +368,13 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageWrapper key="single-image-wrapper">
|
<ImageWrapper key="single-image-wrapper">
|
||||||
<Image key="single-image" src={assetSource} />
|
<Image
|
||||||
|
key="single-image"
|
||||||
|
value={internalValue}
|
||||||
|
collection={collection}
|
||||||
|
field={field}
|
||||||
|
entry={entry}
|
||||||
|
/>
|
||||||
</ImageWrapper>
|
</ImageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -403,7 +392,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <FileLinks key="single-file-links">{renderFileLink(internalValue)}</FileLinks>;
|
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 content = useMemo(() => {
|
||||||
const subject = forImage ? 'image' : 'file';
|
const subject = forImage ? 'image' : 'file';
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
export default {
|
export default {
|
||||||
properties: {
|
properties: {
|
||||||
allow_multiple: { type: 'boolean' },
|
allow_multiple: { type: 'boolean' },
|
||||||
|
default: {
|
||||||
|
oneOf: [
|
||||||
|
{ type: 'string' },
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -311,11 +311,17 @@ const ListControl: FC<WidgetControlProps<ObjectValue[], ListField>> = ({
|
|||||||
<DndContext key="dnd-context" onDragEnd={handleDragEnd}>
|
<DndContext key="dnd-context" onDragEnd={handleDragEnd}>
|
||||||
<SortableContext items={keys}>
|
<SortableContext items={keys}>
|
||||||
<StyledSortableList $collapsed={collapsed}>
|
<StyledSortableList $collapsed={collapsed}>
|
||||||
{internalValue.map((item, index) => (
|
{internalValue.map((item, index) => {
|
||||||
|
const key = keys[index];
|
||||||
|
if (!key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<SortableItem
|
<SortableItem
|
||||||
index={index}
|
index={index}
|
||||||
key={keys[index]}
|
key={key}
|
||||||
id={keys[index]}
|
id={key}
|
||||||
item={item}
|
item={item}
|
||||||
valueType={valueType}
|
valueType={valueType}
|
||||||
handleRemove={handleRemove}
|
handleRemove={handleRemove}
|
||||||
@ -331,7 +337,8 @@ const ListControl: FC<WidgetControlProps<ObjectValue[], ListField>> = ({
|
|||||||
value={item as Record<string, ObjectValue>}
|
value={item as Record<string, ObjectValue>}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</StyledSortableList>
|
</StyledSortableList>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
@ -6,7 +6,7 @@ import type { MapField, WidgetPreviewProps } from '@staticcms/core/interface';
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
const MapPreview: FC<WidgetPreviewProps<string, MapField>> = ({ value }) => {
|
const MapPreview: FC<WidgetPreviewProps<string, MapField>> = ({ value }) => {
|
||||||
return <WidgetPreviewContainer>{value ? value.toString() : null}</WidgetPreviewContainer>;
|
return <WidgetPreviewContainer>{value}</WidgetPreviewContainer>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MapPreview;
|
export default MapPreview;
|
||||||
|
@ -58,7 +58,7 @@ const MarkdownControl: FC<WidgetControlProps<string, MarkdownField>> = ({
|
|||||||
const handleOnChange = useCallback(
|
const handleOnChange = useCallback(
|
||||||
(slateValue: MdValue) => {
|
(slateValue: MdValue) => {
|
||||||
const newValue = slateValue.map(v => serialize(v as BlockType | LeafType)).join('\n');
|
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) {
|
if (newValue !== internalValue) {
|
||||||
setInternalValue(newValue);
|
setInternalValue(newValue);
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
@ -73,7 +73,7 @@ const MarkdownControl: FC<WidgetControlProps<string, MarkdownField>> = ({
|
|||||||
|
|
||||||
const [slateValue, loaded] = useMarkdownToSlate(internalValue);
|
const [slateValue, loaded] = useMarkdownToSlate(internalValue);
|
||||||
|
|
||||||
console.log('[Plate] slateValue', slateValue);
|
// console.log('[Plate] slateValue', slateValue);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => (
|
() => (
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import controlComponent from './MarkdownControl';
|
import controlComponent from './MarkdownControl';
|
||||||
import previewComponent from './MarkdownPreview';
|
import previewComponent from './MarkdownPreview';
|
||||||
|
import schema from './schema';
|
||||||
|
|
||||||
import type { MarkdownField, WidgetParam } from '@staticcms/core/interface';
|
import type { MarkdownField, WidgetParam } from '@staticcms/core/interface';
|
||||||
|
|
||||||
@ -8,10 +9,17 @@ const MarkdownWidget = (): WidgetParam<string, MarkdownField> => {
|
|||||||
name: 'markdown',
|
name: 'markdown',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
options: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export * from './plate';
|
export * from './plate';
|
||||||
export { controlComponent as MarkdownControl, previewComponent as MarkdownPreview };
|
export {
|
||||||
|
controlComponent as MarkdownControl,
|
||||||
|
previewComponent as MarkdownPreview,
|
||||||
|
schema as MarkdownSchema,
|
||||||
|
};
|
||||||
|
|
||||||
export default MarkdownWidget;
|
export default MarkdownWidget;
|
||||||
|
@ -55,6 +55,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
|
import useUUID from '@staticcms/core/lib/hooks/useUUID';
|
||||||
import { BalloonToolbar } from './components/balloon-toolbar';
|
import { BalloonToolbar } from './components/balloon-toolbar';
|
||||||
import { BlockquoteElement } from './components/nodes/blockquote';
|
import { BlockquoteElement } from './components/nodes/blockquote';
|
||||||
import { CodeBlockElement } from './components/nodes/code-block';
|
import { CodeBlockElement } from './components/nodes/code-block';
|
||||||
@ -236,11 +237,14 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
[components],
|
[components],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const id = useUUID();
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => (
|
() => (
|
||||||
<StyledPlateEditor>
|
<StyledPlateEditor>
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<PlateProvider<MdValue>
|
<PlateProvider<MdValue>
|
||||||
|
id={id}
|
||||||
key="plate-provider"
|
key="plate-provider"
|
||||||
initialValue={initialValue}
|
initialValue={initialValue}
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
@ -258,6 +262,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
|
|||||||
<div key="editor-wrapper" ref={editorContainerRef} style={styles.container}>
|
<div key="editor-wrapper" ref={editorContainerRef} style={styles.container}>
|
||||||
<Plate
|
<Plate
|
||||||
key="editor"
|
key="editor"
|
||||||
|
id={id}
|
||||||
editableProps={{
|
editableProps={{
|
||||||
...editableProps,
|
...editableProps,
|
||||||
onFocus: handleOnFocus,
|
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' },
|
value_type: { type: 'string' },
|
||||||
min: { type: 'number' },
|
min: { type: 'number' },
|
||||||
max: { type: 'number' },
|
max: { type: 'number' },
|
||||||
|
default: { type: 'number' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import * as fuzzy from 'fuzzy';
|
||||||
import Autocomplete from '@mui/material/Autocomplete';
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
@ -6,7 +7,13 @@ import get from 'lodash/get';
|
|||||||
import uniqBy from 'lodash/uniqBy';
|
import uniqBy from 'lodash/uniqBy';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
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 { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
import {
|
import {
|
||||||
addFileTemplateFields,
|
addFileTemplateFields,
|
||||||
@ -14,7 +21,10 @@ import {
|
|||||||
expandPath,
|
expandPath,
|
||||||
extractTemplateVars,
|
extractTemplateVars,
|
||||||
} from '@staticcms/core/lib/widgets/stringTemplate';
|
} 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 {
|
import type {
|
||||||
Entry,
|
Entry,
|
||||||
EntryData,
|
EntryData,
|
||||||
@ -106,11 +116,10 @@ function getSelectedValue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RelationControl: FC<WidgetControlProps<string | string[], RelationField>> = ({
|
const RelationControl: FC<WidgetControlProps<string | string[], RelationField>> = ({
|
||||||
path,
|
|
||||||
value,
|
value,
|
||||||
field,
|
field,
|
||||||
onChange,
|
onChange,
|
||||||
query,
|
config,
|
||||||
locale,
|
locale,
|
||||||
label,
|
label,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
@ -118,6 +127,12 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
|
|||||||
const [internalValue, setInternalValue] = useState(value);
|
const [internalValue, setInternalValue] = useState(value);
|
||||||
const [initialOptions, setInitialOptions] = useState<HitOption[]>([]);
|
const [initialOptions, setInitialOptions] = useState<HitOption[]>([]);
|
||||||
|
|
||||||
|
const searchCollectionSelector = useMemo(
|
||||||
|
() => selectCollection(field.collection),
|
||||||
|
[field.collection],
|
||||||
|
);
|
||||||
|
const searchCollection = useAppSelector(searchCollectionSelector);
|
||||||
|
|
||||||
const isMultiple = useMemo(() => {
|
const isMultiple = useMemo(() => {
|
||||||
return field.multiple ?? false;
|
return field.multiple ?? false;
|
||||||
}, [field.multiple]);
|
}, [field.multiple]);
|
||||||
@ -183,6 +198,7 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [options, setOptions] = useState<HitOption[]>([]);
|
const [options, setOptions] = useState<HitOption[]>([]);
|
||||||
|
const [entries, setEntries] = useState<Entry[]>([]);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const valueNotEmpty = useMemo(
|
const valueNotEmpty = useMemo(
|
||||||
() => (Array.isArray(internalValue) ? internalValue.length > 0 : isNotEmpty(internalValue)),
|
() => (Array.isArray(internalValue) ? internalValue.length > 0 : isNotEmpty(internalValue)),
|
||||||
@ -193,33 +209,45 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
|
|||||||
[open, valueNotEmpty, options.length],
|
[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(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
if (!loading || !searchCollection) {
|
||||||
if (!loading) {
|
return;
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
const getOptions = async () => {
|
||||||
const collection = field.collection;
|
const backend = currentBackend(config);
|
||||||
const optionsLength = field.options_length || 20;
|
|
||||||
const searchFieldsArray = field.search_fields;
|
|
||||||
const file = field.file;
|
|
||||||
|
|
||||||
const response = await query(path, collection, searchFieldsArray, '', file, optionsLength);
|
const options = await backend.listAllEntries(searchCollection);
|
||||||
if (alive) {
|
setEntries(options);
|
||||||
if (response?.type === QUERY_SUCCESS) {
|
setOptions(parseHitOptions(options));
|
||||||
const hits = response.payload.hits ?? [];
|
|
||||||
const options = parseHitOptions(hits);
|
|
||||||
setOptions(uniqOptions(initialOptions, options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
alive = false;
|
|
||||||
};
|
};
|
||||||
// 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 uniqueOptions = uniqOptions(initialOptions, options);
|
||||||
const selectedValue = getSelectedValue(internalValue, uniqueOptions, isMultiple);
|
const selectedValue = getSelectedValue(internalValue, uniqueOptions, isMultiple);
|
||||||
@ -230,6 +258,7 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
|
|||||||
disablePortal
|
disablePortal
|
||||||
options={uniqueOptions}
|
options={uniqueOptions}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
filterOptions={filterOptions}
|
||||||
renderInput={params => (
|
renderInput={params => (
|
||||||
<TextField
|
<TextField
|
||||||
key="relation-control-input"
|
key="relation-control-input"
|
||||||
|
@ -9,6 +9,17 @@ export default {
|
|||||||
max: { type: 'integer' },
|
max: { type: 'integer' },
|
||||||
display_fields: { type: 'array', minItems: 1, items: { type: 'string' } },
|
display_fields: { type: 'array', minItems: 1, items: { type: 'string' } },
|
||||||
options_length: { type: 'integer' },
|
options_length: { type: 'integer' },
|
||||||
|
default: {
|
||||||
|
oneOf: [
|
||||||
|
{ type: 'string' },
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
oneOf: [
|
oneOf: [
|
||||||
{
|
{
|
||||||
|
@ -3,6 +3,18 @@ export default {
|
|||||||
multiple: { type: 'boolean' },
|
multiple: { type: 'boolean' },
|
||||||
min: { type: 'integer' },
|
min: { type: 'integer' },
|
||||||
max: { type: 'integer' },
|
max: { type: 'integer' },
|
||||||
|
default: {
|
||||||
|
oneOf: [
|
||||||
|
{ type: 'string' },
|
||||||
|
{ type: 'number' },
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
oneOf: [{ type: 'string' }, { type: 'number' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import schema from './schema';
|
||||||
import controlComponent from './StringControl';
|
import controlComponent from './StringControl';
|
||||||
import previewComponent from './StringPreview';
|
import previewComponent from './StringPreview';
|
||||||
|
|
||||||
@ -8,9 +9,16 @@ const StringWidget = (): WidgetParam<string, StringOrTextField> => {
|
|||||||
name: 'string',
|
name: 'string',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
options: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export { controlComponent as StringControl, previewComponent as StringPreview };
|
export {
|
||||||
|
controlComponent as StringControl,
|
||||||
|
previewComponent as StringPreview,
|
||||||
|
schema as StringSchema,
|
||||||
|
};
|
||||||
|
|
||||||
export default StringWidget;
|
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 controlComponent from './TextControl';
|
||||||
import previewComponent from './TextPreview';
|
import previewComponent from './TextPreview';
|
||||||
|
|
||||||
@ -8,9 +9,12 @@ const TextWidget = (): WidgetParam<string, StringOrTextField> => {
|
|||||||
name: 'text',
|
name: 'text',
|
||||||
controlComponent,
|
controlComponent,
|
||||||
previewComponent,
|
previewComponent,
|
||||||
|
options: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export { controlComponent as TextControl, previewComponent as TextPreview };
|
export { controlComponent as TextControl, previewComponent as TextPreview, schema as TextSchema };
|
||||||
|
|
||||||
export default TextWidget;
|
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