├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── nightly-build.yml ├── .gitignore ├── .nvmrc ├── .stylelintrc.json ├── .travis.yml ├── LICENSE.TXT ├── Makefile ├── README.TXT ├── README.md ├── assets ├── banner-1544x500.jpg ├── banner-1544x500.png ├── banner-772x250.jpg ├── banner-772x250.png ├── icon-256x256.png ├── screenshot-1.gif ├── screenshot-2.jpg ├── screenshot-3.gif ├── screenshot-4.jpg └── stylesheets │ ├── applause.scss │ ├── editor.scss │ ├── feedback.scss │ ├── nps.scss │ ├── poll.scss │ ├── shared │ ├── animations.scss │ └── variables.scss │ └── vote.scss ├── babel.config.js ├── changelog.txt ├── client ├── apifetch │ ├── index.js │ └── middleware.js ├── applause.js ├── blocks │ ├── applause │ │ ├── attributes.js │ │ ├── constants.js │ │ ├── edit.js │ │ ├── edit.scss │ │ ├── index.js │ │ ├── sidebar.js │ │ ├── toolbar.js │ │ └── util.js │ ├── cs-embed │ │ ├── attributes.js │ │ ├── cs-domains.js │ │ ├── edit.js │ │ ├── edit.scss │ │ ├── embed-loading.js │ │ ├── embed-preview.js │ │ ├── index.js │ │ ├── save.js │ │ ├── sidebar.js │ │ ├── toolbar.js │ │ └── variations.js │ ├── feedback │ │ ├── attributes.js │ │ ├── constants.js │ │ ├── edit.js │ │ ├── edit.scss │ │ ├── index.js │ │ ├── sidebar.js │ │ ├── toolbar.js │ │ └── util.js │ ├── nps │ │ ├── attributes.js │ │ ├── constants.js │ │ ├── edit.js │ │ ├── edit.scss │ │ ├── index.js │ │ ├── sidebar.js │ │ ├── toolbar.js │ │ └── util.js │ ├── poll │ │ ├── attributes.js │ │ ├── constants.js │ │ ├── edit-answer.js │ │ ├── edit-answers.js │ │ ├── edit-bar.js │ │ ├── edit.js │ │ ├── edit.scss │ │ ├── hooks.js │ │ ├── index.js │ │ ├── sidebar.js │ │ ├── subscriptions.js │ │ ├── toolbar.js │ │ ├── util.js │ │ └── util.test.js │ ├── vote-item │ │ ├── attributes.js │ │ ├── edit.js │ │ ├── edit.scss │ │ ├── index.js │ │ └── sidebar.js │ └── vote │ │ ├── attributes.js │ │ ├── constants.js │ │ ├── edit.js │ │ ├── edit.scss │ │ ├── index.js │ │ ├── sidebar.js │ │ ├── toolbar.js │ │ └── util.js ├── components │ ├── applause │ │ ├── animation.js │ │ ├── index.js │ │ └── style.scss │ ├── block-alignment-control │ │ ├── constants.js │ │ ├── grid-button.js │ │ ├── grid.js │ │ ├── icon.js │ │ ├── index.js │ │ └── style.scss │ ├── brand-link │ │ ├── index.js │ │ └── style.scss │ ├── connect-to-crowdsignal │ │ ├── index.js │ │ └── style.scss │ ├── dialog-wrapper │ │ ├── index.js │ │ └── style.scss │ ├── editor-notice │ │ ├── index.js │ │ └── style.scss │ ├── feedback │ │ ├── form.js │ │ ├── index.js │ │ ├── popover.js │ │ ├── style.scss │ │ ├── submit.js │ │ ├── toggle.js │ │ └── util.js │ ├── footer-branding │ │ ├── index.js │ │ └── style.scss │ ├── icon │ │ ├── applause.js │ │ ├── border.js │ │ ├── check-circle.js │ │ ├── checklist-multiple-choice.js │ │ ├── checklist-single-choice.js │ │ ├── close-small.js │ │ ├── close.js │ │ ├── counter.js │ │ ├── cslogo.js │ │ ├── feedback.js │ │ ├── nps.js │ │ ├── pencil.js │ │ ├── placement.js │ │ ├── poll.js │ │ ├── quiz.js │ │ ├── signal.js │ │ ├── size.js │ │ ├── survey.js │ │ ├── thank-you.js │ │ ├── thumbs-down.js │ │ ├── thumbs-up.js │ │ ├── vote.js │ │ └── warning-circle.js │ ├── nps │ │ ├── feedback.js │ │ ├── index.js │ │ ├── rating.js │ │ └── style.scss │ ├── poll │ │ ├── answer-results.js │ │ ├── answer.js │ │ ├── closed-banner.js │ │ ├── error-banner.js │ │ ├── index.js │ │ ├── results.js │ │ ├── style.scss │ │ ├── submit-message.js │ │ ├── util.js │ │ ├── util.test.js │ │ └── vote.js │ ├── promotional-tooltip │ │ └── index.js │ ├── retry-notice │ │ └── index.js │ ├── sidebar-promote │ │ ├── index.js │ │ └── style.scss │ ├── signal-warning │ │ └── index.js │ ├── use-autosave │ │ └── index.js │ ├── use-numbered-title │ │ └── index.js │ ├── use-poll-duplicate-cleaner │ │ └── index.js │ ├── vote │ │ ├── index.js │ │ ├── style.scss │ │ ├── util.js │ │ └── vote-item.js │ ├── with-client-id │ │ └── index.js │ ├── with-fallback-styles │ │ ├── index.js │ │ ├── style.scss │ │ ├── util.js │ │ └── util.test.js │ ├── with-fixed-position │ │ └── index.js │ ├── with-fse-check │ │ └── index.js │ └── with-poll-base │ │ └── index.js ├── cs-embed.js ├── data │ ├── feedback │ │ ├── edit.js │ │ └── index.js │ ├── hooks │ │ ├── index.js │ │ └── util.js │ ├── nps │ │ ├── edit.js │ │ └── index.js │ ├── poll │ │ └── index.js │ └── util.js ├── editor.js ├── feedback.js ├── lib │ ├── mutation-observer │ │ └── index.js │ └── tracks.js ├── nps.js ├── poll.js ├── state │ ├── account │ │ ├── actions.js │ │ └── reducer.js │ ├── action-types.js │ ├── actions.js │ ├── index.js │ └── reducer.js └── vote.js ├── composer.json ├── composer.lock ├── crowdsignal-forms.php ├── docker ├── .dockerignore ├── Dockerfile ├── README.md ├── bin │ ├── install.sh │ ├── install_composer.sh │ ├── multisite-convert.sh │ ├── run.sh │ ├── tail.sh │ └── uninstall.sh ├── config │ ├── apache_default │ ├── htaccess │ ├── htaccess-multi │ ├── php.ini │ ├── ssmtp.conf │ └── wp-tests-config.php ├── data │ └── mysql │ │ └── .gitkeep ├── default.env ├── docker-compose.yml ├── logs │ └── .gitkeep ├── mu-plugins │ ├── .gitignore │ ├── 0-force-crowdsignal-forms-api-keys.php │ ├── 1-always-throw-cs-sync-errors.php │ ├── avoid-plugin-deletion.php │ └── debug.php ├── wordpress-develop │ └── .gitkeep └── wordpress │ └── .gitkeep ├── docs └── development-environment.md ├── images └── cs_dashboard_teaser.png ├── includes ├── admin │ ├── admin-styles.css │ ├── class-admin-hooks.php │ ├── class-crowdsignal-forms-admin-notices.php │ ├── class-crowdsignal-forms-admin.php │ ├── class-crowdsignal-forms-notice-icon.php │ ├── class-crowdsignal-forms-settings.php │ ├── class-crowdsignal-forms-setup.php │ ├── index.php │ └── views │ │ ├── html-admin-dashboard-teaser.php │ │ ├── html-admin-notice-core-setup.php │ │ ├── html-admin-settings.php │ │ ├── html-admin-setup-footer.php │ │ ├── html-admin-setup-header.php │ │ ├── html-admin-setup-step-1.php │ │ ├── html-admin-setup-step-2.php │ │ └── html-admin-setup-step-3.php ├── auth │ ├── class-api-auth-provider-interface.php │ ├── class-crowdsignal-forms-api-authenticator.php │ └── class-default-api-auth-provider.php ├── class-autoloader.php ├── class-crowdsignal-forms.php ├── frontend │ ├── blocks │ │ ├── class-crowdsignal-forms-applause-block.php │ │ ├── class-crowdsignal-forms-feedback-block.php │ │ ├── class-crowdsignal-forms-nps-block.php │ │ ├── class-crowdsignal-forms-poll-block.php │ │ ├── class-crowdsignal-forms-vote-block.php │ │ └── class-crowdsignal-forms-vote-item-block.php │ ├── class-crowdsignal-forms-block.php │ ├── class-crowdsignal-forms-blocks-assets.php │ ├── class-crowdsignal-forms-blocks.php │ └── index.php ├── gateways │ ├── class-api-gateway-interface.php │ ├── class-api-gateway.php │ ├── class-canned-api-gateway.php │ ├── class-post-poll-meta-gateway.php │ └── index.php ├── index.php ├── logging │ └── class-webservice-logger.php ├── models │ ├── class-feedback-survey.php │ ├── class-nps-survey.php │ ├── class-poll-answer.php │ ├── class-poll-settings.php │ └── class-poll.php ├── rest-api │ └── controllers │ │ ├── class-account-controller.php │ │ ├── class-feedback-controller.php │ │ ├── class-nps-controller.php │ │ ├── class-polls-controller.php │ │ └── index.php └── synchronization │ ├── class-comment-sync-entity.php │ ├── class-poll-block-synchronizer.php │ ├── class-post-sync-entity.php │ └── class-synchronizable-entity.php ├── index.php ├── languages └── crowdsignal-forms.pot ├── package.json ├── phpcs.xml.dist ├── phpunit.xml ├── pnpm-lock.yaml ├── scripts ├── makepot.sh ├── package-for-release.sh └── prepare-release.sh ├── tests-js └── mocks │ ├── blocks.js │ └── i18n.js ├── tests ├── .keep ├── README.md ├── bin │ └── install.sh ├── bootstrap.php ├── canned-data │ ├── api-data.json │ └── block-data-empty.json ├── framework │ └── class-crowdsignal-forms-unit-test-case.php └── unit-tests │ └── includes │ ├── admin │ └── test-class.admin-hooks.php │ ├── auth │ └── test-class.crowdsignal-forms-api-auth-test.php │ ├── gateways │ ├── test-class.api-gateway-interface.php │ ├── test-class.api-gateway.php │ └── test-class.canned-api-gateway.php │ ├── models │ ├── test-class.poll-settings.php │ └── test-class.poll.php │ └── rest-api │ └── controllers │ ├── test-class.account-controller.php │ └── test-class.polls-controller.php ├── uninstall.php └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.yml] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.txt] 25 | end_of_line = crlf 26 | 27 | [Makefile] 28 | indent_style = tab 29 | indent_size = 4 30 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "plugin:@wordpress/eslint-plugin/recommended" ], 3 | "rules": { 4 | "@wordpress/i18n-text-domain": [ 5 | "error", 6 | { 7 | "allowedTextDomain": "crowdsignal-forms" 8 | } 9 | ] 10 | }, 11 | "settings": { 12 | "import/resolver": { 13 | "webpack": {} 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .idea 3 | *.code-workspace 4 | vendor 5 | .env 6 | node_modules 7 | build 8 | release 9 | ## docker stuff 10 | /docker/data/mysql/* 11 | !/docker/data/mysql/.gitkeep 12 | /docker/logs/* 13 | !/docker/logs/.gitkeep 14 | # Custom environment for docker containers 15 | /docker/.env 16 | /docker/wordpress-develop/* 17 | !/docker/wordpress-develop/.gitkeep 18 | /docker/wordpress/* 19 | !/docker/wordpress/.gitkeep 20 | 21 | # 22 | 23 | # General 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must end with two \r 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .com.apple.timemachine.donotpresent 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | 51 | # 52 | 53 | 54 | # 55 | 56 | *~ 57 | 58 | # temporary files which can be created if a process still has a handle open of a deleted file 59 | .fuse_hidden* 60 | 61 | # KDE directory preferences 62 | .directory 63 | 64 | # Linux trash folder which might appear on any partition or disk 65 | .Trash-* 66 | 67 | # .nfs files are created when an open file is removed but is still being accessed 68 | .nfs* 69 | 70 | 71 | # 72 | 73 | 74 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.13.0 2 | // v14.15.0 3 | // maybe install with 12.13.1 but run with 14.15.0 4 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-wordpress/scss" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: bionic 3 | 4 | language: php 5 | php: 7.4 6 | node_js: 12.13.1 7 | 8 | branches: 9 | only: 10 | - master 11 | - /^release\/.*/ 12 | - /^feature\/.*/ 13 | 14 | cache: 15 | directories: 16 | - build 17 | - node_modules 18 | - vendor 19 | 20 | install: 21 | - composer install 22 | - npm install 23 | 24 | before_script: 25 | - bash tests/bin/install.sh crowdsignal_forms_tests root '' localhost latest true 26 | 27 | jobs: 28 | include: 29 | - stage: lint 30 | script: vendor/bin/phpcs --ignore="*/docker/*" . 31 | - stage: lint 32 | script: npm run lint:js 33 | - stage: lint 34 | script: npm run lint:styles 35 | - stage: test 36 | # script: vendor/bin/phpunit 37 | # - stage: test 38 | script: npm run test 39 | - stage: build 40 | script: npm run build:editor 41 | - stage: build 42 | script: npm run build:styles 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Set up and build the entire project 3 | all: install client 4 | 5 | # Install all project dependencies 6 | install: install-node install-php 7 | 8 | # Install Node dependencies 9 | install-node: 10 | pnpm install 11 | 12 | # Install PHP dependencies 13 | install-php: 14 | composer install 15 | 16 | # Build the frontend client 17 | client: 18 | pnpm build 19 | 20 | # Package for release 21 | release: clean-release client pot 22 | ./scripts/package-for-release.sh 23 | 24 | # Clean the build directory 25 | clean: 26 | rm -rf build 27 | 28 | clean-release: clean 29 | rm -rf release 30 | 31 | docker_build: 32 | docker-compose -f docker/docker-compose.yml build 33 | 34 | docker_up: 35 | docker-compose -f docker/docker-compose.yml up 36 | 37 | docker_up_d: 38 | docker-compose -f docker/docker-compose.yml up -d 39 | 40 | docker_stop: 41 | docker-compose -f docker/docker-compose.yml stop 42 | 43 | docker_down: 44 | docker-compose -f docker/docker-compose.yml down 45 | 46 | docker_sh: 47 | docker-compose -f docker/docker-compose.yml exec wordpress bash 48 | 49 | docker_sh_db: 50 | docker-compose -f docker/docker-compose.yml exec db bash 51 | 52 | docker_install: 53 | docker-compose -f docker/docker-compose.yml exec wordpress bash -c "/var/scripts/install.sh" 54 | 55 | docker_uninstall: 56 | docker-compose -f docker/docker-compose.yml exec wordpress bash -c "/var/scripts/uninstall.sh" 57 | 58 | phpunit: 59 | docker-compose -f docker/docker-compose.yml exec wordpress bash -c "cd /var/www/html/wp-content/plugins/crowdsignal-forms && WP_TESTS_DIR=/tmp/wordpress-develop/tests/phpunit ./vendor/bin/phpunit" 60 | 61 | phpcs: 62 | docker-compose -f docker/docker-compose.yml exec wordpress bash -c "cd /var/www/html/wp-content/plugins/crowdsignal-forms && ./vendor/bin/phpcs" 63 | 64 | phpcbf: 65 | docker-compose -f docker/docker-compose.yml exec wordpress bash -c "cd /var/www/html/wp-content/plugins/crowdsignal-forms && ./vendor/bin/phpcbf" 66 | 67 | composer: 68 | docker-compose -f docker/docker-compose.yml exec wordpress bash -c "cd /var/www/html/wp-content/plugins/crowdsignal-forms && composer install" 69 | 70 | pot: 71 | ./scripts/makepot.sh 72 | 73 | .PHONY: install install-node install-php client clean clean-release docker_up_d docker_build docker_up docker_down docker_stop docker_sh docker_install docker_uninstall phpunit phpcs phpcbf composer release pot 74 | -------------------------------------------------------------------------------- /assets/banner-1544x500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/assets/banner-1544x500.jpg -------------------------------------------------------------------------------- /assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/assets/banner-1544x500.png -------------------------------------------------------------------------------- /assets/banner-772x250.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/assets/banner-772x250.jpg -------------------------------------------------------------------------------- /assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/assets/banner-772x250.png -------------------------------------------------------------------------------- /assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/assets/icon-256x256.png -------------------------------------------------------------------------------- /assets/screenshot-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/assets/screenshot-1.gif -------------------------------------------------------------------------------- /assets/screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/assets/screenshot-2.jpg -------------------------------------------------------------------------------- /assets/screenshot-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/assets/screenshot-3.gif -------------------------------------------------------------------------------- /assets/screenshot-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/assets/screenshot-4.jpg -------------------------------------------------------------------------------- /assets/stylesheets/applause.scss: -------------------------------------------------------------------------------- 1 | // Shared 2 | @import "./shared/animations.scss"; 3 | @import "./shared/variables.scss"; 4 | 5 | // Components 6 | @import "components/applause/style.scss"; 7 | @import "components/brand-link/style.scss"; 8 | -------------------------------------------------------------------------------- /assets/stylesheets/editor.scss: -------------------------------------------------------------------------------- 1 | // Shared 2 | @import "./shared/variables.scss"; 3 | 4 | // Components 5 | @import "components/block-alignment-control/style.scss"; 6 | @import "components/connect-to-crowdsignal/style.scss"; 7 | @import "components/editor-notice/style.scss"; 8 | @import "components/sidebar-promote/style.scss"; 9 | @import "components/footer-branding/style.scss"; 10 | 11 | // Poll 12 | @import "blocks/poll/edit.scss"; 13 | @import "components/poll/style.scss"; 14 | @import "components/with-fallback-styles/style.scss"; 15 | 16 | // Vote 17 | @import "blocks/vote/edit.scss"; 18 | @import "blocks/vote-item/edit.scss"; 19 | @import "components/vote/style.scss"; 20 | 21 | // Applause 22 | @import "blocks/applause/edit.scss"; 23 | @import "components/applause/style.scss"; 24 | 25 | // NPS 26 | @import "blocks/nps/edit.scss"; 27 | @import "components/nps/style.scss"; 28 | 29 | // Feedback 30 | @import "blocks/feedback/edit.scss"; 31 | @import "components/feedback/style.scss"; 32 | 33 | // Embed 34 | @import "blocks/cs-embed/edit.scss" -------------------------------------------------------------------------------- /assets/stylesheets/feedback.scss: -------------------------------------------------------------------------------- 1 | // Shared 2 | @import "./shared/animations.scss"; 3 | @import "./shared/variables.scss"; 4 | 5 | // Components 6 | @import "components/feedback/style.scss"; 7 | @import "components/footer-branding/style.scss"; 8 | -------------------------------------------------------------------------------- /assets/stylesheets/nps.scss: -------------------------------------------------------------------------------- 1 | // Shared 2 | @import "./shared/animations.scss"; 3 | @import "./shared/variables.scss"; 4 | 5 | // Components 6 | @import "components/dialog-wrapper/style.scss"; 7 | @import "components/nps/style.scss"; 8 | @import "components/footer-branding/style.scss"; 9 | -------------------------------------------------------------------------------- /assets/stylesheets/poll.scss: -------------------------------------------------------------------------------- 1 | // Shared 2 | @import "./shared/animations.scss"; 3 | @import "./shared/variables.scss"; 4 | 5 | // Components 6 | @import "components/poll/style.scss"; 7 | @import "components/with-fallback-styles/style.scss"; 8 | @import "components/footer-branding/style.scss"; 9 | -------------------------------------------------------------------------------- /assets/stylesheets/shared/animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes crowdsignal-forms-animation__pop { 2 | 3 | 0% { 4 | transform: scale(0); 5 | } 6 | 7 | 50% { 8 | transform: scale(1.2); 9 | } 10 | 11 | 100% { 12 | transform: scale(1); 13 | } 14 | } 15 | 16 | @keyframes crowdsignal-forms-animation__pulse { 17 | 18 | 0% { 19 | opacity: 0.4; 20 | } 21 | 22 | 50% { 23 | opacity: 0.7; 24 | } 25 | 26 | 100% { 27 | opacity: 0.4; 28 | } 29 | } 30 | 31 | @keyframes crowdsignal-forms-animation__grow { 32 | 33 | 0% { 34 | transform: scale(1); 35 | } 36 | 37 | 50% { 38 | transform: scale(1.4); 39 | } 40 | 41 | 100% { 42 | transform: scale(1); 43 | } 44 | } 45 | 46 | @keyframes crowdsignal-forms-animation__fade-in { 47 | 48 | 0% { 49 | opacity: 0; 50 | } 51 | 52 | 100% { 53 | opacity: 1; 54 | } 55 | } 56 | 57 | @keyframes crowdsignal-forms-animation__fade-out { 58 | 59 | 0% { 60 | opacity: 1; 61 | } 62 | 63 | 100% { 64 | opacity: 0; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /assets/stylesheets/shared/variables.scss: -------------------------------------------------------------------------------- 1 | $font-sans-serif: -apple-system, blinkmacsystemfont, "Segoe UI", roboto, oxygen-sans, ubuntu, cantarell, "Helvetica Neue", sans-serif; 2 | $font-size-gutenberg-system-default: 13px; 3 | -------------------------------------------------------------------------------- /assets/stylesheets/vote.scss: -------------------------------------------------------------------------------- 1 | // Shared 2 | @import "./shared/animations.scss"; 3 | @import "./shared/variables.scss"; 4 | 5 | // Components 6 | @import "components/vote/style.scss"; 7 | @import "components/brand-link/style.scss"; 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ '@wordpress/default' ], 3 | }; 4 | -------------------------------------------------------------------------------- /client/apifetch/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { findIndex, forEach } from 'lodash'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { defaultHeaders, formatURL, formatRequest, wpAuth } from './middleware'; 10 | 11 | /** 12 | * Middlewares to be applied to the apiFetch call. 13 | */ 14 | const middlewareRegistry = []; 15 | 16 | /** 17 | * Applies all middlewares and runs the request. 18 | * 19 | * @param {Object} options Passed as the second param for window.fetch 20 | * @param {string} options.path Request URL 21 | * @param {Function} apply The middleware to apply next 22 | * @return {Promise} Request promise 23 | */ 24 | const run = async ( { path, ...options }, [ apply, ...middlewares ] ) => { 25 | if ( ! apply ) { 26 | const response = await window.fetch( path, options ); 27 | 28 | return response.json(); 29 | } 30 | 31 | return apply( 32 | { 33 | path, 34 | ...options, 35 | }, 36 | ( nextOptions ) => run( nextOptions, middlewares ) 37 | ); 38 | }; 39 | 40 | /** 41 | * Makes a request using window.fetch and registered middleware. 42 | * 43 | * @param {Object} options Request options 44 | * @param {string} options.path Request URL (required) 45 | * @return {Promise} Request promise 46 | */ 47 | const apiFetch = async ( options ) => run( options, middlewareRegistry ); 48 | 49 | /** 50 | * Appends a middleware to apiFetch 51 | * 52 | * @param {Function} middleware Middleware function 53 | */ 54 | apiFetch.use = ( middleware ) => middlewareRegistry.push( middleware ); 55 | 56 | /** 57 | * Removes a middleware from apiFetch 58 | * 59 | * @param {Function} middleware The middleware to remove 60 | */ 61 | apiFetch.disable = ( middleware ) => { 62 | const index = findIndex( middlewareRegistry, ( m ) => m === middleware ); 63 | 64 | if ( index ) { 65 | middlewareRegistry.splice( index, 1 ); 66 | } 67 | }; 68 | 69 | /** 70 | * Export the default middlewares on the apiFetch object 71 | * 72 | * @type {Object} 73 | */ 74 | apiFetch.middleware = { 75 | defaultHeaders, 76 | formatURL, 77 | formatRequest, 78 | wpAuth, 79 | }; 80 | 81 | /** 82 | * Apply the default middlewares 83 | */ 84 | forEach( apiFetch.middleware, apiFetch.use ); 85 | 86 | export default apiFetch; 87 | -------------------------------------------------------------------------------- /client/apifetch/middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prefixes the request URLs with '/wp-json' 3 | * 4 | * @param {Object} options Request options 5 | * @param {Function} next Next middleware 6 | * @return {Promise} Request promsie 7 | */ 8 | export const formatURL = ( options, next ) => { 9 | if ( options.path.indexOf( '/crowdsignal-forms/v1' ) === 0 ) { 10 | options.path = _crowdsignalFormsURL + `/wp-json${ options.path }`; 11 | } 12 | 13 | return next( options ); 14 | }; 15 | 16 | /** 17 | * Set default headers 18 | * 19 | * @param {Object} options Request options 20 | * @param {Function} next Next middleware 21 | * @return {Promise} Request promsie 22 | */ 23 | export const defaultHeaders = ( options, next ) => { 24 | const headers = options.headers || {}; 25 | 26 | return next( { 27 | ...options, 28 | headers: { 29 | ...headers, 30 | // The backend uses the Accept header as a condition for considering an 31 | // incoming request as a REST request. 32 | // 33 | // See: https://core.trac.wordpress.org/ticket/44534 34 | Accept: 'application/json, */*;q=0.1', 35 | }, 36 | } ); 37 | }; 38 | 39 | /** 40 | * Convert data to JSON 41 | * 42 | * @param {Object} options Request options 43 | * @param {Object} options.data Request data 44 | * @param {Function} next Next middleware 45 | * @return {Promise} Request promsie 46 | */ 47 | export const formatRequest = ( { data, ...options }, next ) => { 48 | if ( ! data ) { 49 | return next( options ); 50 | } 51 | 52 | return next( { 53 | ...options, 54 | headers: { 55 | ...options.headers, 56 | 'Content-Type': 'application/json', 57 | }, 58 | body: JSON.stringify( data ), 59 | } ); 60 | }; 61 | 62 | /** 63 | * Auth middleware. 64 | * Detects whether the current user is logged in and if so, include credentials in the request. 65 | * 66 | * @param {Object} options Request options 67 | * @param {Function} next Next middleware 68 | * @return {Promise} Request promsie 69 | */ 70 | export const wpAuth = ( options, next ) => { 71 | if ( ! window._crowdsignalFormsWpNonce ) { 72 | return next( options ); 73 | } 74 | 75 | return next( { 76 | credentials: 'same-origin', 77 | mode: 'same-origin', 78 | ...options, 79 | headers: { 80 | 'X-WP-Nonce': window._crowdsignalFormsWpNonce, 81 | ...options.headers, 82 | }, 83 | } ); 84 | }; 85 | -------------------------------------------------------------------------------- /client/applause.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import Applause from 'components/applause'; 10 | import MutationObserver from 'lib/mutation-observer'; 11 | 12 | MutationObserver( 'data-crowdsignal-applause', ( attributes ) => ( 13 | 14 | ) ); 15 | -------------------------------------------------------------------------------- /client/blocks/applause/attributes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Note: Any changes made to the attributes definition need to be duplicated in 3 | * Crowdsignal_Forms\Frontend\Blocks\Crowdsignal_Forms_Applause_Block::attributes() 4 | * inside includes/frontend/blocks/class-crowdsignal-forms-applause-block.php. 5 | */ 6 | 7 | import { PollStatus } from './constants'; 8 | 9 | export default { 10 | pollId: { 11 | type: 'string', 12 | default: null, 13 | }, 14 | hideBranding: { 15 | type: 'boolean', 16 | default: false, 17 | }, 18 | title: { 19 | type: 'string', 20 | default: null, 21 | }, 22 | answerId: { 23 | type: 'string', 24 | default: null, 25 | }, 26 | size: { 27 | type: 'string', 28 | default: 'medium', 29 | }, 30 | pollStatus: { 31 | type: 'string', 32 | default: PollStatus.OPEN, 33 | }, 34 | closedAfterDateTime: { 35 | type: 'string', 36 | default: null, 37 | }, 38 | textColor: { 39 | type: 'string', 40 | }, 41 | backgroundColor: { 42 | type: 'string', 43 | }, 44 | borderColor: { 45 | type: 'string', 46 | }, 47 | borderWidth: { 48 | type: 'number', 49 | default: 0, 50 | }, 51 | borderRadius: { 52 | type: 'number', 53 | default: 0, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /client/blocks/applause/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | export const PollStatus = Object.freeze( { 7 | OPEN: 'open', 8 | CLOSED: 'closed', 9 | CLOSED_AFTER: 'closed-after', 10 | } ); 11 | 12 | export const DEFAULT_SIZE_CONTROLS = [ 13 | { 14 | title: __( 'Small', 'crowdsignal-forms' ), 15 | size: 'small', 16 | }, 17 | { 18 | title: __( 'Medium', 'crowdsignal-forms' ), 19 | size: 'medium', 20 | }, 21 | { 22 | title: __( 'Large', 'crowdsignal-forms' ), 23 | size: 'large', 24 | }, 25 | ]; 26 | 27 | export const POPOVER_PROPS = { 28 | position: 'bottom right', 29 | isAlternate: true, 30 | className: 'crowdsignal-forms-vote__size-dropdown', 31 | }; 32 | -------------------------------------------------------------------------------- /client/blocks/applause/edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import { get } from 'lodash'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import { compose } from '@wordpress/compose'; 11 | import { __ } from '@wordpress/i18n'; 12 | import { useSelect } from '@wordpress/data'; 13 | 14 | /** 15 | * Internal dependencies 16 | */ 17 | import ConnectToCrowdsignal from 'components/connect-to-crowdsignal'; 18 | import withClientId from 'components/with-client-id'; 19 | import useNumberedTitle from 'components/use-numbered-title'; 20 | import Applause from 'components/applause'; 21 | import withPollBase from 'components/with-poll-base'; 22 | import Toolbar from './toolbar'; 23 | import SideBar from './sidebar'; 24 | import { STORE_NAME } from 'state'; 25 | import withFseCheck from 'components/with-fse-check'; 26 | 27 | const EditApplauseBlock = ( props ) => { 28 | const { attributes, setAttributes, pollDataFromApi } = props; 29 | 30 | const viewResultsUrl = pollDataFromApi 31 | ? pollDataFromApi.viewResultsUrl 32 | : ''; 33 | 34 | useNumberedTitle( 35 | props.name, 36 | __( 'Untitled Applause', 'crowdsignal-forms' ), 37 | attributes, 38 | setAttributes 39 | ); 40 | 41 | const accountInfo = useSelect( ( select ) => 42 | select( STORE_NAME ).getAccountInfo() 43 | ); 44 | 45 | const shouldPromote = get( accountInfo, [ 46 | 'signalCount', 47 | 'shouldDisplay', 48 | ] ); 49 | const signalWarning = 50 | shouldPromote && 51 | get( accountInfo, [ 'signalCount', 'count' ] ) >= 52 | get( accountInfo, [ 'signalCount', 'userLimit' ] ); 53 | 54 | return ( 55 | 59 | 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | export default compose( [ 72 | withFseCheck, 73 | withPollBase, 74 | withClientId( [ 'pollId', 'answerId' ] ), 75 | ] )( EditApplauseBlock ); 76 | -------------------------------------------------------------------------------- /client/blocks/applause/edit.scss: -------------------------------------------------------------------------------- 1 | /* editor styles */ 2 | -------------------------------------------------------------------------------- /client/blocks/applause/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import ApplauseIcon from 'components/icon/applause'; 10 | import EditApplauseBlock from './edit'; 11 | import attributes from './attributes'; 12 | 13 | export default { 14 | title: __( 'Applause', 'crowdsignal-forms' ), 15 | description: __( 16 | 'Let your audience cheer with a big round of applause — powered by Crowdsignal.', 17 | 'crowdsignal-forms' 18 | ), 19 | category: 'crowdsignal-forms', 20 | keywords: [ 21 | 'crowdsignal', 22 | __( 'applause', 'crowdsignal-forms' ), 23 | __( 'cheer', 'crowdsignal-forms' ), 24 | __( 'cheering', 'crowdsignal-forms' ), 25 | __( 'clap', 'crowdsignal-forms' ), 26 | __( 'feedback', 'crowdsignal-forms' ), 27 | __( 'kudos', 'crowdsignal-forms' ), 28 | __( 'like', 'crowdsignal-forms' ), 29 | __( 'opinion', 'crowdsignal-forms' ), 30 | __( 'praise', 'crowdsignal-forms' ), 31 | __( 'rating', 'crowdsignal-forms' ), 32 | __( 'upvote', 'crowdsignal-forms' ), 33 | __( 'upvoting', 'crowdsignal-forms' ), 34 | __( 'votes', 'crowdsignal-forms' ), 35 | __( 'voting', 'crowdsignal-forms' ), 36 | ], 37 | icon: , 38 | edit: EditApplauseBlock, 39 | attributes, 40 | usesContext: [ 'postId', 'queryId' ], 41 | example: { 42 | attributes: { 43 | size: 'large', 44 | }, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /client/blocks/applause/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { isEmpty, kebabCase, mapKeys } from 'lodash'; 10 | 11 | export const getApplauseStyleVars = ( attributes, fallbackStyles ) => { 12 | const textColor = isEmpty( attributes.textColor ) 13 | ? fallbackStyles.textColor 14 | : attributes.textColor; 15 | 16 | return mapKeys( 17 | { 18 | bgColor: 19 | attributes.backgroundColor || fallbackStyles.backgroundColor, 20 | textColor, 21 | hoverColor: fallbackStyles.accentColor, 22 | borderRadius: `${ attributes.borderRadius || 0 }px`, 23 | borderWidth: `${ attributes.borderWidth || 0 }px`, 24 | borderColor: attributes.borderColor, 25 | }, 26 | ( _, key ) => `--crowdsignal-forms-applause-${ kebabCase( key ) }` 27 | ); 28 | }; 29 | 30 | /** 31 | * Returns a css 'class' string of overridden styles given a collection of attributes. 32 | * 33 | * @param {*} attributes The block's attributes 34 | * @param {...any} extraClasses A list of additional classes to add to the class string 35 | */ 36 | export const getBlockCssClasses = ( attributes, ...extraClasses ) => { 37 | return classNames( 38 | { 39 | 'has-bg-color': attributes.backgroundColor, 40 | 'has-text-color': attributes.textColor, 41 | 'has-border-color': attributes.borderColor, 42 | }, 43 | extraClasses 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /client/blocks/cs-embed/attributes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { createInterpolateElement } from '@wordpress/element'; 6 | 7 | export default { 8 | url: { 9 | type: 'string', 10 | }, 11 | caption: { 12 | type: 'string', 13 | source: 'html', 14 | selector: 'figcaption', 15 | }, 16 | type: { 17 | type: 'string', 18 | default: 'html', 19 | }, 20 | providerNameSlug: { 21 | type: 'string', 22 | default: 'crowdsignal', 23 | }, 24 | allowResponsive: { 25 | type: 'boolean', 26 | default: true, 27 | }, 28 | responsive: { 29 | type: 'boolean', 30 | default: false, 31 | }, 32 | previewable: { 33 | type: 'boolean', 34 | default: true, 35 | }, 36 | createLink: { 37 | type: 'string', 38 | default: 39 | 'https://crowdsignal.com/support/add-a-multipage-survey-to-any-wordpress-page-or-post/?ref=surveyembedblock', 40 | }, 41 | createText: { 42 | type: 'string', 43 | default: __( 'Create a new Survey', 'crowdsignal-forms' ), 44 | }, 45 | typeText: { 46 | type: 'string', 47 | default: __( 'survey', 'crowdsignal-forms' ), 48 | }, 49 | editText: { 50 | type: 'string', 51 | default: createInterpolateElement( 52 | __( 53 | 'Edit your surveys on crowdsignal.com', 54 | 'crowdsignal-forms' 55 | ), 56 | { 57 | a: ( 58 | // eslint-disable-next-line jsx-a11y/anchor-has-content 59 | 64 | ), 65 | } 66 | ), 67 | }, 68 | dashboardLink: { 69 | type: 'string', 70 | default: 'https://app.crowdsignal.com/?ref=surveyembedblock', 71 | }, 72 | embedMessage: { 73 | type: 'string', 74 | default: __( 75 | 'Paste a link to the survey you want to display on your site', 76 | 'crowdsignal-forms' 77 | ), 78 | }, 79 | placeholderTitle: { 80 | type: 'string', 81 | default: __( 'Survey Embed', 'crowdsignal-forms' ), 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /client/blocks/cs-embed/cs-domains.js: -------------------------------------------------------------------------------- 1 | // an array of domains that are valid for CS embeds 2 | export default [ 'crowdsignal.com', 'survey.fm', 'crowdsignal.net' ]; 3 | -------------------------------------------------------------------------------- /client/blocks/cs-embed/edit.scss: -------------------------------------------------------------------------------- 1 | /* Editor styles */ 2 | 3 | #editor .editor-styles-wrapper { 4 | 5 | .crowdsignal-logo { 6 | height: 40px; 7 | width: 40px; 8 | } 9 | 10 | .cs-embed__button { 11 | margin-left: 8px; 12 | } 13 | 14 | .cs-embed__field { 15 | width: 85%; 16 | } 17 | 18 | .cs-embed__error { 19 | color: red; 20 | padding-left: 4px; 21 | } 22 | 23 | .cs-embed__instructions { 24 | padding-bottom: 16px; 25 | padding-left: 4px; 26 | } 27 | 28 | .cs-embed__create-link { 29 | padding-top: 16px; 30 | padding-left: 4px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/blocks/cs-embed/embed-loading.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Spinner } from '@wordpress/components'; 5 | 6 | const EmbedLoading = () => ( 7 |
8 | 9 |
10 | ); 11 | 12 | export default EmbedLoading; 13 | -------------------------------------------------------------------------------- /client/blocks/cs-embed/embed-preview.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { SandBox } from '@wordpress/components'; 5 | import { useBlockProps } from '@wordpress/block-editor'; 6 | 7 | export default function EmbedPreview( { html } ) { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /client/blocks/cs-embed/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import Survey from '../../components/icon/survey'; 10 | import EditEmbedBlock from './edit'; 11 | import SaveEmbedBlock from './save'; 12 | import attributes from './attributes'; 13 | import variations from './variations'; 14 | 15 | export default { 16 | title: __( 'Survey', 'crowdsignal-forms' ), 17 | description: __( 18 | 'Create a multipage survey on crowdsignal.com and embed it.', 19 | 'crowdsignal-forms' 20 | ), 21 | category: 'crowdsignal-forms', 22 | keywords: [ __( 'survey', 'crowdsignal-forms' ) ], 23 | icon: , 24 | edit: EditEmbedBlock, 25 | save: SaveEmbedBlock, 26 | variations, 27 | attributes, 28 | supports: { 29 | align: [ 'center', 'wide', 'full' ], 30 | }, 31 | getEditWrapperProps: ( { align } ) => ( { 32 | 'data-align': align, 33 | } ), 34 | }; 35 | -------------------------------------------------------------------------------- /client/blocks/cs-embed/save.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classnames from 'classnames/dedupe'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { useBlockProps } from '@wordpress/block-editor'; 10 | 11 | export default function save( { attributes } ) { 12 | const { url, type, providerNameSlug } = attributes; 13 | if ( ! url ) { 14 | return null; 15 | } 16 | const className = classnames( 'wp-block-embed', { 17 | [ `is-type-${ type }` ]: type, 18 | [ `is-provider-${ providerNameSlug }` ]: providerNameSlug, 19 | [ `wp-block-embed-${ providerNameSlug }` ]: providerNameSlug, 20 | } ); 21 | return ( 22 |
23 |
24 | { `\n${ url }\n` /* URL needs to be on its own line. */ } 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /client/blocks/cs-embed/sidebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { Button, PanelBody, PanelRow } from '@wordpress/components'; 10 | import { InspectorControls } from '@wordpress/block-editor'; 11 | import { __ } from '@wordpress/i18n'; 12 | import { createInterpolateElement } from '@wordpress/element'; 13 | 14 | /** 15 | * Internal dependencies 16 | */ 17 | import SidebarPromote from 'components/sidebar-promote'; 18 | 19 | const Sidebar = ( { attributes, shouldPromote, signalWarning } ) => { 20 | const { editText, createText, dashboardLink } = attributes; 21 | return ( 22 | 23 | 27 |
{ editText }
28 | 29 |
29 | 30 | ); 31 | }; 32 | 33 | export default EditBar; 34 | -------------------------------------------------------------------------------- /client/blocks/poll/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import PollIcon from 'components/icon/poll'; 10 | import 'state'; 11 | import EditPollBlock from './edit'; 12 | import attributes from './attributes'; 13 | 14 | export default { 15 | title: __( 'Poll', 'crowdsignal-forms' ), 16 | description: __( 17 | 'Create polls and get your audience’s opinion — powered by Crowdsignal.', 18 | 'crowdsignal-forms' 19 | ), 20 | category: 'crowdsignal-forms', 21 | keywords: [ 22 | __( 'ask', 'crowdsignal-forms' ), 23 | 'crowdsignal', 24 | __( 'feedback', 'crowdsignal-forms' ), 25 | __( 'form', 'crowdsignal-forms' ), 26 | __( 'opinion', 'crowdsignal-forms' ), 27 | __( 'poll', 'crowdsignal-forms' ), 28 | __( 'pop', 'crowdsignal-forms' ), 29 | __( 'question', 'crowdsignal-forms' ), 30 | __( 'quiz', 'crowdsignal-forms' ), 31 | __( 'research', 'crowdsignal-forms' ), 32 | __( 'survey', 'crowdsignal-forms' ), 33 | __( 'vote', 'crowdsignal-forms' ), 34 | ], 35 | icon: , 36 | edit: EditPollBlock, 37 | attributes, 38 | usesContext: [ 'postId', 'queryId' ], 39 | supports: { 40 | align: [ 'center', 'wide', 'full' ], 41 | }, 42 | getEditWrapperProps: ( { align } ) => ( { 43 | 'data-align': align, 44 | } ), 45 | example: { 46 | attributes: { 47 | question: __( 'How did you hear about us?', 'crowdsignal-forms' ), 48 | answers: [ 49 | { 50 | text: __( 'Search', 'crowdsignal-forms' ), 51 | }, 52 | { 53 | text: __( 'Friend', 'crowdsignal-forms' ), 54 | }, 55 | { 56 | text: __( 'Email', 'crowdsignal-forms' ), 57 | }, 58 | ], 59 | }, 60 | }, 61 | styles: [ 62 | { 63 | name: 'default', 64 | label: __( 'List', 'crowdsignal-forms' ), 65 | isDefault: true, 66 | }, 67 | { 68 | name: 'buttons', 69 | label: __( 'Buttons', 'crowdsignal-forms' ), 70 | }, 71 | ], 72 | variations: [ 73 | { 74 | isDefault: true, 75 | attributes: { 76 | // Force the correct className onto the block by default 77 | className: 'is-style-buttons', 78 | }, 79 | }, 80 | ], 81 | }; 82 | -------------------------------------------------------------------------------- /client/blocks/poll/toolbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import { map } from 'lodash'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import { BlockControls } from '@wordpress/block-editor'; 11 | import { Toolbar } from '@wordpress/components'; 12 | import { __ } from '@wordpress/i18n'; 13 | 14 | /** 15 | * Internal dependencies 16 | */ 17 | import ChecklistMultipleChoiceIcon from 'components/icon/checklist-multiple-choice'; 18 | import ChecklistSingleChoiceIcon from 'components/icon/checklist-single-choice'; 19 | import { toggleButtonStyleAvailability } from './util'; 20 | 21 | const multipleChoiceControls = [ 22 | { 23 | icon: ChecklistSingleChoiceIcon, 24 | title: __( 'Choose one answer', 'crowdsignal-forms' ), 25 | value: false, 26 | }, 27 | { 28 | icon: ChecklistMultipleChoiceIcon, 29 | title: __( 'Choose multiple answers', 'crowdsignal-forms' ), 30 | value: true, 31 | }, 32 | ]; 33 | 34 | const PollToolbar = ( { attributes, setAttributes } ) => { 35 | const multipleChoiceToolbar = map( multipleChoiceControls, ( button ) => ( { 36 | ...button, 37 | isActive: button.value === attributes.isMultipleChoice, 38 | onClick: () => { 39 | setAttributes( { isMultipleChoice: button.value } ); 40 | 41 | toggleButtonStyleAvailability( button.value ); 42 | }, 43 | } ) ); 44 | 45 | return ( 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default PollToolbar; 53 | -------------------------------------------------------------------------------- /client/blocks/vote-item/attributes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Note: Any changes made to the attributes definition need to be duplicated in 3 | * Crowdsignal_Forms\Frontend\Blocks\Crowdsignal_Forms_Vote_Item_Block::attributes() 4 | * inside includes/frontend/blocks/class-crowdsignal-forms-vote-item-block.php. 5 | */ 6 | export default { 7 | answerId: { 8 | type: 'string', 9 | default: null, 10 | }, 11 | type: { 12 | type: 'string', 13 | }, 14 | textColor: { 15 | type: 'string', 16 | }, 17 | backgroundColor: { 18 | type: 'string', 19 | }, 20 | borderColor: { 21 | type: 'string', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /client/blocks/vote-item/edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { compose } from '@wordpress/compose'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | import SideBar from './sidebar'; 15 | import withClientId from 'components/with-client-id'; 16 | import VoteItem from 'components/vote/vote-item'; 17 | import { withFallbackStyles } from 'components/with-fallback-styles'; 18 | 19 | const EditVoteItemBlock = ( props ) => { 20 | const { attributes, className, fallbackStyles, renderStyleProbe } = props; 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 34 | 35 | { renderStyleProbe() } 36 | 37 | ); 38 | }; 39 | 40 | export default compose( [ 41 | withFallbackStyles, 42 | withClientId( [ 'answerId' ] ), 43 | ] )( EditVoteItemBlock ); 44 | -------------------------------------------------------------------------------- /client/blocks/vote-item/edit.scss: -------------------------------------------------------------------------------- 1 | /* editor styles */ 2 | 3 | [data-type="crowdsignal-forms/vote-item"] { 4 | 5 | /* Gutenberg 9 fix for vertically overridden margins in a horizontal 6 | orientation block. */ 7 | margin-top: 28px !important; 8 | margin-bottom: 0 !important; 9 | 10 | &:not(:last-child) { 11 | margin-inline-end: 8px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/blocks/vote-item/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import VoteIcon from 'components/icon/vote'; 10 | import EditVoteItemBlock from './edit'; 11 | import attributes from './attributes'; 12 | 13 | export default { 14 | title: __( 'Vote Item', 'crowdsignal-forms' ), 15 | description: __( 16 | 'Allow your audience to rate your work or express their opinion — powered by Crowdsignal.', 17 | 'crowdsignal-forms' 18 | ), 19 | category: 'crowdsignal-forms', 20 | parent: [ 'crowdsignal-forms/vote' ], 21 | icon: , 22 | edit: EditVoteItemBlock, 23 | attributes, 24 | }; 25 | -------------------------------------------------------------------------------- /client/blocks/vote-item/sidebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { InspectorControls, PanelColorSettings } from '@wordpress/block-editor'; 10 | import { __ } from '@wordpress/i18n'; 11 | 12 | const SideBar = ( { attributes, setAttributes } ) => { 13 | const handleChangeTextColor = ( textColor ) => 14 | setAttributes( { textColor } ); 15 | 16 | const handleChangeBackgroundColor = ( backgroundColor ) => 17 | setAttributes( { backgroundColor } ); 18 | 19 | const handleChangeBorderColor = ( borderColor ) => 20 | setAttributes( { borderColor } ); 21 | return ( 22 | 23 | 44 | 45 | ); 46 | }; 47 | 48 | export default SideBar; 49 | -------------------------------------------------------------------------------- /client/blocks/vote/attributes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Note: Any changes made to the attributes definition need to be duplicated in 3 | * Crowdsignal_Forms\Frontend\Blocks\Crowdsignal_Forms_Vote_Block::attributes() 4 | * inside includes/frontend/blocks/class-crowdsignal-forms-vote-block.php. 5 | */ 6 | 7 | import { PollStatus } from './constants'; 8 | 9 | export default { 10 | pollId: { 11 | type: 'string', 12 | default: null, 13 | }, 14 | hideBranding: { 15 | type: 'boolean', 16 | default: false, 17 | }, 18 | title: { 19 | type: 'string', 20 | default: null, 21 | }, 22 | pollStatus: { 23 | type: 'string', 24 | default: PollStatus.OPEN, 25 | }, 26 | closedAfterDateTime: { 27 | type: 'string', 28 | default: null, 29 | }, 30 | size: { 31 | type: 'string', 32 | default: 'medium', 33 | }, 34 | borderWidth: { 35 | type: 'number', 36 | default: 1, 37 | }, 38 | borderRadius: { 39 | type: 'number', 40 | default: 5, 41 | }, 42 | hideResults: { 43 | type: 'boolean', 44 | default: false, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /client/blocks/vote/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | export const PollStatus = Object.freeze( { 7 | OPEN: 'open', 8 | CLOSED: 'closed', 9 | CLOSED_AFTER: 'closed-after', 10 | } ); 11 | 12 | export const ConnectedAccountState = Object.freeze( { 13 | CONNECTED: 'connected', 14 | NOT_CONNECTED: 'not-connected', 15 | NOT_VERIFIED: 'not-verified', 16 | } ); 17 | 18 | export const DEFAULT_SIZE_CONTROLS = [ 19 | { 20 | title: __( 'Small', 'crowdsignal-forms' ), 21 | size: 'small', 22 | }, 23 | { 24 | title: __( 'Medium', 'crowdsignal-forms' ), 25 | size: 'medium', 26 | }, 27 | { 28 | title: __( 'Large', 'crowdsignal-forms' ), 29 | size: 'large', 30 | }, 31 | ]; 32 | 33 | export const POPOVER_PROPS = { 34 | position: 'bottom right', 35 | isAlternate: true, 36 | className: 'crowdsignal-forms-vote__size-dropdown', 37 | }; 38 | -------------------------------------------------------------------------------- /client/blocks/vote/edit.scss: -------------------------------------------------------------------------------- 1 | /* editor styles */ 2 | 3 | .crowdsignal-forms-vote .block-editor-block-list__layout { 4 | display: flex; 5 | flex-direction: row; 6 | } 7 | 8 | .crowdsignal-forms__border-popover .crowdsignal-forms__row { 9 | padding: 10px; 10 | } 11 | 12 | .crowdsignal-forms-vote-item__count { 13 | 14 | .crowdsignal-forms-vote.no-results & { 15 | display: none; 16 | } 17 | } 18 | 19 | .crowdsignal-forms-vote__size-dropdown { 20 | 21 | .components-button.components-dropdown-menu__menu-item.is-active::after { 22 | content: "\2713"; 23 | margin-inline-start: auto; 24 | margin-inline-end: 0; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/blocks/vote/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { InnerBlocks } from '@wordpress/block-editor'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import VoteIcon from 'components/icon/vote'; 11 | import EditVoteBlock from './edit'; 12 | import attributes from './attributes'; 13 | 14 | export default { 15 | title: __( 'Vote', 'crowdsignal-forms' ), 16 | description: __( 17 | 'Allow your audience to rate your work or express their opinion — powered by Crowdsignal.', 18 | 'crowdsignal-forms' 19 | ), 20 | category: 'crowdsignal-forms', 21 | keywords: [ 22 | __( 'ballot', 'crowdsignal-forms' ), 23 | __( 'button', 'crowdsignal-forms' ), 24 | __( 'count', 'crowdsignal-forms' ), 25 | 'crowdsignal', 26 | __( 'deciding', 'crowdsignal-forms' ), 27 | __( 'decision', 'crowdsignal-forms' ), 28 | __( 'elect', 'crowdsignal-forms' ), 29 | __( 'election', 'crowdsignal-forms' ), 30 | __( 'feedback', 'crowdsignal-forms' ), 31 | __( 'form', 'crowdsignal-forms' ), 32 | __( 'like', 'crowdsignal-forms' ), 33 | __( 'nero', 'crowdsignal-forms' ), 34 | __( 'opinion', 'crowdsignal-forms' ), 35 | __( 'poll', 'crowdsignal-forms' ), 36 | __( 'polling', 'crowdsignal-forms' ), 37 | __( 'rate', 'crowdsignal-forms' ), 38 | __( 'rating', 'crowdsignal-forms' ), 39 | __( 'research', 'crowdsignal-forms' ), 40 | __( 'survey', 'crowdsignal-forms' ), 41 | __( 'thumb down', 'crowdsignal-forms' ), 42 | __( 'thumb up', 'crowdsignal-forms' ), 43 | __( 'thumbs', 'crowdsignal-forms' ), 44 | __( 'vote', 'crowdsignal-forms' ), 45 | __( 'voting', 'crowdsignal-forms' ), 46 | ], 47 | icon: , 48 | edit: EditVoteBlock, 49 | save: () => , 50 | attributes, 51 | usesContext: [ 'postId', 'queryId' ], 52 | example: { 53 | attributes: { 54 | className: 'crowdsignal-forms-vote__example', 55 | size: 'large', 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /client/blocks/vote/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import classNames from 'classnames'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { isEmpty, kebabCase, mapKeys } from 'lodash'; 10 | 11 | export const getVoteStyleVars = ( attributes ) => { 12 | return mapKeys( 13 | { 14 | borderRadius: `${ attributes.borderRadius }px`, 15 | borderWidth: `${ attributes.borderWidth }px`, 16 | }, 17 | ( _, key ) => `--crowdsignal-forms-vote-${ kebabCase( key ) }` 18 | ); 19 | }; 20 | 21 | export const getVoteItemStyleVars = ( attributes, fallbackStyles ) => { 22 | const textColor = isEmpty( attributes.textColor ) 23 | ? fallbackStyles.textColor 24 | : attributes.textColor; 25 | const backgroundColor = isEmpty( attributes.backgroundColor ) 26 | ? fallbackStyles.backgroundColor 27 | : attributes.backgroundColor; 28 | 29 | return mapKeys( 30 | { 31 | borderColor: attributes.borderColor, 32 | bgColor: backgroundColor, 33 | textColor, 34 | votedColor: fallbackStyles.accentColor, 35 | }, 36 | ( _, key ) => `--crowdsignal-forms-vote-${ kebabCase( key ) }` 37 | ); 38 | }; 39 | 40 | /** 41 | * Returns a css 'class' string of overridden styles given a collection of attributes. 42 | * 43 | * @param {*} attributes The block's attributes 44 | * @param {...any} extraClasses A list of additional classes to add to the class string 45 | */ 46 | export const getBlockCssClasses = ( attributes, ...extraClasses ) => { 47 | return classNames( 48 | { 49 | 'has-bg-color': attributes.backgroundColor, 50 | 'has-text-color': attributes.textColor, 51 | 'has-border-color': attributes.borderColor, 52 | }, 53 | extraClasses 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /client/components/block-alignment-control/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | export const GRID = { 7 | '2x2': { 8 | rows: [ 9 | { 10 | label: __( `Top`, 'crowdsignal-forms' ), 11 | value: 'top', 12 | }, 13 | { 14 | label: __( `Bottom`, 'crowdsignal-forms' ), 15 | value: 'bottom', 16 | }, 17 | ], 18 | columns: [ 19 | { 20 | label: __( `Left`, 'crowdsignal-forms' ), 21 | value: 'left', 22 | }, 23 | { 24 | label: __( `Right`, 'crowdsignal-forms' ), 25 | value: 'right', 26 | }, 27 | ], 28 | }, 29 | '2x3': { 30 | rows: [ 31 | { 32 | label: __( `Top`, 'crowdsignal-forms' ), 33 | value: 'top', 34 | }, 35 | { 36 | label: __( `Center`, 'crowdsignal-forms' ), 37 | value: 'center', 38 | }, 39 | { 40 | label: __( `Bottom`, 'crowdsignal-forms' ), 41 | value: 'bottom', 42 | }, 43 | ], 44 | columns: [ 45 | { 46 | label: __( `Left`, 'crowdsignal-forms' ), 47 | value: 'left', 48 | }, 49 | { 50 | label: __( `Right`, 'crowdsignal-forms' ), 51 | value: 'right', 52 | }, 53 | ], 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /client/components/block-alignment-control/grid-button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React, { useCallback } from 'react'; 5 | import { CompositeItem } from 'reakit'; 6 | import classnames from 'classnames'; 7 | 8 | /** 9 | * WordPress dependencies 10 | */ 11 | import { Tooltip, VisuallyHidden } from '@wordpress/components'; 12 | 13 | const BlockAlignmentControlGridButton = ( { 14 | isActive, 15 | column, 16 | onSelect, 17 | row, 18 | ...props 19 | } ) => { 20 | const label = `${ row.label } ${ column.label }`; 21 | 22 | const handleSelect = useCallback( () => { 23 | onSelect( row.value, column.value ); 24 | }, [ onSelect, row.value, column.value ] ); 25 | 26 | const classes = classnames( 27 | 'crowdsignal-forms__block-alignment-control-button', 28 | { 29 | 'is-active': isActive, 30 | } 31 | ); 32 | 33 | return ( 34 | 35 | 41 | { label } 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default BlockAlignmentControlGridButton; 48 | -------------------------------------------------------------------------------- /client/components/block-alignment-control/grid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React, { useEffect } from 'react'; 5 | import { Composite, CompositeGroup, useCompositeState } from 'reakit'; 6 | import { map } from 'lodash'; 7 | 8 | /** 9 | * WordPress dependencies 10 | */ 11 | import { useInstanceId } from '@wordpress/compose'; 12 | import { isRTL } from '@wordpress/i18n'; 13 | 14 | /** 15 | * Internal dependencies 16 | */ 17 | import GridButton from './grid-button'; 18 | 19 | const getButtonId = ( prefix, row, column ) => 20 | `${ prefix }-${ row }-${ column }`; 21 | 22 | function BlockAlignmentControlGrid( { columns, onChange, rows, value } ) { 23 | const baseId = useInstanceId( 24 | BlockAlignmentControlGrid, 25 | 'block-alignment-control-grid' 26 | ); 27 | 28 | const composite = useCompositeState( { 29 | baseId, 30 | currentId: getButtonId( baseId, value.row, value.column ), 31 | rtl: isRTL(), 32 | } ); 33 | 34 | useEffect( () => { 35 | composite.setCurrentId( 36 | getButtonId( baseId, value.row, value.column ) 37 | ); 38 | }, [ value, composite.setCurrentId ] ); 39 | 40 | return ( 41 | 45 | { map( rows, ( row ) => ( 46 | 52 | { map( columns, ( column ) => { 53 | const id = getButtonId( 54 | baseId, 55 | row.value, 56 | column.value 57 | ); 58 | const isActive = 59 | composite.currentId === 60 | getButtonId( baseId, row.value, column.value ); 61 | 62 | return ( 63 | 73 | ); 74 | } ) } 75 | 76 | ) ) } 77 | 78 | ); 79 | } 80 | 81 | export default BlockAlignmentControlGrid; 82 | -------------------------------------------------------------------------------- /client/components/block-alignment-control/icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import classnames from 'classnames'; 6 | import { map } from 'lodash'; 7 | 8 | const BlockAlignmentControlIcon = ( { rows, columns, value } ) => { 9 | let spanKeyNum = 0; 10 | let divKeyNum = 0; 11 | return ( 12 |
13 | { map( rows, ( row ) => ( 14 |
18 | { map( columns, ( column ) => { 19 | const isActive = 20 | row.value === value.row && 21 | column.value === value.column; 22 | 23 | const classes = classnames( 24 | 'crowdsignal-forms__block-alignment-control-icon-dot', 25 | { 26 | 'is-active': isActive, 27 | } 28 | ); 29 | 30 | return ( 31 | 32 | ); 33 | } ) } 34 |
35 | ) ) } 36 |
37 | ); 38 | }; 39 | 40 | export default BlockAlignmentControlIcon; 41 | -------------------------------------------------------------------------------- /client/components/block-alignment-control/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import { noop } from 'lodash'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import { ToolbarButton, Dropdown, Tooltip } from '@wordpress/components'; 11 | import { __ } from '@wordpress/i18n'; 12 | import { DOWN } from '@wordpress/keycodes'; 13 | 14 | /** 15 | * Internal dependencies 16 | */ 17 | import Grid from './grid'; 18 | import Icon from './icon'; 19 | 20 | const BlockAlignmentControl = ( { 21 | closeOnSelectionChanged, 22 | disabled, 23 | label, 24 | onChange, 25 | rows, 26 | columns, 27 | value, 28 | } ) => { 29 | const toolbarIcon = ( 30 | 31 | ); 32 | 33 | return ( 34 | { 40 | const openOnArrowDown = ( event ) => { 41 | if ( isOpen || event.keyCode !== DOWN ) { 42 | return; 43 | } 44 | 45 | event.preventDefault(); 46 | event.stopPropagation(); 47 | onToggle(); 48 | }; 49 | 50 | return ( 51 | 52 | 61 | 62 | ); 63 | } } 64 | renderContent={ ( { onClose } ) => { 65 | const handleChange = ( row, column ) => { 66 | onChange( row, column ); 67 | 68 | if ( 69 | closeOnSelectionChanged && 70 | ( value.row !== row || value.column !== column ) 71 | ) { 72 | onClose(); 73 | } 74 | }; 75 | 76 | return ( 77 | 83 | ); 84 | } } 85 | /> 86 | ); 87 | }; 88 | 89 | BlockAlignmentControl.defaultProps = { 90 | closeOnSelectionChanged: false, 91 | label: __( 'Change block position', 'crowdsignal-forms' ), 92 | onChange: noop, 93 | }; 94 | 95 | export default BlockAlignmentControl; 96 | 97 | export { GRID } from './constants'; 98 | -------------------------------------------------------------------------------- /client/components/block-alignment-control/style.scss: -------------------------------------------------------------------------------- 1 | .crowdsignal-forms__block-alignment-control-popover { 2 | 3 | .components-popover__content { 4 | min-width: auto !important; 5 | } 6 | } 7 | 8 | .crowdsignal-forms__block-alignment-control-grid { 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .crowdsignal-forms__block-alignment-control-row { 14 | display: flex; 15 | } 16 | 17 | .crowdsignal-forms__block-alignment-control-button { 18 | align-items: center; 19 | border: 0; 20 | background: transparent; 21 | cursor: pointer; 22 | display: flex; 23 | height: 30px; 24 | justify-content: center; 25 | width: 30px; 26 | 27 | &::before { 28 | background-color: #b5bcc2; 29 | display: block; 30 | content: ""; 31 | height: 6px; 32 | width: 6px; 33 | } 34 | 35 | &:hover::before { 36 | background-color: #007cba; 37 | } 38 | 39 | &.is-active::before { 40 | background-color: #000; 41 | box-shadow: #000 0 0 0 2px; 42 | } 43 | } 44 | 45 | .crowdsignal-forms__block-alignment-control-icon { 46 | display: flex; 47 | flex-direction: column; 48 | height: 24px; 49 | justify-content: space-between; 50 | width: 24px; 51 | } 52 | 53 | .crowdsignal-forms__block-alignment-control-icon-row { 54 | display: flex; 55 | justify-content: space-between; 56 | width: 100%; 57 | } 58 | 59 | .crowdsignal-forms__block-alignment-control-icon-dot { 60 | display: flex; 61 | padding: 2px; 62 | 63 | &::before { 64 | background-color: #000; 65 | content: ""; 66 | display: block; 67 | height: 2px; 68 | width: 2px; 69 | } 70 | 71 | &.is-active::before { 72 | box-shadow: #000 0 0 0 2px; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /client/components/brand-link/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import { __ } from '@wordpress/i18n'; 7 | 8 | const BrandLink = ( { showBranding, referralCode } ) => { 9 | return ( 10 |
11 | { showBranding && ( 12 | 18 | { __( 'Powered by Crowdsignal', 'crowdsignal-forms' ) } 19 | 20 | ) } 21 | { ! showBranding && ( 22 |   23 | ) } 24 |
25 | ); 26 | }; 27 | 28 | BrandLink.propTypes = { 29 | showBranding: PropTypes.bool, 30 | referralCode: PropTypes.string.isRequired, 31 | }; 32 | 33 | export default BrandLink; 34 | -------------------------------------------------------------------------------- /client/components/brand-link/style.scss: -------------------------------------------------------------------------------- 1 | .crowdsignal-forms__branding { 2 | display: flex; 3 | margin: 8px 4px 0; 4 | font-size: 8px; 5 | 6 | .crowdsignal-forms__branding-link { 7 | font-family: $font-sans-serif; 8 | text-decoration: none !important; 9 | text-transform: uppercase; 10 | box-shadow: none; 11 | border: 0; 12 | 13 | &:hover { 14 | box-shadow: none; 15 | } 16 | 17 | &.with-external-icon::after { 18 | content: "\2197"; 19 | display: inline; 20 | font-size: 6px; 21 | vertical-align: top; 22 | } 23 | 24 | &:not(:hover) { 25 | color: var(--crowdsignal-forms-text-color); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/components/connect-to-crowdsignal/style.scss: -------------------------------------------------------------------------------- 1 | .crowdsignal-forms__connect-to-crowdsignal { 2 | border: 1px solid rgb(0, 0, 0); 3 | font-family: $font-sans-serif; 4 | padding: 24px; 5 | text-align: initial; 6 | } 7 | 8 | .crowdsignal-forms__connect-to-crowdsignal-header { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | } 13 | 14 | .crowdsignal-forms__connect-to-crowdsignal-body { 15 | font-size: $font-size-gutenberg-system-default; 16 | margin-top: 24px; 17 | margin-bottom: 16px; 18 | } 19 | 20 | .crowdsignal-forms__connect-to-crowdsignal-title { 21 | font-size: 24pt; 22 | margin-inline-start: 16px; 23 | } 24 | -------------------------------------------------------------------------------- /client/components/dialog-wrapper/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React, { useRef } from 'react'; 5 | 6 | const DialogWrapper = ( { children, onClose } ) => { 7 | const wrapper = useRef( null ); 8 | 9 | const handleClose = ( event ) => 10 | event.target === wrapper.current && onClose(); 11 | 12 | return ( 13 | // eslint-disable-next-line 14 |
21 | { children } 22 |
23 | ); 24 | }; 25 | 26 | export default DialogWrapper; 27 | -------------------------------------------------------------------------------- /client/components/dialog-wrapper/style.scss: -------------------------------------------------------------------------------- 1 | .crowdsignal-forms-dialog-wrapper { 2 | align-items: center; 3 | background: rgba(0, 0, 0, 0.3); 4 | display: flex; 5 | justify-content: center; 6 | position: fixed; 7 | bottom: 0; 8 | left: 0; 9 | right: 0; 10 | top: 0; 11 | z-index: 10000; 12 | } 13 | -------------------------------------------------------------------------------- /client/components/editor-notice/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Notice, Icon } from '@wordpress/components'; 5 | 6 | const EditorNotice = ( { 7 | icon, 8 | children, 9 | componentActions = [], 10 | ...props 11 | } ) => { 12 | return ( 13 | 14 | { icon && ( 15 |
16 | { } 17 |
18 | ) } 19 |
20 | { children } 21 |
22 | { componentActions.map( ( component ) => component ) } 23 |
24 | ); 25 | }; 26 | 27 | export default EditorNotice; 28 | -------------------------------------------------------------------------------- /client/components/editor-notice/style.scss: -------------------------------------------------------------------------------- 1 | .crowdsignal-forms__editor-notice { 2 | margin: 0 0 15px !important; 3 | 4 | .components-notice__content { 5 | display: flex; 6 | flex-direction: row; 7 | align-items: center; 8 | } 9 | } 10 | 11 | .crowdsignal-forms__editor-notice-icon { 12 | line-height: 0; 13 | padding: 8px 16px 8px 8px; 14 | 15 | .is-warn & { 16 | color: var(--wp-admin-theme-color); 17 | } 18 | } 19 | 20 | .crowdsignal-forms__editor-notice-text { 21 | flex-grow: 1; 22 | color: var(--crowdsignal-forms-text-color); 23 | 24 | a { 25 | text-decoration: underline; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/components/feedback/popover.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React, { useRef, useState } from 'react'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { __ } from '@wordpress/i18n'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | import { views } from 'blocks/feedback/constants'; 15 | import FeedbackForm from './form'; 16 | import FeedbackSubmit from './submit'; 17 | import FooterBranding from 'components/footer-branding'; 18 | 19 | const FeedbackPopover = ( { attributes } ) => { 20 | const [ view, setView ] = useState( views.QUESTION ); 21 | const [ height, setHeight ] = useState( 'auto' ); 22 | 23 | const popover = useRef( null ); 24 | 25 | const handleSubmit = () => { 26 | setHeight( popover.current.offsetHeight ); 27 | setView( views.SUBMIT ); 28 | }; 29 | 30 | const styles = { 31 | height, 32 | }; 33 | 34 | return ( 35 |
40 | { view === views.QUESTION && ( 41 | 45 | ) } 46 | { view === views.SUBMIT && ( 47 | 48 | ) } 49 | { ! attributes.hideBranding && ( 50 | 58 | ) } 59 |
60 | ); 61 | }; 62 | 63 | export default FeedbackPopover; 64 | -------------------------------------------------------------------------------- /client/components/feedback/submit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | /** 7 | * Wordpress dependencies 8 | */ 9 | import { decodeEntities } from '@wordpress/html-entities'; 10 | 11 | const FeedbackSubmit = ( { attributes } ) => ( 12 |

13 | { decodeEntities( attributes.submitText ).split( '
' ).join( '\n' ) } 14 |

15 | ); 16 | 17 | export default FeedbackSubmit; 18 | -------------------------------------------------------------------------------- /client/components/feedback/toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React, { 5 | forwardRef, 6 | useCallback, 7 | useEffect, 8 | useLayoutEffect, 9 | } from 'react'; 10 | import classnames from 'classnames'; 11 | 12 | /** 13 | * WordPress dependencies 14 | */ 15 | import { decodeEntities } from '@wordpress/html-entities'; 16 | import { __ } from '@wordpress/i18n'; 17 | 18 | /** 19 | * Internal dependencies 20 | */ 21 | import CloseIcon from 'components/icon/close-small'; 22 | import { FeedbackToggleMode } from 'blocks/feedback/constants'; 23 | 24 | const FeedbackToggle = ( 25 | { attributes, className, isOpen, onClick, onToggle }, 26 | ref 27 | ) => { 28 | useLayoutEffect( onToggle, [ isOpen ] ); 29 | 30 | useEffect( () => { 31 | if ( isOpen || attributes.toggleOn !== FeedbackToggleMode.PAGE_LOAD ) { 32 | return; 33 | } 34 | 35 | onClick(); 36 | }, [] ); 37 | 38 | const handleHover = useCallback( () => { 39 | if ( isOpen || attributes.toggleOn !== FeedbackToggleMode.HOVER ) { 40 | return; 41 | } 42 | 43 | onClick(); 44 | }, [ attributes.toggleOn, isOpen ] ); 45 | 46 | const classes = classnames( 47 | 'crowdsignal-forms-feedback__trigger', 48 | 'wp-block-button__link', 49 | className, 50 | { 51 | 'is-active': isOpen, 52 | } 53 | ); 54 | 55 | return ( 56 |
57 | { ! isOpen && ( 58 | 68 | ) } 69 | { isOpen && ( 70 | 74 | ) } 75 |
76 | ); 77 | }; 78 | 79 | export default forwardRef( FeedbackToggle ); 80 | -------------------------------------------------------------------------------- /client/components/feedback/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { isObject } from 'lodash'; 5 | 6 | const addFrameOffsets = ( offset, frame ) => ( { 7 | left: offset.left + frame.x + window.scrollX, 8 | right: 9 | offset.right + 10 | ( window.innerWidth > frame.left + frame.width 11 | ? window.innerWidth - frame.left - frame.width 12 | : 0 ), 13 | top: offset.top + frame.y + window.scrollY, 14 | bottom: 15 | offset.bottom + 16 | ( window.innerHeight > frame.top + frame.height 17 | ? window.innerHeight - frame.top - frame.height 18 | : 0 ), 19 | } ); 20 | 21 | const getFeedbackButtonHorizontalPosition = ( align, width, offset ) => { 22 | return { 23 | left: align === 'left' ? offset.left : null, 24 | right: align === 'right' ? offset.right : null, 25 | }; 26 | }; 27 | 28 | const getFeedbackButtonVerticalPosition = ( verticalAlign, height, offset ) => { 29 | if ( verticalAlign === 'center' ) { 30 | return { 31 | top: ( window.innerHeight - height ) / 2, 32 | bottom: null, 33 | }; 34 | } 35 | 36 | return { 37 | top: verticalAlign === 'top' ? offset.top : null, 38 | bottom: verticalAlign === 'bottom' ? offset.bottom : null, 39 | }; 40 | }; 41 | 42 | export const getFeedbackButtonPosition = ( 43 | align, 44 | verticalAlign, 45 | width, 46 | height, 47 | padding, 48 | frameElement = null 49 | ) => { 50 | let offset = { 51 | left: isObject( padding ) ? padding.left : padding, 52 | right: isObject( padding ) ? padding.right : padding, 53 | top: isObject( padding ) ? padding.top : padding, 54 | bottom: isObject( padding ) ? padding.bottom : padding, 55 | }; 56 | 57 | if ( frameElement ) { 58 | offset = addFrameOffsets( 59 | offset, 60 | frameElement.getBoundingClientRect() 61 | ); 62 | } 63 | 64 | return { 65 | ...getFeedbackButtonHorizontalPosition( align, width, offset ), 66 | ...getFeedbackButtonVerticalPosition( verticalAlign, height, offset ), 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /client/components/footer-branding/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | const FooterBranding = ( { 7 | showLogo, 8 | children, 9 | message, 10 | trackRef = 'cs-forms-poll', 11 | } ) => ( 12 |
13 | 25 | 26 | { children } 27 | 28 | { showLogo && ( 29 | ( 7 | 13 | 18 | 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /client/components/icon/nps.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /client/components/icon/pencil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { SVG, Path } from '@wordpress/primitives'; 5 | 6 | const pencil = ( 7 | 8 | 9 | 10 | ); 11 | 12 | export default pencil; 13 | -------------------------------------------------------------------------------- /client/components/icon/placement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | const PlacementIcon = () => ( 7 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | export default PlacementIcon; 22 | -------------------------------------------------------------------------------- /client/components/icon/poll.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 | 8 | 13 | 20 | 27 | 34 | 35 | ); 36 | -------------------------------------------------------------------------------- /client/components/icon/quiz.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function QuizIcon() { 4 | return ( 5 | 12 | 16 | 22 | 26 | 27 | ); 28 | } 29 | 30 | export default QuizIcon; 31 | -------------------------------------------------------------------------------- /client/components/icon/size.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 | 9 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /client/components/icon/survey.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Survey() { 4 | return ( 5 | 12 | 18 | 24 | 28 | 29 | ); 30 | } 31 | 32 | export default Survey; 33 | -------------------------------------------------------------------------------- /client/components/icon/thumbs-down.js: -------------------------------------------------------------------------------- 1 | export default ( { className, fillColor = 'black' } ) => ( 2 | 10 | 11 | 20 | 26 | 27 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | -------------------------------------------------------------------------------- /client/components/icon/thumbs-up.js: -------------------------------------------------------------------------------- 1 | export default ( { className, fillColor = 'black' } ) => ( 2 | 10 | 11 | 20 | 26 | 27 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | -------------------------------------------------------------------------------- /client/components/icon/vote.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 | 9 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /client/components/icon/warning-circle.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 | 9 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /client/components/nps/feedback.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React, { useState } from 'react'; 5 | import { isEmpty } from 'lodash'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { updateNpsResponse } from 'data/nps'; 11 | 12 | const NpsFeedback = ( { attributes, onSubmit, responseMeta } ) => { 13 | const [ feedback, setFeedback ] = useState( '' ); 14 | 15 | const handleSubmit = async () => { 16 | if ( responseMeta !== null && ! isEmpty( feedback ) ) { 17 | updateNpsResponse( attributes.surveyId, { 18 | nonce: attributes.nonce, 19 | feedback, 20 | ...responseMeta, 21 | } ); 22 | } 23 | 24 | onSubmit(); 25 | }; 26 | 27 | return ( 28 |
29 | 36 | 37 |
38 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default NpsFeedback; 51 | -------------------------------------------------------------------------------- /client/components/nps/rating.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React, { useState } from 'react'; 5 | import classnames from 'classnames'; 6 | import { pick, times } from 'lodash'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import { updateNpsResponse } from 'data/nps'; 12 | 13 | const NpsRating = ( { attributes, onSubmit, onSubmitSuccess } ) => { 14 | const [ selected, setSelected ] = useState( -1 ); 15 | 16 | const handleSubmit = ( rating ) => async () => { 17 | setSelected( rating ); 18 | 19 | updateNpsResponse( attributes.surveyId, { 20 | nonce: attributes.nonce, 21 | score: rating, 22 | } ).then( ( data ) => 23 | onSubmitSuccess( pick( data, [ 'r', 'checksum' ] ) ) 24 | ); 25 | 26 | // Wait for the animation to complete before proceeding to the next step 27 | setTimeout( onSubmit, 300 ); 28 | }; 29 | 30 | return ( 31 |
32 |
33 | { attributes.lowRatingLabel } 34 | { attributes.highRatingLabel } 35 |
36 | 37 |
38 | { times( 11, ( n ) => { 39 | const classes = classnames( 40 | 'crowdsignal-forms-nps__rating-button', 41 | { 42 | 'is-active': n === selected, 43 | } 44 | ); 45 | 46 | return ( 47 | 55 | ); 56 | } ) } 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default NpsRating; 63 | -------------------------------------------------------------------------------- /client/components/poll/answer-results.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import classnames from 'classnames'; 7 | 8 | /** 9 | * WordPress dependencies 10 | */ 11 | import { decodeEntities } from '@wordpress/html-entities'; 12 | import { _n, sprintf } from '@wordpress/i18n'; 13 | 14 | const PollAnswerResults = ( { error, loading, text, totalVotes, votes } ) => { 15 | const classes = classnames( 'crowdsignal-forms-poll__answer-results', { 16 | 'is-error': error, 17 | 'is-loading': loading, 18 | } ); 19 | 20 | const showResults = ! loading && ! error; 21 | 22 | const answerShare = 0 === totalVotes ? 0 : ( votes * 100 ) / totalVotes; 23 | 24 | const progressBarStyles = { 25 | width: `${ parseInt( answerShare, 10 ) }%`, 26 | }; 27 | 28 | return ( 29 |
30 |
31 | 32 | { decodeEntities( text ) } 33 | 34 | 35 | 36 | { showResults && 37 | sprintf( 38 | /* translators: %s: Number of votes. */ 39 | _n( 40 | '%s vote', 41 | '%s votes', 42 | votes, 43 | 'crowdsignal-forms' 44 | ), 45 | votes.toLocaleString() 46 | ) } 47 | 48 | 49 | 50 | { showResults && `${ answerShare.toFixed( 2 ) }%` } 51 | 52 |
53 | 54 |
55 |
59 |
60 |
61 | ); 62 | }; 63 | 64 | PollAnswerResults.propTypes = { 65 | loading: PropTypes.bool, 66 | text: PropTypes.string.isRequired, 67 | totalVotes: PropTypes.number, 68 | votes: PropTypes.number, 69 | }; 70 | 71 | export default PollAnswerResults; 72 | -------------------------------------------------------------------------------- /client/components/poll/closed-banner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import classNames from 'classnames'; 6 | import { __ } from '@wordpress/i18n'; 7 | 8 | const ClosedBanner = ( { 9 | hasVoted, 10 | isPollClosed, 11 | isPollHidden, 12 | showSubmitMessage, 13 | } ) => { 14 | const classes = classNames( 15 | { 16 | 'is-transparent': showSubmitMessage, 17 | }, 18 | 'crowdsignal-forms-poll__closed-banner' 19 | ); 20 | 21 | let message = ''; 22 | if ( isPollHidden ) { 23 | message = __( 'This Poll is Hidden', 'crowdsignal-forms' ); 24 | } else if ( isPollClosed ) { 25 | message = __( 'This Poll is Closed', 'crowdsignal-forms' ); 26 | } else if ( hasVoted ) { 27 | message = __( 'Thanks For Voting!', 'crowdsignal-forms' ); 28 | } 29 | 30 | return ( 31 |
32 | { message } 33 |
34 | ); 35 | }; 36 | 37 | export default ClosedBanner; 38 | -------------------------------------------------------------------------------- /client/components/poll/error-banner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | const ErrorBanner = ( { children } ) => ( 7 |
{ children }
8 | ); 9 | 10 | export default ErrorBanner; 11 | -------------------------------------------------------------------------------- /client/components/poll/util.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import seedrandom from 'seedrandom'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { shuffleWithGenerator, isAnswerEmpty } from './util'; 10 | 11 | test( 'shuffleWithGenerator does not modify the original array', () => { 12 | const elements = [ 1, 2, 3, 4, 5 ]; 13 | const elementsClone = elements.slice(); 14 | 15 | shuffleWithGenerator( elements, new seedrandom() ); 16 | 17 | expect( elements ).toEqual( elementsClone ); 18 | } ); 19 | 20 | test( 'shuffleWithGenerator does not gain or lose any elements', () => { 21 | const elements = [ 1, 2, 3, 4, 5 ]; 22 | const shuffled = shuffleWithGenerator( elements, new seedrandom() ); 23 | 24 | expect( shuffled ).toHaveLength( elements.length ); 25 | expect( shuffled ).toEqual( expect.arrayContaining( elements ) ); 26 | } ); 27 | 28 | test( 'shuffleWithGenerator shuffles the same way twice when provided with the same random number generator', () => { 29 | const seed = 1; 30 | let rng = new seedrandom( seed ); 31 | const elements = [ 1, 2, 3, 4, 5 ]; 32 | const shuffled = shuffleWithGenerator( elements, rng ); 33 | 34 | rng = new seedrandom( seed ); 35 | const shuffledAgain = shuffleWithGenerator( elements, rng ); 36 | 37 | expect( shuffled ).toEqual( shuffledAgain ); 38 | } ); 39 | 40 | test( 'shuffleWithGenerator actually shuffles the elements, when given a random number generator', () => { 41 | const rng = new seedrandom(); 42 | const elements = [ 1, 2, 3, 4, 5 ]; 43 | const shuffled = shuffleWithGenerator( elements, rng ); 44 | 45 | expect( shuffled ).not.toEqual( elements ); 46 | } ); 47 | 48 | test( 'shuffleWithGenerator does not shuffle any elements, when given a static number generator', () => { 49 | const elements = [ 1, 2, 3, 4, 5 ]; 50 | 51 | const shuffled = shuffleWithGenerator( elements, () => 1 ); 52 | 53 | expect( elements ).toEqual( shuffled ); 54 | } ); 55 | 56 | test.each( [ 57 | [ 'object is empty', {} ], 58 | [ 'object only has answerId', { answerId: 123 } ], 59 | [ 'object has only text as empty string', { text: '' } ], 60 | [ 'object has only empty text and answerId', { text: '', answerId: 123 } ], 61 | ] )( 'isAnswerEmpty returns true if %s', ( _, answer ) => { 62 | expect( isAnswerEmpty( answer ) ).toEqual( true ); 63 | } ); 64 | 65 | test.each( [ 66 | [ 'object has non-empty text value', { text: 'answer value' } ], 67 | ] )( 'isAnswerEmpty returns false if %s', ( _, answer ) => { 68 | expect( isAnswerEmpty( answer ) ).toEqual( false ); 69 | } ); 70 | -------------------------------------------------------------------------------- /client/components/promotional-tooltip/index.js: -------------------------------------------------------------------------------- 1 | /** WordPress dependencies */ 2 | import { Tooltip } from '@wordpress/components'; 3 | import { __ } from '@wordpress/i18n'; 4 | 5 | const PromotionalTooltip = () => { 6 | return ( 7 | 10 | { __( 'Hide Crowdsignal ads', 'crowdsignal-forms' ) } 11 |
12 | { __( 'and get unlimited', 'crowdsignal-forms' ) } 13 |
14 | { __( 'signals', 'crowdsignal-forms' ) } -{ ' ' } 15 |
20 | { __( 'Upgrade', 'crowdsignal-forms' ) } 21 | 22 | 23 | } 24 | position="top center" 25 | > 26 | 32 | { __( 33 | 'Hide', 34 | 'crowdsignal-forms' 35 | ) } 36 | 37 | 38 | ); 39 | } 40 | 41 | export default PromotionalTooltip; 42 | -------------------------------------------------------------------------------- /client/components/retry-notice/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import EditorNotice from 'components/editor-notice'; 10 | 11 | const RetryNotice = ( { retryHandler } ) => { 12 | return ( 13 | 25 | { __( 26 | `Unfortunately, the block couldn't be saved to Crowdsignal.com.`, 27 | 'crowdsignal-forms' 28 | ) } 29 | 30 | ); 31 | }; 32 | 33 | export default RetryNotice; 34 | -------------------------------------------------------------------------------- /client/components/sidebar-promote/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { Button, ExternalLink } from '@wordpress/components'; 6 | 7 | const SidebarPromote = ( { signalWarning } ) => { 8 | return ( 9 |
10 | 17 | { signalWarning ? ( 18 |
19 | 20 | { __( 21 | 'Your free Crowdsignal account has ', 22 | 'crowdsignal-forms' 23 | ) } 24 | 25 | 26 | { __( 27 | 'reached the signals limit.', 28 | 'crowdsignal-forms' 29 | ) } 30 | 31 | 32 | 33 |
34 | ) : ( 35 |
36 | 37 | { __( 38 | 'Hide Crowdsignal branding and get ', 39 | 'crowdsignal-forms' 40 | ) } 41 | 42 | { __( 'unlimited signals', 'crowdsignal-forms' ) } 43 | 44 | 45 |
46 | ) } 47 |
48 | ); 49 | }; 50 | 51 | export default SidebarPromote; 52 | -------------------------------------------------------------------------------- /client/components/sidebar-promote/style.scss: -------------------------------------------------------------------------------- 1 | .crowdsignal-forms__sidebar-promote { 2 | margin-left: 16px; 3 | flex-grow: 1; 4 | } 5 | -------------------------------------------------------------------------------- /client/components/signal-warning/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { ExternalLink } from '@wordpress/components'; 5 | import { __ } from '@wordpress/i18n'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import EditorNotice from 'components/editor-notice'; 11 | 12 | const SignalWarning = () => { 13 | return ( 14 | 27 | { __( 'Your free Crowdsignal account has ', 'crowdsignal-forms' ) } 28 | 29 | { __( 'exceeded 2500 signals.', 'crowdsignal-forms' ) } 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default SignalWarning; 36 | -------------------------------------------------------------------------------- /client/components/use-autosave/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useCallback, useEffect, useRef, useState } from 'react'; 5 | import { debounce, values } from 'lodash'; 6 | 7 | const SAVE_DEBOUNCE = 1500; 8 | const RETRY_THRESHOLD = 3; 9 | 10 | export const useAutosave = ( onSave, data = {} ) => { 11 | const [ error, setError ] = useState( false ); 12 | 13 | const revision = useRef( 0 ); 14 | 15 | const debouncedSave = useCallback( 16 | debounce( 17 | ( args, onFailure ) => onSave( args ).catch( onFailure ), 18 | SAVE_DEBOUNCE 19 | ), 20 | [] 21 | ); 22 | 23 | const handleSave = useCallback( ( savedRevision, retryCount = 1 ) => { 24 | setError( false ); 25 | 26 | debouncedSave( data, () => { 27 | // Don't retry if there are new changes waiting to be saved 28 | if ( savedRevision !== revision.current ) { 29 | return; 30 | } 31 | 32 | if ( retryCount < RETRY_THRESHOLD ) { 33 | handleSave( savedRevision, retryCount + 1 ); 34 | return; 35 | } 36 | 37 | setError( true ); 38 | } ); 39 | }, values( data ) ); 40 | 41 | useEffect( () => { 42 | // Don't autosave on initial render 43 | if ( 0 === revision.current++ ) { 44 | return; 45 | } 46 | 47 | handleSave( revision.current ); 48 | }, values( data ) ); 49 | 50 | return { 51 | error, 52 | save: () => handleSave( revision.current ), 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /client/components/use-numbered-title/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect } from 'react'; 5 | import { isEmpty, isNil } from 'lodash'; 6 | 7 | const useNumberedTitle = ( 8 | blockName, 9 | titlePrefix, 10 | attributes, 11 | setAttributes 12 | ) => 13 | useEffect( () => { 14 | if ( isEmpty( window.csBlockTypeCount ) ) { 15 | window.csBlockTypeCount = {}; 16 | } 17 | 18 | if ( isNil( window.csBlockTypeCount[ blockName ] ) ) { 19 | window.csBlockTypeCount[ blockName ] = 0; 20 | } 21 | 22 | window.csBlockTypeCount[ blockName ]++; 23 | 24 | if ( null !== attributes.title ) { 25 | // exit if title is set, but only after block count has been set, so newer blocks get the correct count. 26 | return; 27 | } 28 | 29 | if ( 1 === window.csBlockTypeCount[ blockName ] ) { 30 | setAttributes( { 31 | title: titlePrefix, 32 | } ); 33 | } else { 34 | setAttributes( { 35 | title: `${ titlePrefix } ${ window.csBlockTypeCount[ blockName ] }`, 36 | } ); 37 | } 38 | }, [] ); 39 | 40 | export default useNumberedTitle; 41 | -------------------------------------------------------------------------------- /client/components/use-poll-duplicate-cleaner/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect } from 'react'; 5 | import { isEmpty, map, omit } from 'lodash'; 6 | 7 | export default ( blockClientId, pollId, answers, setAttributes ) => 8 | useEffect( () => { 9 | if ( isEmpty( pollId ) ) { 10 | return; 11 | } 12 | 13 | if ( ! window.csPolls ) { 14 | window.csPolls = {}; 15 | } 16 | 17 | if ( ! window.csPolls[ pollId ] ) { 18 | window.csPolls[ pollId ] = [ blockClientId ]; 19 | } else if ( window.csPolls[ pollId ].indexOf( blockClientId ) > -1 ) { 20 | // clientid already known, ignore. 21 | } else { 22 | const newAnswers = map( answers, ( answer ) => 23 | omit( answer, [ 'answerId' ] ) 24 | ); 25 | 26 | setAttributes( { pollId: null, answers: newAnswers } ); 27 | } 28 | }, [ pollId ] ); 29 | -------------------------------------------------------------------------------- /client/components/vote/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { round } from 'lodash'; 5 | 6 | /** 7 | * Formats the counter values on vote items: 8 | * 9 | * @param {number} count Vote count 10 | * @return {string} Formatted count 11 | */ 12 | export const formatVoteCount = ( count ) => { 13 | if ( ! count ) { 14 | return '0'; 15 | } 16 | 17 | if ( count >= 10000000 ) { 18 | return `${ round( count / 1000000 ) }M`; 19 | } 20 | 21 | if ( count >= 1000000 ) { 22 | return `${ ( count / 1000000 ).toFixed( 1 ) }M`; 23 | } 24 | 25 | if ( count >= 10000 ) { 26 | return `${ round( count / 1000 ) }K`; 27 | } 28 | 29 | if ( count >= 1000 ) { 30 | return `${ ( count / 1000 ).toFixed( 1 ) }K`; 31 | } 32 | 33 | return count.toString(); 34 | }; 35 | -------------------------------------------------------------------------------- /client/components/with-client-id/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React, { useEffect } from 'react'; 5 | import { v4 as uuid } from 'uuid'; 6 | import { forEach } from 'lodash'; 7 | 8 | const withClientId = ( clientIdAttributes ) => ( Element ) => { 9 | return ( props ) => { 10 | const { attributes, setAttributes } = props; 11 | 12 | useEffect( () => { 13 | forEach( clientIdAttributes, ( key ) => { 14 | if ( attributes[ key ] ) { 15 | return; 16 | } 17 | 18 | setAttributes( { 19 | [ key ]: uuid(), 20 | } ); 21 | } ); 22 | }, [] ); 23 | 24 | return ; 25 | }; 26 | }; 27 | 28 | export default withClientId; 29 | -------------------------------------------------------------------------------- /client/components/with-fallback-styles/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { 10 | getBackgroundColor, 11 | getBorderColor, 12 | withWordPressFallbackStyles, 13 | } from './util'; 14 | 15 | const StyleProbe = () => ( 16 |
17 |

18 |

Text

19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ); 27 | 28 | const getStyles = ( node ) => { 29 | if ( null === node ) { 30 | return {}; 31 | } 32 | 33 | const buttonNode = node.querySelector( '.wp-block-button__link' ); 34 | const textNode = node.querySelector( 'p' ); 35 | const h3Node = node.querySelector( 'h3' ); 36 | const wideContentNode = node.querySelector( '.alignwide' ); 37 | 38 | let accentColor = getBackgroundColor( buttonNode ); 39 | const backgroundColor = getBackgroundColor( textNode ); 40 | const textColor = window.getComputedStyle( textNode ).color; 41 | 42 | // Ensure that we don't end up with the same color for surface and accent. 43 | // Falls back to button border color, then text color. 44 | if ( accentColor === backgroundColor ) { 45 | const borderColor = getBorderColor( buttonNode ); 46 | accentColor = borderColor ? borderColor : textColor; 47 | } 48 | 49 | return { 50 | accentColor, 51 | backgroundColor, 52 | textColor, 53 | textColorInverted: window.getComputedStyle( buttonNode ).color, 54 | textFont: window.getComputedStyle( textNode ).fontFamily, 55 | textSize: window.getComputedStyle( textNode ).fontSize, 56 | headingFont: window.getComputedStyle( h3Node ).fontFamily, 57 | contentWideWidth: window.getComputedStyle( wideContentNode ).maxWidth, 58 | }; 59 | }; 60 | 61 | export const withFallbackStyles = ( WrappedComponent ) => { 62 | const getFallbackStyles = withWordPressFallbackStyles( ( node ) => ( { 63 | fallbackStyles: getStyles( 64 | node.querySelector( '.crowdsignal-forms__style-probe' ) 65 | ), 66 | } ) ); 67 | 68 | return getFallbackStyles( ( { fallbackStyles, ...props } ) => { 69 | const renderProbe = () => { 70 | if ( fallbackStyles ) { 71 | return null; 72 | } 73 | 74 | return ; 75 | }; 76 | 77 | return ( 78 | 83 | ); 84 | } ); 85 | }; 86 | -------------------------------------------------------------------------------- /client/components/with-fallback-styles/style.scss: -------------------------------------------------------------------------------- 1 | .crowdsignal-forms__style-probe { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /client/components/with-fallback-styles/util.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { getBackgroundColor } from './util'; 5 | 6 | test( 'getBackgroundColor returns parent color if passed in node has a transparent background', () => { 7 | const parentBackgroundColor = '#00ff00'; 8 | const parentNode = document.createElement( 'div' ); 9 | const node = document.createElement( 'div' ); 10 | parentNode.appendChild( node ); 11 | 12 | window.getComputedStyle = ( nodeToCheck ) => { 13 | let backgroundColor = 'rgba(0, 0, 0, 0)'; 14 | 15 | if ( nodeToCheck === parentNode ) { 16 | backgroundColor = parentBackgroundColor; 17 | } 18 | 19 | return { backgroundColor }; 20 | }; 21 | 22 | expect( getBackgroundColor( node ) ).toEqual( parentBackgroundColor ); 23 | } ); 24 | 25 | test( 'getBackgroundColor returns current node background color if background is not transparent', () => { 26 | const backgroundColor = '#00ff00'; 27 | const node = document.createElement( 'div' ); 28 | 29 | window.getComputedStyle = () => ( { backgroundColor } ); 30 | 31 | expect( getBackgroundColor( node ) ).toEqual( backgroundColor ); 32 | } ); 33 | -------------------------------------------------------------------------------- /client/components/with-fse-check/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | import ErrorBanner from 'components/poll/error-banner'; 7 | import { __ } from '@wordpress/i18n'; 8 | 9 | const withFseCheck = ( Element ) => { 10 | return ( props ) => { 11 | const { context } = props; 12 | const { postId, queryId } = context; 13 | 14 | // Prevent block from loading in FSE or a query loop because save handlers don't support those contexts. 15 | // - double == instead of triple === used because we need to test for both null and undefined 16 | if ( null == postId ) { 17 | return { __( 'Crowdsignal blocks cannot be used outside of a post or page. The Site Editor is not supported.', 'crowdsignal-forms' ) }; 18 | } else if ( null != queryId ) { 19 | return { __( 'Crowdsignal blocks are not supported inside a query loop.', 'crowdsignal-forms' ) }; 20 | } 21 | 22 | return ; 23 | }; 24 | }; 25 | 26 | export default withFseCheck; 27 | -------------------------------------------------------------------------------- /client/components/with-poll-base/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React, { useEffect } from 'react'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { compose } from '@wordpress/compose'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | import { 15 | startSubscriptions, 16 | startPolling, 17 | withPollDataSelect, 18 | withPollDataDispatch, 19 | } from 'blocks/poll/subscriptions'; 20 | import usePollDuplicateCleaner from 'components/use-poll-duplicate-cleaner'; 21 | 22 | startSubscriptions(); 23 | 24 | const isP2tenberg = () => 'p2tenberg' in window || 'p2editor' in window; 25 | 26 | const withPollBase = ( Element ) => { 27 | return ( props ) => { 28 | const { 29 | attributes, 30 | setAttributes, 31 | addPollClientId, 32 | removePollClientId, 33 | } = props; 34 | 35 | useEffect( () => { 36 | if ( isP2tenberg() ) { 37 | startPolling(); 38 | } 39 | 40 | if ( attributes.pollId ) { 41 | addPollClientId( attributes.pollId ); 42 | } 43 | 44 | return () => { 45 | if ( attributes.pollId ) { 46 | removePollClientId( attributes.pollId ); 47 | } 48 | }; 49 | }, [] ); 50 | 51 | usePollDuplicateCleaner( 52 | props.clientId, 53 | attributes.pollId, 54 | attributes.answers, 55 | setAttributes 56 | ); 57 | 58 | return ; 59 | }; 60 | }; 61 | 62 | export default ( Element ) => { 63 | return compose( [ 64 | withPollDataSelect(), 65 | withPollDataDispatch(), 66 | withPollBase, 67 | ] )( Element ); 68 | }; 69 | -------------------------------------------------------------------------------- /client/cs-embed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import CSEmbed from 'components/cs-embed'; 10 | import MutationObserver from 'lib/mutation-observer'; 11 | 12 | MutationObserver( 'data-crowdsignal-cs-embed', ( attributes ) => ( 13 | 14 | ) ); 15 | -------------------------------------------------------------------------------- /client/data/feedback/edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import apiFetch from '@wordpress/api-fetch'; 5 | import { trimEnd } from 'lodash'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { withRequestTimeout } from 'data/util'; 11 | 12 | export const updateFeedback = ( data ) => 13 | withRequestTimeout( 14 | apiFetch( { 15 | path: trimEnd( 16 | `/crowdsignal-forms/v1/feedback/${ data.surveyId || '' }`, 17 | '/' 18 | ), 19 | method: 'POST', 20 | data, 21 | } ) 22 | ); 23 | -------------------------------------------------------------------------------- /client/data/feedback/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { trimEnd } from 'lodash'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import apiFetch from '@crowdsignalForms/apifetch'; 10 | import { withRequestTimeout } from 'data/util'; 11 | 12 | export const updateFeedbackResponse = ( surveyId, data ) => 13 | withRequestTimeout( 14 | apiFetch( { 15 | path: trimEnd( 16 | `/crowdsignal-forms/v1/feedback/${ surveyId || '' }/response` 17 | ), 18 | method: 'POST', 19 | data, 20 | } ) 21 | ); 22 | -------------------------------------------------------------------------------- /client/data/hooks/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect, useState } from 'react'; 5 | import Cookies from 'js-cookie'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { requestResults, requestVoteNonce, requestVote } from 'data/poll'; 11 | import { useFetch } from './util'; 12 | 13 | export const usePollResults = ( pollId, doFetch = true ) => { 14 | const { data, error, loading } = useFetch( 15 | () => requestResults( pollId, doFetch ), 16 | [ pollId ] 17 | ); 18 | 19 | return { 20 | error, 21 | loading, 22 | results: data, 23 | }; 24 | }; 25 | 26 | /** 27 | * React Hook that returns state variables for voting status and a function to perform a vote. 28 | * 29 | * @param {number} pollId ID of the poll being loaded. 30 | * @param {boolean} enableVoteTracking sets whether or not the vote cookie is read and set 31 | * @param {boolean} storeAnswerIdsInCookie sets whether or not the answer ids are stored in the vote restriction cookie 32 | */ 33 | export const usePollVote = ( 34 | pollId, 35 | enableVoteTracking = false, 36 | storeAnswerIdsInCookie = false 37 | ) => { 38 | const cookieName = `cs-poll-${ pollId }`; 39 | const [ isVoting, setIsVoting ] = useState( false ); 40 | const [ hasVoted, setHasVoted ] = useState( false ); 41 | const [ storedCookieValue, setStoredCookieValue ] = useState( '' ); 42 | 43 | useEffect( () => { 44 | if ( enableVoteTracking && undefined !== Cookies.get( cookieName ) ) { 45 | setHasVoted( true ); 46 | setStoredCookieValue( Cookies.get( cookieName ) ); 47 | } 48 | }, [] ); 49 | 50 | const vote = async ( selectedAnswerIds, voteCount = 1 ) => { 51 | try { 52 | setIsVoting( true ); 53 | const nonce = await requestVoteNonce( pollId ); 54 | await requestVote( nonce, pollId, selectedAnswerIds, voteCount ); 55 | 56 | setHasVoted( true ); 57 | if ( enableVoteTracking ) { 58 | const cookieValue = storeAnswerIdsInCookie 59 | ? selectedAnswerIds.join( ',' ) 60 | : new Date().getTime(); 61 | 62 | Cookies.set( cookieName, cookieValue, { 63 | sameSite: 'Strict', 64 | expires: 365, 65 | } ); 66 | 67 | setStoredCookieValue( cookieValue ); 68 | } 69 | } finally { 70 | setIsVoting( false ); 71 | } 72 | }; 73 | 74 | return { 75 | hasVoted, 76 | isVoting, 77 | vote, 78 | storedCookieValue, 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /client/data/hooks/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect, useState } from 'react'; 5 | 6 | export const useFetch = ( fetchCallback, watchProps ) => { 7 | const [ data, setData ] = useState( null ); 8 | const [ error, setError ] = useState( null ); 9 | const [ loading, setLoading ] = useState( true ); 10 | 11 | useEffect( () => { 12 | setLoading( true ); 13 | setError( null ); 14 | setData( null ); 15 | 16 | fetchCallback() 17 | .then( setData ) 18 | .catch( setError ) 19 | .finally( () => setLoading( false ) ); 20 | }, watchProps ); 21 | 22 | return { 23 | data, 24 | error, 25 | loading, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /client/data/nps/edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import apiFetch from '@wordpress/api-fetch'; 5 | import { trimEnd } from 'lodash'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { withRequestTimeout } from 'data/util'; 11 | 12 | export const updateNps = ( data ) => 13 | withRequestTimeout( 14 | apiFetch( { 15 | path: trimEnd( 16 | `/crowdsignal-forms/v1/nps/${ data.surveyId || '' }`, 17 | '/' 18 | ), 19 | method: 'POST', 20 | data, 21 | } ) 22 | ); 23 | -------------------------------------------------------------------------------- /client/data/nps/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { trimEnd } from 'lodash'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import apiFetch from '@crowdsignalForms/apifetch'; 10 | import { withRequestTimeout } from 'data/util'; 11 | 12 | export const updateNpsResponse = ( surveyId, data ) => 13 | withRequestTimeout( 14 | apiFetch( { 15 | path: trimEnd( 16 | `/crowdsignal-forms/v1/nps/${ surveyId || '' }/response` 17 | ), 18 | method: 'POST', 19 | data, 20 | } ) 21 | ); 22 | -------------------------------------------------------------------------------- /client/data/util.js: -------------------------------------------------------------------------------- 1 | const WP_API_REQUEST_TIMEOUT = 10000; 2 | 3 | /** 4 | * Wraps a promise in a timeout that will reject 5 | * when it fails to complite within given time. 6 | * 7 | * @param {Promise} promise Promise 8 | * @return {Promise} Promise wrapped in a request timeout 9 | */ 10 | export const withRequestTimeout = ( promise ) => 11 | new Promise( ( resolve, reject ) => { 12 | const timer = setTimeout( 13 | () => reject( new Error( 'Request timed out' ) ), 14 | WP_API_REQUEST_TIMEOUT 15 | ); 16 | 17 | promise.then( resolve, reject ).finally( () => clearTimeout( timer ) ); 18 | } ); 19 | -------------------------------------------------------------------------------- /client/editor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { registerBlockType } from '@wordpress/blocks'; 5 | import { addFilter } from '@wordpress/hooks'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import pollBlock from 'blocks/poll'; 11 | import voteBlock from 'blocks/vote'; 12 | import voteItemBlock from 'blocks/vote-item'; 13 | import applauseBlock from 'blocks/applause'; 14 | import npsBlock from 'blocks/nps'; 15 | import feedbackBlock from 'blocks/feedback'; 16 | import csEmbedBlock from 'blocks/cs-embed'; 17 | import { 18 | withFixedPosition, 19 | withFixedPositionControl, 20 | } from 'components/with-fixed-position'; 21 | 22 | registerBlockType( 'crowdsignal-forms/poll', pollBlock ); 23 | registerBlockType( 'crowdsignal-forms/vote', voteBlock ); 24 | registerBlockType( 'crowdsignal-forms/vote-item', voteItemBlock ); 25 | registerBlockType( 'crowdsignal-forms/applause', applauseBlock ); 26 | registerBlockType( 'crowdsignal-forms/nps', npsBlock ); 27 | registerBlockType( 'crowdsignal-forms/feedback', feedbackBlock ); 28 | registerBlockType( 'crowdsignal-forms/cs-embed', csEmbedBlock ); 29 | 30 | addFilter( 31 | 'editor.BlockListBlock', 32 | 'crowdsignal-forms/with-fixed-position', 33 | withFixedPosition, 34 | 1 35 | ); 36 | addFilter( 37 | 'editor.BlockEdit', 38 | 'crowdsignal-forms/with-fixed-position-control', 39 | withFixedPositionControl 40 | ); 41 | -------------------------------------------------------------------------------- /client/feedback.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import Feedback from 'components/feedback'; 10 | import MutationObserver from 'lib/mutation-observer'; 11 | 12 | MutationObserver( 'data-crowdsignal-feedback', ( attributes ) => ( 13 | 14 | ) ); 15 | -------------------------------------------------------------------------------- /client/lib/mutation-observer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render } from 'react-dom'; 5 | import { camelCase, isEmpty, forEach } from 'lodash'; 6 | 7 | const MutationObserver = ( dataAttributeName, blockBuilder ) => { 8 | if ( 'complete' === document.readyState || 'interactive' === document.readyState ) { 9 | return blockObserver( dataAttributeName, blockBuilder ); 10 | } 11 | 12 | document.addEventListener( 'DOMContentLoaded', () => 13 | blockObserver( dataAttributeName, blockBuilder ) 14 | ); 15 | }; 16 | 17 | const initBlocks = ( dataAttributeName, blockBuilder ) => 18 | forEach( 19 | document.querySelectorAll( `div[${ dataAttributeName }]` ), 20 | ( element ) => { 21 | // Try-catch potentially prevents other blocks from breaking 22 | // when there's more then one on the page 23 | try { 24 | const attributes = JSON.parse( 25 | element.dataset[ 26 | camelCase( dataAttributeName.substr( 'data-'.length ) ) 27 | ] 28 | ); 29 | const block = blockBuilder( attributes, element ); 30 | 31 | element.removeAttribute( dataAttributeName ); 32 | 33 | render( block, element ); 34 | } catch ( error ) { 35 | // eslint-disable-next-line 36 | console.error( 37 | 'Crowdsignal Forms: Failed to parse block data for: %s', 38 | dataAttributeName 39 | ); 40 | } 41 | } 42 | ); 43 | 44 | const blockObserver = ( dataAttributeName, blockBuilder ) => { 45 | if ( 46 | ! isEmpty( window.CrowdsignalMutationObservers ) && 47 | true === window.CrowdsignalMutationObservers[ dataAttributeName ] 48 | ) { 49 | return; 50 | } 51 | 52 | const observer = new window.MutationObserver( () => 53 | initBlocks( dataAttributeName, blockBuilder ) 54 | ); 55 | 56 | observer.observe( document.body, { 57 | attributes: true, 58 | attributeFilter: [ dataAttributeName ], 59 | childList: true, 60 | subtree: true, 61 | } ); 62 | 63 | if ( isEmpty( window.CrowdsignalMutationObservers ) ) { 64 | window.CrowdsignalMutationObservers = []; 65 | } 66 | 67 | window.CrowdsignalMutationObservers[ dataAttributeName ] = true; 68 | 69 | // Run the first pass on load 70 | initBlocks( dataAttributeName, blockBuilder ); 71 | }; 72 | 73 | export default MutationObserver; 74 | -------------------------------------------------------------------------------- /client/lib/tracks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { debounce } from 'lodash'; 5 | 6 | export const trackFailedConnection = debounce( ( authorId, blockName ) => { 7 | window._tkq = window._tkq || []; 8 | window._tkq.push( [ 9 | 'recordEvent', 10 | 'crowdsignal_connection_failed', 11 | { 12 | author_id: authorId, 13 | block_name: blockName, 14 | }, 15 | ] ); 16 | }, 5000 ); 17 | -------------------------------------------------------------------------------- /client/nps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import { render } from 'react-dom'; 6 | import { forEach } from 'lodash'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import DialogWrapper from 'components/dialog-wrapper'; 12 | import NpsBlock from 'components/nps'; 13 | import { NpsStatus } from 'blocks/nps/constants'; 14 | 15 | const NPS_VIEWS_STORAGE_PREFIX = `cs-nps-views-`; 16 | 17 | window.addEventListener( 'load', () => 18 | forEach( 19 | document.querySelectorAll( `div[data-crowdsignal-nps]` ), 20 | ( element ) => { 21 | try { 22 | const attributes = JSON.parse( element.dataset.crowdsignalNps ); 23 | const viewThreshold = parseInt( attributes.viewThreshold, 10 ); 24 | 25 | element.removeAttribute( 'data-crowdsignal-nps' ); 26 | 27 | if ( NpsStatus.CLOSED === attributes.status ) { 28 | return; 29 | } 30 | 31 | if ( 32 | NpsStatus.CLOSED_AFTER === attributes.status && 33 | null !== attributes.closedAfterDateTime && 34 | new Date().toISOString() > attributes.closedAfterDateTime 35 | ) { 36 | return; 37 | } 38 | 39 | if ( ! attributes.isPreview ) { 40 | const key = `${ NPS_VIEWS_STORAGE_PREFIX }${ attributes.surveyId }`; 41 | const viewCount = 42 | 1 + 43 | parseInt( window.localStorage.getItem( key ) || 0, 10 ); 44 | 45 | window.localStorage.setItem( key, viewCount ); 46 | 47 | if ( viewCount !== viewThreshold ) { 48 | return; 49 | } 50 | } 51 | 52 | const closeDialog = () => element.remove(); 53 | 54 | render( 55 | 56 | 61 | , 62 | element 63 | ); 64 | } catch ( error ) { 65 | // eslint-disable-next-line no-console 66 | console.error( error ); 67 | } 68 | } 69 | ) 70 | ); 71 | -------------------------------------------------------------------------------- /client/poll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import Poll from 'components/poll'; 10 | import MutationObserver from 'lib/mutation-observer'; 11 | 12 | MutationObserver( 'data-crowdsignal-poll', ( attributes ) => ( 13 | 14 | ) ); 15 | -------------------------------------------------------------------------------- /client/state/account/actions.js: -------------------------------------------------------------------------------- 1 | import { ACCOUNT_INFO_LOAD, ACCOUNT_INFO_UPDATE } from '../action-types'; 2 | 3 | export function loadAccountInfo() { 4 | return { 5 | type: ACCOUNT_INFO_LOAD, 6 | }; 7 | } 8 | 9 | export function updateAccountInfo( data ) { 10 | return { 11 | type: ACCOUNT_INFO_UPDATE, 12 | data, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /client/state/account/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { combineReducers } from '@wordpress/data'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { ACCOUNT_INFO_UPDATE } from '../action-types'; 10 | 11 | const defaultAccountInfo = { 12 | is_verified: true, 13 | capabilities: [ 'hide-branding' ], 14 | signal_count: { 15 | count: 0, 16 | userLimit: 2500, 17 | shouldDisplay: false, 18 | }, 19 | }; 20 | 21 | const accountInfo = ( state = defaultAccountInfo, action ) => { 22 | if ( action.type === ACCOUNT_INFO_UPDATE ) { 23 | return { 24 | ...state, 25 | ...action.data, 26 | }; 27 | } 28 | 29 | return state; 30 | }; 31 | 32 | export default combineReducers( { 33 | accountInfo, 34 | } ); 35 | -------------------------------------------------------------------------------- /client/state/action-types.js: -------------------------------------------------------------------------------- 1 | export const ACCOUNT_INFO_LOAD = 'ACCOUNT_INFO_LOAD'; 2 | export const ACCOUNT_INFO_UPDATE = 'ACCOUNT_INFO_UPDATE'; 3 | 4 | // legacy 5 | export const SET_TRY_FETCH = 'SET_TRY_FETCH'; 6 | export const IS_FETCHING = 'IS_FETCHING'; 7 | export const SET_POLL = 'SET_POLL'; 8 | export const ADD_POLL_CLIENT_ID = 'ADD_POLL_CLIENT_ID'; 9 | export const REMOVE_POLL_CLIENT_ID = 'REMOVE_POLL_CLIENT_ID'; 10 | -------------------------------------------------------------------------------- /client/state/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_TRY_FETCH, 3 | IS_FETCHING, 4 | SET_POLL, 5 | ADD_POLL_CLIENT_ID, 6 | REMOVE_POLL_CLIENT_ID, 7 | } from './action-types'; 8 | 9 | export function setTryFetchPollData( tryFetch ) { 10 | return { 11 | type: SET_TRY_FETCH, 12 | tryFetch, 13 | }; 14 | } 15 | 16 | export function setIsFetchingPollData( isFetching ) { 17 | return { 18 | type: IS_FETCHING, 19 | isFetching, 20 | }; 21 | } 22 | 23 | export function setPollApiDataForClientId( clientId, pollData ) { 24 | return { 25 | type: SET_POLL, 26 | clientId, 27 | pollData, 28 | }; 29 | } 30 | 31 | export function addPollClientId( clientId ) { 32 | return { 33 | type: ADD_POLL_CLIENT_ID, 34 | clientId, 35 | }; 36 | } 37 | 38 | export function removePollClientId( clientId ) { 39 | return { 40 | type: REMOVE_POLL_CLIENT_ID, 41 | clientId, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /client/state/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * wordpress dependencies 3 | */ 4 | import { createReduxStore, register } from '@wordpress/data'; 5 | 6 | import * as mainActions from './actions'; 7 | import * as accountActions from './account/actions'; 8 | import reducer from './reducer'; 9 | import { fetchAccountInfo } from '../data/poll'; 10 | 11 | /** 12 | * Module Constants 13 | */ 14 | export const STORE_NAME = 'crowdsignal-forms/editor'; 15 | 16 | const storeConfig = { 17 | reducer, 18 | 19 | actions: { 20 | ...mainActions, // legacy, before store refactor 21 | ...accountActions, 22 | }, 23 | 24 | selectors: { 25 | shouldTryFetchingPollData( state ) { 26 | return !! state?.tryFetch; 27 | }, 28 | getPollDataByClientId( state, clientId ) { 29 | return state.pollsByClientId[ clientId ] || null; 30 | }, 31 | getPollClientIds( state ) { 32 | return state.pollClientIds; 33 | }, 34 | isFetchingPollData( state ) { 35 | return !! state?.isFetching; 36 | }, 37 | getAccountInfo( state ) { 38 | return state.account.accountInfo; 39 | }, 40 | }, 41 | 42 | controls: { 43 | ACCOUNT_INFO_LOAD() { 44 | return fetchAccountInfo(); 45 | }, 46 | }, 47 | 48 | resolvers: { 49 | *getAccountInfo() { 50 | const res = yield accountActions.loadAccountInfo(); 51 | return accountActions.updateAccountInfo( res ); 52 | }, 53 | }, 54 | }; 55 | 56 | export const store = createReduxStore( STORE_NAME, storeConfig ); 57 | register( store ); 58 | -------------------------------------------------------------------------------- /client/state/reducer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { combineReducers } from '@wordpress/data'; 5 | import { filter } from 'lodash'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import account from './account/reducer'; 11 | import { 12 | SET_TRY_FETCH, 13 | IS_FETCHING, 14 | SET_POLL, 15 | ADD_POLL_CLIENT_ID, 16 | REMOVE_POLL_CLIENT_ID, 17 | } from './action-types'; 18 | 19 | const DEFAULT_STATE = { 20 | tryFetch: false, 21 | isFetching: false, 22 | pollsByClientId: {}, 23 | pollClientIds: [], 24 | }; 25 | 26 | const tryFetch = ( state = false, action ) => { 27 | if ( action.type === SET_TRY_FETCH ) { 28 | return !! action.tryFetch; 29 | } 30 | return state; 31 | }; 32 | const isFetching = ( state = false, action ) => { 33 | if ( action.type === IS_FETCHING ) { 34 | return !! action.isFetching; 35 | } 36 | return state; 37 | }; 38 | const pollsByClientId = ( state = {}, action ) => { 39 | if ( action.type === SET_POLL ) { 40 | return { 41 | ...state, 42 | [ action.clientId ]: action.pollData, 43 | }; 44 | } 45 | return state; 46 | }; 47 | const pollClientIds = ( state = [], action ) => { 48 | if ( action.type === ADD_POLL_CLIENT_ID ) { 49 | return [ ...state, action.clientId ]; 50 | } 51 | 52 | if ( action.type === REMOVE_POLL_CLIENT_ID ) { 53 | return [ 54 | ...filter( state, ( clientId ) => clientId !== action.clientId ), 55 | ]; 56 | } 57 | return state; 58 | }; 59 | 60 | export default combineReducers( { 61 | tryFetch, 62 | isFetching, 63 | pollsByClientId, 64 | pollClientIds, 65 | account, 66 | } ); 67 | -------------------------------------------------------------------------------- /client/vote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import React from 'react'; 5 | import { isEmpty, forEach } from 'lodash'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import Vote from 'components/vote'; 11 | import MutationObserver from 'lib/mutation-observer'; 12 | 13 | MutationObserver( 'data-crowdsignal-vote', ( attributes, element ) => { 14 | const innerBlocks = []; 15 | 16 | forEach( element.children, ( childElement ) => { 17 | if ( isEmpty( childElement.dataset.crowdsignalVoteItem ) ) { 18 | return; 19 | } 20 | 21 | innerBlocks.push( 22 | JSON.parse( childElement.dataset.crowdsignalVoteItem ) 23 | ); 24 | } ); 25 | 26 | const voteAttributes = { 27 | ...attributes, 28 | innerBlocks, 29 | }; 30 | 31 | return ; 32 | } ); 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automattic/crowdsignal-forms", 3 | "description": "Crowdsignal Forms", 4 | "type": "project", 5 | "license": "GPLv2", 6 | "require-dev": { 7 | "phpcompatibility/phpcompatibility-wp": "2.1.0", 8 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", 9 | "wp-coding-standards/wpcs": "2.2.1", 10 | "psy/psysh": "^0.10.7", 11 | "wp-cli/wp-cli-bundle": "2.6" 12 | }, 13 | "config": { 14 | "allow-plugins": { 15 | "cweagans/composer-patches": true, 16 | "dealerdirect/phpcodesniffer-composer-installer": true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crowdsignal-forms.php: -------------------------------------------------------------------------------- 1 | set_plugin_dir( $crowdsignal_forms_plugin_dir ) 41 | ->register(); 42 | 43 | Crowdsignal_Forms\Crowdsignal_Forms::init(); 44 | -------------------------------------------------------------------------------- /docker/.dockerignore: -------------------------------------------------------------------------------- 1 | data 2 | wordpress 3 | wordpress-develop 4 | develop 5 | logs 6 | -------------------------------------------------------------------------------- /docker/bin/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if $(wp --allow-root core is-installed); then 4 | echo 5 | echo "WordPress has already been installed. Uninstall it first by running:" 6 | echo 7 | echo " make docker_uninstall" 8 | echo 9 | exit 1; 10 | fi 11 | 12 | # Install WP core 13 | wp --allow-root core install \ 14 | --url=${WP_DOMAIN} \ 15 | --title="${WP_TITLE}" \ 16 | --admin_user=${WP_ADMIN_USER} \ 17 | --admin_password=${WP_ADMIN_PASSWORD} \ 18 | --admin_email=${WP_ADMIN_EMAIL} \ 19 | --skip-email 20 | 21 | # Discourage search engines from indexing. Can be changed via UI in Settings->Reading. 22 | wp --allow-root option update blog_public 0 23 | 24 | # Install Query Monitor plugin 25 | # https://wordpress.org/plugins/query-monitor/ 26 | wp --allow-root plugin install query-monitor --activate 27 | 28 | # Install Application Passwords for easy api access. 29 | wp --allow-root plugin install application-passwords --activate 30 | # Activate Crowdsignal Forms 31 | wp --allow-root plugin activate crowdsignal-forms 32 | 33 | echo 34 | echo "WordPress installed. Open ${WP_DOMAIN}" 35 | echo 36 | -------------------------------------------------------------------------------- /docker/bin/install_composer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /tmp 4 | 5 | EXPECTED_CHECKSUM="$(wget -q -O - https://composer.github.io/installer.sig)" 6 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" 7 | ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" 8 | 9 | if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ] 10 | then 11 | >&2 echo 'ERROR: Invalid installer checksum' 12 | rm composer-setup.php 13 | exit 1 14 | fi 15 | 16 | php composer-setup.php --quiet 17 | RESULT=$? 18 | rm composer-setup.php 19 | exit $RESULT 20 | -------------------------------------------------------------------------------- /docker/bin/multisite-convert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! $(wp --allow-root core is-installed); then 4 | echo 5 | echo "WordPress has to be installed first. To install, run:" 6 | echo 7 | echo " yarn docker:install" 8 | echo 9 | exit 1; 10 | fi 11 | 12 | # Do the conversion, requires WP installed 13 | wp --allow-root core multisite-convert 14 | 15 | # Update domain to wp-config.php 16 | wp --allow-root config set DOMAIN_CURRENT_SITE "${WP_DOMAIN}" --type=constant 17 | 18 | # Use multisite htaccess template 19 | cp -f /tmp/htaccess-multi /var/www/html/.htaccess 20 | 21 | # Update domain to DB 22 | wp --allow-root db query "UPDATE wp_blogs SET domain='${WP_DOMAIN}' WHERE blog_id=1;" 23 | 24 | echo 25 | echo "WordPress converted to a multisite. Open ${WP_DOMAIN}" 26 | echo 27 | -------------------------------------------------------------------------------- /docker/bin/tail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WP_DEBUG_LOG=/var/www/html/wp-content/debug.log 4 | 5 | if [ ! -e "$WP_DEBUG_LOG" ] ; then 6 | touch "$WP_DEBUG_LOG" 7 | fi 8 | 9 | tail -F --lines 100 "$WP_DEBUG_LOG" 10 | -------------------------------------------------------------------------------- /docker/bin/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Empty DB 4 | wp --allow-root db reset --yes 5 | 6 | # Ensure we have single-site htaccess instead of multisite, 7 | # just like we would have in fresh container. 8 | cp -f /tmp/htaccess /var/www/html/.htaccess 9 | 10 | # Remove "uploads" and "upgrade" folders 11 | rm -fr /var/www/html/wp-content/uploads /var/www/html/wp-content/upgrade 12 | 13 | # Empty WP debug log 14 | truncate -s 0 /var/www/html/wp-content/debug.log 15 | 16 | # Ensure wp-config.php doesn't have multi-site settings 17 | echo 18 | echo "Clearing out possible multi-site related settings from wp-config.php" 19 | echo "It's okay to see errors if these did't exist..." 20 | wp --allow-root config delete WP_ALLOW_MULTISITE 21 | wp --allow-root config delete MULTISITE 22 | wp --allow-root config delete SUBDOMAIN_INSTALL 23 | wp --allow-root config delete base 24 | wp --allow-root config delete DOMAIN_CURRENT_SITE 25 | wp --allow-root config delete PATH_CURRENT_SITE 26 | wp --allow-root config delete SITE_ID_CURRENT_SITE 27 | wp --allow-root config delete BLOG_ID_CURRENT_SITE 28 | 29 | echo 30 | echo "WordPress uninstalled. To install it again, run:" 31 | echo 32 | echo " yarn docker:install" 33 | echo 34 | -------------------------------------------------------------------------------- /docker/config/apache_default: -------------------------------------------------------------------------------- 1 | ServerName localhost 2 | 3 | 4 | # The ServerName directive sets the request scheme, hostname and port that 5 | # the server uses to identify itself. This is used when creating 6 | # redirection URLs. In the context of virtual hosts, the ServerName 7 | # specifies what hostname must appear in the request's Host: header to 8 | # match this virtual host. For the default virtual host (this file) this 9 | # value is not decisive as it is used as a last resort host regardless. 10 | # However, you must set it for any further virtual host explicitly. 11 | ServerName localhost 12 | 13 | ServerAdmin webmaster@localhost 14 | DocumentRoot /var/www/html 15 | 16 | 17 | AllowOverride All 18 | Require all granted 19 | 20 | 21 | # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, 22 | # error, crit, alert, emerg. 23 | # It is also possible to configure the loglevel for particular 24 | # modules, e.g. 25 | #LogLevel info ssl:warn 26 | 27 | # ErrorLog /var/log/apache-errors.log 28 | # CustomLog /var/log/apache-access.log combined 29 | 30 | # For most configuration files from conf-available/, which are 31 | # enabled or disabled at a global level, it is possible to 32 | # include a line for only one particular virtual host. For example the 33 | # following line enables the CGI configuration for this host only 34 | # after it has been globally disabled with "a2disconf". 35 | #Include conf-available/serve-cgi-bin.conf 36 | 37 | 38 | 39 | # vim: syntax=apache ts=4 sw=4 sts=4 sr noet 40 | -------------------------------------------------------------------------------- /docker/config/htaccess: -------------------------------------------------------------------------------- 1 | # BEGIN WordPress 2 | 3 | RewriteEngine On 4 | RewriteBase / 5 | RewriteRule ^index\.php$ - [L] 6 | RewriteCond %{REQUEST_FILENAME} !-f 7 | RewriteCond %{REQUEST_FILENAME} !-d 8 | RewriteRule . /index.php [L] 9 | 10 | # END WordPress 11 | -------------------------------------------------------------------------------- /docker/config/htaccess-multi: -------------------------------------------------------------------------------- 1 | # BEGIN WordPress 2 | 3 | RewriteEngine On 4 | RewriteBase / 5 | RewriteRule ^index\.php$ - [L] 6 | 7 | # add a trailing slash to /wp-admin 8 | RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L] 9 | 10 | RewriteCond %{REQUEST_FILENAME} -f [OR] 11 | RewriteCond %{REQUEST_FILENAME} -d 12 | RewriteRule ^ - [L] 13 | RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L] 14 | RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L] 15 | RewriteRule . index.php [L] 16 | 17 | # END WordPress 18 | -------------------------------------------------------------------------------- /docker/config/php.ini: -------------------------------------------------------------------------------- 1 | short_open_tag = Off 2 | session.auto_start = Off 3 | file_uploads = On 4 | memory_limit = 64M 5 | upload_max_filesize = 64M 6 | post_max_size = 64M 7 | display_errors = On 8 | error_reporting = E_ALL 9 | error_log = /var/log/php/errors.log 10 | sendmail_path = /usr/sbin/ssmtp -t 11 | xdebug.remote_enable = On 12 | xdebug.remote_host = docker.for.mac.localhost 13 | xdebug.remote_handler = dbgp 14 | xdebug.profiler_enable = Off; 15 | xdebug.profiler_enable_trigger = On; 16 | xdebug.profiler_output_dir = "/var/www/html" 17 | -------------------------------------------------------------------------------- /docker/config/ssmtp.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Config file for sSMTP sendmail 3 | # 4 | # The person who gets all mail for userids < 1000 5 | # Make this empty to disable rewriting. 6 | root=postmaster 7 | 8 | # The place where the mail goes. 9 | mailhub=maildev:25 10 | #UseSTARTTLS=NO 11 | #AuthUser= 12 | #AuthPass= 13 | 14 | # Where will the mail seem to come from? 15 | #rewriteDomain= 16 | 17 | # The full hostname 18 | hostname=localhost 19 | 20 | # Are users allowed to set their own From: address? 21 | # YES - Allow the user to specify their own From: address 22 | # NO - Use the system generated From: address 23 | #FromLineOverride=YES 24 | -------------------------------------------------------------------------------- /docker/data/mysql/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/docker/data/mysql/.gitkeep -------------------------------------------------------------------------------- /docker/default.env: -------------------------------------------------------------------------------- 1 | # Default configuration for Docker containers. 2 | # 3 | # To modify, copy values over to ".env" file. 4 | # Values in ".env" file will override values 5 | # in "default.env". 6 | # 7 | # Values passed via command-line arguments take precedence over .env files: 8 | # $ WP_DOMAIN=example.com yarn docker:up 9 | # 10 | # Note that there is no special handling of quotation marks. 11 | # This means that they are part of the value. 12 | # 13 | # Note that these variables are not available in docker-compose.yml 14 | # Variables show up defined inside containers only. 15 | 16 | # WordPress - Only WP_ADMIN_PASSWORD needs to be changed 17 | WP_DOMAIN=localhost 18 | WP_ADMIN_USER=wordpress 19 | WP_ADMIN_EMAIL=wordpress@example.com 20 | # If this site is or will be publicly accessible, change WP_ADMIN_PASSWORD to something unique and secure 21 | WP_ADMIN_PASSWORD=wordpress 22 | WP_TITLE=HelloWord 23 | 24 | # Database - No changes necessary 25 | MYSQL_HOST=db:3306 26 | MYSQL_ROOT_PASSWORD=somewordpress 27 | MYSQL_DATABASE=wordpress 28 | MYSQL_USER=wordpress 29 | MYSQL_PASSWORD=wordpress 30 | 31 | # SFTP container users (user:pass:UID) - Password needs to be changed 32 | # 33 | # IMPORTANT: One of the users you define must be `wordpress` because paths in 34 | # `docker/docker-compose.yml` are fixed. You can modify their password, though. 35 | # 36 | # Set UID/GID manually for your users if you want them to make changes to 37 | # your mounted volumes with permissions matching your host filesystem. 38 | # 39 | # Define multiple users separated by space 40 | # 41 | # Read more: https://github.com/atmoz/sftp 42 | # 43 | # If this site is or will be publicly accessible, change the password below (the middle part) to something unique and secure 44 | SFTP_USERS=wordpress:wordpress:1001 45 | 46 | # Xdebug 47 | PHP_IDE_CONFIG=serverName=Test 48 | 49 | # The port to bind in the host machine. 50 | HOST_PORT=8000 51 | 52 | # Fill these with your own, and they will always override any other api keys set. 53 | CROWDSIGNAL_FORMS_API_PARTNER_GUID='' 54 | CROWDSIGNAL_FORMS_API_USER_CODE='' 55 | 56 | 57 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | volumes: 4 | ## For making sure the ./docker dir is not bound recursively. 5 | dockerdirectory: 6 | 7 | services: 8 | db: 9 | platform: linux/x86_64 10 | image: mysql:5.7 11 | container_name: cs_forms_mysql 12 | volumes: 13 | - ./data/mysql:/var/lib/mysql 14 | restart: always 15 | env_file: 16 | - default.env 17 | - .env 18 | 19 | wordpress: 20 | container_name: cs_forms_wordpress 21 | depends_on: 22 | - db 23 | build: 24 | context: . 25 | dockerfile: Dockerfile 26 | image: cs_forms_wordpress:localbuild 27 | volumes: 28 | - ..:/var/www/html/wp-content/plugins/crowdsignal-forms 29 | ## Kludge for not having docker contain recursive stuff 30 | ## You will see on your filesystem that this dir gets created 31 | - dockerdirectory:/var/www/html/wp-content/plugins/crowdsignal-forms/docker 32 | - ./mu-plugins:/var/www/html/wp-content/mu-plugins 33 | - ./wordpress-develop:/tmp/wordpress-develop 34 | - ./wordpress:/var/www/html 35 | - ./logs/apache2/:/var/log/apache2 36 | - ./logs/php:/var/log/php 37 | - ./bin:/var/scripts 38 | - ../../crowdsignal-plugin:/var/www/html/wp-content/plugins/polldaddy 39 | ports: 40 | - "${PORT_CS_WORDPRESS:-8000}:80" 41 | restart: always 42 | extra_hosts: 43 | - "api.crowdsignal.com:${CS_SANDBOX_IP:-192.0.123.248}" 44 | - "app.crowdsignal.com:${CS_SANDBOX_IP:-192.0.123.248}" 45 | - "api.polldaddy.com:${CS_SANDBOX_IP:-192.0.123.248}" 46 | env_file: 47 | - default.env 48 | - .env 49 | -------------------------------------------------------------------------------- /docker/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/docker/logs/.gitkeep -------------------------------------------------------------------------------- /docker/mu-plugins/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !debug.php 4 | !avoid-plugin-deletion.php 5 | !0-force-crowdsignal-forms-api-keys.php 6 | !1-always-throw-cs-sync-errors.php 7 | -------------------------------------------------------------------------------- /docker/mu-plugins/0-force-crowdsignal-forms-api-keys.php: -------------------------------------------------------------------------------- 1 | response[ $avoided_plugin ] ) ) { 55 | unset( $plugins->response[ $avoided_plugin ] ); 56 | } 57 | } 58 | return $plugins; 59 | } 60 | add_filter( 'site_transient_update_plugins', 'jetpack_docker_disable_plugin_update' ); 61 | -------------------------------------------------------------------------------- /docker/wordpress-develop/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/docker/wordpress-develop/.gitkeep -------------------------------------------------------------------------------- /docker/wordpress/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/docker/wordpress/.gitkeep -------------------------------------------------------------------------------- /images/cs_dashboard_teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/images/cs_dashboard_teaser.png -------------------------------------------------------------------------------- /includes/admin/class-crowdsignal-forms-admin.php: -------------------------------------------------------------------------------- 1 | setup_page = new Crowdsignal_Forms_Setup(); 45 | $this->settings_page = new Crowdsignal_Forms_Settings(); 46 | } 47 | 48 | /** 49 | * Set up actions during admin initialization. 50 | * 51 | * @todo for future use 52 | */ 53 | public function admin_init() { 54 | add_filter( 'plugin_action_links_' . plugin_basename( CROWDSIGNAL_FORMS_PLUGIN_FILE ), array( $this, 'plugin_action_links' ) ); 55 | } 56 | 57 | /** 58 | * Enqueues CSS and JS assets. 59 | * 60 | * @todo for future use 61 | */ 62 | public function admin_enqueue_scripts() { 63 | } 64 | 65 | /** 66 | * Adds pages to admin menu. 67 | */ 68 | public function admin_menu() { 69 | if ( 70 | isset( $_GET['page'] ) 71 | && ( 'crowdsignal-forms-settings' === $_GET['page'] || 'crowdsignal-forms-setup' === $_GET['page'] ) 72 | ) { 73 | wp_safe_redirect( admin_url( 'options-general.php?page=crowdsignal-settings' ) ); 74 | die(); 75 | } 76 | 77 | if ( ! is_plugin_active( 'polldaddy/polldaddy.php' ) ) { 78 | // Add settings pages. 79 | add_options_page( 'Crowdsignal', 'Crowdsignal', 'manage_options', 'crowdsignal-settings', array( $this->settings_page, 'output' ) ); 80 | } 81 | } 82 | 83 | /** 84 | * Adds to the Action links in the plugin page. 85 | * 86 | * @param array $links 87 | * @return array 88 | */ 89 | public function plugin_action_links( $links ) { 90 | return array_merge( 91 | array( 92 | sprintf( '' . __( 'Settings', 'crowdsignal-forms' ) . '', admin_url( 'options-general.php?page=crowdsignal-settings' ) ), 93 | ), 94 | $links 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /includes/admin/class-crowdsignal-forms-notice-icon.php: -------------------------------------------------------------------------------- 1 | ' . self::svg_icon_warning() . ''; 25 | } 26 | 27 | /** 28 | * Returns the success svg icon wrapped in a span tag 29 | */ 30 | public static function success() { 31 | return '' . self::svg_icon_success() . ''; 32 | } 33 | 34 | /** 35 | * Returns the warning svg icon markup 36 | */ 37 | private static function svg_icon_warning() { 38 | return ' 39 | 40 | 41 | 42 | 43 | 44 | 45 | '; 46 | } 47 | 48 | /** 49 | * Returns the success svg icon markup 50 | */ 51 | private static function svg_icon_success() { 52 | return ' 53 | 54 | 55 | 56 | 57 | 58 | 59 | '; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /includes/admin/index.php: -------------------------------------------------------------------------------- 1 | 14 |
15 |

16 | Crowdsignal.', 'crowdsignal-forms' ) ); 18 | ?> 19 |

20 |

21 | 22 | 23 |

24 |
25 | -------------------------------------------------------------------------------- /includes/admin/views/html-admin-setup-step-2.php: -------------------------------------------------------------------------------- 1 | get_api_key() ) { 15 | $crowdsignal_forms_msg = 'connected'; 16 | } else { 17 | $crowdsignal_forms_msg = 'api-key-not-added'; 18 | } 19 | ?> 20 | 28 | 29 | -------------------------------------------------------------------------------- /includes/admin/views/html-admin-setup-step-3.php: -------------------------------------------------------------------------------- 1 | 12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 |

23 |
24 |

25 | 26 |

27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 | 35 |

36 | Learn more.', 42 | 'crowdsignal-forms' 43 | ), 44 | 'https://crowdsignal.com/support/' 45 | ) 46 | ); 47 | ?> 48 |

49 |
50 |
51 |
52 | -------------------------------------------------------------------------------- /includes/auth/class-api-auth-provider-interface.php: -------------------------------------------------------------------------------- 1 | array( 39 | 'partnerGUID' => $api_key, 40 | 'partnerUserID' => wp_get_current_user()->ID, 41 | 'demands' => array( 42 | 'demand' => array( 43 | 'id' => 'GetUserCode', 44 | ), 45 | ), 46 | ), 47 | ) 48 | ); 49 | 50 | $data = $this->perform_query( $curl_data ); 51 | 52 | if ( isset( $data->pdResponse->userCode ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- data from API. 53 | return $data->pdResponse->userCode; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- data from API. 54 | } else { 55 | return false; 56 | } 57 | } 58 | 59 | /** 60 | * Get the Crowdsignal user code for an API key. 61 | * 62 | * @param string $query query to send to API. 63 | * @return mixed 64 | */ 65 | private function perform_query( $query ) { 66 | $data = wp_remote_post( 67 | 'https://api.crowdsignal.com/v1', 68 | array( 69 | 'method' => 'POST', 70 | 'body' => $query, 71 | 'headers' => array( 'Content-Type' => 'application/json' ), 72 | ) 73 | ); 74 | if ( is_wp_error( $data ) ) { 75 | return array(); 76 | } 77 | return json_decode( $data['body'] ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /includes/frontend/class-crowdsignal-forms-blocks.php: -------------------------------------------------------------------------------- 1 | 0 ) { 39 | return self::$blocks; 40 | } 41 | 42 | self::$blocks = array( 43 | new Blocks\Crowdsignal_Forms_Poll_Block(), 44 | new Blocks\Crowdsignal_Forms_Vote_Block(), 45 | new Blocks\Crowdsignal_Forms_Vote_Item_Block(), 46 | new Blocks\Crowdsignal_Forms_Applause_Block(), 47 | new Blocks\Crowdsignal_Forms_Nps_Block(), 48 | new Blocks\Crowdsignal_Forms_Feedback_Block(), 49 | ); 50 | 51 | return self::$blocks; 52 | } 53 | 54 | /** 55 | * Registers Crowdsignal Forms' custom Gutenberg blocks 56 | */ 57 | public function register() { 58 | foreach ( self::blocks() as $block ) { 59 | $block->register(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /includes/frontend/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | A custom set of code standard rules to check for WordPress themes and plugins. 4 | 5 | 6 | 7 | 8 | 9 | 10 | . 11 | 12 | ^node_modules/* 13 | ^vendor/* 14 | ^build/* 15 | tests/ 16 | 17 | 18 | ^templates/* 19 | ^lib/* 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | includes/**/abstract-*.php 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | tests/ 55 | 56 | 57 | 58 | tests/* 59 | 60 | 61 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ./tests/unit-tests 20 | 21 | 22 | 23 | 24 | ./includes 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /scripts/makepot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # to be ran within a docker instance or somewhere where wp-cli is installed. 4 | # requires gettext. 5 | # usually ran via make translations 6 | 7 | set -e 8 | 9 | composer exec -v -- 'wp i18n make-pot . ./languages/crowdsignal-forms.pot --exclude="docker,tests,release,client"' 10 | -------------------------------------------------------------------------------- /scripts/package-for-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | 6 | command -v zip || { 7 | >&2 echo "zip is required" 8 | exit 1 9 | } 10 | command -v composer || { 11 | >&2 echo "composer is required" 12 | exit 1 13 | } 14 | command -v git || { 15 | >&2 echo "git is required" 16 | exit 1 17 | } 18 | 19 | PLUGIN_DIR=`pwd` 20 | RELEASE_ZIP_FILENAME="crowdsignal-forms.$(git rev-parse --abbrev-ref HEAD | sed 's/\//-/g' | tr -d '\n').zip" 21 | RELEASE_FOLDER="/tmp/crowdsignal-forms-release" 22 | RELEASE_BUILD_FOLDER="$RELEASE_FOLDER/crowdsignal-forms" 23 | 24 | rm -rf "$RELEASE_FOLDER" 25 | mkdir -p "$RELEASE_BUILD_FOLDER" 26 | 27 | cp -r "$PLUGIN_DIR/includes" "$RELEASE_BUILD_FOLDER" 28 | cp -r "$PLUGIN_DIR/build" "$RELEASE_BUILD_FOLDER" 29 | cp -r "$PLUGIN_DIR/languages" "$RELEASE_BUILD_FOLDER" 30 | cp -r "$PLUGIN_DIR/changelog.txt" "$RELEASE_BUILD_FOLDER" 31 | cp -r "$PLUGIN_DIR/index.php" "$RELEASE_BUILD_FOLDER" 32 | cp -r "$PLUGIN_DIR/LICENSE.TXT" "$RELEASE_BUILD_FOLDER" 33 | cp -r "$PLUGIN_DIR/README.TXT" "$RELEASE_BUILD_FOLDER" 34 | cp -r "$PLUGIN_DIR/crowdsignal-forms.php" "$RELEASE_BUILD_FOLDER" 35 | cp -r "$PLUGIN_DIR/uninstall.php" "$RELEASE_BUILD_FOLDER" 36 | 37 | rm -f "$RELEASE_BUILD_FOLDER/includes/gateways/class-canned-api-gateway.php" 38 | 39 | mkdir -p "$PLUGIN_DIR/release" 40 | cd "$RELEASE_FOLDER" && zip -r "$PLUGIN_DIR/release/$RELEASE_ZIP_FILENAME" "crowdsignal-forms" 41 | 42 | echo "Release zip: $PLUGIN_DIR/release/$RELEASE_ZIP_FILENAME" 43 | rm -rf "$RELEASE_BUILD_FOLDER" 44 | -------------------------------------------------------------------------------- /tests-js/mocks/blocks.js: -------------------------------------------------------------------------------- 1 | export const registerBlockStyle = ( blockName, styleObject ) => null; 2 | 3 | export const unregisterBlockStyle = ( blockName, styleName ) => null; 4 | -------------------------------------------------------------------------------- /tests-js/mocks/i18n.js: -------------------------------------------------------------------------------- 1 | export const __ = ( string ) => string; 2 | 3 | export const _n = ( string ) => string; 4 | 5 | export const sprintf = ( string ) => string; 6 | -------------------------------------------------------------------------------- /tests/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/crowdsignal-forms/e0084d0760a16babe9ae441e24de3f4910d57b27/tests/.keep -------------------------------------------------------------------------------- /tests/canned-data/api-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "polls": [ 3 | { 4 | "id": 1, 5 | "question": "What is your favorite color?", 6 | "answers": [ 7 | { 8 | "id": 123, 9 | "answer_text": "blue", 10 | "answer_count": 42 11 | }, 12 | { 13 | "id": 1234, 14 | "answer_text": "green", 15 | "answer_count": 11 16 | } 17 | ] 18 | }, 19 | { 20 | "id": 2, 21 | "question": "What is your favorite 80's disco song?", 22 | "answers": [ 23 | { 24 | "id": 321, 25 | "answer_text": "Disco was in the 70s!", 26 | "answer_count": 42 27 | }, 28 | { 29 | "id": 3210, 30 | "answer_text": "None of the above.", 31 | "answer_count": 11 32 | } 33 | ] 34 | } 35 | ], 36 | "capabilities": [ 37 | "unlimited-questions", 38 | "device-report", 39 | "unlimited-email-responses", 40 | "custom-style", 41 | "feature-poll", 42 | "feature-rating", 43 | "feature-survey", 44 | "feature-quiz", 45 | "dashboard", 46 | "dashboard-migrate", 47 | "map-domain", 48 | "custom-email-notification-address", 49 | "export-pdf", 50 | "export", 51 | "whitelist-maximum-polls", 52 | "survey-custom-finish", 53 | "ssl", 54 | "email", 55 | "hide-branding", 56 | "survey-custom-url", 57 | "survey-responses", 58 | "filter", 59 | "share", 60 | "poll-reports", 61 | "public-json-polls", 62 | "survey-restrictions", 63 | "poll-results-embed", 64 | "poll-restrictions", 65 | "sync-to-external-services", 66 | "survey-timeout" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /tests/canned-data/block-data-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "pollId": 1, 3 | "isMultipleChoice": false, 4 | "title": "Untitled Poll", 5 | "question": "", 6 | "note": "", 7 | "answers": [ 8 | { 9 | "answerId": 1, 10 | "text": "" 11 | }, 12 | { 13 | "answerId": 2, 14 | "text": "" 15 | } 16 | ], 17 | "submitButtonLabel": "Submit", 18 | "submitButtonTextColor": "", 19 | "submitButtonBackgroundColor": "", 20 | "confirmMessageType": "results", 21 | "customConfirmMessage": "", 22 | "redirectAddress": "", 23 | "textColor": "", 24 | "backgroundColor": "", 25 | "borderColor": "", 26 | "borderWidth": 2, 27 | "borderRadius": 0, 28 | "hasBoxShadow": true, 29 | "fontFamily": null, 30 | "hasOneResponsePerComputer": false, 31 | "randomizeAnswers": false, 32 | "blockAlignment": "center", 33 | "pollStatus": "open", 34 | "closedPollState": "show-results", 35 | "closedAfterDateTime": null 36 | } 37 | -------------------------------------------------------------------------------- /tests/framework/class-crowdsignal-forms-unit-test-case.php: -------------------------------------------------------------------------------- 1 | default_user_id = get_current_user_id(); 18 | } 19 | 20 | /** 21 | * Retrieve a test user of a particular role. 22 | * 23 | * @param string $role Role of the user to create. 24 | * @return int 25 | */ 26 | protected function get_user_by_role( $role ) { 27 | $user_prefix = 'crowdsignal_forms_'; 28 | $user = get_user_by( 'email', $user_prefix . $role . '_user@example.com' ); 29 | if ( empty( $user ) ) { 30 | $user_id = wp_create_user( 31 | $user_prefix . $role . '_user', 32 | $user_prefix . $role . '_user', 33 | $user_prefix . $role . '_user@example.com' 34 | ); 35 | $user = get_user_by( 'ID', $user_id ); 36 | $user->set_role( $role ); 37 | } 38 | return $user->ID; 39 | } 40 | 41 | /** 42 | * Login as an admin user. 43 | * 44 | * @return self 45 | */ 46 | protected function login_as_admin() { 47 | return $this->login_as( $this->get_user_by_role( 'administrator' ) ); 48 | } 49 | 50 | /** 51 | * Login as an editor user. 52 | * 53 | * @return self 54 | */ 55 | protected function login_as_editor() { 56 | return $this->login_as( $this->get_user_by_role( 'editor' ) ); 57 | } 58 | 59 | /** 60 | * Login as the default user. 61 | * 62 | * @return self 63 | */ 64 | protected function login_as_default_user() { 65 | return $this->login_as( $this->default_user_id ); 66 | } 67 | 68 | /** 69 | * Login as a particular user. 70 | * 71 | * @param int $user_id ID for the user to login as. 72 | * @return self 73 | */ 74 | protected function login_as( $user_id ) { 75 | wp_set_current_user( $user_id ); 76 | return $this; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /tests/unit-tests/includes/auth/test-class.crowdsignal-forms-api-auth-test.php: -------------------------------------------------------------------------------- 1 | assertEquals( 'test-user-code', $cs_auth->get_user_code() ); 33 | } 34 | } 35 | 36 | class TestProvider implements Api_Auth_Provider_Interface { 37 | 38 | public function get_user_code( $user_id ) { 39 | return 'test-user-code'; 40 | } 41 | 42 | /** 43 | * @inheritDoc 44 | */ 45 | public function fetch_user_code( $user_id ) { 46 | // TODO: Implement fetch_user_code() method. 47 | return 'test-user-code'; 48 | } 49 | 50 | /** 51 | * @inheritDoc 52 | */ 53 | public function fetch_user_code_for_key( $api_key ) { 54 | // TODO: Implement fetch_user_code_for_key() method. 55 | return 'test-user-code'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/unit-tests/includes/gateways/test-class.api-gateway-interface.php: -------------------------------------------------------------------------------- 1 | assertTrue( interface_exists('\Crowdsignal_Forms\Gateways\Api_Gateway_Interface' ) ); 22 | } 23 | 24 | /** 25 | * @covers \Crowdsignal_Forms\Gateways\Api_Gateway_Interface::get_poll\ 26 | * 27 | * @since 0.9.0 28 | */ 29 | public function testInterfaceDefinesGetPoll() { 30 | $this->assertTrue( method_exists('\Crowdsignal_Forms\Gateways\Api_Gateway_Interface', 'get_poll' ) ); 31 | } 32 | 33 | /** 34 | * @covers \Crowdsignal_Forms\Gateways\Api_Gateway_Interface::create_poll 35 | * 36 | * @since 0.9.0 37 | */ 38 | public function testInterfaceDefinesCreatePoll() { 39 | $this->assertTrue( method_exists('\Crowdsignal_Forms\Gateways\Api_Gateway_Interface', 'create_poll' ) ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/unit-tests/includes/gateways/test-class.api-gateway.php: -------------------------------------------------------------------------------- 1 | assertTrue( class_exists('\Crowdsignal_Forms\Gateways\Api_Gateway' ) ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/unit-tests/includes/gateways/test-class.canned-api-gateway.php: -------------------------------------------------------------------------------- 1 | assertTrue( class_exists('\Crowdsignal_Forms\Gateways\Canned_Api_Gateway' ) ); 21 | } 22 | 23 | /** 24 | * @covers \Crowdsignal_Forms\Gateways\Canned_Api_Gateway::get_poll 25 | * 26 | * @since 0.9.0 27 | */ 28 | public function test_get_poll_returns_poll_if_in_canned_data() { 29 | $gateway = new Gateways\Canned_Api_Gateway(); 30 | $poll = $gateway->get_poll( 1 ); 31 | $this->assertTrue( ! is_wp_error( $poll ) ); 32 | } 33 | 34 | /** 35 | * @covers \Crowdsignal_Forms\Gateways\Canned_Api_Gateway::get_poll 36 | * 37 | * @since 0.9.0 38 | */ 39 | public function test_get_poll_returns_error_if_not_in_canned_data() { 40 | $gateway = new Gateways\Canned_Api_Gateway(); 41 | $poll = $gateway->get_poll( 666 ); 42 | $this->assertTrue( is_wp_error( $poll ) ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/unit-tests/includes/models/test-class.poll-settings.php: -------------------------------------------------------------------------------- 1 | tests_dir; 30 | $data = file_get_contents( $tests_dir . '/canned-data/block-data-empty.json' ); 31 | $poll_settings = Poll_Settings::from_array( json_decode( $data, true ) ); 32 | $this->assertTrue( is_a( $poll_settings, Poll_Settings::class ) ); 33 | $poll_array = $poll_settings->to_array(); 34 | $this->assertTrue( array_key_exists( 'title', $poll_array ), 'Poll array should have a "title" prop' ); 35 | $this->assertTrue( array_key_exists( 'after_vote', $poll_array ), 'Poll array should have a "after_vote" prop' ); 36 | $this->assertTrue( array_key_exists( 'after_message', $poll_array ), 'Poll array should have a "after_message" prop' ); 37 | $this->assertTrue( array_key_exists( 'redirect_url', $poll_array ), 'Poll array should have a "redirect_url" prop' ); 38 | $this->assertTrue( array_key_exists( 'randomize_answers', $poll_array ), 'Poll array should have a "randomize_answers" prop' ); 39 | $this->assertTrue( array_key_exists( 'restrict_vote_repeat', $poll_array ), 'Poll array should have a "restrict_vote_repeat" prop' ); 40 | $this->assertTrue( array_key_exists( 'captcha', $poll_array ), 'Poll array should have a "captcha" prop' ); 41 | $this->assertTrue( array_key_exists( 'multiple_choice', $poll_array ), 'Poll array should have a "multiple_choice" prop' ); 42 | $this->assertTrue( array_key_exists( 'close_status', $poll_array ), 'Poll array should have a "close_status" prop' ); 43 | $this->assertTrue( array_key_exists( 'close_after', $poll_array ), 'Poll array should have a "close_after" prop' ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/unit-tests/includes/models/test-class.poll.php: -------------------------------------------------------------------------------- 1 | assertTrue( is_a( $poll,Poll::class ) ); 31 | $this->assertSame( 0, $poll->get_id() ); 32 | $this->assertSame( '', $poll->get_question() ); 33 | $this->assertSame( 0, count( $poll->get_answers() ) ); 34 | } 35 | 36 | /** 37 | * @covers \Crowdsignal_Forms\Rest_Api\Controllers\Poll::from_array 38 | * 39 | * @since 0.9.0 40 | */ 41 | public function test_from_array() { 42 | $data = array( 43 | 'answers' => array(), 44 | 'settings' => array(), 45 | 'id' => 1, 46 | 'question' => 'Best Sci-fi film ever?' 47 | ); 48 | $poll = Poll::from_array( $data ); 49 | $this->assertTrue( is_a( $poll,Poll::class ) ); 50 | $this->assertSame( 1, $poll->get_id() ); 51 | $this->assertSame( 'Best Sci-fi film ever?', $poll->get_question() ); 52 | $this->assertSame( 0, count( $poll->get_answers() ) ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/unit-tests/includes/rest-api/controllers/test-class.account-controller.php: -------------------------------------------------------------------------------- 1 | server = $wp_rest_server; 31 | 32 | do_action( 'rest_api_init' ); 33 | $this->controller = new Account_Controller(); 34 | } 35 | 36 | /** 37 | * Test specific teardown. 38 | * @since 0.9.0 39 | */ 40 | public function tearDown() { 41 | parent::tearDown(); 42 | 43 | global $wp_rest_server; 44 | $wp_rest_server = null; 45 | } 46 | 47 | /** 48 | * @covers \Crowdsignal_Forms\Rest_Api\Controllers\Polls_Controller::get_poll 49 | * 50 | * @since 0.9.0 51 | */ 52 | public function test_get_capabilities() { 53 | Crowdsignal_Forms\Crowdsignal_Forms::instance()->set_api_gateway( new Canned_Api_Gateway() ); 54 | $req = new \WP_REST_Request( 'GET', '/account/capabilities' ); 55 | $response = $this->controller->get_capabilities( $req ); 56 | $this->assertTrue( is_a( $response, \WP_REST_Response::class ) ); 57 | $this->assertTrue( $response->get_status() === 200 ); 58 | $this->assertTrue( in_array( 'hide-branding', $response->data ) ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 | 33 | plugin.constructor.name !== 34 | 'DependencyExtractionWebpackPlugin' && 35 | plugin.constructor.name !== 'CleanWebpackPlugin' 36 | ), 37 | new DependencyExtractionWebpackPlugin( { 38 | injectPolyfill: true, 39 | requestToExternal: ( request ) => { 40 | if ( request === '@crowdsignalForms/apifetch' ) { 41 | return [ 'crowdsignalForms', 'apiFetch' ]; 42 | } 43 | }, 44 | requestToHandle: ( request ) => { 45 | // These values must match the names defined in class-crowdsignal-forms-blocks-assets.php 46 | if ( request === '@crowdsignalForms/apifetch' ) { 47 | return 'crowdsignal-forms-apifetch'; 48 | } 49 | }, 50 | } ), 51 | ], 52 | externals: { 53 | ...webpackConfig.externals, 54 | jquery: 'jQuery', 55 | react: 'React', 56 | 'react-dom': 'ReactDOM', 57 | lodash: 'lodash', 58 | }, 59 | }; 60 | 61 | return config; 62 | }; 63 | --------------------------------------------------------------------------------