mirror of
https://github.com/IT4Change/ohmyform-ui.git
synced 2026-01-20 19:31:17 +00:00
Compare commits
No commits in common. "master" and "0.9.0" have entirely different histories.
74
.eslintrc.js
74
.eslintrc.js
@ -1,74 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
ecmaFeatures: { jsx: true },
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
project: ['./tsconfig.json'],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
'@typescript-eslint/eslint-plugin',
|
|
||||||
'@typescript-eslint',
|
|
||||||
'unused-imports'
|
|
||||||
],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:jsx-a11y/recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
|
||||||
'prettier',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
||||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'react/prop-types': 'off',
|
|
||||||
'@typescript-eslint/no-empty-interface': 'off',
|
|
||||||
'jsx-a11y/no-autofocus': 'off',
|
|
||||||
'array-element-newline': ['error', {
|
|
||||||
'ArrayExpression': 'consistent',
|
|
||||||
'ArrayPattern': {
|
|
||||||
'minItems': 3,
|
|
||||||
'multiline': true,
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
'array-bracket-newline': ['error', {
|
|
||||||
'minItems': 3,
|
|
||||||
'multiline': true,
|
|
||||||
}],
|
|
||||||
'indent': [
|
|
||||||
'error',
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
'SwitchCase': 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'no-tabs': ['error'],
|
|
||||||
'max-len': ['error', {
|
|
||||||
'code': 100,
|
|
||||||
'ignoreComments': true,
|
|
||||||
'ignoreUrls': true,
|
|
||||||
'ignoreTemplateLiterals': true,
|
|
||||||
'ignoreTrailingComments': true,
|
|
||||||
'ignoreStrings': true,
|
|
||||||
}],
|
|
||||||
'quotes': ['error', 'single', { 'avoidEscape': true }],
|
|
||||||
'comma-dangle': ['error', 'always-multiline'],
|
|
||||||
'linebreak-style': [
|
|
||||||
'error',
|
|
||||||
'unix'
|
|
||||||
],
|
|
||||||
'no-trailing-spaces': 'error',
|
|
||||||
'eol-last': 'error',
|
|
||||||
'unused-imports/no-unused-imports': 'error',
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: {
|
|
||||||
version: 'detect',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
43
.github/workflows/docker-image.yml
vendored
43
.github/workflows/docker-image.yml
vendored
@ -1,43 +0,0 @@
|
|||||||
name: Docker Image CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
release:
|
|
||||||
types:
|
|
||||||
- published
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Push Docker image to Docker Hub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
|
||||||
with:
|
|
||||||
images: ohmyform/ui
|
|
||||||
tags: |
|
|
||||||
type=raw,value=latest
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
37
.github/workflows/test.yml
vendored
37
.github/workflows/test.yml
vendored
@ -1,37 +0,0 @@
|
|||||||
name: Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run-linters:
|
|
||||||
name: Run linters
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Check out Git repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 16
|
|
||||||
|
|
||||||
# ESLint and Prettier must be in `package.json`
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
run: yarn install --frozen-lockfile --silent
|
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
uses: reviewdog/action-eslint@v1
|
|
||||||
with:
|
|
||||||
reporter: github-pr-review # Change reporter.
|
|
||||||
eslint_flags: 'pages/ store/ components/ graphql/'
|
|
||||||
|
|
||||||
- name: Typecheck
|
|
||||||
uses: andoshin11/typescript-error-reporter-action@v1.0.2
|
|
||||||
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -26,8 +26,6 @@ yarn-error.log*
|
|||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env
|
|
||||||
|
|
||||||
# development environments
|
# development environments
|
||||||
/.idea
|
/.idea
|
||||||
schema.graphql
|
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "OhMyForm API GraphQL Schema",
|
|
||||||
"schemaPath": "./schema.graphql",
|
|
||||||
"extensions": {
|
|
||||||
"endpoints": {
|
|
||||||
"OhMyForm API GraphQL Endpoint1": {
|
|
||||||
"url": "http://localhost:4100/graphql",
|
|
||||||
"headers": {
|
|
||||||
"user-agent": "JS GraphQL"
|
|
||||||
},
|
|
||||||
"introspect": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
semi: false,
|
|
||||||
trailingComma: 'es5',
|
|
||||||
singleQuote: true,
|
|
||||||
printWidth: 100,
|
|
||||||
tabWidth: 2,
|
|
||||||
useTabs: false,
|
|
||||||
}
|
|
||||||
249
CHANGELOG.md
249
CHANGELOG.md
@ -1,249 +0,0 @@
|
|||||||
# Change Log
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Template for next version
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
### Security
|
|
||||||
-->
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- node prune location (https://github.com/ohmyform/ohmyform/issues/184)
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
## [1.0.3] - 2022-03-27
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- default form now has an end page
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- sorting of fields in excel export
|
|
||||||
|
|
||||||
## [1.0.2] - 2022-03-13
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- field sort in excel submission export (https://github.com/ohmyform/ohmyform/issues/163)
|
|
||||||
|
|
||||||
## [1.0.1] - 2022-03-01
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- map field type
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- update translations (https://github.com/ohmyform/ui/pull/70)
|
|
||||||
- show warning icon in form list if not public
|
|
||||||
- default form layout is now "card"
|
|
||||||
- creating of new fields combined in new field types
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- locale scripts were missing dependency
|
|
||||||
- edit user shows now email in title
|
|
||||||
- focus is now passed also do slide layout fields
|
|
||||||
- empty fields are no longer submitted
|
|
||||||
- stuttery form because of logic rerenders
|
|
||||||
|
|
||||||
## [1.0.0] - 2022-02-28
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- ability to change user passwords
|
|
||||||
- add default page background
|
|
||||||
- add environment list in [doc](doc/environment.md)
|
|
||||||
- show error message on homepage in case there is a problem with api connection
|
|
||||||
- new slider field type
|
|
||||||
- new card layout for forms
|
|
||||||
- field logic
|
|
||||||
- add environment config
|
|
||||||
- anonymous form submissions (fixes https://github.com/ohmyform/ohmyform/issues/108)
|
|
||||||
- checkbox field type (fixed https://github.com/ohmyform/ohmyform/issues/138)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- combined notificationts to become more versatile
|
|
||||||
- use exported hooks for graphql
|
|
||||||
- disable swipe gesture
|
|
||||||
- upgrade to nextjs 12
|
|
||||||
- change default value from value to defaultValue
|
|
||||||
- handle options and values as json correctly
|
|
||||||
- exclude empty submissions per default (https://github.com/ohmyform/ohmyform/issues/153)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- links at the bottom for new users
|
|
||||||
- fixes for hide contrib setting
|
|
||||||
- fix problem with node-prune on production build
|
|
||||||
- translation for prev / continue during form submission
|
|
||||||
- reload form list after adding new one (https://github.com/ohmyform/ohmyform/issues/139)
|
|
||||||
- android screen size fix (https://github.com/ohmyform/ohmyform/issues/114)
|
|
||||||
- sending finish mutation (https://github.com/ohmyform/ui/pull/67)
|
|
||||||
- fix dev documentation (https://github.com/ohmyform/ui/issues/65)
|
|
||||||
- remove next/image as it does not work with static exports (https://github.com/ohmyform/ohmyform/issues/154)
|
|
||||||
- switch back to form.prefixName (https://github.com/ohmyform/ohmyform/issues/150)
|
|
||||||
- upgrade all packages to latest versions
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- upgrad all packages
|
|
||||||
|
|
||||||
## [0.9.9] - 2021-02-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Submission export
|
|
||||||
- Lokalize reference
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- updated french translations by @Vercety87
|
|
||||||
- upgrade to node 14 (https://github.com/ohmyform/ohmyform/issues/99)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- missing dependency to @apollo/client
|
|
||||||
- footer rendering during authentication check
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- authentication check for profile page
|
|
||||||
|
|
||||||
## [0.9.8] - 2020-09-02
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- menu selection type
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
## [0.9.7] - 2020-09-02
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- improved german translation (https://github.com/ohmyform/ui/pull/28)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- colors for landing page buttons
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- upgraded dependencies
|
|
||||||
|
|
||||||
## [0.9.6] - 2020-07-17
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- slug for fields to be able to set value by url parameter
|
|
||||||
- form submission hokks
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- minify containers to reduce layer size
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- do not show login note if it is not set
|
|
||||||
- typo in dropdown options https://github.com/ohmyform/ohmyform/issues/96
|
|
||||||
- query parms are not parsed https://github.com/ohmyform/ui/pull/27 https://github.com/ohmyform/ohmyform/issues/100
|
|
||||||
- errors because of missing user reference (https://github.com/ohmyform/ohmyform/issues/102)
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- container now runs as non root user
|
|
||||||
|
|
||||||
## [0.9.5] - 2020-06-10
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- mobile improvements for lists and home page
|
|
||||||
- markdown support for page paragraphs and field description
|
|
||||||
- hideable omf badge
|
|
||||||
- login notes
|
|
||||||
- username in admin toolbar
|
|
||||||
- github stars in multiple places
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- verified spanish translations https://github.com/ohmyform/ui/pull/23
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- yes / no field fixed on admin and user view
|
|
||||||
- prev property error on div
|
|
||||||
- rating field default on admin
|
|
||||||
- number field defaults
|
|
||||||
- translations for field validation
|
|
||||||
- number validation
|
|
||||||
- side menu only shows accessible entries
|
|
||||||
|
|
||||||
## [0.9.4] - 2020-06-09
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Fetch Server Settings to determine if signup is available
|
|
||||||
- `SPA` env variable to have static page with loading spinner before redirect
|
|
||||||
- `de`, `fr`, `es`, `it`, `cn` base folders for translations
|
|
||||||
- finish translating `de` and `en`
|
|
||||||
- add `yarn translation:sort` to order translations (to ensure the same order
|
|
||||||
when we add / change translations)
|
|
||||||
- add `yarn translation:missing <lang>` to print a list of missing translations
|
|
||||||
for the given language (this takes `en` as a baseline)
|
|
||||||
- travis for tests
|
|
||||||
- eslint with prettier
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `export` uses now spa mode for initial loading screen
|
|
||||||
- change value to defaultValue for initial form
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- dropdown options are not saved (https://github.com/ohmyform/ohmyform/issues/93)
|
|
||||||
- redirect attempts on static export
|
|
||||||
- date can now be prefilled by url
|
|
||||||
|
|
||||||
## [0.9.2] - 2020-06-04
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- type error
|
|
||||||
|
|
||||||
## [0.9.1] - 2020-06-02
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- radio fields
|
|
||||||
- dropdown fields
|
|
||||||
- min and max for date fields
|
|
||||||
- logout on home screen
|
|
||||||
- translation system
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- initial Page is now correct also in SPA mode
|
|
||||||
- initial value for form adding
|
|
||||||
- anonymous submission of forms
|
|
||||||
|
|
||||||
31
Dockerfile
31
Dockerfile
@ -1,39 +1,12 @@
|
|||||||
FROM node:14-alpine AS builder
|
FROM node:12-alpine
|
||||||
MAINTAINER OhMyForm <admin@ohmyform.com>
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apk --update --no-cache add curl bash g++ make libpng-dev
|
|
||||||
|
|
||||||
# install node-prune (https://github.com/tj/node-prune)
|
|
||||||
RUN curl -sf https://gobinaries.com/tj/node-prune | sh
|
|
||||||
|
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
|
||||||
RUN yarn install --frozen-lock-file
|
RUN yarn install --frozen-lock-file
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
# remove development dependencies
|
ENV PORT=4000
|
||||||
RUN npm prune --production
|
|
||||||
|
|
||||||
# run node prune
|
|
||||||
# there is some problem running node prune that then prevents the frontend to load (just start with /form/1 and it will crash)
|
|
||||||
#RUN /usr/local/bin/node-prune
|
|
||||||
|
|
||||||
FROM node:14-alpine
|
|
||||||
MAINTAINER OhMyForm <admin@ohmyform.com>
|
|
||||||
|
|
||||||
# Create a group and a user with name "ohmyform".
|
|
||||||
RUN addgroup --gid 9999 ohmyform && adduser -D --uid 9999 -G ohmyform ohmyform
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY --from=builder /usr/src/app /usr/src/app
|
|
||||||
|
|
||||||
ENV PORT=4000 \
|
|
||||||
NODE_ENV=production
|
|
||||||
|
|
||||||
# Change to non-root privilege
|
|
||||||
USER ohmyform
|
|
||||||
|
|
||||||
CMD [ "yarn", "start" ]
|
CMD [ "yarn", "start" ]
|
||||||
|
|||||||
682
LICENSE.md
682
LICENSE.md
@ -1,661 +1,21 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
## License
|
||||||
Version 3, 19 November 2007
|
(The MIT License)
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
a copy of this software and associated documentation files (the
|
||||||
of this license document, but changing it is not allowed.
|
'Software'), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
Preamble
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
the following conditions:
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
share and change all versions of a program--to make sure it remains free
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
software for all its users.
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
When we speak of free software, we are referring to freedom, not
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|||||||
15
README.md
15
README.md
@ -1,17 +1,6 @@
|
|||||||
# OhMyForm UI
|
# OhMyForm UI
|
||||||
|
|
||||||
[](https://travis-ci.org/ohmyform/ui)
|
[](https://opencollective.com/ohmyform-sustainability) ![Project Status]
|
||||||

|
|
||||||
[](https://hub.docker.com/r/ohmyform/ui)
|
|
||||||
[](https://app.lokalise.com/public/379418475ede5d5c6937b0.31012044/)
|
|
||||||

|
|
||||||
|
|
||||||
[Demo](https://demo.ohmyform.com/login)
|
|
||||||
|
|
||||||
> An *open source alternative to TypeForm* that can create stunning mobile-ready forms, surveys and questionnaires.
|
|
||||||
|
|
||||||
[](https://discord.gg/MJqAuAZ)
|
[](https://discord.gg/MJqAuAZ)
|
||||||
[](https://opencollective.com/ohmyform-sustainability)
|
> An *open source alternative to TypeForm* that can create stunning mobile-ready forms, surveys and questionnaires.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
@import "variables";
|
@import "variables";
|
||||||
@import "node_modules/swiper/swiper.scss";
|
@import "node_modules/swiper/swiper.scss";
|
||||||
@import "../node_modules/react-github-button/assets/style.css";
|
|
||||||
@import "../node_modules/leaflet/dist/leaflet.css";
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--backgroundColor: #{$background-color};
|
--backgroundColor: #{$background-color};
|
||||||
@ -26,33 +24,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-height {
|
|
||||||
height: 100vh;
|
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-spin-nested-loading > div > .ant-spin {
|
.ant-spin-nested-loading > div > .ant-spin {
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.swiper-container {
|
.swiper-container {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: calc(var(--vh, 1vh) * 100);
|
|
||||||
|
|
||||||
.swiper-wrapper {
|
.swiper-wrapper {
|
||||||
position: fixed
|
position: fixed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin {
|
|
||||||
.sidemenu {
|
|
||||||
.ant-layout-sider-children {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.language-selector {
|
|
||||||
padding-left: 12px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 618 B |
@ -1,18 +0,0 @@
|
|||||||
.footer {
|
|
||||||
position: absolute;
|
|
||||||
padding-left: 16px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,142 +1,54 @@
|
|||||||
import { Button, Select } from 'antd'
|
import {Button} from 'antd'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import GitHubButton from 'react-github-button'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useSettingsQuery } from '../../graphql/query/settings.query'
|
|
||||||
import { languages } from '../../i18n'
|
|
||||||
import { clearAuth, withAuth } from '../with.auth'
|
|
||||||
import scss from './footer.module.scss'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
me?: {
|
|
||||||
id: string
|
|
||||||
username: string
|
|
||||||
roles: string[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthFooterInner: React.FC<Props> = (props) => {
|
|
||||||
const { t, i18n } = useTranslation()
|
|
||||||
const router = useRouter()
|
|
||||||
const { data, loading } = useSettingsQuery()
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
clearAuth()
|
|
||||||
router.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export const AuthFooter: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<footer className={scss.footer}>
|
<div
|
||||||
{props.me
|
|
||||||
? [
|
|
||||||
<span style={{ color: '#FFF' }} key={'user'}>
|
|
||||||
Hi, {props.me.username}
|
|
||||||
</span>,
|
|
||||||
props.me.roles.includes('admin') && (
|
|
||||||
<Link key={'admin'} href={'/admin'}>
|
|
||||||
<Button
|
|
||||||
type={'link'}
|
|
||||||
style={{
|
style={{
|
||||||
color: '#FFF',
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('admin')}
|
<Link href={'/admin'}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
ghost
|
||||||
|
>
|
||||||
|
Admin
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
),
|
<Link href={'/login'}>
|
||||||
<Link key={'profile'} href={'/admin/profile'}>
|
|
||||||
<Button
|
<Button
|
||||||
type={'link'}
|
type={'link'}
|
||||||
style={{
|
ghost
|
||||||
color: '#FFF',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t('profile')}
|
Login
|
||||||
</Button>
|
|
||||||
</Link>,
|
|
||||||
<Button
|
|
||||||
key={'logout'}
|
|
||||||
type={'link'}
|
|
||||||
onClick={logout}
|
|
||||||
style={{
|
|
||||||
color: '#FFF',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('logout')}
|
|
||||||
</Button>,
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
<Link href={'/login'} key={'login'}>
|
|
||||||
<Button
|
|
||||||
type={'link'}
|
|
||||||
style={{
|
|
||||||
color: '#FFF',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('login')}
|
|
||||||
</Button>
|
|
||||||
</Link>,
|
|
||||||
!loading && !data?.disabledSignUp.value && (
|
|
||||||
<Link href={'/register'} key={'register'}>
|
|
||||||
<Button
|
|
||||||
type={'link'}
|
|
||||||
style={{
|
|
||||||
color: '#FFF',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('register')}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
),
|
<Link href={'/register'}>
|
||||||
]}
|
<Button
|
||||||
<div style={{ flex: 1 }} />
|
type={'link'}
|
||||||
<Select
|
ghost
|
||||||
bordered={false}
|
|
||||||
value={i18n.language.replace(/-.*/, '')}
|
|
||||||
onChange={(next) => i18n.changeLanguage(next)}
|
|
||||||
style={{
|
|
||||||
color: '#FFF',
|
|
||||||
paddingLeft: 18,
|
|
||||||
}}
|
|
||||||
suffixIcon={false}
|
|
||||||
>
|
>
|
||||||
{languages.map((language) => (
|
Register
|
||||||
<Select.Option value={language} key={language}>
|
</Button>
|
||||||
{t(`language:${language}`)}
|
</Link>
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{!loading && !data?.hideContrib.value && (
|
|
||||||
<>
|
|
||||||
<GitHubButton type="stargazers" namespace="ohmyform" repo="ohmyform" />
|
|
||||||
<Button
|
<Button
|
||||||
type={'link'}
|
type={'link'}
|
||||||
target={'_blank'}
|
target={'_blank'}
|
||||||
rel={'noreferrer'}
|
ghost
|
||||||
href={'https://www.ohmyform.com'}
|
href={'https://www.ohmyform.com'}
|
||||||
style={{
|
style={{
|
||||||
color: '#FFF',
|
float: 'right',
|
||||||
|
color: '#FFF'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
OhMyForm
|
© OhMyForm
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</div>
|
||||||
type={'link'}
|
|
||||||
target={'_blank'}
|
|
||||||
rel={'noreferrer'}
|
|
||||||
href={'https://lokalise.com/'}
|
|
||||||
style={{
|
|
||||||
color: '#FFF',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
translated with Lokalize
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</footer>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthFooter = withAuth(AuthFooterInner, [], true)
|
|
||||||
|
|||||||
@ -1,23 +1,17 @@
|
|||||||
import { Layout, Spin } from 'antd'
|
import {Layout, Spin} from 'antd'
|
||||||
import getConfig from 'next/config'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { NextConfigType } from '../../next.config.type'
|
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig() as NextConfigType
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthLayout: React.FC<Props> = (props) => {
|
export const AuthLayout: React.FC<Props> = props => {
|
||||||
return (
|
return (
|
||||||
<Spin spinning={props.loading || false}>
|
<Spin spinning={props.loading}>
|
||||||
<Layout
|
<Layout style={{
|
||||||
style={{
|
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
background: publicRuntimeConfig.mainBackground,
|
background: '#437fdc'
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable */
|
|
||||||
const omitDeepArrayWalk = (arr, key) => {
|
const omitDeepArrayWalk = (arr, key) => {
|
||||||
return arr.map((val) => {
|
return arr.map((val) => {
|
||||||
if (Array.isArray(val)) return omitDeepArrayWalk(val, key)
|
if (Array.isArray(val)) return omitDeepArrayWalk(val, key)
|
||||||
@ -8,18 +8,18 @@ const omitDeepArrayWalk = (arr, key) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const omitDeep = (obj: any, key: string | number): any => {
|
const omitDeep = (obj: any, key: string | number): any => {
|
||||||
const keys: Array<any> = Object.keys(obj)
|
const keys: Array<any> = Object.keys(obj);
|
||||||
const newObj: any = {}
|
const newObj: any = {};
|
||||||
keys.forEach((i: any) => {
|
keys.forEach((i: any) => {
|
||||||
if (i !== key) {
|
if (i !== key) {
|
||||||
const val: any = obj[i]
|
const val: any = obj[i];
|
||||||
if (val instanceof Date) newObj[i] = val
|
if (val instanceof Date) newObj[i] = val;
|
||||||
else if (Array.isArray(val)) newObj[i] = omitDeepArrayWalk(val, key)
|
else if (Array.isArray(val)) newObj[i] = omitDeepArrayWalk(val, key);
|
||||||
else if (typeof val === 'object' && val !== null) newObj[i] = omitDeep(val, key)
|
else if (typeof val === 'object' && val !== null) newObj[i] = omitDeep(val, key);
|
||||||
else newObj[i] = val
|
else newObj[i] = val;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return newObj
|
return newObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cleanInput = <T>(obj: T): T => {
|
export const cleanInput = <T>(obj: T): T => {
|
||||||
|
|||||||
@ -7,15 +7,13 @@ interface Props {
|
|||||||
hideTime?: boolean
|
hideTime?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DateTime: React.FC<Props> = (props) => {
|
export const DateTime: React.FC<Props> = props => {
|
||||||
const format = props.hideTime ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm'
|
const format = props.hideTime ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{
|
||||||
style={{
|
display: 'inline-block'
|
||||||
display: 'inline-block',
|
}}>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{dayjs(props.date).format(format)}
|
{dayjs(props.date).format(format)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,15 +2,13 @@ import React from 'react'
|
|||||||
|
|
||||||
export const ErrorPage: React.FC = () => {
|
export const ErrorPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{
|
||||||
style={{
|
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<h1>ERROR</h1>
|
<h1>ERROR</h1>
|
||||||
<p>there was an error with your request</p>
|
<p>there was an error with your request</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
import { Form, Input, Select, Switch, Tabs } from 'antd'
|
import {Form, Input, Select, Switch, Tabs} from 'antd'
|
||||||
import { TabPaneProps } from 'antd/lib/tabs'
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import {languages} from '../../../i18n'
|
||||||
import { languages } from '../../../i18n'
|
|
||||||
|
|
||||||
export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
|
export const BaseDataTab: React.FC<TabPaneProps> = props => {
|
||||||
return (
|
return (
|
||||||
<Tabs.TabPane {...props}>
|
<Tabs.TabPane {...props}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:baseData.isLive')}
|
label="Is Live"
|
||||||
name={['form', 'isLive']}
|
name={['form', 'isLive']}
|
||||||
valuePropName={'checked'}
|
valuePropName={'checked'}
|
||||||
>
|
>
|
||||||
@ -18,12 +15,12 @@ export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:baseData.title')}
|
label="Title"
|
||||||
name={['form', 'title']}
|
name={['form', 'title']}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t('validation:titleRequired'),
|
message: 'Please provide a Title',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@ -31,39 +28,28 @@ export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:baseData.language')}
|
label="Language"
|
||||||
name={['form', 'language']}
|
name={['form', 'language']}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t('validation:languageRequired'),
|
message: 'Please select a Language',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select>
|
<Select>
|
||||||
{languages.map((language) => (
|
{languages.map(language => <Select.Option value={language} key={language}>{language.toUpperCase()}</Select.Option> )}
|
||||||
<Select.Option value={language} key={language}>
|
|
||||||
{t(`language:${language}`)}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:baseData.showFooter')}
|
label="Show Footer"
|
||||||
name={['form', 'showFooter']}
|
name={['form', 'showFooter']}
|
||||||
valuePropName={'checked'}
|
valuePropName={'checked'}
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('form:baseData.anonymousSubmission')}
|
|
||||||
name={['form', 'anonymousSubmission']}
|
|
||||||
valuePropName={'checked'}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,46 +1,27 @@
|
|||||||
import { Form, Input, Select, Tabs } from 'antd'
|
import {Form, Input, Tabs} from 'antd'
|
||||||
import { TabPaneProps } from 'antd/lib/tabs'
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import {InputColor} from '../../input/color'
|
||||||
import { InputColor } from '../../input/color'
|
|
||||||
|
|
||||||
export const DesignTab: React.FC<TabPaneProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
|
export const DesignTab: React.FC<TabPaneProps> = props => {
|
||||||
return (
|
return (
|
||||||
<Tabs.TabPane {...props}>
|
<Tabs.TabPane {...props}>
|
||||||
<Form.Item label={t('form:design.font')} name={[
|
<Form.Item
|
||||||
'form', 'design', 'font',
|
label="Font"
|
||||||
]}>
|
name={['form', 'design', 'font']}
|
||||||
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t('form:design.layouts')} name={[
|
|
||||||
'form', 'design', 'layout',
|
|
||||||
]}>
|
|
||||||
<Select
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
value: null,
|
|
||||||
label: t('form:design.layout.slider'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'card',
|
|
||||||
label: t('form:design.layout.card'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{[
|
{[
|
||||||
'background', 'question', 'answer', 'button', 'buttonActive', 'buttonText',
|
{name: 'backgroundColor', label: 'Background Color'},
|
||||||
].map((name) => (
|
{name: 'questionColor', label: 'Question Color'},
|
||||||
<Form.Item
|
{name: 'answerColor', label: 'Answer Color'},
|
||||||
key={name}
|
{name: 'buttonColor', label: 'Button Color'},
|
||||||
label={t(`form:design.color.${name}`)}
|
{name: 'buttonActiveColor', label: 'Button Active Color'},
|
||||||
name={[
|
{name: 'buttonTextColor', label: 'Button Text Color'},
|
||||||
'form', 'design', 'colors', name,
|
].map(({label, name}) => (
|
||||||
]}
|
<Form.Item key={name} label={label} name={['form', 'design', 'colors', name]}>
|
||||||
>
|
|
||||||
<InputColor />
|
<InputColor />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,53 +1,44 @@
|
|||||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
|
import {DeleteOutlined, PlusOutlined} from '@ant-design/icons/lib'
|
||||||
import { Button, Card, Form, Input, Switch, Tabs } from 'antd'
|
import {Button, Card, Form, Input, Switch, Tabs} from 'antd'
|
||||||
import { TabPaneProps } from 'antd/lib/tabs'
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import {InputColor} from '../../input/color'
|
||||||
import { InputColor } from '../../input/color'
|
|
||||||
|
|
||||||
export const EndPageTab: React.FC<TabPaneProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
|
export const EndPageTab: React.FC<TabPaneProps> = props => {
|
||||||
return (
|
return (
|
||||||
<Tabs.TabPane {...props}>
|
<Tabs.TabPane {...props}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:endPage.show')}
|
label={'Show'}
|
||||||
name={[
|
name={['form', 'endPage', 'show']}
|
||||||
'form', 'endPage', 'show',
|
|
||||||
]}
|
|
||||||
valuePropName={'checked'}
|
valuePropName={'checked'}
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('form:endPage.title')} name={[
|
<Form.Item
|
||||||
'form', 'endPage', 'title',
|
label={'Title'}
|
||||||
]}>
|
name={['form', 'endPage', 'title']}
|
||||||
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:endPage.paragraph')}
|
label={'Paragraph'}
|
||||||
name={[
|
name={['form', 'endPage', 'paragraph']}
|
||||||
'form', 'endPage', 'paragraph',
|
|
||||||
]}
|
|
||||||
extra={t('type:descriptionInfo')}
|
|
||||||
>
|
>
|
||||||
<Input.TextArea autoSize />
|
<Input.TextArea autoSize />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:endPage.continueButtonText')}
|
label={'Continue Button Text'}
|
||||||
name={[
|
name={['form', 'endPage', 'buttonText']}
|
||||||
'form', 'endPage', 'buttonText',
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.List name={[
|
<Form.List
|
||||||
'form', 'endPage', 'buttons',
|
name={['form', 'endPage', 'buttons']}
|
||||||
]}>
|
>
|
||||||
{(fields, { add, remove }) => {
|
{(fields, { add, remove }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -56,56 +47,43 @@ export const EndPageTab: React.FC<TabPaneProps> = (props) => {
|
|||||||
wrapperCol={{
|
wrapperCol={{
|
||||||
sm: { offset: index === 0 ? 0 : 6 },
|
sm: { offset: index === 0 ? 0 : 6 },
|
||||||
}}
|
}}
|
||||||
label={index === 0 ? t('form:endPage.buttons') : ''}
|
label={index === 0 ? 'Buttons' : ''}
|
||||||
key={field.key}
|
key={field.key}
|
||||||
>
|
>
|
||||||
<Card actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}>
|
<Card
|
||||||
|
actions={[
|
||||||
|
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:endPage.url')}
|
label={'Url'}
|
||||||
name={[field.key, 'url']}
|
name={[field.key, 'url']}
|
||||||
rules={[{ type: 'url', message: t('validation:invalidUrl') }]}
|
rules={[
|
||||||
|
{type: 'url', message: 'Must be a valid url'}
|
||||||
|
]}
|
||||||
labelCol={{ span: 6 }}
|
labelCol={{ span: 6 }}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Action'} name={[field.key, 'action']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:endPage.action')}
|
|
||||||
name={[field.key, 'action']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Text'} name={[field.key, 'text']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:endPage.text')}
|
|
||||||
name={[field.key, 'text']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Background Color'} name={[field.key, 'bgColor']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:endPage.bgColor')}
|
|
||||||
name={[field.key, 'bgColor']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<InputColor />
|
<InputColor />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Active Color'} name={[field.key, 'activeColor']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:endPage.activeColor')}
|
|
||||||
name={[field.key, 'activeColor']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<InputColor />
|
<InputColor />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Color'} name={[field.key, 'color']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:endPage.color')}
|
|
||||||
name={[field.key, 'color']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<InputColor />
|
<InputColor />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Card>
|
</Card>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
wrapperCol={{
|
wrapperCol={{
|
||||||
sm: { offset: 6 },
|
sm: { offset: 6 },
|
||||||
@ -114,11 +92,11 @@ export const EndPageTab: React.FC<TabPaneProps> = (props) => {
|
|||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add()
|
add();
|
||||||
}}
|
}}
|
||||||
style={{ width: '60%' }}
|
style={{ width: '60%' }}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> {t('form:endPage.addButton')}
|
<PlusOutlined /> Add Button
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,125 +0,0 @@
|
|||||||
import { message } from 'antd'
|
|
||||||
import ExcelJS, { CellValue } from 'exceljs'
|
|
||||||
import { useCallback, useState } from 'react'
|
|
||||||
import { SubmissionFragment } from '../../../graphql/fragment/submission.fragment'
|
|
||||||
import { useFormQuery } from '../../../graphql/query/form.query'
|
|
||||||
import { useSubmissionPagerImperativeQuery } from '../../../graphql/query/submission.pager.query'
|
|
||||||
import { fieldTypes } from '../types'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
form: string
|
|
||||||
trigger: (open: () => any, loading: boolean) => JSX.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExportSubmissionAction: React.FC<Props> = (props) => {
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
|
|
||||||
const form = useFormQuery({
|
|
||||||
variables: {
|
|
||||||
id: props.form,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const getSubmissions = useSubmissionPagerImperativeQuery()
|
|
||||||
|
|
||||||
const exportSubmissions = useCallback(async () => {
|
|
||||||
if (loading) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const workbook = new ExcelJS.Workbook()
|
|
||||||
workbook.creator = 'OhMyForm'
|
|
||||||
workbook.lastModifiedBy = 'OhMyForm'
|
|
||||||
workbook.created = new Date()
|
|
||||||
workbook.modified = new Date()
|
|
||||||
|
|
||||||
const orderedFields = form.data.form.fields
|
|
||||||
.map(field => field)
|
|
||||||
.sort((a, b) => (a.idx ?? 0) - (b.idx ?? 0))
|
|
||||||
|
|
||||||
// TODO should go through deleted fields as well to have a complete overview!
|
|
||||||
|
|
||||||
const sheet = workbook.addWorksheet('Submissions')
|
|
||||||
sheet.getRow(1).values = [
|
|
||||||
'Submission ID',
|
|
||||||
'Created',
|
|
||||||
'Last Change',
|
|
||||||
'Country',
|
|
||||||
'City',
|
|
||||||
'User Agent',
|
|
||||||
'Device',
|
|
||||||
...orderedFields.map((field) => `${field.title} (${field.type})`),
|
|
||||||
]
|
|
||||||
|
|
||||||
const firstPage = await getSubmissions({
|
|
||||||
form: props.form,
|
|
||||||
limit: 50,
|
|
||||||
start: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
const buildRow = (data: SubmissionFragment): CellValue[] => {
|
|
||||||
const row: CellValue[] = [
|
|
||||||
data.id,
|
|
||||||
data.created,
|
|
||||||
data.lastModified,
|
|
||||||
data.geoLocation.country,
|
|
||||||
data.geoLocation.city,
|
|
||||||
data.device.type,
|
|
||||||
data.device.name,
|
|
||||||
]
|
|
||||||
|
|
||||||
orderedFields.forEach((formField) => {
|
|
||||||
const field = data.fields.find(submission => submission.field?.id === formField.id)
|
|
||||||
|
|
||||||
try {
|
|
||||||
fieldTypes[field.type]?.stringifyValue(field.value)
|
|
||||||
|
|
||||||
row.push(fieldTypes[field.type]?.stringifyValue(field.value))
|
|
||||||
} catch (e) {
|
|
||||||
row.push('')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
firstPage.data.pager.entries.forEach((row, index) => {
|
|
||||||
sheet.getRow(index + 2).values = buildRow(row)
|
|
||||||
})
|
|
||||||
|
|
||||||
const pages = Math.ceil(firstPage.data.pager.total / 50)
|
|
||||||
for (let page = 1; page < pages; page++) {
|
|
||||||
// now process each page!
|
|
||||||
const next = await getSubmissions({
|
|
||||||
form: props.form,
|
|
||||||
limit: 50,
|
|
||||||
start: page * 50,
|
|
||||||
})
|
|
||||||
|
|
||||||
next.data.pager.entries.forEach((row, index) => {
|
|
||||||
sheet.getRow(index + 2 + page * 50).values = buildRow(row)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = await workbook.xlsx.writeBuffer()
|
|
||||||
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = window.URL.createObjectURL(new Blob([buffer], { type: 'application/xlsx' }))
|
|
||||||
link.download = 'submissions.xlsx'
|
|
||||||
link.click()
|
|
||||||
} catch (e) {
|
|
||||||
console.log('error', e)
|
|
||||||
void message.error({
|
|
||||||
content: 'Failed to generate export',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setLoading(false)
|
|
||||||
}, [
|
|
||||||
form, getSubmissions, props.form, setLoading, loading,
|
|
||||||
])
|
|
||||||
|
|
||||||
return props.trigger(() => exportSubmissions(), loading)
|
|
||||||
}
|
|
||||||
@ -1,56 +1,39 @@
|
|||||||
import { VerticalAlignBottomOutlined, VerticalAlignTopOutlined } from '@ant-design/icons'
|
import {DeleteOutlined} from '@ant-design/icons/lib'
|
||||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
|
import {Button, Card, Checkbox, Form, Input, Popconfirm, Tag} from 'antd'
|
||||||
import { Button, Card, Checkbox, Form, Input, Popconfirm, Popover, Space, Tag, Tooltip } from 'antd'
|
import {FormInstance} from 'antd/lib/form'
|
||||||
import { FormInstance } from 'antd/lib/form'
|
import {FieldData} from 'rc-field-form/lib/interface'
|
||||||
import { FieldData } from 'rc-field-form/lib/interface'
|
import React, {useEffect, useState} from 'react'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||||
import { useTranslation } from 'react-i18next'
|
import {adminTypes} from './types'
|
||||||
import { FormFieldFragment, FormFieldLogicFragment } from '../../../graphql/fragment/form.fragment'
|
import {TextType} from './types/text.type'
|
||||||
import { fieldTypes } from '../types'
|
|
||||||
import { LogicBlock } from './logic.block'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
form: FormInstance
|
form: FormInstance
|
||||||
fields: FormFieldFragment[]
|
fields: AdminFormFieldFragment[]
|
||||||
onChangeFields: (fields: FormFieldFragment[]) => void
|
onChangeFields: (fields: AdminFormFieldFragment[]) => any
|
||||||
field: FieldData
|
field: FieldData
|
||||||
remove: (index: number) => void
|
remove: (index: number) => void
|
||||||
move: (from: number, to: number) => void
|
|
||||||
index: number
|
index: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FieldCard: React.FC<Props> = ({
|
export const FieldCard: React.FC<Props> = props => {
|
||||||
|
const {
|
||||||
form,
|
form,
|
||||||
field,
|
field,
|
||||||
fields,
|
fields,
|
||||||
onChangeFields,
|
onChangeFields,
|
||||||
remove,
|
remove,
|
||||||
move,
|
|
||||||
index,
|
index,
|
||||||
}) => {
|
} = props
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const type = form.getFieldValue([
|
const type = form.getFieldValue(['form', 'fields', field.name as string, 'type'])
|
||||||
'form', 'fields', field.name as string, 'type',
|
const TypeComponent = adminTypes[type] || TextType
|
||||||
]) as string
|
|
||||||
const TypeComponent = (fieldTypes[type] || fieldTypes['textfield']).adminFormField()
|
|
||||||
|
|
||||||
const [shouldUpdate, setShouldUpdate] = useState(false)
|
const [nextTitle, setNextTitle] = useState(form.getFieldValue(['form', 'fields', field.name as string, 'title']))
|
||||||
const [nextTitle, setNextTitle] = useState<string>(
|
|
||||||
form.getFieldValue([
|
|
||||||
'form', 'fields', field.name as string, 'title',
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldUpdate) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = setTimeout(() => {
|
const id = setTimeout(() => {
|
||||||
setShouldUpdate(false)
|
onChangeFields(fields.map((field, i) => {
|
||||||
onChangeFields(
|
|
||||||
fields.map((field, i) => {
|
|
||||||
if (i === index) {
|
if (i === index) {
|
||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
@ -59,186 +42,69 @@ export const FieldCard: React.FC<Props> = ({
|
|||||||
} else {
|
} else {
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
return () => clearTimeout(id)
|
return () => clearTimeout(id)
|
||||||
}, [
|
}, [nextTitle])
|
||||||
nextTitle, shouldUpdate, fields,
|
|
||||||
])
|
|
||||||
|
|
||||||
const addLogic = useCallback((add: (defaults: unknown) => void, index: number) => {
|
|
||||||
return (
|
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
|
||||||
<Space
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
const defaults: FormFieldLogicFragment = {
|
|
||||||
id: `NEW-${Date.now()}`,
|
|
||||||
formula: null,
|
|
||||||
action: null,
|
|
||||||
jumpTo: null,
|
|
||||||
visible: null,
|
|
||||||
disable: null,
|
|
||||||
require: null,
|
|
||||||
enabled: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
add(defaults)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusOutlined /> {t('form:logic.add')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={nextTitle}
|
title={nextTitle}
|
||||||
type={'inner'}
|
type={'inner'}
|
||||||
extra={
|
extra={(
|
||||||
<Space>
|
<div>
|
||||||
<Tooltip title={t('form:field.move.up')}>
|
<Tag color={'blue'}>{type}</Tag>
|
||||||
<Button
|
|
||||||
type={'text'}
|
|
||||||
disabled={index === 0}
|
|
||||||
onClick={() => move(index, index - 1)}
|
|
||||||
icon={<VerticalAlignTopOutlined />}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={t('form:field.move.down')}>
|
|
||||||
<Button
|
|
||||||
type={'text'}
|
|
||||||
disabled={index + 1 >= form.getFieldValue(['form', 'fields']).length}
|
|
||||||
onClick={() => move(index, index + 1)}
|
|
||||||
icon={<VerticalAlignBottomOutlined />}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Form.Item noStyle shouldUpdate>
|
|
||||||
{() => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const slug = form.getFieldValue([
|
|
||||||
'form', 'fields', field.name as string, 'slug',
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!slug) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Tag color={'warning'}>Slug: {slug}</Tag>
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
<Popover
|
|
||||||
placement={'left'}
|
|
||||||
content={
|
|
||||||
<Form.Item
|
|
||||||
name={[field.name as string, 'slug']}
|
|
||||||
label={false}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
pattern: /^[a-z0-9_]+$/,
|
|
||||||
message: t('validation:invalidSlug'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
help={t('type:slugInfo')}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
}
|
|
||||||
title={t('type:slug')}
|
|
||||||
>
|
|
||||||
<Tag color={'blue'}>{t(`type:${type}.name`)}</Tag>
|
|
||||||
</Popover>
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
placement={'left'}
|
placement={'left'}
|
||||||
title={t('type:confirmDelete')}
|
title={'Really remove this field? Check that it is not referenced anywhere!'}
|
||||||
okText={t('type:deleteNow')}
|
okText={'Delete Field'}
|
||||||
okButtonProps={{ danger: true }}
|
okButtonProps={{ danger: true }}
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
remove(index)
|
remove(index)
|
||||||
onChangeFields(fields.filter((e, i) => i !== index))
|
onChangeFields(fields.filter((e, i) => i !== index))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button danger>
|
<Button danger><DeleteOutlined /></Button>
|
||||||
<DeleteOutlined />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</div>
|
||||||
}
|
)}
|
||||||
|
actions={[
|
||||||
|
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Form.Item name={[field.name as string, 'type']} noStyle>
|
<Form.Item name={[field.name as string, 'type']} noStyle><Input type={'hidden'} /></Form.Item>
|
||||||
<Input type={'hidden'} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('type:title')}
|
label={'Title'}
|
||||||
name={[field.name as string, 'title']}
|
name={[field.name as string, 'title']}
|
||||||
rules={[{ required: true, message: 'Title is required' }]}
|
rules={[
|
||||||
|
{ required: true, message: 'Title is required' }
|
||||||
|
]}
|
||||||
labelCol={{ span: 6 }}
|
labelCol={{ span: 6 }}
|
||||||
>
|
>
|
||||||
<Input
|
<Input onChange={e => setNextTitle(e.target.value)}/>
|
||||||
onChange={(e) => {
|
|
||||||
setNextTitle(e.target.value)
|
|
||||||
setShouldUpdate(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('type:description')}
|
label={'Description'}
|
||||||
name={[field.name as string, 'description']}
|
name={[field.name as string, 'description']}
|
||||||
labelCol={{ span: 6 }}
|
labelCol={{ span: 6 }}
|
||||||
extra={t('type:descriptionInfo')}
|
|
||||||
>
|
>
|
||||||
<Input.TextArea autoSize />
|
<Input.TextArea autoSize />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('type:required')}
|
label={'Required'}
|
||||||
name={[field.name as string, 'required']}
|
name={[field.name as string, 'required']}
|
||||||
labelCol={{ span: 6 }}
|
labelCol={{ span: 6 }}
|
||||||
valuePropName={'checked'}
|
valuePropName={'checked'}
|
||||||
extra={type === 'hidden' && t('type:requiredInfo')}
|
extra={type === 'hidden' && 'If required, default value must be set to enable users to submit form!'}
|
||||||
>
|
>
|
||||||
<Checkbox />
|
<Checkbox />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<TypeComponent field={field} form={form} />
|
<TypeComponent
|
||||||
|
|
||||||
<Form.List name={[field.name as string, 'logic']}>
|
|
||||||
{(logic, { add, remove, move }) => {
|
|
||||||
const addAndMove = (index: number) => (defaults) => {
|
|
||||||
add(defaults)
|
|
||||||
move(fields.length, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{addLogic(addAndMove(0), 0)}
|
|
||||||
{logic.map((field, index) => (
|
|
||||||
<div key={field.key}>
|
|
||||||
<Form.Item wrapperCol={{ span: 24 }} noStyle>
|
|
||||||
<LogicBlock
|
|
||||||
field={field}
|
field={field}
|
||||||
form={form}
|
form={form}
|
||||||
fields={fields}
|
|
||||||
index={index}
|
|
||||||
remove={remove}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
|
||||||
{addLogic(addAndMove(index + 1), index + 1)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,83 +1,58 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons/lib'
|
import {PlusOutlined} from '@ant-design/icons/lib'
|
||||||
import { Button, Form, Select, Space, Tabs } from 'antd'
|
import {Button, Form, Select, Space, Tabs} from 'antd'
|
||||||
import { FormInstance } from 'antd/lib/form'
|
import {FormInstance} from 'antd/lib/form'
|
||||||
import { TabPaneProps } from 'antd/lib/tabs'
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
import debug from 'debug'
|
import React, {useCallback, useState} from 'react'
|
||||||
import { FieldData } from 'rc-field-form/lib/interface'
|
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||||
import React, { useCallback, useState } from 'react'
|
import {FieldCard} from './field.card'
|
||||||
import { useTranslation } from 'react-i18next'
|
import {adminTypes} from './types'
|
||||||
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
|
|
||||||
import { fieldTypes } from '../types'
|
|
||||||
import { FieldCard } from './field.card'
|
|
||||||
|
|
||||||
const logger = debug('FieldsTab')
|
|
||||||
|
|
||||||
interface Props extends TabPaneProps {
|
interface Props extends TabPaneProps {
|
||||||
form: FormInstance
|
form: FormInstance
|
||||||
fields: FormFieldFragment[]
|
fields: AdminFormFieldFragment[]
|
||||||
onChangeFields: (fields: FormFieldFragment[]) => void
|
onChangeFields: (fields: AdminFormFieldFragment[]) => any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FieldsTab: React.FC<Props> = (props) => {
|
export const FieldsTab: React.FC<Props> = props => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const [nextType, setNextType] = useState('textfield')
|
const [nextType, setNextType] = useState('textfield')
|
||||||
|
|
||||||
const renderType = useCallback(
|
const renderType = useCallback((field, index, remove) => {
|
||||||
(
|
|
||||||
field: FieldData,
|
|
||||||
index: number,
|
|
||||||
remove: (index: number) => void,
|
|
||||||
move: (from: number, to: number) => void
|
|
||||||
) => {
|
|
||||||
return (
|
return (
|
||||||
<FieldCard
|
<FieldCard
|
||||||
form={props.form}
|
form={props.form}
|
||||||
field={field}
|
field={field}
|
||||||
index={index}
|
index={index}
|
||||||
remove={(index: number) => {
|
remove={remove}
|
||||||
logger('remove %d', index)
|
|
||||||
remove(index)
|
|
||||||
}}
|
|
||||||
move={(from: number, to: number) => {
|
|
||||||
logger('move %d TO %d', from, to)
|
|
||||||
move(from, to)
|
|
||||||
}}
|
|
||||||
fields={props.fields}
|
fields={props.fields}
|
||||||
onChangeFields={props.onChangeFields}
|
onChangeFields={props.onChangeFields}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
}, [props.fields])
|
||||||
[props.fields]
|
|
||||||
)
|
|
||||||
|
|
||||||
const addField = useCallback(
|
const addField = useCallback((add, index) => {
|
||||||
(add: (defaults: unknown) => void, index: number) => {
|
|
||||||
return (
|
return (
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
<Form.Item
|
||||||
|
wrapperCol={{span: 24}}
|
||||||
|
>
|
||||||
<Space
|
<Space
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Select value={nextType} onChange={(e) => setNextType(e)} style={{ minWidth: 200 }}>
|
<Select value={nextType} onChange={e => setNextType(e)} style={{ minWidth: 200 }}>
|
||||||
{Object.keys(fieldTypes).map((type) => (
|
{Object.keys(adminTypes).map(type => <Select.Option value={type} key={type}>{type}</Select.Option> )}
|
||||||
<Select.Option value={type} key={type}>
|
|
||||||
{t(`type:${type}.name`)}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const defaults: FormFieldFragment = {
|
const defaults: AdminFormFieldFragment = {
|
||||||
logic: [],
|
|
||||||
options: [],
|
|
||||||
id: `NEW-${Date.now()}`,
|
id: `NEW-${Date.now()}`,
|
||||||
type: nextType,
|
type: nextType,
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
required: false,
|
required: false,
|
||||||
|
value: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
add(defaults)
|
add(defaults)
|
||||||
@ -86,20 +61,22 @@ export const FieldsTab: React.FC<Props> = (props) => {
|
|||||||
props.onChangeFields(next)
|
props.onChangeFields(next)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> {t('type:add')}
|
<PlusOutlined /> Add Field
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
},
|
}, [props.fields, nextType])
|
||||||
[props.fields, nextType]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs.TabPane {...props}>
|
<Tabs.TabPane {...props}>
|
||||||
<Form.List name={['form', 'fields']}>
|
|
||||||
|
<Form.List
|
||||||
|
name={['form', 'fields']}
|
||||||
|
>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove, move }) => {
|
||||||
const addAndMove = (index: number) => (defaults) => {
|
const addAndMove = (index) => (defaults) => {
|
||||||
add(defaults)
|
add(defaults)
|
||||||
move(fields.length, index)
|
move(fields.length, index)
|
||||||
}
|
}
|
||||||
@ -110,7 +87,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
|
|||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<div key={field.key}>
|
<div key={field.key}>
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
<Form.Item wrapperCol={{ span: 24 }}>
|
||||||
{renderType(field, index, remove, move)}
|
{renderType(field, index, remove)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{addField(addAndMove(index + 1), index + 1)}
|
{addField(addAndMove(index + 1), index + 1)}
|
||||||
</div>
|
</div>
|
||||||
@ -119,6 +96,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,98 +0,0 @@
|
|||||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
|
|
||||||
import { Button, Card, Checkbox, Form, Input, Popconfirm, Space, Tabs } from 'antd'
|
|
||||||
import { TabPaneProps } from 'antd/lib/tabs'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
interface Props extends TabPaneProps {}
|
|
||||||
|
|
||||||
export const HooksTab: React.FC<Props> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs.TabPane {...props}>
|
|
||||||
<Form.List name={['form', 'hooks']}>
|
|
||||||
{(hooks, { add, remove }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
|
||||||
<Space
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
const defaults = {
|
|
||||||
id: `NEW-${Date.now()}`,
|
|
||||||
enabled: false,
|
|
||||||
url: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
add(defaults)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusOutlined /> {t('form:hooks.add')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
{hooks.map((hook, index) => (
|
|
||||||
<div key={hook.key}>
|
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
|
||||||
<Card
|
|
||||||
title={
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
name={[hook.name, 'enabled']}
|
|
||||||
valuePropName={'checked'}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<Checkbox />
|
|
||||||
</Form.Item>
|
|
||||||
{t('form:hooks.enabled')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
type={'inner'}
|
|
||||||
extra={
|
|
||||||
<div>
|
|
||||||
<Popconfirm
|
|
||||||
placement={'left'}
|
|
||||||
title={t('form:hooks.confirmDelete')}
|
|
||||||
okText={t('form:hooks.deleteNow')}
|
|
||||||
okButtonProps={{ danger: true }}
|
|
||||||
onConfirm={() => {
|
|
||||||
remove(index)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button danger>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
label={t('form:hooks.url')}
|
|
||||||
name={[hook.name, 'url']}
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: t('validation:urlRequired') },
|
|
||||||
{ type: 'url', message: t('validation:invalidUrl') },
|
|
||||||
]}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</Card>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,26 +1,22 @@
|
|||||||
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons/lib'
|
import {CheckCircleOutlined, CloseCircleOutlined} from '@ant-design/icons/lib'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isLive: boolean
|
isLive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormIsLive: React.FC<Props> = (props) => {
|
export const FormIsLive: React.FC<Props> = props => {
|
||||||
if (props.isLive) {
|
if (props.isLive) {
|
||||||
return (
|
return (
|
||||||
<CheckCircleOutlined
|
<CheckCircleOutlined style={{
|
||||||
style={{
|
color: 'green'
|
||||||
color: 'green',
|
}} />
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CloseCircleOutlined
|
<CloseCircleOutlined style={{
|
||||||
style={{
|
color: 'red'
|
||||||
color: 'red',
|
}} />
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,233 +0,0 @@
|
|||||||
import { DeleteOutlined } from '@ant-design/icons'
|
|
||||||
import { Alert, Button, Checkbox, Form, Mentions, Popconfirm, Select } from 'antd'
|
|
||||||
import { FormInstance } from 'antd/lib/form'
|
|
||||||
import { FieldData } from 'rc-field-form/lib/interface'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
|
|
||||||
import { useMath } from '../../use.math'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
form: FormInstance
|
|
||||||
fields: FormFieldFragment[]
|
|
||||||
field: FieldData
|
|
||||||
remove: (index: number) => void
|
|
||||||
index: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LogicBlock: React.FC<Props> = ({
|
|
||||||
form,
|
|
||||||
field,
|
|
||||||
fields,
|
|
||||||
remove,
|
|
||||||
index,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const evaluator = useMath()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
borderRight: '5px solid #DDD',
|
|
||||||
paddingRight: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name={[field.name as string, 'formula']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
label={'Formula'}
|
|
||||||
rules={[{ required: true, message: 'combine other fields' }]}
|
|
||||||
extra={'Save form to get new @IDs and $slugs. (example: $slug < 21 or @id = 42)'}
|
|
||||||
>
|
|
||||||
<Mentions rows={1}>
|
|
||||||
{fields.map((field) => (
|
|
||||||
<Mentions.Option key={field.id} value={field.id}>
|
|
||||||
{field.title}
|
|
||||||
</Mentions.Option>
|
|
||||||
))}
|
|
||||||
</Mentions>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item noStyle shouldUpdate>
|
|
||||||
{(form: FormInstance & { prefixName: string[] }) => {
|
|
||||||
try {
|
|
||||||
const defaults = {}
|
|
||||||
|
|
||||||
fields.forEach((field) => {
|
|
||||||
defaults[`@${field.id}`] = field.defaultValue
|
|
||||||
|
|
||||||
if (field.slug) {
|
|
||||||
defaults[`$${field.slug}`] = field.defaultValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = evaluator(
|
|
||||||
form.getFieldValue([
|
|
||||||
...form.prefixName,
|
|
||||||
field.name as string,
|
|
||||||
'formula',
|
|
||||||
]),
|
|
||||||
defaults
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
type={result ? 'success' : 'warning'}
|
|
||||||
message={
|
|
||||||
result
|
|
||||||
? 'would trigger action with current default values'
|
|
||||||
: 'would NOT trigger action with current default values'
|
|
||||||
}
|
|
||||||
style={{ marginBottom: 24 }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} catch (e) {
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
message={(e as Error).message || 'Failed to process formula'}
|
|
||||||
type={'error'}
|
|
||||||
style={{ marginBottom: 24 }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name={[field.name as string, 'action']} labelCol={{ span: 6 }} label={'Action'}>
|
|
||||||
<Select
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
value: 'jumpTo',
|
|
||||||
label: t('form:logic.action.jumpTo'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'visible',
|
|
||||||
label: t('form:logic.action.visible'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'disable',
|
|
||||||
label: t('form:logic.action.disable'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'require',
|
|
||||||
label: t('form:logic.action.require'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item noStyle shouldUpdate>
|
|
||||||
{(form: FormInstance & { prefixName: string[] }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item
|
|
||||||
hidden={
|
|
||||||
form.getFieldValue([
|
|
||||||
...form.prefixName, field.name as string, 'action',
|
|
||||||
]) !==
|
|
||||||
'jumpTo'
|
|
||||||
}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
label={t('form:logic.action.jumpTo')}
|
|
||||||
rules={[{ required: true, message: 'Jump target is required' }]}
|
|
||||||
extra={'after selecting field (works best with clickable values)'}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
options={fields
|
|
||||||
.filter((field) => !/NEW/i.test(field.id))
|
|
||||||
.map((field) => ({
|
|
||||||
value: field.id,
|
|
||||||
label: field.title,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item noStyle shouldUpdate>
|
|
||||||
{(form: FormInstance & { prefixName: string[] }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item
|
|
||||||
hidden={
|
|
||||||
form.getFieldValue([
|
|
||||||
...form.prefixName, field.name as string, 'action',
|
|
||||||
]) !==
|
|
||||||
'visible'
|
|
||||||
}
|
|
||||||
initialValue={true}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
label={t('form:logic.action.visible')}
|
|
||||||
valuePropName={'checked'}
|
|
||||||
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
|
|
||||||
getValueProps={(e: string) => ({ checked: !!e })}
|
|
||||||
>
|
|
||||||
<Checkbox />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item noStyle shouldUpdate>
|
|
||||||
{(form: FormInstance & { prefixName: string[] }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item
|
|
||||||
hidden={
|
|
||||||
form.getFieldValue([
|
|
||||||
...form.prefixName, field.name as string, 'action',
|
|
||||||
]) !==
|
|
||||||
'disable'
|
|
||||||
}
|
|
||||||
initialValue={false}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
label={t('form:logic.action.disable')}
|
|
||||||
valuePropName={'checked'}
|
|
||||||
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
|
|
||||||
getValueProps={(e: string) => ({ checked: !!e })}
|
|
||||||
>
|
|
||||||
<Checkbox />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item noStyle shouldUpdate>
|
|
||||||
{(form: FormInstance & { prefixName: string[] }) => {
|
|
||||||
return (
|
|
||||||
<Form.Item
|
|
||||||
hidden={
|
|
||||||
form.getFieldValue([
|
|
||||||
...form.prefixName, field.name as string, 'action',
|
|
||||||
]) !==
|
|
||||||
'require'
|
|
||||||
}
|
|
||||||
initialValue={true}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
label={t('form:logic.action.require')}
|
|
||||||
valuePropName={'checked'}
|
|
||||||
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
|
|
||||||
getValueProps={(e: string) => ({ checked: !!e })}
|
|
||||||
>
|
|
||||||
<Checkbox />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<div style={{ textAlign: 'right' }}>
|
|
||||||
<Popconfirm
|
|
||||||
placement={'right'}
|
|
||||||
title={t('type:confirmDelete')}
|
|
||||||
okText={t('type:deleteNow')}
|
|
||||||
okButtonProps={{ danger: true }}
|
|
||||||
onConfirm={() => {
|
|
||||||
remove(index)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button danger>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,247 +0,0 @@
|
|||||||
import { DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons/lib'
|
|
||||||
import { Button, Card, Form, Input, Popconfirm, Select, Switch } from 'antd'
|
|
||||||
import { FormInstance } from 'antd/lib/form'
|
|
||||||
import { FieldData } from 'rc-field-form/lib/interface'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
|
||||||
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
form: FormInstance
|
|
||||||
field: FieldData
|
|
||||||
groups: {
|
|
||||||
[key: string]: FormFieldFragment[]
|
|
||||||
}
|
|
||||||
remove: (index: number) => void
|
|
||||||
index: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotificationCard: React.FC<Props> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { form, field, remove, index, groups } = props
|
|
||||||
|
|
||||||
const [enabled, setEnabled] = useState<boolean>()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={'Notification'}
|
|
||||||
type={'inner'}
|
|
||||||
extra={
|
|
||||||
<div>
|
|
||||||
<Popconfirm
|
|
||||||
placement={'left'}
|
|
||||||
title={t('type:confirmDelete')}
|
|
||||||
okText={t('type:deleteNow')}
|
|
||||||
okButtonProps={{ danger: true }}
|
|
||||||
onConfirm={() => {
|
|
||||||
remove(index)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button danger>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
label={t('form:notifications.enabled')}
|
|
||||||
name={[field.name as string, 'enabled']}
|
|
||||||
valuePropName={'checked'}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Switch onChange={(e) => setEnabled(e.valueOf())} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => (
|
|
||||||
<Form.Item
|
|
||||||
label={t('form:notifications.subject')}
|
|
||||||
name={[field.name as string, 'subject']}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: Boolean(
|
|
||||||
form.getFieldValue([
|
|
||||||
'form', 'notifications', field.name as string, 'enabled',
|
|
||||||
])
|
|
||||||
),
|
|
||||||
message: t('validation:subjectRequired'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => (
|
|
||||||
<Form.Item
|
|
||||||
label={t('form:notifications.htmlTemplate')}
|
|
||||||
name={[field.name as string, 'htmlTemplate']}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: Boolean(
|
|
||||||
form.getFieldValue([
|
|
||||||
'form', 'notifications', field.name as string, 'enabled',
|
|
||||||
])
|
|
||||||
),
|
|
||||||
message: t('validation:templateRequired'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
extra={
|
|
||||||
<div>
|
|
||||||
<Trans>form:notifications.htmlTemplateInfo</Trans>
|
|
||||||
<a
|
|
||||||
href={'https://mjml.io/try-it-live'}
|
|
||||||
target={'_blank'}
|
|
||||||
rel={'noreferrer'}
|
|
||||||
style={{
|
|
||||||
marginLeft: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<InfoCircleOutlined />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input.TextArea autoSize />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => (
|
|
||||||
<Form.Item
|
|
||||||
label={t('form:notifications.fromField')}
|
|
||||||
name={[field.name as string, 'fromField']}
|
|
||||||
extra={t('form:notifications.fromFieldInfo')}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: Boolean(
|
|
||||||
form.getFieldValue([
|
|
||||||
'form', 'notifications', field.name as string, 'enabled',
|
|
||||||
]) &&
|
|
||||||
!form.getFieldValue([
|
|
||||||
'form',
|
|
||||||
'notifications',
|
|
||||||
field.name as string,
|
|
||||||
'fromEmail',
|
|
||||||
])
|
|
||||||
),
|
|
||||||
message: t('validation:emailFieldRequired'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Select>
|
|
||||||
{Object.keys(groups).map((key) => (
|
|
||||||
<Select.OptGroup label={key.toUpperCase()} key={key}>
|
|
||||||
{groups[key].map((element) => (
|
|
||||||
<Select.Option value={element.id} key={element.id}>
|
|
||||||
{element.title}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select.OptGroup>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => (
|
|
||||||
<Form.Item
|
|
||||||
label={t('form:notifications.fromEmail')}
|
|
||||||
name={[field.name as string, 'fromEmail']}
|
|
||||||
extra={t('form:notifications.fromEmailInfo')}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: Boolean(
|
|
||||||
form.getFieldValue([
|
|
||||||
'form', 'notifications', field.name as string, 'enabled',
|
|
||||||
]) &&
|
|
||||||
!form.getFieldValue([
|
|
||||||
'form',
|
|
||||||
'notifications',
|
|
||||||
field.name as string,
|
|
||||||
'fromField',
|
|
||||||
])
|
|
||||||
),
|
|
||||||
message: t('validation:emailFieldRequired'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => (
|
|
||||||
<Form.Item
|
|
||||||
label={t('form:notifications.toField')}
|
|
||||||
name={[field.name as string, 'toField']}
|
|
||||||
extra={t('form:notifications.toFieldInfo')}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: Boolean(
|
|
||||||
form.getFieldValue([
|
|
||||||
'form', 'notifications', field.name as string, 'enabled',
|
|
||||||
]) &&
|
|
||||||
!form.getFieldValue([
|
|
||||||
'form', 'notifications', field.name as string, 'toEmail',
|
|
||||||
])
|
|
||||||
),
|
|
||||||
message: t('validation:emailFieldRequired'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Select>
|
|
||||||
{Object.keys(groups).map((key) => (
|
|
||||||
<Select.OptGroup label={key.toUpperCase()} key={key}>
|
|
||||||
{groups[key].map((field) => (
|
|
||||||
<Select.Option value={field.id} key={field.id}>
|
|
||||||
{field.title}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select.OptGroup>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => (
|
|
||||||
<Form.Item
|
|
||||||
label={t('form:notifications.toEmail')}
|
|
||||||
name={[field.name as string, 'toEmail']}
|
|
||||||
extra={t('form:notifications.toEmailInfo')}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: Boolean(
|
|
||||||
form.getFieldValue([
|
|
||||||
'form', 'notifications', field.name as string, 'enabled',
|
|
||||||
]) &&
|
|
||||||
!form.getFieldValue([
|
|
||||||
'form', 'notifications', field.name as string, 'toField',
|
|
||||||
])
|
|
||||||
),
|
|
||||||
message: t('validation:emailFieldRequired'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons/lib'
|
|
||||||
import { Button, Form, Space, Tabs } from 'antd'
|
|
||||||
import { FormInstance } from 'antd/lib/form'
|
|
||||||
import { TabPaneProps } from 'antd/lib/tabs'
|
|
||||||
import React, { useCallback } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import {
|
|
||||||
FormFieldFragment,
|
|
||||||
FormNotificationFragment,
|
|
||||||
} from '../../../graphql/fragment/form.fragment'
|
|
||||||
import { NotificationCard } from './notification.card'
|
|
||||||
|
|
||||||
interface Props extends TabPaneProps {
|
|
||||||
form: FormInstance
|
|
||||||
fields: FormFieldFragment[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotificationsTab: React.FC<Props> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const groups: {
|
|
||||||
[key: string]: FormFieldFragment[]
|
|
||||||
} = {}
|
|
||||||
props.fields.forEach((field) => {
|
|
||||||
if (!groups[field.type]) {
|
|
||||||
groups[field.type] = []
|
|
||||||
}
|
|
||||||
groups[field.type].push(field)
|
|
||||||
})
|
|
||||||
|
|
||||||
const addField = useCallback(
|
|
||||||
(add: (defaults: unknown) => void, index: number) => {
|
|
||||||
return (
|
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
|
||||||
<Space
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
const defaults: FormNotificationFragment = {
|
|
||||||
id: Math.random().toString(),
|
|
||||||
enabled: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
add(defaults)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusOutlined /> {t('form:notifications.add')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[props.fields]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs.TabPane {...props}>
|
|
||||||
<Form.List name={['form', 'notifications']}>
|
|
||||||
{(fields, { add, remove, move }) => {
|
|
||||||
const addAndMove = (index: number) => (defaults) => {
|
|
||||||
add(defaults)
|
|
||||||
move(fields.length, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{addField(addAndMove(0), 0)}
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<div key={field.key}>
|
|
||||||
<Form.Item wrapperCol={{ span: 24 }}>
|
|
||||||
<NotificationCard
|
|
||||||
form={props.form}
|
|
||||||
field={field}
|
|
||||||
index={index}
|
|
||||||
remove={remove}
|
|
||||||
groups={groups}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
{addField(addAndMove(index + 1), index + 1)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
107
components/form/admin/respondent.notifications.tab.tsx
Normal file
107
components/form/admin/respondent.notifications.tab.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import {Form, Input, Select, Switch, Tabs} from 'antd'
|
||||||
|
import {FormInstance} from 'antd/lib/form'
|
||||||
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||||
|
|
||||||
|
interface Props extends TabPaneProps {
|
||||||
|
form: FormInstance
|
||||||
|
fields: AdminFormFieldFragment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RespondentNotificationsTab: React.FC<Props> = props => {
|
||||||
|
const [enabled, setEnabled] = useState<boolean>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])
|
||||||
|
|
||||||
|
if (next !== enabled) {
|
||||||
|
setEnabled(next)
|
||||||
|
}
|
||||||
|
}, [props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
props.form.validateFields([
|
||||||
|
['form', 'respondentNotifications', 'subject'],
|
||||||
|
['form', 'respondentNotifications', 'htmlTemplate'],
|
||||||
|
['form', 'respondentNotifications', 'toField'],
|
||||||
|
])
|
||||||
|
}, [enabled])
|
||||||
|
|
||||||
|
const groups = {}
|
||||||
|
|
||||||
|
props.fields.forEach(field => {
|
||||||
|
if (!groups[field.type]) {
|
||||||
|
groups[field.type] = []
|
||||||
|
}
|
||||||
|
groups[field.type].push(field)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane {...props}>
|
||||||
|
<Form.Item
|
||||||
|
label={'Enabled'}
|
||||||
|
name={['form', 'respondentNotifications', 'enabled']}
|
||||||
|
valuePropName={'checked'}
|
||||||
|
>
|
||||||
|
<Switch onChange={e => setEnabled(e.valueOf())} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Subject'}
|
||||||
|
name={['form', 'respondentNotifications', 'subject']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Subject',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'HTML Template'}
|
||||||
|
name={['form', 'respondentNotifications', 'htmlTemplate']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Template',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Email Field'}
|
||||||
|
name={['form', 'respondentNotifications', 'toField']}
|
||||||
|
extra={'Field with Email for receipt'}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Email Field',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
{Object.keys(groups).map(key => (
|
||||||
|
<Select.OptGroup label={key.toUpperCase()} key={key}>
|
||||||
|
{groups[key].map(field => (
|
||||||
|
<Select.Option value={field.id} key={field.id}>{field.title}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select.OptGroup>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Sender Email'}
|
||||||
|
name={['form', 'respondentNotifications', 'fromEmail']}
|
||||||
|
extra={'Make sure your mailserver can send from this email'}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
99
components/form/admin/self.notifications.tab.tsx
Normal file
99
components/form/admin/self.notifications.tab.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import {Form, Input, Select, Switch, Tabs} from 'antd'
|
||||||
|
import {FormInstance} from 'antd/lib/form'
|
||||||
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||||
|
|
||||||
|
interface Props extends TabPaneProps {
|
||||||
|
form: FormInstance
|
||||||
|
fields: AdminFormFieldFragment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelfNotificationsTab: React.FC<Props> = props => {
|
||||||
|
const [enabled, setEnabled] = useState<boolean>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const next = props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])
|
||||||
|
|
||||||
|
if (next !== enabled) {
|
||||||
|
setEnabled(next)
|
||||||
|
}
|
||||||
|
}, [props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
props.form.validateFields([
|
||||||
|
['form', 'selfNotifications', 'subject'],
|
||||||
|
['form', 'selfNotifications', 'htmlTemplate'],
|
||||||
|
])
|
||||||
|
}, [enabled])
|
||||||
|
|
||||||
|
const groups = {}
|
||||||
|
props.fields.forEach(field => {
|
||||||
|
if (!groups[field.type]) {
|
||||||
|
groups[field.type] = []
|
||||||
|
}
|
||||||
|
groups[field.type].push(field)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane {...props}>
|
||||||
|
<Form.Item
|
||||||
|
label={'Enabled'}
|
||||||
|
name={['form', 'selfNotifications', 'enabled']}
|
||||||
|
valuePropName={'checked'}
|
||||||
|
>
|
||||||
|
<Switch onChange={e => setEnabled(e.valueOf())} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Subject'}
|
||||||
|
name={['form', 'selfNotifications', 'subject']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Subject',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'HTML Template'}
|
||||||
|
name={['form', 'selfNotifications', 'htmlTemplate']}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: enabled,
|
||||||
|
message: 'Please provide a Template',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Email Field'}
|
||||||
|
name={['form', 'selfNotifications', 'fromField']}
|
||||||
|
extra={'Field with Email, will set the Reply-To header'}
|
||||||
|
>
|
||||||
|
<Select>
|
||||||
|
{Object.keys(groups).map(key => (
|
||||||
|
<Select.OptGroup label={key.toUpperCase()} key={key}>
|
||||||
|
{groups[key].map(field => (
|
||||||
|
<Select.Option value={field.id} key={field.id}>{field.title}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select.OptGroup>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={'Your Email'}
|
||||||
|
name={['form', 'selfNotifications', 'toEmail']}
|
||||||
|
extra={'If not set will send to the admin of the form'}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,53 +1,44 @@
|
|||||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
|
import {DeleteOutlined, PlusOutlined} from '@ant-design/icons/lib'
|
||||||
import { Button, Card, Form, Input, Switch, Tabs } from 'antd'
|
import {Button, Card, Form, Input, Switch, Tabs} from 'antd'
|
||||||
import { TabPaneProps } from 'antd/lib/tabs'
|
import {TabPaneProps} from 'antd/lib/tabs'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import {InputColor} from '../../input/color'
|
||||||
import { InputColor } from '../../input/color'
|
|
||||||
|
|
||||||
export const StartPageTab: React.FC<TabPaneProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
|
export const StartPageTab: React.FC<TabPaneProps> = props => {
|
||||||
return (
|
return (
|
||||||
<Tabs.TabPane {...props}>
|
<Tabs.TabPane {...props}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:startPage.show')}
|
label={'Show'}
|
||||||
name={[
|
name={['form', 'startPage', 'show']}
|
||||||
'form', 'startPage', 'show',
|
|
||||||
]}
|
|
||||||
valuePropName={'checked'}
|
valuePropName={'checked'}
|
||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('form:startPage.title')} name={[
|
<Form.Item
|
||||||
'form', 'startPage', 'title',
|
label={'Title'}
|
||||||
]}>
|
name={['form', 'startPage', 'title']}
|
||||||
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:startPage.paragraph')}
|
label={'Paragraph'}
|
||||||
name={[
|
name={['form', 'startPage', 'paragraph']}
|
||||||
'form', 'startPage', 'paragraph',
|
|
||||||
]}
|
|
||||||
extra={t('form:startPage.paragraphInfo')}
|
|
||||||
>
|
>
|
||||||
<Input.TextArea autoSize />
|
<Input.TextArea autoSize />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:startPage.continueButtonText')}
|
label={'Continue Button Text'}
|
||||||
name={[
|
name={['form', 'startPage', 'buttonText']}
|
||||||
'form', 'startPage', 'buttonText',
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.List name={[
|
<Form.List
|
||||||
'form', 'startPage', 'buttons',
|
name={['form', 'startPage', 'buttons']}
|
||||||
]}>
|
>
|
||||||
{(fields, { add, remove }) => {
|
{(fields, { add, remove }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -56,56 +47,43 @@ export const StartPageTab: React.FC<TabPaneProps> = (props) => {
|
|||||||
wrapperCol={{
|
wrapperCol={{
|
||||||
sm: { offset: index === 0 ? 0 : 6 },
|
sm: { offset: index === 0 ? 0 : 6 },
|
||||||
}}
|
}}
|
||||||
label={index === 0 ? t('form:startPage.buttons') : ''}
|
label={index === 0 ? 'Buttons' : ''}
|
||||||
key={field.key}
|
key={field.key}
|
||||||
>
|
>
|
||||||
<Card actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}>
|
<Card
|
||||||
|
actions={[
|
||||||
|
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('form:startPage.url')}
|
label={'Url'}
|
||||||
name={[field.key, 'url']}
|
name={[field.key, 'url']}
|
||||||
rules={[{ type: 'url', message: t('validation:invalidUrl') }]}
|
rules={[
|
||||||
|
{type: 'url', message: 'Must be a valid url'}
|
||||||
|
]}
|
||||||
labelCol={{ span: 6 }}
|
labelCol={{ span: 6 }}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Action'} name={[field.key, 'action']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:startPage.action')}
|
|
||||||
name={[field.key, 'action']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Text'} name={[field.key, 'text']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:startPage.text')}
|
|
||||||
name={[field.key, 'text']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Background Color'} name={[field.key, 'bgColor']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:startPage.bgColor')}
|
|
||||||
name={[field.key, 'bgColor']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<InputColor />
|
<InputColor />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Active Color'} name={[field.key, 'activeColor']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:startPage.activeColor')}
|
|
||||||
name={[field.key, 'activeColor']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<InputColor />
|
<InputColor />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={'Color'} name={[field.key, 'color']} labelCol={{ span: 6 }}>
|
||||||
label={t('form:startPage.color')}
|
|
||||||
name={[field.key, 'color']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<InputColor />
|
<InputColor />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Card>
|
</Card>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
wrapperCol={{
|
wrapperCol={{
|
||||||
sm: { offset: 6 },
|
sm: { offset: 6 },
|
||||||
@ -114,11 +92,11 @@ export const StartPageTab: React.FC<TabPaneProps> = (props) => {
|
|||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add()
|
add();
|
||||||
}}
|
}}
|
||||||
style={{ width: '60%' }}
|
style={{ width: '60%' }}
|
||||||
>
|
>
|
||||||
<PlusOutlined /> {t('form:startPage.addButton')}
|
<PlusOutlined /> Add Button
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,63 +1,58 @@
|
|||||||
import { Descriptions, Table } from 'antd'
|
import {Descriptions, Table} from 'antd'
|
||||||
import { ColumnsType } from 'antd/lib/table/interface'
|
import {ColumnsType} from 'antd/lib/table/interface'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FormPagerFragment } from '../../../graphql/fragment/form.pager.fragment'
|
|
||||||
import {
|
import {
|
||||||
SubmissionFieldFragment,
|
AdminPagerSubmissionEntryFieldQueryData,
|
||||||
SubmissionFragment,
|
AdminPagerSubmissionEntryQueryData,
|
||||||
} from '../../../graphql/fragment/submission.fragment'
|
AdminPagerSubmissionFormQueryData
|
||||||
import { fieldTypes } from '../types'
|
} from '../../../graphql/query/admin.pager.submission.query'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
form: FormPagerFragment
|
form: AdminPagerSubmissionFormQueryData
|
||||||
submission: SubmissionFragment
|
submission: AdminPagerSubmissionEntryQueryData
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubmissionValues: React.FC<Props> = (props) => {
|
export const SubmissionValues: React.FC<Props> = props => {
|
||||||
const { t } = useTranslation()
|
const columns: ColumnsType<AdminPagerSubmissionEntryFieldQueryData> = [
|
||||||
|
|
||||||
const columns: ColumnsType<SubmissionFieldFragment> = [
|
|
||||||
{
|
{
|
||||||
title: t('submission:field'),
|
title: 'Field',
|
||||||
render(_, row) {
|
render: (row: AdminPagerSubmissionEntryFieldQueryData) => {
|
||||||
|
|
||||||
if (row.field) {
|
if (row.field) {
|
||||||
return `${row.field.title}${row.field.required ? '*' : ''}`
|
return `${row.field.title}${row.field.required ? '*' : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${row.id}`
|
return `${row.id}`
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('submission:value'),
|
title: 'Value',
|
||||||
render(_, row) {
|
render: row => {
|
||||||
try {
|
try {
|
||||||
return fieldTypes[row.type]?.displayValue(row.value)
|
const data = JSON.parse(row.value)
|
||||||
|
|
||||||
|
return data.value
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return row.value
|
return row.value
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Descriptions title={t('submission:submission')}>
|
<Descriptions title={'Submission'}>
|
||||||
<Descriptions.Item label={t('submission:country')}>
|
<Descriptions.Item label="Country">{props.submission.geoLocation.country}</Descriptions.Item>
|
||||||
{props.submission.geoLocation.country}
|
<Descriptions.Item label="City">{props.submission.geoLocation.city}</Descriptions.Item>
|
||||||
</Descriptions.Item>
|
<Descriptions.Item label="Device Type">{props.submission.device.type}</Descriptions.Item>
|
||||||
<Descriptions.Item label={t('submission:city')}>
|
<Descriptions.Item label="Device Name">{props.submission.device.name}</Descriptions.Item>
|
||||||
{props.submission.geoLocation.city}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={t('submission:device.type')}>
|
|
||||||
{props.submission.device.type}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={t('submission:device.name')}>
|
|
||||||
{props.submission.device.name}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
|
||||||
<Table columns={columns} dataSource={props.submission.fields} rowKey={'id'} />
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={props.submission.fields}
|
||||||
|
rowKey={'id'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
42
components/form/admin/types/date.type.tsx
Normal file
42
components/form/admin/types/date.type.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import {DatePicker, Form} from 'antd'
|
||||||
|
import moment from 'moment'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const DateType: React.FC<AdminFieldTypeProps> = ({field, form}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Date'}
|
||||||
|
name={[field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
getValueFromEvent={e => e ? e.format('YYYY-MM-DD') : undefined}
|
||||||
|
getValueProps={e => ({value: e ? moment(e) : undefined})}
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
format={'YYYY-MM-DD'}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
{/* TODO add options
|
||||||
|
<Form.Item
|
||||||
|
label={'Min Date'}
|
||||||
|
name={[field.name, 'min']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
getValueFromEvent={e => e.format('YYYY-MM-DD')}
|
||||||
|
getValueProps={e => ({value: e ? moment(e) : undefined})}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={'Max Date'}
|
||||||
|
name={[field.name, 'max']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
getValueFromEvent={e => e.format('YYYY-MM-DD')}
|
||||||
|
getValueProps={e => ({value: e ? moment(e) : undefined})}
|
||||||
|
>
|
||||||
|
<DatePicker />
|
||||||
|
</Form.Item>
|
||||||
|
*/}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
components/form/admin/types/dropdown.type.tsx
Normal file
18
components/form/admin/types/dropdown.type.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const DropdownType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
// TODO add dropdown options
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
components/form/admin/types/email.type.tsx
Normal file
20
components/form/admin/types/email.type.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const EmailType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Email'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ type: 'email', message: 'Must be a valid email' }
|
||||||
|
]}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input type={'email'} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
components/form/admin/types/hidden.type.tsx
Normal file
17
components/form/admin/types/hidden.type.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const HiddenType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
components/form/admin/types/index.ts
Normal file
29
components/form/admin/types/index.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {DateType} from './date.type'
|
||||||
|
import {DropdownType} from './dropdown.type'
|
||||||
|
import {EmailType} from './email.type'
|
||||||
|
import {HiddenType} from './hidden.type'
|
||||||
|
import {LinkType} from './link.type'
|
||||||
|
import {NumberType} from './number.type'
|
||||||
|
import {RadioType} from './radio.type'
|
||||||
|
import {RatingType} from './rating.type'
|
||||||
|
import {TextType} from './text.type'
|
||||||
|
import {TextareaType} from './textarea.type'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
import {YesNoType} from './yes_no.type'
|
||||||
|
|
||||||
|
export const adminTypes: {
|
||||||
|
[key: string]: React.FC<AdminFieldTypeProps>
|
||||||
|
} = {
|
||||||
|
'textfield': TextType,
|
||||||
|
'date': DateType,
|
||||||
|
'email': EmailType,
|
||||||
|
'textarea': TextareaType,
|
||||||
|
'link': LinkType,
|
||||||
|
'dropdown': DropdownType,
|
||||||
|
'rating': RatingType,
|
||||||
|
'radio': RadioType,
|
||||||
|
'hidden': HiddenType,
|
||||||
|
'yes_no': YesNoType,
|
||||||
|
'number': NumberType,
|
||||||
|
}
|
||||||
20
components/form/admin/types/link.type.tsx
Normal file
20
components/form/admin/types/link.type.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const LinkType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Link'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ type: 'url', message: 'Must be a valid URL' }
|
||||||
|
]}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input type={'url'} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
components/form/admin/types/number.type.tsx
Normal file
17
components/form/admin/types/number.type.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {Form, InputNumber} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const NumberType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Number'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
19
components/form/admin/types/radio.type.tsx
Normal file
19
components/form/admin/types/radio.type.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const RadioType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
// TODO Add radio support
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
components/form/admin/types/rating.type.tsx
Normal file
22
components/form/admin/types/rating.type.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {Form, Rate} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const RatingType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
// TODO add ratings
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
extra={'Click again to remove default value'}
|
||||||
|
>
|
||||||
|
<Rate
|
||||||
|
allowHalf
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
15
components/form/admin/types/text.type.tsx
Normal file
15
components/form/admin/types/text.type.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const TextType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
components/form/admin/types/textarea.type.tsx
Normal file
17
components/form/admin/types/textarea.type.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const TextareaType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
components/form/admin/types/type.props.ts
Normal file
6
components/form/admin/types/type.props.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {FormInstance} from 'antd/lib/form'
|
||||||
|
|
||||||
|
export interface AdminFieldTypeProps {
|
||||||
|
form: FormInstance
|
||||||
|
field: any
|
||||||
|
}
|
||||||
18
components/form/admin/types/yes_no.type.tsx
Normal file
18
components/form/admin/types/yes_no.type.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const YesNoType: React.FC<AdminFieldTypeProps> = props => {
|
||||||
|
// TODO add switch
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
label={'Default Value'}
|
||||||
|
name={[props.field.name, 'value']}
|
||||||
|
labelCol={{ span: 6 }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
85
components/form/field.tsx
Normal file
85
components/form/field.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import {Form, message} from 'antd'
|
||||||
|
import {useForm} from 'antd/lib/form/Form'
|
||||||
|
import React from 'react'
|
||||||
|
import {FormDesignFragment, FormFieldFragment} from '../../graphql/fragment/form.fragment'
|
||||||
|
import {StyledButton} from '../styled/button'
|
||||||
|
import {StyledH1} from '../styled/h1'
|
||||||
|
import {StyledP} from '../styled/p'
|
||||||
|
import {fieldTypes} from './types'
|
||||||
|
import {TextType} from './types/text.type'
|
||||||
|
import {FieldTypeProps} from './types/type.props'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
field: FormFieldFragment
|
||||||
|
design: FormDesignFragment
|
||||||
|
|
||||||
|
save: (data: any) => any
|
||||||
|
next: () => any
|
||||||
|
prev: () => any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Field: React.FC<Props> = ({field, save, design, children, next, prev, ...props}) => {
|
||||||
|
const [form] = useForm()
|
||||||
|
|
||||||
|
const FieldInput: React.FC<FieldTypeProps> = fieldTypes[field.type] || TextType
|
||||||
|
|
||||||
|
const finish = (data) => {
|
||||||
|
console.log('received field data', data)
|
||||||
|
save(data)
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = () => {
|
||||||
|
message.error('Check inputs!')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={finish}
|
||||||
|
onFinishFailed={error}
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: 32,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}>
|
||||||
|
<StyledH1 design={design} type={'question'}>{field.title}</StyledH1>
|
||||||
|
{field.description && <StyledP design={design} type={'question'}>{field.description}</StyledP>}
|
||||||
|
|
||||||
|
<FieldInput
|
||||||
|
design={design}
|
||||||
|
field={field}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: 32,
|
||||||
|
display: 'flex',
|
||||||
|
}}>
|
||||||
|
<StyledButton
|
||||||
|
background={design.colors.buttonColor}
|
||||||
|
color={design.colors.buttonTextColor}
|
||||||
|
highlight={design.colors.buttonActiveColor}
|
||||||
|
onClick={prev}
|
||||||
|
>{'Previous'}</StyledButton>
|
||||||
|
|
||||||
|
<div style={{flex: 1}} />
|
||||||
|
|
||||||
|
<StyledButton
|
||||||
|
background={design.colors.buttonColor}
|
||||||
|
color={design.colors.buttonTextColor}
|
||||||
|
highlight={design.colors.buttonActiveColor}
|
||||||
|
size={'large'}
|
||||||
|
onClick={form.submit}
|
||||||
|
>{'Next'}</StyledButton>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
FormPublicDesignFragment,
|
|
||||||
FormPublicFieldFragment,
|
|
||||||
} from '../../../../graphql/fragment/form.public.fragment'
|
|
||||||
import { StyledH1 } from '../../../styled/h1'
|
|
||||||
import { StyledMarkdown } from '../../../styled/markdown'
|
|
||||||
import { useRouter } from '../../../use.router'
|
|
||||||
import { fieldTypes } from '../../types'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
focus?: boolean
|
|
||||||
field: FormPublicFieldFragment
|
|
||||||
design: FormPublicDesignFragment
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Field: React.FC<Props> = ({ field, design, focus, ...props }) => {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const FieldInput = (fieldTypes[field.type] || fieldTypes['text']).inputFormField()
|
|
||||||
|
|
||||||
const getUrlDefault = (): string => {
|
|
||||||
if (router.query[field.id]) {
|
|
||||||
return router.query[field.id] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
if (router.query[field.slug]) {
|
|
||||||
return router.query[field.slug] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
padding: 32,
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledH1 design={design} type={'question'}>
|
|
||||||
{field.title}
|
|
||||||
</StyledH1>
|
|
||||||
{field.description && (
|
|
||||||
<StyledMarkdown design={design} type={'question'} >{field.description}</StyledMarkdown>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FieldInput
|
|
||||||
design={design}
|
|
||||||
field={field}
|
|
||||||
urlValue={getUrlDefault()}
|
|
||||||
focus={focus}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,221 +0,0 @@
|
|||||||
import { Card, Form, message, Modal, Spin } from 'antd'
|
|
||||||
import debug from 'debug'
|
|
||||||
import { darken, lighten } from 'polished'
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
import { Omf } from '../../../omf'
|
|
||||||
import { StyledButton } from '../../../styled/button'
|
|
||||||
import { useMath } from '../../../use.math'
|
|
||||||
import { fieldTypes } from '../../types'
|
|
||||||
import { LayoutProps } from '../layout.props'
|
|
||||||
import { Field } from './field'
|
|
||||||
import { Page } from './page'
|
|
||||||
|
|
||||||
type Step = 'start' | 'form' | 'end'
|
|
||||||
|
|
||||||
const logger = debug('layout/card')
|
|
||||||
|
|
||||||
const MyCard = styled.div<{ background: string }>`
|
|
||||||
background: ${(props) => darken(0.1, props.background)};
|
|
||||||
height: 100%;
|
|
||||||
min-height: 100vh;
|
|
||||||
min-height: calc(var(--vh, 1vh) * 100);
|
|
||||||
|
|
||||||
padding: 32px;
|
|
||||||
|
|
||||||
.ant-card {
|
|
||||||
background: ${(props) => props.background};
|
|
||||||
border-color: ${(props) => lighten(0.4, props.background)};
|
|
||||||
width: 800px;
|
|
||||||
margin: auto;
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const CardLayout: React.FC<LayoutProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [form] = Form.useForm()
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [step, setStep] = useState<Step>(props.form.startPage.show ? 'start' : 'form')
|
|
||||||
const evaluator = useMath()
|
|
||||||
const [visiblity, setVisibility] = useState({})
|
|
||||||
|
|
||||||
const { design, startPage, endPage, fields } = props.form
|
|
||||||
const { setField } = props.submission
|
|
||||||
|
|
||||||
const updateValues = useCallback(() => {
|
|
||||||
const defaults = {}
|
|
||||||
|
|
||||||
fields.forEach(field => {
|
|
||||||
const defaultValue = field.defaultValue
|
|
||||||
? fieldTypes[field.type].parseValue(field.defaultValue)
|
|
||||||
: null
|
|
||||||
|
|
||||||
defaults[`@${field.id}`] = form.getFieldValue([field.id, 'value']) ?? defaultValue
|
|
||||||
|
|
||||||
if (field.slug) {
|
|
||||||
defaults[`$${field.slug}`] = form.getFieldValue([field.id, 'value']) ?? defaultValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// now calculate visibility
|
|
||||||
const nextVisibility = {}
|
|
||||||
fields.forEach(field => {
|
|
||||||
if (!field.logic) return
|
|
||||||
|
|
||||||
const logic = field.logic
|
|
||||||
.filter(logic => logic.action === 'visible')
|
|
||||||
|
|
||||||
if (logic.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextVisibility[field.id] = logic
|
|
||||||
.map(logic => {
|
|
||||||
try {
|
|
||||||
const r = evaluator(
|
|
||||||
logic.formula,
|
|
||||||
defaults
|
|
||||||
)
|
|
||||||
|
|
||||||
return Boolean(r)
|
|
||||||
} catch {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.reduce<boolean>((previous, current) => previous && current, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO improve logic of how we calculate new logic checks
|
|
||||||
if (Object.values(nextVisibility).join(',') == Object.values(visiblity).join(',')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setVisibility(nextVisibility)
|
|
||||||
}, [
|
|
||||||
fields, form, visiblity,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateValues()
|
|
||||||
}, [updateValues])
|
|
||||||
|
|
||||||
const finish = async (data: { [key: number]: unknown }) => {
|
|
||||||
logger('finish form %O', data)
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// save fields
|
|
||||||
await Promise.all(Object.keys(data).map((fieldId) => setField(fieldId, data[fieldId])))
|
|
||||||
|
|
||||||
await props.submission.finish()
|
|
||||||
|
|
||||||
if (endPage.show) {
|
|
||||||
setStep('end')
|
|
||||||
} else {
|
|
||||||
Modal.success({
|
|
||||||
content: t('form:submitted'),
|
|
||||||
okText: t('from:restart'),
|
|
||||||
onOk: () => {
|
|
||||||
window.location.reload()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger('failed to finish form %O', e)
|
|
||||||
void message.error({
|
|
||||||
content: 'Error saving Input',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('render')
|
|
||||||
|
|
||||||
const render = () => {
|
|
||||||
switch (step) {
|
|
||||||
case 'start':
|
|
||||||
return <Page page={startPage} design={design} next={() => setStep('form')} />
|
|
||||||
|
|
||||||
case 'form':
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
onFinish={finish}
|
|
||||||
onValuesChange={updateValues}
|
|
||||||
>
|
|
||||||
{fields.map((field, i) => {
|
|
||||||
if (field.type === 'hidden') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visiblity[field.id] === false) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Field
|
|
||||||
key={field.id}
|
|
||||||
field={field}
|
|
||||||
design={design}
|
|
||||||
focus={i === 0}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: 32,
|
|
||||||
display: 'flex',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{startPage.show && (
|
|
||||||
<StyledButton
|
|
||||||
background={design.colors.button}
|
|
||||||
color={design.colors.buttonText}
|
|
||||||
highlight={design.colors.buttonActive}
|
|
||||||
onClick={() => setStep('start')}
|
|
||||||
>
|
|
||||||
{t('form:previous')}
|
|
||||||
</StyledButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ flex: 1 }} />
|
|
||||||
|
|
||||||
<StyledButton
|
|
||||||
background={design.colors.button}
|
|
||||||
color={design.colors.buttonText}
|
|
||||||
highlight={design.colors.buttonActive}
|
|
||||||
size={'large'}
|
|
||||||
onClick={form.submit}
|
|
||||||
>
|
|
||||||
{t('form:next')}
|
|
||||||
</StyledButton>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
|
|
||||||
case 'end':
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
page={endPage}
|
|
||||||
design={design}
|
|
||||||
next={() => {
|
|
||||||
window.location.reload()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MyCard background={design.colors.background}>
|
|
||||||
<Omf />
|
|
||||||
|
|
||||||
<Spin spinning={loading}>{render()}</Spin>
|
|
||||||
</MyCard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import { Card } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import {
|
|
||||||
FormPublicDesignFragment,
|
|
||||||
FormPublicPageFragment,
|
|
||||||
} from '../../../../graphql/fragment/form.public.fragment'
|
|
||||||
import { StyledButton } from '../../../styled/button'
|
|
||||||
import { StyledH1 } from '../../../styled/h1'
|
|
||||||
import { StyledMarkdown } from '../../../styled/markdown'
|
|
||||||
import { PageButtons } from '../page.buttons'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
page: FormPublicPageFragment
|
|
||||||
design: FormPublicDesignFragment
|
|
||||||
|
|
||||||
next?: () => void
|
|
||||||
prev?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Page: React.FC<Props> = ({ design, page, next, prev }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<StyledH1 design={design} type={'question'}>
|
|
||||||
{page.title}
|
|
||||||
</StyledH1>
|
|
||||||
<StyledMarkdown design={design} type={'question'}>{page.paragraph}</StyledMarkdown>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: 32,
|
|
||||||
display: 'flex',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{prev && (
|
|
||||||
<StyledButton
|
|
||||||
background={design.colors.button}
|
|
||||||
color={design.colors.buttonText}
|
|
||||||
highlight={design.colors.buttonActive}
|
|
||||||
onClick={prev}
|
|
||||||
>
|
|
||||||
{t('form:restart')}
|
|
||||||
</StyledButton>
|
|
||||||
)}
|
|
||||||
<PageButtons buttons={page.buttons} />
|
|
||||||
|
|
||||||
<div style={{ flex: 1 }} />
|
|
||||||
|
|
||||||
{next && (
|
|
||||||
<StyledButton
|
|
||||||
background={design.colors.button}
|
|
||||||
color={design.colors.buttonText}
|
|
||||||
highlight={design.colors.buttonActive}
|
|
||||||
size={'large'}
|
|
||||||
onClick={next}
|
|
||||||
>
|
|
||||||
{page.buttonText || t('form:continue')}
|
|
||||||
</StyledButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { FormPublicFragment } from '../../../graphql/fragment/form.public.fragment'
|
|
||||||
import { Submission } from '../../use.submission'
|
|
||||||
|
|
||||||
export interface LayoutProps {
|
|
||||||
form: FormPublicFragment
|
|
||||||
submission: Submission
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import { Space } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { FormPublicPageButtonFragment } from '../../../graphql/fragment/form.public.fragment'
|
|
||||||
import { StyledButton } from '../../styled/button'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
buttons: FormPublicPageButtonFragment[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PageButtons: React.FC<Props> = ({ buttons }) => {
|
|
||||||
if (buttons.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
{buttons.map((button, key) => {
|
|
||||||
return (
|
|
||||||
<StyledButton
|
|
||||||
background={button.bgColor}
|
|
||||||
color={button.color}
|
|
||||||
highlight={button.activeColor}
|
|
||||||
key={key}
|
|
||||||
href={button.url}
|
|
||||||
target={'_blank'}
|
|
||||||
rel={'noreferrer'}
|
|
||||||
>
|
|
||||||
{button.text}
|
|
||||||
</StyledButton>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
import { Form, message } from 'antd'
|
|
||||||
import { useForm } from 'antd/lib/form/Form'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import {
|
|
||||||
FormPublicDesignFragment,
|
|
||||||
FormPublicFieldFragment,
|
|
||||||
} from '../../../../graphql/fragment/form.public.fragment'
|
|
||||||
import { StyledButton } from '../../../styled/button'
|
|
||||||
import { StyledH1 } from '../../../styled/h1'
|
|
||||||
import { StyledMarkdown } from '../../../styled/markdown'
|
|
||||||
import { useRouter } from '../../../use.router'
|
|
||||||
import { fieldTypes } from '../../types'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
focus: boolean
|
|
||||||
field: FormPublicFieldFragment
|
|
||||||
design: FormPublicDesignFragment
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
save: (data: any) => void
|
|
||||||
next: () => void
|
|
||||||
prev: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...props }) => {
|
|
||||||
const [form] = useForm()
|
|
||||||
const router = useRouter()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const FieldInput = (fieldTypes[field.type] || fieldTypes[field.type]).inputFormField()
|
|
||||||
|
|
||||||
const finish = (data) => {
|
|
||||||
console.log('received field data', data)
|
|
||||||
save(data)
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = async () => {
|
|
||||||
await message.error('Check inputs!')
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUrlDefault = (): string => {
|
|
||||||
if (router.query[field.id]) {
|
|
||||||
return router.query[field.id] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
if (router.query[field.slug]) {
|
|
||||||
return router.query[field.slug] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
onFinish={finish}
|
|
||||||
onFinishFailed={error}
|
|
||||||
{...props}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
padding: 32,
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledH1 design={design} type={'question'}>
|
|
||||||
{field.title}
|
|
||||||
</StyledH1>
|
|
||||||
{field.description && (
|
|
||||||
<StyledMarkdown design={design} type={'question'}>{field.description}</StyledMarkdown>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FieldInput
|
|
||||||
design={design}
|
|
||||||
field={field}
|
|
||||||
focus={props.focus}
|
|
||||||
urlValue={getUrlDefault()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: 32,
|
|
||||||
display: 'flex',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledButton
|
|
||||||
background={design.colors.button}
|
|
||||||
color={design.colors.buttonText}
|
|
||||||
highlight={design.colors.buttonActive}
|
|
||||||
onClick={prev}
|
|
||||||
>
|
|
||||||
{t('form:previous')}
|
|
||||||
</StyledButton>
|
|
||||||
|
|
||||||
<div style={{ flex: 1 }} />
|
|
||||||
|
|
||||||
<StyledButton
|
|
||||||
background={design.colors.button}
|
|
||||||
color={design.colors.buttonText}
|
|
||||||
highlight={design.colors.buttonActive}
|
|
||||||
size={'large'}
|
|
||||||
onClick={form.submit}
|
|
||||||
>
|
|
||||||
{t('form:next')}
|
|
||||||
</StyledButton>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import { Modal } from 'antd'
|
|
||||||
import debug from 'debug'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import SwiperClass from 'swiper'
|
|
||||||
import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'
|
|
||||||
import { Omf } from '../../../omf'
|
|
||||||
import { useWindowSize } from '../../../use.window.size'
|
|
||||||
import { LayoutProps } from '../layout.props'
|
|
||||||
import { Field } from './field'
|
|
||||||
import { FormPage } from './page'
|
|
||||||
|
|
||||||
const logger = debug('layout/slider')
|
|
||||||
|
|
||||||
export const SliderLayout: React.FC<LayoutProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [swiper, setSwiper] = useState<SwiperClass>(null)
|
|
||||||
const { height } = useWindowSize()
|
|
||||||
const { design, startPage, endPage, fields } = props.form
|
|
||||||
const { finish, setField } = props.submission
|
|
||||||
|
|
||||||
const goNext = () => {
|
|
||||||
if (!swiper) return
|
|
||||||
|
|
||||||
logger('goNext')
|
|
||||||
swiper.allowSlideNext = true
|
|
||||||
swiper.slideNext()
|
|
||||||
swiper.allowSlideNext = false
|
|
||||||
}
|
|
||||||
const goPrev = () => {
|
|
||||||
if (!swiper) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger('goPrevious')
|
|
||||||
swiper.slidePrev()
|
|
||||||
}
|
|
||||||
|
|
||||||
const swiperConfig: SwiperProps = {
|
|
||||||
direction: 'vertical',
|
|
||||||
allowSlideNext: false,
|
|
||||||
allowSlidePrev: true,
|
|
||||||
noSwiping: true,
|
|
||||||
updateOnWindowResize: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={'swiper-container'}
|
|
||||||
style={{
|
|
||||||
background: design.colors.background,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Omf />
|
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
|
|
||||||
<Swiper
|
|
||||||
height={height}
|
|
||||||
{...swiperConfig}
|
|
||||||
onSwiper={next => {
|
|
||||||
logger('setSwiper')
|
|
||||||
setSwiper(next)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{[
|
|
||||||
startPage.show ? (
|
|
||||||
<SwiperSlide key={'start'}>
|
|
||||||
<FormPage page={startPage} design={design} next={goNext} prev={goPrev} />
|
|
||||||
</SwiperSlide>
|
|
||||||
) : undefined,
|
|
||||||
...fields
|
|
||||||
.map((field, i) => {
|
|
||||||
if (field.type === 'hidden') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SwiperSlide key={field.id}>
|
|
||||||
<Field
|
|
||||||
field={field}
|
|
||||||
focus={swiper?.activeIndex === (startPage.show ? 1 : 0) + i}
|
|
||||||
design={design}
|
|
||||||
save={async (values: { [key: string]: unknown }) => {
|
|
||||||
await setField(field.id, values[field.id])
|
|
||||||
|
|
||||||
if (fields.length === i + 1) {
|
|
||||||
await finish()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
next={() => {
|
|
||||||
if (fields.length === i + 1) {
|
|
||||||
// prevent going back!
|
|
||||||
swiper.allowSlidePrev = true
|
|
||||||
|
|
||||||
if (!endPage.show) {
|
|
||||||
Modal.success({
|
|
||||||
content: t('form:submitted'),
|
|
||||||
okText: t('from:restart'),
|
|
||||||
onOk: () => {
|
|
||||||
window.location.reload()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
goNext()
|
|
||||||
}}
|
|
||||||
prev={goPrev}
|
|
||||||
/>
|
|
||||||
</SwiperSlide>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.filter((e) => e !== null),
|
|
||||||
endPage.show ? (
|
|
||||||
<SwiperSlide key={'end'}>
|
|
||||||
<FormPage page={endPage} design={design} next={finish} prev={goPrev} />
|
|
||||||
</SwiperSlide>
|
|
||||||
) : undefined,
|
|
||||||
].filter((e) => !!e)}
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
.main {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import {
|
|
||||||
FormPublicDesignFragment,
|
|
||||||
FormPublicPageFragment,
|
|
||||||
} from '../../../../graphql/fragment/form.public.fragment'
|
|
||||||
import { StyledButton } from '../../../styled/button'
|
|
||||||
import { StyledH1 } from '../../../styled/h1'
|
|
||||||
import { StyledMarkdown } from '../../../styled/markdown'
|
|
||||||
import { PageButtons } from '../page.buttons'
|
|
||||||
import scss from './page.module.scss'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
page: FormPublicPageFragment
|
|
||||||
design: FormPublicDesignFragment
|
|
||||||
className?: string
|
|
||||||
|
|
||||||
next: () => void
|
|
||||||
prev: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FormPage: React.FC<Props> = ({ page, design, next, prev, className, ...props }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
if (!page.show) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={[scss.main, className].filter((c) => !!c).join(' ')} {...props}>
|
|
||||||
<div className={scss.content}>
|
|
||||||
<StyledH1 design={design} type={'question'}>
|
|
||||||
{page.title}
|
|
||||||
</StyledH1>
|
|
||||||
<StyledMarkdown design={design} type={'question'}>{page.paragraph}</StyledMarkdown>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: 32,
|
|
||||||
display: 'flex',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{prev && (
|
|
||||||
<StyledButton
|
|
||||||
background={design.colors.button}
|
|
||||||
color={design.colors.buttonText}
|
|
||||||
highlight={design.colors.buttonActive}
|
|
||||||
onClick={prev}
|
|
||||||
>
|
|
||||||
{t('form:previous')}
|
|
||||||
</StyledButton>
|
|
||||||
)}
|
|
||||||
<PageButtons buttons={page.buttons} />
|
|
||||||
|
|
||||||
<div style={{ flex: 1 }} />
|
|
||||||
|
|
||||||
<StyledButton
|
|
||||||
background={design.colors.button}
|
|
||||||
color={design.colors.buttonText}
|
|
||||||
highlight={design.colors.buttonActive}
|
|
||||||
size={'large'}
|
|
||||||
onClick={next}
|
|
||||||
>
|
|
||||||
{page.buttonText || t('form:continue')}
|
|
||||||
</StyledButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
70
components/form/page.tsx
Normal file
70
components/form/page.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import {Space} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {FormDesignFragment, FormPageFragment} from '../../graphql/fragment/form.fragment'
|
||||||
|
import {StyledButton} from '../styled/button'
|
||||||
|
import {StyledH1} from '../styled/h1'
|
||||||
|
import {StyledP} from '../styled/p'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: 'start' | 'end'
|
||||||
|
page: FormPageFragment
|
||||||
|
design: FormDesignFragment
|
||||||
|
|
||||||
|
next: () => any
|
||||||
|
prev: () => any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormPage: React.FC<Props> = ({page, design, next, prev, type, children, ...props}) => {
|
||||||
|
if (!page.show) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}} {...props}>
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<StyledH1 design={design} type={'question'}>{page.title}</StyledH1>
|
||||||
|
<StyledP design={design} type={'question'}>{page.paragraph}</StyledP>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: 32,
|
||||||
|
display: 'flex',
|
||||||
|
}}>
|
||||||
|
{page.buttons.length > 0 && (
|
||||||
|
<Space>
|
||||||
|
{page.buttons.map((button, key) => {
|
||||||
|
return (
|
||||||
|
<StyledButton
|
||||||
|
background={button.bgColor}
|
||||||
|
color={button.color}
|
||||||
|
highlight={button.activeColor}
|
||||||
|
key={key}
|
||||||
|
href={button.url}
|
||||||
|
target={'_blank'}
|
||||||
|
>{button.text}</StyledButton>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{flex: 1}} />
|
||||||
|
|
||||||
|
<StyledButton
|
||||||
|
background={design.colors.buttonColor}
|
||||||
|
color={design.colors.buttonTextColor}
|
||||||
|
highlight={design.colors.buttonActiveColor}
|
||||||
|
size={'large'}
|
||||||
|
onClick={next}
|
||||||
|
>{page.buttonText || 'Continue'}</StyledButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import React, { ComponentType } from 'react'
|
|
||||||
import { FieldAdminProps } from './field.admin.props'
|
|
||||||
import { FieldInputProps } from './field.input.props'
|
|
||||||
|
|
||||||
export abstract class AbstractType<A = any> {
|
|
||||||
public parseValue(raw: string): A {
|
|
||||||
return JSON.parse(raw) as A
|
|
||||||
}
|
|
||||||
|
|
||||||
public parseUrlValue(raw: string): A {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
||||||
return raw as any
|
|
||||||
}
|
|
||||||
|
|
||||||
public stringifyValue(raw: string): string {
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
public displayValue(raw: string): JSX.Element {
|
|
||||||
const data = this.parseValue(raw)
|
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
return (
|
|
||||||
<ul>
|
|
||||||
{data.map(r => (
|
|
||||||
<li key={r}>{JSON.stringify(r)}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>{this.stringifyValue(raw)}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract adminFormField(): ComponentType<FieldAdminProps>
|
|
||||||
|
|
||||||
public abstract inputFormField(): ComponentType<FieldInputProps>
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import { Button, Col, Form, Input, Row } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
|
|
||||||
export const CheckboxAdmin: React.FC<FieldAdminProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:checkbox:default')}
|
|
||||||
name={[props.field.name as string, 'defaultValue']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.List name={[props.field.name as string, 'options']}>
|
|
||||||
{(fields, { add, remove }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{
|
|
||||||
sm: { offset: index === 0 ? 0 : 6 },
|
|
||||||
}}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
label={index === 0 ? t('type:checkbox:options') : ''}
|
|
||||||
key={field.key}
|
|
||||||
>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{ span: 24 }}
|
|
||||||
name={[field.name, 'title']}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder={t('type:checkbox:titlePlaceholder')} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{ span: 24 }}
|
|
||||||
name={[field.name, 'value']}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
rules={[{ required: true, message: t('validation:valueRequired') }]}
|
|
||||||
>
|
|
||||||
<Input placeholder={t('type:checkbox:valuePlaceholder')} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Button danger onClick={() => remove(index)}>
|
|
||||||
{t('type:checkbox:removeOption')}
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{
|
|
||||||
sm: { offset: 6 },
|
|
||||||
}}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Button type={'dashed'} onClick={() => add()}>
|
|
||||||
{t('type:checkbox:addOption')}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
import { Checkbox, Form } from 'antd'
|
|
||||||
import debug from 'debug'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { StyledCheckbox } from '../../../styled/checkbox'
|
|
||||||
import { FieldInputBuilderType } from '../field.input.builder.type'
|
|
||||||
|
|
||||||
const logger = debug('checkbox.input')
|
|
||||||
|
|
||||||
export const builder: FieldInputBuilderType = ({
|
|
||||||
parseUrlValue,
|
|
||||||
parseValue,
|
|
||||||
}) => function CheckboxInput ({
|
|
||||||
field,
|
|
||||||
design,
|
|
||||||
urlValue,
|
|
||||||
focus,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
let initialValue: string = undefined
|
|
||||||
|
|
||||||
if (field.defaultValue) {
|
|
||||||
try {
|
|
||||||
initialValue = parseValue(field.defaultValue)
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid default value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlValue) {
|
|
||||||
initialValue = parseUrlValue(urlValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
name={[field.id]}
|
|
||||||
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
|
|
||||||
initialValue={field.options
|
|
||||||
.map((option) => option.value)
|
|
||||||
.find((value) => value === initialValue)}
|
|
||||||
>
|
|
||||||
<Checkbox.Group>
|
|
||||||
{field.options
|
|
||||||
.filter((option) => option.key === null)
|
|
||||||
.map((option, i) => (
|
|
||||||
<StyledCheckbox
|
|
||||||
design={design}
|
|
||||||
value={option.value}
|
|
||||||
key={option.value}
|
|
||||||
autoFocus={i === 0 && focus}
|
|
||||||
>
|
|
||||||
{option.title || option.value}
|
|
||||||
</StyledCheckbox>
|
|
||||||
))}
|
|
||||||
</Checkbox.Group>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class CheckboxType extends AbstractType<string> {
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./checkbox.admin').then(c => c.CheckboxAdmin));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return dynamic(() => import('./checkbox.input').then(c => c.builder(this)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
components/form/types/date.type.tsx
Normal file
30
components/form/types/date.type.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {Form} from 'antd'
|
||||||
|
import moment from 'moment'
|
||||||
|
import React from 'react'
|
||||||
|
import {StyledDateInput} from '../../styled/date.input'
|
||||||
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const DateType: React.FC<FieldTypeProps> = ({ field, design}) => {
|
||||||
|
// TODO check min and max
|
||||||
|
// TODO if default is passed, then the changing should not be required
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
name={[field.id, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
|
]}
|
||||||
|
getValueFromEvent={e => e.format('YYYY-MM-DD')}
|
||||||
|
getValueProps={e => ({value: e ? moment(e) : undefined})}
|
||||||
|
>
|
||||||
|
<StyledDateInput
|
||||||
|
size={'large'}
|
||||||
|
defaultValue={field.value ? moment(field.value) : undefined}
|
||||||
|
design={design}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import { DatePicker, Form } from 'antd'
|
|
||||||
import moment, { Moment } from 'moment'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
|
|
||||||
export const DateAdmin: React.FC<FieldAdminProps> = ({ field }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:date.default')}
|
|
||||||
name={[field.name as string, 'defaultValue']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
getValueFromEvent={(e: Moment) => (e ? e.format('YYYY-MM-DD') : undefined)}
|
|
||||||
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
|
|
||||||
>
|
|
||||||
<DatePicker format={'YYYY-MM-DD'} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:date.min')}
|
|
||||||
name={[
|
|
||||||
field.name as string,
|
|
||||||
'optionKeys',
|
|
||||||
'min',
|
|
||||||
]}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
|
|
||||||
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
|
|
||||||
>
|
|
||||||
<DatePicker />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:date.max')}
|
|
||||||
name={[
|
|
||||||
field.name as string,
|
|
||||||
'optionKeys',
|
|
||||||
'max',
|
|
||||||
]}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
|
|
||||||
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
|
|
||||||
>
|
|
||||||
<DatePicker />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
import { Form } from 'antd'
|
|
||||||
import dayjs, { Dayjs } from 'dayjs'
|
|
||||||
import debug from 'debug'
|
|
||||||
import moment, { Moment } from 'moment'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { StyledDateInput } from '../../../styled/date.input'
|
|
||||||
import { FieldInputBuilderType } from '../field.input.builder.type'
|
|
||||||
|
|
||||||
const logger = debug('date.input')
|
|
||||||
|
|
||||||
export const builder: FieldInputBuilderType = ({
|
|
||||||
parseUrlValue,
|
|
||||||
parseValue,
|
|
||||||
}) => function DateInput ({
|
|
||||||
field,
|
|
||||||
design,
|
|
||||||
urlValue,
|
|
||||||
focus,
|
|
||||||
}) {
|
|
||||||
const [min, setMin] = useState<Dayjs>()
|
|
||||||
const [max, setMax] = useState<Dayjs>()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
field.options.forEach((option) => {
|
|
||||||
if (option.key === 'min') {
|
|
||||||
setMin(dayjs(option.value))
|
|
||||||
}
|
|
||||||
if (option.key === 'max') {
|
|
||||||
setMax(dayjs(option.value))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [field])
|
|
||||||
|
|
||||||
let initialValue: Moment = undefined
|
|
||||||
|
|
||||||
if (field.defaultValue) {
|
|
||||||
try {
|
|
||||||
initialValue = parseValue(field.defaultValue)
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid default value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlValue) {
|
|
||||||
initialValue = parseUrlValue(urlValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
name={[field.id]}
|
|
||||||
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
|
|
||||||
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
|
|
||||||
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
|
|
||||||
initialValue={initialValue}
|
|
||||||
>
|
|
||||||
<StyledDateInput
|
|
||||||
autoFocus={focus}
|
|
||||||
size={'large'}
|
|
||||||
design={design}
|
|
||||||
disabledDate={(d: Moment) => {
|
|
||||||
if (min && min.isAfter(d.toDate())) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (max && max.isBefore(d.toDate())) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import moment, { Moment } from 'moment'
|
|
||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class DateType extends AbstractType<Moment> {
|
|
||||||
parseValue(raw: string): Moment {
|
|
||||||
return moment(JSON.parse(raw))
|
|
||||||
}
|
|
||||||
|
|
||||||
parseUrlValue(raw: string): Moment {
|
|
||||||
return moment(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./date.admin').then(c => c.DateAdmin));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return dynamic(() => import('./date.input').then(c => c.builder(this)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
components/form/types/dropdown.type.tsx
Normal file
19
components/form/types/dropdown.type.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const DropdownType: React.FC<FieldTypeProps> = ({field}) => {
|
||||||
|
// TODO add dropdown options
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
name={[field.id, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import { Button, Col, Form, Input, Row } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
|
|
||||||
export const DropdownAdmin: React.FC<FieldAdminProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:dropdown.default')}
|
|
||||||
name={[props.field.name as string, 'defaultValue']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.List name={[props.field.name as string, 'options']}>
|
|
||||||
{(fields, { add, remove }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{
|
|
||||||
sm: { offset: index === 0 ? 0 : 6 },
|
|
||||||
}}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
label={index === 0 ? t('type:dropdown.options') : ''}
|
|
||||||
key={field.key}
|
|
||||||
>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{ span: 24 }}
|
|
||||||
name={[field.name, 'title']}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder={t('type:dropdown.titlePlaceholder')} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{ span: 24 }}
|
|
||||||
name={[field.name, 'value']}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
rules={[{ required: true, message: t('validation:valueRequired') }]}
|
|
||||||
>
|
|
||||||
<Input placeholder={t('type:dropdown.valuePlaceholder')} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Button danger onClick={() => remove(index)}>
|
|
||||||
{t('type:dropdown.removeOption')}
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{
|
|
||||||
sm: { offset: 6 },
|
|
||||||
}}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Button type={'dashed'} onClick={() => add()}>
|
|
||||||
{t('type:dropdown.addOption')}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
import { Form, Select } from 'antd'
|
|
||||||
import debug from 'debug'
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { StyledSelect } from '../../../styled/select'
|
|
||||||
import { FieldInputBuilderType } from '../field.input.builder.type'
|
|
||||||
|
|
||||||
const logger = debug('field/dropdown')
|
|
||||||
|
|
||||||
export const builder: FieldInputBuilderType = ({
|
|
||||||
parseUrlValue,
|
|
||||||
parseValue,
|
|
||||||
}) => function DateInput ({
|
|
||||||
field,
|
|
||||||
design,
|
|
||||||
urlValue,
|
|
||||||
focus,
|
|
||||||
}) {
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
let initialValue = null
|
|
||||||
|
|
||||||
if (field.defaultValue) {
|
|
||||||
try {
|
|
||||||
initialValue = parseValue(field.defaultValue)
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid default value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlValue) {
|
|
||||||
initialValue = parseUrlValue(urlValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
name={[field.id]}
|
|
||||||
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
|
|
||||||
initialValue={initialValue}
|
|
||||||
>
|
|
||||||
<StyledSelect
|
|
||||||
autoFocus={focus}
|
|
||||||
design={design}
|
|
||||||
open={open}
|
|
||||||
onBlur={() => setOpen(false)}
|
|
||||||
onFocus={() => setOpen(true)}
|
|
||||||
onSelect={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
{field.options
|
|
||||||
.filter((option) => option.key === null)
|
|
||||||
.map((option) => (
|
|
||||||
<Select.Option value={option.value} key={option.value}>
|
|
||||||
{option.title || option.value}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</StyledSelect>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class DropdownType extends AbstractType<string> {
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./dropdown.admin').then(c => c.DropdownAdmin));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return dynamic(() => import('./dropdown.input').then(c => c.builder(this)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
components/form/types/email.type.tsx
Normal file
25
components/form/types/email.type.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {Form} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {StyledInput} from '../../styled/input'
|
||||||
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const EmailType: React.FC<FieldTypeProps> = ({field, design}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
name={[field.id, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
|
{ type: 'email', message: 'Must be a valid email' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<StyledInput
|
||||||
|
design={design}
|
||||||
|
allowClear
|
||||||
|
size={'large'}
|
||||||
|
defaultValue={field.value}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { Form, Input } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
|
|
||||||
export const EmailAdmin: React.FC<FieldAdminProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:email.default')}
|
|
||||||
name={[props.field.name as string, 'defaultValue']}
|
|
||||||
rules={[{ type: 'email', message: t('validation:emailRequired') }]}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input type={'email'} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { Form } from 'antd'
|
|
||||||
import debug from 'debug'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { StyledInput } from '../../../styled/input'
|
|
||||||
import { FieldInputBuilderType } from '../field.input.builder.type'
|
|
||||||
|
|
||||||
const logger = debug('email.input')
|
|
||||||
|
|
||||||
export const builder: FieldInputBuilderType = ({
|
|
||||||
parseUrlValue,
|
|
||||||
parseValue,
|
|
||||||
}) => function EmailInput ({
|
|
||||||
field,
|
|
||||||
design,
|
|
||||||
urlValue,
|
|
||||||
focus,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
let initialValue = null
|
|
||||||
|
|
||||||
if (field.defaultValue) {
|
|
||||||
try {
|
|
||||||
initialValue = parseValue(field.defaultValue)
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid default value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlValue) {
|
|
||||||
initialValue = parseUrlValue(urlValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
name={[field.id]}
|
|
||||||
rules={[
|
|
||||||
{ required: field.required, message: t('validation:valueRequired') },
|
|
||||||
{ type: 'email', message: t('validation:invalidEmail') },
|
|
||||||
]}
|
|
||||||
initialValue={initialValue}
|
|
||||||
>
|
|
||||||
<StyledInput autoFocus={focus} design={design} allowClear size={'large'} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class EmailType extends AbstractType<string> {
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./email.admin').then(c => c.EmailAdmin));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return dynamic(() => import('./email.input').then(c => c.builder(this)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { FormInstance } from 'antd/lib/form'
|
|
||||||
import { FieldData } from 'rc-field-form/lib/interface'
|
|
||||||
|
|
||||||
export interface FieldAdminProps {
|
|
||||||
form: FormInstance
|
|
||||||
field: FieldData
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { ComponentType } from 'react'
|
|
||||||
|
|
||||||
import { AbstractType } from './abstract.type'
|
|
||||||
import { FieldInputProps } from './field.input.props'
|
|
||||||
|
|
||||||
export type FieldInputBuilderType<A = AbstractType> = (type: A) => ComponentType<FieldInputProps>
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import {
|
|
||||||
FormPublicDesignFragment,
|
|
||||||
FormPublicFieldFragment,
|
|
||||||
} from '../../../graphql/fragment/form.public.fragment'
|
|
||||||
|
|
||||||
export interface FieldInputProps {
|
|
||||||
field: FormPublicFieldFragment
|
|
||||||
design: FormPublicDesignFragment
|
|
||||||
focus?: boolean
|
|
||||||
urlValue?: string
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { Form, Input } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
|
|
||||||
export const HiddenAdmin: React.FC<FieldAdminProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:hidden.default')}
|
|
||||||
name={[props.field.name as string, 'defaultValue']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class HiddenType extends AbstractType<string> {
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./hidden.admin').then(c => c.HiddenAdmin));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
export {}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
export {}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
TODO
|
|
||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class ImageType extends AbstractType<string> {
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./dropdown.admin').then(c => c.DropdownAdmin));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return dynamic(() => import('./dropdown.input').then(c => c.builder(this)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
export {}
|
|
||||||
@ -1,34 +1,27 @@
|
|||||||
import { AbstractType } from './abstract.type'
|
import React from 'react'
|
||||||
import { CheckboxType } from './checkbox'
|
import {DateType} from './date.type'
|
||||||
import { DateType } from './date'
|
import {DropdownType} from './dropdown.type'
|
||||||
import { DropdownType } from './dropdown'
|
import {EmailType} from './email.type'
|
||||||
import { EmailType } from './email'
|
import {LinkType} from './link.type'
|
||||||
import { HiddenType } from './hidden'
|
import {NumberType} from './number.type'
|
||||||
import { LinkType } from './link'
|
import {RadioType} from './radio.type'
|
||||||
import { LocationType } from './location'
|
import {RatingType} from './rating.type'
|
||||||
import { NumberType } from './number'
|
import {TextType} from './text.type'
|
||||||
import { RadioType } from './radio'
|
import {TextareaType} from './textarea.type'
|
||||||
import { RatingType } from './rating'
|
import {FieldTypeProps} from './type.props'
|
||||||
import { SliderType } from './slider'
|
import {YesNoType} from './yes_no.type'
|
||||||
import { TextareaType } from './textarea'
|
|
||||||
import { TextfieldType } from './textfield'
|
|
||||||
import { YesNoType } from './yes_no'
|
|
||||||
|
|
||||||
export const fieldTypes: {
|
export const fieldTypes: {
|
||||||
[key: string]: AbstractType
|
[key: string]: React.FC<FieldTypeProps>
|
||||||
} = {
|
} = {
|
||||||
checkbox: new CheckboxType(),
|
'textfield': TextType,
|
||||||
date: new DateType(),
|
'date': DateType,
|
||||||
dropdown: new DropdownType(),
|
'email': EmailType,
|
||||||
email: new EmailType(),
|
'textarea': TextareaType,
|
||||||
hidden: new HiddenType(),
|
'link': LinkType,
|
||||||
link: new LinkType(),
|
'dropdown': DropdownType,
|
||||||
location: new LocationType(),
|
'rating': RatingType,
|
||||||
number: new NumberType(),
|
'radio': RadioType,
|
||||||
radio: new RadioType(),
|
'yes_no': YesNoType,
|
||||||
rating: new RatingType(),
|
'number': NumberType,
|
||||||
slider: new SliderType(),
|
|
||||||
textarea: new TextareaType(),
|
|
||||||
textfield: new TextfieldType(),
|
|
||||||
yes_no: new YesNoType(),
|
|
||||||
}
|
}
|
||||||
|
|||||||
25
components/form/types/link.type.tsx
Normal file
25
components/form/types/link.type.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {Form} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {StyledInput} from '../../styled/input'
|
||||||
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const LinkType: React.FC<FieldTypeProps> = ({field, design}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
name={[field.id, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
|
{ type: 'url', message: 'Must be a valid URL' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<StyledInput
|
||||||
|
design={design}
|
||||||
|
allowClear
|
||||||
|
size={'large'}
|
||||||
|
defaultValue={field.value}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class LinkType extends AbstractType<string> {
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./link.admin').then(c => c.LinkAdmin));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return dynamic(() => import('./link.input').then(c => c.builder(this)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { Form, Input } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
|
|
||||||
export const LinkAdmin: React.FC<FieldAdminProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:link.default')}
|
|
||||||
name={[props.field.name as string, 'defaultValue']}
|
|
||||||
rules={[{ type: 'url', message: t('validation:invalidUrl') }]}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input type={'url'} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { Form } from 'antd'
|
|
||||||
import debug from 'debug'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { StyledInput } from '../../../styled/input'
|
|
||||||
import { FieldInputBuilderType } from '../field.input.builder.type'
|
|
||||||
|
|
||||||
const logger = debug('link.input')
|
|
||||||
|
|
||||||
export const builder: FieldInputBuilderType = ({
|
|
||||||
parseUrlValue,
|
|
||||||
parseValue,
|
|
||||||
}) => function LinkInput ({
|
|
||||||
field,
|
|
||||||
design,
|
|
||||||
urlValue,
|
|
||||||
focus,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
let initialValue = null
|
|
||||||
|
|
||||||
if (field.defaultValue) {
|
|
||||||
try {
|
|
||||||
initialValue = parseValue(field.defaultValue)
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid default value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlValue) {
|
|
||||||
initialValue = parseUrlValue(urlValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
name={[field.id]}
|
|
||||||
rules={[
|
|
||||||
{ required: field.required, message: t('validation:valueRequired') },
|
|
||||||
{ type: 'url', message: t('validation:invalidUrl') },
|
|
||||||
]}
|
|
||||||
initialValue={initialValue}
|
|
||||||
>
|
|
||||||
<StyledInput autoFocus={focus} design={design} allowClear size={'large'} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class LocationType extends AbstractType<{ lat: number, lng: number }> {
|
|
||||||
parseUrlValue(raw: string): { lat: number; lng: number } {
|
|
||||||
if (raw.includes(',')) {
|
|
||||||
const [lat, lng] = raw.split(',')
|
|
||||||
|
|
||||||
return {
|
|
||||||
lat: parseFloat(lat),
|
|
||||||
lng: parseFloat(lng),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('no separator found')
|
|
||||||
}
|
|
||||||
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./location.admin').then(c => c.LocationAdmin), { ssr: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return dynamic(() => import('./location.input').then(c => c.builder(this)), { ssr: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
stringifyValue(raw: string): string {
|
|
||||||
const data = this.parseValue(raw)
|
|
||||||
|
|
||||||
return `${data.lat}, ${data.lng}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
import { Alert, Form, Input, InputNumber, Space } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { MapContainer, TileLayer } from 'react-leaflet'
|
|
||||||
import { DraggableMarker } from '../../../map/draggable.marker'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
|
|
||||||
export const LocationAdmin: React.FC<FieldAdminProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:location:default')}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
<Form.Item
|
|
||||||
name={[
|
|
||||||
props.field.name as string,
|
|
||||||
'defaultValue',
|
|
||||||
'lat',
|
|
||||||
]}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<InputNumber addonAfter={'LAT'} precision={7} step={0.00001} max={90} min={-90} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name={[
|
|
||||||
props.field.name as string,
|
|
||||||
'defaultValue',
|
|
||||||
'lng',
|
|
||||||
]}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<InputNumber addonAfter={'LNG'} precision={7} step={0.00001} max={180} min={-180} />
|
|
||||||
</Form.Item>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:location.initialZoom')}
|
|
||||||
name={[
|
|
||||||
props.field.name as string,
|
|
||||||
'optionKeys',
|
|
||||||
'initialZoom',
|
|
||||||
]}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
initialValue={1}
|
|
||||||
>
|
|
||||||
<InputNumber precision={0} min={1} max={18} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:location.tiles')}
|
|
||||||
name={[
|
|
||||||
props.field.name as string,
|
|
||||||
'optionKeys',
|
|
||||||
'tiles',
|
|
||||||
]}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
initialValue={'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'}
|
|
||||||
>
|
|
||||||
<Input placeholder={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item shouldUpdate>
|
|
||||||
{(form) => {
|
|
||||||
//const prefix = React.useContext(FormItemContext).prefixName
|
|
||||||
const prefix = (form as any).prefixName
|
|
||||||
|
|
||||||
const zoom = form.getFieldValue([
|
|
||||||
...prefix,
|
|
||||||
props.field.name as string,
|
|
||||||
'optionKeys',
|
|
||||||
'initialZoom',
|
|
||||||
])
|
|
||||||
|
|
||||||
const center = form.getFieldValue([
|
|
||||||
...prefix,
|
|
||||||
props.field.name as string,
|
|
||||||
'defaultValue',
|
|
||||||
])
|
|
||||||
|
|
||||||
const tiles = form.getFieldValue([
|
|
||||||
...prefix,
|
|
||||||
props.field.name as string,
|
|
||||||
'optionKeys',
|
|
||||||
'tiles',
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!tiles) {
|
|
||||||
return <Alert message={'Tiles missing!'} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<MapContainer
|
|
||||||
center={center}
|
|
||||||
zoom={zoom}
|
|
||||||
style={{ height: 300, width: '100%' }}
|
|
||||||
>
|
|
||||||
<TileLayer
|
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
url={tiles}
|
|
||||||
/>
|
|
||||||
{center?.lat && center?.lng && (
|
|
||||||
<DraggableMarker
|
|
||||||
value={center}
|
|
||||||
onChange={next => {
|
|
||||||
form.setFields([
|
|
||||||
{
|
|
||||||
name: [
|
|
||||||
...prefix,
|
|
||||||
props.field.name as string,
|
|
||||||
'defaultValue',
|
|
||||||
'lng',
|
|
||||||
],
|
|
||||||
value: next.lng,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: [
|
|
||||||
...prefix,
|
|
||||||
props.field.name as string,
|
|
||||||
'defaultValue',
|
|
||||||
'lat',
|
|
||||||
],
|
|
||||||
value: next.lat,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</MapContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
import { Alert, Form, InputNumber, Space, Spin } from 'antd'
|
|
||||||
import debug from 'debug'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { MapContainer, TileLayer } from 'react-leaflet'
|
|
||||||
import { DraggableMarker } from '../../../map/draggable.marker'
|
|
||||||
import { FieldInputBuilderType } from '../field.input.builder.type'
|
|
||||||
|
|
||||||
const logger = debug('location.number')
|
|
||||||
|
|
||||||
export const builder: FieldInputBuilderType = ({
|
|
||||||
parseUrlValue,
|
|
||||||
parseValue,
|
|
||||||
}) => function LocationInput ({
|
|
||||||
field,
|
|
||||||
urlValue,
|
|
||||||
}) {
|
|
||||||
const [initialZoom, setInitialZoom] = useState<number>(13)
|
|
||||||
const [tiles, setTiles] = useState<string>()
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
field.options.forEach((option) => {
|
|
||||||
if (option.key === 'initialZoom') {
|
|
||||||
try {
|
|
||||||
setInitialZoom(JSON.parse(option.value))
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid initialZoom value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (option.key === 'tiles') {
|
|
||||||
try {
|
|
||||||
setTiles(JSON.parse(option.value))
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid tiles value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
}, [field])
|
|
||||||
|
|
||||||
let initialValue: { lat: number, lng: number } = undefined
|
|
||||||
|
|
||||||
if (field.defaultValue) {
|
|
||||||
try {
|
|
||||||
initialValue = parseValue(field.defaultValue)
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid default value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlValue) {
|
|
||||||
try {
|
|
||||||
initialValue = parseUrlValue(urlValue)
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid url value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Spin />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tiles) {
|
|
||||||
return <Alert message={'Tiles missing!'} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item>
|
|
||||||
<Space>
|
|
||||||
<Form.Item
|
|
||||||
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
|
|
||||||
name={[
|
|
||||||
field.id,
|
|
||||||
'lat',
|
|
||||||
]}
|
|
||||||
initialValue={initialValue?.lat}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<InputNumber addonAfter={'LAT'} precision={7} step={0.00001} max={90} min={-90} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
|
|
||||||
name={[
|
|
||||||
field.id,
|
|
||||||
'lng',
|
|
||||||
]}
|
|
||||||
initialValue={initialValue?.lng}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<InputNumber addonAfter={'LNG'} precision={7} step={0.00001} max={180} min={-180} />
|
|
||||||
</Form.Item>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item dependencies={[[field.id, 'lat'], [field.id, 'lng']]}>
|
|
||||||
{(form) => {
|
|
||||||
const center = form.getFieldValue([field.id])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<MapContainer
|
|
||||||
center={initialValue}
|
|
||||||
zoom={initialZoom}
|
|
||||||
style={{ height: 300, width: '100%' }}
|
|
||||||
>
|
|
||||||
<TileLayer
|
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
||||||
url={tiles}
|
|
||||||
/>
|
|
||||||
{center.lat && center.lng && (
|
|
||||||
<DraggableMarker
|
|
||||||
value={center}
|
|
||||||
onChange={next => {
|
|
||||||
form.setFields([
|
|
||||||
{
|
|
||||||
name: [
|
|
||||||
field.id,
|
|
||||||
'lng',
|
|
||||||
],
|
|
||||||
value: next.lng,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: [
|
|
||||||
field.id,
|
|
||||||
'lat',
|
|
||||||
],
|
|
||||||
value: next.lat,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</MapContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
24
components/form/types/number.type.tsx
Normal file
24
components/form/types/number.type.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {Form} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {StyledNumberInput} from '../../styled/number.input'
|
||||||
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const NumberType: React.FC<FieldTypeProps> = ({field, design}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
name={[field.id, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ type: 'number', message: 'Must be a valid URL' },
|
||||||
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<StyledNumberInput
|
||||||
|
design={design}
|
||||||
|
size={'large'}
|
||||||
|
defaultValue={parseFloat(field.value)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class NumberType extends AbstractType<number> {
|
|
||||||
parseUrlValue(raw: string): number {
|
|
||||||
return parseFloat(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./number.admin').then(c => c.NumberAdmin));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return dynamic(() => import('./number.input').then(c => c.builder(this)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { Form, InputNumber } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
|
|
||||||
export const NumberAdmin: React.FC<FieldAdminProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:number:default')}
|
|
||||||
name={[props.field.name as string, 'defaultValue']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<InputNumber precision={2} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { Form } from 'antd'
|
|
||||||
import debug from 'debug'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { StyledNumberInput } from '../../../styled/number.input'
|
|
||||||
import { FieldInputBuilderType } from '../field.input.builder.type'
|
|
||||||
|
|
||||||
const logger = debug('number.input')
|
|
||||||
|
|
||||||
export const builder: FieldInputBuilderType = ({
|
|
||||||
parseUrlValue,
|
|
||||||
parseValue,
|
|
||||||
}) => function NumberInput ({
|
|
||||||
field,
|
|
||||||
design,
|
|
||||||
urlValue,
|
|
||||||
focus,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
let initialValue: number = undefined
|
|
||||||
|
|
||||||
if (field.defaultValue) {
|
|
||||||
try {
|
|
||||||
initialValue = parseValue(field.defaultValue)
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid default value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlValue) {
|
|
||||||
initialValue = parseUrlValue(urlValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
name={[field.id]}
|
|
||||||
rules={[
|
|
||||||
{ type: 'number', message: t('validation:invalidNumber') },
|
|
||||||
{ required: field.required, message: t('validation:valueRequired') },
|
|
||||||
]}
|
|
||||||
initialValue={initialValue}
|
|
||||||
>
|
|
||||||
<StyledNumberInput autoFocus={focus} design={design} size={'large'} />
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
20
components/form/types/radio.type.tsx
Normal file
20
components/form/types/radio.type.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Form, Input} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const RadioType: React.FC<FieldTypeProps> = ({field}) => {
|
||||||
|
// TODO Add radio support
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
name={[field.id, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic'
|
|
||||||
import { ComponentType } from 'react'
|
|
||||||
import { AbstractType } from '../abstract.type'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
import { FieldInputProps } from '../field.input.props'
|
|
||||||
|
|
||||||
export class RadioType extends AbstractType<string> {
|
|
||||||
adminFormField(): ComponentType<FieldAdminProps> {
|
|
||||||
return dynamic(() => import('./radio.admin').then(c => c.RadioAdmin));
|
|
||||||
}
|
|
||||||
|
|
||||||
inputFormField(): ComponentType<FieldInputProps> {
|
|
||||||
return dynamic(() => import('./radio.input').then(c => c.builder(this)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import { Button, Col, Form, Input, Row } from 'antd'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { FieldAdminProps } from '../field.admin.props'
|
|
||||||
|
|
||||||
export const RadioAdmin: React.FC<FieldAdminProps> = (props) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t('type:radio:default')}
|
|
||||||
name={[props.field.name as string, 'defaultValue']}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.List name={[props.field.name as string, 'options']}>
|
|
||||||
{(fields, { add, remove }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{
|
|
||||||
sm: { offset: index === 0 ? 0 : 6 },
|
|
||||||
}}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
label={index === 0 ? t('type:radio:options') : ''}
|
|
||||||
key={field.key}
|
|
||||||
>
|
|
||||||
<Row gutter={16}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{ span: 24 }}
|
|
||||||
name={[field.name, 'title']}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
>
|
|
||||||
<Input placeholder={t('type:radio:titlePlaceholder')} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{ span: 24 }}
|
|
||||||
name={[field.name, 'value']}
|
|
||||||
style={{ marginBottom: 0 }}
|
|
||||||
rules={[{ required: true, message: t('validation:valueRequired') }]}
|
|
||||||
>
|
|
||||||
<Input placeholder={t('type:radio:valuePlaceholder')} />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={4}>
|
|
||||||
<Button danger onClick={() => remove(index)}>
|
|
||||||
{t('type:radio:removeOption')}
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
wrapperCol={{
|
|
||||||
sm: { offset: 6 },
|
|
||||||
}}
|
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
|
||||||
<Button type={'dashed'} onClick={() => add()}>
|
|
||||||
{t('type:radio:addOption')}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { Form, Radio } from 'antd'
|
|
||||||
import debug from 'debug'
|
|
||||||
import React from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { StyledRadio } from '../../../styled/radio'
|
|
||||||
import { FieldInputBuilderType } from '../field.input.builder.type'
|
|
||||||
|
|
||||||
const logger = debug('radio.input')
|
|
||||||
|
|
||||||
export const builder: FieldInputBuilderType = ({
|
|
||||||
parseUrlValue,
|
|
||||||
parseValue,
|
|
||||||
}) => function RadioInput ({
|
|
||||||
field,
|
|
||||||
design,
|
|
||||||
urlValue,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
let initialValue: string = undefined
|
|
||||||
|
|
||||||
if (field.defaultValue) {
|
|
||||||
try {
|
|
||||||
initialValue = parseValue(field.defaultValue)
|
|
||||||
} catch (e) {
|
|
||||||
logger('invalid default value %O', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlValue) {
|
|
||||||
initialValue = parseUrlValue(urlValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
name={[field.id]}
|
|
||||||
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
|
|
||||||
initialValue={field.options
|
|
||||||
.map((option) => option.value)
|
|
||||||
.find((value) => value === initialValue)}
|
|
||||||
>
|
|
||||||
<Radio.Group>
|
|
||||||
{field.options
|
|
||||||
.filter((option) => option.key === null)
|
|
||||||
.map((option) => (
|
|
||||||
<StyledRadio design={design} value={option.value} key={option.value}>
|
|
||||||
{option.title || option.value}
|
|
||||||
</StyledRadio>
|
|
||||||
))}
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
20
components/form/types/rating.type.tsx
Normal file
20
components/form/types/rating.type.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Form, Rate} from 'antd'
|
||||||
|
import React from 'react'
|
||||||
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
|
export const RatingType: React.FC<FieldTypeProps> = ({field}) => {
|
||||||
|
// TODO add ratings
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item
|
||||||
|
name={[field.id, 'value']}
|
||||||
|
rules={[
|
||||||
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Rate allowHalf defaultValue={parseFloat(field.value)} />
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user