mirror of
https://github.com/IT4Change/ohmyform-ui.git
synced 2026-01-20 19:31:17 +00:00
Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1bef3bdb4 | ||
|
|
535c660f59 | ||
|
|
011a6baef4 | ||
|
|
19339436a3 | ||
|
|
84d96cd5ee | ||
|
|
ba302dcd63 | ||
|
|
fe90770999 | ||
|
|
dd45330a0f | ||
|
|
abf57a6976 | ||
|
|
30ff2c96bc | ||
|
|
23e67c8f7e | ||
|
|
674678dba3 | ||
|
|
1b87f9352c | ||
|
|
4c8ca21fd2 | ||
|
|
fe51c528d2 | ||
|
|
951dd2e5b4 | ||
|
|
019cd7f55e | ||
|
|
2e2d1b9a21 | ||
|
|
cf53f46b48 | ||
|
|
7b5e717a56 | ||
|
|
1e90c11f30 | ||
|
|
5909d1439c | ||
|
|
ca5edbbb3b | ||
|
|
e33b3ff392 | ||
|
|
66300ee391 | ||
|
|
1e2eb5792f | ||
|
|
bc56a70fea | ||
|
|
5e2596f131 | ||
|
|
9b275fddd9 | ||
|
|
2428935ef9 | ||
|
|
4c6d158560 | ||
|
|
1f0926c904 | ||
|
|
eaa48e1d0d | ||
|
|
2f6e63120c | ||
|
|
ed2eed16d5 | ||
|
|
f9ac50b737 | ||
|
|
107b2da182 | ||
|
|
0c1fa45288 | ||
|
|
5bcaedb556 | ||
|
|
831e0d7779 | ||
|
|
92bc295580 | ||
|
|
ebc7d755b5 | ||
|
|
18693be229 | ||
|
|
e54da2b111 | ||
|
|
26c2f9e095 | ||
|
|
1f37215af4 | ||
|
|
bef465f8d3 | ||
|
|
d5dff46816 | ||
|
|
de1180d547 | ||
|
|
94f1de3e25 | ||
|
|
c793321ecb | ||
|
|
117216c851 | ||
|
|
020ec938b7 | ||
|
|
b0f0ff2a50 | ||
|
|
31a84fb520 | ||
|
|
76ab0ceb67 | ||
|
|
8ee1e0ff8a | ||
|
|
c6cf6783b4 | ||
|
|
078510d41e | ||
|
|
b6296691c2 | ||
|
|
8713c0a8c6 | ||
|
|
dfbad77d7e | ||
|
|
aeffebb0f7 | ||
|
|
9b988d1326 | ||
|
|
3d3614b88a | ||
|
|
8645bdd505 | ||
|
|
14258f465f | ||
|
|
09aa1a4293 | ||
|
|
bf0febfaaa | ||
|
|
efb0b75bc5 | ||
|
|
34e232967a | ||
|
|
1c4a8d6d5c | ||
|
|
546df83ab0 | ||
|
|
0c4918774f | ||
|
|
9e9b9bb787 | ||
|
|
d7bc84e14f | ||
|
|
ac9f95907a | ||
|
|
79f08b96e6 | ||
|
|
f9e2cfea4f | ||
|
|
0b3f682172 | ||
|
|
63ff54e93d | ||
|
|
986e8e682b | ||
|
|
98b33bd0f3 | ||
|
|
0c20033c3e | ||
|
|
4eab2b42b8 | ||
|
|
1cd4464b5c | ||
|
|
94af92711b | ||
|
|
3514a80a6b | ||
|
|
7c00204576 | ||
|
|
49ec58434c | ||
|
|
baf7546840 | ||
|
|
78cadf0942 | ||
|
|
39c8296c61 | ||
|
|
a0105d9105 | ||
|
|
2cc49bdce3 | ||
|
|
5d8cbbdf4e | ||
|
|
561a8d6f10 | ||
|
|
6d733f20dd | ||
|
|
97a9e1dc60 | ||
|
|
3f5104bee5 | ||
|
|
84f23706b5 | ||
|
|
491cbff2de | ||
|
|
a6c98b6dfd | ||
|
|
3937e584ed | ||
|
|
a8f90d6bf1 | ||
|
|
d06a5df272 | ||
|
|
a4fd1321cd | ||
|
|
6c59b71f86 | ||
|
|
3148c2c8ee | ||
|
|
5a3e7e884f | ||
|
|
698ba04f7e | ||
|
|
7a46e66e05 | ||
|
|
38cadd23f8 | ||
|
|
a4666eb011 | ||
|
|
6f58cc5fd0 | ||
|
|
f617f94f0f | ||
|
|
0c0d06b6c4 | ||
|
|
c448cac85a | ||
|
|
e7b5fc50b4 | ||
|
|
44f1d14d8d | ||
|
|
dfc1fd6cc0 | ||
|
|
f42e8b9e7c | ||
|
|
0854e63d4f | ||
|
|
d6132170b3 | ||
|
|
e124aa1d67 | ||
|
|
7fab683f0d | ||
|
|
c023187efc | ||
|
|
b07443cca9 | ||
|
|
d40acbbc0b | ||
|
|
acb26096d6 | ||
|
|
a983cb218a | ||
|
|
e2afd0686e | ||
|
|
8d81390c83 | ||
|
|
c2608f047e | ||
|
|
ef250af6cb | ||
|
|
0836f69a7c | ||
|
|
f9fb0709db | ||
|
|
50e01a334a | ||
|
|
e5f3f1b38b | ||
|
|
5cf4f88273 | ||
|
|
86a500bad5 | ||
|
|
1eeb940844 | ||
|
|
90b141ff8d | ||
|
|
35f7a7d271 | ||
|
|
85a9a4a7a5 | ||
|
|
6ac044342e | ||
|
|
ca51b7ed2c | ||
|
|
1726d83f88 | ||
|
|
fd227b1884 | ||
|
|
0698c337f8 | ||
|
|
f346ed3900 | ||
|
|
11c7148df3 | ||
|
|
fce8bb5ace | ||
|
|
2f4f2ab4c2 | ||
|
|
eb21b9ef32 |
74
.eslintrc.js
Normal file
74
.eslintrc.js
Normal file
@ -0,0 +1,74 @@
|
||||
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
Normal file
43
.github/workflows/docker-image.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
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
Normal file
37
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
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,6 +26,8 @@ yarn-error.log*
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env
|
||||
|
||||
# development environments
|
||||
/.idea
|
||||
schema.graphql
|
||||
|
||||
8
.prettierrc.js
Normal file
8
.prettierrc.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
trailingComma: 'es5',
|
||||
singleQuote: true,
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
}
|
||||
249
CHANGELOG.md
Normal file
249
CHANGELOG.md
Normal file
@ -0,0 +1,249 @@
|
||||
# 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,12 +1,39 @@
|
||||
FROM node:12-alpine
|
||||
FROM node:14-alpine AS builder
|
||||
MAINTAINER OhMyForm <admin@ohmyform.com>
|
||||
|
||||
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 . ./
|
||||
|
||||
RUN yarn install --frozen-lock-file
|
||||
RUN yarn build
|
||||
|
||||
ENV PORT=4000
|
||||
# remove development dependencies
|
||||
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" ]
|
||||
|
||||
16
README.md
16
README.md
@ -1,7 +1,17 @@
|
||||
# OhMyForm UI
|
||||
|
||||
[](https://opencollective.com/ohmyform-sustainability)
|
||||

|
||||
[](https://travis-ci.org/ohmyform/ui)
|
||||

|
||||
[](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)
|
||||
> An *open source alternative to TypeForm* that can create stunning mobile-ready forms, surveys and questionnaires.
|
||||
[](https://opencollective.com/ohmyform-sustainability)
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
@import "variables";
|
||||
@import "node_modules/swiper/swiper.scss";
|
||||
@import "../node_modules/react-github-button/assets/style.css";
|
||||
@import "../node_modules/leaflet/dist/leaflet.css";
|
||||
|
||||
:root {
|
||||
--backgroundColor: #{$background-color};
|
||||
@ -24,14 +26,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.full-height {
|
||||
height: 100vh;
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
}
|
||||
|
||||
.ant-spin-nested-loading > div > .ant-spin {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
.swiper-container {
|
||||
height: 100vh;
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
|
||||
.swiper-wrapper {
|
||||
position: fixed
|
||||
}
|
||||
}
|
||||
|
||||
.admin {
|
||||
.sidemenu {
|
||||
.ant-layout-sider-children {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.language-selector {
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
assets/images/marker-icon-2x.png
Normal file
BIN
assets/images/marker-icon-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
assets/images/marker-shadow.png
Normal file
BIN
assets/images/marker-shadow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
18
components/auth/footer.module.scss
Normal file
18
components/auth/footer.module.scss
Normal file
@ -0,0 +1,18 @@
|
||||
.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,20 +1,26 @@
|
||||
import {Button} from 'antd'
|
||||
import { Button, Select } from 'antd'
|
||||
import Link from 'next/link'
|
||||
import {useRouter} from 'next/router'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import {useTranslation} from 'react-i18next'
|
||||
import {clearAuth, withAuth} from '../with.auth'
|
||||
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 } = useTranslation()
|
||||
const AuthFooterInner: React.FC<Props> = (props) => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { data, loading } = useSettingsQuery()
|
||||
|
||||
const logout = () => {
|
||||
clearAuth()
|
||||
@ -22,71 +28,115 @@ const AuthFooterInner: React.FC<Props> = props => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<Link href={'/admin'}>
|
||||
<Button
|
||||
type={'link'}
|
||||
ghost
|
||||
>
|
||||
{t('admin')}
|
||||
</Button>
|
||||
</Link>
|
||||
{props.me ? (
|
||||
[
|
||||
<span style={{color: '#FFF'}} key={'user'}>
|
||||
Hi, {props.me.username}
|
||||
<footer className={scss.footer}>
|
||||
{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={{
|
||||
color: '#FFF',
|
||||
}}
|
||||
>
|
||||
{t('admin')}
|
||||
</Button>
|
||||
</Link>
|
||||
),
|
||||
<Link key={'profile'} href={'/admin/profile'}>
|
||||
<Button
|
||||
type={'link'}
|
||||
style={{
|
||||
color: '#FFF',
|
||||
}}
|
||||
>
|
||||
{t('profile')}
|
||||
</Button>
|
||||
</Link>,
|
||||
<Button
|
||||
key={'Logout'}
|
||||
key={'logout'}
|
||||
type={'link'}
|
||||
ghost
|
||||
onClick={logout}
|
||||
style={{
|
||||
color: '#FFF',
|
||||
}}
|
||||
>
|
||||
{t('logout')}
|
||||
</Button>
|
||||
</Button>,
|
||||
]
|
||||
): (
|
||||
[
|
||||
: [
|
||||
<Link href={'/login'} key={'login'}>
|
||||
<Button
|
||||
type={'link'}
|
||||
ghost
|
||||
style={{
|
||||
color: '#FFF',
|
||||
}}
|
||||
>
|
||||
{t('login')}
|
||||
</Button>
|
||||
</Link>,
|
||||
<Link href={'/register'} key={'register'}>
|
||||
<Button
|
||||
type={'link'}
|
||||
ghost
|
||||
>
|
||||
{t('register')}
|
||||
</Button>
|
||||
</Link>
|
||||
]
|
||||
)}
|
||||
|
||||
<Button
|
||||
type={'link'}
|
||||
target={'_blank'}
|
||||
ghost
|
||||
href={'https://www.ohmyform.com'}
|
||||
!loading && !data?.disabledSignUp.value && (
|
||||
<Link href={'/register'} key={'register'}>
|
||||
<Button
|
||||
type={'link'}
|
||||
style={{
|
||||
color: '#FFF',
|
||||
}}
|
||||
>
|
||||
{t('register')}
|
||||
</Button>
|
||||
</Link>
|
||||
),
|
||||
]}
|
||||
<div style={{ flex: 1 }} />
|
||||
<Select
|
||||
bordered={false}
|
||||
value={i18n.language.replace(/-.*/, '')}
|
||||
onChange={(next) => i18n.changeLanguage(next)}
|
||||
style={{
|
||||
float: 'right',
|
||||
color: '#FFF'
|
||||
color: '#FFF',
|
||||
paddingLeft: 18,
|
||||
}}
|
||||
suffixIcon={false}
|
||||
>
|
||||
© OhMyForm
|
||||
</Button>
|
||||
</div>
|
||||
{languages.map((language) => (
|
||||
<Select.Option value={language} key={language}>
|
||||
{t(`language:${language}`)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{!loading && !data?.hideContrib.value && (
|
||||
<>
|
||||
<GitHubButton type="stargazers" namespace="ohmyform" repo="ohmyform" />
|
||||
<Button
|
||||
type={'link'}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
href={'https://www.ohmyform.com'}
|
||||
style={{
|
||||
color: '#FFF',
|
||||
}}
|
||||
>
|
||||
OhMyForm
|
||||
</Button>
|
||||
<Button
|
||||
type={'link'}
|
||||
target={'_blank'}
|
||||
rel={'noreferrer'}
|
||||
href={'https://lokalise.com/'}
|
||||
style={{
|
||||
color: '#FFF',
|
||||
}}
|
||||
>
|
||||
translated with Lokalize
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export const AuthFooter = withAuth(AuthFooterInner)
|
||||
export const AuthFooter = withAuth(AuthFooterInner, [], true)
|
||||
|
||||
@ -1,17 +1,23 @@
|
||||
import {Layout, Spin} from 'antd'
|
||||
import { Layout, Spin } from 'antd'
|
||||
import getConfig from 'next/config'
|
||||
import React from 'react'
|
||||
import { NextConfigType } from '../../next.config.type'
|
||||
|
||||
const { publicRuntimeConfig } = getConfig() as NextConfigType
|
||||
|
||||
interface Props {
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export const AuthLayout: React.FC<Props> = props => {
|
||||
export const AuthLayout: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<Spin spinning={props.loading}>
|
||||
<Layout style={{
|
||||
height: '100vh',
|
||||
background: '#437fdc'
|
||||
}}>
|
||||
<Spin spinning={props.loading || false}>
|
||||
<Layout
|
||||
style={{
|
||||
height: '100vh',
|
||||
background: publicRuntimeConfig.mainBackground,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Layout>
|
||||
</Spin>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
|
||||
/* eslint-disable */
|
||||
const omitDeepArrayWalk = (arr, key) => {
|
||||
return arr.map((val) => {
|
||||
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 keys: Array<any> = Object.keys(obj);
|
||||
const newObj: any = {};
|
||||
const keys: Array<any> = Object.keys(obj)
|
||||
const newObj: any = {}
|
||||
keys.forEach((i: any) => {
|
||||
if (i !== key) {
|
||||
const val: any = obj[i];
|
||||
if (val instanceof Date) newObj[i] = val;
|
||||
else if (Array.isArray(val)) newObj[i] = omitDeepArrayWalk(val, key);
|
||||
else if (typeof val === 'object' && val !== null) newObj[i] = omitDeep(val, key);
|
||||
else newObj[i] = val;
|
||||
const val: any = obj[i]
|
||||
if (val instanceof Date) newObj[i] = val
|
||||
else if (Array.isArray(val)) newObj[i] = omitDeepArrayWalk(val, key)
|
||||
else if (typeof val === 'object' && val !== null) newObj[i] = omitDeep(val, key)
|
||||
else newObj[i] = val
|
||||
}
|
||||
});
|
||||
return newObj;
|
||||
})
|
||||
return newObj
|
||||
}
|
||||
|
||||
export const cleanInput = <T>(obj: T): T => {
|
||||
|
||||
@ -7,13 +7,15 @@ interface Props {
|
||||
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'
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'inline-block'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
{dayjs(props.date).format(format)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -2,13 +2,15 @@ import React from 'react'
|
||||
|
||||
export const ErrorPage: React.FC = () => {
|
||||
return (
|
||||
<div style={{
|
||||
height: '100vh',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<h1>ERROR</h1>
|
||||
<p>there was an error with your request</p>
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
import {Form, Input, Select, Switch, Tabs} from 'antd'
|
||||
import {TabPaneProps} from 'antd/lib/tabs'
|
||||
import { Form, Input, Select, Switch, Tabs } from 'antd'
|
||||
import { TabPaneProps } from 'antd/lib/tabs'
|
||||
import React from 'react'
|
||||
import {languages} from '../../../i18n'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { languages } from '../../../i18n'
|
||||
|
||||
export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
export const BaseDataTab: React.FC<TabPaneProps> = props => {
|
||||
return (
|
||||
<Tabs.TabPane {...props}>
|
||||
<Form.Item
|
||||
label="Is Live"
|
||||
label={t('form:baseData.isLive')}
|
||||
name={['form', 'isLive']}
|
||||
valuePropName={'checked'}
|
||||
>
|
||||
@ -15,12 +18,12 @@ export const BaseDataTab: React.FC<TabPaneProps> = props => {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Title"
|
||||
label={t('form:baseData.title')}
|
||||
name={['form', 'title']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please provide a Title',
|
||||
message: t('validation:titleRequired'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
@ -28,28 +31,39 @@ export const BaseDataTab: React.FC<TabPaneProps> = props => {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Language"
|
||||
label={t('form:baseData.language')}
|
||||
name={['form', 'language']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please select a Language',
|
||||
message: t('validation:languageRequired'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{languages.map(language => <Select.Option value={language} key={language}>{language.toUpperCase()}</Select.Option> )}
|
||||
{languages.map((language) => (
|
||||
<Select.Option value={language} key={language}>
|
||||
{t(`language:${language}`)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Show Footer"
|
||||
label={t('form:baseData.showFooter')}
|
||||
name={['form', 'showFooter']}
|
||||
valuePropName={'checked'}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('form:baseData.anonymousSubmission')}
|
||||
name={['form', 'anonymousSubmission']}
|
||||
valuePropName={'checked'}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Tabs.TabPane>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,27 +1,46 @@
|
||||
import {Form, Input, Tabs} from 'antd'
|
||||
import {TabPaneProps} from 'antd/lib/tabs'
|
||||
import { Form, Input, Select, Tabs } from 'antd'
|
||||
import { TabPaneProps } from 'antd/lib/tabs'
|
||||
import React from 'react'
|
||||
import {InputColor} from '../../input/color'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InputColor } from '../../input/color'
|
||||
|
||||
export const DesignTab: React.FC<TabPaneProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
export const DesignTab: React.FC<TabPaneProps> = props => {
|
||||
return (
|
||||
<Tabs.TabPane {...props}>
|
||||
<Form.Item
|
||||
label="Font"
|
||||
name={['form', 'design', 'font']}
|
||||
>
|
||||
<Form.Item label={t('form:design.font')} name={[
|
||||
'form', 'design', 'font',
|
||||
]}>
|
||||
<Input />
|
||||
</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>
|
||||
|
||||
{[
|
||||
{name: 'backgroundColor', label: 'Background Color'},
|
||||
{name: 'questionColor', label: 'Question Color'},
|
||||
{name: 'answerColor', label: 'Answer Color'},
|
||||
{name: 'buttonColor', label: 'Button Color'},
|
||||
{name: 'buttonActiveColor', label: 'Button Active Color'},
|
||||
{name: 'buttonTextColor', label: 'Button Text Color'},
|
||||
].map(({label, name}) => (
|
||||
<Form.Item key={name} label={label} name={['form', 'design', 'colors', name]}>
|
||||
'background', 'question', 'answer', 'button', 'buttonActive', 'buttonText',
|
||||
].map((name) => (
|
||||
<Form.Item
|
||||
key={name}
|
||||
label={t(`form:design.color.${name}`)}
|
||||
name={[
|
||||
'form', 'design', 'colors', name,
|
||||
]}
|
||||
>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
))}
|
||||
|
||||
@ -1,89 +1,111 @@
|
||||
import {DeleteOutlined, PlusOutlined} from '@ant-design/icons/lib'
|
||||
import {Button, Card, Form, Input, Switch, Tabs} from 'antd'
|
||||
import {TabPaneProps} from 'antd/lib/tabs'
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
|
||||
import { Button, Card, Form, Input, Switch, Tabs } from 'antd'
|
||||
import { TabPaneProps } from 'antd/lib/tabs'
|
||||
import React from 'react'
|
||||
import {InputColor} from '../../input/color'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InputColor } from '../../input/color'
|
||||
|
||||
export const EndPageTab: React.FC<TabPaneProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
export const EndPageTab: React.FC<TabPaneProps> = props => {
|
||||
return (
|
||||
<Tabs.TabPane {...props}>
|
||||
<Form.Item
|
||||
label={'Show'}
|
||||
name={['form', 'endPage', 'show']}
|
||||
label={t('form:endPage.show')}
|
||||
name={[
|
||||
'form', 'endPage', 'show',
|
||||
]}
|
||||
valuePropName={'checked'}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={'Title'}
|
||||
name={['form', 'endPage', 'title']}
|
||||
>
|
||||
<Form.Item label={t('form:endPage.title')} name={[
|
||||
'form', 'endPage', 'title',
|
||||
]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={'Paragraph'}
|
||||
name={['form', 'endPage', 'paragraph']}
|
||||
label={t('form:endPage.paragraph')}
|
||||
name={[
|
||||
'form', 'endPage', 'paragraph',
|
||||
]}
|
||||
extra={t('type:descriptionInfo')}
|
||||
>
|
||||
<Input.TextArea autoSize />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={'Continue Button Text'}
|
||||
name={['form', 'endPage', 'buttonText']}
|
||||
label={t('form:endPage.continueButtonText')}
|
||||
name={[
|
||||
'form', 'endPage', 'buttonText',
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.List
|
||||
name={['form', 'endPage', 'buttons']}
|
||||
>
|
||||
<Form.List name={[
|
||||
'form', 'endPage', 'buttons',
|
||||
]}>
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
wrapperCol={{
|
||||
sm: { offset: index === 0 ? 0 : 6 },
|
||||
}}
|
||||
label={index === 0 ? 'Buttons' : ''}
|
||||
key={field.key}
|
||||
>
|
||||
<Card
|
||||
actions={[
|
||||
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
|
||||
]}
|
||||
<Form.Item
|
||||
wrapperCol={{
|
||||
sm: { offset: index === 0 ? 0 : 6 },
|
||||
}}
|
||||
label={index === 0 ? t('form:endPage.buttons') : ''}
|
||||
key={field.key}
|
||||
>
|
||||
<Card actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}>
|
||||
<Form.Item
|
||||
label={t('form:endPage.url')}
|
||||
name={[field.key, 'url']}
|
||||
rules={[{ type: 'url', message: t('validation:invalidUrl') }]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Form.Item
|
||||
label={'Url'}
|
||||
name={[field.key, 'url']}
|
||||
rules={[
|
||||
{type: 'url', message: 'Must be a valid url'}
|
||||
]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Action'} name={[field.key, 'action']} labelCol={{ span: 6 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Text'} name={[field.key, 'text']} labelCol={{ span: 6 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Background Color'} name={[field.key, 'bgColor']} labelCol={{ span: 6 }}>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Active Color'} name={[field.key, 'activeColor']} labelCol={{ span: 6 }}>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Color'} name={[field.key, 'color']} labelCol={{ span: 6 }}>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Form.Item>
|
||||
)
|
||||
)}
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:endPage.action')}
|
||||
name={[field.key, 'action']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:endPage.text')}
|
||||
name={[field.key, 'text']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:endPage.bgColor')}
|
||||
name={[field.key, 'bgColor']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:endPage.activeColor')}
|
||||
name={[field.key, 'activeColor']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:endPage.color')}
|
||||
name={[field.key, 'color']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item
|
||||
wrapperCol={{
|
||||
sm: { offset: 6 },
|
||||
@ -92,11 +114,11 @@ export const EndPageTab: React.FC<TabPaneProps> = props => {
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
add()
|
||||
}}
|
||||
style={{ width: '60%' }}
|
||||
>
|
||||
<PlusOutlined /> Add Button
|
||||
<PlusOutlined /> {t('form:endPage.addButton')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
125
components/form/admin/export.submission.action.tsx
Normal file
125
components/form/admin/export.submission.action.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
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,110 +1,244 @@
|
||||
import {DeleteOutlined} from '@ant-design/icons/lib'
|
||||
import {Button, Card, Checkbox, Form, Input, Popconfirm, Tag} from 'antd'
|
||||
import {FormInstance} from 'antd/lib/form'
|
||||
import {FieldData} from 'rc-field-form/lib/interface'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||
import {adminTypes} from './types'
|
||||
import {TextType} from './types/text.type'
|
||||
import { VerticalAlignBottomOutlined, VerticalAlignTopOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
|
||||
import { Button, Card, Checkbox, Form, Input, Popconfirm, Popover, Space, Tag, Tooltip } from 'antd'
|
||||
import { FormInstance } from 'antd/lib/form'
|
||||
import { FieldData } from 'rc-field-form/lib/interface'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormFieldFragment, FormFieldLogicFragment } from '../../../graphql/fragment/form.fragment'
|
||||
import { fieldTypes } from '../types'
|
||||
import { LogicBlock } from './logic.block'
|
||||
|
||||
interface Props {
|
||||
form: FormInstance
|
||||
fields: AdminFormFieldFragment[]
|
||||
onChangeFields: (fields: AdminFormFieldFragment[]) => any
|
||||
fields: FormFieldFragment[]
|
||||
onChangeFields: (fields: FormFieldFragment[]) => void
|
||||
field: FieldData
|
||||
remove: (index: number) => void
|
||||
move: (from: number, to: number) => void
|
||||
index: number
|
||||
}
|
||||
|
||||
export const FieldCard: React.FC<Props> = props => {
|
||||
const {
|
||||
form,
|
||||
field,
|
||||
fields,
|
||||
onChangeFields,
|
||||
remove,
|
||||
index,
|
||||
} = props
|
||||
export const FieldCard: React.FC<Props> = ({
|
||||
form,
|
||||
field,
|
||||
fields,
|
||||
onChangeFields,
|
||||
remove,
|
||||
move,
|
||||
index,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const type = form.getFieldValue(['form', 'fields', field.name as string, 'type'])
|
||||
const TypeComponent = adminTypes[type] || TextType
|
||||
const type = form.getFieldValue([
|
||||
'form', 'fields', field.name as string, 'type',
|
||||
]) as string
|
||||
const TypeComponent = (fieldTypes[type] || fieldTypes['textfield']).adminFormField()
|
||||
|
||||
const [nextTitle, setNextTitle] = useState(form.getFieldValue(['form', 'fields', field.name as string, 'title']))
|
||||
const [shouldUpdate, setShouldUpdate] = useState(false)
|
||||
const [nextTitle, setNextTitle] = useState<string>(
|
||||
form.getFieldValue([
|
||||
'form', 'fields', field.name as string, 'title',
|
||||
])
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = setTimeout(() => {
|
||||
onChangeFields(fields.map((field, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...field,
|
||||
title: nextTitle,
|
||||
setShouldUpdate(false)
|
||||
onChangeFields(
|
||||
fields.map((field, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...field,
|
||||
title: nextTitle,
|
||||
}
|
||||
} else {
|
||||
return field
|
||||
}
|
||||
} else {
|
||||
return field
|
||||
}
|
||||
}))
|
||||
})
|
||||
)
|
||||
}, 500)
|
||||
|
||||
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 (
|
||||
<Card
|
||||
title={nextTitle}
|
||||
type={'inner'}
|
||||
extra={(
|
||||
<div>
|
||||
<Tag color={'blue'}>{type}</Tag>
|
||||
extra={
|
||||
<Space>
|
||||
<Tooltip title={t('form:field.move.up')}>
|
||||
<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
|
||||
placement={'left'}
|
||||
title={'Really remove this field? Check that it is not referenced anywhere!'}
|
||||
okText={'Delete Field'}
|
||||
title={t('type:confirmDelete')}
|
||||
okText={t('type:deleteNow')}
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => {
|
||||
remove(index)
|
||||
onChangeFields(fields.filter((e, i) => i !== index))
|
||||
}}
|
||||
>
|
||||
<Button danger><DeleteOutlined /></Button>
|
||||
<Button danger>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
)}
|
||||
actions={[
|
||||
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
|
||||
]}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form.Item name={[field.name as string, 'type']} noStyle><Input type={'hidden'} /></Form.Item>
|
||||
<Form.Item
|
||||
label={'Title'}
|
||||
name={[field.name as string, 'title']}
|
||||
rules={[
|
||||
{ required: true, message: 'Title is required' }
|
||||
]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input onChange={e => setNextTitle(e.target.value)}/>
|
||||
<Form.Item name={[field.name as string, 'type']} noStyle>
|
||||
<Input type={'hidden'} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={'Description'}
|
||||
label={t('type:title')}
|
||||
name={[field.name as string, 'title']}
|
||||
rules={[{ required: true, message: 'Title is required' }]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input
|
||||
onChange={(e) => {
|
||||
setNextTitle(e.target.value)
|
||||
setShouldUpdate(true)
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('type:description')}
|
||||
name={[field.name as string, 'description']}
|
||||
labelCol={{ span: 6 }}
|
||||
extra={t('type:descriptionInfo')}
|
||||
>
|
||||
<Input.TextArea autoSize />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={'Required'}
|
||||
label={t('type:required')}
|
||||
name={[field.name as string, 'required']}
|
||||
labelCol={{ span: 6 }}
|
||||
valuePropName={'checked'}
|
||||
extra={type === 'hidden' && 'If required, default value must be set to enable users to submit form!'}
|
||||
extra={type === 'hidden' && t('type:requiredInfo')}
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
|
||||
<TypeComponent
|
||||
field={field}
|
||||
form={form}
|
||||
/>
|
||||
<TypeComponent field={field} form={form} />
|
||||
|
||||
<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}
|
||||
form={form}
|
||||
fields={fields}
|
||||
index={index}
|
||||
remove={remove}
|
||||
/>
|
||||
</Form.Item>
|
||||
{addLogic(addAndMove(index + 1), index + 1)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form.List>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,86 +1,105 @@
|
||||
import {PlusOutlined} from '@ant-design/icons/lib'
|
||||
import {Button, Form, Select, Space, Tabs} from 'antd'
|
||||
import {FormInstance} from 'antd/lib/form'
|
||||
import {TabPaneProps} from 'antd/lib/tabs'
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
|
||||
import {FieldCard} from './field.card'
|
||||
import {adminTypes} from './types'
|
||||
import { PlusOutlined } from '@ant-design/icons/lib'
|
||||
import { Button, Form, Select, Space, Tabs } from 'antd'
|
||||
import { FormInstance } from 'antd/lib/form'
|
||||
import { TabPaneProps } from 'antd/lib/tabs'
|
||||
import debug from 'debug'
|
||||
import { FieldData } from 'rc-field-form/lib/interface'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
|
||||
import { fieldTypes } from '../types'
|
||||
import { FieldCard } from './field.card'
|
||||
|
||||
const logger = debug('FieldsTab')
|
||||
|
||||
interface Props extends TabPaneProps {
|
||||
form: FormInstance
|
||||
fields: AdminFormFieldFragment[]
|
||||
onChangeFields: (fields: AdminFormFieldFragment[]) => any
|
||||
fields: FormFieldFragment[]
|
||||
onChangeFields: (fields: FormFieldFragment[]) => void
|
||||
}
|
||||
|
||||
export const FieldsTab: React.FC<Props> = props => {
|
||||
export const FieldsTab: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const [nextType, setNextType] = useState('textfield')
|
||||
|
||||
const renderType = useCallback((field, index, remove) => {
|
||||
return (
|
||||
<FieldCard
|
||||
form={props.form}
|
||||
field={field}
|
||||
index={index}
|
||||
remove={remove}
|
||||
fields={props.fields}
|
||||
onChangeFields={props.onChangeFields}
|
||||
/>
|
||||
)
|
||||
}, [props.fields])
|
||||
|
||||
const addField = useCallback((add, index) => {
|
||||
return (
|
||||
<Form.Item
|
||||
wrapperCol={{span: 24}}
|
||||
>
|
||||
<Space
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
const renderType = useCallback(
|
||||
(
|
||||
field: FieldData,
|
||||
index: number,
|
||||
remove: (index: number) => void,
|
||||
move: (from: number, to: number) => void
|
||||
) => {
|
||||
return (
|
||||
<FieldCard
|
||||
form={props.form}
|
||||
field={field}
|
||||
index={index}
|
||||
remove={(index: number) => {
|
||||
logger('remove %d', index)
|
||||
remove(index)
|
||||
}}
|
||||
>
|
||||
<Select value={nextType} onChange={e => setNextType(e)} style={{ minWidth: 200 }}>
|
||||
{Object.keys(adminTypes).map(type => <Select.Option value={type} key={type}>{type}</Select.Option> )}
|
||||
</Select>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
const defaults: AdminFormFieldFragment = {
|
||||
logicJump: {
|
||||
enabled: false,
|
||||
},
|
||||
options: [],
|
||||
id: `NEW-${Date.now()}`,
|
||||
type: nextType,
|
||||
title: '',
|
||||
description: '',
|
||||
required: false,
|
||||
value: ''
|
||||
}
|
||||
move={(from: number, to: number) => {
|
||||
logger('move %d TO %d', from, to)
|
||||
move(from, to)
|
||||
}}
|
||||
fields={props.fields}
|
||||
onChangeFields={props.onChangeFields}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[props.fields]
|
||||
)
|
||||
|
||||
add(defaults)
|
||||
const next = [...props.fields]
|
||||
next.splice(index, 0, defaults)
|
||||
props.onChangeFields(next)
|
||||
const addField = useCallback(
|
||||
(add: (defaults: unknown) => void, index: number) => {
|
||||
return (
|
||||
<Form.Item wrapperCol={{ span: 24 }}>
|
||||
<Space
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<PlusOutlined /> Add Field
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
)
|
||||
}, [props.fields, nextType])
|
||||
<Select value={nextType} onChange={(e) => setNextType(e)} style={{ minWidth: 200 }}>
|
||||
{Object.keys(fieldTypes).map((type) => (
|
||||
<Select.Option value={type} key={type}>
|
||||
{t(`type:${type}.name`)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
const defaults: FormFieldFragment = {
|
||||
logic: [],
|
||||
options: [],
|
||||
id: `NEW-${Date.now()}`,
|
||||
type: nextType,
|
||||
title: '',
|
||||
description: '',
|
||||
required: false,
|
||||
}
|
||||
|
||||
add(defaults)
|
||||
const next = [...props.fields]
|
||||
next.splice(index, 0, defaults)
|
||||
props.onChangeFields(next)
|
||||
}}
|
||||
>
|
||||
<PlusOutlined /> {t('type:add')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
)
|
||||
},
|
||||
[props.fields, nextType]
|
||||
)
|
||||
|
||||
return (
|
||||
<Tabs.TabPane {...props}>
|
||||
|
||||
<Form.List
|
||||
name={['form', 'fields']}
|
||||
>
|
||||
<Form.List name={['form', 'fields']}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
const addAndMove = (index) => (defaults) => {
|
||||
const addAndMove = (index: number) => (defaults) => {
|
||||
add(defaults)
|
||||
move(fields.length, index)
|
||||
}
|
||||
@ -91,7 +110,7 @@ export const FieldsTab: React.FC<Props> = props => {
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.key}>
|
||||
<Form.Item wrapperCol={{ span: 24 }}>
|
||||
{renderType(field, index, remove)}
|
||||
{renderType(field, index, remove, move)}
|
||||
</Form.Item>
|
||||
{addField(addAndMove(index + 1), index + 1)}
|
||||
</div>
|
||||
@ -100,7 +119,6 @@ export const FieldsTab: React.FC<Props> = props => {
|
||||
)
|
||||
}}
|
||||
</Form.List>
|
||||
|
||||
</Tabs.TabPane>
|
||||
)
|
||||
}
|
||||
|
||||
98
components/form/admin/hooks.tab.tsx
Normal file
98
components/form/admin/hooks.tab.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
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,22 +1,26 @@
|
||||
import {CheckCircleOutlined, CloseCircleOutlined} from '@ant-design/icons/lib'
|
||||
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons/lib'
|
||||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
isLive: boolean
|
||||
}
|
||||
|
||||
export const FormIsLive: React.FC<Props> = props => {
|
||||
export const FormIsLive: React.FC<Props> = (props) => {
|
||||
if (props.isLive) {
|
||||
return (
|
||||
<CheckCircleOutlined style={{
|
||||
color: 'green'
|
||||
}} />
|
||||
<CheckCircleOutlined
|
||||
style={{
|
||||
color: 'green',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CloseCircleOutlined style={{
|
||||
color: 'red'
|
||||
}} />
|
||||
<CloseCircleOutlined
|
||||
style={{
|
||||
color: 'red',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
233
components/form/admin/logic.block.tsx
Normal file
233
components/form/admin/logic.block.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
247
components/form/admin/notification.card.tsx
Normal file
247
components/form/admin/notification.card.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
93
components/form/admin/notifications.tab.tsx
Normal file
93
components/form/admin/notifications.tab.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
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,89 +1,111 @@
|
||||
import {DeleteOutlined, PlusOutlined} from '@ant-design/icons/lib'
|
||||
import {Button, Card, Form, Input, Switch, Tabs} from 'antd'
|
||||
import {TabPaneProps} from 'antd/lib/tabs'
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
|
||||
import { Button, Card, Form, Input, Switch, Tabs } from 'antd'
|
||||
import { TabPaneProps } from 'antd/lib/tabs'
|
||||
import React from 'react'
|
||||
import {InputColor} from '../../input/color'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InputColor } from '../../input/color'
|
||||
|
||||
export const StartPageTab: React.FC<TabPaneProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
export const StartPageTab: React.FC<TabPaneProps> = props => {
|
||||
return (
|
||||
<Tabs.TabPane {...props}>
|
||||
<Form.Item
|
||||
label={'Show'}
|
||||
name={['form', 'startPage', 'show']}
|
||||
label={t('form:startPage.show')}
|
||||
name={[
|
||||
'form', 'startPage', 'show',
|
||||
]}
|
||||
valuePropName={'checked'}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={'Title'}
|
||||
name={['form', 'startPage', 'title']}
|
||||
>
|
||||
<Form.Item label={t('form:startPage.title')} name={[
|
||||
'form', 'startPage', 'title',
|
||||
]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={'Paragraph'}
|
||||
name={['form', 'startPage', 'paragraph']}
|
||||
label={t('form:startPage.paragraph')}
|
||||
name={[
|
||||
'form', 'startPage', 'paragraph',
|
||||
]}
|
||||
extra={t('form:startPage.paragraphInfo')}
|
||||
>
|
||||
<Input.TextArea autoSize />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={'Continue Button Text'}
|
||||
name={['form', 'startPage', 'buttonText']}
|
||||
label={t('form:startPage.continueButtonText')}
|
||||
name={[
|
||||
'form', 'startPage', 'buttonText',
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.List
|
||||
name={['form', 'startPage', 'buttons']}
|
||||
>
|
||||
<Form.List name={[
|
||||
'form', 'startPage', 'buttons',
|
||||
]}>
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item
|
||||
wrapperCol={{
|
||||
sm: { offset: index === 0 ? 0 : 6 },
|
||||
}}
|
||||
label={index === 0 ? 'Buttons' : ''}
|
||||
key={field.key}
|
||||
>
|
||||
<Card
|
||||
actions={[
|
||||
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
|
||||
]}
|
||||
<Form.Item
|
||||
wrapperCol={{
|
||||
sm: { offset: index === 0 ? 0 : 6 },
|
||||
}}
|
||||
label={index === 0 ? t('form:startPage.buttons') : ''}
|
||||
key={field.key}
|
||||
>
|
||||
<Card actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}>
|
||||
<Form.Item
|
||||
label={t('form:startPage.url')}
|
||||
name={[field.key, 'url']}
|
||||
rules={[{ type: 'url', message: t('validation:invalidUrl') }]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Form.Item
|
||||
label={'Url'}
|
||||
name={[field.key, 'url']}
|
||||
rules={[
|
||||
{type: 'url', message: 'Must be a valid url'}
|
||||
]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Action'} name={[field.key, 'action']} labelCol={{ span: 6 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Text'} name={[field.key, 'text']} labelCol={{ span: 6 }}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Background Color'} name={[field.key, 'bgColor']} labelCol={{ span: 6 }}>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Active Color'} name={[field.key, 'activeColor']} labelCol={{ span: 6 }}>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
<Form.Item label={'Color'} name={[field.key, 'color']} labelCol={{ span: 6 }}>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Form.Item>
|
||||
)
|
||||
)}
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:startPage.action')}
|
||||
name={[field.key, 'action']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:startPage.text')}
|
||||
name={[field.key, 'text']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:startPage.bgColor')}
|
||||
name={[field.key, 'bgColor']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:startPage.activeColor')}
|
||||
name={[field.key, 'activeColor']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('form:startPage.color')}
|
||||
name={[field.key, 'color']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<InputColor />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item
|
||||
wrapperCol={{
|
||||
sm: { offset: 6 },
|
||||
@ -92,11 +114,11 @@ export const StartPageTab: React.FC<TabPaneProps> = props => {
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
add()
|
||||
}}
|
||||
style={{ width: '60%' }}
|
||||
>
|
||||
<PlusOutlined /> Add Button
|
||||
<PlusOutlined /> {t('form:startPage.addButton')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@ -1,58 +1,63 @@
|
||||
import {Descriptions, Table} from 'antd'
|
||||
import {ColumnsType} from 'antd/lib/table/interface'
|
||||
import { Descriptions, Table } from 'antd'
|
||||
import { ColumnsType } from 'antd/lib/table/interface'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormPagerFragment } from '../../../graphql/fragment/form.pager.fragment'
|
||||
import {
|
||||
AdminPagerSubmissionEntryFieldQueryData,
|
||||
AdminPagerSubmissionEntryQueryData,
|
||||
AdminPagerSubmissionFormQueryData
|
||||
} from '../../../graphql/query/admin.pager.submission.query'
|
||||
SubmissionFieldFragment,
|
||||
SubmissionFragment,
|
||||
} from '../../../graphql/fragment/submission.fragment'
|
||||
import { fieldTypes } from '../types'
|
||||
|
||||
interface Props {
|
||||
form: AdminPagerSubmissionFormQueryData
|
||||
submission: AdminPagerSubmissionEntryQueryData
|
||||
form: FormPagerFragment
|
||||
submission: SubmissionFragment
|
||||
}
|
||||
|
||||
export const SubmissionValues: React.FC<Props> = props => {
|
||||
const columns: ColumnsType<AdminPagerSubmissionEntryFieldQueryData> = [
|
||||
{
|
||||
title: 'Field',
|
||||
render: (row: AdminPagerSubmissionEntryFieldQueryData) => {
|
||||
export const SubmissionValues: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const columns: ColumnsType<SubmissionFieldFragment> = [
|
||||
{
|
||||
title: t('submission:field'),
|
||||
render(_, row) {
|
||||
if (row.field) {
|
||||
return `${row.field.title}${row.field.required ? '*' : ''}`
|
||||
}
|
||||
|
||||
return `${row.id}`
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
render: row => {
|
||||
title: t('submission:value'),
|
||||
render(_, row) {
|
||||
try {
|
||||
const data = JSON.parse(row.value)
|
||||
|
||||
return data.value
|
||||
return fieldTypes[row.type]?.displayValue(row.value)
|
||||
} catch (e) {
|
||||
return row.value
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Descriptions title={'Submission'}>
|
||||
<Descriptions.Item label="Country">{props.submission.geoLocation.country}</Descriptions.Item>
|
||||
<Descriptions.Item label="City">{props.submission.geoLocation.city}</Descriptions.Item>
|
||||
<Descriptions.Item label="Device Type">{props.submission.device.type}</Descriptions.Item>
|
||||
<Descriptions.Item label="Device Name">{props.submission.device.name}</Descriptions.Item>
|
||||
<Descriptions title={t('submission:submission')}>
|
||||
<Descriptions.Item label={t('submission:country')}>
|
||||
{props.submission.geoLocation.country}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('submission:city')}>
|
||||
{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>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={props.submission.fields}
|
||||
rowKey={'id'}
|
||||
/>
|
||||
<Table columns={columns} dataSource={props.submission.fields} rowKey={'id'} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
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>
|
||||
<Form.Item
|
||||
label={'Min Date'}
|
||||
name={[field.name, 'optionKeys', '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, 'optionKeys', 'max']}
|
||||
labelCol={{ span: 6 }}
|
||||
getValueFromEvent={e => e.format('YYYY-MM-DD')}
|
||||
getValueProps={e => ({value: e ? moment(e) : undefined})}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
import {FormInstance} from 'antd/lib/form'
|
||||
|
||||
export interface AdminFieldTypeProps {
|
||||
form: FormInstance
|
||||
field: any
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
67
components/form/layouts/card/field.tsx
Normal file
67
components/form/layouts/card/field.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
0
components/form/layouts/card/index.module.scss
Normal file
0
components/form/layouts/card/index.module.scss
Normal file
221
components/form/layouts/card/index.tsx
Normal file
221
components/form/layouts/card/index.tsx
Normal file
@ -0,0 +1,221 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
65
components/form/layouts/card/page.tsx
Normal file
65
components/form/layouts/card/page.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
7
components/form/layouts/layout.props.ts
Normal file
7
components/form/layouts/layout.props.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { FormPublicFragment } from '../../../graphql/fragment/form.public.fragment'
|
||||
import { Submission } from '../../use.submission'
|
||||
|
||||
export interface LayoutProps {
|
||||
form: FormPublicFragment
|
||||
submission: Submission
|
||||
}
|
||||
34
components/form/layouts/page.buttons.tsx
Normal file
34
components/form/layouts/page.buttons.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
119
components/form/layouts/slider/field.tsx
Normal file
119
components/form/layouts/slider/field.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
122
components/form/layouts/slider/index.tsx
Normal file
122
components/form/layouts/slider/index.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
20
components/form/layouts/slider/page.module.scss
Normal file
20
components/form/layouts/slider/page.module.scss
Normal file
@ -0,0 +1,20 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
69
components/form/layouts/slider/page.tsx
Normal file
69
components/form/layouts/slider/page.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
38
components/form/types/abstract.type.tsx
Normal file
38
components/form/types/abstract.type.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
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,23 +1,23 @@
|
||||
import {Button, Col, Form, Input, Row} from 'antd'
|
||||
import { Button, Col, Form, Input, Row } from 'antd'
|
||||
import React from 'react'
|
||||
import {AdminFieldTypeProps} from './type.props'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FieldAdminProps } from '../field.admin.props'
|
||||
|
||||
export const CheckboxAdmin: React.FC<FieldAdminProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
export const DropdownType: React.FC<AdminFieldTypeProps> = props => {
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={'Default Value'}
|
||||
name={[props.field.name, 'value']}
|
||||
label={t('type:checkbox:default')}
|
||||
name={[props.field.name as string, 'defaultValue']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.List
|
||||
name={[props.field.name, 'options']}
|
||||
>
|
||||
<Form.List name={[props.field.name as string, 'options']}>
|
||||
{(fields, { add, remove }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
@ -26,7 +26,7 @@ export const DropdownType: React.FC<AdminFieldTypeProps> = props => {
|
||||
sm: { offset: index === 0 ? 0 : 6 },
|
||||
}}
|
||||
labelCol={{ span: 6 }}
|
||||
label={index === 0 ? 'Options' : ''}
|
||||
label={index === 0 ? t('type:checkbox:options') : ''}
|
||||
key={field.key}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
@ -34,26 +34,24 @@ export const DropdownType: React.FC<AdminFieldTypeProps> = props => {
|
||||
<Form.Item
|
||||
wrapperCol={{ span: 24 }}
|
||||
name={[field.name, 'title']}
|
||||
style={{marginBottom: 0}}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input placeholder={'Title'} />
|
||||
<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: 'Please provide a value' }
|
||||
]}
|
||||
style={{ marginBottom: 0 }}
|
||||
rules={[{ required: true, message: t('validation:valueRequired') }]}
|
||||
>
|
||||
<Input placeholder={'Value'} />
|
||||
<Input placeholder={t('type:checkbox:valuePlaceholder')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Button danger onClick={() => remove(index)}>
|
||||
Remove
|
||||
{t('type:checkbox:removeOption')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
@ -66,10 +64,9 @@ export const DropdownType: React.FC<AdminFieldTypeProps> = props => {
|
||||
}}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Button
|
||||
type={'dashed'}
|
||||
onClick={() => add()}
|
||||
>Add Option</Button>
|
||||
<Button type={'dashed'} onClick={() => add()}>
|
||||
{t('type:checkbox:addOption')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
61
components/form/types/checkbox/checkbox.input.tsx
Normal file
61
components/form/types/checkbox/checkbox.input.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
15
components/form/types/checkbox/index.ts
Normal file
15
components/form/types/checkbox/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
import {Form} from 'antd'
|
||||
import dayjs, {Dayjs} from 'dayjs'
|
||||
import moment from 'moment'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {StyledDateInput} from '../../styled/date.input'
|
||||
import {FieldTypeProps} from './type.props'
|
||||
|
||||
export const DateType: React.FC<FieldTypeProps> = ({ field, design}) => {
|
||||
const [min, setMin] = useState<Dayjs>()
|
||||
const [max, setMax] = useState<Dayjs>()
|
||||
|
||||
useEffect(() => {
|
||||
field.options.forEach(option => {
|
||||
if (option.key === 'min') {
|
||||
setMin(dayjs(option.value))
|
||||
}
|
||||
if (option.key === 'max') {
|
||||
setMax(dayjs(option.value))
|
||||
}
|
||||
})
|
||||
}, [field])
|
||||
|
||||
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})}
|
||||
initialValue={field.value ? moment(field.value) : undefined}
|
||||
>
|
||||
<StyledDateInput
|
||||
size={'large'}
|
||||
design={design}
|
||||
autoFocus
|
||||
disabledDate={(d: any) => {
|
||||
if (min && min.isAfter(d)) {
|
||||
return true
|
||||
}
|
||||
if (max && max.isBefore(d)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
50
components/form/types/date/date.admin.tsx
Normal file
50
components/form/types/date/date.admin.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
78
components/form/types/date/date.input.tsx
Normal file
78
components/form/types/date/date.input.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
24
components/form/types/date/index.ts
Normal file
24
components/form/types/date/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import {Form, Select} from 'antd'
|
||||
import React, {useState} from 'react'
|
||||
import {StyledSelect} from '../../styled/select'
|
||||
import {FieldTypeProps} from './type.props'
|
||||
|
||||
export const DropdownType: React.FC<FieldTypeProps> = ({field, design}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
name={[field.id, 'value']}
|
||||
rules={[
|
||||
{ required: field.required, message: 'Please provide Information' },
|
||||
]}
|
||||
initialValue={field.value || null}
|
||||
>
|
||||
<StyledSelect 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}>OK{option.title || option.value}</Select.Option>
|
||||
))}
|
||||
</StyledSelect>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
77
components/form/types/dropdown/dropdown.admin.tsx
Normal file
77
components/form/types/dropdown/dropdown.admin.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
62
components/form/types/dropdown/dropdown.input.tsx
Normal file
62
components/form/types/dropdown/dropdown.input.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
15
components/form/types/dropdown/index.ts
Normal file
15
components/form/types/dropdown/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
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' }
|
||||
]}
|
||||
initialValue={field.value}
|
||||
>
|
||||
<StyledInput
|
||||
design={design}
|
||||
allowClear
|
||||
size={'large'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
components/form/types/email/email.admin.tsx
Normal file
21
components/form/types/email/email.admin.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
49
components/form/types/email/email.input.tsx
Normal file
49
components/form/types/email/email.input.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
15
components/form/types/email/index.ts
Normal file
15
components/form/types/email/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
7
components/form/types/field.admin.props.ts
Normal file
7
components/form/types/field.admin.props.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { FormInstance } from 'antd/lib/form'
|
||||
import { FieldData } from 'rc-field-form/lib/interface'
|
||||
|
||||
export interface FieldAdminProps {
|
||||
form: FormInstance
|
||||
field: FieldData
|
||||
}
|
||||
6
components/form/types/field.input.builder.type.ts
Normal file
6
components/form/types/field.input.builder.type.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ComponentType } from 'react'
|
||||
|
||||
import { AbstractType } from './abstract.type'
|
||||
import { FieldInputProps } from './field.input.props'
|
||||
|
||||
export type FieldInputBuilderType<A = AbstractType> = (type: A) => ComponentType<FieldInputProps>
|
||||
11
components/form/types/field.input.props.ts
Normal file
11
components/form/types/field.input.props.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {
|
||||
FormPublicDesignFragment,
|
||||
FormPublicFieldFragment,
|
||||
} from '../../../graphql/fragment/form.public.fragment'
|
||||
|
||||
export interface FieldInputProps {
|
||||
field: FormPublicFieldFragment
|
||||
design: FormPublicDesignFragment
|
||||
focus?: boolean
|
||||
urlValue?: string
|
||||
}
|
||||
20
components/form/types/hidden/hidden.admin.tsx
Normal file
20
components/form/types/hidden/hidden.admin.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
15
components/form/types/hidden/index.ts
Normal file
15
components/form/types/hidden/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
2
components/form/types/image/image.admin.tsx
Normal file
2
components/form/types/image/image.admin.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
export {}
|
||||
2
components/form/types/image/image.type.tsx
Normal file
2
components/form/types/image/image.type.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
export {}
|
||||
19
components/form/types/image/index.ts
Normal file
19
components/form/types/image/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
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,27 +1,34 @@
|
||||
import React from 'react'
|
||||
import {DateType} from './date.type'
|
||||
import {DropdownType} from './dropdown.type'
|
||||
import {EmailType} from './email.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 {FieldTypeProps} from './type.props'
|
||||
import {YesNoType} from './yes_no.type'
|
||||
import { AbstractType } from './abstract.type'
|
||||
import { CheckboxType } from './checkbox'
|
||||
import { DateType } from './date'
|
||||
import { DropdownType } from './dropdown'
|
||||
import { EmailType } from './email'
|
||||
import { HiddenType } from './hidden'
|
||||
import { LinkType } from './link'
|
||||
import { LocationType } from './location'
|
||||
import { NumberType } from './number'
|
||||
import { RadioType } from './radio'
|
||||
import { RatingType } from './rating'
|
||||
import { SliderType } from './slider'
|
||||
import { TextareaType } from './textarea'
|
||||
import { TextfieldType } from './textfield'
|
||||
import { YesNoType } from './yes_no'
|
||||
|
||||
export const fieldTypes: {
|
||||
[key: string]: React.FC<FieldTypeProps>
|
||||
[key: string]: AbstractType
|
||||
} = {
|
||||
'textfield': TextType,
|
||||
'date': DateType,
|
||||
'email': EmailType,
|
||||
'textarea': TextareaType,
|
||||
'link': LinkType,
|
||||
'dropdown': DropdownType,
|
||||
'rating': RatingType,
|
||||
'radio': RadioType,
|
||||
'yes_no': YesNoType,
|
||||
'number': NumberType,
|
||||
checkbox: new CheckboxType(),
|
||||
date: new DateType(),
|
||||
dropdown: new DropdownType(),
|
||||
email: new EmailType(),
|
||||
hidden: new HiddenType(),
|
||||
link: new LinkType(),
|
||||
location: new LocationType(),
|
||||
number: new NumberType(),
|
||||
radio: new RadioType(),
|
||||
rating: new RatingType(),
|
||||
slider: new SliderType(),
|
||||
textarea: new TextareaType(),
|
||||
textfield: new TextfieldType(),
|
||||
yes_no: new YesNoType(),
|
||||
}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
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' }
|
||||
]}
|
||||
initialValue={field.value}
|
||||
>
|
||||
<StyledInput
|
||||
design={design}
|
||||
allowClear
|
||||
size={'large'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
components/form/types/link/index.ts
Normal file
15
components/form/types/link/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
21
components/form/types/link/link.admin.tsx
Normal file
21
components/form/types/link/link.admin.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
49
components/form/types/link/link.input.tsx
Normal file
49
components/form/types/link/link.input.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
34
components/form/types/location/index.ts
Normal file
34
components/form/types/location/index.ts
Normal file
@ -0,0 +1,34 @@
|
||||
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}`
|
||||
}
|
||||
}
|
||||
142
components/form/types/location/location.admin.tsx
Normal file
142
components/form/types/location/location.admin.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
151
components/form/types/location/location.input.tsx
Normal file
151
components/form/types/location/location.input.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
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' },
|
||||
]}
|
||||
initialValue={parseFloat(field.value)}
|
||||
>
|
||||
<StyledNumberInput
|
||||
design={design}
|
||||
size={'large'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
19
components/form/types/number/index.ts
Normal file
19
components/form/types/number/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
20
components/form/types/number/number.admin.tsx
Normal file
20
components/form/types/number/number.admin.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
49
components/form/types/number/number.input.tsx
Normal file
49
components/form/types/number/number.input.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import {Form, Radio} from 'antd'
|
||||
import React from 'react'
|
||||
import {StyledRadio} from '../../styled/radio'
|
||||
import {FieldTypeProps} from './type.props'
|
||||
|
||||
export const RadioType: React.FC<FieldTypeProps> = ({field, design}) => {
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
name={[field.id, 'value']}
|
||||
rules={[
|
||||
{ required: field.required, message: 'Please provide Information' },
|
||||
]}
|
||||
initialValue={field.options.map(option => option.value).find(value => value === field.value)}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
15
components/form/types/radio/index.ts
Normal file
15
components/form/types/radio/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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,23 +1,23 @@
|
||||
import {Button, Col, Form, Input, Row} from 'antd'
|
||||
import { Button, Col, Form, Input, Row } from 'antd'
|
||||
import React from 'react'
|
||||
import {AdminFieldTypeProps} from './type.props'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FieldAdminProps } from '../field.admin.props'
|
||||
|
||||
export const RadioAdmin: React.FC<FieldAdminProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
export const RadioType: React.FC<AdminFieldTypeProps> = props => {
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={'Default Value'}
|
||||
name={[props.field.name, 'value']}
|
||||
label={t('type:radio:default')}
|
||||
name={[props.field.name as string, 'defaultValue']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.List
|
||||
name={[props.field.name, 'options']}
|
||||
>
|
||||
<Form.List name={[props.field.name as string, 'options']}>
|
||||
{(fields, { add, remove }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
@ -26,7 +26,7 @@ export const RadioType: React.FC<AdminFieldTypeProps> = props => {
|
||||
sm: { offset: index === 0 ? 0 : 6 },
|
||||
}}
|
||||
labelCol={{ span: 6 }}
|
||||
label={index === 0 ? 'Options' : ''}
|
||||
label={index === 0 ? t('type:radio:options') : ''}
|
||||
key={field.key}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
@ -34,26 +34,24 @@ export const RadioType: React.FC<AdminFieldTypeProps> = props => {
|
||||
<Form.Item
|
||||
wrapperCol={{ span: 24 }}
|
||||
name={[field.name, 'title']}
|
||||
style={{marginBottom: 0}}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Input placeholder={'Title'} />
|
||||
<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: 'Please provide a value' }
|
||||
]}
|
||||
style={{ marginBottom: 0 }}
|
||||
rules={[{ required: true, message: t('validation:valueRequired') }]}
|
||||
>
|
||||
<Input placeholder={'Value'} />
|
||||
<Input placeholder={t('type:radio:valuePlaceholder')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Button danger onClick={() => remove(index)}>
|
||||
Remove
|
||||
{t('type:radio:removeOption')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
@ -66,10 +64,9 @@ export const RadioType: React.FC<AdminFieldTypeProps> = props => {
|
||||
}}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Button
|
||||
type={'dashed'}
|
||||
onClick={() => add()}
|
||||
>Add Option</Button>
|
||||
<Button type={'dashed'} onClick={() => add()}>
|
||||
{t('type:radio:addOption')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
55
components/form/types/radio/radio.input.tsx
Normal file
55
components/form/types/radio/radio.input.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
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' },
|
||||
]}
|
||||
initialValue={parseFloat(field.value)}
|
||||
>
|
||||
<Rate allowHalf />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
19
components/form/types/rating/index.ts
Normal file
19
components/form/types/rating/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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 RatingType extends AbstractType<number> {
|
||||
parseUrlValue(raw: string): number {
|
||||
return parseFloat(raw)
|
||||
}
|
||||
|
||||
adminFormField(): ComponentType<FieldAdminProps> {
|
||||
return dynamic(() => import('./rating.admin').then(c => c.RatingAdmin));
|
||||
}
|
||||
|
||||
inputFormField(): ComponentType<FieldInputProps> {
|
||||
return dynamic(() => import('./rating.input').then(c => c.builder(this)));
|
||||
}
|
||||
}
|
||||
21
components/form/types/rating/rating.admin.tsx
Normal file
21
components/form/types/rating/rating.admin.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Form, Rate } from 'antd'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FieldAdminProps } from '../field.admin.props'
|
||||
|
||||
export const RatingAdmin: React.FC<FieldAdminProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:rating:default')}
|
||||
name={[props.field.name as string, 'defaultValue']}
|
||||
labelCol={{ span: 6 }}
|
||||
extra={t('type:rating.clearNote')}
|
||||
>
|
||||
<Rate allowHalf allowClear />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
43
components/form/types/rating/rating.input.tsx
Normal file
43
components/form/types/rating/rating.input.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Form, Rate } from 'antd'
|
||||
import debug from 'debug'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FieldInputBuilderType } from '../field.input.builder.type'
|
||||
|
||||
const logger = debug('rating.input')
|
||||
|
||||
export const builder: FieldInputBuilderType = ({
|
||||
parseUrlValue,
|
||||
parseValue,
|
||||
}) => function RatingInput ({
|
||||
field,
|
||||
urlValue,
|
||||
}) {
|
||||
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={[{ required: field.required, message: t('validation:valueRequired') }]}
|
||||
initialValue={initialValue}
|
||||
>
|
||||
<Rate allowHalf />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
19
components/form/types/slider/index.ts
Normal file
19
components/form/types/slider/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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 SliderType extends AbstractType<number> {
|
||||
parseUrlValue(raw: string): number {
|
||||
return parseFloat(raw)
|
||||
}
|
||||
|
||||
adminFormField(): ComponentType<FieldAdminProps> {
|
||||
return dynamic(() => import('./slider.admin').then(c => c.SliderAdmin));
|
||||
}
|
||||
|
||||
inputFormField(): ComponentType<FieldInputProps> {
|
||||
return dynamic(() => import('./slider.input').then(c => c.builder(this)));
|
||||
}
|
||||
}
|
||||
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