diff --git a/CHANGELOG.md b/CHANGELOG.md index b26b66d..1380b25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,11 @@ - Add upgrade profile functionality @robgietema - Add login by email @robgietema - Add url to action @robgietema +- Add navroot endpoint @robgietema +- Add language token to content objects @robgietema +- Add support for expanders in content fetching @robgietema +- Add available languages endpoint @robgietema +- Add multilingual support @robgietema ### Bugfix @@ -53,6 +58,8 @@ - Update workflow from seeds when already exists @robgietema - Return default layout for content objects @robgietema - Fix delete within transaction @robgietema +- Fix keywords endpoint @robgietema +- Fix supported languages endpoint @robgietema ### Internal diff --git a/docs/examples/breadcrumbs/get.res b/docs/examples/breadcrumbs/get.res index 2fffe58..dd54142 100644 --- a/docs/examples/breadcrumbs/get.res +++ b/docs/examples/breadcrumbs/get.res @@ -12,5 +12,6 @@ Content-Type: application/json "@id": "http://localhost:8000/events/event-1", "title": "Event 1" } - ] + ], + "root": "/" } diff --git a/docs/examples/content/get.res b/docs/examples/content/get.res index 13205a6..b8a058c 100644 --- a/docs/examples/content/get.res +++ b/docs/examples/content/get.res @@ -15,9 +15,39 @@ Content-Type: application/json ] }, "items": [], + "@components": { + "actions": { + "@id": "http://localhost:8000/news/@actions" + }, + "breadcrumbs": { + "@id": "http://localhost:8000/news/@breadcrumbs" + }, + "catalog": { + "@id": "http://localhost:8000/news/@catalog" + }, + "navigation": { + "@id": "http://localhost:8000/news/@navigation" + }, + "navroot": { + "@id": "http://localhost:8000/news/@navroot" + }, + "translations": { + "@id": "http://localhost:8000/news/@translations" + }, + "types": { + "@id": "http://localhost:8000/news/@types" + }, + "workflow": { + "@id": "http://localhost:8000/news/@workflow" + } + }, "@id": "http://localhost:8000/news", "@type": "Folder", "id": "news", + "language": { + "title": "English", + "token": "en" + }, "created": "2022-04-02T20:22:00.000Z", "modified": "2022-04-02T20:22:00.000Z", "effective": "2022-04-02T20:22:00.000Z", diff --git a/docs/examples/content/get_root.res b/docs/examples/content/get_root.res index a345f3d..782cdc0 100644 --- a/docs/examples/content/get_root.res +++ b/docs/examples/content/get_root.res @@ -2,7 +2,7 @@ HTTP/1.1 200 OK Content-Type: application/json { - "title": "Welcome to Volto", + "title": "Welcome to Nick!", "blocks": { "495efb73-cbdd-4bef-935a-a56f70a20854": { "text": { @@ -10,7 +10,67 @@ Content-Type: application/json { "key": "9f35d", "data": {}, - "text": "Congratulations! You have succesfully installed Volto.", + "text": "This is the demo site of Nick and is build with the Volto frontend.", + "type": "unstyled", + "depth": 0, + "entityRanges": [ + { "key": 0, "length": 4, "offset": 25 }, + { "key": 1, "length": 5, "offset": 52 } + ], + "inlineStyleRanges": [] + } + ], + "entityMap": { + "0": { + "data": { "url": "https://nickcms.org" }, + "type": "LINK", + "mutability": "MUTABLE" + }, + "1": { + "data": { "url": "https://voltocms.com" }, + "type": "LINK", + "mutability": "MUTABLE" + } + } + }, + "@type": "text" + }, + "6a6d1e67-fefd-4049-98a3-300f0302abcb": { + "text": { + "blocks": [ + { + "key": "3jol2", + "data": {}, + "text": "You can use this site to test the latest version of Nick. You can login with username admin and password admin.", + "type": "unstyled", + "depth": 0, + "entityRanges": [{ "key": 0, "length": 4, "offset": 52 }], + "inlineStyleRanges": [ + { "style": "ITALIC", "length": 8, "offset": 77 }, + { "style": "ITALIC", "length": 8, "offset": 96 }, + { "style": "BOLD", "length": 5, "offset": 86 }, + { "style": "BOLD", "length": 5, "offset": 105 } + ] + } + ], + "entityMap": { + "0": { + "data": { "url": "https://nickcms.org" }, + "type": "LINK", + "mutability": "MUTABLE" + } + } + }, + "@type": "text" + }, + "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" }, + "be383a3d-7409-42b5-a5bc-555e2fbf068d": { + "text": { + "blocks": [ + { + "key": "atc31", + "data": {}, + "text": "This instance is reset every night so feel free to make any changes!", "type": "unstyled", "depth": 0, "entityRanges": [], @@ -21,104 +81,112 @@ Content-Type: application/json }, "@type": "text" }, - "79ba8858-1dd3-4719-b731-5951e32fbf79": { - "@type": "title" + "eb024f35-ab6a-4034-ac5b-77c91fe3d400": { + "text": { + "blocks": [ + { + "key": "5s8ah", + "data": {}, + "text": "Demo", + "type": "header-three", + "depth": 0, + "entityRanges": [], + "inlineStyleRanges": [] + } + ], + "entityMap": {} + }, + "@type": "text" } }, "effective": "2022-04-02T20:00:00.000Z", - "description": "Congratulations! You have successfully installed Volto.", + "description": "Congratulations! You have successfully installed Nick.", "blocks_layout": { "items": [ "79ba8858-1dd3-4719-b731-5951e32fbf79", - "495efb73-cbdd-4bef-935a-a56f70a20854" + "495efb73-cbdd-4bef-935a-a56f70a20854", + "eb024f35-ab6a-4034-ac5b-77c91fe3d400", + "6a6d1e67-fefd-4049-98a3-300f0302abcb", + "be383a3d-7409-42b5-a5bc-555e2fbf068d" ] }, "items": [ { "title": "Events", - "blocks": { - "79ba8858-1dd3-4719-b731-5951e32fbf79": { - "@type": "title" - } - }, + "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } }, "effective": "2022-04-02T20:30:00.000Z", - "blocks_layout": { - "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] - }, + "blocks_layout": { "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] }, "@id": "http://localhost:8000/events", "@type": "Folder", "id": "events", "created": "2022-04-02T20:30:00.000Z", "modified": "2022-04-02T20:30:00.000Z", "UID": "1a2123ba-14e8-4910-8e6b-c04a40d72a41", + "owner": "admin", + "layout": "view", "is_folderish": true, + "language": { "token": "en", "title": "English" }, "review_state": "published", - "lock": { - "locked": false, - "stealable": true - } + "lock": { "locked": false, "stealable": true } }, { "title": "News", - "blocks": { - "79ba8858-1dd3-4719-b731-5951e32fbf79": { - "@type": "title" - } - }, + "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } }, "effective": "2022-04-02T20:22:00.000Z", "description": "News Items", - "blocks_layout": { - "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] - }, + "blocks_layout": { "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] }, "@id": "http://localhost:8000/news", "@type": "Folder", "id": "news", "created": "2022-04-02T20:22:00.000Z", "modified": "2022-04-02T20:22:00.000Z", "UID": "32215c67-86de-462a-8cc0-eabcd2b39c26", + "owner": "admin", + "layout": "view", "is_folderish": true, + "language": { "token": "en", "title": "English" }, "review_state": "published", - "lock": { - "locked": false, - "stealable": true - } + "lock": { "locked": false, "stealable": true } }, { "title": "Users", - "blocks": { - "79ba8858-1dd3-4719-b731-5951e32fbf79": { - "@type": "title" - } - }, + "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } }, "effective": "2022-04-02T20:24:00.000Z", - "blocks_layout": { - "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] - }, + "blocks_layout": { "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] }, "@id": "http://localhost:8000/users", "@type": "Folder", "id": "users", "created": "2022-04-02T20:24:00.000Z", "modified": "2022-04-02T20:24:00.000Z", "UID": "80994493-74ca-4b94-9a7c-145a33a6dd80", + "owner": "admin", + "layout": "view", "is_folderish": true, + "language": { "token": "en", "title": "English" }, "review_state": "published", - "lock": { - "locked": false, - "stealable": true - } + "lock": { "locked": false, "stealable": true } } ], + "@components": { + "catalog": { "@id": "http://localhost:8000/@catalog" }, + "actions": { "@id": "http://localhost:8000/@actions" }, + "breadcrumbs": { "@id": "http://localhost:8000/@breadcrumbs" }, + "navigation": { "@id": "http://localhost:8000/@navigation" }, + "navroot": { "@id": "http://localhost:8000/@navroot" }, + "types": { "@id": "http://localhost:8000/@types" }, + "workflow": { "@id": "http://localhost:8000/@workflow" }, + "translations": { "@id": "http://localhost:8000/@translations" } + }, "@id": "http://localhost:8000", "@type": "Site", "id": "root", "created": "2022-04-02T20:00:00.000Z", "modified": "2022-04-02T20:00:00.000Z", "UID": "92a80817-f5b7-400d-8f58-b08126f0f09b", - "is_folderish": true, + "owner": "admin", "layout": "view", + "is_folderish": true, + "language": { "token": "en", "title": "English" }, "review_state": "published", - "lock": { - "locked": false, - "stealable": true - } + "lock": { "locked": false, "stealable": true } } diff --git a/docs/examples/content/post.res b/docs/examples/content/post.res index d90774d..c90a8cb 100644 --- a/docs/examples/content/post.res +++ b/docs/examples/content/post.res @@ -4,6 +4,32 @@ Content-Type: application/json { "title": "My News Item", "description": "News Description", + "@components": { + "actions": { + "@id": "http://localhost:8000/news/@actions" + }, + "breadcrumbs": { + "@id": "http://localhost:8000/news/@breadcrumbs" + }, + "catalog": { + "@id": "http://localhost:8000/news/@catalog" + }, + "navigation": { + "@id": "http://localhost:8000/news/@navigation" + }, + "navroot": { + "@id": "http://localhost:8000/news/@navroot" + }, + "translations": { + "@id": "http://localhost:8000/news/@translations" + }, + "types": { + "@id": "http://localhost:8000/news/@types" + }, + "workflow": { + "@id": "http://localhost:8000/news/@workflow" + } + }, "@id": "http://localhost:8000/news/my-news-item", "@type": "Page", "id": "my-news-item", diff --git a/docs/examples/history/get.res b/docs/examples/history/get.res index 06a1085..e6aaca7 100644 --- a/docs/examples/history/get.res +++ b/docs/examples/history/get.res @@ -1,7 +1,6 @@ HTTP/1.1 200 OK Content-Type: application/json - [ { "@id": "http://localhost:8000/news/@history/1", diff --git a/docs/examples/history/get_version.res b/docs/examples/history/get_version.res index bb0edf1..ab4f97a 100644 --- a/docs/examples/history/get_version.res +++ b/docs/examples/history/get_version.res @@ -10,11 +10,39 @@ Content-Type: application/json }, "description": "News Items", "blocks_layout": { - "items": [ - "79ba8858-1dd3-4719-b731-5951e32fbf79" - ] + "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] }, "items": [], + "@components": { + "actions": { + "@id": "http://localhost:8000/news/@actions" + }, + "breadcrumbs": { + "@id": "http://localhost:8000/news/@breadcrumbs" + }, + "catalog": { + "@id": "http://localhost:8000/news/@catalog" + }, + "navigation": { + "@id": "http://localhost:8000/news/@navigation" + }, + "navroot": { + "@id": "http://localhost:8000/news/@navroot" + }, + "translations": { + "@id": "http://localhost:8000/news/@translations" + }, + "types": { + "@id": "http://localhost:8000/news/@types" + }, + "workflow": { + "@id": "http://localhost:8000/news/@workflow" + } + }, + "language": { + "title": "English", + "token": "en" + }, "@id": "http://localhost:8000/news", "@type": "Folder", "id": "news", diff --git a/docs/examples/navroot/get.req b/docs/examples/navroot/get.req new file mode 100644 index 0000000..41fd59b --- /dev/null +++ b/docs/examples/navroot/get.req @@ -0,0 +1,3 @@ +GET /news/@navroot HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk diff --git a/docs/examples/navroot/get.res b/docs/examples/navroot/get.res new file mode 100644 index 0000000..8647216 --- /dev/null +++ b/docs/examples/navroot/get.res @@ -0,0 +1,182 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "title": "Welcome to Nick!", + "blocks": { + "495efb73-cbdd-4bef-935a-a56f70a20854": { + "text": { + "blocks": [ + { + "key": "9f35d", + "data": {}, + "text": "This is the demo site of Nick and is build with the Volto frontend.", + "type": "unstyled", + "depth": 0, + "entityRanges": [ + { "key": 0, "length": 4, "offset": 25 }, + { "key": 1, "length": 5, "offset": 52 } + ], + "inlineStyleRanges": [] + } + ], + "entityMap": { + "0": { + "data": { "url": "https://nickcms.org" }, + "type": "LINK", + "mutability": "MUTABLE" + }, + "1": { + "data": { "url": "https://voltocms.com" }, + "type": "LINK", + "mutability": "MUTABLE" + } + } + }, + "@type": "text" + }, + "6a6d1e67-fefd-4049-98a3-300f0302abcb": { + "text": { + "blocks": [ + { + "key": "3jol2", + "data": {}, + "text": "You can use this site to test the latest version of Nick. You can login with username admin and password admin.", + "type": "unstyled", + "depth": 0, + "entityRanges": [{ "key": 0, "length": 4, "offset": 52 }], + "inlineStyleRanges": [ + { "style": "ITALIC", "length": 8, "offset": 77 }, + { "style": "ITALIC", "length": 8, "offset": 96 }, + { "style": "BOLD", "length": 5, "offset": 86 }, + { "style": "BOLD", "length": 5, "offset": 105 } + ] + } + ], + "entityMap": { + "0": { + "data": { "url": "https://nickcms.org" }, + "type": "LINK", + "mutability": "MUTABLE" + } + } + }, + "@type": "text" + }, + "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" }, + "be383a3d-7409-42b5-a5bc-555e2fbf068d": { + "text": { + "blocks": [ + { + "key": "atc31", + "data": {}, + "text": "This instance is reset every night so feel free to make any changes!", + "type": "unstyled", + "depth": 0, + "entityRanges": [], + "inlineStyleRanges": [] + } + ], + "entityMap": {} + }, + "@type": "text" + }, + "eb024f35-ab6a-4034-ac5b-77c91fe3d400": { + "text": { + "blocks": [ + { + "key": "5s8ah", + "data": {}, + "text": "Demo", + "type": "header-three", + "depth": 0, + "entityRanges": [], + "inlineStyleRanges": [] + } + ], + "entityMap": {} + }, + "@type": "text" + } + }, + "effective": "2022-04-02T20:00:00.000Z", + "description": "Congratulations! You have successfully installed Nick.", + "blocks_layout": { + "items": [ + "79ba8858-1dd3-4719-b731-5951e32fbf79", + "495efb73-cbdd-4bef-935a-a56f70a20854", + "eb024f35-ab6a-4034-ac5b-77c91fe3d400", + "6a6d1e67-fefd-4049-98a3-300f0302abcb", + "be383a3d-7409-42b5-a5bc-555e2fbf068d" + ] + }, + "items": [ + { + "title": "Events", + "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } }, + "effective": "2022-04-02T20:30:00.000Z", + "blocks_layout": { "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] }, + "@id": "http://localhost:8000/events", + "@type": "Folder", + "id": "events", + "created": "2022-04-02T20:30:00.000Z", + "modified": "2022-04-02T20:30:00.000Z", + "UID": "1a2123ba-14e8-4910-8e6b-c04a40d72a41", + "owner": "admin", + "layout": "view", + "is_folderish": true, + "language": { "token": "en", "title": "English" }, + "review_state": "published", + "lock": { "locked": false, "stealable": true } + }, + { + "title": "News", + "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } }, + "effective": "2022-04-02T20:22:00.000Z", + "description": "News Items", + "blocks_layout": { "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] }, + "@id": "http://localhost:8000/news", + "@type": "Folder", + "id": "news", + "created": "2022-04-02T20:22:00.000Z", + "modified": "2022-04-02T20:22:00.000Z", + "UID": "32215c67-86de-462a-8cc0-eabcd2b39c26", + "owner": "admin", + "layout": "view", + "is_folderish": true, + "language": { "token": "en", "title": "English" }, + "review_state": "published", + "lock": { "locked": false, "stealable": true } + }, + { + "title": "Users", + "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" } }, + "effective": "2022-04-02T20:24:00.000Z", + "blocks_layout": { "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] }, + "@id": "http://localhost:8000/users", + "@type": "Folder", + "id": "users", + "created": "2022-04-02T20:24:00.000Z", + "modified": "2022-04-02T20:24:00.000Z", + "UID": "80994493-74ca-4b94-9a7c-145a33a6dd80", + "owner": "admin", + "layout": "view", + "is_folderish": true, + "language": { "token": "en", "title": "English" }, + "review_state": "published", + "lock": { "locked": false, "stealable": true } + } + ], + "@id": "http://localhost:8000", + "@type": "Site", + "id": "root", + "created": "2022-04-02T20:00:00.000Z", + "modified": "2022-04-02T20:00:00.000Z", + "UID": "92a80817-f5b7-400d-8f58-b08126f0f09b", + "owner": "admin", + "layout": "view", + "is_folderish": true, + "language": { "token": "en", "title": "English" }, + "review_state": "published", + "lock": { "locked": false, "stealable": true } +} diff --git a/docs/examples/translations/delete.req b/docs/examples/translations/delete.req new file mode 100644 index 0000000..38c6dbd --- /dev/null +++ b/docs/examples/translations/delete.req @@ -0,0 +1,7 @@ +DELETE /en/events/@translations HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk + +{ + "language": "nl" +} \ No newline at end of file diff --git a/docs/examples/translations/delete.res b/docs/examples/translations/delete.res new file mode 100644 index 0000000..58e46ab --- /dev/null +++ b/docs/examples/translations/delete.res @@ -0,0 +1 @@ +HTTP/1.1 204 No Content diff --git a/docs/examples/translations/get.req b/docs/examples/translations/get.req new file mode 100644 index 0000000..db614bc --- /dev/null +++ b/docs/examples/translations/get.req @@ -0,0 +1,3 @@ +GET /en/events/@translations HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk diff --git a/docs/examples/translations/get.res b/docs/examples/translations/get.res new file mode 100644 index 0000000..482898c --- /dev/null +++ b/docs/examples/translations/get.res @@ -0,0 +1,16 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "@id": "http://localhost:8000/en/events/@translations", + "items": [ + { + "@id": "http://localhost:8000/nl/evenementen", + "language": "nl" + } + ], + "root": { + "en": "http://localhost:8000/en", + "nl": "http://localhost:8000/nl" + } +} \ No newline at end of file diff --git a/docs/examples/translations/get_expansion.req b/docs/examples/translations/get_expansion.req new file mode 100644 index 0000000..69a1a9f --- /dev/null +++ b/docs/examples/translations/get_expansion.req @@ -0,0 +1,3 @@ +GET /en/events HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk diff --git a/docs/examples/translations/get_expansion.res b/docs/examples/translations/get_expansion.res new file mode 100644 index 0000000..72dc950 --- /dev/null +++ b/docs/examples/translations/get_expansion.res @@ -0,0 +1,50 @@ +HTTP/1.1 201 Created +Content-Type: application/json + +{ + "title": "Events", + "@components": { + "actions": { + "@id": "http://localhost:8000/en/events/@actions" + }, + "breadcrumbs": { + "@id": "http://localhost:8000/en/events/@breadcrumbs" + }, + "catalog": { + "@id": "http://localhost:8000/en/events/@catalog" + }, + "navigation": { + "@id": "http://localhost:8000/en/events/@navigation" + }, + "navroot": { + "@id": "http://localhost:8000/en/events/@navroot" + }, + "translations": { + "@id": "http://localhost:8000/en/events/@translations" + }, + "types": { + "@id": "http://localhost:8000/en/events/@types" + }, + "workflow": { + "@id": "http://localhost:8000/en/events/@workflow" + } + }, + "@id": "http://localhost:8000/en/events/my-news-item", + "@type": "Page", + "id": "events", + "created": "2022-04-08T16:00:00.000Z", + "modified": "2022-04-08T16:00:00.000Z", + "UID": "a95388f2-e4b3-4292-98aa-62656cbd5b9c", + "is_folderish": true, + "layout": "view", + "owner": "admin", + "review_state": "private", + "language": { + "title": "English", + "token": "en" + }, + "lock": { + "locked": false, + "stealable": true + } +} diff --git a/docs/examples/translations/get_expansion_expanded.req b/docs/examples/translations/get_expansion_expanded.req new file mode 100644 index 0000000..4685da3 --- /dev/null +++ b/docs/examples/translations/get_expansion_expanded.req @@ -0,0 +1,3 @@ +GET /en/events/?expand=translations HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk diff --git a/docs/examples/translations/get_expansion_expanded.res b/docs/examples/translations/get_expansion_expanded.res new file mode 100644 index 0000000..c38ed0b --- /dev/null +++ b/docs/examples/translations/get_expansion_expanded.res @@ -0,0 +1,60 @@ +HTTP/1.1 201 Created +Content-Type: application/json + +{ + "title": "Events", + "@components": { + "actions": { + "@id": "http://localhost:8000/en/events/@actions" + }, + "breadcrumbs": { + "@id": "http://localhost:8000/en/events/@breadcrumbs" + }, + "catalog": { + "@id": "http://localhost:8000/en/events/@catalog" + }, + "navigation": { + "@id": "http://localhost:8000/en/events/@navigation" + }, + "navroot": { + "@id": "http://localhost:8000/en/events/@navroot" + }, + "translations": { + "@id": "http://localhost:8000/en/events/@translations", + "items": [ + { + "@id": "http://localhost:8000/nl/evenementen", + "language": "nl" + } + ], + "root": { + "en": "http://localhost:8000/en", + "nl": "http://localhost:8000/nl" + } + }, + "types": { + "@id": "http://localhost:8000/en/events/@types" + }, + "workflow": { + "@id": "http://localhost:8000/en/events/@workflow" + } + }, + "@id": "http://localhost:8000/en/events/my-news-item", + "@type": "Page", + "id": "events", + "created": "2022-04-08T16:00:00.000Z", + "modified": "2022-04-08T16:00:00.000Z", + "UID": "a95388f2-e4b3-4292-98aa-62656cbd5b9c", + "is_folderish": true, + "layout": "view", + "owner": "admin", + "review_state": "private", + "language": { + "title": "English", + "token": "en" + }, + "lock": { + "locked": false, + "stealable": true + } +} diff --git a/docs/examples/translations/get_locator.req b/docs/examples/translations/get_locator.req new file mode 100644 index 0000000..690d5b7 --- /dev/null +++ b/docs/examples/translations/get_locator.req @@ -0,0 +1,3 @@ +GET /en/events/@translation-locator?target_language=nl HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk diff --git a/docs/examples/translations/get_locator.res b/docs/examples/translations/get_locator.res new file mode 100644 index 0000000..343cce0 --- /dev/null +++ b/docs/examples/translations/get_locator.res @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "@id": "http://localhost:8000/nl" +} diff --git a/docs/examples/translations/post.req b/docs/examples/translations/post.req new file mode 100644 index 0000000..4fe3cdb --- /dev/null +++ b/docs/examples/translations/post.req @@ -0,0 +1,12 @@ +POST /nl HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk +Content-Type: application/json + +{ + "@type": "Page", + "id": "evenementen", + "language": "nl", + "title": "Evenementen", + "translation_of": "495efb73-cbdd-4bef-935a-a56f70a20854" +} diff --git a/docs/examples/translations/post.res b/docs/examples/translations/post.res new file mode 100644 index 0000000..26fbc97 --- /dev/null +++ b/docs/examples/translations/post.res @@ -0,0 +1,50 @@ +HTTP/1.1 201 Created +Content-Type: application/json + +{ + "title": "Evenementen", + "@components": { + "actions": { + "@id": "http://localhost:8000/nl/evenementen/@actions" + }, + "breadcrumbs": { + "@id": "http://localhost:8000/nl/evenementen/@breadcrumbs" + }, + "catalog": { + "@id": "http://localhost:8000/nl/evenementen/@catalog" + }, + "navigation": { + "@id": "http://localhost:8000/nl/evenementen/@navigation" + }, + "navroot": { + "@id": "http://localhost:8000/nl/evenementen/@navroot" + }, + "translations": { + "@id": "http://localhost:8000/nl/evenementen/@translations" + }, + "types": { + "@id": "http://localhost:8000/nl/evenementen/@types" + }, + "workflow": { + "@id": "http://localhost:8000/nl/evenementen/@workflow" + } + }, + "@id": "http://localhost:8000/nl/evenementen/my-news-item", + "@type": "Page", + "id": "evenementen", + "created": "2022-04-08T16:00:00.000Z", + "modified": "2022-04-08T16:00:00.000Z", + "UID": "a95388f2-e4b3-4292-98aa-62656cbd5b9c", + "is_folderish": true, + "layout": "view", + "owner": "admin", + "review_state": "private", + "language": { + "title": "Nederlands", + "token": "nl" + }, + "lock": { + "locked": false, + "stealable": true + } +} diff --git a/docs/examples/translations/post_path.req b/docs/examples/translations/post_path.req new file mode 100644 index 0000000..585e737 --- /dev/null +++ b/docs/examples/translations/post_path.req @@ -0,0 +1,8 @@ +POST /en/events/@translations HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk +Content-Type: application/json + +{ + "id": "/nl/evenementen" +} diff --git a/docs/examples/translations/post_path.res b/docs/examples/translations/post_path.res new file mode 100644 index 0000000..58e46ab --- /dev/null +++ b/docs/examples/translations/post_path.res @@ -0,0 +1 @@ +HTTP/1.1 204 No Content diff --git a/docs/examples/translations/post_url.req b/docs/examples/translations/post_url.req new file mode 100644 index 0000000..8e8ca13 --- /dev/null +++ b/docs/examples/translations/post_url.req @@ -0,0 +1,8 @@ +POST /en/events/@translations HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk +Content-Type: application/json + +{ + "id": "http://localhost:8000/nl/evenementen" +} diff --git a/docs/examples/translations/post_url.res b/docs/examples/translations/post_url.res new file mode 100644 index 0000000..58e46ab --- /dev/null +++ b/docs/examples/translations/post_url.res @@ -0,0 +1 @@ +HTTP/1.1 204 No Content diff --git a/docs/examples/translations/post_uuid.req b/docs/examples/translations/post_uuid.req new file mode 100644 index 0000000..28e3666 --- /dev/null +++ b/docs/examples/translations/post_uuid.req @@ -0,0 +1,8 @@ +POST /en/events/@translations HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImZ1bGxuYW1lIjoiQWRtaW4iLCJpYXQiOjE2NDkzMTI0NDl9.RS1Ny_r0v7vIylFfK6q0JVJrkiDuTOh9iG9IL8xbzAk +Content-Type: application/json + +{ + "id": "495efb73-cbdd-4bef-935a-a56f70a20854" +} diff --git a/docs/examples/translations/post_uuid.res b/docs/examples/translations/post_uuid.res new file mode 100644 index 0000000..58e46ab --- /dev/null +++ b/docs/examples/translations/post_uuid.res @@ -0,0 +1 @@ +HTTP/1.1 204 No Content diff --git a/docs/examples/types/get.res b/docs/examples/types/get.res index bc2eecc..61effcb 100644 --- a/docs/examples/types/get.res +++ b/docs/examples/types/get.res @@ -2,61 +2,37 @@ HTTP/1.1 200 OK Content-Type: application/json { - "required": [ - "title" - ], + "required": ["title"], "fieldsets": [ { "id": "default", "title": "Default", - "fields": [ - "title", - "description", - "changeNote" - ] + "fields": ["title", "description", "changeNote"] }, { "id": "categorization", "title": "Categorization", - "fields": [ - "subjects", - "language", - "relatedItems", - "coverage" - ] + "fields": ["subjects", "language", "relatedItems", "coverage"] }, { "id": "ownership", "title": "Ownership", - "fields": [ - "rights", - "source", - "publisher" - ] + "fields": ["rights", "source", "publisher"] }, { "id": "dates", "title": "Dates", - "fields": [ - "effective", - "expires" - ] + "fields": ["effective", "expires"] }, { "id": "layout", "title": "Layout", - "fields": [ - "blocks", - "blocks_layout" - ] + "fields": ["blocks", "blocks_layout"] }, { "id": "settings", "title": "Settings", - "fields": [ - "id", - "exclude_from_nav" - ] + "fields": ["id", "exclude_from_nav"] } ], "properties": { @@ -104,51 +80,21 @@ Content-Type: application/json "description": "The spatial or temporal topic of this item, spatial applicability of this item, or jurisdiction under which this item is relevant." }, "language": { - "enum": [ - "nl", - "en-US" - ], "type": "string", "title": "Language", - "choices": [ - [ - "nl", - "Dutch" - ], - [ - "en-US", - "English (United States)" - ] - ], - "default": "en-US", - "enumNames": [ - "Dutch", - "English (United States)" - ], + "factory": "Choice", + "vocabulary": { + "@id": "availableLanguages" + }, "description": "The language of this item." }, "subjects": { - "enum": [ - "Plone", - "Tokyo" - ], "type": "string", "title": "Tags", - "choices": [ - [ - "Plone", - "Plone" - ], - [ - "Tokyo", - "Tokyo" - ] - ], - "enumNames": [ - "Plone", - "Tokyo" - ], - "vocabulary": "plone.app.vocabularies.Keywords", + "factory": "Choice", + "vocabulary": { + "@id": "subjects" + }, "description": "The topic of this item." }, "effective": { diff --git a/docs/examples/vocabularies/list.res b/docs/examples/vocabularies/list.res index 603084d..98f7ed0 100644 --- a/docs/examples/vocabularies/list.res +++ b/docs/examples/vocabularies/list.res @@ -6,6 +6,10 @@ Content-Type: application/json "@id": "http://localhost:8000/@vocabularies/actions", "title": "actions" }, + { + "@id": "http://localhost:8000/@vocabularies/availableLanguages", + "title": "availableLanguages" + }, { "@id": "http://localhost:8000/@vocabularies/behaviors", "title": "behaviors" diff --git a/docs/navroot.md b/docs/navroot.md new file mode 100644 index 0000000..866ec4a --- /dev/null +++ b/docs/navroot.md @@ -0,0 +1,24 @@ +--- +nav_order: 16 +permalink: /navroot +--- + +# Navigation + +## Navigation root + +Nick has a concept called navigation root which provides a way to root catalog queries, searches, breadcrumbs, and so on in a given section of the site. This feature is useful when working with subsites or multilingual sites, because it allows the site manager to restrict searches or navigation queries to a specific location in the site. + +This navigation root information is different depending on the context of the request. For instance, in a default multilingual site when browsing the contents inside a language folder such as `www.domain.com/en`, the context is `en` and its navigation root will be `/en/`. In a non-multilingual site, the context is the root of the site such as `www.domain.com` and the navigation root will be `/`. + +To get the information about the navigation root, the REST API has a `@navroot` contextual endpoint. For instance, send a `GET` request to the `@navroot` endpoint at the root of the site: + +``` +{% include_relative examples/navroot/get.req %} +``` + +The response will contain the navigation root information for the site: + +``` +{% include_relative examples/navroot/get.res %} +``` diff --git a/docs/translations.md b/docs/translations.md new file mode 100644 index 0000000..9f89483 --- /dev/null +++ b/docs/translations.md @@ -0,0 +1,114 @@ +--- +nav_order: 25 +permalink: /translations +--- + +# Translations + +Multilingual is included in Nick. It is not enabled by default. + +You can enable the multilingual support by adding the `multilingual` profile to your configuration file. You can also replace the `default` profile with the `multilingualcontent` profile if you want to your initial content to be multilingual. + +Nick provides a `@translations` endpoint to handle the translation information of the content objects. + +Once we enabled more than one language, we can link two content items of different languages to be the translation of each other issuing a `POST` query to the `@translations` endpoint, including the `id` of the content to which it should be linked. The `id` of the content must be a full URL of the content object: + +``` +{% include_relative examples/translations/post_url.req %} +``` + +The API will return a 201 Created response, if the linking was successful: + +``` +{% include_relative examples/translations/post_url.res %} +``` + +We can also use the object's path to link the translation instead of the full URL: + +``` +{% include_relative examples/translations/post_path.req %} +``` + +``` +{% include_relative examples/translations/post_path.res %} +``` + +We can also use the object's UID to link the translation: + +``` +{% include_relative examples/translations/post_uuid.req %} +``` + +``` +{% include_relative examples/translations/post_uuid.res %} +``` + +After linking the contents, we can get the list of the translations of that content item by issuing a `GET` request on the `@translations` endpoint of that content item: + +``` +{% include_relative examples/translations/get.req %} +``` + +``` +{% include_relative examples/translations/get.res %} +``` + +To unlink the content, issue a `DELETE` request on the `@translations` endpoint of the content item, and provide the language code you want to unlink: + +``` +{% include_relative examples/translations/delete.req %} +``` + +``` +{% include_relative examples/translations/delete.res %} +``` + +## Creating a translation from an existing content + +The `POST` content endpoint to a folder is also capable of linking this new content with an existing translation using two parameters: `translationOf` and `language`. + +``` +{% include_relative examples/translations/post.req %} +``` + +``` +{% include_relative examples/translations/post.res %} +``` + +## Get location in the tree for new translations + +When you create a translation in Plone, there are policies in place for finding a suitable placement for it. This endpoint returns the proper placement for the newly created translation: + +``` +{% include_relative examples/translations/get_locator.req %} +``` + +``` +{% include_relative examples/translations/get_locator.res %} +``` + +## Expansion + +This service can be used with the Expansion mechanism which allows getting additional information about a content item in one query, avoiding additional requests. + +Translation information can be provided by the API expansion for translatable content items. + +If a simple `GET` request is done on the content item, a new entry will be shown on the `@components` entry, with the URL of the `@translations` endpoint: + +``` +{% include_relative examples/translations/get_expansion.req %} +``` + +``` +{% include_relative examples/translations/get_expansion.res %} +``` + +In order to expand and embed the translations component, use the GET parameter expand with the value translations. + +``` +{% include_relative examples/translations/get_expansion_expanded.req %} +``` + +``` +{% include_relative examples/translations/get_expansion_expanded.res %} +``` diff --git a/src/app.js b/src/app.js index 354bee5..9574619 100644 --- a/src/app.js +++ b/src/app.js @@ -63,6 +63,7 @@ map(routes, (route) => { compact(getPath(req).split('/')), // Slugs req.user, await req.user.fetchUserGroupRolesByDocument(root.uuid), // Root roles + root, trx, ); @@ -108,6 +109,7 @@ map(routes, (route) => { // Call handler req.document = document; + req.navroot = result.navroot; req.type = type; req.permissions = uniq([ ...permissions, diff --git a/src/helpers/index.js b/src/helpers/index.js index 215be38..0c45774 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -37,7 +37,7 @@ export { log, logger } from './log/log'; export { sendMail } from './mail/mail'; export { mergeSchemas, translateSchema } from './schema/schema'; export { testRequest } from './tests/tests'; -export { getPath, getUrl, getRootUrl } from './url/url'; +export { getPath, getUrl, getUrlByPath, getRootUrl } from './url/url'; export { arrayToVocabulary, isPromise, diff --git a/src/helpers/url/url.js b/src/helpers/url/url.js index ea76d3c..e9c60a7 100644 --- a/src/helpers/url/url.js +++ b/src/helpers/url/url.js @@ -17,6 +17,17 @@ export function getUrl(req) { }`; } +/** + * Get url by path + * @method getUrlByPath + * @param {Object} req Request object + * @param {string} path Path + * @returns {string} Url + */ +export function getUrlByPath(req, path) { + return `${req.protocol}://${req.headers.host}${path === '/' ? '' : path}`; +} + /** * Get root url * @method getRootUrl diff --git a/src/migrations/202411270928_translations.js b/src/migrations/202411270928_translations.js new file mode 100644 index 0000000..e4e9456 --- /dev/null +++ b/src/migrations/202411270928_translations.js @@ -0,0 +1,13 @@ +export const up = async (knex) => { + await knex.schema.alterTable('document', (table) => { + table.uuid('translation_group').index(); + table.string('language').index(); + }); +}; + +export const down = async (knex) => { + await knex.schema.alterTable('document', (table) => { + table.dropColumn('translation_group'); + table.dropColumn('language'); + }); +}; diff --git a/src/models/document/document.js b/src/models/document/document.js index 8cb1f31..c38afc5 100644 --- a/src/models/document/document.js +++ b/src/models/document/document.js @@ -27,6 +27,7 @@ import _, { } from 'lodash'; import { v4 as uuid } from 'uuid'; +import languages from '../../constants/languages'; import { Catalog, Model, Permission, Redirect, Role, User } from '../../models'; import { copyFile, @@ -40,6 +41,7 @@ import { } from '../../helpers'; import { DocumentCollection } from '../../collections'; import behaviors from '../../behaviors'; +import { TokenExpiredError } from 'jsonwebtoken'; const { config } = require(`${process.cwd()}/config`); @@ -329,9 +331,7 @@ export class Document extends Model { * @param {Object} req Request object. * @returns {Object} JSON object. */ - async toJSON(req) { - const components = {}; - + async toJSON(req, components = {}) { // Check if version data const version = this._version ? { @@ -423,11 +423,6 @@ export class Document extends Model { ); } - // Add catalog if available - if (this._catalog) { - components.catalog = this._catalog.toJSON(req); - } - // Return data return { ...json, @@ -443,6 +438,14 @@ export class Document extends Model { is_folderish: this._type ? includes(this._type._schema.behaviors, 'folderish') : true, + ...(this.language + ? { + language: { + token: this.language, + title: languages[this.language], + }, + } + : {}), review_state: this.workflow_state, lock: this.lock.locked && lockExpired(this) @@ -463,7 +466,7 @@ export class Document extends Model { * @param {Object} trx Transaction object. * @returns {Promise} A Promise that resolves to an object. */ - async traverse(slugs, user, roles, trx) { + async traverse(slugs, user, roles, navroot, trx) { // Check if at leaf node if (slugs.length === 0) { // Add owner to roles if current document owned by user @@ -476,6 +479,7 @@ export class Document extends Model { return { document: this, localRoles: extendedRoles, + navroot, }; } else { // Fetch child matching the id @@ -499,11 +503,18 @@ export class Document extends Model { trx, ); + // Fetch navroot + await child.fetchRelated('_type', trx); + const is_navroot = child._type + ? includes(child._type._schema.behaviors, 'navigation_root') + : false; + // Recursively call the traverse on child return child.traverse( drop(slugs), user, child.inherit_roles ? uniq([...roles, ...childRoles]) : childRoles, + is_navroot ? child : navroot, trx, ); } diff --git a/src/profiles/core/behaviors/categorization.json b/src/profiles/core/behaviors/categorization.json index 4573d4c..4515bc9 100644 --- a/src/profiles/core/behaviors/categorization.json +++ b/src/profiles/core/behaviors/categorization.json @@ -17,16 +17,13 @@ "type": "string" }, "language": { - "choices": [ - ["nl", "Dutch"], - ["en-US", "English (United States)"] - ], - "default": "en-US", "description:i18n": "The language of this item.", - "enum": ["nl", "en-US"], - "enumNames": ["Dutch", "English (United States)"], + "factory": "Choice", "title:i18n": "Language", - "type": "string" + "type": "string", + "vocabulary": { + "@id": "availableLanguages" + } }, "relatedItems": { "additionalItems": true, @@ -48,16 +45,13 @@ "factory": "Relation List" }, "subjects": { - "choices": [ - ["Plone", "Plone"], - ["Tokyo", "Tokyo"] - ], "description:i18n": "The topic of this item.", - "enum": ["Plone", "Tokyo"], - "enumNames": ["Plone", "Tokyo"], + "factory": "Choice", "title:i18n": "Tags", "type": "string", - "vocabulary": "plone.app.vocabularies.Keywords" + "vocabulary": { + "@id": "subjects" + } } } } diff --git a/src/profiles/core/types/site.json b/src/profiles/core/types/site.json index b985cf1..8bfe871 100644 --- a/src/profiles/core/types/site.json +++ b/src/profiles/core/types/site.json @@ -6,7 +6,14 @@ "filter_content_types": false, "allowed_content_types": [], "schema": { - "behaviors": ["dublin_core", "dates", "blocks", "versioning", "folderish"] + "behaviors": [ + "dublin_core", + "dates", + "blocks", + "versioning", + "folderish", + "navigation_root" + ] }, "workflow": "simple_publication_workflow" } diff --git a/src/profiles/default/documents/_root.json b/src/profiles/default/documents/_root.json index fe74cc9..dfcf681 100644 --- a/src/profiles/default/documents/_root.json +++ b/src/profiles/default/documents/_root.json @@ -8,7 +8,7 @@ "created": "2022-04-02T20:00:00.000Z", "modified": "2022-04-02T20:00:00.000Z", "effective": "2022-04-02T20:00:00.000Z", - + "language": "en", "blocks": { "495efb73-cbdd-4bef-935a-a56f70a20854": { "text": { diff --git a/src/profiles/default/documents/events.event-1.json b/src/profiles/default/documents/events.event-1.json index 9a75e42..ccc1edf 100644 --- a/src/profiles/default/documents/events.event-1.json +++ b/src/profiles/default/documents/events.event-1.json @@ -8,6 +8,7 @@ "modified": "2022-04-02T20:10:00.000Z", "effective": "2022-04-02T20:10:00.000Z", "subjects": ["event"], + "language": "en", "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" diff --git a/src/profiles/default/documents/events.json b/src/profiles/default/documents/events.json index 4482947..92d6dfe 100644 --- a/src/profiles/default/documents/events.json +++ b/src/profiles/default/documents/events.json @@ -7,6 +7,7 @@ "created": "2022-04-02T20:30:00.000Z", "modified": "2022-04-02T20:30:00.000Z", "effective": "2022-04-02T20:30:00.000Z", + "language": "en", "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" diff --git a/src/profiles/default/documents/news.json b/src/profiles/default/documents/news.json index d59a139..804ddc3 100644 --- a/src/profiles/default/documents/news.json +++ b/src/profiles/default/documents/news.json @@ -8,6 +8,7 @@ "created": "2022-04-02T20:22:00.000Z", "modified": "2022-04-02T20:22:00.000Z", "effective": "2022-04-02T20:22:00.000Z", + "language": "en", "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" diff --git a/src/profiles/default/documents/users.json b/src/profiles/default/documents/users.json index 24b778c..c4a57b3 100644 --- a/src/profiles/default/documents/users.json +++ b/src/profiles/default/documents/users.json @@ -7,6 +7,7 @@ "created": "2022-04-02T20:24:00.000Z", "modified": "2022-04-02T20:24:00.000Z", "effective": "2022-04-02T20:24:00.000Z", + "language": "en", "blocks": { "79ba8858-1dd3-4719-b731-5951e32fbf79": { "@type": "title" diff --git a/src/profiles/multilingual/metadata.json b/src/profiles/multilingual/metadata.json new file mode 100644 index 0000000..5dd0e5b --- /dev/null +++ b/src/profiles/multilingual/metadata.json @@ -0,0 +1,6 @@ +{ + "id": "@robgietema/nick:multilingual", + "title": "Nick Multilingual", + "description": "Multilingual functionality of Nick", + "version": 1000 +} diff --git a/src/profiles/multilingual/types/languageroot.json b/src/profiles/multilingual/types/languageroot.json new file mode 100644 index 0000000..1528caa --- /dev/null +++ b/src/profiles/multilingual/types/languageroot.json @@ -0,0 +1,21 @@ +{ + "id": "Languageroot", + "title:i18n": "Languageroot", + "description:i18n": "", + "global_allow": true, + "filter_content_types": false, + "allowed_content_types": [], + "schema": { + "behaviors": [ + "dublin_core", + "dates", + "blocks", + "versioning", + "short_name", + "id_from_title", + "folderish", + "navigation_root" + ] + }, + "workflow": "simple_publication_workflow" +} diff --git a/src/profiles/multilingualcontent/controlpanels/language.json b/src/profiles/multilingualcontent/controlpanels/language.json new file mode 100644 index 0000000..3b4d6ba --- /dev/null +++ b/src/profiles/multilingualcontent/controlpanels/language.json @@ -0,0 +1,7 @@ +{ + "id": "language", + "data": { + "default_language": "en", + "available_languages": ["en", "nl"] + } +} diff --git a/src/profiles/multilingualcontent/documents/_root.json b/src/profiles/multilingualcontent/documents/_root.json new file mode 100644 index 0000000..7508f05 --- /dev/null +++ b/src/profiles/multilingualcontent/documents/_root.json @@ -0,0 +1,14 @@ +{ + "uuid": "92aa0817-f5b7-400d-8f58-b08146f0f09b", + "type": "Site", + "title": "", + "description": "", + "owner": "admin", + "workflow_state": "published", + "created": "2022-04-02T20:00:00.000Z", + "modified": "2022-04-02T20:00:00.000Z", + "effective": "2022-04-02T20:00:00.000Z", + "language": "", + "blocks": {}, + "blocks_layout": {} +} diff --git a/src/profiles/multilingualcontent/documents/en.events.event-1.json b/src/profiles/multilingualcontent/documents/en.events.event-1.json new file mode 100644 index 0000000..ccc1edf --- /dev/null +++ b/src/profiles/multilingualcontent/documents/en.events.event-1.json @@ -0,0 +1,40 @@ +{ + "uuid": "405ca717-0c68-43a0-88ac-629a82658675", + "type": "Page", + "title": "Event 1", + "owner": "admin", + "workflow_state": "published", + "created": "2022-04-02T20:10:00.000Z", + "modified": "2022-04-02T20:10:00.000Z", + "effective": "2022-04-02T20:10:00.000Z", + "subjects": ["event"], + "language": "en", + "blocks": { + "79ba8858-1dd3-4719-b731-5951e32fbf79": { + "@type": "title" + }, + "495efb73-cbdd-4bef-935a-a56f70a20854": { + "@type": "text", + "text": { + "blocks": [ + { + "key": "9f35d", + "text": "This is the first event.", + "type": "unstyled", + "depth": 0, + "inlineStyleRanges": [], + "entityRanges": [], + "data": {} + } + ], + "entityMap": {} + } + } + }, + "blocks_layout": { + "items": [ + "79ba8858-1dd3-4719-b731-5951e32fbf79", + "495efb73-cbdd-4bef-935a-a56f70a20854" + ] + } +} diff --git a/src/profiles/multilingualcontent/documents/en.events.json b/src/profiles/multilingualcontent/documents/en.events.json new file mode 100644 index 0000000..f0e8977 --- /dev/null +++ b/src/profiles/multilingualcontent/documents/en.events.json @@ -0,0 +1,20 @@ +{ + "uuid": "1a2123ba-14e8-4910-8e6b-c04a40d72a41", + "type": "Folder", + "title": "Events", + "owner": "admin", + "workflow_state": "published", + "created": "2022-04-02T20:30:00.000Z", + "modified": "2022-04-02T20:30:00.000Z", + "effective": "2022-04-02T20:30:00.000Z", + "language": "en", + "translation_group": "1a2123ba-14e8-4910-8e6b-c04a40d72a41", + "blocks": { + "79ba8858-1dd3-4719-b731-5951e32fbf79": { + "@type": "title" + } + }, + "blocks_layout": { + "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] + } +} diff --git a/src/profiles/multilingualcontent/documents/en.json b/src/profiles/multilingualcontent/documents/en.json new file mode 100644 index 0000000..8deefc3 --- /dev/null +++ b/src/profiles/multilingualcontent/documents/en.json @@ -0,0 +1,156 @@ +{ + "uuid": "92a80817-f5b7-400d-8f58-b08126f0f09b", + "type": "Languageroot", + "title": "Welcome to Nick!", + "description": "Congratulations! You have successfully installed Nick.", + "owner": "admin", + "workflow_state": "published", + "created": "2022-04-02T20:00:00.000Z", + "modified": "2022-04-02T20:00:00.000Z", + "effective": "2022-04-02T20:00:00.000Z", + "language": "en", + "translation_group": "92a80817-f5b7-400d-8f58-b08126f0f09b", + "blocks": { + "495efb73-cbdd-4bef-935a-a56f70a20854": { + "text": { + "blocks": [ + { + "key": "9f35d", + "data": {}, + "text": "This is the demo site of Nick and is build with the Volto frontend.", + "type": "unstyled", + "depth": 0, + "entityRanges": [ + { + "key": 0, + "length": 4, + "offset": 25 + }, + { + "key": 1, + "length": 5, + "offset": 52 + } + ], + "inlineStyleRanges": [] + } + ], + "entityMap": { + "0": { + "data": { + "url": "https://nickcms.org" + }, + "type": "LINK", + "mutability": "MUTABLE" + }, + "1": { + "data": { + "url": "https://voltocms.com" + }, + "type": "LINK", + "mutability": "MUTABLE" + } + } + }, + "@type": "text" + }, + "6a6d1e67-fefd-4049-98a3-300f0302abcb": { + "text": { + "blocks": [ + { + "key": "3jol2", + "data": {}, + "text": "You can use this site to test the latest version of Nick. You can login with username admin and password admin.", + "type": "unstyled", + "depth": 0, + "entityRanges": [ + { + "key": 0, + "length": 4, + "offset": 52 + } + ], + "inlineStyleRanges": [ + { + "style": "ITALIC", + "length": 8, + "offset": 77 + }, + { + "style": "ITALIC", + "length": 8, + "offset": 96 + }, + { + "style": "BOLD", + "length": 5, + "offset": 86 + }, + { + "style": "BOLD", + "length": 5, + "offset": 105 + } + ] + } + ], + "entityMap": { + "0": { + "data": { + "url": "https://nickcms.org" + }, + "type": "LINK", + "mutability": "MUTABLE" + } + } + }, + "@type": "text" + }, + "79ba8858-1dd3-4719-b731-5951e32fbf79": { + "@type": "title" + }, + "be383a3d-7409-42b5-a5bc-555e2fbf068d": { + "text": { + "blocks": [ + { + "key": "atc31", + "data": {}, + "text": "This instance is reset every night so feel free to make any changes!", + "type": "unstyled", + "depth": 0, + "entityRanges": [], + "inlineStyleRanges": [] + } + ], + "entityMap": {} + }, + "@type": "text" + }, + "eb024f35-ab6a-4034-ac5b-77c91fe3d400": { + "text": { + "blocks": [ + { + "key": "5s8ah", + "data": {}, + "text": "Demo", + "type": "header-three", + "depth": 0, + "entityRanges": [], + "inlineStyleRanges": [] + } + ], + "entityMap": {} + }, + "@type": "text" + } + }, + "blocks_layout": { + "items": [ + "79ba8858-1dd3-4719-b731-5951e32fbf79", + "495efb73-cbdd-4bef-935a-a56f70a20854", + "eb024f35-ab6a-4034-ac5b-77c91fe3d400", + "6a6d1e67-fefd-4049-98a3-300f0302abcb", + "be383a3d-7409-42b5-a5bc-555e2fbf068d" + ] + } +} diff --git a/src/profiles/multilingualcontent/documents/en.news.json b/src/profiles/multilingualcontent/documents/en.news.json new file mode 100644 index 0000000..804ddc3 --- /dev/null +++ b/src/profiles/multilingualcontent/documents/en.news.json @@ -0,0 +1,60 @@ +{ + "uuid": "32215c67-86de-462a-8cc0-eabcd2b39c26", + "type": "Folder", + "title": "News", + "description": "News Items", + "owner": "admin", + "workflow_state": "published", + "created": "2022-04-02T20:22:00.000Z", + "modified": "2022-04-02T20:22:00.000Z", + "effective": "2022-04-02T20:22:00.000Z", + "language": "en", + "blocks": { + "79ba8858-1dd3-4719-b731-5951e32fbf79": { + "@type": "title" + } + }, + "blocks_layout": { + "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] + }, + "workflow_history": [ + { + "action": "publish", + "actor": "admin", + "review_state": "published", + "state_title": "Published", + "time": "2022-04-03T20:22:00.000Z", + "transition_title": "Publish" + } + ], + "versions": [ + { + "created": "2022-04-02T20:22:00.000Z", + "actor": "admin", + "title": "Old News", + "changeNote": "Initial version", + "blocks": { + "79ba8858-1dd3-4719-b731-5951e32fbf79": { + "@type": "title" + } + }, + "blocks_layout": { + "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] + } + }, + { + "created": "2022-04-04T20:22:00.000Z", + "actor": "admin", + "title": "News", + "changeNote": "Changed title", + "blocks": { + "79ba8858-1dd3-4719-b731-5951e32fbf79": { + "@type": "title" + } + }, + "blocks_layout": { + "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] + } + } + ] +} diff --git a/src/profiles/multilingualcontent/documents/en.users.json b/src/profiles/multilingualcontent/documents/en.users.json new file mode 100644 index 0000000..c4a57b3 --- /dev/null +++ b/src/profiles/multilingualcontent/documents/en.users.json @@ -0,0 +1,33 @@ +{ + "uuid": "80994493-74ca-4b94-9a7c-145a33a6dd80", + "type": "Folder", + "title": "Users", + "owner": "admin", + "workflow_state": "published", + "created": "2022-04-02T20:24:00.000Z", + "modified": "2022-04-02T20:24:00.000Z", + "effective": "2022-04-02T20:24:00.000Z", + "language": "en", + "blocks": { + "79ba8858-1dd3-4719-b731-5951e32fbf79": { + "@type": "title" + } + }, + "blocks_layout": { + "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] + }, + "sharing": { + "users": [ + { + "id": "anonymous", + "roles": ["Reader"] + } + ], + "groups": [ + { + "id": "Administrators", + "roles": ["Editor"] + } + ] + } +} diff --git a/src/profiles/multilingualcontent/documents/nl.evenementen.json b/src/profiles/multilingualcontent/documents/nl.evenementen.json new file mode 100644 index 0000000..7886cda --- /dev/null +++ b/src/profiles/multilingualcontent/documents/nl.evenementen.json @@ -0,0 +1,20 @@ +{ + "uuid": "1a8123ba-14e8-4910-8e6b-c04a40d72af1", + "type": "Folder", + "title": "Evenementen", + "owner": "admin", + "workflow_state": "published", + "created": "2022-04-02T20:30:00.000Z", + "modified": "2022-04-02T20:30:00.000Z", + "effective": "2022-04-02T20:30:00.000Z", + "language": "nl", + "translation_group": "1a2123ba-14e8-4910-8e6b-c04a40d72a41", + "blocks": { + "79ba8858-1dd3-4719-b731-5951e32fbf79": { + "@type": "title" + } + }, + "blocks_layout": { + "items": ["79ba8858-1dd3-4719-b731-5951e32fbf79"] + } +} diff --git a/src/profiles/multilingualcontent/documents/nl.json b/src/profiles/multilingualcontent/documents/nl.json new file mode 100644 index 0000000..efc9828 --- /dev/null +++ b/src/profiles/multilingualcontent/documents/nl.json @@ -0,0 +1,146 @@ +{ + "uuid": "92a80817-f527-400d-8f58-b081a6f0f09b", + "type": "Languageroot", + "title": "Welkom bij Nick!", + "description": "Gefeliciteerd! Je hebt Nick succesvol geïnstalleerd.", + "owner": "admin", + "workflow_state": "published", + "created": "2022-04-02T20:00:00.000Z", + "modified": "2022-04-02T20:00:00.000Z", + "effective": "2022-04-02T20:00:00.000Z", + "language": "nl", + "translation_group": "92a80817-f5b7-400d-8f58-b08126f0f09b", + "blocks": { + "495efb73-cbdd-4bef-935a-a56f70a20854": { + "text": { + "blocks": [ + { + "key": "9f35d", + "data": {}, + "text": "This is the demo site of Nick and is build with the Volto frontend.", + "type": "unstyled", + "depth": 0, + "entityRanges": [ + { + "key": 0, + "length": 4, + "offset": 25 + }, + { + "key": 1, + "length": 5, + "offset": 52 + } + ], + "inlineStyleRanges": [] + } + ], + "entityMap": { + "0": { + "data": { + "url": "https://nickcms.org" + }, + "type": "LINK", + "mutability": "MUTABLE" + }, + "1": { + "data": { + "url": "https://voltocms.com" + }, + "type": "LINK", + "mutability": "MUTABLE" + } + } + }, + "@type": "text" + }, + "6a6d1e67-fefd-4049-98a3-300f0302abcb": { + "text": { + "blocks": [ + { + "key": "3jol2", + "text": "U kunt deze site gebruiken om de nieuwste versie van Nick te testen. U kunt inloggen met gebruikersnaam admin en wachtwoord admin.", + "type": "unstyled", + "depth": 0, + "inlineStyleRanges": [ + { + "offset": 89, + "length": 14, + "style": "ITALIC" + }, + { + "offset": 113, + "length": 10, + "style": "ITALIC" + } + ], + "entityRanges": [ + { + "offset": 53, + "length": 4, + "key": 0 + } + ], + "data": {} + } + ], + "entityMap": { + "0": { + "type": "LINK", + "mutability": "MUTABLE", + "data": { + "url": "https://nickcms.org" + } + } + } + }, + "@type": "text" + }, + "79ba8858-1dd3-4719-b731-5951e32fbf79": { + "@type": "title" + }, + "be383a3d-7409-42b5-a5bc-555e2fbf068d": { + "text": { + "blocks": [ + { + "key": "atc31", + "text": "Deze site wordt elke nacht gereset, dus u kunt gerust wijzigingen aanbrengen!", + "type": "unstyled", + "depth": 0, + "inlineStyleRanges": [], + "entityRanges": [], + "data": {} + } + ], + "entityMap": {} + }, + "@type": "text" + }, + "eb024f35-ab6a-4034-ac5b-77c91fe3d400": { + "text": { + "blocks": [ + { + "key": "5s8ah", + "data": {}, + "text": "Demo", + "type": "header-three", + "depth": 0, + "entityRanges": [], + "inlineStyleRanges": [] + } + ], + "entityMap": {} + }, + "@type": "text" + } + }, + "blocks_layout": { + "items": [ + "79ba8858-1dd3-4719-b731-5951e32fbf79", + "495efb73-cbdd-4bef-935a-a56f70a20854", + "eb024f35-ab6a-4034-ac5b-77c91fe3d400", + "6a6d1e67-fefd-4049-98a3-300f0302abcb", + "be383a3d-7409-42b5-a5bc-555e2fbf068d" + ] + } +} diff --git a/src/profiles/multilingualcontent/groups.json b/src/profiles/multilingualcontent/groups.json new file mode 100644 index 0000000..d93ea90 --- /dev/null +++ b/src/profiles/multilingualcontent/groups.json @@ -0,0 +1,12 @@ +{ + "purge": true, + "groups": [ + { + "id": "Administrators", + "title:i18n": "Administrators", + "description:i18n": "", + "email": "", + "roles": ["Administrator"] + } + ] +} diff --git a/src/profiles/multilingualcontent/metadata.json b/src/profiles/multilingualcontent/metadata.json new file mode 100644 index 0000000..b3bf08b --- /dev/null +++ b/src/profiles/multilingualcontent/metadata.json @@ -0,0 +1,6 @@ +{ + "id": "@robgietema/nick:multilingualcontent", + "title": "Nick Multilingual Content", + "description": "Multilingual content for Nick", + "version": 1000 +} diff --git a/src/profiles/multilingualcontent/users.json b/src/profiles/multilingualcontent/users.json new file mode 100644 index 0000000..299e3d2 --- /dev/null +++ b/src/profiles/multilingualcontent/users.json @@ -0,0 +1,12 @@ +{ + "purge": false, + "users": [ + { + "id": "admin", + "password": "admin", + "fullname": "Admin", + "email": "admin@example.com", + "roles": ["Administrator"] + } + ] +} diff --git a/src/routes/actions/actions.js b/src/routes/actions/actions.js index ed5c51a..f9528e6 100644 --- a/src/routes/actions/actions.js +++ b/src/routes/actions/actions.js @@ -5,16 +5,18 @@ import { Action } from '../../models'; +export const handler = async (req, trx) => { + const actions = await Action.fetchAll({}, { order: 'order' }, trx); + return { + json: actions.toJSON(req), + }; +}; + export default [ { op: 'get', view: '/@actions', permission: 'View', - handler: async (req, trx) => { - const actions = await Action.fetchAll({}, { order: 'order' }, trx); - return { - json: actions.toJSON(req), - }; - }, + handler, }, ]; diff --git a/src/routes/breadcrumbs/breadcrumbs.js b/src/routes/breadcrumbs/breadcrumbs.js index d8294d4..9d15db4 100644 --- a/src/routes/breadcrumbs/breadcrumbs.js +++ b/src/routes/breadcrumbs/breadcrumbs.js @@ -3,7 +3,7 @@ * @module routes/breadcrumbs/breadcrumbs */ -import { compact, drop, head, last } from 'lodash'; +import { compact, drop, head, includes, last } from 'lodash'; import { Document } from '../../models'; import { getUrl, getRootUrl, getPath } from '../../helpers'; @@ -21,8 +21,8 @@ async function traverse(document, slugs, items, trx) { if (slugs.length === 0) { return items; } else { - // Get parent - let parent = await Document.fetchOne( + // Get child + let child = await Document.fetchOne( { parent: document.uuid, id: head(slugs), @@ -32,17 +32,19 @@ async function traverse(document, slugs, items, trx) { ); // Apply behaviors - await parent.applyBehaviors(trx); + await child.applyBehaviors(trx); // Traverse up return traverse( - parent, + child, drop(slugs), [ - ...items, + ...(includes(child._type._schema.behaviors, 'navigation_root') + ? [] + : items), { - '@id': `${last(items)['@id']}/${parent.id}`, - title: parent.getTitle(), + '@id': `${last(items)['@id']}/${child.id}`, + title: child.getTitle(), }, ], trx, @@ -50,35 +52,38 @@ async function traverse(document, slugs, items, trx) { } } +export const handler = async (req, trx) => { + const slugs = getPath(req).split('/'); + let document = await Document.fetchOne({ parent: null }, {}, trx); + + // Apply behaviors + await document.applyBehaviors(trx); + + const items = await traverse( + document, + compact(slugs), + [ + { + '@id': getRootUrl(req), + title: document.getTitle(), + }, + ], + trx, + ); + return { + json: { + '@id': `${getUrl(req)}/@breadcrumbs`, + items: drop(items), + root: req.navroot.path, + }, + }; +}; + export default [ { op: 'get', view: '/@breadcrumbs', permission: 'View', - handler: async (req, trx) => { - const slugs = getPath(req).split('/'); - let document = await Document.fetchOne({ parent: null }, {}, trx); - - // Apply behaviors - await document.applyBehaviors(trx); - - const items = await traverse( - document, - compact(slugs), - [ - { - '@id': getRootUrl(req), - title: document.getTitle(), - }, - ], - trx, - ); - return { - json: { - '@id': `${getUrl(req)}/@breadcrumbs`, - items: drop(items), - }, - }; - }, + handler, }, ]; diff --git a/src/routes/content/content.js b/src/routes/content/content.js index 5b202f9..c38833b 100644 --- a/src/routes/content/content.js +++ b/src/routes/content/content.js @@ -16,11 +16,13 @@ import { omit, pick, split, + startsWith, uniq, } from 'lodash'; import { v4 as uuid } from 'uuid'; import { + getUrl, getRootUrl, lockExpired, mapAsync, @@ -34,9 +36,94 @@ import { } from '../../helpers'; import { Document, Type } from '../../models'; +import { handler as actions } from '../actions/actions'; +import { handler as breadcrumbs } from '../breadcrumbs/breadcrumbs'; +import { handler as navigation } from '../navigation/navigation'; +import { handler as navroot } from '../navroot/navroot'; +import { handler as translations } from '../translations/translations'; +import { handler as types } from '../types/types'; +import { handler as workflow } from '../workflow/workflow'; + const { config } = require(`${process.cwd()}/config`); -const omitProperties = ['@type', 'id', 'changeNote']; +const omitProperties = ['@type', 'id', 'changeNote', 'language']; + +const getComponents = async (req, trx) => { + const components = {}; + + // Get nodes to expand + const expand = req.query?.expand?.split(',') || []; + + // Get base url + const baseUrl = getUrl(req); + + // Include catalog expander + if (includes(expand, 'catalog')) { + await req.document.fetchRelated('_catalog', trx); + + if (req.document._children) { + await mapAsync(req.document._children, async (child) => { + await child.fetchRelated('_catalog', trx); + await child.fetchRelationLists(trx); + }); + } + components.catalog = req.document._catalog.toJSON(req); + } else { + components.catalog = { '@id': `${baseUrl}/@catalog` }; + } + + // Include actions expander + if (includes(expand, 'actions')) { + components.actions = (await actions(req, trx)).json; + } else { + components.actions = { '@id': `${baseUrl}/@actions` }; + } + + // Include breadcrumbs expander + if (includes(expand, 'breadcrumbs')) { + components.breadcrumbs = (await breadcrumbs(req, trx)).json; + } else { + components.breadcrumbs = { '@id': `${baseUrl}/@breadcrumbs` }; + } + + // Include navigation expander + if (includes(expand, 'navigation')) { + components.navigation = (await navigation(req, trx)).json; + } else { + components.navigation = { '@id': `${baseUrl}/@navigation` }; + } + + // Include navroot expander + if (includes(expand, 'navroot')) { + components.navroot = (await navroot(req, trx)).json; + } else { + components.navroot = { '@id': `${baseUrl}/@navroot` }; + } + + // Include types expander + if (includes(expand, 'types')) { + components.types = (await types(req, trx)).json; + } else { + components.types = { '@id': `${baseUrl}/@types` }; + } + + // Include workflow expander + if (includes(expand, 'workflow')) { + components.workflow = (await workflow(req, trx)).json; + } else { + components.workflow = { '@id': `${baseUrl}/@workflow` }; + } + + // Include translations expander + if (includes(expand, 'translations')) { + components.translations = (await translations(req, trx)).json; + } else { + components.translations = { '@id': `${baseUrl}/@translations` }; + } + + // Return components + return components; +}; export default [ { @@ -185,7 +272,7 @@ export default [ await req.document.fetchVersion(parseInt(req.params.version, 10), trx); await req.document.fetchRelationLists(trx); return { - json: await req.document.toJSON(req), + json: await req.document.toJSON(req, await getComponents(req, trx)), }; }, }, @@ -289,18 +376,8 @@ export default [ handler: async (req, trx) => { await req.document.fetchRelated('[_children(order)._type, _type]', trx); await req.document.fetchRelationLists(trx); - if (req.query?.expand === 'catalog') { - await req.document.fetchRelated('_catalog', trx); - - if (req.document._children) { - await mapAsync(req.document._children, async (child) => { - await child.fetchRelated('_catalog', trx); - await child.fetchRelationLists(trx); - }); - } - } return { - json: await req.document.toJSON(req), + json: await req.document.toJSON(req, await getComponents(req)), }; }, }, @@ -336,7 +413,27 @@ export default [ // Get json data const properties = type._schema.properties; - // Handle file uploads + // Set uuid + const newUuid = req.body.uuid || uuid(); + + // Set translation + let translation_group = uuid; + if (req.body.translation_of) { + if (startsWith(req.body.translation_of, '/')) { + const translation = await Document.fetchOne( + { path: req.body.translation_of }, + {}, + trx, + ); + if (translation) { + translation_group = translation.uuid; + } + } else { + translation_group = req.body.translation_of; + } + } + + // Remove fields which are not in the schema let json = { ...omit(pick(req.body, keys(properties)), omitProperties), }; @@ -348,9 +445,11 @@ export default [ // Create new document let document = Document.fromJson({ - uuid: req.body.uuid || uuid(), + uuid: newUuid, type: req.body['@type'], created, + translation_group, + language: req.body.language, modified: created, version: 0, position_in_parent: @@ -423,7 +522,7 @@ export default [ // Send data back to client return { status: 201, - json: await document.toJSON(req), + json: await document.toJSON(req, await getComponents(req)), }; }, }, diff --git a/src/routes/history/history.js b/src/routes/history/history.js index 1e964ef..6e388ce 100644 --- a/src/routes/history/history.js +++ b/src/routes/history/history.js @@ -1,6 +1,6 @@ /** - * Content routes. - * @module routes/content/content + * History routes. + * @module routes/history/history */ import moment from 'moment'; diff --git a/src/routes/index.js b/src/routes/index.js index 96420c5..7814cac 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -14,12 +14,14 @@ import groups from './groups/groups'; import history from './history/history'; import lock from './lock/lock'; import navigation from './navigation/navigation'; +import navroot from './navroot/navroot'; import querystring from './querystring/querystring'; import roles from './roles/roles'; import search from './search/search'; import sharing from './sharing/sharing'; import site from './site/site'; import system from './system/system'; +import translations from './translations/translations'; import types from './types/types'; import users from './users/users'; import vocabularies from './vocabularies/vocabularies'; @@ -36,12 +38,14 @@ export default [ ...history, ...lock, ...navigation, + ...navroot, ...querystring, ...roles, ...search, ...sharing, ...site, ...system, + ...translations, ...types, ...users, ...vocabularies, diff --git a/src/routes/navigation/navigation.js b/src/routes/navigation/navigation.js index 1ab811c..66bb5e6 100644 --- a/src/routes/navigation/navigation.js +++ b/src/routes/navigation/navigation.js @@ -7,47 +7,48 @@ import { Catalog, Controlpanel, Document } from '../../models'; import { getUrl } from '../../helpers'; import { compact, includes, map, split } from 'lodash'; -export default [ - { - op: 'get', - view: '/@navigation', - permission: 'View', - handler: async (req, trx) => { - const root = await Document.fetchOne({ parent: null }, {}, trx); - const items = await Catalog.fetchAllRestricted( - { _parent: root.uuid }, - { order: '_getObjPositionInParent' }, - trx, - req, - ); +export const handler = async (req, trx) => { + const items = await Catalog.fetchAllRestricted( + { _parent: req.navroot.uuid }, + { order: '_getObjPositionInParent' }, + trx, + req, + ); - // Omit exclude from nav items - items.omitBy((item) => item.exclude_from_nav); + // Omit exclude from nav items + items.omitBy((item) => item.exclude_from_nav); - // Fetch settings - const controlpanel = await Controlpanel.fetchById('navigation', {}, trx); - const settings = controlpanel.data; + // Fetch settings + const controlpanel = await Controlpanel.fetchById('navigation', {}, trx); + const settings = controlpanel.data; - // Omit by type - items.omitBy((item) => !includes(settings.displayed_types, item.Type)); + // Omit by type + items.omitBy((item) => !includes(settings.displayed_types, item.Type)); - // Return navigation - return { - json: { - '@id': `${getUrl(req)}/@navigation`, - items: [ - ...map(compact(split(settings.additional_items, '\n')), (item) => { - const navitem = item.split('|'); - return { - title: navitem[0], - description: navitem[1], - '@id': navitem[2], - }; - }), - ...(await items.toJSON(req)), - ], - }, - }; + // Return navigation + return { + json: { + '@id': `${getUrl(req)}/@navigation`, + items: [ + ...map(compact(split(settings.additional_items, '\n')), (item) => { + const navitem = item.split('|'); + return { + title: navitem[0], + description: navitem[1], + '@id': navitem[2], + }; + }), + ...(await items.toJSON(req)), + ], }, + }; +}; + +export default [ + { + op: 'get', + view: '/@navigation', + permission: 'View', + handler, }, ]; diff --git a/src/routes/navroot/navroot.js b/src/routes/navroot/navroot.js new file mode 100644 index 0000000..c8a9da2 --- /dev/null +++ b/src/routes/navroot/navroot.js @@ -0,0 +1,22 @@ +/** + * Navigation root route. + * @module routes/navroot/navroot + */ + +export const handler = async (req, trx) => { + await req.navroot.fetchRelated('[_children(order)._type, _type]', trx); + await req.navroot.fetchRelationLists(trx); + + return { + json: await req.navroot.toJSON(req), + }; +}; + +export default [ + { + op: 'get', + view: '/@navroot', + permission: 'View', + handler, + }, +]; diff --git a/src/routes/navroot/navroot.test.js b/src/routes/navroot/navroot.test.js new file mode 100644 index 0000000..25b9989 --- /dev/null +++ b/src/routes/navroot/navroot.test.js @@ -0,0 +1,6 @@ +import app from '../../app'; +import { testRequest } from '../../helpers'; + +describe('Navroot', () => { + it('should get the navroot', () => testRequest(app, 'navroot/get')); +}); diff --git a/src/routes/translations/translations.js b/src/routes/translations/translations.js new file mode 100644 index 0000000..1e4a885 --- /dev/null +++ b/src/routes/translations/translations.js @@ -0,0 +1,128 @@ +/** + * Translations routes. + * @module routes/translations/translations + */ + +import { fromPairs, map } from 'lodash'; + +import { Document, Controlpanel } from '../../models'; +import { getPath, getUrl, getUrlByPath } from '../../helpers'; + +export const handler = async (req, trx) => { + const documents = req.document.translation_group + ? await Document.fetchAll( + { translation_group: req.document.translation_group }, + {}, + trx, + ) + : []; + const controlpanel = await Controlpanel.fetchById('language', {}, trx); + const settings = controlpanel.data; + + return { + json: { + '@id': `${getUrl(req)}/@translations`, + items: map(documents.models, (document) => ({ + '@id': getUrlByPath(req, document.path), + language: document.language, + })), + root: fromPairs( + map(settings.available_languages, (language) => [ + language, + getUrlByPath(req, `/${language}`), + ]), + ), + }, + }; +}; + +export default [ + { + op: 'get', + view: '/@translations', + permission: 'View', + handler, + }, + { + op: 'delete', + view: '/@translations', + permission: 'Modify', + handler: async (req, trx) => { + const document = await Document.fetchOne( + { + translation_group: req.document.translation_group, + language: req.body.language, + }, + {}, + trx, + ); + await document.update( + { + translation_group: null, + }, + trx, + ); + return { + json: {}, + }; + }, + }, + { + op: 'post', + view: '/@translations', + permission: 'Modify', + handler: async (req, trx) => { + // Strip prefix of url + const id = getPath(req.body.id); + + let target; + // Check if path + if (startsWith(id, '/')) { + target = await Document.fetchOne({ path: id }, {}, trx); + // Else it is a uuid + } else { + target = await Document.fetchOne({ uuid: id }, {}, trx); + } + + // Link documents + await document.update( + { + translation_group: req.document.translation_group, + }, + trx, + ); + + return { + json: {}, + }; + }, + }, + { + op: 'get', + view: '/@translation-locator', + permission: 'View', + handler: async (req, trx) => { + // Fetch parent + const parent = await Document.fetchOne({ + uuid: req.document.parent, + }); + + const document = await Document.fetchOne( + { + translation_group: parent.translation_group, + language: req.query.target_language, + }, + {}, + trx, + ); + + return { + json: { + '@id': document + ? getUrlByPath(req, document.path) + : getUrlByPath(req, `/${req.query.target_language}`), + }, + }; + }, + }, +]; diff --git a/src/routes/types/types.js b/src/routes/types/types.js index 9ab768e..f250bcb 100644 --- a/src/routes/types/types.js +++ b/src/routes/types/types.js @@ -8,17 +8,19 @@ import { omit } from 'lodash'; import { RequestException, translateSchema } from '../../helpers'; import { Type } from '../../models'; +export const handler = async (req, trx) => { + const types = await Type.fetchAll({}, {}, trx); + return { + json: types.toJSON(req), + }; +}; + export default [ { op: 'get', view: '/@types', permission: 'View', - handler: async (req, trx) => { - const types = await Type.fetchAll({}, {}, trx); - return { - json: types.toJSON(req), - }; - }, + handler, }, { op: 'get', diff --git a/src/routes/workflow/workflow.js b/src/routes/workflow/workflow.js index f6619e1..7b5c95a 100644 --- a/src/routes/workflow/workflow.js +++ b/src/routes/workflow/workflow.js @@ -7,6 +7,13 @@ import moment from 'moment'; import { hasPermission, RequestException } from '../../helpers'; +export const handler = async (req, trx) => { + await req.type.fetchRelated('_workflow', trx); + return { + json: req.type._workflow.toJSON(req), + }; +}; + export default [ { op: 'post', @@ -75,11 +82,6 @@ export default [ op: 'get', view: '/@workflow', permission: 'View', - handler: async (req, trx) => { - await req.type.fetchRelated('_workflow', trx); - return { - json: req.type._workflow.toJSON(req), - }; - }, + handler, }, ]; diff --git a/src/seeds/document/document.js b/src/seeds/document/document.js index fc478ba..efbc283 100644 --- a/src/seeds/document/document.js +++ b/src/seeds/document/document.js @@ -25,6 +25,8 @@ const documentFields = [ 'versions', 'owner', 'lock', + 'translation_group', + 'language', 'workflow_state', 'workflow_history', 'sharing', @@ -73,10 +75,12 @@ export const seedDocument = async (trx, profilePath) => { document = await handleFiles(document, type, profilePath); document = await handleImages(document, type, profilePath); + const newUuid = document.uuid || uuid(); + // Insert document let insert = await Document.create( { - uuid: document.uuid || uuid(), + uuid: newUuid, version: 'version' in document ? document.version : versionCount - 1, id, path, @@ -92,6 +96,8 @@ export const seedDocument = async (trx, profilePath) => { type: document.type || 'Page', created: document.created || moment.utc().format(), modified: document.modified || moment.utc().format(), + translation_group: document.translation_group || newUuid, + language: document.language, json: omit(document, documentFields), }, {}, diff --git a/src/vocabularies/available-languages/available-languages.js b/src/vocabularies/available-languages/available-languages.js new file mode 100644 index 0000000..63b7875 --- /dev/null +++ b/src/vocabularies/available-languages/available-languages.js @@ -0,0 +1,23 @@ +/** + * Available languages vocabulary. + * @module vocabularies/available-languages/available-languages + */ + +import { Controlpanel } from '../../models'; +import languages from '../../constants/languages'; +import { objectToVocabulary } from '../../helpers'; +import { pick } from 'lodash'; + +/** + * Returns the available languages vocabulary. + * @method availableLanguages + * @returns {Array} Array of terms. + */ +export async function availableLanguages(req, trx) { + // Fetch settings + const controlpanel = await Controlpanel.fetchById('language', {}, trx); + const settings = controlpanel.data; + + // Return languages + return objectToVocabulary(pick(languages, settings.available_languages)); +} diff --git a/src/vocabularies/index.js b/src/vocabularies/index.js index 1f717f0..7f4b898 100644 --- a/src/vocabularies/index.js +++ b/src/vocabularies/index.js @@ -4,6 +4,7 @@ */ import { actions } from './actions/actions'; +import { availableLanguages } from './available-languages/available-languages'; import { behaviors } from './behaviors/behaviors'; import { groups } from './groups/groups'; import { imageScales } from './image-scales/image-scales'; @@ -22,6 +23,7 @@ const { config } = require(`${process.cwd()}/config`); export const vocabularies = { actions, + availableLanguages, behaviors, groups, imageScales,