1
0
mirror of https://github.com/dwaxweiler/connector-mobilizon synced 2025-06-05 21:59:25 +02:00

136 Commits
v0.7.0 ... main

Author SHA1 Message Date
b8c918db67 add missing backticks to changelog, add picture loading to features list 2025-05-24 17:15:52 +02:00
a3cd59e38f remove old CodeQL config file 2025-05-23 22:28:31 +02:00
392bf0d0a3 create codeql.yml 2025-05-23 22:25:55 +02:00
ffc31458f6 remove qodana 2025-05-23 22:18:19 +02:00
c6b5a2701a add VS Code settings 2025-05-23 22:16:26 +02:00
40f259d817 improve image downloading 2025-05-23 22:07:33 +02:00
10f4f3eceb fix showing group not found error in block 2025-05-23 21:21:04 +02:00
3e2aac7657 escape translated strings to prevent HTML injections 2025-05-23 18:08:28 +02:00
516f08a6ac escape string and mark as translatable 2025-05-23 17:47:10 +02:00
f7dad38513 add donation link to plugin on plugins page 2025-05-23 17:45:24 +02:00
95d18630ca add settings link to plugin on plugins page 2025-05-23 17:25:52 +02:00
968de8889f document basic installation 2025-05-23 17:02:54 +02:00
3430b98f94 Add show more events button below the events list #22 (#24) 2025-05-23 16:48:19 +02:00
778c4e3b92 prepare next release 2025-05-22 21:38:57 +02:00
44f35aa007 release version 1.4.0 2025-05-22 21:37:51 +02:00
f3b7dcf735 confirm compatibility with WordPress 6.8 2025-05-22 21:27:35 +02:00
e3f2e5b133 replace outdated browser-env with jsdom 2025-05-22 21:26:37 +02:00
09c0698509 update dependencies 2025-05-22 21:06:03 +02:00
f69769d291 prepare next release 2024-11-10 09:53:15 +01:00
e21c86f2e4 release version 1.3.0 2024-11-10 09:52:09 +01:00
5bcc03f9d1 handle location being null 2024-11-10 09:46:26 +01:00
460ea7894d update dependencies 2024-11-10 09:38:57 +01:00
8caceeaf76 add missing changelog entries 2024-11-10 09:12:09 +01:00
0c40efd565 load block script only in footer 2024-11-09 22:32:53 +01:00
e91ca97e88 add version to wp_register_script() 2024-11-09 11:20:34 +01:00
df2feceaaa use escape-only function for event-related data 2024-11-09 10:01:50 +01:00
bcd1f5247d confirm compatibility with WordPress 6.7 2024-11-09 09:54:47 +01:00
e2d34032cd add comment for translators 2024-11-08 23:17:19 +01:00
9708203f3c prepare next release 2024-08-04 19:01:13 +02:00
370e0d9e46 release version 1.2.0 2024-08-04 19:00:14 +02:00
9412b9cb90 add missing changelog entry 2024-08-04 18:54:48 +02:00
72045a31b0 update dependencies 2024-08-04 18:54:13 +02:00
0edad986d3 display event picture (#23) 2024-08-04 18:31:29 +02:00
a543a25a8a prepare next release 2024-07-18 19:20:22 +02:00
f0e955aa47 release version 1.1.0 2024-07-18 19:19:17 +02:00
80abd9a461 npm audit fix 2024-07-18 19:04:55 +02:00
fb1db8e836 update dependencies 2024-07-18 19:03:41 +02:00
db080657db confirm compatibility with WordPress 6.6 2024-07-18 18:51:47 +02:00
979ecbc91f upgrade gulp to version 5 2024-05-16 08:57:35 +01:00
08e80615c6 add note about API to description 2024-05-15 08:34:06 +01:00
526d57d1b2 add missing changelog entry 2024-05-15 08:32:32 +01:00
718d66506b update some dev dependencies 2024-05-15 08:31:06 +01:00
cccd1a78b5 add some spacing between event items 2024-05-14 10:30:42 +02:00
fa99821ffc fix undefined variable for both error views 2024-05-14 10:28:39 +02:00
7bc35a3923 mention use of GraphQL API 2024-05-13 14:23:37 +02:00
82800a9db3 clarify two steps in release procedure 2024-04-06 08:27:24 +02:00
46170377af prepare next release 2024-04-06 08:25:16 +02:00
5c1a186456 release version 1.0.0 2024-04-06 08:22:26 +02:00
7a0075e5ed clarify features 2024-04-06 07:50:47 +02:00
392d552dab npm audit fix 2024-04-06 07:42:42 +02:00
797a08cfb6 update dev dependencies 2024-04-06 07:41:15 +02:00
c5279a37f3 remove unused code and dependencies 2024-04-05 23:44:53 +02:00
99e58d0382 update dependencies 2024-04-05 23:39:30 +02:00
dc1949ba4c move requests to backend (#18) 2024-04-05 23:26:11 +02:00
3269ccca1a add missing readme changelog entry 2024-04-02 22:13:20 +02:00
2d91350cc9 prepare next release 2024-04-02 22:09:58 +02:00
e7fd47a346 release version 0.11.5 2024-04-02 22:09:20 +02:00
8d4e81878e confirm compatibility with WordPress 6.5 2024-04-02 22:02:55 +02:00
418dc829d0 document after-release step 2024-04-02 22:01:53 +02:00
a75d3a3915 prepare next release 2023-11-11 20:43:50 +01:00
d2fb67b5bc release version 0.11.4 2023-11-11 20:39:06 +01:00
a9f5205d78 remove Qodana Github action 2023-11-11 20:31:50 +01:00
25c76e4998 increase Node version number in Github action 2023-11-11 20:30:22 +01:00
6157cf3988 update dependencies 2023-11-11 20:28:16 +01:00
8644a7103c confirm compatibility with WordPress 6.4 2023-11-11 20:24:59 +01:00
4fd516bcf8 release version 0.11.3 2023-07-25 22:25:55 +02:00
238fdcf261 release version 0.11.2 2023-07-25 22:14:24 +02:00
eaf1ffa0c8 confirm compatibility with WordPress 6.3 2023-07-25 20:17:28 +02:00
48477a158e add missing changelog entry 2023-07-25 20:16:49 +02:00
9b4778b494 npm audit fix 2023-07-25 20:04:04 +02:00
60363d8a5c update dependencies 2023-07-25 20:03:31 +02:00
51ae9b9616 update dependencies 2023-05-07 18:00:02 +02:00
637142d00d release version 0.11.1 2023-05-07 17:46:27 +02:00
163d0d27a6 revert minimum PHP version to 7.4 to allow some more time for upgrading PHP 2023-05-07 17:41:36 +02:00
d0ada74642 release version 0.11.0 2023-03-29 23:00:36 +02:00
b242c33733 remove bootstrap from qodana.yaml 2023-03-29 22:47:59 +02:00
cac233ec45 set minimum PHP version to 8.0 2023-03-29 22:32:29 +02:00
9e394ac837 confirm compatibility with WP 6.2 2023-03-29 22:30:45 +02:00
b2811ad169 update dependencies 2023-03-29 22:26:44 +02:00
40f351efbf prefer online 2023-02-16 08:10:10 +01:00
ad0a9f77f8 ignore scripts too 2023-02-16 08:04:32 +01:00
76a2217582 use npm ci in qodana instead 2023-02-16 08:00:01 +01:00
742b16808c add qodana.yaml 2023-02-16 07:57:10 +01:00
41419e6550 Merge pull request #14 from wordpress-connector-for-mobilizon/update-deps
Update dependencies
2023-02-12 18:13:49 +01:00
ab7e7274d0 fix versions 2023-02-12 18:11:24 +01:00
b7742d3803 npm audit fix 2023-02-12 18:05:30 +01:00
e5dc313f6c update luxon 2023-02-12 18:02:33 +01:00
3b4c53da81 update dev deps 2023-02-12 17:56:48 +01:00
87434fc1c1 add qodana 2023-02-12 17:20:20 +01:00
81c4759a9e release version 0.10.1 2022-10-26 23:41:32 +02:00
ad5f1753cb change node version of test pipeline 2022-10-26 23:34:03 +02:00
6cde75b65c npm audit fix 2022-10-26 23:32:23 +02:00
5a3550568c update dependencies 2022-10-26 23:31:05 +02:00
23390db0a9 update dev dependencies 2022-10-26 23:18:39 +02:00
4d5acc3714 confirm compatibility with WordPress 6.1 2022-10-26 22:57:39 +02:00
72df79b092 add note to release procedure about adding new files in SVN 2022-06-19 09:52:14 +02:00
d1be3b169d release version 0.10.0 2022-06-19 09:48:06 +02:00
5af00fc85e reuse constant 2022-06-19 09:30:36 +02:00
f76653f05c remove unused Gutenberg block editor dependency 2022-06-19 09:24:42 +02:00
995f6681e6 split up release procedure step to be clearer 2022-06-19 09:19:58 +02:00
a6f0bd9584 update dev dependencies 2022-06-19 09:18:45 +02:00
59970db3b8 add comma 2022-06-10 12:37:17 +02:00
d2e3c55e1a add missing changelog entry 2022-06-10 12:33:45 +02:00
99d97c5a5e update dev dependencies 2022-06-09 20:24:32 +02:00
64a61426e0 fix typo 2022-06-09 20:24:04 +02:00
3f09fbb563 create installation section in readme.txt 2022-06-09 20:16:07 +02:00
3360a9e7a7 add Gutenberg events list block 2022-06-09 20:09:08 +02:00
c042851184 Merge branch 'main' into block 2022-06-03 23:32:46 +02:00
09a47029f3 add missing changelog entries to readme.txt 2022-05-19 18:30:10 +02:00
977d7e57c6 release version 0.9.1 2022-05-19 18:27:06 +02:00
e84a4cfc73 real fix 2022-05-19 18:25:38 +02:00
eb10341337 fix WordPress compatibility version number 2022-05-19 18:22:04 +02:00
f769d5d8c1 release version 0.9.0 2022-05-19 18:16:17 +02:00
f54126babc confirm compatibility with WordPress 6.0 2022-05-19 18:09:37 +02:00
7dab9d71c6 update dependencies 2022-05-19 18:02:49 +02:00
0479deca56 improve explanation of group name filter 2022-05-19 08:39:20 +02:00
c51610b054 fix displaying error message for the case the group is not found 2022-05-19 08:08:21 +02:00
367a1c97b2 update changelog 2022-03-20 10:27:30 +01:00
01c5f19e39 switch to exact version number dependencies 2022-03-20 10:25:05 +01:00
42c2c945ba npm audit fix 2022-03-20 10:23:31 +01:00
9311fb42bb update dependencies 2022-03-20 10:21:38 +01:00
ba3b069527 update dev dependencies 2022-03-20 10:15:34 +01:00
87a55e5302 update dev dependencies 2022-02-18 16:30:46 +01:00
dd42ebf712 release version 0.8.0 2022-01-09 13:10:38 +01:00
0c6bf3d3a4 npm audit fix 2022-01-09 12:59:52 +01:00
77b58ccf07 add changelog reference to README 2022-01-09 12:58:27 +01:00
184d5627a3 trim events' location, change variable name 2022-01-09 12:54:58 +01:00
ec12889815 add babel 2022-01-09 12:35:26 +01:00
4270145a55 update changelog 2022-01-09 12:16:38 +01:00
3ed5129cd2 update dependency luxon 2022-01-09 12:14:49 +01:00
6dca75b2ce update dev dependencies c8, lint-staged 2022-01-09 12:13:15 +01:00
647629b5a9 update dev dependencies ava, eslint, eslint-plugin-ava and migrate to ES modules 2022-01-09 12:11:24 +01:00
fa91324f18 confirm compatibility with WordPress 5.9 2022-01-05 17:45:40 +01:00
876c3e3840 Merge branch 'main' into block 2021-12-14 17:54:47 +01:00
53d08e7174 Merge branch 'main' into block 2021-12-14 15:33:07 +01:00
0b7021942e add basic block 2021-11-01 20:55:37 +01:00
60 changed files with 19322 additions and 12840 deletions

View File

@ -3,26 +3,50 @@
"browser": true,
"es2020": true
},
"globals": {
"MOBILIZON_CONNECTOR": "readonly"
},
"parser": "@babel/eslint-parser",
"extends": [
"eslint:recommended",
"plugin:ava/recommended"
"plugin:ava/recommended",
"plugin:@wordpress/eslint-plugin/recommended"
],
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
"requireConfigFile": false,
"sourceType": "module",
"babelOptions": {
"presets": ["@babel/preset-react"]
}
},
"plugins": [
"ava"
"ava",
"jsx"
],
"rules": {
"import/no-unresolved": [
"off"
],
"indent": [
"error",
2
],
"no-console": [
"error", {
"allow": ["error"]
}
],
"prettier/prettier": [
"off"
],
"quotes": [
"error",
"single"
],
"react/jsx-key": [
"off"
],
"semi": [
"error",
"never"

View File

@ -1,71 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'
on:
push:
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
schedule:
- cron: '39 19 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['javascript']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
#- name: Autobuild
# uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
- run: |
npm install
npm run build-prod
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

96
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,96 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '20 7 * * 0'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
npm install
npm run build-prod
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
node-version: [14.x]
node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@ -24,5 +24,7 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci --omit=dev --ignore-scripts
- name: Upgrade NPM
run: npm install -g npm
- run: npm ci --ignore-scripts
- run: npm run build-prod

3
.gitignore vendored
View File

@ -3,3 +3,6 @@ build/
coverage/
node_modules/
vendor/
.phpunit.cache
.phpunit.result.cache

5
.gulp.json Normal file
View File

@ -0,0 +1,5 @@
{
"flags": {
"gulpfile": "gulpfile.cjs"
}
}

View File

@ -1,5 +1,2 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm test
npx lint-staged
lint-staged

73
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,73 @@
{
"intelephense.stubs": [
"apache",
"bcmath",
"bz2",
"calendar",
"com_dotnet",
"Core",
"ctype",
"curl",
"date",
"dba",
"dom",
"enchant",
"exif",
"FFI",
"fileinfo",
"filter",
"fpm",
"ftp",
"gd",
"gettext",
"gmp",
"hash",
"iconv",
"imap",
"intl",
"json",
"ldap",
"libxml",
"mbstring",
"meta",
"mysqli",
"oci8",
"odbc",
"openssl",
"pcntl",
"pcre",
"PDO",
"pgsql",
"Phar",
"posix",
"pspell",
"random",
"readline",
"Reflection",
"session",
"shmop",
"SimpleXML",
"snmp",
"soap",
"sockets",
"sodium",
"SPL",
"sqlite3",
"standard",
"superglobals",
"sysvmsg",
"sysvsem",
"sysvshm",
"tidy",
"tokenizer",
"xml",
"xmlreader",
"xmlrpc",
"xmlwriter",
"xsl",
"Zend OPcache",
"zip",
"zlib",
"wordpress"
]
}

View File

@ -4,6 +4,10 @@ Connector for Mobilizon allows you to display the upcoming events of [Mobilizon]
More details can be found in the [WordPress Plugin Directory](https://wordpress.org/plugins/connector-mobilizon/).
The current changelog can be found under [source/changelog.txt](source/changelog.txt).
This plugin uses [Mobilizon's GraphQL API](https://docs.joinmobilizon.org/contribute/graphql_api/).
## Development
### Setup
@ -19,18 +23,24 @@ More details can be found in the [WordPress Plugin Directory](https://wordpress.
### Release procedure
1. Make sure `changelog.txt` is up-to-date. Use a new version number and copy over the new section into `readme.txt`.
2. Update `package.json` with the same version number.
3. Update the `package-lock.json`: `npm i --package-lock-only`
4. Build: `npm run build-prod`
5. Make sure screenshots are up-to-date.
6. Copy the built plugin into `/trunk` of SVN.
7. Create a new tag of the new version: `svn cp trunk tags/<version>`
8. Check the version number occurrences in both folders.
9. Commit everything together to the release SVN: `svn ci -m "release version <version>"`
10. Commit the new version in git with the same message.
11. Tag the new version: `git tag v<version>`
12. Push the new tag to the repository: `git push --tags`
1. Make sure `changelog.txt` is up-to-date.
2. Create a new section with a new version number.
3. Copy over the new section into `readme.txt`.
4. Update `package.json` with the same version number.
5. Update the `package-lock.json`: `npm i --package-lock-only`
6. Build: `npm run build-prod`
7. Make sure screenshots are up-to-date.
8. Copy the built plugin into `/trunk` of SVN.
9. Create a new tag of the new version: `svn cp trunk tags/<version>`
10. Check the version number occurrences in both folders.
11. Make sure to handle exclamation and question marks in `svn status`.
12. Commit everything together to the release SVN: `svn ci -m "release version <version>"`
13. Commit the new version in git with the same message.
14. Tag the new version: `git tag v<version>`
15. Push the new tag to the repository: `git push --tags`
16. Append `-next` to the version number in `package.json`.
17. Update the `package-lock.json`: `npm i --package-lock-only`
18. Commit: `git commit -am "prepare next release"`
### Other commands
@ -41,3 +51,4 @@ More details can be found in the [WordPress Plugin Directory](https://wordpress.
- Update PHP dependencies: `composer update`
- Check for direct PHP dependency updates: `composer outdated --direct`
- Format code with prettier: `npm run format`
- Generate `vendor/autoload.php` file after creating new class: `composer dump-autoload`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 76 KiB

BIN
assets/screenshot-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

3
babel.config.json Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

View File

@ -1 +1,10 @@
{}
{
"autoload": {
"psr-4": {
"MobilizonConnector\\": "source/includes/"
}
},
"require-dev": {
"phpunit/phpunit": "^9.6"
}
}

1738
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,12 +10,12 @@ const FOLDER_BUILD = './build'
function injectMetadata() {
return src(
[
FOLDER_BUILD + '/front/events-loader.js',
FOLDER_BUILD + '/front/block-events-loader.js',
FOLDER_BUILD + '/' + PACKAGE.name + '.php',
FOLDER_BUILD + '/includes/constants.php',
FOLDER_BUILD + '/includes/Constants.php',
FOLDER_BUILD + '/readme.txt',
],
{ base: './' }
{ base: './' },
)
.pipe(replace('<wordpress-author-name>', PACKAGE.author.name))
.pipe(replace('<wordpress-author-url>', PACKAGE.author.url))
@ -25,22 +25,22 @@ function injectMetadata() {
.pipe(
replace(
'<wordpress-minimum-version>',
PACKAGE.additionalDetails.wordpressMinimumVersion
)
PACKAGE.additionalDetails.wordpressMinimumVersion,
),
)
.pipe(replace('<wordpress-name>', PACKAGE.name))
.pipe(replace('<wordpress-nice-name>', PACKAGE.additionalDetails.niceName))
.pipe(
replace(
'<wordpress-php-minimum-version>',
PACKAGE.additionalDetails.phpMinimumVersion
)
PACKAGE.additionalDetails.phpMinimumVersion,
),
)
.pipe(
replace(
'<wordpress-tested-up-to-version>',
PACKAGE.additionalDetails.wordpressTestedUpToVersion
)
PACKAGE.additionalDetails.wordpressTestedUpToVersion,
),
)
.pipe(replace('<wordpress-version>', PACKAGE.version))
.pipe(dest('.'))

28378
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,18 @@
{
"name": "connector-mobilizon",
"version": "0.7.0",
"version": "1.4.0-next",
"description": "Display Mobilizon events in WordPress.",
"private": true,
"type": "module",
"scripts": {
"build-dev": "webpack --mode=development && gulp inject",
"build-prod": "ava && webpack --mode=production && gulp inject",
"build-prod": "rimraf -- build && ava && webpack --mode=production && gulp inject",
"clean": "rimraf -- build",
"coverage": "c8 --all --reporter=html --reporter=text --include=source/**/*.js ava",
"eslint": "npx eslint source/**/*.js",
"format": "npx prettier --write .",
"prepare": "husky install",
"test": "ava"
"prepare": "husky",
"test": "ava && ./vendor/bin/phpunit"
},
"author": {
"name": "Daniel Waxweiler",
@ -23,41 +24,44 @@
},
"license": "Apache-2.0",
"dependencies": {
"graphql": "^16.2.0",
"graphql-request": "^3.7.0",
"luxon": "^2.2.0",
"object-hash": "^2.2.0"
"graphql": "16.11.0",
"luxon": "3.6.1"
},
"devDependencies": {
"ava": "^3.15.0",
"c8": "^7.10.0",
"copy-webpack-plugin": "^10.2.0",
"eslint": "^8.4.1",
"eslint-plugin-ava": "^13.1.0",
"esm": "^3.2.25",
"gulp": "^4.0.2",
"gulp-replace": "^1.1.3",
"husky": "^7.0.4",
"jsdom": "^19.0.0",
"lint-staged": "^12.1.3",
"prettier": "2.5.1",
"rimraf": "^3.0.2",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1"
"@babel/core": "7.27.1",
"@babel/eslint-parser": "7.27.1",
"@babel/preset-env": "7.27.2",
"@babel/preset-react": "7.27.1",
"@wordpress/eslint-plugin": "22.10.0",
"ava": "6.3.0",
"babel-loader": "10.0.0",
"c8": "10.1.3",
"copy-webpack-plugin": "13.0.0",
"eslint": "8.57.1",
"eslint-plugin-ava": "14.0.0",
"eslint-plugin-jsx": "0.1.0",
"eslint-plugin-react": "7.37.5",
"esm": "3.2.25",
"gulp": "5.0.0",
"gulp-replace": "1.1.4",
"husky": "9.1.7",
"jsdom": "26.1.0",
"lint-staged": "15.5.2",
"prettier": "3.5.3",
"rimraf": "5.0.10",
"webpack": "5.99.9",
"webpack-cli": "6.0.1"
},
"ava": {
"files": [
"./source/**/*test.js"
],
"require": [
"esm"
]
},
"additionalDetails": {
"niceName": "Connector for Mobilizon",
"phpMinimumVersion": 7.4,
"wordpressMinimumVersion": 5.6,
"wordpressTestedUpToVersion": 5.8
"wordpressTestedUpToVersion": "6.8"
},
"lint-staged": {
"source/**/*.js": "eslint",

27
phpunit.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
forceCoversAnnotation="false"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">source/includes</directory>
</include>
</coverage>
</phpunit>

View File

@ -1,10 +1,124 @@
### [Unreleased]
#### Added
- Display "Show more events" button below the events list
- Document basic installation
- Add settings link to plugin on plugins page
- Add donation link to plugin on plugins page
#### Changed
- Use `wp_remote_get()` instead of cURL functions for downloading the images
#### Deprecated
#### Removed
#### Fixed
- Show group not found error message in block
#### Security
- Escape translated strings to prevent HTML injections
### [1.4.0]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.8
### [1.3.0]
#### Added
- Comment for translators what placeholder will contain
#### Changed
- Confirm compatibility with WordPress 6.7
- Load block script only in footer to reduce waiting time
- Update dependencies
#### Fixed
- Mark event-related data as non-translatable within plugin
- Add version number to script registration to break browser caching
- Handle location being null
### [1.2.0]
#### Added
- Display event picture if available
#### Changed
- Update dependencies
### [1.1.0]
#### Added
- Add some spacing between event items
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.6
#### Fixed
- Fix undefined variable $classNamePrefix for both error views
### [1.0.0]
#### Added
- Display name of group when it cannot be found
#### Changed
- Let backend do requests to API of Mobilizon instance for increased privacy
- Update dependencies
#### Fixed
- Fix displaying more than one block in the editor
### [0.11.5]
#### Changed
- Confirm compatibility with WordPress 6.5
### [0.11.4]
#### Changed
- Confirm compatibility with WordPress 6.4
- Update dependencies
### [0.11.3]
#### Fixed
- Clean up distributed files
### [0.11.2]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.3
### [0.11.1]
#### Fixed
- Revert minimum PHP version to 7.4 to allow some more time for upgrading PHP
### [0.11.0]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.2
#### Security
- Set minimum PHP version to oldest stable 8.0
### [0.10.1]
#### Changed
- Confirm compatibility with WordPress 6.1
- Update dependencies
### [0.10.0]
#### Added
- Add Gutenberg events list block
- Show loading indicator during request
#### Changed
- Set list style type to none and left padding to zero for all occurences
- Move shortcut usage description into installation section in `readme.txt`
- Update dependencies
### [0.9.1] - 2020-05-19
#### Fixed
- Fix WordPress compatibility version number
### [0.9.0] - 2020-05-19
#### Added
- Improve explanation of group name filter
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.0
#### Fixed
- Fix displaying error message for the case the group is not found
### [0.8.0] - 2022-01-09
#### Added
- Add support for older browsers using babel
#### Changed
- Confirm compatibility with WordPress 5.9
- Update dependencies
#### Fixed
- Use ES modules correctly
- Trim events' location
### [0.7.0] - 2021-12-23
#### Added

View File

@ -10,10 +10,18 @@
* License: <wordpress-license>
*/
require_once __DIR__ . '/includes/constants.php';
require_once __DIR__ . '/includes/settings.php';
require_once __DIR__ . '/includes/events-list-shortcut.php';
require_once __DIR__ . '/includes/events-list-widget.php';
require_once __DIR__ . '/includes/exceptions/GeneralException.php';
require_once __DIR__ . '/includes/exceptions/GroupNotFoundException.php';
require_once __DIR__ . '/includes/Constants.php';
require_once __DIR__ . '/includes/Api.php';
require_once __DIR__ . '/includes/EventsCache.php';
require_once __DIR__ . '/includes/Settings.php';
require_once __DIR__ . '/includes/DateTimeWrapper.php';
require_once __DIR__ . '/includes/Formatter.php';
require_once __DIR__ . '/includes/GraphQlClient.php';
require_once __DIR__ . '/includes/EventsListBlock.php';
require_once __DIR__ . '/includes/EventsListShortcut.php';
require_once __DIR__ . '/includes/EventsListWidget.php';
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
@ -23,11 +31,14 @@ if (!defined('ABSPATH')) {
final class Mobilizon_Connector {
private function __construct() {
add_action('init', [$this, 'register_settings']);
add_action('init', [$this, 'register_api']);
add_action('init', [$this, 'register_blocks']);
add_action('init', [$this, 'register_settings'], 1); // required for register_blocks
add_action('init', [$this, 'register_shortcut']);
add_action('widgets_init', [$this, 'register_widget']);
add_action('wp_enqueue_scripts', [$this, 'register_scripts']);
register_activation_hook(__FILE__, [$this, 'enable_activation']);
add_filter('plugin_action_links_connector-mobilizon/connector-mobilizon.php', [$this, 'add_donation_link_to_plugins_page']);
add_filter('plugin_action_links_connector-mobilizon/connector-mobilizon.php', [$this, 'add_settings_link_to_plugins_page']);
}
public static function init() {
@ -39,16 +50,51 @@ final class Mobilizon_Connector {
return $instance;
}
public function add_donation_link_to_plugins_page(array $links) {
$url = esc_url('<wordpress-donation-link>');
$settings_link = "<a href='$url'>" . esc_html__('Donate', 'connector-mobilizon') . '</a>';
array_unshift($links, $settings_link);
return $links;
}
public function add_settings_link_to_plugins_page(array $links) {
$url = esc_url(
add_query_arg(
'page',
'connector-mobilizon-settings',
get_admin_url() . 'options-general.php'
)
);
$settings_link = "<a href='$url'>" . esc_html__('Settings', 'connector-mobilizon') . '</a>';
array_unshift($links, $settings_link);
return $links;
}
public function enable_activation() {
MobilizonConnector\Settings::setDefaultOptions();
}
public function register_settings() {
MobilizonConnector\Settings::init();
private function load_settings_globally_before_script($scriptName) {
$settings = array(
'isShortOffsetNameShown' => MobilizonConnector\Settings::isShortOffsetNameShown(),
'locale' => str_replace('_', '-', get_locale()),
'timeZone' => wp_timezone_string(),
'url' => MobilizonConnector\Settings::getUrl()
);
wp_add_inline_script($scriptName, 'var MOBILIZON_CONNECTOR = ' . json_encode($settings), 'before');
}
public function register_scripts() {
wp_enqueue_script(MobilizonConnector\NAME . '-js', plugins_url('front/events-loader.js', __FILE__ ));
public function register_api() {
MobilizonConnector\Api::init();
}
public function register_blocks() {
$scriptName = MobilizonConnector\EventsListBlock::initAndReturnScriptName();
$this->load_settings_globally_before_script($scriptName);
}
public function register_settings() {
MobilizonConnector\Settings::init();
}
public function register_shortcut() {

View File

@ -0,0 +1 @@
import './blocks/events-list/index.js'

View File

@ -0,0 +1,147 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable @wordpress/i18n-ellipsis */
import {
clearEventsList,
displayErrorMessage,
displayEvents,
hideErrorMessages,
showLoadingIndicator,
} from '../../events-displayer.js'
import Formatter from '../../formatter.js'
const { InspectorControls, useBlockProps } = wp.blockEditor
const { Panel, PanelBody } = wp.components
const { useEffect } = wp.element
const { __ } = wp.i18n
const NAME = '<wordpress-name>'
export default ({ attributes, setAttributes }) => {
let timer
const blockProps = useBlockProps({
className: NAME + '_events-list',
})
function reloadEventList(eventsCount, groupName) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(async () => {
const container = document.getElementById(blockProps.id)
if (container) {
hideErrorMessages(container)
clearEventsList(container)
showLoadingIndicator(container)
let url = `/wp-json/connector-mobilizon/v1/events?eventsCount=${eventsCount}`
let showMoreUrl = window.MOBILIZON_CONNECTOR.url
if (groupName) {
showMoreUrl += '/@' + groupName + '/events'
url += `&groupName=${groupName}`
}
container.querySelector('a').href = showMoreUrl
await fetch(url)
.then(async (response) => {
if (!response.ok) {
// Reject if no 2xx response.
const data = await response.text()
return Promise.reject(data)
}
return response.text()
})
.then((data) => {
const events = JSON.parse(data)
displayEvents({
events,
document,
container,
maxEventsCount: eventsCount,
})
})
.catch((data) => {
const parsedData = JSON.parse(data)
displayErrorMessage({ data: parsedData, container })
})
}
}, 500)
}
useEffect(() => {
reloadEventList(attributes.eventsCount, attributes.groupName)
}, [])
function updateEventsCount(event) {
let newValue = Number(event.target.value)
if (newValue < 1) {
newValue = 1
}
setAttributes({ eventsCount: newValue })
reloadEventList(newValue, attributes.groupName)
}
function updateGroupName(event) {
const newValue = event.target.value
setAttributes({ groupName: newValue })
reloadEventList(attributes.eventsCount, newValue)
}
return [
<InspectorControls>
<Panel>
<PanelBody
title={Formatter.escapeHTML(
__('Events List Settings', '<wordpress-name>'),
)}
>
<label
className="components-base-control__label"
htmlFor={NAME + '_events-count'}
>
{Formatter.escapeHTML(
__('Number of events to show', '<wordpress-name>'),
)}
</label>
<input
className="components-text-control__input"
type="number"
value={attributes.eventsCount}
onChange={updateEventsCount}
id={NAME + '_events-count'}
/>
<label
className="components-base-control__label"
htmlFor={NAME + '_group-name'}
>
{Formatter.escapeHTML(
__('Group name (optional)', '<wordpress-name>'),
)}
</label>
<input
className="components-text-control__input"
type="text"
value={attributes.groupName}
onChange={updateGroupName}
id={NAME + '_group-name'}
/>
</PanelBody>
</Panel>
</InspectorControls>,
<div {...blockProps}>
<div className="general-error" style={{ display: 'none' }}>
{Formatter.escapeHTML(
__('The events could not be loaded!', '<wordpress-name>'),
)}
</div>
<div className="group-not-found" style={{ display: 'none' }}>
{Formatter.escapeHTML(
__('The group could not be found!', '<wordpress-name>'),
)}
</div>
<div className="loading-indicator" style={{ display: 'none' }}>
{Formatter.escapeHTML(__('Loading...', '<wordpress-name>'))}
</div>
<ul style={{ 'list-style-type': 'none', 'padding-left': 0 }}></ul>
<a
href=""
target="_blank"
style={{ display: 'inline-block', 'margin-top': '20px;' }}
>
{Formatter.escapeHTML(__('Show more events', '<wordpress-name>'))}
</a>
</div>,
]
}

View File

@ -0,0 +1,7 @@
import edit from './edit.js'
const { registerBlockType } = wp.blocks
const NAME = '<wordpress-name>'
registerBlockType(NAME + '/events-list', { edit })

View File

@ -1,5 +1,5 @@
import test from 'ava'
import DateTimeWrapper from './date-time-wrapper'
import DateTimeWrapper from './date-time-wrapper.js'
test('#getShortDate usual date', (t) => {
const d = new DateTimeWrapper({ text: '2020-12-24T16:45:00Z' })

View File

@ -1,81 +1,113 @@
import test from 'ava'
import { JSDOM } from 'jsdom'
import { displayEvents, displayErrorMessage } from './events-displayer'
let document
import {
displayEvents,
displayErrorMessage,
hideErrorMessages,
showLoadingIndicator,
} from './events-displayer.js'
test.before(() => {
document = new JSDOM().window.document
const dom = new JSDOM()
global.document = dom.window.document
global.window = dom.window
window.MOBILIZON_CONNECTOR = {
locale: 'en-GB',
timeZone: 'utc',
}
})
test.beforeEach((t) => {
t.context.list = document.createElement('ul')
t.context.list.setAttribute('data-locale', 'en-GB')
t.context.list.setAttribute('data-maximum', '2')
t.context.list.setAttribute('data-time-zone', 'utc')
const listElement = document.createElement('li')
listElement.setAttribute('style', 'display: none;')
t.context.list.appendChild(listElement)
const listElement2 = document.createElement('li')
listElement2.setAttribute('style', 'display: none;')
t.context.list.appendChild(listElement2)
t.context.container = document.createElement('div')
const errorMessageGeneral = document.createElement('div')
errorMessageGeneral.setAttribute('class', 'general-error')
errorMessageGeneral.setAttribute('style', 'display: none;')
t.context.container.appendChild(errorMessageGeneral)
const errorMessageGroupNotFound = document.createElement('div')
errorMessageGroupNotFound.setAttribute('class', 'group-not-found')
errorMessageGroupNotFound.setAttribute('style', 'display: none;')
t.context.container.appendChild(errorMessageGroupNotFound)
const loadingIndicator = document.createElement('div')
loadingIndicator.setAttribute('class', 'loading-indicator')
loadingIndicator.setAttribute('style', 'display: none;')
t.context.container.appendChild(loadingIndicator)
const list = document.createElement('ul')
t.context.container.appendChild(list)
})
test('#displayEvents one event', (t) => {
const list = t.context.list
const data = {
events: {
elements: [
{
title: 'a',
url: 'b',
beginsOn: '2021-04-15T10:30:00Z',
endsOn: '2021-04-15T15:30:00Z',
physicalAddress: {
description: 'c',
locality: 'd',
},
},
],
const events = [
{
title: 'a',
url: 'b',
beginsOn: '2021-04-15T10:30:00Z',
endsOn: '2021-04-15T15:30:00Z',
physicalAddress: {
description: 'c',
locality: 'd',
},
},
}
displayEvents({ data, document, list })
t.is(list.children.length, 3)
t.is(list.children[2].childNodes[0].tagName, 'A')
t.is(list.children[2].childNodes[0].getAttribute('href'), 'b')
t.is(list.children[2].childNodes[0].childNodes[0].nodeValue, 'a')
t.is(list.children[2].childNodes[1].tagName, 'BR')
t.is(list.children[2].childNodes[2].nodeValue, '15/04/2021 10:30 - 15:30')
t.is(list.children[2].childNodes[3].tagName, 'BR')
t.is(list.children[2].childNodes[4].nodeValue, 'c, d')
]
const container = t.context.container
displayEvents({ events, document, container, maxEventsCount: 2 })
const list = container.querySelector('ul')
t.is(list.children[0].childNodes[0].tagName, 'A')
t.is(list.children[0].childNodes[0].getAttribute('href'), 'b')
t.is(list.children[0].childNodes[0].childNodes[0].nodeValue, 'a')
t.is(list.children[0].childNodes[1].tagName, 'BR')
t.is(list.children[0].childNodes[2].nodeValue, '15/04/2021 10:30 - 15:30')
t.is(list.children[0].childNodes[3].tagName, 'BR')
t.is(list.children[0].childNodes[4].nodeValue, 'c, d')
})
test('#displayErrorMessage no children added', (t) => {
const list = t.context.list
displayErrorMessage({ data: '', list })
t.is(list.children.length, 2)
test('#displayErrorMessage no list entries shown', (t) => {
const container = t.context.container
displayErrorMessage({ data: '', container })
const list = container.querySelector('ul')
t.is(list.children.length, 0)
})
test('#displayErrorMessage error message display', (t) => {
const list = t.context.list
displayErrorMessage({ data: '', list })
t.is(list.children[0].style.display, 'block')
t.is(list.children[1].style.display, 'none')
test('#displayErrorMessage general error message display', (t) => {
const container = t.context.container
displayErrorMessage({ data: '', container })
t.is(container.querySelector('.general-error').style.display, 'block')
t.is(container.querySelector('.group-not-found').style.display, 'none')
t.is(container.querySelector('.loading-indicator').style.display, 'none')
})
test('#displayErrorMessage group not found error message display', (t) => {
const list = t.context.list
const container = t.context.container
const data = {
response: {
errors: [
{
code: 'group_not_found',
},
],
},
code: 'group_not_found',
}
displayErrorMessage({ data, list })
t.is(list.children[0].style.display, 'none')
t.is(list.children[1].style.display, 'block')
displayErrorMessage({ data, container })
t.is(container.querySelector('.general-error').style.display, 'none')
t.is(container.querySelector('.group-not-found').style.display, 'block')
t.is(container.querySelector('.loading-indicator').style.display, 'none')
})
test('#showLoadingIndicator remove events', (t) => {
const container = t.context.container
const loadingIndicator = container.querySelector('.loading-indicator')
t.is(loadingIndicator.style.display, 'none')
showLoadingIndicator(container)
t.is(loadingIndicator.style.display, 'block')
})
test('#hideErrorMessages remove events', (t) => {
const container = t.context.container
const generalErrorMessage = container.querySelector('.general-error')
const groupNotFoundErrorMessage = container.querySelector('.group-not-found')
generalErrorMessage.style.display = 'block'
groupNotFoundErrorMessage.style.display = 'block'
t.is(generalErrorMessage.style.display, 'block')
t.is(groupNotFoundErrorMessage.style.display, 'block')
hideErrorMessages(container)
t.is(generalErrorMessage.style.display, 'none')
t.is(groupNotFoundErrorMessage.style.display, 'none')
})

View File

@ -1,19 +1,36 @@
import Formatter from './formatter'
import { createAnchorElement } from './html-creator'
import Formatter from './formatter.js'
import { createAnchorElement, createImageElement } from './html-creator.js'
export function clearEventsList(container) {
const list = container.querySelector('ul')
list.replaceChildren()
}
export function displayEvents({ events, document, container, maxEventsCount }) {
hideLoadingIndicator(container)
const isShortOffsetNameShown =
window.MOBILIZON_CONNECTOR.isShortOffsetNameShown
const locale = window.MOBILIZON_CONNECTOR.locale
const timeZone = window.MOBILIZON_CONNECTOR.timeZone
export function displayEvents({ data, document, list }) {
const locale = list.getAttribute('data-locale')
const maxEventsCount = list.getAttribute('data-maximum')
const timeZone = list.getAttribute('data-time-zone')
const isShortOffsetNameShown = list.hasAttribute(
'data-is-short-offset-name-shown'
)
const events = data.events
? data.events.elements
: data.group.organizedEvents.elements
const eventsCount = Math.min(maxEventsCount, events.length)
const list = container.querySelector('ul')
for (let i = 0; i < eventsCount; i++) {
const li = document.createElement('li')
li.style.lineHeight = '150%'
li.style.marginTop = '20px'
if (events[i].picture) {
const img = createImageElement({
document,
alt: events[i].picture.alt ? events[i].picture.alt : '',
src: events[i].picture.base64 ? events[i].picture.base64 : '',
})
img.style.display = 'block'
img.style.maxWidth = '100%'
li.appendChild(img)
}
const a = createAnchorElement({
document,
@ -53,17 +70,29 @@ export function displayEvents({ data, document, list }) {
}
}
export function displayErrorMessage({ data, list }) {
console.error(data)
if (
Object.prototype.hasOwnProperty.call(data, 'response') &&
Object.prototype.hasOwnProperty.call(data.response, 'errors') &&
data.response.errors.length > 0 &&
Object.prototype.hasOwnProperty.call(data.response.errors[0], 'code') &&
data.response.errors[0].code === 'group_not_found'
) {
list.children[1].style.display = 'block'
export function displayErrorMessage({ data, container }) {
hideLoadingIndicator(container)
if (Object.hasOwn(data, 'code') && data.code === 'group_not_found') {
const message = container.querySelector('.group-not-found')
message.style.display = 'block'
} else {
list.children[0].style.display = 'block'
const message = container.querySelector('.general-error')
message.style.display = 'block'
console.error(data)
}
}
export function showLoadingIndicator(container) {
const indicator = container.querySelector('.loading-indicator')
indicator.style.display = 'block'
}
function hideLoadingIndicator(container) {
const indicator = container.querySelector('.loading-indicator')
indicator.style.display = 'none'
}
export function hideErrorMessages(container) {
container.querySelector('.group-not-found').style.display = 'none'
container.querySelector('.general-error').style.display = 'none'
}

View File

@ -1,22 +0,0 @@
import { displayEvents, displayErrorMessage } from './events-displayer'
import * as GraphqlWrapper from './graphql-wrapper'
const NAME = '<wordpress-name>'
document.addEventListener('DOMContentLoaded', () => {
const eventLists = document.getElementsByClassName(NAME + '_events-list')
for (const list of eventLists) {
const url = list.getAttribute('data-url') + '/api'
const limit = parseInt(list.getAttribute('data-maximum'))
const groupName = list.getAttribute('data-group-name')
if (groupName) {
GraphqlWrapper.getUpcomingEventsByGroupName({ url, limit, groupName })
.then((data) => displayEvents({ data, document, list }))
.catch((data) => displayErrorMessage({ data, list }))
} else {
GraphqlWrapper.getUpcomingEvents({ url, limit })
.then((data) => displayEvents({ data, document, list }))
.catch((data) => displayErrorMessage({ data, list }))
}
}
})

View File

@ -1,5 +1,17 @@
import test from 'ava'
import Formatter from './formatter'
import { JSDOM } from 'jsdom'
import Formatter from './formatter.js'
test.beforeEach(() => {
const dom = new JSDOM()
global.document = dom.window.document
})
test('#escapeHTML', (t) => {
const escaped = Formatter.escapeHTML('<b>a</b>')
t.is(escaped, '&lt;b&gt;a&lt;/b&gt;')
})
test('#formatDate one date', (t) => {
const date = Formatter.formatDate({
@ -53,16 +65,21 @@ test('#formatDate second date is null with short offset name', (t) => {
})
test('#formatLocation both parameters', (t) => {
const date = Formatter.formatLocation({ description: 'a', locality: 'b' })
t.is(date, 'a, b')
const location = Formatter.formatLocation({ description: 'a', locality: 'b' })
t.is(location, 'a, b')
})
test('#formatLocation description only', (t) => {
const date = Formatter.formatLocation({ description: 'a' })
t.is(date, 'a')
const location = Formatter.formatLocation({ description: 'a' })
t.is(location, 'a')
})
test('#formatLocation description with space only', (t) => {
const location = Formatter.formatLocation({ description: ' ' })
t.is(location, '')
})
test('#formatLocation locality only', (t) => {
const date = Formatter.formatLocation({ locality: 'a' })
t.is(date, 'a')
const location = Formatter.formatLocation({ locality: 'a' })
t.is(location, 'a')
})

View File

@ -1,6 +1,12 @@
import DateTimeWrapper from './date-time-wrapper'
import DateTimeWrapper from './date-time-wrapper.js'
export default class Formatter {
static escapeHTML(input) {
const div = document.createElement('div')
div.appendChild(document.createTextNode(input))
return div.innerHTML
}
static formatDate({ locale, timeZone, start, end, isShortOffsetNameShown }) {
const startDateTime = new DateTimeWrapper({
locale,
@ -31,8 +37,8 @@ export default class Formatter {
static formatLocation({ description, locality }) {
let location = ''
if (description) {
location += description
if (description && description.trim()) {
location += description.trim()
}
if (location && locality) {
location += ', '

View File

@ -1,78 +0,0 @@
import SessionCache from './session-cache'
import { request } from 'graphql-request'
import DateTimeWrapper from './date-time-wrapper'
export function getUpcomingEvents({ url, limit }) {
const query = `
query ($limit: Int) {
events(limit: $limit) {
elements {
id,
title,
url,
beginsOn,
endsOn,
physicalAddress {
description,
locality
}
},
total
}
}
`
const dataInCache = SessionCache.get(sessionStorage, {
url,
query,
variables: { limit },
})
if (dataInCache !== null) {
return Promise.resolve(dataInCache)
}
return request(url, query, { limit }).then((data) => {
SessionCache.add(sessionStorage, { url, query, variables: { limit } }, data)
return Promise.resolve(data)
})
}
export function getUpcomingEventsByGroupName({ url, limit, groupName }) {
const query = `
query ($afterDatetime: DateTime, $groupName: String, $limit: Int) {
group(preferredUsername: $groupName) {
organizedEvents(afterDatetime: $afterDatetime, limit: $limit) {
elements {
id,
title,
url,
beginsOn,
endsOn,
physicalAddress {
description,
locality
}
},
total
}
}
}
`
const afterDatetime = DateTimeWrapper.getCurrentDatetimeAsString()
const dataInCache = SessionCache.get(sessionStorage, {
url,
query,
variables: { afterDatetime, groupName, limit },
})
if (dataInCache !== null) {
return Promise.resolve(dataInCache)
}
return request(url, query, { afterDatetime, groupName, limit }).then(
(data) => {
SessionCache.add(
sessionStorage,
{ url, query, variables: { afterDatetime, groupName, limit } },
data
)
return Promise.resolve(data)
}
)
}

View File

@ -1,12 +1,11 @@
import test from 'ava'
import { JSDOM } from 'jsdom'
import { createAnchorElement } from './html-creator'
let document
import { createAnchorElement } from './html-creator.js'
test.beforeEach(() => {
document = new JSDOM().window.document
const dom = new JSDOM()
global.document = dom.window.document
})
test('#createAnchorElement usual parameters', (t) => {

View File

@ -5,3 +5,10 @@ export function createAnchorElement({ document, text, url }) {
a.innerHTML = text
return a
}
export function createImageElement({ document, alt, src }) {
const img = document.createElement('img')
img.setAttribute('alt', alt)
img.setAttribute('src', src)
return img
}

View File

@ -1,6 +0,0 @@
import test from 'ava'
import hash from './object-hash-wrapper'
test('#hash object', (t) => {
t.is(hash({ foo: 'bar' }), 'a75c05bdca7d704bdfcd761913e5a4e4636e956b')
})

View File

@ -1,5 +0,0 @@
import objectHash from 'object-hash'
export default function hash(object) {
return objectHash(object)
}

View File

@ -1,33 +0,0 @@
import test from 'ava'
import SessionCache from './session-cache'
const fakeStorage = {
elements: {},
clear() {
this.elements = {}
},
getItem(key) {
const value = this.elements[key]
if (value === undefined) return null
return value
},
setItem(key, value) {
this.elements[key] = value
},
}
test.afterEach(() => {
fakeStorage.clear()
})
test('#add & #get', (t) => {
SessionCache.add(fakeStorage, { a: 'b' }, { c: 'd' })
t.deepEqual(SessionCache.get(fakeStorage, { a: 'b' }), { c: 'd' })
})
test('#get no entry', (t) => {
t.is(SessionCache.get(fakeStorage, { a: 'bb' }), null)
})

View File

@ -1,27 +0,0 @@
import hash from './object-hash-wrapper'
const MAX_AGE_IN_MS = 120000
export default class SessionCache {
static add(storage, parameters, data) {
const key = hash(parameters)
const timestamp = Date.now()
const value = {
data,
timestamp,
}
storage.setItem(key, JSON.stringify(value))
}
static get(storage, parameters) {
const key = hash(parameters)
const value = JSON.parse(storage.getItem(key))
if (
value &&
value.timestamp &&
value.timestamp > Date.now() - MAX_AGE_IN_MS
)
return value.data
return null
}
}

53
source/includes/Api.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace MobilizonConnector;
class Api {
public static function init() {
add_action('rest_api_init', 'MobilizonConnector\Api::init_api');
}
public static function init_api() {
register_rest_route(
NAME . '/v1',
'/events',
[
'methods' => 'GET',
'callback' => 'MobilizonConnector\Api::get_events',
'args' => [
'eventsCount' => [
'required' => true,
'validate_callback' => function($param, $request, $key) {
return is_numeric($param) && $param > 0;
}
],
'groupName' => [
'validate_callback' => function($param, $request, $key) {
return !is_numeric($param);
}
]
],
'permission_callback' => '__return_true',
]
);
}
public static function get_events($request) {
$eventsCount = $request['eventsCount'];
$groupName = isset($request['groupName']) ? $request['groupName'] : '';
$url = Settings::getUrl();
try {
if ($groupName) {
$events = GraphQlClient::get_upcoming_events_by_group_name($url, (int) $eventsCount, $groupName);
} else {
$events = GraphQlClient::get_upcoming_events($url, (int) $eventsCount);
}
return $events;
} catch (GeneralException $e) {
return new \WP_Error('events_not_loading', 'The events could not be loaded!', array('status' => 500));
} catch (GroupNotFoundException $e) {
return new \WP_Error('group_not_found', sprintf('The group "%s" could not be found!', $groupName), array('status' => 404));
}
}
}

View File

@ -1,11 +1,6 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
const DEFAULT_EVENTS_COUNT = 5;
const NAME = '<wordpress-name>';
const NICE_NAME = '<wordpress-nice-name>';

View File

@ -0,0 +1,34 @@
<?php
namespace MobilizonConnector;
final class DateTimeWrapper {
private $dateTime;
private $locale;
private $timeZone;
public function __construct(string $text, string $locale = 'en-GB', string $timeZone = 'utc') {
if (!$locale) {
$locale = 'en-GB';
}
if (!$timeZone) {
$timeZone = 'utc';
}
$this->dateTime = new \DateTime($text);
$this->locale = $locale;
$this->timeZone = new \DateTimeZone($timeZone);
}
public function get24Time(): string {
$formatter = \IntlDateFormatter::create($this->locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT, $this->timeZone);
return $formatter->format($this->dateTime);
}
public function getShortDate(): string {
$formatter = \IntlDateFormatter::create($this->locale, \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE, $this->timeZone);
return $formatter->format($this->dateTime);
}
public function getTimeZoneName(): string {
return $this->timeZone->getName();
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace MobilizonConnector;
final class EventsCache {
private static $MAX_AGE_IN_S = 120;
public static function set(array $parameters, mixed $data): void {
// md5 is used as key must be 172 characters or fewer in length.
$key = md5(json_encode($parameters));
set_transient($key, $data, self::$MAX_AGE_IN_S);
}
public static function get(array $parameters): mixed {
$key = md5(json_encode($parameters));
$data = get_transient($key);
return $data;
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace MobilizonConnector;
class EventsListBlock {
public static function initAndReturnScriptName(): string {
$scriptName = NAME . '-block-starter';
wp_register_script($scriptName, plugins_url(NAME . '/front/block-events-loader.js'), [
'wp-block-editor',
'wp-blocks',
'wp-components',
'wp-i18n'
], '<wordpress-version>', array('in_footer' => true));
register_block_type(NAME . '/events-list', [
'api_version' => 2,
'title' => esc_html__('Events List', 'connector-mobilizon'),
'description' => esc_html__('A list of the upcoming events of the connected Mobilizon instance.', 'connector-mobilizon'),
'category' => 'widgets',
'icon' => 'list-view',
'supports' => [
'html' => false
],
'attributes' => [
'eventsCount' => [
'type' => 'number',
'default' => 3,
],
'groupName' => [
'type' => 'string',
],
],
'editor_script' => $scriptName,
'render_callback' => 'MobilizonConnector\EventsListBlock::render',
]);
return $scriptName;
}
public static function render($block_attributes, $content) {
$url = Settings::getUrl();
$eventsCount = $block_attributes['eventsCount'];
$groupName = isset($block_attributes['groupName']) ? $block_attributes['groupName'] : '';
$classNamePrefix = NAME;
ob_start();
try {
$showMoreUrl = Settings::getUrl();
if ($groupName) {
$events = GraphQlClient::get_upcoming_events_by_group_name($url, (int) $eventsCount, $groupName);
$showMoreUrl .= '/@' . $groupName . '/events';
} else {
$events = GraphQlClient::get_upcoming_events($url, (int) $eventsCount);
}
$locale = get_locale();
$isShortOffsetNameShown = Settings::isShortOffsetNameShown();
$timeZone = wp_timezone_string();
require dirname(__DIR__) . '/view/events-list.php';
} catch (GeneralException $e) {
require dirname(__DIR__) . '/view/events-list-not-loaded.php';
} catch (GroupNotFoundException $e) {
require dirname(__DIR__) . '/view/events-list-group-not-found.php';
}
$output = ob_get_clean();
return $output;
}
}

View File

@ -1,11 +1,6 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
class EventsListShortcut {
public static function init() {
@ -24,16 +19,31 @@ class EventsListShortcut {
), $atts
);
$classNamePrefix = NAME;
$eventsCount = $atts_with_overriden_defaults['events-count'];
$locale = str_replace('_', '-', get_locale());
$groupName = $atts_with_overriden_defaults['group-name'];
$url = Settings::getUrl();
$timeZone = wp_timezone_string();
$isShortOffsetNameShown = Settings::isShortOffsetNameShown();
$eventsCount = $atts_with_overriden_defaults['events-count'];
$groupName = $atts_with_overriden_defaults['group-name'];
$classNamePrefix = NAME;
ob_start();
require dirname(__DIR__) . '/view/events-list.php';
try {
$showMoreUrl = Settings::getUrl();
if ($groupName) {
$events = GraphQlClient::get_upcoming_events_by_group_name($url, (int) $eventsCount, $groupName);
$showMoreUrl .= '/@' . $groupName . '/events';
} else {
$events = GraphQlClient::get_upcoming_events($url, (int) $eventsCount);
}
$locale = get_locale();
$isShortOffsetNameShown = Settings::isShortOffsetNameShown();
$timeZone = wp_timezone_string();
require dirname(__DIR__) . '/view/events-list.php';
} catch (GeneralException $e) {
require dirname(__DIR__) . '/view/events-list-not-loaded.php';
} catch (GroupNotFoundException $e) {
require dirname(__DIR__) . '/view/events-list-group-not-found.php';
}
$output = ob_get_clean();
return $output;
}

View File

@ -1,19 +1,14 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
class EventsListWidget extends \WP_Widget {
public function __construct() {
parent::__construct(
NAME . '-events-list',
NICE_NAME . ' ' . __('Events List', 'connector-mobilizon'),
NICE_NAME . ' ' . esc_html__('Events List', 'connector-mobilizon'),
array(
'description' => __('A list of the upcoming events of the connected Mobilizon instance.', 'connector-mobilizon'),
'description' => esc_html__('A list of the upcoming events of the connected Mobilizon instance.', 'connector-mobilizon'),
),
);
}
@ -25,21 +20,36 @@ class EventsListWidget extends \WP_Widget {
echo $args['before_title'].apply_filters('widget_title', $options['title']).$args['after_title'];
}
$classNamePrefix = NAME;
$eventsCount = $options['eventsCount'];
$locale = str_replace('_', '-', get_locale());
$groupName = isset($options['groupName']) ? $options['groupName'] : '';
$url = Settings::getUrl();
$timeZone = wp_timezone_string();
$isShortOffsetNameShown = Settings::isShortOffsetNameShown();
$eventsCount = $options['eventsCount'];
$groupName = isset($options['groupName']) ? $options['groupName'] : '';
$classNamePrefix = NAME;
require dirname(__DIR__) . '/view/events-list.php';
try {
$showMoreUrl = Settings::getUrl();
if ($groupName) {
$events = GraphQlClient::get_upcoming_events_by_group_name($url, (int) $eventsCount, $groupName);
$showMoreUrl .= '/@' . $groupName . '/events';
} else {
$events = GraphQlClient::get_upcoming_events($url, (int) $eventsCount);
}
$locale = get_locale();
$isShortOffsetNameShown = Settings::isShortOffsetNameShown();
$timeZone = wp_timezone_string();
require dirname(__DIR__) . '/view/events-list.php';
} catch (GeneralException $e) {
require dirname(__DIR__) . '/view/events-list-not-loaded.php';
} catch (GroupNotFoundException $e) {
require dirname(__DIR__) . '/view/events-list-group-not-found.php';
}
echo $args['after_widget'];
}
public function form($options) {
$title = !empty($options['title']) ? $options['title'] : __('Events', 'connector-mobilizon');
$title = !empty($options['title']) ? $options['title'] : esc_html__('Events', 'connector-mobilizon');
$eventsCount = !empty($options['eventsCount']) ? $options['eventsCount'] : DEFAULT_EVENTS_COUNT;
$groupName = !empty($options['groupName']) ? $options['groupName'] : '';

View File

@ -0,0 +1,42 @@
<?php
namespace MobilizonConnector;
final class Formatter
{
public static function format_date(string $locale, string $timeZone, string $start, ?string $end, bool $isShortOffsetNameShown): string {
$startDateTime = new DateTimeWrapper($start, $locale, $timeZone);
$dateText = $startDateTime->getShortDate();
$dateText .= ' ' . $startDateTime->get24Time();
if (!$end && $isShortOffsetNameShown) {
$dateText .= ' (' . $startDateTime->getTimeZoneName() . ')';
}
if ($end) {
$endDateTime = new DateTimeWrapper($end, $locale, $timeZone);
if ($startDateTime->getShortDate() != $endDateTime->getShortDate()) {
$dateText .= ' - ';
$dateText .= $endDateTime->getShortDate() . ' ';
} else {
$dateText .= ' - ';
}
$dateText .= $endDateTime->get24Time();
if ($isShortOffsetNameShown) {
$dateText .= ' (' . $endDateTime->getTimeZoneName() . ')';
}
}
return $dateText;
}
public static function format_location(string $description, ?string $locality): string {
$location = '';
if ($description && trim($description)) {
$location .= trim($description);
}
if ($location && $locality) {
$location .= ', ';
}
if ($locality) {
$location .= $locality;
}
return $location;
}
}

View File

@ -0,0 +1,152 @@
<?php
namespace MobilizonConnector;
final class GraphQlClient {
public static function query(string $endpoint, string $query, array $variables = [], ?string $token = null): array
{
$headers = ['Content-Type: application/json'];
if ($token !== null) {
$headers[] = "Authorization: bearer $token";
}
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => $headers,
'content' => json_encode(['query' => $query, 'variables' => $variables]),
]
]);
$data = @file_get_contents($endpoint, false, $context);
if ($data === false) {
$error = error_get_last();
throw new \ErrorException($error['message'], $error['type']);
}
return json_decode($data, true);
}
public static function get_upcoming_events(string $url, int $limit): array {
$query = <<<'GRAPHQL'
query ($limit: Int) {
events(limit: $limit) {
elements {
id,
title,
url,
beginsOn,
endsOn,
physicalAddress {
description,
locality
},
picture {
alt,
contentType,
url
}
},
total
}
}
GRAPHQL;
$cachedEvents = EventsCache::get(['url' => $url, 'query' => $query, 'limit' => $limit]);
if ($cachedEvents !== false) {
return $cachedEvents;
}
$endpoint = $url . '/api';
$data = self::query($endpoint, $query, ['limit' => $limit]);
self::checkData($data);
$events = $data['data']['events']['elements'];
foreach ($events as &$event) {
if ($event['picture']) {
$picture_response = self::download_image($event['picture']['url']);
if ($picture_response !== false) {
$picture_encoded = 'data:' . $event['picture']['contentType'] . ';base64,' . base64_encode($picture_response);
$event['picture']['base64'] = $picture_encoded;
}
}
unset($event);
}
EventsCache::set(['url' => $url, 'query' => $query, 'limit' => $limit], $events);
return $events;
}
public static function get_upcoming_events_by_group_name(string $url, int $limit, string $groupName): array {
$query = <<<'GRAPHQL'
query ($afterDatetime: DateTime, $groupName: String!, $limit: Int) {
group(preferredUsername: $groupName) {
organizedEvents(afterDatetime: $afterDatetime, limit: $limit) {
elements {
id,
title,
url,
beginsOn,
endsOn,
physicalAddress {
description,
locality
},
picture {
alt,
contentType,
url
}
},
total
}
}
}
GRAPHQL;
$afterDatetime = date(\DateTime::ISO8601);
$cachedEvents = EventsCache::get(['url' => $url, 'query' => $query, 'afterDatetime' => $afterDatetime, 'groupName' => $groupName, 'limit' => $limit]);
if ($cachedEvents !== false) {
return $cachedEvents;
}
$endpoint = $url . '/api';
$data = self::query($endpoint, $query, ['afterDatetime' => $afterDatetime, 'groupName' => $groupName, 'limit' => $limit]);
self::checkData($data);
$events = $data['data']['group']['organizedEvents']['elements'];
foreach ($events as &$event) {
if ($event['picture']) {
$picture_response = self::download_image($event['picture']['url']);
if ($picture_response !== false) {
$picture_encoded = 'data:' . $event['picture']['contentType'] . ';base64,' . base64_encode($picture_response);
$event['picture']['base64'] = $picture_encoded;
}
}
unset($event);
}
EventsCache::set(['url' => $url, 'query' => $query, 'afterDatetime' => $afterDatetime, 'groupName' => $groupName, 'limit' => $limit], $events);
return $events;
}
private static function checkData($data) {
if (isset($data['errors'])) {
if (count($data['errors']) > 0 &&
isset($data['errors'][0]['code']) &&
$data['errors'][0]['code'] === 'group_not_found') {
throw new GroupNotFoundException(serialize($data['errors'][0]));
} else {
throw new GeneralException(serialize($data['errors']));
}
}
}
private static function download_image($url) {
$response = wp_remote_get($url);
$image_data = $response['body'];
return $image_data;
}
}

View File

@ -1,11 +1,6 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
class Settings {
private static $DEFAULT_OPTION_URL = 'https://mobilizon.fr';
@ -36,14 +31,14 @@ class Settings {
add_settings_section(
self::$SETTINGS_SECTION_NAME,
__('General Settings', 'connector-mobilizon'),
esc_html__('General Settings', 'connector-mobilizon'),
'',
self::$PAGE_NAME
);
add_settings_field(
self::$SETTING_FIELD_NAME_URL,
__('URL', 'connector-mobilizon'),
esc_html__('URL', 'connector-mobilizon'),
'MobilizonConnector\Settings::output_field_url',
self::$PAGE_NAME,
self::$SETTINGS_SECTION_NAME,
@ -53,7 +48,7 @@ class Settings {
);
add_settings_field(
self::$SETTING_FIELD_NAME_IS_SHORT_OFFSET_NAME_SHOWN,
__('Display named offset', 'connector-mobilizon'),
esc_html__('Display named offset', 'connector-mobilizon'),
'MobilizonConnector\Settings::output_field_is_short_offset_name_shown',
self::$PAGE_NAME,
self::$SETTINGS_SECTION_NAME,
@ -79,7 +74,7 @@ class Settings {
add_settings_error(
self::$OPTION_NAME_URL,
'wordpress_mobilizon_field_url_error',
__('The URL is invalid.', 'connector-mobilizon'),
esc_html__('The URL is invalid.', 'connector-mobilizon'),
'error'
);
}
@ -91,7 +86,7 @@ class Settings {
public static function register_settings_page() {
add_options_page(
NICE_NAME . ' ' . __('Settings', 'connector-mobilizon'),
NICE_NAME . ' ' . esc_html__('Settings', 'connector-mobilizon'),
NICE_NAME,
'manage_options',
NAME . '-settings',

View File

@ -0,0 +1,12 @@
<?php
namespace MobilizonConnector;
class GeneralException extends \Exception {
public function __construct($message, $code = 0, Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
public function __toString() {
return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace MobilizonConnector;
class GroupNotFoundException extends \Exception {
public function __construct($message, $code = 0, Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
public function __toString() {
return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
}
}

View File

@ -15,27 +15,148 @@ License: <wordpress-license>
<wordpress-nice-name> allows you to display the upcoming events of [Mobilizon](https://joinmobilizon.org/), which is a federated event listing platform, on your WordPress website.
Features
- Display events as widget and as shortcut
- Display events' title, date, and location if available
- Cache requests' responses for 2 minutes in the browser's `sessionStorage`
- Configure number of events to show per widget and per shortcut
- Optionally filter events by a specific group per widget and per shortcut
- Display events as Gutenberg block, as widget and as shortcut
- Display events' picture, if available, title with link, date, and location, if available
- Cache requests' responses for 2 minutes in the database
- Configure number of events to show per block, per widget and per shortcut
- Optionally filter events by a specific public group per block, per widget and per shortcut
- Set the URL of the Mobilizon instance in the settings
- Toggle adding named offset in brackets after the time in the settings
Shortcut format with limiting the number of events to show to 3 for example: `[<wordpress-name>-events-list events-count=3]`
Optionally, you can only show the events of a specific group by indicatings its name: `[<wordpress-name>-events-list events-count=3 group-name="mygroup"]`
This plugin requests the events via Mobilizon's GraphQL API.
The source code is available on [Github](https://github.com/dwaxweiler/connector-mobilizon).
## Installation
Install this plugin on the "Add Plugin" page in the administrator backend of your WordPress website by searching for it by its name or by uploading its archive by clicking on "Upload Plugin".
In both cases, you then need to click the corresponding "Install now" button.
After the installation, you can adapt the URL of the Mobilizon instance whose events you want to list on the plugin's settings' page.
### Shortcut usage
Shortcut format with limiting the number of events to show to 3 for example: `[<wordpress-name>-events-list events-count=3]`
Optionally, you can only show the events of a specific group by indicatings its name: `[<wordpress-name>-events-list events-count=3 group-name="mygroup"]`
You have to use their username, e.g. `@nosliensvivants`, and append the name of their instance if they use a different one, e.g. `@yaam_berlin@mobilize.berlin`.
## Screenshots
1. Events list
2. General settings
3. Widget creation
4. Shortcut creation
5. Gutenberg block in editor
## Changelog
### [1.4.0]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.8
### [1.3.0]
#### Added
- Comment for translators what placeholder will contain
#### Changed
- Confirm compatibility with WordPress 6.7
- Load block script only in footer to reduce waiting time
- Update dependencies
#### Fixed
- Mark event-related data as non-translatable within plugin
- Add version number to script registration to break browser caching
- Handle location being null
### [1.2.0]
#### Added
- Display event picture if available
#### Changed
- Update dependencies
### [1.1.0]
#### Added
- Add some spacing between event items
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.6
#### Fixed
- Fix undefined variable $classNamePrefix for both error views
### [1.0.0]
#### Added
- Display name of group when it cannot be found
#### Changed
- Let backend do requests to API of Mobilizon instance for increased privacy
- Update dependencies
#### Fixed
- Fix displaying more than one block in the editor
### [0.11.5]
#### Changed
- Confirm compatibility with WordPress 6.5
### [0.11.4]
#### Changed
- Confirm compatibility with WordPress 6.4
- Update dependencies
### [0.11.3]
#### Fixed
- Clean up distributed files
### [0.11.2]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.3
### [0.11.1]
#### Fixed
- Revert minimum PHP version to 7.4 to allow some more time for upgrading PHP
### [0.11.0]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.2
#### Security
- Set minimum PHP version to oldest stable 8.0
### [0.10.1]
#### Changed
- Confirm compatibility with WordPress 6.1
- Update dependencies
### [0.10.0]
#### Added
- Add Gutenberg events list block
- Show loading indicator during request
#### Changed
- Set list style type to none and left padding to zero for all occurences
- Move shortcut usage description into installation section in `readme.txt`
- Update dependencies
### [0.9.1] - 2020-05-19
#### Fixed
- Fix WordPress compatibility version number
### [0.9.0] - 2020-05-19
#### Added
- Improve explanation of group name filter
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.0
#### Fixed
- Fix displaying error message for the case the group is not found
### [0.8.0] - 2022-01-09
#### Added
- Add support for older browsers using babel
#### Changed
- Confirm compatibility with WordPress 5.9
- Update dependencies
#### Fixed
- Use ES modules correctly
- Trim events' location
### [0.7.0] - 2021-12-23
#### Added
- Add specific error message for the case the group is not found

View File

@ -1,6 +1,6 @@
<?php
require_once __DIR__ . '/includes/constants.php';
require_once __DIR__ . '/includes/settings.php';
require_once __DIR__ . '/includes/Constants.php';
require_once __DIR__ . '/includes/Settings.php';
// If uninstall.php is not called by WordPress, exit.
if (!defined('WP_UNINSTALL_PLUGIN')) {

View File

@ -0,0 +1,14 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="<?php echo esc_attr($classNamePrefix); ?>_events-list">
<?php
/* translators: %s is replaced with the name of the group. */
echo esc_html(sprintf(__('The group "%s" could not be found!', 'connector-mobilizon'), $groupName));
?>
</div>

View File

@ -0,0 +1,11 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="<?php echo esc_attr($classNamePrefix); ?>_events-list">
<?php esc_html_e('The events could not be loaded!', 'connector-mobilizon'); ?>
</div>

View File

@ -1,4 +1,6 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;

View File

@ -1,16 +1,29 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
?>
<ul class="<?php echo esc_attr($classNamePrefix); ?>_events-list"
data-url="<?php echo esc_attr($url); ?>"
data-locale="<?php echo esc_attr($locale); ?>"
data-maximum="<?php echo esc_attr($eventsCount); ?>"
data-group-name="<?php echo esc_attr($groupName); ?>"
data-time-zone="<?php echo esc_attr($timeZone); ?>"
<?php echo $isShortOffsetNameShown ? 'data-is-short-offset-name-shown' : ''; ?>>
<li style="display: none;"><?php esc_html_e('The events could not be loaded!', 'connector-mobilizon'); ?></li>
<li style="display: none;"><?php esc_html_e('The group could not be found!', 'connector-mobilizon'); ?></li>
</ul>
<div class="<?php echo esc_attr($classNamePrefix); ?>_events-list">
<ul style="list-style-type: none; padding-left: 0;">
<?php foreach ($events as $event) { ?>
<li style="line-height: 150%; margin-top: 20px;">
<?php if (isset($event['picture'])) { ?>
<img alt="<?php echo esc_attr($event['picture']['alt']); ?>" src="<?php echo esc_attr($event['picture']['base64']); ?>" style="display: block; max-width: 100%;">
<?php } ?>
<a href="<?php echo esc_attr($event['url']); ?>"><?php echo esc_html($event['title']); ?></a>
<br>
<?php echo esc_html(Formatter::format_date($locale, $timeZone, $event['beginsOn'], $event['endsOn'], $isShortOffsetNameShown)); ?>
<?php if (isset($event['physicalAddress'])) { ?>
<br>
<?php echo esc_html(Formatter::format_location($event['physicalAddress']['description'], $event['physicalAddress']['locality'])) ?>
<?php } ?>
</li>
<?php } ?>
</ul>
<a href="<?php echo esc_attr($showMoreUrl); ?>" class="button" style="display:inline-block; margin-top: 20px;">
<?php esc_html_e('Show more events', 'connector-mobilizon'); ?>
</a>
</div>

View File

@ -1,4 +1,6 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;

View File

@ -1,4 +1,6 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;

View File

@ -1,4 +1,6 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
use MobilizonConnector\DateTimeWrapper;
use PHPUnit\Framework\TestCase;
final class DateTimeWrapperTest extends TestCase {
public function testCanGet24TimeForUsualTime(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z');
$this->assertSame('16:45', $d->get24Time());
}
public function testCanGetShortDateForUsualDate(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortDateForUsualDateWithLocaleWithUnderscore(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z');
$this->assertSame('24/12/2020', $d->getShortDate(), 'en_GB');
}
public function testCanGetShortDateForUsualDateWithTimezoneString(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z', 'en-GB', 'Europe/Rome');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortDateForUsualDateWithNamedOffset(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z', 'en-GB', 'UTC');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortDateForUsualDateWithOffset(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z', 'en-GB', '+02:00');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortDateForUsualDateWithEmptyTimezone(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z', 'en-GB', '');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortOffsetNameForUsualTime(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z');
$this->assertSame('UTC', $d->getTimeZoneName());
}
}

56
tests/FormatterTest.php Normal file
View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
use MobilizonConnector\Formatter;
use PHPUnit\Framework\TestCase;
final class FormatterTest extends PHPUnit\Framework\TestCase
{
public function testCanDateFormatOneDate(): void {
$this->assertSame('15/04/2021 10:30 - 15:30', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', '2021-04-15T15:30:00Z', false));
}
public function testCanDateFormatOneDateWithOffset(): void {
$this->assertSame('15/04/2021 10:30 - 15:30 (UTC)', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', '2021-04-15T15:30:00Z', true));
}
public function testCanDateFormatOneDateWithTimeZoneOffset(): void {
$this->assertSame('15/04/2021 11:30 - 16:30', Formatter::format_date('en-GB', '+01:00', '2021-04-15T10:30:00Z', '2021-04-15T15:30:00Z', false));
}
public function testCanDateFormatTwoDates(): void {
$this->assertSame('15/04/2021 10:30 - 16/04/2021 15:30', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', '2021-04-16T15:30:00Z', false));
}
public function testCanDateFormatTwoDatesWithOffset(): void {
$this->assertSame('15/04/2021 10:30 - 16/04/2021 15:30 (UTC)', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', '2021-04-16T15:30:00Z', true));
}
public function testCanDateFormatWhenSecondDateIsNull(): void {
$this->assertSame('15/04/2021 10:30', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', null, false));
}
public function testCanDateFormatWhenSecondDateIsNullWithOffset(): void {
$this->assertSame('15/04/2021 10:30 (UTC)', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', null, true));
}
public function testCanLocationFormatBothParameters(): void {
$this->assertSame('a, b', Formatter::format_location('a', 'b'));
}
public function testLocationFormatDescriptionOnly(): void {
$this->assertSame('a', Formatter::format_location('a', ''));
}
public function testLocationFormatDescriptionOnlyWithNull(): void {
$this->assertSame('a', Formatter::format_location('a', null));
}
public function testLocationFormatDescriptionWithSpaceOnly(): void {
$this->assertSame('', Formatter::format_location(' ', ''));
}
public function testLocationFormatLocalityOnly(): void {
$this->assertSame('a', Formatter::format_location('', 'a'));
}
}

View File

@ -6,10 +6,26 @@ const CopyPlugin = require('copy-webpack-plugin')
const FOLDER_SOURCE = './source'
module.exports = {
entry: FOLDER_SOURCE + '/front/events-loader.js',
entry: {
'block-events-loader': FOLDER_SOURCE + '/front/block-events-loader.js',
},
output: {
filename: 'events-loader.js',
path: path.resolve(__dirname, 'build/' + '/front'),
filename: '[name].js',
path: path.resolve(__dirname, 'build/front'),
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [['@babel/preset-env', { targets: 'defaults' }]],
},
},
},
],
},
plugins: [
new CopyPlugin({