Compare commits

..

64 Commits

Author SHA1 Message Date
Teifun2
f1bef3bdb4 Update use.math.ts
To Statsify LINT and fix the build:
https://eslint.org/docs/latest/rules/no-prototype-builtins
2022-11-14 14:03:21 +01:00
Teifun2
535c660f59 Fix some issues with Logic
values.hasOwnProperty(term) is needed as the if statement validates to false even if values contains the term  but the value of the term is currently false (yes / no option with default no)
2022-11-14 13:18:01 +01:00
Michael Schramm
011a6baef4 fix node prune location
fixes https://github.com/ohmyform/ohmyform/issues/184
2022-07-28 08:01:08 +02:00
Michael Schramm
19339436a3 release 1.0.3 2022-03-27 11:31:10 +02:00
Michael Schramm
84d96cd5ee changed default form now has an end page 2022-03-27 11:29:29 +02:00
Michael Schramm
ba302dcd63 fixed sorting of fields in excel export 2022-03-27 11:29:18 +02:00
Michael Schramm
fe90770999 fix build 2022-03-14 17:17:15 +01:00
Michael Schramm
dd45330a0f add todo and improve class names 2022-03-14 16:56:30 +01:00
Michael Schramm
abf57a6976 release 1.0.2 2022-03-13 23:41:41 +01:00
Michael Schramm
30ff2c96bc fix field sort in excel submission export 2022-03-13 23:30:46 +01:00
Michael Schramm
23e67c8f7e release 1.0.1 2022-03-01 23:39:20 +01:00
Michael Schramm
674678dba3 fix lint 2022-03-01 16:10:17 +01:00
Michael Schramm
1b87f9352c fix lint, and readd focus passing 2022-03-01 16:10:17 +01:00
Michael Schramm
4c8ca21fd2 improve render while still keeping logic refreshes 2022-03-01 16:10:17 +01:00
Michael Schramm
fe51c528d2 change default form layout to card 2022-03-01 16:10:17 +01:00
Michael Schramm
951dd2e5b4 change default form layout to card 2022-03-01 16:10:17 +01:00
Michael Schramm
019cd7f55e add admin for location field type 2022-03-01 16:10:17 +01:00
Michael Schramm
2e2d1b9a21 edit user shown now email in title 2022-02-28 23:01:38 +01:00
Michael Schramm
cf53f46b48 show warning icon in form list if not public 2022-02-28 22:51:45 +01:00
Michael Schramm
7b5e717a56 add missing dev dependency for locale scripts 2022-02-28 22:51:14 +01:00
Michael Schramm
1e90c11f30 update translation status 2022-02-28 18:37:20 +01:00
Michael Schramm
5909d1439c update changelog for version 1.0.0 2022-02-28 08:27:28 +01:00
Michael Schramm
ca5edbbb3b upgrade packages, improve field logic, fix slider, hide empty submissions, fix urls for buttons, improve data handling for fields, improve sqlite migration handling 2022-02-27 12:58:52 +01:00
Michael Schramm
e33b3ff392 update changelog 2022-02-26 22:17:41 +01:00
Michael Schramm
66300ee391 upgrade packages and improve styles 2022-02-26 22:13:08 +01:00
Michael Schramm
1e2eb5792f revert to access form.prefixName instead of context
fixes https://github.com/ohmyform/ohmyform/issues/150
2022-02-26 22:10:39 +01:00
Michael Schramm
bc56a70fea remove next/image to fix static exports
fixes https://github.com/ohmyform/ohmyform/issues/154
2022-02-13 23:13:59 +01:00
Michael Schramm
5e2596f131 fix development documentation
fixes https://github.com/ohmyform/ui/issues/65
2022-02-13 22:23:30 +01:00
Michael Schramm
9b275fddd9 update changelog 2022-02-13 22:21:39 +01:00
ravid
2428935ef9 Fixed webhook 2022-02-13 22:18:04 +01:00
Michael Schramm
4c6d158560 fix android screen size
fix https://github.com/ohmyform/ohmyform/issues/114
2022-01-03 20:18:41 +01:00
Michael Schramm
1f0926c904 test new tags 2022-01-03 13:01:24 +01:00
Michael Schramm
eaa48e1d0d test new tags 2022-01-03 12:46:18 +01:00
Michael Schramm
2f6e63120c test new tags 2022-01-03 12:44:05 +01:00
Michael Schramm
ed2eed16d5 test new tags 2022-01-03 12:42:50 +01:00
Michael Schramm
f9ac50b737 fix where to run lint commands 2022-01-03 10:15:28 +01:00
Michael Schramm
107b2da182 add linter and fix types for submission field 2022-01-03 10:13:18 +01:00
Michael Schramm
0c1fa45288 test github workflow 2022-01-03 09:53:25 +01:00
Michael Schramm
5bcaedb556 add checkbox field type
fixes https://github.com/ohmyform/ohmyform/issues/138
2022-01-03 09:00:31 +01:00
Michael Schramm
831e0d7779 reload list after adding new form
fixes https://github.com/ohmyform/ohmyform/issues/139
2022-01-03 08:47:02 +01:00
Michael Schramm
92bc295580 cleanup 2022-01-03 08:39:42 +01:00
Michael Schramm
ebc7d755b5 anonymous form submissions
fixes anonymous form submissions (fixes https://github.com/ohmyform/ohmyform/issues/108)
2022-01-03 08:23:03 +01:00
Michael Schramm
18693be229 upgrade packages 2022-01-03 07:52:02 +01:00
Michael Schramm
e54da2b111 upgrade to nextjs 12, add visible logic check 2022-01-03 00:39:47 +01:00
Michael Schramm
26c2f9e095 update prefixName as suggested in https://github.com/ant-design/ant-design/issues/30529 2022-01-03 00:39:47 +01:00
Michael Schramm
1f37215af4 add field logic in admin 2022-01-03 00:39:47 +01:00
Michael Schramm
bef465f8d3 update autofocus 2021-05-15 17:44:34 +02:00
Michael Schramm
d5dff46816 add form layouts 2021-05-15 17:30:49 +02:00
Michael Schramm
de1180d547 add slider field type 2021-05-15 10:25:45 +02:00
Michael Schramm
94f1de3e25 improvements 2021-05-15 10:25:22 +02:00
Michael Schramm
c793321ecb switch to webpack5 2021-05-15 09:04:56 +02:00
Michael Schramm
117216c851 update translation status 2021-05-15 08:22:05 +02:00
Michael Schramm
020ec938b7 fix problem with node-prune on production build 2021-05-06 20:11:04 +02:00
Michael Schramm
b0f0ff2a50 improve position of api not connected error message 2021-05-04 18:15:15 +02:00
Michael Schramm
31a84fb520 show forms only for admins 2021-05-04 18:14:54 +02:00
Michael Schramm
76ab0ceb67 fix import 2021-05-02 18:23:40 +02:00
Michael Schramm
8ee1e0ff8a show error message on homepage in case there is a problem with api connection 2021-05-02 15:58:06 +02:00
Michael Schramm
c6cf6783b4 fix export command 2021-05-02 15:52:19 +02:00
Michael Schramm
078510d41e add new languages 2021-05-02 13:09:31 +02:00
Michael Schramm
b6296691c2 update translation status 2021-05-02 12:55:01 +02:00
Michael Schramm
8713c0a8c6 - ability to change user passwords
- add default page background
- add environment list in [doc](doc/environment.md)
- combined notificationts to become more versatile
- use exported hooks for graphql
- links at the bottom for new users
- fixes for hide contrib setting
- upgrad all packages
2021-05-02 12:43:55 +02:00
Daisuke Inoue
dfbad77d7e Add Japanese translations.
on correct places.
2021-02-22 09:20:51 +01:00
Daisuke Inoue
aeffebb0f7 Remove wrong files. 2021-02-22 09:20:51 +01:00
Daisuke Inoue
9b988d1326 Translated to Japanese 2021-02-22 09:20:51 +01:00
395 changed files with 11351 additions and 8114 deletions

View File

@ -6,21 +6,65 @@ module.exports = {
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',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier',
],
rules: {
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
'@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',
'@typescript-eslint/no-var-requires': '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: {

43
.github/workflows/docker-image.yml vendored Normal file
View 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
View 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
View File

@ -26,6 +26,8 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
.env
# development environments
/.idea
schema.graphql

View File

@ -1,12 +0,0 @@
language: node_js
node_js:
- 12
cache:
directories:
- node_modules
script:
- yarn
- yarn lint
- yarn export

View File

@ -5,7 +5,9 @@ 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/).
## UNRELEASED
<!--
Template for next version
## [Unreleased]
### Added
@ -14,6 +16,99 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### 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
@ -123,11 +218,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed
- `export` uses now spa mode for initial loading screen
- change value to defaultValue for initial form
### Fixed
- [OMF#93](https://github.com/ohmyform/ohmyform/issues/93) dropdown options are not saved
- 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

View File

@ -3,10 +3,10 @@ MAINTAINER OhMyForm <admin@ohmyform.com>
WORKDIR /usr/src/app
RUN apk update && apk add curl bash && rm -rf /var/cache/apk/*
RUN apk --update --no-cache add curl bash g++ make libpng-dev
# install node-prune (https://github.com/tj/node-prune)
RUN curl -sfL https://install.goreleaser.com/github.com/tj/node-prune.sh | bash -s -- -b /usr/local/bin
RUN curl -sf https://gobinaries.com/tj/node-prune | sh
COPY . ./
@ -17,7 +17,8 @@ RUN yarn build
RUN npm prune --production
# run node prune
RUN /usr/local/bin/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>
@ -29,7 +30,8 @@ WORKDIR /usr/src/app
COPY --from=builder /usr/src/app /usr/src/app
ENV PORT=4000
ENV PORT=4000 \
NODE_ENV=production
# Change to non-root privilege
USER ohmyform

View File

@ -1,6 +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};
@ -25,12 +26,18 @@
}
}
.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

View File

@ -1,5 +1,7 @@
.footer {
position: absolute;
padding-left: 16px;
margin-bottom: 4px;
bottom: 0;
left: 0;
right: 0;

View File

@ -1,11 +1,10 @@
import { useQuery } from '@apollo/react-hooks'
import { Button, Select } from 'antd'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React from 'react'
import GitHubButton from 'react-github-button'
import { useTranslation } from 'react-i18next'
import { SETTINGS_QUERY, SettingsQueryData } from '../../graphql/query/settings.query'
import { useSettingsQuery } from '../../graphql/query/settings.query'
import { languages } from '../../i18n'
import { clearAuth, withAuth } from '../with.auth'
import scss from './footer.module.scss'
@ -14,13 +13,14 @@ interface Props {
me?: {
id: string
username: string
roles: string[]
}
}
const AuthFooterInner: React.FC<Props> = (props) => {
const { t, i18n } = useTranslation()
const router = useRouter()
const { data } = useQuery<SettingsQueryData>(SETTINGS_QUERY)
const { data, loading } = useSettingsQuery()
const logout = () => {
clearAuth()
@ -29,59 +29,68 @@ const AuthFooterInner: React.FC<Props> = (props) => {
return (
<footer className={scss.footer}>
<Link href={'/admin'}>
<Button
type={'link'}
ghost
style={{
color: '#FFF',
}}
>
{t('admin')}
</Button>
</Link>
{props.me
? [
<span style={{ color: '#FFF' }} key={'user'}>
<span style={{ color: '#FFF' }} key={'user'}>
Hi, {props.me.username}
</span>,
<Button
key={'logout'}
type={'link'}
ghost
onClick={logout}
style={{
color: '#FFF',
}}
>
{t('logout')}
</Button>,
]
: [
<Link href={'/login'} key={'login'}>
</span>,
props.me.roles.includes('admin') && (
<Link key={'admin'} href={'/admin'}>
<Button
type={'link'}
ghost
style={{
color: '#FFF',
}}
>
{t('login')}
{t('admin')}
</Button>
</Link>,
</Link>
),
<Link key={'profile'} href={'/admin/profile'}>
<Button
type={'link'}
style={{
color: '#FFF',
}}
>
{t('profile')}
</Button>
</Link>,
<Button
key={'logout'}
type={'link'}
onClick={logout}
style={{
color: '#FFF',
}}
>
{t('logout')}
</Button>,
]
: [
<Link href={'/login'} key={'login'}>
<Button
type={'link'}
style={{
color: '#FFF',
}}
>
{t('login')}
</Button>
</Link>,
!loading && !data?.disabledSignUp.value && (
<Link href={'/register'} key={'register'}>
<Button
type={'link'}
ghost
disabled={data ? data.disabledSignUp.value : false}
style={{
color: '#FFF',
}}
>
{t('register')}
</Button>
</Link>,
]}
</Link>
),
]}
<div style={{ flex: 1 }} />
<Select
bordered={false}
@ -99,31 +108,33 @@ const AuthFooterInner: React.FC<Props> = (props) => {
</Select.Option>
))}
</Select>
<GitHubButton type="stargazers" namespace="ohmyform" repo="ohmyform" />
<Button
type={'link'}
target={'_blank'}
rel={'noreferrer'}
ghost
href={'https://www.ohmyform.com'}
style={{
color: '#FFF',
}}
>
OhMyForm
</Button>
<Button
type={'link'}
target={'_blank'}
rel={'noreferrer'}
ghost
href={'https://lokalise.com/'}
style={{
color: '#FFF',
}}
>
translated with Lokalize
</Button>
{!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>
)
}

View File

@ -1,5 +1,9 @@
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
@ -11,7 +15,7 @@ export const AuthLayout: React.FC<Props> = (props) => {
<Layout
style={{
height: '100vh',
background: '#437fdc',
background: publicRuntimeConfig.mainBackground,
}}
>
{props.children}

View File

@ -56,6 +56,14 @@ export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
>
<Switch />
</Form.Item>
<Form.Item
label={t('form:baseData.anonymousSubmission')}
name={['form', 'anonymousSubmission']}
valuePropName={'checked'}
>
<Switch />
</Form.Item>
</Tabs.TabPane>
)
}

View File

@ -1,4 +1,4 @@
import { Form, Input, Tabs } from 'antd'
import { Form, Input, Select, Tabs } from 'antd'
import { TabPaneProps } from 'antd/lib/tabs'
import React from 'react'
import { useTranslation } from 'react-i18next'
@ -9,22 +9,37 @@ export const DesignTab: React.FC<TabPaneProps> = (props) => {
return (
<Tabs.TabPane {...props}>
<Form.Item label={t('form:design.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>
{[
'backgroundColor',
'questionColor',
'answerColor',
'buttonColor',
'buttonActiveColor',
'buttonTextColor',
'background', 'question', 'answer', 'button', 'buttonActive', 'buttonText',
].map((name) => (
<Form.Item
key={name}
label={t(`form:design.${name}`)}
name={['form', 'design', 'colors', name]}
label={t(`form:design.color.${name}`)}
name={[
'form', 'design', 'colors', name,
]}
>
<InputColor />
</Form.Item>

View File

@ -12,19 +12,25 @@ export const EndPageTab: React.FC<TabPaneProps> = (props) => {
<Tabs.TabPane {...props}>
<Form.Item
label={t('form:endPage.show')}
name={['form', 'endPage', 'show']}
name={[
'form', 'endPage', 'show',
]}
valuePropName={'checked'}
>
<Switch />
</Form.Item>
<Form.Item label={t('form:endPage.title')} name={['form', 'endPage', 'title']}>
<Form.Item label={t('form:endPage.title')} name={[
'form', 'endPage', 'title',
]}>
<Input />
</Form.Item>
<Form.Item
label={t('form:endPage.paragraph')}
name={['form', 'endPage', 'paragraph']}
name={[
'form', 'endPage', 'paragraph',
]}
extra={t('type:descriptionInfo')}
>
<Input.TextArea autoSize />
@ -32,12 +38,16 @@ export const EndPageTab: React.FC<TabPaneProps> = (props) => {
<Form.Item
label={t('form:endPage.continueButtonText')}
name={['form', 'endPage', 'buttonText']}
name={[
'form', 'endPage', 'buttonText',
]}
>
<Input />
</Form.Item>
<Form.List name={['form', 'endPage', 'buttons']}>
<Form.List name={[
'form', 'endPage', 'buttons',
]}>
{(fields, { add, remove }) => {
return (
<div>

View File

@ -1,19 +1,10 @@
import { useQuery } from '@apollo/react-hooks'
import { message } from 'antd'
import ExcelJS, { CellValue } from 'exceljs'
import { useCallback, useState } from 'react'
import ExcelJS from 'exceljs'
import {
ADMIN_FORM_QUERY,
AdminFormQueryData,
AdminFormQueryVariables,
} from '../../../graphql/query/admin.form.query'
import {
ADMIN_PAGER_SUBMISSION_QUERY,
AdminPagerSubmissionEntryQueryData,
AdminPagerSubmissionQueryData,
AdminPagerSubmissionQueryVariables,
} from '../../../graphql/query/admin.pager.submission.query'
import { useImperativeQuery } from '../../use.imerative.query'
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
@ -23,16 +14,13 @@ interface Props {
export const ExportSubmissionAction: React.FC<Props> = (props) => {
const [loading, setLoading] = useState(false)
const form = useQuery<AdminFormQueryData, AdminFormQueryVariables>(ADMIN_FORM_QUERY, {
const form = useFormQuery({
variables: {
id: props.form,
},
})
const getSubmissions = useImperativeQuery<
AdminPagerSubmissionQueryData,
AdminPagerSubmissionQueryVariables
>(ADMIN_PAGER_SUBMISSION_QUERY)
const getSubmissions = useSubmissionPagerImperativeQuery()
const exportSubmissions = useCallback(async () => {
if (loading) {
@ -48,6 +36,12 @@ export const ExportSubmissionAction: React.FC<Props> = (props) => {
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',
@ -57,7 +51,7 @@ export const ExportSubmissionAction: React.FC<Props> = (props) => {
'City',
'User Agent',
'Device',
...form.data.form.fields.map((field) => `${field.title} (${field.type})`),
...orderedFields.map((field) => `${field.title} (${field.type})`),
]
const firstPage = await getSubmissions({
@ -66,8 +60,8 @@ export const ExportSubmissionAction: React.FC<Props> = (props) => {
start: 0,
})
const buildRow = (data: AdminPagerSubmissionEntryQueryData): any[] => {
const row = [
const buildRow = (data: SubmissionFragment): CellValue[] => {
const row: CellValue[] = [
data.id,
data.created,
data.lastModified,
@ -77,10 +71,13 @@ export const ExportSubmissionAction: React.FC<Props> = (props) => {
data.device.name,
]
data.fields.forEach((field) => {
orderedFields.forEach((formField) => {
const field = data.fields.find(submission => submission.field?.id === formField.id)
try {
const decoded = JSON.parse(field.value)
row.push(decoded.value)
fieldTypes[field.type]?.stringifyValue(field.value)
row.push(fieldTypes[field.type]?.stringifyValue(field.value))
} catch (e) {
row.push('')
}
@ -120,7 +117,9 @@ export const ExportSubmissionAction: React.FC<Props> = (props) => {
})
}
setLoading(false)
}, [form, getSubmissions, props.form, setLoading, loading])
}, [
form, getSubmissions, props.form, setLoading, loading,
])
return props.trigger(() => exportSubmissions(), loading)
}

View File

@ -1,35 +1,54 @@
import { DeleteOutlined } from '@ant-design/icons/lib'
import { Button, Card, Checkbox, Form, Input, Popconfirm, Popover, Tag } from 'antd'
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, { useEffect, useState } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFormFieldFragment } from '../../../graphql/fragment/admin.form.fragment'
import { adminTypes } from './types'
import { TextType } from './types/text.type'
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[]) => void
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) => {
export const FieldCard: React.FC<Props> = ({
form,
field,
fields,
onChangeFields,
remove,
move,
index,
}) => {
const { t } = useTranslation()
const { form, field, fields, onChangeFields, remove, index } = props
const type = form.getFieldValue(['form', 'fields', field.name as string, 'type']) as string
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 [shouldUpdate, setShouldUpdate] = useState(false)
const [nextTitle, setNextTitle] = useState<string>(
form.getFieldValue(['form', 'fields', field.name as string, 'title'])
form.getFieldValue([
'form', 'fields', field.name as string, 'title',
])
)
useEffect(() => {
if (!shouldUpdate) {
return
}
const id = setTimeout(() => {
setShouldUpdate(false)
onChangeFields(
fields.map((field, i) => {
if (i === index) {
@ -45,14 +64,79 @@ export const FieldCard: React.FC<Props> = (props) => {
}, 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>
<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={
@ -88,9 +172,8 @@ export const FieldCard: React.FC<Props> = (props) => {
<DeleteOutlined />
</Button>
</Popconfirm>
</div>
</Space>
}
actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}
>
<Form.Item name={[field.name as string, 'type']} noStyle>
<Input type={'hidden'} />
@ -101,7 +184,12 @@ export const FieldCard: React.FC<Props> = (props) => {
rules={[{ required: true, message: 'Title is required' }]}
labelCol={{ span: 6 }}
>
<Input onChange={(e) => setNextTitle(e.target.value)} />
<Input
onChange={(e) => {
setNextTitle(e.target.value)
setShouldUpdate(true)
}}
/>
</Form.Item>
<Form.Item
label={t('type:description')}
@ -122,6 +210,35 @@ export const FieldCard: React.FC<Props> = (props) => {
</Form.Item>
<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>
)
}

View File

@ -2,17 +2,20 @@ 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 { AdminFormFieldFragment } from '../../../graphql/fragment/admin.form.fragment'
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
import { fieldTypes } from '../types'
import { FieldCard } from './field.card'
import { adminTypes } from './types'
const logger = debug('FieldsTab')
interface Props extends TabPaneProps {
form: FormInstance
fields: AdminFormFieldFragment[]
onChangeFields: (fields: AdminFormFieldFragment[]) => void
fields: FormFieldFragment[]
onChangeFields: (fields: FormFieldFragment[]) => void
}
export const FieldsTab: React.FC<Props> = (props) => {
@ -20,13 +23,25 @@ export const FieldsTab: React.FC<Props> = (props) => {
const [nextType, setNextType] = useState('textfield')
const renderType = useCallback(
(field: FieldData, index: number, remove: (index: number) => void) => {
(
field: FieldData,
index: number,
remove: (index: number) => void,
move: (from: number, to: number) => void
) => {
return (
<FieldCard
form={props.form}
field={field}
index={index}
remove={remove}
remove={(index: number) => {
logger('remove %d', index)
remove(index)
}}
move={(from: number, to: number) => {
logger('move %d TO %d', from, to)
move(from, to)
}}
fields={props.fields}
onChangeFields={props.onChangeFields}
/>
@ -46,7 +61,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
}}
>
<Select value={nextType} onChange={(e) => setNextType(e)} style={{ minWidth: 200 }}>
{Object.keys(adminTypes).map((type) => (
{Object.keys(fieldTypes).map((type) => (
<Select.Option value={type} key={type}>
{t(`type:${type}.name`)}
</Select.Option>
@ -55,17 +70,14 @@ export const FieldsTab: React.FC<Props> = (props) => {
<Button
type="dashed"
onClick={() => {
const defaults: AdminFormFieldFragment = {
logicJump: {
enabled: false,
},
const defaults: FormFieldFragment = {
logic: [],
options: [],
id: `NEW-${Date.now()}`,
type: nextType,
title: '',
description: '',
required: false,
value: '',
}
add(defaults)
@ -87,7 +99,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
<Tabs.TabPane {...props}>
<Form.List name={['form', 'fields']}>
{(fields, { add, remove, move }) => {
const addAndMove = (index) => (defaults) => {
const addAndMove = (index: number) => (defaults) => {
add(defaults)
move(fields.length, index)
}
@ -98,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>

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@ -1,131 +0,0 @@
import { InfoCircleOutlined } from '@ant-design/icons/lib'
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 { Trans, useTranslation } from 'react-i18next'
import { AdminFormFieldFragment } from '../../../graphql/fragment/admin.form.fragment'
interface Props extends TabPaneProps {
form: FormInstance
fields: AdminFormFieldFragment[]
}
export const RespondentNotificationsTab: React.FC<Props> = (props) => {
const { t } = useTranslation()
const [enabled, setEnabled] = useState<boolean>()
useEffect(() => {
const next = props.form.getFieldValue(['form', 'respondentNotifications', 'enabled']) as boolean
if (next !== enabled) {
setEnabled(next)
}
}, [props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])])
useEffect(() => {
props.form
.validateFields([
['form', 'respondentNotifications', 'subject'],
['form', 'respondentNotifications', 'htmlTemplate'],
['form', 'respondentNotifications', 'toField'],
])
.catch((e: Error) => console.error('failed to validate fields', e))
}, [enabled])
const groups: {
[key: string]: AdminFormFieldFragment[]
} = {}
props.fields.forEach((field) => {
if (!groups[field.type]) {
groups[field.type] = []
}
groups[field.type].push(field)
})
return (
<Tabs.TabPane {...props}>
<Form.Item
label={t('form:respondentNotifications.enabled')}
name={['form', 'respondentNotifications', 'enabled']}
valuePropName={'checked'}
>
<Switch onChange={(e) => setEnabled(e.valueOf())} />
</Form.Item>
<Form.Item
label={t('form:respondentNotifications.subject')}
name={['form', 'respondentNotifications', 'subject']}
rules={[
{
required: enabled,
message: t('validation:subjectRequired'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('form:respondentNotifications.htmlTemplate')}
name={['form', 'respondentNotifications', 'htmlTemplate']}
rules={[
{
required: enabled,
message: t('validation:templateRequired'),
},
]}
extra={
<div>
<Trans>form:respondentNotifications.htmlTemplateInfo</Trans>
<a
href={'https://mjml.io/try-it-live'}
target={'_blank'}
rel={'noreferrer'}
style={{
marginLeft: 16,
}}
>
<InfoCircleOutlined />
</a>
</div>
}
>
<Input.TextArea autoSize />
</Form.Item>
<Form.Item
label={t('form:respondentNotifications.toField')}
name={['form', 'respondentNotifications', 'toField']}
extra={t('form:respondentNotifications.toFieldInfo')}
rules={[
{
required: enabled,
message: t('validation:emailFieldRequired'),
},
]}
>
<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={t('form:respondentNotifications.fromEmail')}
name={['form', 'respondentNotifications', 'fromEmail']}
extra={t('form:respondentNotifications.fromEmailInfo')}
>
<Input />
</Form.Item>
</Tabs.TabPane>
)
}

View File

@ -1,123 +0,0 @@
import { InfoCircleOutlined } from '@ant-design/icons/lib'
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 { Trans, useTranslation } from 'react-i18next'
import { AdminFormFieldFragment } from '../../../graphql/fragment/admin.form.fragment'
interface Props extends TabPaneProps {
form: FormInstance
fields: AdminFormFieldFragment[]
}
export const SelfNotificationsTab: React.FC<Props> = (props) => {
const { t } = useTranslation()
const [enabled, setEnabled] = useState<boolean>()
useEffect(() => {
const next = props.form.getFieldValue(['form', 'selfNotifications', 'enabled']) as boolean
if (next !== enabled) {
setEnabled(next)
}
}, [props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])])
useEffect(() => {
props.form
.validateFields([
['form', 'selfNotifications', 'subject'],
['form', 'selfNotifications', 'htmlTemplate'],
])
.catch((e: Error) => console.error('failed to validate', e))
}, [enabled])
const groups: {
[key: string]: AdminFormFieldFragment[]
} = {}
props.fields.forEach((field) => {
if (!groups[field.type]) {
groups[field.type] = []
}
groups[field.type].push(field)
})
return (
<Tabs.TabPane {...props}>
<Form.Item
label={t('form:selfNotifications.enabled')}
name={['form', 'selfNotifications', 'enabled']}
valuePropName={'checked'}
>
<Switch onChange={(e) => setEnabled(e.valueOf())} />
</Form.Item>
<Form.Item
label={t('form:selfNotifications.subject')}
name={['form', 'selfNotifications', 'subject']}
rules={[
{
required: enabled,
message: t('validation:subjectRequired'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('form:selfNotifications.htmlTemplate')}
name={['form', 'selfNotifications', 'htmlTemplate']}
rules={[
{
required: enabled,
message: t('validation:templateRequired'),
},
]}
extra={
<div>
<Trans>form:selfNotifications.htmlTemplateInfo</Trans>
<a
href={'https://mjml.io/try-it-live'}
target={'_blank'}
rel={'noreferrer'}
style={{
marginLeft: 16,
}}
>
<InfoCircleOutlined />
</a>
</div>
}
>
<Input.TextArea autoSize />
</Form.Item>
<Form.Item
label={t('form:selfNotifications.fromField')}
name={['form', 'selfNotifications', 'fromField']}
extra={t('form:selfNotifications.fromFieldInfo')}
>
<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={t('form:selfNotifications.toEmail')}
name={['form', 'selfNotifications', 'toEmail']}
extra={t('form:selfNotifications.toEmailInfo')}
>
<Input />
</Form.Item>
</Tabs.TabPane>
)
}

View File

@ -12,19 +12,25 @@ export const StartPageTab: React.FC<TabPaneProps> = (props) => {
<Tabs.TabPane {...props}>
<Form.Item
label={t('form:startPage.show')}
name={['form', 'startPage', 'show']}
name={[
'form', 'startPage', 'show',
]}
valuePropName={'checked'}
>
<Switch />
</Form.Item>
<Form.Item label={t('form:startPage.title')} name={['form', 'startPage', 'title']}>
<Form.Item label={t('form:startPage.title')} name={[
'form', 'startPage', 'title',
]}>
<Input />
</Form.Item>
<Form.Item
label={t('form:startPage.paragraph')}
name={['form', 'startPage', 'paragraph']}
name={[
'form', 'startPage', 'paragraph',
]}
extra={t('form:startPage.paragraphInfo')}
>
<Input.TextArea autoSize />
@ -32,12 +38,16 @@ export const StartPageTab: React.FC<TabPaneProps> = (props) => {
<Form.Item
label={t('form:startPage.continueButtonText')}
name={['form', 'startPage', 'buttonText']}
name={[
'form', 'startPage', 'buttonText',
]}
>
<Input />
</Form.Item>
<Form.List name={['form', 'startPage', 'buttons']}>
<Form.List name={[
'form', 'startPage', 'buttons',
]}>
{(fields, { add, remove }) => {
return (
<div>

View File

@ -2,24 +2,25 @@ 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 { t } = useTranslation()
const columns: ColumnsType<AdminPagerSubmissionEntryFieldQueryData> = [
const columns: ColumnsType<SubmissionFieldFragment> = [
{
title: t('submission:field'),
render(row: AdminPagerSubmissionEntryFieldQueryData) {
render(_, row) {
if (row.field) {
return `${row.field.title}${row.field.required ? '*' : ''}`
}
@ -29,11 +30,9 @@ export const SubmissionValues: React.FC<Props> = (props) => {
},
{
title: t('submission:value'),
render(row: AdminPagerSubmissionEntryFieldQueryData) {
render(_, row) {
try {
const data = JSON.parse(row.value) as { value: string }
return data.value
return fieldTypes[row.type]?.displayValue(row.value)
} catch (e) {
return row.value
}

View File

@ -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,
}

View File

@ -1,24 +0,0 @@
import { Form, InputNumber } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
export const NumberType: React.FC<AdminFieldTypeProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:number:default')}
name={[props.field.name as string, 'value']}
labelCol={{ span: 6 }}
getValueFromEvent={(value: number) =>
typeof value === 'number' ? value.toFixed(2) : value
}
getValueProps={(value: string) => ({ value: value ? parseFloat(value) : undefined })}
>
<InputNumber precision={2} />
</Form.Item>
</div>
)
}

View File

@ -1,26 +0,0 @@
import { Form, Rate } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
export const RatingType: React.FC<AdminFieldTypeProps> = (props) => {
const { t } = useTranslation()
// TODO add ratings
return (
<div>
<Form.Item
label={t('type:rating:default')}
name={[props.field.name as string, 'value']}
labelCol={{ span: 6 }}
extra={t('type:rating.clearNote')}
getValueFromEvent={(value: number) =>
typeof value === 'number' ? value.toFixed(2) : value
}
getValueProps={(value: string) => ({ value: value ? parseFloat(value) : undefined })}
>
<Rate allowHalf allowClear />
</Form.Item>
</div>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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
}

View 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>
)
}

View File

@ -1,18 +1,21 @@
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 { StyledMarkdown } from '../styled/markdown'
import { useRouter } from '../use.router'
import { fieldTypes } from './types'
import { TextType } from './types/text.type'
import { FieldTypeProps } from './types/type.props'
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 {
field: FormFieldFragment
design: FormDesignFragment
focus: boolean
field: FormPublicFieldFragment
design: FormPublicDesignFragment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
save: (data: any) => void
@ -23,8 +26,9 @@ interface Props {
export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...props }) => {
const [form] = useForm()
const router = useRouter()
const { t } = useTranslation()
const FieldInput: React.FC<FieldTypeProps> = fieldTypes[field.type] || TextType
const FieldInput = (fieldTypes[field.type] || fieldTypes[field.type]).inputFormField()
const finish = (data) => {
console.log('received field data', data)
@ -57,6 +61,7 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<div
@ -72,10 +77,15 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
{field.title}
</StyledH1>
{field.description && (
<StyledMarkdown design={design} type={'question'} source={field.description} />
<StyledMarkdown design={design} type={'question'}>{field.description}</StyledMarkdown>
)}
<FieldInput design={design} field={field} urlValue={getUrlDefault()} />
<FieldInput
design={design}
field={field}
focus={props.focus}
urlValue={getUrlDefault()}
/>
</div>
<div
style={{
@ -84,24 +94,24 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
}}
>
<StyledButton
background={design.colors.buttonColor}
color={design.colors.buttonTextColor}
highlight={design.colors.buttonActiveColor}
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
onClick={prev}
>
{'Previous'}
{t('form:previous')}
</StyledButton>
<div style={{ flex: 1 }} />
<StyledButton
background={design.colors.buttonColor}
color={design.colors.buttonTextColor}
highlight={design.colors.buttonActiveColor}
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
size={'large'}
onClick={form.submit}
>
{'Next'}
{t('form:next')}
</StyledButton>
</div>
</Form>

View 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>
)
}

View File

@ -1,6 +1,7 @@
.main {
display: flex;
flex-direction: column;
height: 100%;
.content {
flex: 1;

View 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>
)
}

View File

@ -1,73 +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 { StyledMarkdown } from '../styled/markdown'
import scss from './page.module.scss'
interface Props {
type: 'start' | 'end'
page: FormPageFragment
design: FormDesignFragment
className?: string
next: () => void
prev: () => void
}
export const FormPage: React.FC<Props> = ({ page, design, next, prev, className, ...props }) => {
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'} source={page.paragraph} />
</div>
<div
style={{
padding: 32,
display: 'flex',
}}
>
{prev && <div />}
{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'}
rel={'noreferrer'}
>
{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>
)
}

View 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>
}

View 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 CheckboxAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:checkbox:default')}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
<Form.List name={[props.field.name as string, 'options']}>
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item
wrapperCol={{
sm: { offset: index === 0 ? 0 : 6 },
}}
labelCol={{ span: 6 }}
label={index === 0 ? t('type:checkbox:options') : ''}
key={field.key}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
wrapperCol={{ span: 24 }}
name={[field.name, 'title']}
style={{ marginBottom: 0 }}
>
<Input placeholder={t('type:checkbox:titlePlaceholder')} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
wrapperCol={{ span: 24 }}
name={[field.name, 'value']}
style={{ marginBottom: 0 }}
rules={[{ required: true, message: t('validation:valueRequired') }]}
>
<Input placeholder={t('type:checkbox:valuePlaceholder')} />
</Form.Item>
</Col>
<Col span={4}>
<Button danger onClick={() => remove(index)}>
{t('type:checkbox:removeOption')}
</Button>
</Col>
</Row>
</Form.Item>
))}
<Form.Item
wrapperCol={{
sm: { offset: 6 },
}}
labelCol={{ span: 6 }}
>
<Button type={'dashed'} onClick={() => add()}>
{t('type:checkbox:addOption')}
</Button>
</Form.Item>
</div>
)
}}
</Form.List>
</div>
)
}

View 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>
)
}

View 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)));
}
}

View File

@ -2,16 +2,16 @@ import { DatePicker, Form } from 'antd'
import moment, { Moment } from 'moment'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const DateType: React.FC<AdminFieldTypeProps> = ({ field }) => {
export const DateAdmin: React.FC<FieldAdminProps> = ({ field }) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:date.default')}
name={[field.name as string, 'value']}
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 })}
@ -20,7 +20,11 @@ export const DateType: React.FC<AdminFieldTypeProps> = ({ field }) => {
</Form.Item>
<Form.Item
label={t('type:date.min')}
name={[field.name as string, 'optionKeys', '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 })}
@ -30,7 +34,11 @@ export const DateType: React.FC<AdminFieldTypeProps> = ({ field }) => {
<Form.Item
label={t('type:date.max')}
name={[field.name as string, 'optionKeys', '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 })}

View File

@ -1,12 +1,23 @@
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 { FieldTypeProps } from './type.props'
import { StyledDateInput } from '../../../styled/date.input'
import { FieldInputBuilderType } from '../field.input.builder.type'
export const DateType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
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()
@ -24,33 +35,40 @@ export const DateType: React.FC<FieldTypeProps> = ({ field, design, urlValue })
let initialValue: Moment = undefined
if (field.value) {
initialValue = moment(field.value)
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = moment(field.value)
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id, 'value']}
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
}}
/>

View 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)));
}
}

View File

@ -1,16 +1,16 @@
import { Button, Col, Form, Input, Row } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const DropdownType: React.FC<AdminFieldTypeProps> = (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, 'value']}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
>
<Input />

View File

@ -1,21 +1,47 @@
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 { FieldTypeProps } from './type.props'
import { StyledSelect } from '../../../styled/select'
import { FieldInputBuilderType } from '../field.input.builder.type'
export const DropdownType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
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, 'value']}
name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={urlValue || field.value || null}
initialValue={initialValue}
>
<StyledSelect
autoFocus={focus}
design={design}
open={open}
onBlur={() => setOpen(false)}

View 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)));
}
}

View File

@ -1,24 +0,0 @@
import { Form } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledInput } from '../../styled/input'
import { FieldTypeProps } from './type.props'
export const EmailType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ required: field.required, message: t('validation:valueRequired') },
{ type: 'email', message: t('validation:invalidEmail') },
]}
initialValue={urlValue || field.value}
>
<StyledInput design={design} allowClear size={'large'} />
</Form.Item>
</div>
)
}

View File

@ -1,16 +1,16 @@
import { Form, Input } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const EmailType: React.FC<AdminFieldTypeProps> = (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, 'value']}
name={[props.field.name as string, 'defaultValue']}
rules={[{ type: 'email', message: t('validation:emailRequired') }]}
labelCol={{ span: 6 }}
>

View 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>
)
}

View 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)));
}
}

View File

@ -1,7 +1,7 @@
import { FormInstance } from 'antd/lib/form'
import { FieldData } from 'rc-field-form/lib/interface'
export interface AdminFieldTypeProps {
export interface FieldAdminProps {
form: FormInstance
field: FieldData
}

View 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>

View 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
}

View File

@ -1,16 +1,16 @@
import { Form, Input } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const HiddenType: React.FC<AdminFieldTypeProps> = (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, 'value']}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
>
<Input />

View 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;
}
}

View File

@ -0,0 +1,2 @@
export {}

View File

@ -0,0 +1,2 @@
export {}

View 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 {}

View File

@ -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(),
}

View File

@ -1,24 +0,0 @@
import { Form } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledInput } from '../../styled/input'
import { FieldTypeProps } from './type.props'
export const LinkType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ required: field.required, message: t('validation:valueRequired') },
{ type: 'url', message: t('validation:invalidUrl') },
]}
initialValue={urlValue || field.value}
>
<StyledInput design={design} allowClear size={'large'} />
</Form.Item>
</div>
)
}

View 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)));
}
}

View File

@ -1,16 +1,16 @@
import { Form, Input } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const LinkType: React.FC<AdminFieldTypeProps> = (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, 'value']}
name={[props.field.name as string, 'defaultValue']}
rules={[{ type: 'url', message: t('validation:invalidUrl') }]}
labelCol={{ span: 6 }}
>

View 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>
)
}

View 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}`
}
}

View 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='&copy; <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>
)
}

View 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='&copy; <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>
)
}

View File

@ -1,34 +0,0 @@
import { Form } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledNumberInput } from '../../styled/number.input'
import { FieldTypeProps } from './type.props'
export const NumberType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
const { t } = useTranslation()
let initialValue: number = undefined
if (field.value) {
initialValue = parseFloat(field.value)
}
if (urlValue) {
initialValue = parseFloat(urlValue)
}
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ type: 'number', message: t('validation:invalidNumber') },
{ required: field.required, message: t('validation:valueRequired') },
]}
initialValue={initialValue}
>
<StyledNumberInput design={design} size={'large'} />
</Form.Item>
</div>
)
}

View 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)));
}
}

View 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>
)
}

View 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>
)
}

View 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)));
}
}

View File

@ -1,16 +1,16 @@
import { Button, Col, Form, Input, Row } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const RadioType: React.FC<AdminFieldTypeProps> = (props) => {
export const RadioAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:radio:default')}
name={[props.field.name as string, 'value']}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
>
<Input />

View File

@ -1,26 +1,40 @@
import { Form, Radio } from 'antd'
import debug from 'debug'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledRadio } from '../../styled/radio'
import { FieldTypeProps } from './type.props'
import { StyledRadio } from '../../../styled/radio'
import { FieldInputBuilderType } from '../field.input.builder.type'
export const RadioType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
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.value) {
initialValue = field.value
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = urlValue
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id, 'value']}
name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={field.options
.map((option) => option.value)

View File

@ -1,31 +0,0 @@
import { Form, Rate } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldTypeProps } from './type.props'
export const RatingType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
const { t } = useTranslation()
// TODO add ratings
let initialValue: number = undefined
if (field.value) {
initialValue = parseFloat(field.value)
}
if (urlValue) {
initialValue = parseFloat(urlValue)
}
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue}
>
<Rate allowHalf />
</Form.Item>
</div>
)
}

View 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)));
}
}

View 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>
)
}

View 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>
)
}

View 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)));
}
}

View File

@ -0,0 +1,88 @@
import { Form, InputNumber, Slider } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldAdminProps } from '../field.admin.props'
export const SliderAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item shouldUpdate noStyle>
{(form) => {
//const prefix = React.useContext(FormItemContext).prefixName
const prefix = (form as any).prefixName
const getValue = (name, defaultValue: number): number => {
const current: unknown = form.getFieldValue([
...prefix,
props.field.name as string,
'optionKeys',
name,
])
if (!current) {
return defaultValue
}
return parseFloat(current as string)
}
const max = getValue('max', 100)
const min = getValue('min', 0)
const step = getValue('step', 1)
return (
<Form.Item
label={t('type:slider.default')}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
getValueProps={(value: string) => ({ value: value ? parseFloat(value) : undefined })}
>
<Slider min={min} max={max} step={step} dots={(max - min) / step <= 10} />
</Form.Item>
)
}}
</Form.Item>
<Form.Item
label={t('type:slider.min')}
name={[
props.field.name as string,
'optionKeys',
'min',
]}
labelCol={{ span: 6 }}
initialValue={0}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t('type:slider.max')}
name={[
props.field.name as string,
'optionKeys',
'max',
]}
labelCol={{ span: 6 }}
initialValue={100}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t('type:slider.step')}
name={[
props.field.name as string,
'optionKeys',
'step',
]}
labelCol={{ span: 6 }}
initialValue={1}
>
<InputNumber />
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,92 @@
import { Form, Slider, Spin } from 'antd'
import debug from 'debug'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('slider.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function SliderInput ({
field,
urlValue,
focus,
}) {
const [min, setMin] = useState<number>()
const [max, setMax] = useState<number>()
const [step, setStep] = useState<number>()
const [loading, setLoading] = useState(true)
const { t } = useTranslation()
useEffect(() => {
field.options.forEach((option) => {
if (option.key === 'min') {
try {
setMin(JSON.parse(option.value))
} catch (e) {
logger('invalid min value %O', e)
}
}
if (option.key === 'max') {
try {
setMax(JSON.parse(option.value))
} catch (e) {
logger('invalid max value %O', e)
}
}
if (option.key === 'step') {
try {
setStep(JSON.parse(option.value))
} catch (e) {
logger('invalid step value %O', e)
}
}
})
setLoading(false)
}, [field])
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)
}
if (loading) {
return (
<div>
<Spin />
</div>
)
}
return (
<div>
<Form.Item
name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue}
>
<Slider
autoFocus={focus}
min={min}
max={max}
step={step}
tooltipVisible={true}
dots={(max - min) / step <= 10}
/>
</Form.Item>
</div>
)
}

View File

@ -1,22 +0,0 @@
import { Form } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledInput } from '../../styled/input'
import { FieldTypeProps } from './type.props'
export const TextType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
const { t } = useTranslation()
// TODO focus when becomes visible
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={urlValue || field.value}
>
<StyledInput design={design} allowClear size={'large'} />
</Form.Item>
</div>
)
}

View File

@ -1,21 +0,0 @@
import { Form } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledTextareaInput } from '../../styled/textarea.input'
import { FieldTypeProps } from './type.props'
export const TextareaType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={urlValue || field.value}
>
<StyledTextareaInput design={design} allowClear autoSize />
</Form.Item>
</div>
)
}

View 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 TextareaType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./textarea.admin').then(c => c.TextareaAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./textarea.input').then(c => c.builder(this)));
}
}

View File

@ -1,16 +1,16 @@
import { Form, Input } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const TextareaType: React.FC<AdminFieldTypeProps> = (props) => {
export const TextareaAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:textarea:default')}
name={[props.field.name as string, 'value']}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
>
<Input.TextArea autoSize />

View File

@ -0,0 +1,46 @@
import { Form } from 'antd'
import debug from 'debug'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledTextareaInput } from '../../../styled/textarea.input'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('textarea.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function TextareaInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation()
let initialValue = 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}
>
<StyledTextareaInput autoFocus={focus} design={design} allowClear autoSize />
</Form.Item>
</div>
)
}

View 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 TextfieldType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./textfield.admin').then(c => c.TextfieldAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./textfield.input').then(c => c.builder(this)));
}
}

View File

@ -1,15 +1,15 @@
import { Form, Input } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const TextType: React.FC<AdminFieldTypeProps> = (props) => {
export const TextfieldAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<Form.Item
label={t('type:textfield:default')}
name={[props.field.name as string, 'value']}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
>
<Input />

View File

@ -0,0 +1,52 @@
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('textfield.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function TextfieldInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation()
// TODO focus when becomes visible
let initialValue = 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}
>
<StyledInput
autoFocus={focus}
design={design}
allowClear
size={'large'}
/>
</Form.Item>
</div>
)
}

View File

@ -1,7 +0,0 @@
import { FormDesignFragment, FormFieldFragment } from '../../../graphql/fragment/form.fragment'
export interface FieldTypeProps {
field: FormFieldFragment
design: FormDesignFragment
urlValue?: string
}

View File

@ -1,29 +0,0 @@
import { Form, Switch } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldTypeProps } from './type.props'
export const YesNoType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
const { t } = useTranslation()
let initialValue = !!field.value
if (urlValue !== undefined) {
initialValue = !!urlValue
}
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue}
valuePropName={'checked'}
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
getValueProps={(e: string) => ({ checked: !!e })}
>
<Switch />
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,36 @@
import { Tag } from 'antd'
import dynamic from 'next/dynamic'
import React, { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class YesNoType extends AbstractType<boolean> {
parseUrlValue(raw: string): boolean {
return !!raw
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./yes_no.admin').then(c => c.YesNoAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./yes_no.input').then(c => c.builder(this)));
}
stringifyValue(raw: string): string {
if (this.parseValue(raw)) {
return 'YES'
} else {
return 'NO'
}
}
displayValue(raw: string): JSX.Element {
if (this.parseValue(raw)) {
return <Tag color={'green'}>YES</Tag>
} else {
return <Tag color={'red'}>NO</Tag>
}
}
}

View File

@ -1,21 +1,18 @@
import { Form, Switch } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const YesNoType: React.FC<AdminFieldTypeProps> = (props) => {
export const YesNoAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
// TODO add switch
return (
<div>
<Form.Item
label={t('type:yes_no:default')}
name={[props.field.name as string, 'value']}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
valuePropName={'checked'}
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
getValueProps={(e: string) => ({ checked: !!e })}
>
<Switch />
</Form.Item>

View File

@ -0,0 +1,45 @@
import { Form, Switch } 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('yes_no.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function YesNoInput ({
field,
urlValue,
}) {
const { t } = useTranslation()
let initialValue: boolean = undefined
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue !== undefined) {
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue}
valuePropName={'checked'}
>
<Switch />
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,36 @@
import L from 'leaflet'
import React, { FC, useMemo, useRef } from 'react'
import { Marker } from 'react-leaflet'
interface Props {
value: { lat: number, lng: number }
onChange: (value: { lat: number, lng: number }) => void
}
export const DraggableMarker: FC<Props> = (props) => {
const markerRef = useRef<L.Marker>(null)
const eventHandlers = useMemo(
() => ({
dragend() {
const marker = markerRef.current
if (marker != null) {
props.onChange(marker.getLatLng())
}
},
}),
[],
)
return (
<Marker
draggable={true}
eventHandlers={eventHandlers}
position={props.value}
ref={markerRef}
icon={L.icon({
iconUrl: require('assets/images/marker-icon-2x.png'),
iconSize: [50/2, 82/2],
iconAnchor: [50 / 4, 82/2],
})}
/>
)
}

View File

@ -1,10 +1,9 @@
import { useQuery } from '@apollo/react-hooks'
import React from 'react'
import { SETTINGS_QUERY, SettingsQueryData } from '../graphql/query/settings.query'
import { useSettingsQuery } from '../graphql/query/settings.query'
import scss from './omf.module.scss'
export const Omf: React.FC = () => {
const { data, loading } = useQuery<SettingsQueryData>(SETTINGS_QUERY)
const { data, loading } = useSettingsQuery()
if (loading || (data && data.hideContrib.value)) {
return null

View File

@ -30,6 +30,7 @@ export const sideMenu: SideMenuElement[] = [
key: 'public',
name: 'admin:forms',
group: true,
role: 'admin',
items: [
{
key: 'forms',

Some files were not shown because too many files have changed in this diff Show More