add-cms
This commit is contained in:
383
source/admin/packages/decap-cms-widget-list/CHANGELOG.md
Normal file
383
source/admin/packages/decap-cms-widget-list/CHANGELOG.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.3.0...decap-cms-widget-list@3.4.0) (2025-06-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.2.2...decap-cms-widget-list@3.3.0) (2025-01-29)
|
||||
|
||||
### Features
|
||||
|
||||
- visual editing (click-to-edit) ([#7374](https://github.com/decaporg/decap-cms/issues/7374)) ([989c2dd](https://github.com/decaporg/decap-cms/commit/989c2dd6ed80f69b572b8b73c4e37b5106ae04fb))
|
||||
|
||||
## [3.2.2](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.2.1...decap-cms-widget-list@3.2.2) (2024-08-13)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "Update dependencies (#7264)" ([22d483a](https://github.com/decaporg/decap-cms/commit/22d483a5b0c654071ae05735ac4f49abdc13d38c)), closes [#7264](https://github.com/decaporg/decap-cms/issues/7264)
|
||||
|
||||
## [3.2.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.2.0...decap-cms-widget-list@3.2.1) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.1.1...decap-cms-widget-list@3.2.0) (2024-08-07)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.1.0-beta.1...decap-cms-widget-list@3.1.1) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.1.0-beta.1...decap-cms-widget-list@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.1.0-beta.0...decap-cms-widget-list@3.1.0-beta.1) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.1.0...decap-cms-widget-list@3.1.0-beta.0) (2023-10-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([b89fc89](https://github.com/decaporg/decap-cms/commit/b89fc894dfbb5f4136b2e5427fd25a29378a58c6))
|
||||
|
||||
## [3.0.4](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.0.3...decap-cms-widget-list@3.0.4) (2023-10-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [3.0.3](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.0.2...decap-cms-widget-list@3.0.3) (2023-10-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [3.0.2](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.0.1...decap-cms-widget-list@3.0.2) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@3.0.0...decap-cms-widget-list@3.0.1) (2023-08-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update peer dependencies ([#6886](https://github.com/decaporg/decap-cms/issues/6886)) ([e580ce5](https://github.com/decaporg/decap-cms/commit/e580ce52ce5f80fa040e8fbcab7fed0744f4f695))
|
||||
|
||||
# [3.0.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@2.11.0...decap-cms-widget-list@3.0.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
# [2.11.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@2.11.0-beta.0...decap-cms-widget-list@2.11.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
# 2.11.0-beta.0 (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
- rename packages ([#6863](https://github.com/decaporg/decap-cms/issues/6863)) ([d515e7b](https://github.com/decaporg/decap-cms/commit/d515e7bd33216a775d96887b08c4f7b1962941bb))
|
||||
|
||||
## [2.10.2-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@2.10.1...decap-cms-widget-list@2.10.2-beta.0) (2023-07-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.10.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@2.10.0...decap-cms-widget-list@2.10.1) (2021-08-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-list:** add missing translations in widget list top bar ([#5471](https://github.com/decaporg/decap-cms/issues/5471)) ([#5679](https://github.com/decaporg/decap-cms/issues/5679)) ([db560cc](https://github.com/decaporg/decap-cms/commit/db560cc082fcc0a9842919e28f715e44a6e4625a))
|
||||
|
||||
# [2.10.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@2.9.3...decap-cms-widget-list@2.10.0) (2021-07-14)
|
||||
|
||||
### Features
|
||||
|
||||
- **list:** Add heading for list widgets ([#5544](https://github.com/decaporg/decap-cms/issues/5544)) ([d60df87](https://github.com/decaporg/decap-cms/commit/d60df8786d7ea01af94c86a6e889654ce20e53ca))
|
||||
|
||||
## [2.9.3](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-list@2.9.2...decap-cms-widget-list@2.9.3) (2021-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **list-control:** better value validation ([#5592](https://github.com/decaporg/decap-cms/issues/5592)) ([fb0f825](https://github.com/decaporg/decap-cms/commit/fb0f8259eae60c27a3a35c3db834c8311131d328))
|
||||
|
||||
## [2.9.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.9.1...decap-cms-widget-list@2.9.2) (2021-06-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.9.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.9.0...decap-cms-widget-list@2.9.1) (2021-05-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency react-sortable-hoc to v2 ([#5371](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/5371)) ([b5dabc2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/b5dabc212953348bee83d41c36ca1f949c87b6b5))
|
||||
|
||||
# [2.9.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.8.5...decap-cms-widget-list@2.9.0) (2021-05-04)
|
||||
|
||||
### Features
|
||||
|
||||
- added react 17 as peer dependency in packages ([#5316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
|
||||
|
||||
## [2.8.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.8.4...decap-cms-widget-list@2.8.5) (2021-04-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow any default list as default value for list widgets ([#5030](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/5030)) ([83c2354](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/83c235423e76023fe10f167c8f9087cba7a4c922))
|
||||
|
||||
## [2.8.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.8.3...decap-cms-widget-list@2.8.4) (2021-02-25)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.8.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.8.2...decap-cms-widget-list@2.8.3) (2021-02-23)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.8.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.8.1...decap-cms-widget-list@2.8.2) (2021-02-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.8.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.8.0...decap-cms-widget-list@2.8.1) (2020-11-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- list markdown widgets ([#4492](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/4492)) ([4789d20](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/4789d205386f589c3556df32700df25ad078904f))
|
||||
|
||||
# [2.8.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.7.0...decap-cms-widget-list@2.8.0) (2020-10-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-list:** don't split an empty string ([#4464](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/4464)) ([f3a8eac](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/f3a8eac816710dbc639ad40b06742fa90885e613))
|
||||
|
||||
### Features
|
||||
|
||||
- add add_to_top option to list widget ([#4465](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/4465)) ([4c962f9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/4c962f9149722074652c8939486650a4ddb1d7b3))
|
||||
|
||||
# [2.7.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.6.6...decap-cms-widget-list@2.7.0) (2020-10-20)
|
||||
|
||||
### Features
|
||||
|
||||
- **widget-list:** add min max configuration ([#4394](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/4394)) ([5fdfe40](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/5fdfe40dd29e9e22c9ae7d6219bc057f7ea7280b))
|
||||
|
||||
## [2.6.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.6.5...decap-cms-widget-list@2.6.6) (2020-09-20)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.6.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.6.4...decap-cms-widget-list@2.6.5) (2020-09-15)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## 2.6.4 (2020-09-08)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
|
||||
|
||||
## 2.6.3 (2020-08-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/82624879ccbcb16610090041db28f00714d924c8))
|
||||
|
||||
## 2.6.2 (2020-07-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/118d50a7a70295f25073e564b5161aa2b9883056))
|
||||
|
||||
## [2.6.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.6.0...decap-cms-widget-list@2.6.1) (2020-07-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **prop-types:** check for react components via PropTypes.elementType ([#4025](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/4025)) ([d3831b1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/d3831b1ed44fcff51a63f6645a5aa68332467dab))
|
||||
|
||||
# [2.6.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.5.1...decap-cms-widget-list@2.6.0) (2020-06-18)
|
||||
|
||||
### Features
|
||||
|
||||
- add widgets schema validation ([#3841](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/3841)) ([2b46608](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/2b46608f86d22c8ad34f75e396be7c34462d9e99))
|
||||
|
||||
## [2.5.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.5.0...decap-cms-widget-list@2.5.1) (2020-06-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Error UI improvements for nested lists/objects ([#3726](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/3726)) ([3978578](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/397857855b2c8514c2f7ce83756af6b6698abc3d))
|
||||
|
||||
# [2.5.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.4.6...decap-cms-widget-list@2.5.0) (2020-05-19)
|
||||
|
||||
### Features
|
||||
|
||||
- **decap-cms-widget-list:** allow 'summary' field ([#3616](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/3616)) ([7cc4c89](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/7cc4c89539ddbd410ec7a1767c66f04e8d711cc5))
|
||||
- **widget-list:** add hiding list content with minimize_collapsed option ([#3607](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/3607)) ([4dd58c5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/4dd58c5dcb22f4c0456a36d6c38883b54d8a7c4d))
|
||||
|
||||
## [2.4.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.4.4...decap-cms-widget-list@2.4.6) (2020-05-04)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.4.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.4.3...decap-cms-widget-list@2.4.4) (2020-04-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- list widget item collapse toggle ([#3623](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/3623)) ([3a666e2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/3a666e26b507f16767d0dafb82cd8a030424eec3))
|
||||
- list widget validation after sort ([#3611](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/3611)) ([3d0856e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/3d0856ea884f5acdceb23ea09e39fb67b0c902a9))
|
||||
|
||||
## [2.4.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.4.2...decap-cms-widget-list@2.4.3) (2020-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- remove empty list item ([#3245](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/3245)) ([f915bf3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/f915bf375fc8faa9b4ed5b3684861dfbe462a032))
|
||||
|
||||
## [2.4.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.4.1...decap-cms-widget-list@2.4.2) (2020-02-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- change getAsset to not return a promise ([#3232](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/3232)) ([ab685e8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/ab685e85943d1ac48142f157683bc2126fd6af16))
|
||||
|
||||
## [2.4.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.4.0...decap-cms-widget-list@2.4.1) (2020-01-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- cleanup nested widget validation ([#2991](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2991)) ([e4ba4d9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/e4ba4d9d749c864e594c10e0bb31b0b8c4e6e60b))
|
||||
|
||||
# [2.4.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.4.0-beta.0...decap-cms-widget-list@2.4.0) (2019-12-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
# [2.4.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.5-beta.1...decap-cms-widget-list@2.4.0-beta.0) (2019-12-16)
|
||||
|
||||
### Features
|
||||
|
||||
- Code Widget + Markdown Widget Internal Overhaul ([#2828](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2828)) ([18c579d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/18c579d0e9f0ff71ed8c52f5c66f2309259af054))
|
||||
|
||||
## [2.3.5-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.5-beta.0...decap-cms-widget-list@2.3.5-beta.1) (2019-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-list:** when single field value is object widget ([#2387](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2387)) ([90748ff](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/90748ff4fe25f47dfc6aabd46cfc02df079bc126))
|
||||
|
||||
## [2.3.5-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.4...decap-cms-widget-list@2.3.5-beta.0) (2019-09-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- pass List instead of array to onChange ([#2611](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2611)) ([a801636](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/a801636))
|
||||
|
||||
## [2.3.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.3...decap-cms-widget-list@2.3.4) (2019-07-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.3.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.2...decap-cms-widget-list@2.3.3) (2019-07-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-list:** honor default values for widgets in lists ([#2395](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2395)) ([83bd5d5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/83bd5d5))
|
||||
|
||||
## [2.3.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.2-beta.0...decap-cms-widget-list@2.3.2) (2019-04-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.3.2-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.1...decap-cms-widget-list@2.3.2-beta.0) (2019-04-05)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.3.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.1-beta.2...decap-cms-widget-list@2.3.1) (2019-03-29)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.3.1-beta.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.1-beta.1...decap-cms-widget-list@2.3.1-beta.2) (2019-03-28)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
## [2.3.1-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.1-beta.0...decap-cms-widget-list@2.3.1-beta.1) (2019-03-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export on decap-cms and maps on esm ([#2244](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2244)) ([6ffd13b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/6ffd13b))
|
||||
|
||||
## [2.3.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.3.0...decap-cms-widget-list@2.3.1-beta.0) (2019-03-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update peer dep versions ([#2234](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2234)) ([7987091](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/7987091))
|
||||
|
||||
# [2.3.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.2.0...decap-cms-widget-list@2.3.0) (2019-03-22)
|
||||
|
||||
### Features
|
||||
|
||||
- add ES module builds ([#2215](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2215)) ([d142b32](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/d142b32))
|
||||
|
||||
# [2.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.2.0-beta.0...decap-cms-widget-list@2.2.0) (2019-03-22)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
# [2.2.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.1.2-beta.0...decap-cms-widget-list@2.2.0-beta.0) (2019-03-21)
|
||||
|
||||
### Features
|
||||
|
||||
- provide usable UMD builds for all packages ([#2141](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2141)) ([82cc794](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/82cc794))
|
||||
|
||||
## [2.1.2-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.1.1...decap-cms-widget-list@2.1.2-beta.0) (2019-03-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency react-sortable-hoc to v1 ([#2198](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2198)) ([b5180e9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/b5180e9))
|
||||
|
||||
### Features
|
||||
|
||||
- upgrade to Emotion 10 ([#2166](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2166)) ([ccef446](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/ccef446))
|
||||
|
||||
## [2.1.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.1.0...decap-cms-widget-list@2.1.1) (2019-02-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **decap-cms-core:** fix fields metadata for objects and lists ([#2011](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/2011)) ([2d1d1c1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/2d1d1c1))
|
||||
- **decap-cms-core:** validate nested fields ([#1873](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/1873)) ([627e600](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/627e600))
|
||||
|
||||
# [2.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.0.7...decap-cms-widget-list@2.1.0) (2018-12-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **decap-cms-widget-list:** add variable type definitions to list widget ([#1857](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/1857)) ([8ddc168](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/8ddc168))
|
||||
|
||||
## [2.0.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.0.6...decap-cms-widget-list@2.0.7) (2018-11-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-list:** fix list item deletion ([#1815](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/1815)) ([cd2036f](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/cd2036f))
|
||||
|
||||
<a name="2.0.6"></a>
|
||||
|
||||
## [2.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.0.5...decap-cms-widget-list@2.0.6) (2018-08-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
<a name="2.0.5"></a>
|
||||
|
||||
## [2.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.0.4...decap-cms-widget-list@2.0.5) (2018-08-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **list-widget:** fix single field usage in list widget ([#1395](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/issues/1395)) ([06d3650](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/commit/06d3650))
|
||||
|
||||
<a name="2.0.4"></a>
|
||||
|
||||
## [2.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.0.3...decap-cms-widget-list@2.0.4) (2018-08-07)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
<a name="2.0.3"></a>
|
||||
|
||||
## [2.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.0.2...decap-cms-widget-list@2.0.3) (2018-08-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
<a name="2.0.2"></a>
|
||||
|
||||
## [2.0.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list/compare/decap-cms-widget-list@2.0.1...decap-cms-widget-list@2.0.2) (2018-07-28)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
|
||||
<a name="2.0.1"></a>
|
||||
|
||||
## 2.0.1 (2018-07-26)
|
||||
|
||||
<a name="2.0.0"></a>
|
||||
|
||||
# 2.0.0 (2018-07-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-list
|
||||
9
source/admin/packages/decap-cms-widget-list/README.md
Normal file
9
source/admin/packages/decap-cms-widget-list/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Docs coming soon!
|
||||
|
||||
Decap CMS was converted from a single npm package to a "monorepo" of over 20 packages.
|
||||
We haven't created a README for this package yet, but you can:
|
||||
|
||||
1. Check out the [main readme](https://github.com/decaporg/decap-cms/#readme) or the [documentation
|
||||
site](https://www.decapcms.org) for more info.
|
||||
2. Reach out to the [community chat](https://decapcms.org/chat/) if you need help.
|
||||
3. Help out and [write the readme yourself](https://github.com/decaporg/decap-cms/edit/main/packages/decap-cms-widget-list/README.md)!
|
||||
40
source/admin/packages/decap-cms-widget-list/package.json
Normal file
40
source/admin/packages/decap-cms-widget-list/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "decap-cms-widget-list",
|
||||
"description": "Widget for editing lists in Decap CMS.",
|
||||
"version": "3.4.0",
|
||||
"homepage": "https://www.decapcms.org/docs/widgets/#list",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-list",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-widget-list.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"widget",
|
||||
"list",
|
||||
"object"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"develop": "npm run build:esm -- --watch",
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/modifiers": "^6.0.1",
|
||||
"@dnd-kit/sortable": "^7.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-lib-widgets": "^3.0.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"decap-cms-widget-object": "^3.0.0",
|
||||
"immutable": "^3.7.6",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0",
|
||||
"react-immutable-proptypes": "^2.1.0"
|
||||
}
|
||||
}
|
||||
817
source/admin/packages/decap-cms-widget-list/src/ListControl.js
Normal file
817
source/admin/packages/decap-cms-widget-list/src/ListControl.js
Normal file
@@ -0,0 +1,817 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from '@emotion/styled';
|
||||
import { css, ClassNames } from '@emotion/react';
|
||||
import { List, Map, fromJS } from 'immutable';
|
||||
import partial from 'lodash/partial';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import DecapCmsWidgetObject from 'decap-cms-widget-object';
|
||||
import {
|
||||
DndContext,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import { restrictToParentElement } from '@dnd-kit/modifiers';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import {
|
||||
ListItemTopBar,
|
||||
ObjectWidgetTopBar,
|
||||
colors,
|
||||
lengths,
|
||||
FieldLabel,
|
||||
} from 'decap-cms-ui-default';
|
||||
import { stringTemplate, validations } from 'decap-cms-lib-widgets';
|
||||
|
||||
import {
|
||||
TYPES_KEY,
|
||||
getTypedFieldForValue,
|
||||
resolveFieldKeyType,
|
||||
getErrorMessageForTypedFieldAndValue,
|
||||
} from './typedListHelpers';
|
||||
|
||||
const ObjectControl = DecapCmsWidgetObject.controlComponent;
|
||||
|
||||
const ListItem = styled.div();
|
||||
|
||||
const StyledListItemTopBar = styled(ListItemTopBar)`
|
||||
background-color: ${colors.textFieldBorder};
|
||||
`;
|
||||
|
||||
const NestedObjectLabel = styled.div`
|
||||
display: ${props => (props.collapsed ? 'block' : 'none')};
|
||||
border-top: 0;
|
||||
color: ${props => (props.error ? colors.errorText : 'inherit')};
|
||||
background-color: ${colors.textFieldBorder};
|
||||
padding: 13px;
|
||||
border-radius: 0 0 ${lengths.borderRadius} ${lengths.borderRadius};
|
||||
`;
|
||||
|
||||
const styleStrings = {
|
||||
collapsedObjectControl: `
|
||||
display: none;
|
||||
`,
|
||||
objectWidgetTopBarContainer: `
|
||||
padding: ${lengths.objectWidgetTopBarContainerPadding};
|
||||
`,
|
||||
};
|
||||
|
||||
const styles = {
|
||||
listControlItem: css`
|
||||
margin-top: 18px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 26px;
|
||||
}
|
||||
`,
|
||||
listControlItemCollapsed: css`
|
||||
padding-bottom: 0;
|
||||
`,
|
||||
};
|
||||
|
||||
function SortableList({ items, children, onSortEnd, keys }) {
|
||||
const activationConstraint = { distance: 4 };
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, { activationConstraint }),
|
||||
useSensor(TouchSensor, { activationConstraint }),
|
||||
);
|
||||
|
||||
function handleSortEnd({ active, over }) {
|
||||
onSortEnd({
|
||||
oldIndex: keys.indexOf(active.id),
|
||||
newIndex: keys.indexOf(over.id),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DndContext
|
||||
modifiers={[restrictToParentElement]}
|
||||
collisionDetection={closestCenter}
|
||||
sensors={sensors}
|
||||
onDragEnd={handleSortEnd}
|
||||
>
|
||||
<SortableContext items={items}>{children}</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SortableListItem(props) {
|
||||
const { setNodeRef, transform, transition } = useSortable({
|
||||
id: props.id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const { collapsed } = props;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
sortable
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
css={[styles.listControlItem, collapsed && styles.listControlItemCollapsed]}
|
||||
>
|
||||
{props.children}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DragHandle({ children, id }) {
|
||||
const { attributes, listeners } = useSortable({
|
||||
id,
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...attributes} {...listeners}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const valueTypes = {
|
||||
SINGLE: 'SINGLE',
|
||||
MULTIPLE: 'MULTIPLE',
|
||||
MIXED: 'MIXED',
|
||||
};
|
||||
|
||||
function handleSummary(summary, entry, label, item) {
|
||||
const data = stringTemplate.addFileTemplateFields(
|
||||
entry.get('path'),
|
||||
item.set('fields.label', label),
|
||||
);
|
||||
return stringTemplate.compileStringTemplate(summary, null, '', data);
|
||||
}
|
||||
|
||||
function validateItem(field, item) {
|
||||
if (!Map.isMap(item)) {
|
||||
console.warn(
|
||||
`'${field.get('name')}' field item value value should be a map but is a '${typeof item}'`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
function LabelComponent({ field, isActive, hasErrors, uniqueFieldId, isFieldOptional, t }) {
|
||||
const label = `${field.get('label', field.get('name'))}`;
|
||||
return (
|
||||
<FieldLabel isActive={isActive} hasErrors={hasErrors} htmlFor={uniqueFieldId}>
|
||||
{label} {`${isFieldOptional ? ` (${t('editor.editorControl.field.optional')})` : ''}`}
|
||||
</FieldLabel>
|
||||
);
|
||||
}
|
||||
|
||||
export default class ListControl extends React.Component {
|
||||
childRefs = {};
|
||||
|
||||
static propTypes = {
|
||||
metadata: ImmutablePropTypes.map,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onChangeObject: PropTypes.func.isRequired,
|
||||
onValidateObject: PropTypes.func.isRequired,
|
||||
validate: PropTypes.func.isRequired,
|
||||
value: ImmutablePropTypes.list,
|
||||
field: PropTypes.object,
|
||||
forID: PropTypes.string,
|
||||
controlRef: PropTypes.func,
|
||||
mediaPaths: ImmutablePropTypes.map.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
onOpenMediaLibrary: PropTypes.func.isRequired,
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onRemoveInsertedMedia: PropTypes.func.isRequired,
|
||||
classNameWrapper: PropTypes.string.isRequired,
|
||||
setActiveStyle: PropTypes.func.isRequired,
|
||||
setInactiveStyle: PropTypes.func.isRequired,
|
||||
editorControl: PropTypes.elementType.isRequired,
|
||||
resolveWidget: PropTypes.func.isRequired,
|
||||
clearFieldErrors: PropTypes.func.isRequired,
|
||||
fieldsErrors: ImmutablePropTypes.map.isRequired,
|
||||
entry: ImmutablePropTypes.map.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: List(),
|
||||
parentIds: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { field, value } = props;
|
||||
const listCollapsed = field.get('collapsed', true);
|
||||
const itemsCollapsed = (value && Array(value.size).fill(listCollapsed)) || [];
|
||||
const keys = (value && Array.from({ length: value.size }, () => uuid())) || [];
|
||||
|
||||
this.state = {
|
||||
listCollapsed,
|
||||
itemsCollapsed,
|
||||
value: this.valueToString(value),
|
||||
keys,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(ListControl.propTypes, this.props, 'prop', 'ListControl');
|
||||
}
|
||||
|
||||
valueToString = value => {
|
||||
let stringValue;
|
||||
if (List.isList(value) || Array.isArray(value)) {
|
||||
stringValue = value.join(',');
|
||||
} else {
|
||||
console.warn(
|
||||
`Expected List value to be an array but received '${value}' with type of '${typeof value}'. Please check the value provided to the '${this.props.field.get(
|
||||
'name',
|
||||
)}' field`,
|
||||
);
|
||||
stringValue = String(value);
|
||||
}
|
||||
return stringValue.replace(/,([^\s]|$)/g, ', $1');
|
||||
};
|
||||
|
||||
getValueType = () => {
|
||||
const { field } = this.props;
|
||||
if (field.get('fields')) {
|
||||
return valueTypes.MULTIPLE;
|
||||
} else if (field.get('field')) {
|
||||
return valueTypes.SINGLE;
|
||||
} else if (field.get(TYPES_KEY)) {
|
||||
return valueTypes.MIXED;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
uniqueFieldId = uniqueId(`${this.props.field.get('name')}-field-`);
|
||||
/**
|
||||
* Always update so that each nested widget has the option to update. This is
|
||||
* required because ControlHOC provides a default `shouldComponentUpdate`
|
||||
* which only updates if the value changes, but every widget must be allowed
|
||||
* to override this.
|
||||
*/
|
||||
shouldComponentUpdate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
const { onChange } = this.props;
|
||||
const oldValue = this.state.value;
|
||||
const newValue = e.target.value.trim();
|
||||
const listValue = newValue ? newValue.split(',') : [];
|
||||
if (newValue.match(/,$/) && oldValue.match(/, $/)) {
|
||||
listValue.pop();
|
||||
}
|
||||
|
||||
const parsedValue = this.valueToString(listValue);
|
||||
this.setState({ value: parsedValue });
|
||||
onChange(List(listValue.map(val => val.trim())));
|
||||
};
|
||||
|
||||
handleFocus = () => {
|
||||
this.props.setActiveStyle();
|
||||
};
|
||||
|
||||
handleBlur = e => {
|
||||
const listValue = e.target.value
|
||||
.split(',')
|
||||
.map(el => el.trim())
|
||||
.filter(el => el);
|
||||
this.setState({ value: this.valueToString(listValue) });
|
||||
this.props.setInactiveStyle();
|
||||
};
|
||||
|
||||
handleAdd = e => {
|
||||
e.preventDefault();
|
||||
const { field } = this.props;
|
||||
const parsedValue =
|
||||
this.getValueType() === valueTypes.SINGLE
|
||||
? this.singleDefault()
|
||||
: fromJS(this.multipleDefault(field.get('fields')));
|
||||
this.addItem(parsedValue);
|
||||
};
|
||||
|
||||
singleDefault = () => {
|
||||
return this.props.field.getIn(['field', 'default'], null);
|
||||
};
|
||||
|
||||
multipleDefault = fields => {
|
||||
return this.getFieldsDefault(fields);
|
||||
};
|
||||
|
||||
handleAddType = (type, typeKey) => {
|
||||
const parsedValue = fromJS(this.mixedDefault(typeKey, type));
|
||||
this.addItem(parsedValue);
|
||||
};
|
||||
|
||||
mixedDefault = (typeKey, type) => {
|
||||
const selectedType = this.props.field.get(TYPES_KEY).find(f => f.get('name') === type);
|
||||
const fields = selectedType.get('fields') || [selectedType.get('field')];
|
||||
|
||||
return this.getFieldsDefault(fields, { [typeKey]: type });
|
||||
};
|
||||
|
||||
getFieldsDefault = (fields, initialValue = {}) => {
|
||||
return fields.reduce((acc, item) => {
|
||||
const subfields = item.get('field') || item.get('fields');
|
||||
const object = item.get('widget') == 'object';
|
||||
const name = item.get('name');
|
||||
const defaultValue = item.get('default', null);
|
||||
|
||||
if (List.isList(subfields) && object) {
|
||||
const subDefaultValue = this.getFieldsDefault(subfields);
|
||||
!isEmpty(subDefaultValue) && (acc[name] = subDefaultValue);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (Map.isMap(subfields) && object) {
|
||||
const subDefaultValue = this.getFieldsDefault([subfields]);
|
||||
!isEmpty(subDefaultValue) && (acc[name] = subDefaultValue);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (defaultValue !== null) {
|
||||
acc[name] = defaultValue;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, initialValue);
|
||||
};
|
||||
|
||||
addItem = parsedValue => {
|
||||
const { value, onChange, field } = this.props;
|
||||
const addToTop = field.get('add_to_top', false);
|
||||
|
||||
const itemKey = uuid();
|
||||
this.setState({
|
||||
itemsCollapsed: addToTop
|
||||
? [false, ...this.state.itemsCollapsed]
|
||||
: [...this.state.itemsCollapsed, false],
|
||||
keys: addToTop ? [itemKey, ...this.state.keys] : [...this.state.keys, itemKey],
|
||||
});
|
||||
|
||||
const listValue = value || List();
|
||||
if (addToTop) {
|
||||
onChange(listValue.unshift(parsedValue));
|
||||
} else {
|
||||
onChange(listValue.push(parsedValue));
|
||||
}
|
||||
};
|
||||
|
||||
processControlRef = ref => {
|
||||
if (!ref) return;
|
||||
const {
|
||||
props: { validationKey: key },
|
||||
} = ref;
|
||||
this.childRefs[key] = ref;
|
||||
this.props.controlRef?.(this);
|
||||
};
|
||||
|
||||
validate = () => {
|
||||
// First validate child widgets if this is a complex list
|
||||
const hasChildWidgets = this.getValueType() && Object.keys(this.childRefs).length > 0;
|
||||
if (hasChildWidgets) {
|
||||
Object.values(this.childRefs).forEach(widget => {
|
||||
widget?.validate?.();
|
||||
});
|
||||
} else {
|
||||
this.props.validate();
|
||||
}
|
||||
this.props.onValidateObject(this.props.forID, this.validateSize());
|
||||
};
|
||||
|
||||
validateSize = () => {
|
||||
const { field, value, t } = this.props;
|
||||
const min = field.get('min');
|
||||
const max = field.get('max');
|
||||
const required = field.get('required', true);
|
||||
|
||||
if (!required && !value?.size) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const error = validations.validateMinMax(
|
||||
t,
|
||||
field.get('label', field.get('name')),
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
);
|
||||
|
||||
return error ? [error] : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* In case the `onChangeObject` function is frozen by a child widget implementation,
|
||||
* e.g. when debounced, always get the latest object value instead of using
|
||||
* `this.props.value` directly.
|
||||
*/
|
||||
getObjectValue = idx => this.props.value.get(idx) || Map();
|
||||
|
||||
handleChangeFor(index) {
|
||||
return (f, newValue, newMetadata) => {
|
||||
const { value, metadata, onChange, field } = this.props;
|
||||
const collectionName = field.get('name');
|
||||
const listFieldObjectWidget = field.getIn(['field', 'widget']) === 'object';
|
||||
const withNameKey =
|
||||
this.getValueType() !== valueTypes.SINGLE ||
|
||||
(this.getValueType() === valueTypes.SINGLE && listFieldObjectWidget);
|
||||
const newObjectValue = withNameKey
|
||||
? this.getObjectValue(index).set(f.get('name'), newValue)
|
||||
: newValue;
|
||||
const parsedMetadata = {
|
||||
[collectionName]: Object.assign(metadata ? metadata.toJS() : {}, newMetadata || {}),
|
||||
};
|
||||
onChange(value.set(index, newObjectValue), parsedMetadata);
|
||||
};
|
||||
}
|
||||
|
||||
handleRemove = (index, event) => {
|
||||
event.preventDefault();
|
||||
const { itemsCollapsed } = this.state;
|
||||
const {
|
||||
value,
|
||||
metadata,
|
||||
onChange,
|
||||
field,
|
||||
clearFieldErrors,
|
||||
onValidateObject,
|
||||
forID,
|
||||
fieldsErrors,
|
||||
} = this.props;
|
||||
|
||||
const collectionName = field.get('name');
|
||||
const isSingleField = this.getValueType() === valueTypes.SINGLE;
|
||||
|
||||
const metadataRemovePath = isSingleField ? value.get(index) : value.get(index).valueSeq();
|
||||
const parsedMetadata =
|
||||
metadata && !metadata.isEmpty()
|
||||
? { [collectionName]: metadata.removeIn(metadataRemovePath) }
|
||||
: metadata;
|
||||
|
||||
// Get the key of the item being removed
|
||||
const removedKey = this.state.keys[index];
|
||||
|
||||
// Update state while preserving keys for remaining items
|
||||
const newKeys = [...this.state.keys];
|
||||
newKeys.splice(index, 1);
|
||||
itemsCollapsed.splice(index, 1);
|
||||
|
||||
this.setState({
|
||||
itemsCollapsed: [...itemsCollapsed],
|
||||
keys: newKeys,
|
||||
});
|
||||
|
||||
// Clear the ref for the removed item
|
||||
delete this.childRefs[removedKey];
|
||||
|
||||
const newValue = value.delete(index);
|
||||
|
||||
// Clear errors for the removed item and its children
|
||||
if (fieldsErrors) {
|
||||
Object.entries(fieldsErrors.toJS()).forEach(([fieldId, errors]) => {
|
||||
if (errors.some(err => err.parentIds?.includes(removedKey))) {
|
||||
clearFieldErrors(fieldId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If list is empty, mark it as valid
|
||||
if (newValue.size === 0) {
|
||||
clearFieldErrors(forID);
|
||||
onValidateObject(forID, []);
|
||||
}
|
||||
|
||||
// Update the value last to ensure all error states are cleared
|
||||
onChange(newValue, parsedMetadata);
|
||||
};
|
||||
|
||||
handleItemCollapseToggle = (index, event) => {
|
||||
event.preventDefault();
|
||||
const { itemsCollapsed } = this.state;
|
||||
const newItemsCollapsed = itemsCollapsed.map((collapsed, itemIndex) => {
|
||||
if (index === itemIndex) {
|
||||
return !collapsed;
|
||||
}
|
||||
return collapsed;
|
||||
});
|
||||
this.setState({
|
||||
itemsCollapsed: newItemsCollapsed,
|
||||
});
|
||||
};
|
||||
|
||||
handleCollapseAllToggle = e => {
|
||||
e.preventDefault();
|
||||
const { value, field } = this.props;
|
||||
const { itemsCollapsed, listCollapsed } = this.state;
|
||||
const minimizeCollapsedItems = field.get('minimize_collapsed', false);
|
||||
const listCollapsedByDefault = field.get('collapsed', true);
|
||||
const allItemsCollapsed = itemsCollapsed.every(val => val === true);
|
||||
|
||||
if (minimizeCollapsedItems) {
|
||||
let updatedItemsCollapsed = itemsCollapsed;
|
||||
// Only allow collapsing all items in this mode but not opening all at once
|
||||
if (!listCollapsed || !listCollapsedByDefault) {
|
||||
updatedItemsCollapsed = Array(value.size).fill(!listCollapsed);
|
||||
}
|
||||
this.setState({ listCollapsed: !listCollapsed, itemsCollapsed: updatedItemsCollapsed });
|
||||
} else {
|
||||
this.setState({ itemsCollapsed: Array(value.size).fill(!allItemsCollapsed) });
|
||||
}
|
||||
};
|
||||
|
||||
objectLabel(item) {
|
||||
const { field, entry } = this.props;
|
||||
const valueType = this.getValueType();
|
||||
switch (valueType) {
|
||||
case valueTypes.MIXED: {
|
||||
if (!validateItem(field, item)) {
|
||||
return;
|
||||
}
|
||||
const itemType = getTypedFieldForValue(field, item);
|
||||
const label = itemType.get('label', itemType.get('name'));
|
||||
// each type can have its own summary, but default to the list summary if exists
|
||||
const summary = itemType.get('summary', field.get('summary'));
|
||||
const labelReturn = summary ? handleSummary(summary, entry, label, item) : label;
|
||||
return labelReturn;
|
||||
}
|
||||
case valueTypes.SINGLE: {
|
||||
const singleField = field.get('field');
|
||||
const label = singleField.get('label', singleField.get('name'));
|
||||
const summary = field.get('summary');
|
||||
const data = fromJS({ [singleField.get('name')]: item });
|
||||
const labelReturn = summary ? handleSummary(summary, entry, label, data) : label;
|
||||
return labelReturn;
|
||||
}
|
||||
case valueTypes.MULTIPLE: {
|
||||
if (!validateItem(field, item)) {
|
||||
return;
|
||||
}
|
||||
const multiFields = field.get('fields');
|
||||
const labelField = multiFields && multiFields.first();
|
||||
const value = item.get(labelField.get('name'));
|
||||
const summary = field.get('summary');
|
||||
const labelReturn = summary ? handleSummary(summary, entry, value, item) : value;
|
||||
return (labelReturn || `No ${labelField.get('name')}`).toString();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
onSortEnd = ({ oldIndex, newIndex }) => {
|
||||
const { value } = this.props;
|
||||
const { itemsCollapsed, keys } = this.state;
|
||||
|
||||
// Update value
|
||||
const item = value.get(oldIndex);
|
||||
const newValue = value.delete(oldIndex).insert(newIndex, item);
|
||||
this.props.onChange(newValue);
|
||||
|
||||
// Update collapsing
|
||||
const collapsed = itemsCollapsed[oldIndex];
|
||||
itemsCollapsed.splice(oldIndex, 1);
|
||||
const updatedItemsCollapsed = [...itemsCollapsed];
|
||||
updatedItemsCollapsed.splice(newIndex, 0, collapsed);
|
||||
|
||||
// Move keys to maintain relationships
|
||||
const movedKey = keys[oldIndex];
|
||||
const updatedKeys = [...keys];
|
||||
updatedKeys.splice(oldIndex, 1);
|
||||
updatedKeys.splice(newIndex, 0, movedKey);
|
||||
|
||||
this.setState({ itemsCollapsed: updatedItemsCollapsed, keys: updatedKeys });
|
||||
};
|
||||
|
||||
hasError = index => {
|
||||
const { fieldsErrors } = this.props;
|
||||
if (fieldsErrors && fieldsErrors.size > 0) {
|
||||
return Object.values(fieldsErrors.toJS()).some(arr =>
|
||||
arr.some(err => err.parentIds && err.parentIds.includes(this.state.keys[index])),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
focus(path) {
|
||||
const [index, ...remainingPath] = path.split('.');
|
||||
|
||||
if (this.state.listCollapsed || this.state.itemsCollapsed[index]) {
|
||||
const newItemsCollapsed = [...this.state.itemsCollapsed];
|
||||
newItemsCollapsed[index] = false;
|
||||
this.setState(
|
||||
{
|
||||
listCollapsed: false,
|
||||
itemsCollapsed: newItemsCollapsed,
|
||||
},
|
||||
() => {
|
||||
const key = this.state.keys[index];
|
||||
const control = this.childRefs[key];
|
||||
if (control?.focus) {
|
||||
control.focus(remainingPath.join('.'));
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const key = this.state.keys[index];
|
||||
const control = this.childRefs[key];
|
||||
if (control?.focus) {
|
||||
control.focus(remainingPath.join('.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
renderItem = (item, index) => {
|
||||
const {
|
||||
classNameWrapper,
|
||||
editorControl,
|
||||
onValidateObject,
|
||||
metadata,
|
||||
clearFieldErrors,
|
||||
fieldsErrors,
|
||||
controlRef,
|
||||
resolveWidget,
|
||||
parentIds,
|
||||
forID,
|
||||
t,
|
||||
} = this.props;
|
||||
|
||||
const { itemsCollapsed, keys } = this.state;
|
||||
const collapsed = itemsCollapsed[index];
|
||||
const key = keys[index];
|
||||
let field = this.props.field;
|
||||
const hasError = this.hasError(index);
|
||||
const isVariableTypesList = this.getValueType() === valueTypes.MIXED;
|
||||
if (isVariableTypesList) {
|
||||
field = getTypedFieldForValue(field, item);
|
||||
if (!field) {
|
||||
return this.renderErroneousTypedItem(index, item);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SortableListItem
|
||||
css={[styles.listControlItem, collapsed && styles.listControlItemCollapsed]}
|
||||
index={index}
|
||||
key={key}
|
||||
id={key}
|
||||
keys={keys}
|
||||
>
|
||||
{isVariableTypesList && (
|
||||
<LabelComponent
|
||||
field={field}
|
||||
isActive={false}
|
||||
hasErrors={hasError}
|
||||
uniqueFieldId={this.uniqueFieldId}
|
||||
isFieldOptional={field.get('required') === false}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
<StyledListItemTopBar
|
||||
collapsed={collapsed}
|
||||
onCollapseToggle={partial(this.handleItemCollapseToggle, index)}
|
||||
dragHandle={DragHandle}
|
||||
id={key}
|
||||
onRemove={partial(this.handleRemove, index)}
|
||||
data-testid={`styled-list-item-top-bar-${key}`}
|
||||
/>
|
||||
<NestedObjectLabel collapsed={collapsed} error={hasError}>
|
||||
{this.objectLabel(item)}
|
||||
</NestedObjectLabel>
|
||||
<ClassNames>
|
||||
{({ css, cx }) => (
|
||||
<ObjectControl
|
||||
classNameWrapper={cx(classNameWrapper, {
|
||||
[css`
|
||||
${styleStrings.collapsedObjectControl};
|
||||
`]: collapsed,
|
||||
})}
|
||||
value={item}
|
||||
field={field}
|
||||
onChangeObject={this.handleChangeFor(index)}
|
||||
editorControl={editorControl}
|
||||
resolveWidget={resolveWidget}
|
||||
metadata={metadata}
|
||||
forList
|
||||
onValidateObject={onValidateObject}
|
||||
clearFieldErrors={clearFieldErrors}
|
||||
fieldsErrors={fieldsErrors}
|
||||
ref={this.processControlRef}
|
||||
controlRef={controlRef}
|
||||
validationKey={key}
|
||||
collapsed={collapsed}
|
||||
data-testid={`object-control-${key}`}
|
||||
hasError={hasError}
|
||||
parentIds={[...parentIds, forID, key]}
|
||||
/>
|
||||
)}
|
||||
</ClassNames>
|
||||
</SortableListItem>
|
||||
);
|
||||
};
|
||||
|
||||
renderErroneousTypedItem(index, item) {
|
||||
const field = this.props.field;
|
||||
const errorMessage = getErrorMessageForTypedFieldAndValue(field, item);
|
||||
const key = `item-${index}`;
|
||||
return (
|
||||
<SortableListItem
|
||||
css={[styles.listControlItem, styles.listControlItemCollapsed]}
|
||||
index={index}
|
||||
key={key}
|
||||
>
|
||||
<StyledListItemTopBar
|
||||
onCollapseToggle={null}
|
||||
onRemove={partial(this.handleRemove, index, key)}
|
||||
dragHandle={DragHandle}
|
||||
id={key}
|
||||
/>
|
||||
<NestedObjectLabel collapsed={true} error={true}>
|
||||
{errorMessage}
|
||||
</NestedObjectLabel>
|
||||
</SortableListItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderListControl() {
|
||||
const { value, forID, field, classNameWrapper, t } = this.props;
|
||||
const { itemsCollapsed, listCollapsed, keys } = this.state;
|
||||
const items = value || List();
|
||||
const label = field.get('label', field.get('name'));
|
||||
const labelSingular = field.get('label_singular') || field.get('label', field.get('name'));
|
||||
const listLabel = items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase();
|
||||
const minimizeCollapsedItems = field.get('minimize_collapsed', false);
|
||||
const allItemsCollapsed = itemsCollapsed.every(val => val === true);
|
||||
const selfCollapsed = allItemsCollapsed && (listCollapsed || !minimizeCollapsedItems);
|
||||
|
||||
const itemsArray = keys.map(key => ({ id: key }));
|
||||
|
||||
return (
|
||||
<ClassNames>
|
||||
{({ cx, css }) => (
|
||||
<div
|
||||
id={forID}
|
||||
className={cx(
|
||||
classNameWrapper,
|
||||
css`
|
||||
${styleStrings.objectWidgetTopBarContainer}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<ObjectWidgetTopBar
|
||||
allowAdd={field.get('allow_add', true)}
|
||||
onAdd={this.handleAdd}
|
||||
types={field.get(TYPES_KEY, null)}
|
||||
onAddType={type => this.handleAddType(type, resolveFieldKeyType(field))}
|
||||
heading={`${items.size} ${listLabel}`}
|
||||
label={labelSingular.toLowerCase()}
|
||||
onCollapseToggle={this.handleCollapseAllToggle}
|
||||
collapsed={selfCollapsed}
|
||||
t={t}
|
||||
/>
|
||||
{(!selfCollapsed || !minimizeCollapsedItems) && (
|
||||
<SortableList items={itemsArray} keys={keys} onSortEnd={this.onSortEnd}>
|
||||
{items.map(this.renderItem)}
|
||||
</SortableList>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
renderInput() {
|
||||
const { forID, classNameWrapper } = this.props;
|
||||
const { value } = this.state;
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
id={forID}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
className={classNameWrapper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.getValueType() !== null) {
|
||||
return this.renderListControl();
|
||||
} else {
|
||||
return this.renderInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import ListControl from '../ListControl';
|
||||
|
||||
jest.mock('decap-cms-widget-object', () => {
|
||||
const React = require('react');
|
||||
|
||||
class MockObjectControl extends React.Component {
|
||||
render() {
|
||||
return <mock-object-control {...this.props}>{this.props.children}</mock-object-control>;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
controlComponent: MockObjectControl,
|
||||
};
|
||||
});
|
||||
jest.mock('decap-cms-ui-default', () => {
|
||||
const actual = jest.requireActual('decap-cms-ui-default');
|
||||
|
||||
function ListItemTopBar(props) {
|
||||
return (
|
||||
<mock-list-item-top-bar {...props} onClick={props.onCollapseToggle}>
|
||||
<button onClick={props.onRemove}>Remove</button>
|
||||
{props.children}
|
||||
</mock-list-item-top-bar>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...actual,
|
||||
ListItemTopBar,
|
||||
};
|
||||
});
|
||||
jest.mock('uuid');
|
||||
|
||||
describe('ListControl', () => {
|
||||
const props = {
|
||||
onChange: jest.fn(),
|
||||
onChangeObject: jest.fn(),
|
||||
onValidateObject: jest.fn(),
|
||||
validate: jest.fn(),
|
||||
mediaPaths: fromJS({}),
|
||||
getAsset: jest.fn(),
|
||||
onOpenMediaLibrary: jest.fn(),
|
||||
onAddAsset: jest.fn(),
|
||||
onRemoveInsertedMedia: jest.fn(),
|
||||
classNameWrapper: 'classNameWrapper',
|
||||
setActiveStyle: jest.fn(),
|
||||
setInactiveStyle: jest.fn(),
|
||||
editorControl: jest.fn(),
|
||||
resolveWidget: jest.fn(),
|
||||
clearFieldErrors: jest.fn(),
|
||||
fieldsErrors: fromJS({}),
|
||||
entry: fromJS({
|
||||
path: 'posts/index.md',
|
||||
}),
|
||||
forID: 'forID',
|
||||
t: key => key,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const uuid = require('uuid');
|
||||
let id = 0;
|
||||
uuid.v4.mockImplementation(() => {
|
||||
return id++;
|
||||
});
|
||||
});
|
||||
it('should render empty list', () => {
|
||||
const field = fromJS({ name: 'list', label: 'List' });
|
||||
const { asFragment } = render(<ListControl {...props} field={field} />);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render list with string array', () => {
|
||||
const field = fromJS({ name: 'list', label: 'List' });
|
||||
const { asFragment } = render(
|
||||
<ListControl {...props} field={field} value={fromJS(['item 1', 'item 2'])} />,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render list with nested object', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
field: {
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
label: 'Object',
|
||||
fields: [{ name: 'title', widget: 'string', label: 'Title' }],
|
||||
},
|
||||
});
|
||||
const { asFragment, getByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ object: { title: 'item 1' } }, { object: { title: 'item 2' } }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed');
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render list with nested object with collapse = false', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
field: {
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
label: 'Object',
|
||||
fields: [{ name: 'title', widget: 'string', label: 'Title' }],
|
||||
},
|
||||
});
|
||||
const { asFragment, getByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ object: { title: 'item 1' } }, { object: { title: 'item 2' } }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should collapse all items on top bar collapse click', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
field: {
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
label: 'Object',
|
||||
fields: [{ name: 'title', widget: 'string', label: 'Title' }],
|
||||
},
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ object: { title: 'item 1' } }, { object: { title: 'item 2' } }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
fireEvent.click(getByTestId('expand-button'));
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed');
|
||||
});
|
||||
|
||||
it('should collapse a single item on collapse item click', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
field: {
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
label: 'Object',
|
||||
fields: [{ name: 'title', widget: 'string', label: 'Title' }],
|
||||
},
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ object: { title: 'item 1' } }, { object: { title: 'item 2' } }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
fireEvent.click(getByTestId('styled-list-item-top-bar-0'));
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).not.toHaveAttribute('collapsed');
|
||||
});
|
||||
|
||||
it('should expand all items on top bar expand click', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
field: {
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
label: 'Object',
|
||||
fields: [{ name: 'title', widget: 'string', label: 'Title' }],
|
||||
},
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ object: { title: 'item 1' } }, { object: { title: 'item 2' } }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed');
|
||||
|
||||
fireEvent.click(getByTestId('expand-button'));
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).not.toHaveAttribute('collapsed');
|
||||
});
|
||||
|
||||
it('should expand a single item on expand item click', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
field: {
|
||||
name: 'object',
|
||||
widget: 'object',
|
||||
label: 'Object',
|
||||
fields: [{ name: 'title', widget: 'string', label: 'Title' }],
|
||||
},
|
||||
});
|
||||
const { getByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ object: { title: 'item 1' } }, { object: { title: 'item 2' } }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed');
|
||||
|
||||
fireEvent.click(getByTestId('styled-list-item-top-bar-0'));
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed');
|
||||
});
|
||||
|
||||
it('should use widget name when no summary or label are configured for mixed types', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
types: [
|
||||
{
|
||||
name: 'type_1_object',
|
||||
widget: 'object',
|
||||
fields: [
|
||||
{ label: 'First Name', name: 'first_name', widget: 'string' },
|
||||
{ label: 'Last Name', name: 'last_name', widget: 'string' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getAllByText } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ first_name: 'hello', last_name: 'world', type: 'type_1_object' }])}
|
||||
/>,
|
||||
);
|
||||
expect(getAllByText('type_1_object')[1]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use label when no summary is configured for mixed types', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
types: [
|
||||
{
|
||||
label: 'Type 1 Object',
|
||||
name: 'type_1_object',
|
||||
widget: 'object',
|
||||
fields: [
|
||||
{ label: 'First Name', name: 'first_name', widget: 'string' },
|
||||
{ label: 'Last Name', name: 'last_name', widget: 'string' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getAllByText } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ first_name: 'hello', last_name: 'world', type: 'type_1_object' }])}
|
||||
/>,
|
||||
);
|
||||
expect(getAllByText('Type 1 Object')[1]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use summary when configured for mixed types', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
types: [
|
||||
{
|
||||
label: 'Type 1 Object',
|
||||
name: 'type_1_object',
|
||||
summary: '{{first_name}} - {{last_name}} - {{filename}}.{{extension}}',
|
||||
widget: 'object',
|
||||
fields: [
|
||||
{ label: 'First Name', name: 'first_name', widget: 'string' },
|
||||
{ label: 'Last Name', name: 'last_name', widget: 'string' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ first_name: 'hello', last_name: 'world', type: 'type_1_object' }])}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('hello - world - index.md')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use widget name when no summary or label are configured for a single field', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
field: { name: 'name', widget: 'string' },
|
||||
});
|
||||
|
||||
const { getByText } = render(<ListControl {...props} field={field} value={fromJS(['Name'])} />);
|
||||
expect(getByText('name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use label when no summary is configured for a single field', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
field: { name: 'name', widget: 'string', label: 'Name' },
|
||||
});
|
||||
|
||||
const { getByText } = render(<ListControl {...props} field={field} value={fromJS(['Name'])} />);
|
||||
expect(getByText('Name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use summary when configured for a single field', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
summary: 'Name - {{fields.name}}',
|
||||
field: { name: 'name', widget: 'string', label: 'Name' },
|
||||
});
|
||||
|
||||
const { getByText } = render(<ListControl {...props} field={field} value={fromJS(['Name'])} />);
|
||||
expect(getByText('Name - Name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use first field value when no summary or label are configured for multiple fields', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
fields: [
|
||||
{ name: 'first_name', widget: 'string', label: 'First Name' },
|
||||
{ name: 'last_name', widget: 'string', label: 'Last Name' },
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ first_name: 'hello', last_name: 'world' }])}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('hello')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show `No <field>` when value is missing from first field for multiple fields', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
fields: [
|
||||
{ name: 'first_name', widget: 'string', label: 'First Name' },
|
||||
{ name: 'last_name', widget: 'string', label: 'Last Name' },
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = render(
|
||||
<ListControl {...props} field={field} value={fromJS([{ last_name: 'world' }])} />,
|
||||
);
|
||||
expect(getByText('No first_name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use summary when configured for multiple fields', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: true,
|
||||
summary: '{{first_name}} - {{last_name}} - {{filename}}.{{extension}}',
|
||||
fields: [
|
||||
{ name: 'first_name', widget: 'string', label: 'First Name' },
|
||||
{ name: 'last_name', widget: 'string', label: 'Last Name' },
|
||||
],
|
||||
});
|
||||
|
||||
const { getByText } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ first_name: 'hello', last_name: 'world' }])}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('hello - world - index.md')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render list with fields with default collapse ("true") and minimize_collapsed ("false")', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const { asFragment, getByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed');
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render list with fields with collapse = "false" and default minimize_collapsed ("false")', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const { asFragment, getByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render list with fields with default collapse ("true") and minimize_collapsed = "true"', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
minimize_collapsed: true,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const { asFragment, getByTestId, queryByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByTestId('styled-list-item-top-bar-0')).toBeNull();
|
||||
expect(queryByTestId('styled-list-item-top-bar-1')).toBeNull();
|
||||
|
||||
expect(queryByTestId('object-control-0')).toBeNull();
|
||||
expect(queryByTestId('object-control-1')).toBeNull();
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
fireEvent.click(getByTestId('expand-button'));
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).toHaveAttribute('collapsed');
|
||||
});
|
||||
|
||||
it('should render list with fields with collapse = "false" and default minimize_collapsed = "true"', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
minimize_collapsed: true,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const { asFragment, getByTestId, queryByTestId } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('styled-list-item-top-bar-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(getByTestId('object-control-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-1')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
fireEvent.click(getByTestId('expand-button'));
|
||||
|
||||
expect(queryByTestId('styled-list-item-top-bar-0')).toBeNull();
|
||||
expect(queryByTestId('styled-list-item-top-bar-1')).toBeNull();
|
||||
|
||||
expect(queryByTestId('object-control-0')).toBeNull();
|
||||
expect(queryByTestId('object-control-1')).toBeNull();
|
||||
});
|
||||
|
||||
it('should add to list when add button is clicked', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const { asFragment, getByText, queryByTestId, rerender, getByTestId } = render(
|
||||
<ListControl {...props} field={field} value={fromJS([])} />,
|
||||
);
|
||||
|
||||
expect(queryByTestId('object-control-0')).toBeNull();
|
||||
|
||||
fireEvent.click(getByText('editor.editorWidgets.list.add'));
|
||||
|
||||
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.onChange).toHaveBeenCalledWith(fromJS([{}]));
|
||||
|
||||
rerender(<ListControl {...props} field={field} value={fromJS([{}])} />);
|
||||
|
||||
expect(getByTestId('styled-list-item-top-bar-0')).not.toHaveAttribute('collapsed');
|
||||
expect(getByTestId('object-control-0')).not.toHaveAttribute('collapsed');
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should remove from list when remove button is clicked', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
minimize_collapsed: true,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const { asFragment, getAllByText, rerender } = render(
|
||||
<ListControl
|
||||
{...props}
|
||||
field={field}
|
||||
value={fromJS([{ string: 'item 1' }, { string: 'item 2' }])}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
let mock;
|
||||
try {
|
||||
mock = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
|
||||
const items = getAllByText('Remove');
|
||||
fireEvent.click(items[0]);
|
||||
|
||||
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.onChange).toHaveBeenCalledWith(fromJS([{ string: 'item 2' }]), undefined);
|
||||
|
||||
rerender(<ListControl {...props} field={field} value={fromJS([{ string: 'item 2' }])} />);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
} finally {
|
||||
mock.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should give validation error if below min elements', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
minimize_collapsed: true,
|
||||
required: true,
|
||||
min: 2,
|
||||
max: 3,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const listControl = new ListControl({
|
||||
...props,
|
||||
field,
|
||||
value: fromJS([{ string: 'item 1' }]),
|
||||
});
|
||||
|
||||
listControl.validate();
|
||||
expect(props.onValidateObject).toHaveBeenCalledWith('forID', [
|
||||
{
|
||||
message: 'editor.editorControlPane.widget.rangeCount',
|
||||
type: 'RANGE',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should give min validation error if below min elements', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
minimize_collapsed: true,
|
||||
required: true,
|
||||
min: 2,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const listControl = new ListControl({
|
||||
...props,
|
||||
field,
|
||||
value: fromJS([{ string: 'item 1' }]),
|
||||
});
|
||||
|
||||
listControl.validate();
|
||||
expect(props.onValidateObject).toHaveBeenCalledWith('forID', [
|
||||
{
|
||||
message: 'editor.editorControlPane.widget.rangeMin',
|
||||
type: 'RANGE',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should give validation error if above max elements', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
minimize_collapsed: true,
|
||||
required: true,
|
||||
min: 2,
|
||||
max: 3,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const listControl = new ListControl({
|
||||
...props,
|
||||
field,
|
||||
value: fromJS([
|
||||
{ string: 'item 1' },
|
||||
{ string: 'item 2' },
|
||||
{ string: 'item 3' },
|
||||
{ string: 'item 4' },
|
||||
]),
|
||||
});
|
||||
|
||||
listControl.validate();
|
||||
expect(props.onValidateObject).toHaveBeenCalledWith('forID', [
|
||||
{
|
||||
message: 'editor.editorControlPane.widget.rangeCount',
|
||||
type: 'RANGE',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should give max validation error if above max elements', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
minimize_collapsed: true,
|
||||
required: true,
|
||||
max: 3,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const listControl = new ListControl({
|
||||
...props,
|
||||
field,
|
||||
value: fromJS([
|
||||
{ string: 'item 1' },
|
||||
{ string: 'item 2' },
|
||||
{ string: 'item 3' },
|
||||
{ string: 'item 4' },
|
||||
]),
|
||||
});
|
||||
|
||||
listControl.validate();
|
||||
expect(props.onValidateObject).toHaveBeenCalledWith('forID', [
|
||||
{
|
||||
message: 'editor.editorControlPane.widget.rangeMax',
|
||||
type: 'RANGE',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should give no validation error if between min and max elements', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
minimize_collapsed: true,
|
||||
required: true,
|
||||
min: 2,
|
||||
max: 3,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const listControl = new ListControl({
|
||||
...props,
|
||||
field,
|
||||
value: fromJS([{ string: 'item 1' }, { string: 'item 2' }, { string: 'item 3' }]),
|
||||
});
|
||||
|
||||
listControl.validate();
|
||||
expect(props.onValidateObject).toHaveBeenCalledWith('forID', []);
|
||||
});
|
||||
|
||||
it('should give no validation error if no elements and optional', () => {
|
||||
const field = fromJS({
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
collapsed: false,
|
||||
minimize_collapsed: true,
|
||||
required: false,
|
||||
min: 2,
|
||||
max: 3,
|
||||
fields: [{ label: 'String', name: 'string', widget: 'string' }],
|
||||
});
|
||||
const listControl = new ListControl({
|
||||
...props,
|
||||
field,
|
||||
value: fromJS([]),
|
||||
});
|
||||
|
||||
listControl.validate();
|
||||
expect(props.onValidateObject).toHaveBeenCalledWith('forID', []);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
19
source/admin/packages/decap-cms-widget-list/src/index.js
Normal file
19
source/admin/packages/decap-cms-widget-list/src/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import DecapCmsWidgetObject from 'decap-cms-widget-object';
|
||||
|
||||
import controlComponent from './ListControl';
|
||||
import schema from './schema';
|
||||
|
||||
const previewComponent = DecapCmsWidgetObject.previewComponent;
|
||||
|
||||
function Widget(opts = {}) {
|
||||
return {
|
||||
name: 'list',
|
||||
controlComponent,
|
||||
previewComponent,
|
||||
schema,
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
export const DecapCmsWidgetList = { Widget, controlComponent, previewComponent };
|
||||
export default DecapCmsWidgetList;
|
||||
12
source/admin/packages/decap-cms-widget-list/src/schema.js
Normal file
12
source/admin/packages/decap-cms-widget-list/src/schema.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
properties: {
|
||||
allow_add: { type: 'boolean' },
|
||||
collapsed: { type: 'boolean' },
|
||||
summary: { type: 'string' },
|
||||
minimize_collapsed: { type: 'boolean' },
|
||||
label_singular: { type: 'string' },
|
||||
i18n: { type: 'boolean' },
|
||||
min: { type: 'number' },
|
||||
max: { type: 'number' },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
export const TYPES_KEY = 'types';
|
||||
export const TYPE_KEY = 'typeKey';
|
||||
export const DEFAULT_TYPE_KEY = 'type';
|
||||
|
||||
export function getTypedFieldForValue(field, value) {
|
||||
const typeKey = resolveFieldKeyType(field);
|
||||
const types = field.get(TYPES_KEY);
|
||||
const valueType = value.get(typeKey);
|
||||
return types.find(type => type.get('name') === valueType);
|
||||
}
|
||||
|
||||
export function resolveFunctionForTypedField(field) {
|
||||
const typeKey = resolveFieldKeyType(field);
|
||||
const types = field.get(TYPES_KEY);
|
||||
return value => {
|
||||
const valueType = value.get(typeKey);
|
||||
return types.find(type => type.get('name') === valueType);
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveFieldKeyType(field) {
|
||||
return field.get(TYPE_KEY, DEFAULT_TYPE_KEY);
|
||||
}
|
||||
|
||||
export function getErrorMessageForTypedFieldAndValue(field, value) {
|
||||
const keyType = resolveFieldKeyType(field);
|
||||
const type = value.get(keyType);
|
||||
let errorMessage;
|
||||
if (!type) {
|
||||
errorMessage = `Error: item has no '${keyType}' property`;
|
||||
} else {
|
||||
errorMessage = `Error: item has illegal '${keyType}' property: '${type}'`;
|
||||
}
|
||||
return errorMessage;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
const { getConfig } = require('../../scripts/webpack.js');
|
||||
|
||||
module.exports = getConfig();
|
||||
Reference in New Issue
Block a user