A simple react-redux-saga typescript front-end for Drupal 8 with jsonapi module enabled
git clone https://stevaidis.mywire.org:4080/ste/react-drupal-starter-ts.git
cd react-drupal-starter
npm install
npm start
Runs the app in the development mode. Open http://localhost:3000 to view it in the browser.
This project was bootstrapped with Create React App.
- Front page - List of article links with:
- Fields: Title, Image, Tags
- Pager
- Tag filter
- Article page
- Authenticate user
- Login form
- Logout button
- Post article form with fields:
- Title
- image (Drag and Drop )
- Body
- Tags (with auto-complete and new tag creation)
Functional components with a few hooks
store.api
: isLoading, browser url parameters, pager datastore.user
: Drupal response to POST loginstore.article
: Drupal response to GET a single articlestore.articles
: Drupal response to GET list of articlesstore.articlePost
: Form data for POST new article
-
userLoginWatcher
: listens for USER_LOGIN_REQUEST action and POST the payload -
userLogoutWatcher
: listens for USER_LOGOUT_REQUEST actionand and POST the payload -
articlesWatcher
: listens for GET_ARTICLES action, fetch articles, dispatch SET_ARTICLES -
articleWatcher
: listens for GET_ARTICLE action, fetch article, dispatch SET_ARTICLE -
articlePostWatcher
: listens for actions and POST the action.payload:- POST_ARTICLE
- POST_ARTICLE_FILE
- POST_TAG
After the user creates a new tag, he expects the new tag to be included in the selected tags, so after every POST_TAG the saga worker will:
- referesh the
store.articlePost.vocabulary
by dispatching the GET_VOCABULARY action - add the new tag to selected tags at
store.articlePost.selected
by dispatching the ADD_SELECTED action
Component | Path | Permisions |
---|---|---|
<Articles /> |
/ /?offset=2 /?terms=myterm /?terms=myterm&offset=2 |
public |
<Article /> |
/article/my article | public |
<UserLogin /> |
/user/login | public |
<ArticlePost /> |
/article/create | protected |
The component /header/Menu.js
uses the /header/LinkPrivate.js
component to hide the protected <Link />
from non-authenticated users
The App.js
uses the /utils/RouteProtected.js
to redirect the non-authenticated users from the protected routes to /user/login
- Install the redux browser extention for chrome or firefox
- Get the postman collection: react-drupal-starter.postman_collection.json
- Have fun
For the react-dropzone-uploader
to work with Drupal authentication cookies there is a usefull patch which Add withCredentials property to support CORS requests. For a quick test you can overwrite the working library files with the patched files from /fix
directory
cp -rv fix/react-dropzone-uploader/dist node_modules/react-dropzone-uploade
You can use the installation script drupal/anew.sh
to setup a fresh drupal 8 site ready to work with the react front-end. The script is tested on centos 8 with nginx/php-fpm, it uses composer
and drush
, and it needs to be run under /var/www
dir (/var/www/anew.sh
)
For my normal setup on a centos 8 virtualbox machine with nginx/php-fpm, the composer needed at least 4GB of ram and also a smal swap file to play nice without any problems
cp drupal/anew.sh /var/www
cd /var/www
./anew.sh react-drupal-backend
The script will:
- use the composer to install a fresh drush 8
- create settings.php and services.php
- create a database
- set filesystem permissions
- install contributed modules
- enable contributed modules
- create manager/1234 user
- create a directory with the new site at
/var/www/react-drupal-backend
The script will install for follwing contributed Modules
devel
: usefull fordevel_generate
sub module to generate some demo articlestoken
: used bypathauto
module for path aliaspathauto
: you can request article by path alias instead of idrestui
: enable Login, Register, Logout endpointsjsonapi_extras
: Include count in collection queriesjsonapi_include
: merge include and relationship data (nodes with images and tags)jsonapi_image_styles
: exposes image style urlsfieldable_path
: get article by url aliaspager_serializer
: provide the pager links
vi core/modules/jsonapi/src/Query/OffsetPage.php
- const SIZE_MAX = 50;
+ const SIZE_MAX = 999;
location @rewrite {
- rewrite ^/(.*)$ /index.php?q=$1;
+ rewrite ^/(.*)$ /index.php?query_string last;
}
The project tested with versions:
- Drupal 8.9.8
- Drush 10
- Composer 1.10.13
- PHP 7.4.12
- NGINX 1.16.1
http://localhost/admin/config/search/path/patterns
New Pattern: Article
- Pattern Type: Content
- Path pattern:
article/[node:title]
- Content type: Article
- Label: Article
- Enabled: [x]
New Pattern: Tags
- Pattern Type: taxonomy term
- Path pattern:
term/[term:name]
- Vocabulary: [tags]
- Label: Term
- Enabled: [x]
http://localhost/admin/structure/types/manage/article
- Preview before submittings:
[Disable]
- Fields
- Body
body
- Comments
comment
- Image
field_iamge
(set as required) - Path
field_path
(set as required) - Tags
field_tags
(set as required for the work of thedevel_generate
)
- Body
http://localhost/admin/config/development/generate/content
You can download the postman collection react-drupal-starter.postman_collection.json or use the curl from the console
- Non authenticated users recieve a different one every time they GET response
- Authenticated users get the same that already have gotten from the POST Login reqponse
curl --location --request GET 'http://localhost/session/token'
The part ?include=field_image,field_tags
needs the jsponapi_include
drupal module
curl --location --request GET 'http://localhost/jsonapi/node/article?include=field_image,field_tags
The filter part is &filter...field_tags.name&filter...myterm
curl --location --request GET 'http://localhost/jsonapi/node/article \
?include=field_image,field_tags \
&filter[titleFilter][condition][path]=field_tags.name \
&filter[titleFilter][condition][value]=myterm'
The filter by field_path
is done by the fieldable_path
module
curl --location --request GET 'http://localhost/jsonapi/node/article \
?include=field_image,field_tags,uid \
&filter[field_path][value]=/article/mytitle'
Using the standar cookie authentication
curl --location --request POST 'http://localhost/user/login?_format=json' \
--header 'Content-type: application/json' \
--data-raw '{"name":"admin", "pass":"1234"}'
we get the response
{
"current_user": {
"uid": "1",
"roles": [
"authenticated",
"administrator"
],
"name": "admin"
},
"csrf_token": "YKXBwr_qDlYq2GH_L8RWdauCIDV5GL_eXGxly0sR6Kg",
"logout_token": "w5D4blEDudgg0F3a51xLKXvE0NztsEBigVjNBMqK1BM"
}
this object is stored in redux store.user
curl --location --request POST 'http://localhost/jsonapi/node/article/field_image' \
--header 'Accept: application/vnd.api+json' \
--header 'Content-Type: application/octet-stream' \
--header 'X-CSRF-Token: QtqRwdIdCxl2rPZezdUAelvTzghLQjF_pm3xb7j8_LI' \
--header 'Content-Disposition: file; filename="156696.jpg"' \
--header 'X-Requested-With: XMLHttpRequest' \
--header 'Accept-Encoding: gzip, deflate' \
--header 'Cookie: SESS2f4ff3168b8423453fc408c2c2581ce0=FFZMHxxhCcxP4AoU99WTuS0lfZ3k8uBMTRiTd_7ht2Y' \
--data-binary '@/home/ste/Pictures/wallpapers/156696.jpg'
The user can create new tags in the same input with <CreatableSelect ... />
from react-select
curl --location --request POST 'http://localhost/jsonapi/taxonomy_term/tags' \
--header 'Content-Type: application/vnd.api+json' \
--header 'Accept: application/vnd.api+json' \
--header 'Authorization: Basic YWRtaW46MTIzNA==' \
--header 'X-CSRF-Token: ab9GUlrf7UfccnaNKSmicMF60N0TcVzoWupcA3UBv7c' \
--data-raw '{
"data": {
"type": "taxonomy_term--tags",
"attributes": {
"name": "latest term"
}
}
}'
curl --location --request POST 'http://localhost/jsonapi/node/article' \
--header 'Content-Type: application/vnd.api+json' \
--header 'X-CSRF-Token: ab9GUlrf7UfccnaNKSmicMF60N0TcVzoWupcA3UBv7c' \
--data-raw '{
"data": {
"type": "node--article",
"attributes": {
"title": "from postman title with image",
"body": {
"value": "from postman body",
"format": "plain_text"
}
},
"relationships": {
"field_image": {
"data": {
"type": "file--file",
"id": "a59d672b-07d8-42d4-b716-bb3fb8b565e5",
"meta": {
"alt": "Json Uploaded Testing1",
"title": "Json Uploaded Testing1",
"width": null,
"height": null
}
}
},
"field_tags": {
"data": [{
"type": "taxonomy_term--tags",
"id": "fc5fd77d-1672-49fa-97a8-f84af218c90b"
}]
}
}
}
}'