Compare commits

..

No commits in common. "master" and "0.9.2" have entirely different histories.

459 changed files with 8642 additions and 16793 deletions

View File

@ -1,74 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: { jsx: true },
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
plugins: [
'@typescript-eslint/eslint-plugin',
'@typescript-eslint',
'unused-imports'
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:react/recommended',
'plugin:jsx-a11y/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier',
],
rules: {
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'react/prop-types': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'jsx-a11y/no-autofocus': 'off',
'array-element-newline': ['error', {
'ArrayExpression': 'consistent',
'ArrayPattern': {
'minItems': 3,
'multiline': true,
}
}],
'array-bracket-newline': ['error', {
'minItems': 3,
'multiline': true,
}],
'indent': [
'error',
2,
{
'SwitchCase': 1
}
],
'no-tabs': ['error'],
'max-len': ['error', {
'code': 100,
'ignoreComments': true,
'ignoreUrls': true,
'ignoreTemplateLiterals': true,
'ignoreTrailingComments': true,
'ignoreStrings': true,
}],
'quotes': ['error', 'single', { 'avoidEscape': true }],
'comma-dangle': ['error', 'always-multiline'],
'linebreak-style': [
'error',
'unix'
],
'no-trailing-spaces': 'error',
'eol-last': 'error',
'unused-imports/no-unused-imports': 'error',
},
settings: {
react: {
version: 'detect',
},
},
}

View File

@ -1,43 +0,0 @@
name: Docker Image CI
on:
push:
branches:
- master
release:
types:
- published
jobs:
build:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ohmyform/ui
tags: |
type=raw,value=latest
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}}
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,37 +0,0 @@
name: Lint
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 16
# ESLint and Prettier must be in `package.json`
- name: Install Node.js dependencies
run: yarn install --frozen-lockfile --silent
- name: Lint
uses: reviewdog/action-eslint@v1
with:
reporter: github-pr-review # Change reporter.
eslint_flags: 'pages/ store/ components/ graphql/'
- name: Typecheck
uses: andoshin11/typescript-error-reporter-action@v1.0.2

2
.gitignore vendored
View File

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

View File

@ -1,8 +0,0 @@
module.exports = {
semi: false,
trailingComma: 'es5',
singleQuote: true,
printWidth: 100,
tabWidth: 2,
useTabs: false,
}

View File

@ -1,249 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
<!--
Template for next version
## [Unreleased]
### Added
### Changed
### Fixed
### Security
-->
## [Unreleased]
### Added
### Changed
### Fixed
- node prune location (https://github.com/ohmyform/ohmyform/issues/184)
### Security
## [1.0.3] - 2022-03-27
### Changed
- default form now has an end page
### Fixed
- sorting of fields in excel export
## [1.0.2] - 2022-03-13
### Fixed
- field sort in excel submission export (https://github.com/ohmyform/ohmyform/issues/163)
## [1.0.1] - 2022-03-01
### Added
- map field type
### Changed
- update translations (https://github.com/ohmyform/ui/pull/70)
- show warning icon in form list if not public
- default form layout is now "card"
- creating of new fields combined in new field types
### Fixed
- locale scripts were missing dependency
- edit user shows now email in title
- focus is now passed also do slide layout fields
- empty fields are no longer submitted
- stuttery form because of logic rerenders
## [1.0.0] - 2022-02-28
### Added
- ability to change user passwords
- add default page background
- add environment list in [doc](doc/environment.md)
- show error message on homepage in case there is a problem with api connection
- new slider field type
- new card layout for forms
- field logic
- add environment config
- anonymous form submissions (fixes https://github.com/ohmyform/ohmyform/issues/108)
- checkbox field type (fixed https://github.com/ohmyform/ohmyform/issues/138)
### Changed
- combined notificationts to become more versatile
- use exported hooks for graphql
- disable swipe gesture
- upgrade to nextjs 12
- change default value from value to defaultValue
- handle options and values as json correctly
- exclude empty submissions per default (https://github.com/ohmyform/ohmyform/issues/153)
### Fixed
- links at the bottom for new users
- fixes for hide contrib setting
- fix problem with node-prune on production build
- translation for prev / continue during form submission
- reload form list after adding new one (https://github.com/ohmyform/ohmyform/issues/139)
- android screen size fix (https://github.com/ohmyform/ohmyform/issues/114)
- sending finish mutation (https://github.com/ohmyform/ui/pull/67)
- fix dev documentation (https://github.com/ohmyform/ui/issues/65)
- remove next/image as it does not work with static exports (https://github.com/ohmyform/ohmyform/issues/154)
- switch back to form.prefixName (https://github.com/ohmyform/ohmyform/issues/150)
- upgrade all packages to latest versions
### Security
- upgrad all packages
## [0.9.9] - 2021-02-14
### Added
- Submission export
- Lokalize reference
### Changed
- updated french translations by @Vercety87
- upgrade to node 14 (https://github.com/ohmyform/ohmyform/issues/99)
### Fixed
- missing dependency to @apollo/client
- footer rendering during authentication check
### Security
- authentication check for profile page
## [0.9.8] - 2020-09-02
### Fixed
- menu selection type
### Security
## [0.9.7] - 2020-09-02
### Changed
- improved german translation (https://github.com/ohmyform/ui/pull/28)
### Fixed
- colors for landing page buttons
### Security
- upgraded dependencies
## [0.9.6] - 2020-07-17
### Added
- slug for fields to be able to set value by url parameter
- form submission hokks
### Changed
- minify containers to reduce layer size
### Fixed
- do not show login note if it is not set
- typo in dropdown options https://github.com/ohmyform/ohmyform/issues/96
- query parms are not parsed https://github.com/ohmyform/ui/pull/27 https://github.com/ohmyform/ohmyform/issues/100
- errors because of missing user reference (https://github.com/ohmyform/ohmyform/issues/102)
### Security
- container now runs as non root user
## [0.9.5] - 2020-06-10
### Added
- mobile improvements for lists and home page
- markdown support for page paragraphs and field description
- hideable omf badge
- login notes
- username in admin toolbar
- github stars in multiple places
### Changed
- verified spanish translations https://github.com/ohmyform/ui/pull/23
### Fixed
- yes / no field fixed on admin and user view
- prev property error on div
- rating field default on admin
- number field defaults
- translations for field validation
- number validation
- side menu only shows accessible entries
## [0.9.4] - 2020-06-09
### Added
- Fetch Server Settings to determine if signup is available
- `SPA` env variable to have static page with loading spinner before redirect
- `de`, `fr`, `es`, `it`, `cn` base folders for translations
- finish translating `de` and `en`
- add `yarn translation:sort` to order translations (to ensure the same order
when we add / change translations)
- add `yarn translation:missing <lang>` to print a list of missing translations
for the given language (this takes `en` as a baseline)
- travis for tests
- eslint with prettier
### Changed
- `export` uses now spa mode for initial loading screen
- change value to defaultValue for initial form
### Fixed
- dropdown options are not saved (https://github.com/ohmyform/ohmyform/issues/93)
- redirect attempts on static export
- date can now be prefilled by url
## [0.9.2] - 2020-06-04
### Fixed
- type error
## [0.9.1] - 2020-06-02
### Added
- radio fields
- dropdown fields
- min and max for date fields
- logout on home screen
- translation system
### Fixed
- initial Page is now correct also in SPA mode
- initial value for form adding
- anonymous submission of forms

View File

@ -1,39 +1,12 @@
FROM node:14-alpine AS builder FROM node:12-alpine
MAINTAINER OhMyForm <admin@ohmyform.com>
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apk --update --no-cache add curl bash g++ make libpng-dev
# install node-prune (https://github.com/tj/node-prune)
RUN curl -sf https://gobinaries.com/tj/node-prune | sh
COPY . ./ COPY . ./
RUN yarn install --frozen-lock-file RUN yarn install --frozen-lock-file
RUN yarn build RUN yarn build
# remove development dependencies ENV PORT=4000
RUN npm prune --production
# run node prune
# there is some problem running node prune that then prevents the frontend to load (just start with /form/1 and it will crash)
#RUN /usr/local/bin/node-prune
FROM node:14-alpine
MAINTAINER OhMyForm <admin@ohmyform.com>
# Create a group and a user with name "ohmyform".
RUN addgroup --gid 9999 ohmyform && adduser -D --uid 9999 -G ohmyform ohmyform
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app /usr/src/app
ENV PORT=4000 \
NODE_ENV=production
# Change to non-root privilege
USER ohmyform
CMD [ "yarn", "start" ] CMD [ "yarn", "start" ]

View File

@ -1,17 +1,7 @@
# OhMyForm UI # OhMyForm UI
[![Build Status](https://travis-ci.org/ohmyform/ui.svg?branch=master)](https://travis-ci.org/ohmyform/ui) [![Financial Contributors on Open Collective](https://opencollective.com/ohmyform-sustainability/all/badge.svg?label=financial+contributors)](https://opencollective.com/ohmyform-sustainability)
![Latest Release](https://badgen.net/github/tag/ohmyform/ui) ![Project Status](https://img.shields.io/badge/status-0.9.0-green.svg)
[![Docker Pulls](https://badgen.net/docker/pulls/ohmyform/ui)](https://hub.docker.com/r/ohmyform/ui)
[![Lokalise](https://badgen.net/badge/Lokalise/EN/green?icon=libraries)](https://app.lokalise.com/public/379418475ede5d5c6937b0.31012044/)
![Last Commit](https://badgen.net/github/last-commit/ohmyform/ui)
[Demo](https://demo.ohmyform.com/login)
> An *open source alternative to TypeForm* that can create stunning mobile-ready forms, surveys and questionnaires.
[![Discord](https://img.shields.io/discord/595773457862492190.svg?label=Discord%20Chat)](https://discord.gg/MJqAuAZ) [![Discord](https://img.shields.io/discord/595773457862492190.svg?label=Discord%20Chat)](https://discord.gg/MJqAuAZ)
[![Financial Contributors on Open Collective](https://opencollective.com/ohmyform-sustainability/all/badge.svg?label=financial+contributors)](https://opencollective.com/ohmyform-sustainability) > An *open source alternative to TypeForm* that can create stunning mobile-ready forms, surveys and questionnaires.

View File

@ -1,7 +1,5 @@
@import "variables"; @import "variables";
@import "node_modules/swiper/swiper.scss"; @import "node_modules/swiper/swiper.scss";
@import "../node_modules/react-github-button/assets/style.css";
@import "../node_modules/leaflet/dist/leaflet.css";
:root { :root {
--backgroundColor: #{$background-color}; --backgroundColor: #{$background-color};
@ -26,33 +24,14 @@
} }
} }
.full-height {
height: 100vh;
height: calc(var(--vh, 1vh) * 100);
}
.ant-spin-nested-loading > div > .ant-spin { .ant-spin-nested-loading > div > .ant-spin {
max-height: unset; max-height: unset;
} }
.swiper-container { .swiper-container {
height: 100vh; height: 100vh;
height: calc(var(--vh, 1vh) * 100);
.swiper-wrapper { .swiper-wrapper {
position: fixed position: fixed
} }
} }
.admin {
.sidemenu {
.ant-layout-sider-children {
display: flex;
flex-direction: column;
.language-selector {
padding-left: 12px !important;
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 B

View File

@ -1,18 +0,0 @@
.footer {
position: absolute;
padding-left: 16px;
margin-bottom: 4px;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 600px) {
position: relative;
margin-bottom: 16px;
flex-direction: column;
text-align: center;
}
}

View File

@ -1,26 +1,20 @@
import { Button, Select } from 'antd' import {Button} from 'antd'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import {useRouter} from 'next/router'
import React from 'react' import React from 'react'
import GitHubButton from 'react-github-button' import {useTranslation} from 'react-i18next'
import { useTranslation } from 'react-i18next' import {clearAuth, withAuth} from '../with.auth'
import { useSettingsQuery } from '../../graphql/query/settings.query'
import { languages } from '../../i18n'
import { clearAuth, withAuth } from '../with.auth'
import scss from './footer.module.scss'
interface Props { interface Props {
me?: { me?: {
id: string id: string
username: string username: string
roles: string[]
} }
} }
const AuthFooterInner: React.FC<Props> = (props) => { const AuthFooterInner: React.FC<Props> = props => {
const { t, i18n } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const { data, loading } = useSettingsQuery()
const logout = () => { const logout = () => {
clearAuth() clearAuth()
@ -28,115 +22,71 @@ const AuthFooterInner: React.FC<Props> = (props) => {
} }
return ( return (
<footer className={scss.footer}> <div
{props.me style={{
? [ position: 'absolute',
<span style={{ color: '#FFF' }} key={'user'}> bottom: 0,
Hi, {props.me.username} left: 0,
</span>, right: 0,
props.me.roles.includes('admin') && ( }}
<Link key={'admin'} href={'/admin'}> >
<Link href={'/admin'}>
<Button <Button
type={'link'} type={'link'}
style={{ ghost
color: '#FFF',
}}
> >
{t('admin')} {t('admin')}
</Button> </Button>
</Link> </Link>
), {props.me ? (
<Link key={'profile'} href={'/admin/profile'}> [
<span style={{color: '#FFF'}} key={'user'}>
Hi, {props.me.username}
</span>,
<Button <Button
key={'Logout'}
type={'link'} type={'link'}
style={{ ghost
color: '#FFF',
}}
>
{t('profile')}
</Button>
</Link>,
<Button
key={'logout'}
type={'link'}
onClick={logout} onClick={logout}
style={{
color: '#FFF',
}}
> >
{t('logout')} {t('logout')}
</Button>, </Button>
] ]
: [ ): (
[
<Link href={'/login'} key={'login'}> <Link href={'/login'} key={'login'}>
<Button <Button
type={'link'} type={'link'}
style={{ ghost
color: '#FFF',
}}
> >
{t('login')} {t('login')}
</Button> </Button>
</Link>, </Link>,
!loading && !data?.disabledSignUp.value && (
<Link href={'/register'} key={'register'}> <Link href={'/register'} key={'register'}>
<Button <Button
type={'link'} type={'link'}
style={{ ghost
color: '#FFF',
}}
> >
{t('register')} {t('register')}
</Button> </Button>
</Link> </Link>
), ]
]} )}
<div style={{ flex: 1 }} />
<Select
bordered={false}
value={i18n.language.replace(/-.*/, '')}
onChange={(next) => i18n.changeLanguage(next)}
style={{
color: '#FFF',
paddingLeft: 18,
}}
suffixIcon={false}
>
{languages.map((language) => (
<Select.Option value={language} key={language}>
{t(`language:${language}`)}
</Select.Option>
))}
</Select>
{!loading && !data?.hideContrib.value && (
<>
<GitHubButton type="stargazers" namespace="ohmyform" repo="ohmyform" />
<Button <Button
type={'link'} type={'link'}
target={'_blank'} target={'_blank'}
rel={'noreferrer'} ghost
href={'https://www.ohmyform.com'} href={'https://www.ohmyform.com'}
style={{ style={{
color: '#FFF', float: 'right',
color: '#FFF'
}} }}
> >
OhMyForm &copy; OhMyForm
</Button> </Button>
<Button </div>
type={'link'}
target={'_blank'}
rel={'noreferrer'}
href={'https://lokalise.com/'}
style={{
color: '#FFF',
}}
>
translated with Lokalize
</Button>
</>
)}
</footer>
) )
} }
export const AuthFooter = withAuth(AuthFooterInner, [], true) export const AuthFooter = withAuth(AuthFooterInner)

View File

@ -1,23 +1,17 @@
import { Layout, Spin } from 'antd' import {Layout, Spin} from 'antd'
import getConfig from 'next/config'
import React from 'react' import React from 'react'
import { NextConfigType } from '../../next.config.type'
const { publicRuntimeConfig } = getConfig() as NextConfigType
interface Props { interface Props {
loading?: boolean loading?: boolean
} }
export const AuthLayout: React.FC<Props> = (props) => { export const AuthLayout: React.FC<Props> = props => {
return ( return (
<Spin spinning={props.loading || false}> <Spin spinning={props.loading}>
<Layout <Layout style={{
style={{
height: '100vh', height: '100vh',
background: publicRuntimeConfig.mainBackground, background: '#437fdc'
}} }}>
>
{props.children} {props.children}
</Layout> </Layout>
</Spin> </Spin>

View File

@ -1,4 +1,4 @@
/* eslint-disable */
const omitDeepArrayWalk = (arr, key) => { const omitDeepArrayWalk = (arr, key) => {
return arr.map((val) => { return arr.map((val) => {
if (Array.isArray(val)) return omitDeepArrayWalk(val, key) if (Array.isArray(val)) return omitDeepArrayWalk(val, key)
@ -8,18 +8,18 @@ const omitDeepArrayWalk = (arr, key) => {
} }
const omitDeep = (obj: any, key: string | number): any => { const omitDeep = (obj: any, key: string | number): any => {
const keys: Array<any> = Object.keys(obj) const keys: Array<any> = Object.keys(obj);
const newObj: any = {} const newObj: any = {};
keys.forEach((i: any) => { keys.forEach((i: any) => {
if (i !== key) { if (i !== key) {
const val: any = obj[i] const val: any = obj[i];
if (val instanceof Date) newObj[i] = val if (val instanceof Date) newObj[i] = val;
else if (Array.isArray(val)) newObj[i] = omitDeepArrayWalk(val, key) else if (Array.isArray(val)) newObj[i] = omitDeepArrayWalk(val, key);
else if (typeof val === 'object' && val !== null) newObj[i] = omitDeep(val, key) else if (typeof val === 'object' && val !== null) newObj[i] = omitDeep(val, key);
else newObj[i] = val else newObj[i] = val;
} }
}) });
return newObj return newObj;
} }
export const cleanInput = <T>(obj: T): T => { export const cleanInput = <T>(obj: T): T => {

View File

@ -7,15 +7,13 @@ interface Props {
hideTime?: boolean hideTime?: boolean
} }
export const DateTime: React.FC<Props> = (props) => { export const DateTime: React.FC<Props> = props => {
const format = props.hideTime ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm' const format = props.hideTime ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm'
return ( return (
<div <div style={{
style={{ display: 'inline-block'
display: 'inline-block', }}>
}}
>
{dayjs(props.date).format(format)} {dayjs(props.date).format(format)}
</div> </div>
) )

View File

@ -2,15 +2,13 @@ import React from 'react'
export const ErrorPage: React.FC = () => { export const ErrorPage: React.FC = () => {
return ( return (
<div <div style={{
style={{
height: '100vh', height: '100vh',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}} }}>
>
<h1>ERROR</h1> <h1>ERROR</h1>
<p>there was an error with your request</p> <p>there was an error with your request</p>
</div> </div>

View File

@ -1,16 +1,13 @@
import { Form, Input, Select, Switch, Tabs } from 'antd' import {Form, Input, Select, Switch, Tabs} from 'antd'
import { TabPaneProps } from 'antd/lib/tabs' import {TabPaneProps} from 'antd/lib/tabs'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import {languages} from '../../../i18n'
import { languages } from '../../../i18n'
export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
const { t } = useTranslation()
export const BaseDataTab: React.FC<TabPaneProps> = props => {
return ( return (
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.Item <Form.Item
label={t('form:baseData.isLive')} label="Is Live"
name={['form', 'isLive']} name={['form', 'isLive']}
valuePropName={'checked'} valuePropName={'checked'}
> >
@ -18,12 +15,12 @@ export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('form:baseData.title')} label="Title"
name={['form', 'title']} name={['form', 'title']}
rules={[ rules={[
{ {
required: true, required: true,
message: t('validation:titleRequired'), message: 'Please provide a Title',
}, },
]} ]}
> >
@ -31,39 +28,28 @@ export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('form:baseData.language')} label="Language"
name={['form', 'language']} name={['form', 'language']}
rules={[ rules={[
{ {
required: true, required: true,
message: t('validation:languageRequired'), message: 'Please select a Language',
}, },
]} ]}
> >
<Select> <Select>
{languages.map((language) => ( {languages.map(language => <Select.Option value={language} key={language}>{language.toUpperCase()}</Select.Option> )}
<Select.Option value={language} key={language}>
{t(`language:${language}`)}
</Select.Option>
))}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('form:baseData.showFooter')} label="Show Footer"
name={['form', 'showFooter']} name={['form', 'showFooter']}
valuePropName={'checked'} valuePropName={'checked'}
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item
label={t('form:baseData.anonymousSubmission')}
name={['form', 'anonymousSubmission']}
valuePropName={'checked'}
>
<Switch />
</Form.Item>
</Tabs.TabPane> </Tabs.TabPane>
) )
} }

View File

@ -1,46 +1,27 @@
import { Form, Input, Select, Tabs } from 'antd' import {Form, Input, Tabs} from 'antd'
import { TabPaneProps } from 'antd/lib/tabs' import {TabPaneProps} from 'antd/lib/tabs'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import {InputColor} from '../../input/color'
import { InputColor } from '../../input/color'
export const DesignTab: React.FC<TabPaneProps> = (props) => {
const { t } = useTranslation()
export const DesignTab: React.FC<TabPaneProps> = props => {
return ( return (
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.Item label={t('form:design.font')} name={[ <Form.Item
'form', 'design', 'font', label="Font"
]}> name={['form', 'design', 'font']}
>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t('form:design.layouts')} name={[
'form', 'design', 'layout',
]}>
<Select
options={[
{
value: null,
label: t('form:design.layout.slider'),
},
{
value: 'card',
label: t('form:design.layout.card'),
},
]}
/>
</Form.Item>
{[ {[
'background', 'question', 'answer', 'button', 'buttonActive', 'buttonText', {name: 'backgroundColor', label: 'Background Color'},
].map((name) => ( {name: 'questionColor', label: 'Question Color'},
<Form.Item {name: 'answerColor', label: 'Answer Color'},
key={name} {name: 'buttonColor', label: 'Button Color'},
label={t(`form:design.color.${name}`)} {name: 'buttonActiveColor', label: 'Button Active Color'},
name={[ {name: 'buttonTextColor', label: 'Button Text Color'},
'form', 'design', 'colors', name, ].map(({label, name}) => (
]} <Form.Item key={name} label={label} name={['form', 'design', 'colors', name]}>
>
<InputColor /> <InputColor />
</Form.Item> </Form.Item>
))} ))}

View File

@ -1,53 +1,44 @@
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib' import {DeleteOutlined, PlusOutlined} from '@ant-design/icons/lib'
import { Button, Card, Form, Input, Switch, Tabs } from 'antd' import {Button, Card, Form, Input, Switch, Tabs} from 'antd'
import { TabPaneProps } from 'antd/lib/tabs' import {TabPaneProps} from 'antd/lib/tabs'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import {InputColor} from '../../input/color'
import { InputColor } from '../../input/color'
export const EndPageTab: React.FC<TabPaneProps> = (props) => {
const { t } = useTranslation()
export const EndPageTab: React.FC<TabPaneProps> = props => {
return ( return (
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.Item <Form.Item
label={t('form:endPage.show')} label={'Show'}
name={[ name={['form', 'endPage', 'show']}
'form', 'endPage', 'show',
]}
valuePropName={'checked'} valuePropName={'checked'}
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item label={t('form:endPage.title')} name={[ <Form.Item
'form', 'endPage', 'title', label={'Title'}
]}> name={['form', 'endPage', 'title']}
>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('form:endPage.paragraph')} label={'Paragraph'}
name={[ name={['form', 'endPage', 'paragraph']}
'form', 'endPage', 'paragraph',
]}
extra={t('type:descriptionInfo')}
> >
<Input.TextArea autoSize /> <Input.TextArea autoSize />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('form:endPage.continueButtonText')} label={'Continue Button Text'}
name={[ name={['form', 'endPage', 'buttonText']}
'form', 'endPage', 'buttonText',
]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.List name={[ <Form.List
'form', 'endPage', 'buttons', name={['form', 'endPage', 'buttons']}
]}> >
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
return ( return (
<div> <div>
@ -56,56 +47,43 @@ export const EndPageTab: React.FC<TabPaneProps> = (props) => {
wrapperCol={{ wrapperCol={{
sm: { offset: index === 0 ? 0 : 6 }, sm: { offset: index === 0 ? 0 : 6 },
}} }}
label={index === 0 ? t('form:endPage.buttons') : ''} label={index === 0 ? 'Buttons' : ''}
key={field.key} key={field.key}
> >
<Card actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}> <Card
actions={[
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
]}
>
<Form.Item <Form.Item
label={t('form:endPage.url')} label={'Url'}
name={[field.key, 'url']} name={[field.key, 'url']}
rules={[{ type: 'url', message: t('validation:invalidUrl') }]} rules={[
{type: 'url', message: 'Must be a valid url'}
]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Action'} name={[field.key, 'action']} labelCol={{ span: 6 }}>
label={t('form:endPage.action')}
name={[field.key, 'action']}
labelCol={{ span: 6 }}
>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Text'} name={[field.key, 'text']} labelCol={{ span: 6 }}>
label={t('form:endPage.text')}
name={[field.key, 'text']}
labelCol={{ span: 6 }}
>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Background Color'} name={[field.key, 'bgColor']} labelCol={{ span: 6 }}>
label={t('form:endPage.bgColor')}
name={[field.key, 'bgColor']}
labelCol={{ span: 6 }}
>
<InputColor /> <InputColor />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Active Color'} name={[field.key, 'activeColor']} labelCol={{ span: 6 }}>
label={t('form:endPage.activeColor')}
name={[field.key, 'activeColor']}
labelCol={{ span: 6 }}
>
<InputColor /> <InputColor />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Color'} name={[field.key, 'color']} labelCol={{ span: 6 }}>
label={t('form:endPage.color')}
name={[field.key, 'color']}
labelCol={{ span: 6 }}
>
<InputColor /> <InputColor />
</Form.Item> </Form.Item>
</Card> </Card>
</Form.Item> </Form.Item>
))} )
)}
<Form.Item <Form.Item
wrapperCol={{ wrapperCol={{
sm: { offset: 6 }, sm: { offset: 6 },
@ -114,11 +92,11 @@ export const EndPageTab: React.FC<TabPaneProps> = (props) => {
<Button <Button
type="dashed" type="dashed"
onClick={() => { onClick={() => {
add() add();
}} }}
style={{ width: '60%' }} style={{ width: '60%' }}
> >
<PlusOutlined /> {t('form:endPage.addButton')} <PlusOutlined /> Add Button
</Button> </Button>
</Form.Item> </Form.Item>
</div> </div>

View File

@ -1,125 +0,0 @@
import { message } from 'antd'
import ExcelJS, { CellValue } from 'exceljs'
import { useCallback, useState } from 'react'
import { SubmissionFragment } from '../../../graphql/fragment/submission.fragment'
import { useFormQuery } from '../../../graphql/query/form.query'
import { useSubmissionPagerImperativeQuery } from '../../../graphql/query/submission.pager.query'
import { fieldTypes } from '../types'
interface Props {
form: string
trigger: (open: () => any, loading: boolean) => JSX.Element
}
export const ExportSubmissionAction: React.FC<Props> = (props) => {
const [loading, setLoading] = useState(false)
const form = useFormQuery({
variables: {
id: props.form,
},
})
const getSubmissions = useSubmissionPagerImperativeQuery()
const exportSubmissions = useCallback(async () => {
if (loading) {
return
}
setLoading(true)
try {
const workbook = new ExcelJS.Workbook()
workbook.creator = 'OhMyForm'
workbook.lastModifiedBy = 'OhMyForm'
workbook.created = new Date()
workbook.modified = new Date()
const orderedFields = form.data.form.fields
.map(field => field)
.sort((a, b) => (a.idx ?? 0) - (b.idx ?? 0))
// TODO should go through deleted fields as well to have a complete overview!
const sheet = workbook.addWorksheet('Submissions')
sheet.getRow(1).values = [
'Submission ID',
'Created',
'Last Change',
'Country',
'City',
'User Agent',
'Device',
...orderedFields.map((field) => `${field.title} (${field.type})`),
]
const firstPage = await getSubmissions({
form: props.form,
limit: 50,
start: 0,
})
const buildRow = (data: SubmissionFragment): CellValue[] => {
const row: CellValue[] = [
data.id,
data.created,
data.lastModified,
data.geoLocation.country,
data.geoLocation.city,
data.device.type,
data.device.name,
]
orderedFields.forEach((formField) => {
const field = data.fields.find(submission => submission.field?.id === formField.id)
try {
fieldTypes[field.type]?.stringifyValue(field.value)
row.push(fieldTypes[field.type]?.stringifyValue(field.value))
} catch (e) {
row.push('')
}
})
return row
}
firstPage.data.pager.entries.forEach((row, index) => {
sheet.getRow(index + 2).values = buildRow(row)
})
const pages = Math.ceil(firstPage.data.pager.total / 50)
for (let page = 1; page < pages; page++) {
// now process each page!
const next = await getSubmissions({
form: props.form,
limit: 50,
start: page * 50,
})
next.data.pager.entries.forEach((row, index) => {
sheet.getRow(index + 2 + page * 50).values = buildRow(row)
})
}
const buffer = await workbook.xlsx.writeBuffer()
const link = document.createElement('a')
link.href = window.URL.createObjectURL(new Blob([buffer], { type: 'application/xlsx' }))
link.download = 'submissions.xlsx'
link.click()
} catch (e) {
console.log('error', e)
void message.error({
content: 'Failed to generate export',
})
}
setLoading(false)
}, [
form, getSubmissions, props.form, setLoading, loading,
])
return props.trigger(() => exportSubmissions(), loading)
}

View File

@ -1,56 +1,39 @@
import { VerticalAlignBottomOutlined, VerticalAlignTopOutlined } from '@ant-design/icons' import {DeleteOutlined} from '@ant-design/icons/lib'
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib' import {Button, Card, Checkbox, Form, Input, Popconfirm, Tag} from 'antd'
import { Button, Card, Checkbox, Form, Input, Popconfirm, Popover, Space, Tag, Tooltip } from 'antd' import {FormInstance} from 'antd/lib/form'
import { FormInstance } from 'antd/lib/form' import {FieldData} from 'rc-field-form/lib/interface'
import { FieldData } from 'rc-field-form/lib/interface' import React, {useEffect, useState} from 'react'
import React, { useCallback, useEffect, useState } from 'react' import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
import { useTranslation } from 'react-i18next' import {adminTypes} from './types'
import { FormFieldFragment, FormFieldLogicFragment } from '../../../graphql/fragment/form.fragment' import {TextType} from './types/text.type'
import { fieldTypes } from '../types'
import { LogicBlock } from './logic.block'
interface Props { interface Props {
form: FormInstance form: FormInstance
fields: FormFieldFragment[] fields: AdminFormFieldFragment[]
onChangeFields: (fields: FormFieldFragment[]) => void onChangeFields: (fields: AdminFormFieldFragment[]) => any
field: FieldData field: FieldData
remove: (index: number) => void remove: (index: number) => void
move: (from: number, to: number) => void
index: number index: number
} }
export const FieldCard: React.FC<Props> = ({ export const FieldCard: React.FC<Props> = props => {
const {
form, form,
field, field,
fields, fields,
onChangeFields, onChangeFields,
remove, remove,
move,
index, index,
}) => { } = props
const { t } = useTranslation()
const type = form.getFieldValue([ const type = form.getFieldValue(['form', 'fields', field.name as string, 'type'])
'form', 'fields', field.name as string, 'type', const TypeComponent = adminTypes[type] || TextType
]) as string
const TypeComponent = (fieldTypes[type] || fieldTypes['textfield']).adminFormField()
const [shouldUpdate, setShouldUpdate] = useState(false) const [nextTitle, setNextTitle] = useState(form.getFieldValue(['form', 'fields', field.name as string, 'title']))
const [nextTitle, setNextTitle] = useState<string>(
form.getFieldValue([
'form', 'fields', field.name as string, 'title',
])
)
useEffect(() => { useEffect(() => {
if (!shouldUpdate) {
return
}
const id = setTimeout(() => { const id = setTimeout(() => {
setShouldUpdate(false) onChangeFields(fields.map((field, i) => {
onChangeFields(
fields.map((field, i) => {
if (i === index) { if (i === index) {
return { return {
...field, ...field,
@ -59,186 +42,69 @@ export const FieldCard: React.FC<Props> = ({
} else { } else {
return field return field
} }
}) }))
)
}, 500) }, 500)
return () => clearTimeout(id) return () => clearTimeout(id)
}, [ }, [nextTitle])
nextTitle, shouldUpdate, fields,
])
const addLogic = useCallback((add: (defaults: unknown) => void, index: number) => {
return (
<Form.Item wrapperCol={{ span: 24 }}>
<Space
style={{
width: '100%',
justifyContent: 'flex-end',
}}
>
<Button
type="dashed"
onClick={() => {
const defaults: FormFieldLogicFragment = {
id: `NEW-${Date.now()}`,
formula: null,
action: null,
jumpTo: null,
visible: null,
disable: null,
require: null,
enabled: false,
}
add(defaults)
}}
>
<PlusOutlined /> {t('form:logic.add')}
</Button>
</Space>
</Form.Item>
)
}, [])
return ( return (
<Card <Card
title={nextTitle} title={nextTitle}
type={'inner'} type={'inner'}
extra={ extra={(
<Space> <div>
<Tooltip title={t('form:field.move.up')}> <Tag color={'blue'}>{type}</Tag>
<Button
type={'text'}
disabled={index === 0}
onClick={() => move(index, index - 1)}
icon={<VerticalAlignTopOutlined />}
/>
</Tooltip>
<Tooltip title={t('form:field.move.down')}>
<Button
type={'text'}
disabled={index + 1 >= form.getFieldValue(['form', 'fields']).length}
onClick={() => move(index, index + 1)}
icon={<VerticalAlignBottomOutlined />}
/>
</Tooltip>
<Form.Item noStyle shouldUpdate>
{() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const slug = form.getFieldValue([
'form', 'fields', field.name as string, 'slug',
])
if (!slug) {
return null
}
return <Tag color={'warning'}>Slug: {slug}</Tag>
}}
</Form.Item>
<Popover
placement={'left'}
content={
<Form.Item
name={[field.name as string, 'slug']}
label={false}
rules={[
{
pattern: /^[a-z0-9_]+$/,
message: t('validation:invalidSlug'),
},
]}
help={t('type:slugInfo')}
>
<Input />
</Form.Item>
}
title={t('type:slug')}
>
<Tag color={'blue'}>{t(`type:${type}.name`)}</Tag>
</Popover>
<Popconfirm <Popconfirm
placement={'left'} placement={'left'}
title={t('type:confirmDelete')} title={'Really remove this field? Check that it is not referenced anywhere!'}
okText={t('type:deleteNow')} okText={'Delete Field'}
okButtonProps={{ danger: true }} okButtonProps={{ danger: true }}
onConfirm={() => { onConfirm={() => {
remove(index) remove(index)
onChangeFields(fields.filter((e, i) => i !== index)) onChangeFields(fields.filter((e, i) => i !== index))
}} }}
> >
<Button danger> <Button danger><DeleteOutlined /></Button>
<DeleteOutlined />
</Button>
</Popconfirm> </Popconfirm>
</Space> </div>
} )}
actions={[
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
]}
> >
<Form.Item name={[field.name as string, 'type']} noStyle> <Form.Item name={[field.name as string, 'type']} noStyle><Input type={'hidden'} /></Form.Item>
<Input type={'hidden'} />
</Form.Item>
<Form.Item <Form.Item
label={t('type:title')} label={'Title'}
name={[field.name as string, 'title']} name={[field.name as string, 'title']}
rules={[{ required: true, message: 'Title is required' }]} rules={[
{ required: true, message: 'Title is required' }
]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input <Input onChange={e => setNextTitle(e.target.value)}/>
onChange={(e) => {
setNextTitle(e.target.value)
setShouldUpdate(true)
}}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('type:description')} label={'Description'}
name={[field.name as string, 'description']} name={[field.name as string, 'description']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
extra={t('type:descriptionInfo')}
> >
<Input.TextArea autoSize /> <Input.TextArea autoSize />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('type:required')} label={'Required'}
name={[field.name as string, 'required']} name={[field.name as string, 'required']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
valuePropName={'checked'} valuePropName={'checked'}
extra={type === 'hidden' && t('type:requiredInfo')} extra={type === 'hidden' && 'If required, default value must be set to enable users to submit form!'}
> >
<Checkbox /> <Checkbox />
</Form.Item> </Form.Item>
<TypeComponent field={field} form={form} /> <TypeComponent
<Form.List name={[field.name as string, 'logic']}>
{(logic, { add, remove, move }) => {
const addAndMove = (index: number) => (defaults) => {
add(defaults)
move(fields.length, index)
}
return (
<div>
{addLogic(addAndMove(0), 0)}
{logic.map((field, index) => (
<div key={field.key}>
<Form.Item wrapperCol={{ span: 24 }} noStyle>
<LogicBlock
field={field} field={field}
form={form} form={form}
fields={fields}
index={index}
remove={remove}
/> />
</Form.Item>
{addLogic(addAndMove(index + 1), index + 1)}
</div>
))}
</div>
)
}}
</Form.List>
</Card> </Card>
) )
} }

View File

@ -1,83 +1,62 @@
import { PlusOutlined } from '@ant-design/icons/lib' import {PlusOutlined} from '@ant-design/icons/lib'
import { Button, Form, Select, Space, Tabs } from 'antd' import {Button, Form, Select, Space, Tabs} from 'antd'
import { FormInstance } from 'antd/lib/form' import {FormInstance} from 'antd/lib/form'
import { TabPaneProps } from 'antd/lib/tabs' import {TabPaneProps} from 'antd/lib/tabs'
import debug from 'debug' import React, {useCallback, useState} from 'react'
import { FieldData } from 'rc-field-form/lib/interface' import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
import React, { useCallback, useState } from 'react' import {FieldCard} from './field.card'
import { useTranslation } from 'react-i18next' import {adminTypes} from './types'
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
import { fieldTypes } from '../types'
import { FieldCard } from './field.card'
const logger = debug('FieldsTab')
interface Props extends TabPaneProps { interface Props extends TabPaneProps {
form: FormInstance form: FormInstance
fields: FormFieldFragment[] fields: AdminFormFieldFragment[]
onChangeFields: (fields: FormFieldFragment[]) => void onChangeFields: (fields: AdminFormFieldFragment[]) => any
} }
export const FieldsTab: React.FC<Props> = (props) => { export const FieldsTab: React.FC<Props> = props => {
const { t } = useTranslation()
const [nextType, setNextType] = useState('textfield') const [nextType, setNextType] = useState('textfield')
const renderType = useCallback( const renderType = useCallback((field, index, remove) => {
(
field: FieldData,
index: number,
remove: (index: number) => void,
move: (from: number, to: number) => void
) => {
return ( return (
<FieldCard <FieldCard
form={props.form} form={props.form}
field={field} field={field}
index={index} index={index}
remove={(index: number) => { remove={remove}
logger('remove %d', index)
remove(index)
}}
move={(from: number, to: number) => {
logger('move %d TO %d', from, to)
move(from, to)
}}
fields={props.fields} fields={props.fields}
onChangeFields={props.onChangeFields} onChangeFields={props.onChangeFields}
/> />
) )
}, }, [props.fields])
[props.fields]
)
const addField = useCallback( const addField = useCallback((add, index) => {
(add: (defaults: unknown) => void, index: number) => {
return ( return (
<Form.Item wrapperCol={{ span: 24 }}> <Form.Item
wrapperCol={{span: 24}}
>
<Space <Space
style={{ style={{
width: '100%', width: '100%',
justifyContent: 'flex-end', justifyContent: 'flex-end',
}} }}
> >
<Select value={nextType} onChange={(e) => setNextType(e)} style={{ minWidth: 200 }}> <Select value={nextType} onChange={e => setNextType(e)} style={{ minWidth: 200 }}>
{Object.keys(fieldTypes).map((type) => ( {Object.keys(adminTypes).map(type => <Select.Option value={type} key={type}>{type}</Select.Option> )}
<Select.Option value={type} key={type}>
{t(`type:${type}.name`)}
</Select.Option>
))}
</Select> </Select>
<Button <Button
type="dashed" type="dashed"
onClick={() => { onClick={() => {
const defaults: FormFieldFragment = { const defaults: AdminFormFieldFragment = {
logic: [], logicJump: {
enabled: false,
},
options: [], options: [],
id: `NEW-${Date.now()}`, id: `NEW-${Date.now()}`,
type: nextType, type: nextType,
title: '', title: '',
description: '', description: '',
required: false, required: false,
value: ''
} }
add(defaults) add(defaults)
@ -86,20 +65,22 @@ export const FieldsTab: React.FC<Props> = (props) => {
props.onChangeFields(next) props.onChangeFields(next)
}} }}
> >
<PlusOutlined /> {t('type:add')} <PlusOutlined /> Add Field
</Button> </Button>
</Space> </Space>
</Form.Item> </Form.Item>
) )
}, }, [props.fields, nextType])
[props.fields, nextType]
)
return ( return (
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.List name={['form', 'fields']}>
<Form.List
name={['form', 'fields']}
>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
const addAndMove = (index: number) => (defaults) => { const addAndMove = (index) => (defaults) => {
add(defaults) add(defaults)
move(fields.length, index) move(fields.length, index)
} }
@ -110,7 +91,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
{fields.map((field, index) => ( {fields.map((field, index) => (
<div key={field.key}> <div key={field.key}>
<Form.Item wrapperCol={{ span: 24 }}> <Form.Item wrapperCol={{ span: 24 }}>
{renderType(field, index, remove, move)} {renderType(field, index, remove)}
</Form.Item> </Form.Item>
{addField(addAndMove(index + 1), index + 1)} {addField(addAndMove(index + 1), index + 1)}
</div> </div>
@ -119,6 +100,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
) )
}} }}
</Form.List> </Form.List>
</Tabs.TabPane> </Tabs.TabPane>
) )
} }

View File

@ -1,98 +0,0 @@
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
import { Button, Card, Checkbox, Form, Input, Popconfirm, Space, Tabs } from 'antd'
import { TabPaneProps } from 'antd/lib/tabs'
import React from 'react'
import { useTranslation } from 'react-i18next'
interface Props extends TabPaneProps {}
export const HooksTab: React.FC<Props> = (props) => {
const { t } = useTranslation()
return (
<Tabs.TabPane {...props}>
<Form.List name={['form', 'hooks']}>
{(hooks, { add, remove }) => {
return (
<div>
<Form.Item wrapperCol={{ span: 24 }}>
<Space
style={{
width: '100%',
justifyContent: 'flex-end',
}}
>
<Button
type="dashed"
onClick={() => {
const defaults = {
id: `NEW-${Date.now()}`,
enabled: false,
url: '',
}
add(defaults)
}}
>
<PlusOutlined /> {t('form:hooks.add')}
</Button>
</Space>
</Form.Item>
{hooks.map((hook, index) => (
<div key={hook.key}>
<Form.Item wrapperCol={{ span: 24 }}>
<Card
title={
<div>
<Form.Item
name={[hook.name, 'enabled']}
valuePropName={'checked'}
noStyle
>
<Checkbox />
</Form.Item>
&nbsp;{t('form:hooks.enabled')}
</div>
}
type={'inner'}
extra={
<div>
<Popconfirm
placement={'left'}
title={t('form:hooks.confirmDelete')}
okText={t('form:hooks.deleteNow')}
okButtonProps={{ danger: true }}
onConfirm={() => {
remove(index)
}}
>
<Button danger>
<DeleteOutlined />
</Button>
</Popconfirm>
</div>
}
actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}
>
<Form.Item
label={t('form:hooks.url')}
name={[hook.name, 'url']}
rules={[
{ required: true, message: t('validation:urlRequired') },
{ type: 'url', message: t('validation:invalidUrl') },
]}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
</Card>
</Form.Item>
</div>
))}
</div>
)
}}
</Form.List>
</Tabs.TabPane>
)
}

View File

@ -1,26 +1,22 @@
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons/lib' import {CheckCircleOutlined, CloseCircleOutlined} from '@ant-design/icons/lib'
import React from 'react' import React from 'react'
interface Props { interface Props {
isLive: boolean isLive: boolean
} }
export const FormIsLive: React.FC<Props> = (props) => { export const FormIsLive: React.FC<Props> = props => {
if (props.isLive) { if (props.isLive) {
return ( return (
<CheckCircleOutlined <CheckCircleOutlined style={{
style={{ color: 'green'
color: 'green', }} />
}}
/>
) )
} }
return ( return (
<CloseCircleOutlined <CloseCircleOutlined style={{
style={{ color: 'red'
color: 'red', }} />
}}
/>
) )
} }

View File

@ -1,233 +0,0 @@
import { DeleteOutlined } from '@ant-design/icons'
import { Alert, Button, Checkbox, Form, Mentions, Popconfirm, Select } from 'antd'
import { FormInstance } from 'antd/lib/form'
import { FieldData } from 'rc-field-form/lib/interface'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
import { useMath } from '../../use.math'
interface Props {
form: FormInstance
fields: FormFieldFragment[]
field: FieldData
remove: (index: number) => void
index: number
}
export const LogicBlock: React.FC<Props> = ({
form,
field,
fields,
remove,
index,
}) => {
const { t } = useTranslation()
const evaluator = useMath()
return (
<div
style={{
borderRight: '5px solid #DDD',
paddingRight: 10,
}}
>
<Form.Item
name={[field.name as string, 'formula']}
labelCol={{ span: 6 }}
label={'Formula'}
rules={[{ required: true, message: 'combine other fields' }]}
extra={'Save form to get new @IDs and $slugs. (example: $slug < 21 or @id = 42)'}
>
<Mentions rows={1}>
{fields.map((field) => (
<Mentions.Option key={field.id} value={field.id}>
{field.title}
</Mentions.Option>
))}
</Mentions>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form: FormInstance & { prefixName: string[] }) => {
try {
const defaults = {}
fields.forEach((field) => {
defaults[`@${field.id}`] = field.defaultValue
if (field.slug) {
defaults[`$${field.slug}`] = field.defaultValue
}
})
const result = evaluator(
form.getFieldValue([
...form.prefixName,
field.name as string,
'formula',
]),
defaults
)
return (
<Alert
type={result ? 'success' : 'warning'}
message={
result
? 'would trigger action with current default values'
: 'would NOT trigger action with current default values'
}
style={{ marginBottom: 24 }}
/>
)
} catch (e) {
return (
<Alert
message={(e as Error).message || 'Failed to process formula'}
type={'error'}
style={{ marginBottom: 24 }}
/>
)
}
}}
</Form.Item>
<Form.Item name={[field.name as string, 'action']} labelCol={{ span: 6 }} label={'Action'}>
<Select
options={[
{
value: 'jumpTo',
label: t('form:logic.action.jumpTo'),
},
{
value: 'visible',
label: t('form:logic.action.visible'),
},
{
value: 'disable',
label: t('form:logic.action.disable'),
},
{
value: 'require',
label: t('form:logic.action.require'),
},
]}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form: FormInstance & { prefixName: string[] }) => {
return (
<Form.Item
hidden={
form.getFieldValue([
...form.prefixName, field.name as string, 'action',
]) !==
'jumpTo'
}
labelCol={{ span: 6 }}
label={t('form:logic.action.jumpTo')}
rules={[{ required: true, message: 'Jump target is required' }]}
extra={'after selecting field (works best with clickable values)'}
>
<Select
options={fields
.filter((field) => !/NEW/i.test(field.id))
.map((field) => ({
value: field.id,
label: field.title,
}))}
/>
</Form.Item>
)
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form: FormInstance & { prefixName: string[] }) => {
return (
<Form.Item
hidden={
form.getFieldValue([
...form.prefixName, field.name as string, 'action',
]) !==
'visible'
}
initialValue={true}
labelCol={{ span: 6 }}
label={t('form:logic.action.visible')}
valuePropName={'checked'}
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
getValueProps={(e: string) => ({ checked: !!e })}
>
<Checkbox />
</Form.Item>
)
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form: FormInstance & { prefixName: string[] }) => {
return (
<Form.Item
hidden={
form.getFieldValue([
...form.prefixName, field.name as string, 'action',
]) !==
'disable'
}
initialValue={false}
labelCol={{ span: 6 }}
label={t('form:logic.action.disable')}
valuePropName={'checked'}
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
getValueProps={(e: string) => ({ checked: !!e })}
>
<Checkbox />
</Form.Item>
)
}}
</Form.Item>
<Form.Item noStyle shouldUpdate>
{(form: FormInstance & { prefixName: string[] }) => {
return (
<Form.Item
hidden={
form.getFieldValue([
...form.prefixName, field.name as string, 'action',
]) !==
'require'
}
initialValue={true}
labelCol={{ span: 6 }}
label={t('form:logic.action.require')}
valuePropName={'checked'}
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
getValueProps={(e: string) => ({ checked: !!e })}
>
<Checkbox />
</Form.Item>
)
}}
</Form.Item>
<Form.Item>
<div style={{ textAlign: 'right' }}>
<Popconfirm
placement={'right'}
title={t('type:confirmDelete')}
okText={t('type:deleteNow')}
okButtonProps={{ danger: true }}
onConfirm={() => {
remove(index)
}}
>
<Button danger>
<DeleteOutlined />
</Button>
</Popconfirm>
</div>
</Form.Item>
</div>
)
}

View File

@ -1,247 +0,0 @@
import { DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons/lib'
import { Button, Card, Form, Input, Popconfirm, Select, Switch } from 'antd'
import { FormInstance } from 'antd/lib/form'
import { FieldData } from 'rc-field-form/lib/interface'
import React, { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
interface Props {
form: FormInstance
field: FieldData
groups: {
[key: string]: FormFieldFragment[]
}
remove: (index: number) => void
index: number
}
export const NotificationCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { form, field, remove, index, groups } = props
const [enabled, setEnabled] = useState<boolean>()
return (
<Card
title={'Notification'}
type={'inner'}
extra={
<div>
<Popconfirm
placement={'left'}
title={t('type:confirmDelete')}
okText={t('type:deleteNow')}
okButtonProps={{ danger: true }}
onConfirm={() => {
remove(index)
}}
>
<Button danger>
<DeleteOutlined />
</Button>
</Popconfirm>
</div>
}
actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}
>
<Form.Item
label={t('form:notifications.enabled')}
name={[field.name as string, 'enabled']}
valuePropName={'checked'}
labelCol={{ span: 6 }}
>
<Switch onChange={(e) => setEnabled(e.valueOf())} />
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.subject')}
name={[field.name as string, 'subject']}
rules={[
{
required: Boolean(
form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
])
),
message: t('validation:subjectRequired'),
},
]}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.htmlTemplate')}
name={[field.name as string, 'htmlTemplate']}
rules={[
{
required: Boolean(
form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
])
),
message: t('validation:templateRequired'),
},
]}
extra={
<div>
<Trans>form:notifications.htmlTemplateInfo</Trans>
<a
href={'https://mjml.io/try-it-live'}
target={'_blank'}
rel={'noreferrer'}
style={{
marginLeft: 16,
}}
>
<InfoCircleOutlined />
</a>
</div>
}
labelCol={{ span: 6 }}
>
<Input.TextArea autoSize />
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.fromField')}
name={[field.name as string, 'fromField']}
extra={t('form:notifications.fromFieldInfo')}
labelCol={{ span: 6 }}
rules={[
{
required: Boolean(
form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
]) &&
!form.getFieldValue([
'form',
'notifications',
field.name as string,
'fromEmail',
])
),
message: t('validation:emailFieldRequired'),
},
]}
>
<Select>
{Object.keys(groups).map((key) => (
<Select.OptGroup label={key.toUpperCase()} key={key}>
{groups[key].map((element) => (
<Select.Option value={element.id} key={element.id}>
{element.title}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.fromEmail')}
name={[field.name as string, 'fromEmail']}
extra={t('form:notifications.fromEmailInfo')}
labelCol={{ span: 6 }}
rules={[
{
required: Boolean(
form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
]) &&
!form.getFieldValue([
'form',
'notifications',
field.name as string,
'fromField',
])
),
message: t('validation:emailFieldRequired'),
},
]}
>
<Input />
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.toField')}
name={[field.name as string, 'toField']}
extra={t('form:notifications.toFieldInfo')}
rules={[
{
required: Boolean(
form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
]) &&
!form.getFieldValue([
'form', 'notifications', field.name as string, 'toEmail',
])
),
message: t('validation:emailFieldRequired'),
},
]}
labelCol={{ span: 6 }}
>
<Select>
{Object.keys(groups).map((key) => (
<Select.OptGroup label={key.toUpperCase()} key={key}>
{groups[key].map((field) => (
<Select.Option value={field.id} key={field.id}>
{field.title}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.toEmail')}
name={[field.name as string, 'toEmail']}
extra={t('form:notifications.toEmailInfo')}
labelCol={{ span: 6 }}
rules={[
{
required: Boolean(
form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
]) &&
!form.getFieldValue([
'form', 'notifications', field.name as string, 'toField',
])
),
message: t('validation:emailFieldRequired'),
},
]}
>
<Input />
</Form.Item>
)}
</Form.Item>
</Card>
)
}

View File

@ -1,93 +0,0 @@
import { PlusOutlined } from '@ant-design/icons/lib'
import { Button, Form, Space, Tabs } from 'antd'
import { FormInstance } from 'antd/lib/form'
import { TabPaneProps } from 'antd/lib/tabs'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
FormFieldFragment,
FormNotificationFragment,
} from '../../../graphql/fragment/form.fragment'
import { NotificationCard } from './notification.card'
interface Props extends TabPaneProps {
form: FormInstance
fields: FormFieldFragment[]
}
export const NotificationsTab: React.FC<Props> = (props) => {
const { t } = useTranslation()
const groups: {
[key: string]: FormFieldFragment[]
} = {}
props.fields.forEach((field) => {
if (!groups[field.type]) {
groups[field.type] = []
}
groups[field.type].push(field)
})
const addField = useCallback(
(add: (defaults: unknown) => void, index: number) => {
return (
<Form.Item wrapperCol={{ span: 24 }}>
<Space
style={{
width: '100%',
justifyContent: 'flex-end',
}}
>
<Button
type="dashed"
onClick={() => {
const defaults: FormNotificationFragment = {
id: Math.random().toString(),
enabled: false,
}
add(defaults)
}}
>
<PlusOutlined /> {t('form:notifications.add')}
</Button>
</Space>
</Form.Item>
)
},
[props.fields]
)
return (
<Tabs.TabPane {...props}>
<Form.List name={['form', 'notifications']}>
{(fields, { add, remove, move }) => {
const addAndMove = (index: number) => (defaults) => {
add(defaults)
move(fields.length, index)
}
return (
<div>
{addField(addAndMove(0), 0)}
{fields.map((field, index) => (
<div key={field.key}>
<Form.Item wrapperCol={{ span: 24 }}>
<NotificationCard
form={props.form}
field={field}
index={index}
remove={remove}
groups={groups}
/>
</Form.Item>
{addField(addAndMove(index + 1), index + 1)}
</div>
))}
</div>
)
}}
</Form.List>
</Tabs.TabPane>
)
}

View File

@ -0,0 +1,107 @@
import {Form, Input, Select, Switch, Tabs} from 'antd'
import {FormInstance} from 'antd/lib/form'
import {TabPaneProps} from 'antd/lib/tabs'
import React, {useEffect, useState} from 'react'
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
interface Props extends TabPaneProps {
form: FormInstance
fields: AdminFormFieldFragment[]
}
export const RespondentNotificationsTab: React.FC<Props> = props => {
const [enabled, setEnabled] = useState<boolean>()
useEffect(() => {
const next = props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])
if (next !== enabled) {
setEnabled(next)
}
}, [props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])])
useEffect(() => {
props.form.validateFields([
['form', 'respondentNotifications', 'subject'],
['form', 'respondentNotifications', 'htmlTemplate'],
['form', 'respondentNotifications', 'toField'],
])
}, [enabled])
const groups = {}
props.fields.forEach(field => {
if (!groups[field.type]) {
groups[field.type] = []
}
groups[field.type].push(field)
})
return (
<Tabs.TabPane {...props}>
<Form.Item
label={'Enabled'}
name={['form', 'respondentNotifications', 'enabled']}
valuePropName={'checked'}
>
<Switch onChange={e => setEnabled(e.valueOf())} />
</Form.Item>
<Form.Item
label={'Subject'}
name={['form', 'respondentNotifications', 'subject']}
rules={[
{
required: enabled,
message: 'Please provide a Subject',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={'HTML Template'}
name={['form', 'respondentNotifications', 'htmlTemplate']}
rules={[
{
required: enabled,
message: 'Please provide a Template',
},
]}
>
<Input.TextArea autoSize />
</Form.Item>
<Form.Item
label={'Email Field'}
name={['form', 'respondentNotifications', 'toField']}
extra={'Field with Email for receipt'}
rules={[
{
required: enabled,
message: 'Please provide a Email Field',
},
]}
>
<Select>
{Object.keys(groups).map(key => (
<Select.OptGroup label={key.toUpperCase()} key={key}>
{groups[key].map(field => (
<Select.Option value={field.id} key={field.id}>{field.title}</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</Form.Item>
<Form.Item
label={'Sender Email'}
name={['form', 'respondentNotifications', 'fromEmail']}
extra={'Make sure your mailserver can send from this email'}
>
<Input />
</Form.Item>
</Tabs.TabPane>
)
}

View File

@ -0,0 +1,99 @@
import {Form, Input, Select, Switch, Tabs} from 'antd'
import {FormInstance} from 'antd/lib/form'
import {TabPaneProps} from 'antd/lib/tabs'
import React, {useEffect, useState} from 'react'
import {AdminFormFieldFragment} from '../../../graphql/fragment/admin.form.fragment'
interface Props extends TabPaneProps {
form: FormInstance
fields: AdminFormFieldFragment[]
}
export const SelfNotificationsTab: React.FC<Props> = props => {
const [enabled, setEnabled] = useState<boolean>()
useEffect(() => {
const next = props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])
if (next !== enabled) {
setEnabled(next)
}
}, [props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])])
useEffect(() => {
props.form.validateFields([
['form', 'selfNotifications', 'subject'],
['form', 'selfNotifications', 'htmlTemplate'],
])
}, [enabled])
const groups = {}
props.fields.forEach(field => {
if (!groups[field.type]) {
groups[field.type] = []
}
groups[field.type].push(field)
})
return (
<Tabs.TabPane {...props}>
<Form.Item
label={'Enabled'}
name={['form', 'selfNotifications', 'enabled']}
valuePropName={'checked'}
>
<Switch onChange={e => setEnabled(e.valueOf())} />
</Form.Item>
<Form.Item
label={'Subject'}
name={['form', 'selfNotifications', 'subject']}
rules={[
{
required: enabled,
message: 'Please provide a Subject',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={'HTML Template'}
name={['form', 'selfNotifications', 'htmlTemplate']}
rules={[
{
required: enabled,
message: 'Please provide a Template',
},
]}
>
<Input.TextArea autoSize />
</Form.Item>
<Form.Item
label={'Email Field'}
name={['form', 'selfNotifications', 'fromField']}
extra={'Field with Email, will set the Reply-To header'}
>
<Select>
{Object.keys(groups).map(key => (
<Select.OptGroup label={key.toUpperCase()} key={key}>
{groups[key].map(field => (
<Select.Option value={field.id} key={field.id}>{field.title}</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</Form.Item>
<Form.Item
label={'Your Email'}
name={['form', 'selfNotifications', 'toEmail']}
extra={'If not set will send to the admin of the form'}
>
<Input />
</Form.Item>
</Tabs.TabPane>
)
}

View File

@ -1,53 +1,44 @@
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib' import {DeleteOutlined, PlusOutlined} from '@ant-design/icons/lib'
import { Button, Card, Form, Input, Switch, Tabs } from 'antd' import {Button, Card, Form, Input, Switch, Tabs} from 'antd'
import { TabPaneProps } from 'antd/lib/tabs' import {TabPaneProps} from 'antd/lib/tabs'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import {InputColor} from '../../input/color'
import { InputColor } from '../../input/color'
export const StartPageTab: React.FC<TabPaneProps> = (props) => {
const { t } = useTranslation()
export const StartPageTab: React.FC<TabPaneProps> = props => {
return ( return (
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.Item <Form.Item
label={t('form:startPage.show')} label={'Show'}
name={[ name={['form', 'startPage', 'show']}
'form', 'startPage', 'show',
]}
valuePropName={'checked'} valuePropName={'checked'}
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item label={t('form:startPage.title')} name={[ <Form.Item
'form', 'startPage', 'title', label={'Title'}
]}> name={['form', 'startPage', 'title']}
>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('form:startPage.paragraph')} label={'Paragraph'}
name={[ name={['form', 'startPage', 'paragraph']}
'form', 'startPage', 'paragraph',
]}
extra={t('form:startPage.paragraphInfo')}
> >
<Input.TextArea autoSize /> <Input.TextArea autoSize />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('form:startPage.continueButtonText')} label={'Continue Button Text'}
name={[ name={['form', 'startPage', 'buttonText']}
'form', 'startPage', 'buttonText',
]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.List name={[ <Form.List
'form', 'startPage', 'buttons', name={['form', 'startPage', 'buttons']}
]}> >
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
return ( return (
<div> <div>
@ -56,56 +47,43 @@ export const StartPageTab: React.FC<TabPaneProps> = (props) => {
wrapperCol={{ wrapperCol={{
sm: { offset: index === 0 ? 0 : 6 }, sm: { offset: index === 0 ? 0 : 6 },
}} }}
label={index === 0 ? t('form:startPage.buttons') : ''} label={index === 0 ? 'Buttons' : ''}
key={field.key} key={field.key}
> >
<Card actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}> <Card
actions={[
<DeleteOutlined key={'delete'} onClick={() => remove(index)} />
]}
>
<Form.Item <Form.Item
label={t('form:startPage.url')} label={'Url'}
name={[field.key, 'url']} name={[field.key, 'url']}
rules={[{ type: 'url', message: t('validation:invalidUrl') }]} rules={[
{type: 'url', message: 'Must be a valid url'}
]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Action'} name={[field.key, 'action']} labelCol={{ span: 6 }}>
label={t('form:startPage.action')}
name={[field.key, 'action']}
labelCol={{ span: 6 }}
>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Text'} name={[field.key, 'text']} labelCol={{ span: 6 }}>
label={t('form:startPage.text')}
name={[field.key, 'text']}
labelCol={{ span: 6 }}
>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Background Color'} name={[field.key, 'bgColor']} labelCol={{ span: 6 }}>
label={t('form:startPage.bgColor')}
name={[field.key, 'bgColor']}
labelCol={{ span: 6 }}
>
<InputColor /> <InputColor />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Active Color'} name={[field.key, 'activeColor']} labelCol={{ span: 6 }}>
label={t('form:startPage.activeColor')}
name={[field.key, 'activeColor']}
labelCol={{ span: 6 }}
>
<InputColor /> <InputColor />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={'Color'} name={[field.key, 'color']} labelCol={{ span: 6 }}>
label={t('form:startPage.color')}
name={[field.key, 'color']}
labelCol={{ span: 6 }}
>
<InputColor /> <InputColor />
</Form.Item> </Form.Item>
</Card> </Card>
</Form.Item> </Form.Item>
))} )
)}
<Form.Item <Form.Item
wrapperCol={{ wrapperCol={{
sm: { offset: 6 }, sm: { offset: 6 },
@ -114,11 +92,11 @@ export const StartPageTab: React.FC<TabPaneProps> = (props) => {
<Button <Button
type="dashed" type="dashed"
onClick={() => { onClick={() => {
add() add();
}} }}
style={{ width: '60%' }} style={{ width: '60%' }}
> >
<PlusOutlined /> {t('form:startPage.addButton')} <PlusOutlined /> Add Button
</Button> </Button>
</Form.Item> </Form.Item>
</div> </div>

View File

@ -1,63 +1,58 @@
import { Descriptions, Table } from 'antd' import {Descriptions, Table} from 'antd'
import { ColumnsType } from 'antd/lib/table/interface' import {ColumnsType} from 'antd/lib/table/interface'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import { FormPagerFragment } from '../../../graphql/fragment/form.pager.fragment'
import { import {
SubmissionFieldFragment, AdminPagerSubmissionEntryFieldQueryData,
SubmissionFragment, AdminPagerSubmissionEntryQueryData,
} from '../../../graphql/fragment/submission.fragment' AdminPagerSubmissionFormQueryData
import { fieldTypes } from '../types' } from '../../../graphql/query/admin.pager.submission.query'
interface Props { interface Props {
form: FormPagerFragment form: AdminPagerSubmissionFormQueryData
submission: SubmissionFragment submission: AdminPagerSubmissionEntryQueryData
} }
export const SubmissionValues: React.FC<Props> = (props) => { export const SubmissionValues: React.FC<Props> = props => {
const { t } = useTranslation() const columns: ColumnsType<AdminPagerSubmissionEntryFieldQueryData> = [
const columns: ColumnsType<SubmissionFieldFragment> = [
{ {
title: t('submission:field'), title: 'Field',
render(_, row) { render: (row: AdminPagerSubmissionEntryFieldQueryData) => {
if (row.field) { if (row.field) {
return `${row.field.title}${row.field.required ? '*' : ''}` return `${row.field.title}${row.field.required ? '*' : ''}`
} }
return `${row.id}` return `${row.id}`
}, }
}, },
{ {
title: t('submission:value'), title: 'Value',
render(_, row) { render: row => {
try { try {
return fieldTypes[row.type]?.displayValue(row.value) const data = JSON.parse(row.value)
return data.value
} catch (e) { } catch (e) {
return row.value return row.value
} }
}, }
}, }
] ]
return ( return (
<div> <div>
<Descriptions title={t('submission:submission')}> <Descriptions title={'Submission'}>
<Descriptions.Item label={t('submission:country')}> <Descriptions.Item label="Country">{props.submission.geoLocation.country}</Descriptions.Item>
{props.submission.geoLocation.country} <Descriptions.Item label="City">{props.submission.geoLocation.city}</Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="Device Type">{props.submission.device.type}</Descriptions.Item>
<Descriptions.Item label={t('submission:city')}> <Descriptions.Item label="Device Name">{props.submission.device.name}</Descriptions.Item>
{props.submission.geoLocation.city}
</Descriptions.Item>
<Descriptions.Item label={t('submission:device.type')}>
{props.submission.device.type}
</Descriptions.Item>
<Descriptions.Item label={t('submission:device.name')}>
{props.submission.device.name}
</Descriptions.Item>
</Descriptions> </Descriptions>
<Table columns={columns} dataSource={props.submission.fields} rowKey={'id'} /> <Table
columns={columns}
dataSource={props.submission.fields}
rowKey={'id'}
/>
</div> </div>
) )
} }

View File

@ -0,0 +1,41 @@
import {DatePicker, Form} from 'antd'
import moment from 'moment'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const DateType: React.FC<AdminFieldTypeProps> = ({field, form}) => {
return (
<div>
<Form.Item
label={'Default Date'}
name={[field.name, 'value']}
labelCol={{ span: 6 }}
getValueFromEvent={e => e ? e.format('YYYY-MM-DD') : undefined}
getValueProps={e => ({value: e ? moment(e) : undefined})}
>
<DatePicker
format={'YYYY-MM-DD'}
/>
</Form.Item>
<Form.Item
label={'Min Date'}
name={[field.name, 'optionKeys', 'min']}
labelCol={{ span: 6 }}
getValueFromEvent={e => e.format('YYYY-MM-DD')}
getValueProps={e => ({value: e ? moment(e) : undefined})}
>
<DatePicker />
</Form.Item>
<Form.Item
label={'Max Date'}
name={[field.name, 'optionKeys', 'max']}
labelCol={{ span: 6 }}
getValueFromEvent={e => e.format('YYYY-MM-DD')}
getValueProps={e => ({value: e ? moment(e) : undefined})}
>
<DatePicker />
</Form.Item>
</div>
)
}

View File

@ -1,23 +1,23 @@
import { Button, Col, Form, Input, Row } from 'antd' import {Button, Col, Form, Input, Row} from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import {AdminFieldTypeProps} from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const RadioAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
export const DropdownType: React.FC<AdminFieldTypeProps> = props => {
return ( return (
<div> <div>
<Form.Item <Form.Item
label={t('type:radio:default')} label={'Default Value'}
name={[props.field.name as string, 'defaultValue']} name={[props.field.name, 'value']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.List name={[props.field.name as string, 'options']}> <Form.List
name={[props.field.name, 'options']}
>
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@ -26,7 +26,7 @@ export const RadioAdmin: React.FC<FieldAdminProps> = (props) => {
sm: { offset: index === 0 ? 0 : 6 }, sm: { offset: index === 0 ? 0 : 6 },
}} }}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
label={index === 0 ? t('type:radio:options') : ''} label={index === 0 ? 'Options' : ''}
key={field.key} key={field.key}
> >
<Row gutter={16}> <Row gutter={16}>
@ -34,24 +34,26 @@ export const RadioAdmin: React.FC<FieldAdminProps> = (props) => {
<Form.Item <Form.Item
wrapperCol={{ span: 24 }} wrapperCol={{ span: 24 }}
name={[field.name, 'title']} name={[field.name, 'title']}
style={{ marginBottom: 0 }} style={{marginBottom: 0}}
> >
<Input placeholder={t('type:radio:titlePlaceholder')} /> <Input placeholder={'Title'} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={8}>
<Form.Item <Form.Item
wrapperCol={{ span: 24 }} wrapperCol={{ span: 24 }}
name={[field.name, 'value']} name={[field.name, 'value']}
style={{ marginBottom: 0 }} style={{marginBottom: 0}}
rules={[{ required: true, message: t('validation:valueRequired') }]} rules={[
{ required: true, message: 'Please provide a value' }
]}
> >
<Input placeholder={t('type:radio:valuePlaceholder')} /> <Input placeholder={'Value'} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={4}> <Col span={4}>
<Button danger onClick={() => remove(index)}> <Button danger onClick={() => remove(index)}>
{t('type:radio:removeOption')} Remove
</Button> </Button>
</Col> </Col>
</Row> </Row>
@ -64,9 +66,10 @@ export const RadioAdmin: React.FC<FieldAdminProps> = (props) => {
}} }}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Button type={'dashed'} onClick={() => add()}> <Button
{t('type:radio:addOption')} type={'dashed'}
</Button> onClick={() => add()}
>Add Option</Button>
</Form.Item> </Form.Item>
</div> </div>
) )

View File

@ -0,0 +1,20 @@
import {Form, Input} from 'antd'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const EmailType: React.FC<AdminFieldTypeProps> = props => {
return (
<div>
<Form.Item
label={'Default Email'}
name={[props.field.name, 'value']}
rules={[
{ type: 'email', message: 'Must be a valid email' }
]}
labelCol={{ span: 6 }}
>
<Input type={'email'} />
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,17 @@
import {Form, Input} from 'antd'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const HiddenType: React.FC<AdminFieldTypeProps> = props => {
return (
<div>
<Form.Item
label={'Default Value'}
name={[props.field.name, 'value']}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,29 @@
import React from 'react'
import {DateType} from './date.type'
import {DropdownType} from './dropdown.type'
import {EmailType} from './email.type'
import {HiddenType} from './hidden.type'
import {LinkType} from './link.type'
import {NumberType} from './number.type'
import {RadioType} from './radio.type'
import {RatingType} from './rating.type'
import {TextType} from './text.type'
import {TextareaType} from './textarea.type'
import {AdminFieldTypeProps} from './type.props'
import {YesNoType} from './yes_no.type'
export const adminTypes: {
[key: string]: React.FC<AdminFieldTypeProps>
} = {
'textfield': TextType,
'date': DateType,
'email': EmailType,
'textarea': TextareaType,
'link': LinkType,
'dropdown': DropdownType,
'rating': RatingType,
'radio': RadioType,
'hidden': HiddenType,
'yes_no': YesNoType,
'number': NumberType,
}

View File

@ -0,0 +1,20 @@
import {Form, Input} from 'antd'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const LinkType: React.FC<AdminFieldTypeProps> = props => {
return (
<div>
<Form.Item
label={'Default Link'}
name={[props.field.name, 'value']}
rules={[
{ type: 'url', message: 'Must be a valid URL' }
]}
labelCol={{ span: 6 }}
>
<Input type={'url'} />
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,17 @@
import {Form, InputNumber} from 'antd'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const NumberType: React.FC<AdminFieldTypeProps> = props => {
return (
<div>
<Form.Item
label={'Default Number'}
name={[props.field.name, 'value']}
labelCol={{ span: 6 }}
>
<InputNumber />
</Form.Item>
</div>
)
}

View File

@ -1,23 +1,23 @@
import { Button, Col, Form, Input, Row } from 'antd' import {Button, Col, Form, Input, Row} from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import {AdminFieldTypeProps} from './type.props'
import { FieldAdminProps } from '../field.admin.props'
export const CheckboxAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
export const RadioType: React.FC<AdminFieldTypeProps> = props => {
return ( return (
<div> <div>
<Form.Item <Form.Item
label={t('type:checkbox:default')} label={'Default Value'}
name={[props.field.name as string, 'defaultValue']} name={[props.field.name, 'value']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.List name={[props.field.name as string, 'options']}> <Form.List
name={[props.field.name, 'options']}
>
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@ -26,7 +26,7 @@ export const CheckboxAdmin: React.FC<FieldAdminProps> = (props) => {
sm: { offset: index === 0 ? 0 : 6 }, sm: { offset: index === 0 ? 0 : 6 },
}} }}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
label={index === 0 ? t('type:checkbox:options') : ''} label={index === 0 ? 'Options' : ''}
key={field.key} key={field.key}
> >
<Row gutter={16}> <Row gutter={16}>
@ -34,24 +34,26 @@ export const CheckboxAdmin: React.FC<FieldAdminProps> = (props) => {
<Form.Item <Form.Item
wrapperCol={{ span: 24 }} wrapperCol={{ span: 24 }}
name={[field.name, 'title']} name={[field.name, 'title']}
style={{ marginBottom: 0 }} style={{marginBottom: 0}}
> >
<Input placeholder={t('type:checkbox:titlePlaceholder')} /> <Input placeholder={'Title'} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={8}>
<Form.Item <Form.Item
wrapperCol={{ span: 24 }} wrapperCol={{ span: 24 }}
name={[field.name, 'value']} name={[field.name, 'value']}
style={{ marginBottom: 0 }} style={{marginBottom: 0}}
rules={[{ required: true, message: t('validation:valueRequired') }]} rules={[
{ required: true, message: 'Please provide a value' }
]}
> >
<Input placeholder={t('type:checkbox:valuePlaceholder')} /> <Input placeholder={'Value'} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={4}> <Col span={4}>
<Button danger onClick={() => remove(index)}> <Button danger onClick={() => remove(index)}>
{t('type:checkbox:removeOption')} Remove
</Button> </Button>
</Col> </Col>
</Row> </Row>
@ -64,9 +66,10 @@ export const CheckboxAdmin: React.FC<FieldAdminProps> = (props) => {
}} }}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Button type={'dashed'} onClick={() => add()}> <Button
{t('type:checkbox:addOption')} type={'dashed'}
</Button> onClick={() => add()}
>Add Option</Button>
</Form.Item> </Form.Item>
</div> </div>
) )

View File

@ -0,0 +1,22 @@
import {Form, Rate} from 'antd'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const RatingType: React.FC<AdminFieldTypeProps> = props => {
// TODO add ratings
return (
<div>
<Form.Item
label={'Default Value'}
name={[props.field.name, 'value']}
labelCol={{ span: 6 }}
extra={'Click again to remove default value'}
>
<Rate
allowHalf
allowClear
/>
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,15 @@
import {Form, Input} from 'antd'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const TextType: React.FC<AdminFieldTypeProps> = props => {
return (
<Form.Item
label={'Default Value'}
name={[props.field.name, 'value']}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
)
}

View File

@ -0,0 +1,17 @@
import {Form, Input} from 'antd'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const TextareaType: React.FC<AdminFieldTypeProps> = props => {
return (
<div>
<Form.Item
label={'Default Value'}
name={[props.field.name, 'value']}
labelCol={{ span: 6 }}
>
<Input.TextArea autoSize />
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,6 @@
import {FormInstance} from 'antd/lib/form'
export interface AdminFieldTypeProps {
form: FormInstance
field: any
}

View File

@ -0,0 +1,18 @@
import {Form, Input} from 'antd'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const YesNoType: React.FC<AdminFieldTypeProps> = props => {
// TODO add switch
return (
<div>
<Form.Item
label={'Default Value'}
name={[props.field.name, 'value']}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
</div>
)
}

85
components/form/field.tsx Normal file
View File

@ -0,0 +1,85 @@
import {Form, message} from 'antd'
import {useForm} from 'antd/lib/form/Form'
import React from 'react'
import {FormDesignFragment, FormFieldFragment} from '../../graphql/fragment/form.fragment'
import {StyledButton} from '../styled/button'
import {StyledH1} from '../styled/h1'
import {StyledP} from '../styled/p'
import {fieldTypes} from './types'
import {TextType} from './types/text.type'
import {FieldTypeProps} from './types/type.props'
interface Props {
field: FormFieldFragment
design: FormDesignFragment
save: (data: any) => any
next: () => any
prev: () => any
}
export const Field: React.FC<Props> = ({field, save, design, children, next, prev, ...props}) => {
const [form] = useForm()
const FieldInput: React.FC<FieldTypeProps> = fieldTypes[field.type] || TextType
const finish = (data) => {
console.log('received field data', data)
save(data)
next()
}
const error = () => {
message.error('Check inputs!')
}
return (
<Form
form={form}
onFinish={finish}
onFinishFailed={error}
{...props}
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
padding: 32,
justifyContent: 'flex-end',
}}>
<StyledH1 design={design} type={'question'}>{field.title}</StyledH1>
{field.description && <StyledP design={design} type={'question'}>{field.description}</StyledP>}
<FieldInput
design={design}
field={field}
/>
</div>
<div style={{
padding: 32,
display: 'flex',
}}>
<StyledButton
background={design.colors.buttonColor}
color={design.colors.buttonTextColor}
highlight={design.colors.buttonActiveColor}
onClick={prev}
>{'Previous'}</StyledButton>
<div style={{flex: 1}} />
<StyledButton
background={design.colors.buttonColor}
color={design.colors.buttonTextColor}
highlight={design.colors.buttonActiveColor}
size={'large'}
onClick={form.submit}
>{'Next'}</StyledButton>
</div>
</Form>
)
}

View File

@ -1,67 +0,0 @@
import React from 'react'
import {
FormPublicDesignFragment,
FormPublicFieldFragment,
} from '../../../../graphql/fragment/form.public.fragment'
import { StyledH1 } from '../../../styled/h1'
import { StyledMarkdown } from '../../../styled/markdown'
import { useRouter } from '../../../use.router'
import { fieldTypes } from '../../types'
interface Props {
focus?: boolean
field: FormPublicFieldFragment
design: FormPublicDesignFragment
}
export const Field: React.FC<Props> = ({ field, design, focus, ...props }) => {
const router = useRouter()
const FieldInput = (fieldTypes[field.type] || fieldTypes['text']).inputFormField()
const getUrlDefault = (): string => {
if (router.query[field.id]) {
return router.query[field.id] as string
}
if (router.query[field.slug]) {
return router.query[field.slug] as string
}
return undefined
}
return (
<div
{...props}
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
padding: 32,
justifyContent: 'flex-end',
}}
>
<StyledH1 design={design} type={'question'}>
{field.title}
</StyledH1>
{field.description && (
<StyledMarkdown design={design} type={'question'} >{field.description}</StyledMarkdown>
)}
<FieldInput
design={design}
field={field}
urlValue={getUrlDefault()}
focus={focus}
/>
</div>
</div>
)
}

View File

@ -1,221 +0,0 @@
import { Card, Form, message, Modal, Spin } from 'antd'
import debug from 'debug'
import { darken, lighten } from 'polished'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { Omf } from '../../../omf'
import { StyledButton } from '../../../styled/button'
import { useMath } from '../../../use.math'
import { fieldTypes } from '../../types'
import { LayoutProps } from '../layout.props'
import { Field } from './field'
import { Page } from './page'
type Step = 'start' | 'form' | 'end'
const logger = debug('layout/card')
const MyCard = styled.div<{ background: string }>`
background: ${(props) => darken(0.1, props.background)};
height: 100%;
min-height: 100vh;
min-height: calc(var(--vh, 1vh) * 100);
padding: 32px;
.ant-card {
background: ${(props) => props.background};
border-color: ${(props) => lighten(0.4, props.background)};
width: 800px;
margin: auto;
max-width: 90%;
}
`
export const CardLayout: React.FC<LayoutProps> = (props) => {
const { t } = useTranslation()
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const [step, setStep] = useState<Step>(props.form.startPage.show ? 'start' : 'form')
const evaluator = useMath()
const [visiblity, setVisibility] = useState({})
const { design, startPage, endPage, fields } = props.form
const { setField } = props.submission
const updateValues = useCallback(() => {
const defaults = {}
fields.forEach(field => {
const defaultValue = field.defaultValue
? fieldTypes[field.type].parseValue(field.defaultValue)
: null
defaults[`@${field.id}`] = form.getFieldValue([field.id, 'value']) ?? defaultValue
if (field.slug) {
defaults[`$${field.slug}`] = form.getFieldValue([field.id, 'value']) ?? defaultValue
}
})
// now calculate visibility
const nextVisibility = {}
fields.forEach(field => {
if (!field.logic) return
const logic = field.logic
.filter(logic => logic.action === 'visible')
if (logic.length === 0) {
return
}
nextVisibility[field.id] = logic
.map(logic => {
try {
const r = evaluator(
logic.formula,
defaults
)
return Boolean(r)
} catch {
return true
}
})
.reduce<boolean>((previous, current) => previous && current, true)
})
// TODO improve logic of how we calculate new logic checks
if (Object.values(nextVisibility).join(',') == Object.values(visiblity).join(',')) {
return
}
setVisibility(nextVisibility)
}, [
fields, form, visiblity,
])
useEffect(() => {
updateValues()
}, [updateValues])
const finish = async (data: { [key: number]: unknown }) => {
logger('finish form %O', data)
setLoading(true)
try {
// save fields
await Promise.all(Object.keys(data).map((fieldId) => setField(fieldId, data[fieldId])))
await props.submission.finish()
if (endPage.show) {
setStep('end')
} else {
Modal.success({
content: t('form:submitted'),
okText: t('from:restart'),
onOk: () => {
window.location.reload()
},
})
}
} catch (e) {
logger('failed to finish form %O', e)
void message.error({
content: 'Error saving Input',
})
}
setLoading(false)
}
console.log('render')
const render = () => {
switch (step) {
case 'start':
return <Page page={startPage} design={design} next={() => setStep('form')} />
case 'form':
return (
<Card>
<Form
form={form}
onFinish={finish}
onValuesChange={updateValues}
>
{fields.map((field, i) => {
if (field.type === 'hidden') {
return null
}
if (visiblity[field.id] === false) {
return null
}
return (
<Field
key={field.id}
field={field}
design={design}
focus={i === 0}
/>
)
})}
<div
style={{
padding: 32,
display: 'flex',
}}
>
{startPage.show && (
<StyledButton
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
onClick={() => setStep('start')}
>
{t('form:previous')}
</StyledButton>
)}
<div style={{ flex: 1 }} />
<StyledButton
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
size={'large'}
onClick={form.submit}
>
{t('form:next')}
</StyledButton>
</div>
</Form>
</Card>
)
case 'end':
return (
<Page
page={endPage}
design={design}
next={() => {
window.location.reload()
}}
/>
)
}
}
return (
<MyCard background={design.colors.background}>
<Omf />
<Spin spinning={loading}>{render()}</Spin>
</MyCard>
)
}

View File

@ -1,65 +0,0 @@
import { Card } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
FormPublicDesignFragment,
FormPublicPageFragment,
} from '../../../../graphql/fragment/form.public.fragment'
import { StyledButton } from '../../../styled/button'
import { StyledH1 } from '../../../styled/h1'
import { StyledMarkdown } from '../../../styled/markdown'
import { PageButtons } from '../page.buttons'
interface Props {
page: FormPublicPageFragment
design: FormPublicDesignFragment
next?: () => void
prev?: () => void
}
export const Page: React.FC<Props> = ({ design, page, next, prev }) => {
const { t } = useTranslation()
return (
<Card>
<StyledH1 design={design} type={'question'}>
{page.title}
</StyledH1>
<StyledMarkdown design={design} type={'question'}>{page.paragraph}</StyledMarkdown>
<div
style={{
padding: 32,
display: 'flex',
}}
>
{prev && (
<StyledButton
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
onClick={prev}
>
{t('form:restart')}
</StyledButton>
)}
<PageButtons buttons={page.buttons} />
<div style={{ flex: 1 }} />
{next && (
<StyledButton
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
size={'large'}
onClick={next}
>
{page.buttonText || t('form:continue')}
</StyledButton>
)}
</div>
</Card>
)
}

View File

@ -1,7 +0,0 @@
import { FormPublicFragment } from '../../../graphql/fragment/form.public.fragment'
import { Submission } from '../../use.submission'
export interface LayoutProps {
form: FormPublicFragment
submission: Submission
}

View File

@ -1,34 +0,0 @@
import { Space } from 'antd'
import React from 'react'
import { FormPublicPageButtonFragment } from '../../../graphql/fragment/form.public.fragment'
import { StyledButton } from '../../styled/button'
interface Props {
buttons: FormPublicPageButtonFragment[]
}
export const PageButtons: React.FC<Props> = ({ buttons }) => {
if (buttons.length === 0) {
return null
}
return (
<Space>
{buttons.map((button, key) => {
return (
<StyledButton
background={button.bgColor}
color={button.color}
highlight={button.activeColor}
key={key}
href={button.url}
target={'_blank'}
rel={'noreferrer'}
>
{button.text}
</StyledButton>
)
})}
</Space>
)
}

View File

@ -1,119 +0,0 @@
import { Form, message } from 'antd'
import { useForm } from 'antd/lib/form/Form'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
FormPublicDesignFragment,
FormPublicFieldFragment,
} from '../../../../graphql/fragment/form.public.fragment'
import { StyledButton } from '../../../styled/button'
import { StyledH1 } from '../../../styled/h1'
import { StyledMarkdown } from '../../../styled/markdown'
import { useRouter } from '../../../use.router'
import { fieldTypes } from '../../types'
interface Props {
focus: boolean
field: FormPublicFieldFragment
design: FormPublicDesignFragment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
save: (data: any) => void
next: () => void
prev: () => void
}
export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...props }) => {
const [form] = useForm()
const router = useRouter()
const { t } = useTranslation()
const FieldInput = (fieldTypes[field.type] || fieldTypes[field.type]).inputFormField()
const finish = (data) => {
console.log('received field data', data)
save(data)
next()
}
const error = async () => {
await message.error('Check inputs!')
}
const getUrlDefault = (): string => {
if (router.query[field.id]) {
return router.query[field.id] as string
}
if (router.query[field.slug]) {
return router.query[field.slug] as string
}
return undefined
}
return (
<Form
form={form}
onFinish={finish}
onFinishFailed={error}
{...props}
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
padding: 32,
justifyContent: 'flex-end',
}}
>
<StyledH1 design={design} type={'question'}>
{field.title}
</StyledH1>
{field.description && (
<StyledMarkdown design={design} type={'question'}>{field.description}</StyledMarkdown>
)}
<FieldInput
design={design}
field={field}
focus={props.focus}
urlValue={getUrlDefault()}
/>
</div>
<div
style={{
padding: 32,
display: 'flex',
}}
>
<StyledButton
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
onClick={prev}
>
{t('form:previous')}
</StyledButton>
<div style={{ flex: 1 }} />
<StyledButton
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
size={'large'}
onClick={form.submit}
>
{t('form:next')}
</StyledButton>
</div>
</Form>
)
}

View File

@ -1,122 +0,0 @@
import { Modal } from 'antd'
import debug from 'debug'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import SwiperClass from 'swiper'
import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'
import { Omf } from '../../../omf'
import { useWindowSize } from '../../../use.window.size'
import { LayoutProps } from '../layout.props'
import { Field } from './field'
import { FormPage } from './page'
const logger = debug('layout/slider')
export const SliderLayout: React.FC<LayoutProps> = (props) => {
const { t } = useTranslation()
const [swiper, setSwiper] = useState<SwiperClass>(null)
const { height } = useWindowSize()
const { design, startPage, endPage, fields } = props.form
const { finish, setField } = props.submission
const goNext = () => {
if (!swiper) return
logger('goNext')
swiper.allowSlideNext = true
swiper.slideNext()
swiper.allowSlideNext = false
}
const goPrev = () => {
if (!swiper) {
return
}
logger('goPrevious')
swiper.slidePrev()
}
const swiperConfig: SwiperProps = {
direction: 'vertical',
allowSlideNext: false,
allowSlidePrev: true,
noSwiping: true,
updateOnWindowResize: true,
}
return (
<div
className={'swiper-container'}
style={{
background: design.colors.background,
}}
>
<Omf />
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
<Swiper
height={height}
{...swiperConfig}
onSwiper={next => {
logger('setSwiper')
setSwiper(next)
}}
>
{[
startPage.show ? (
<SwiperSlide key={'start'}>
<FormPage page={startPage} design={design} next={goNext} prev={goPrev} />
</SwiperSlide>
) : undefined,
...fields
.map((field, i) => {
if (field.type === 'hidden') {
return null
}
return (
<SwiperSlide key={field.id}>
<Field
field={field}
focus={swiper?.activeIndex === (startPage.show ? 1 : 0) + i}
design={design}
save={async (values: { [key: string]: unknown }) => {
await setField(field.id, values[field.id])
if (fields.length === i + 1) {
await finish()
}
}}
next={() => {
if (fields.length === i + 1) {
// prevent going back!
swiper.allowSlidePrev = true
if (!endPage.show) {
Modal.success({
content: t('form:submitted'),
okText: t('from:restart'),
onOk: () => {
window.location.reload()
},
})
}
}
goNext()
}}
prev={goPrev}
/>
</SwiperSlide>
)
})
.filter((e) => e !== null),
endPage.show ? (
<SwiperSlide key={'end'}>
<FormPage page={endPage} design={design} next={finish} prev={goPrev} />
</SwiperSlide>
) : undefined,
].filter((e) => !!e)}
</Swiper>
</div>
)
}

View File

@ -1,20 +0,0 @@
.main {
display: flex;
flex-direction: column;
height: 100%;
.content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
overflow: auto;
padding: 16px;
@media (max-width: 600px) {
display: block;
}
}
}

View File

@ -1,69 +0,0 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
FormPublicDesignFragment,
FormPublicPageFragment,
} from '../../../../graphql/fragment/form.public.fragment'
import { StyledButton } from '../../../styled/button'
import { StyledH1 } from '../../../styled/h1'
import { StyledMarkdown } from '../../../styled/markdown'
import { PageButtons } from '../page.buttons'
import scss from './page.module.scss'
interface Props {
page: FormPublicPageFragment
design: FormPublicDesignFragment
className?: string
next: () => void
prev: () => void
}
export const FormPage: React.FC<Props> = ({ page, design, next, prev, className, ...props }) => {
const { t } = useTranslation()
if (!page.show) {
return null
}
return (
<div className={[scss.main, className].filter((c) => !!c).join(' ')} {...props}>
<div className={scss.content}>
<StyledH1 design={design} type={'question'}>
{page.title}
</StyledH1>
<StyledMarkdown design={design} type={'question'}>{page.paragraph}</StyledMarkdown>
</div>
<div
style={{
padding: 32,
display: 'flex',
}}
>
{prev && (
<StyledButton
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
onClick={prev}
>
{t('form:previous')}
</StyledButton>
)}
<PageButtons buttons={page.buttons} />
<div style={{ flex: 1 }} />
<StyledButton
background={design.colors.button}
color={design.colors.buttonText}
highlight={design.colors.buttonActive}
size={'large'}
onClick={next}
>
{page.buttonText || t('form:continue')}
</StyledButton>
</div>
</div>
)
}

70
components/form/page.tsx Normal file
View File

@ -0,0 +1,70 @@
import {Space} from 'antd'
import React from 'react'
import {FormDesignFragment, FormPageFragment} from '../../graphql/fragment/form.fragment'
import {StyledButton} from '../styled/button'
import {StyledH1} from '../styled/h1'
import {StyledP} from '../styled/p'
interface Props {
type: 'start' | 'end'
page: FormPageFragment
design: FormDesignFragment
next: () => any
prev: () => any
}
export const FormPage: React.FC<Props> = ({page, design, next, prev, type, children, ...props}) => {
if (!page.show) {
return null
}
return (
<div style={{
display: 'flex',
flexDirection: 'column',
}} {...props}>
<div style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
}}>
<StyledH1 design={design} type={'question'}>{page.title}</StyledH1>
<StyledP design={design} type={'question'}>{page.paragraph}</StyledP>
</div>
<div style={{
padding: 32,
display: 'flex',
}}>
{page.buttons.length > 0 && (
<Space>
{page.buttons.map((button, key) => {
return (
<StyledButton
background={button.bgColor}
color={button.color}
highlight={button.activeColor}
key={key}
href={button.url}
target={'_blank'}
>{button.text}</StyledButton>
)
})}
</Space>
)}
<div style={{flex: 1}} />
<StyledButton
background={design.colors.buttonColor}
color={design.colors.buttonTextColor}
highlight={design.colors.buttonActiveColor}
size={'large'}
onClick={next}
>{page.buttonText || 'Continue'}</StyledButton>
</div>
</div>
)
}

View File

@ -1,38 +0,0 @@
import React, { ComponentType } from 'react'
import { FieldAdminProps } from './field.admin.props'
import { FieldInputProps } from './field.input.props'
export abstract class AbstractType<A = any> {
public parseValue(raw: string): A {
return JSON.parse(raw) as A
}
public parseUrlValue(raw: string): A {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return raw as any
}
public stringifyValue(raw: string): string {
return raw
}
public displayValue(raw: string): JSX.Element {
const data = this.parseValue(raw)
if (Array.isArray(data)) {
return (
<ul>
{data.map(r => (
<li key={r}>{JSON.stringify(r)}</li>
))}
</ul>
)
}
return <div>{this.stringifyValue(raw)}</div>
}
public abstract adminFormField(): ComponentType<FieldAdminProps>
public abstract inputFormField(): ComponentType<FieldInputProps>
}

View File

@ -1,61 +0,0 @@
import { Checkbox, Form } from 'antd'
import debug from 'debug'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledCheckbox } from '../../../styled/checkbox'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('checkbox.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function CheckboxInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation()
let initialValue: string = undefined
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={field.options
.map((option) => option.value)
.find((value) => value === initialValue)}
>
<Checkbox.Group>
{field.options
.filter((option) => option.key === null)
.map((option, i) => (
<StyledCheckbox
design={design}
value={option.value}
key={option.value}
autoFocus={i === 0 && focus}
>
{option.title || option.value}
</StyledCheckbox>
))}
</Checkbox.Group>
</Form.Item>
</div>
)
}

View File

@ -1,15 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class CheckboxType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./checkbox.admin').then(c => c.CheckboxAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./checkbox.input').then(c => c.builder(this)));
}
}

View File

@ -0,0 +1,51 @@
import {Form} from 'antd'
import dayjs, {Dayjs} from 'dayjs'
import moment from 'moment'
import React, {useEffect, useState} from 'react'
import {StyledDateInput} from '../../styled/date.input'
import {FieldTypeProps} from './type.props'
export const DateType: React.FC<FieldTypeProps> = ({ field, design}) => {
const [min, setMin] = useState<Dayjs>()
const [max, setMax] = useState<Dayjs>()
useEffect(() => {
field.options.forEach(option => {
if (option.key === 'min') {
setMin(dayjs(option.value))
}
if (option.key === 'max') {
setMax(dayjs(option.value))
}
})
}, [field])
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ required: field.required, message: 'Please provide Information' },
]}
getValueFromEvent={e => e.format('YYYY-MM-DD')}
getValueProps={e => ({value: e ? moment(e) : undefined})}
initialValue={field.value ? moment(field.value) : undefined}
>
<StyledDateInput
size={'large'}
design={design}
autoFocus
disabledDate={(d: any) => {
if (min && min.isAfter(d)) {
return true
}
if (max && max.isBefore(d)) {
return true
}
return false
}}
/>
</Form.Item>
</div>
)
}

View File

@ -1,50 +0,0 @@
import { DatePicker, Form } from 'antd'
import moment, { Moment } from 'moment'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldAdminProps } from '../field.admin.props'
export const DateAdmin: React.FC<FieldAdminProps> = ({ field }) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:date.default')}
name={[field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
getValueFromEvent={(e: Moment) => (e ? e.format('YYYY-MM-DD') : undefined)}
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
>
<DatePicker format={'YYYY-MM-DD'} />
</Form.Item>
<Form.Item
label={t('type:date.min')}
name={[
field.name as string,
'optionKeys',
'min',
]}
labelCol={{ span: 6 }}
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
>
<DatePicker />
</Form.Item>
<Form.Item
label={t('type:date.max')}
name={[
field.name as string,
'optionKeys',
'max',
]}
labelCol={{ span: 6 }}
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
>
<DatePicker />
</Form.Item>
</div>
)
}

View File

@ -1,78 +0,0 @@
import { Form } from 'antd'
import dayjs, { Dayjs } from 'dayjs'
import debug from 'debug'
import moment, { Moment } from 'moment'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StyledDateInput } from '../../../styled/date.input'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('date.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function DateInput ({
field,
design,
urlValue,
focus,
}) {
const [min, setMin] = useState<Dayjs>()
const [max, setMax] = useState<Dayjs>()
const { t } = useTranslation()
useEffect(() => {
field.options.forEach((option) => {
if (option.key === 'min') {
setMin(dayjs(option.value))
}
if (option.key === 'max') {
setMax(dayjs(option.value))
}
})
}, [field])
let initialValue: Moment = undefined
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
initialValue={initialValue}
>
<StyledDateInput
autoFocus={focus}
size={'large'}
design={design}
disabledDate={(d: Moment) => {
if (min && min.isAfter(d.toDate())) {
return true
}
if (max && max.isBefore(d.toDate())) {
return true
}
return false
}}
/>
</Form.Item>
</div>
)
}

View File

@ -1,24 +0,0 @@
import moment, { Moment } from 'moment'
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class DateType extends AbstractType<Moment> {
parseValue(raw: string): Moment {
return moment(JSON.parse(raw))
}
parseUrlValue(raw: string): Moment {
return moment(raw)
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./date.admin').then(c => c.DateAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./date.input').then(c => c.builder(this)));
}
}

View File

@ -0,0 +1,26 @@
import {Form, Select} from 'antd'
import React, {useState} from 'react'
import {StyledSelect} from '../../styled/select'
import {FieldTypeProps} from './type.props'
export const DropdownType: React.FC<FieldTypeProps> = ({field, design}) => {
const [open, setOpen] = useState(false)
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ required: field.required, message: 'Please provide Information' },
]}
initialValue={field.value || null}
>
<StyledSelect design={design} open={open} onBlur={() => setOpen(false)} onFocus={() => setOpen(true)} onSelect={() => setOpen(false)}>
{field.options.filter(option => option.key === null).map(option => (
<Select.Option value={option.value} key={option.value}>OK{option.title || option.value}</Select.Option>
))}
</StyledSelect>
</Form.Item>
</div>
)
}

View File

@ -1,77 +0,0 @@
import { Button, Col, Form, Input, Row } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldAdminProps } from '../field.admin.props'
export const DropdownAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:dropdown.default')}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
<Form.List name={[props.field.name as string, 'options']}>
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item
wrapperCol={{
sm: { offset: index === 0 ? 0 : 6 },
}}
labelCol={{ span: 6 }}
label={index === 0 ? t('type:dropdown.options') : ''}
key={field.key}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
wrapperCol={{ span: 24 }}
name={[field.name, 'title']}
style={{ marginBottom: 0 }}
>
<Input placeholder={t('type:dropdown.titlePlaceholder')} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
wrapperCol={{ span: 24 }}
name={[field.name, 'value']}
style={{ marginBottom: 0 }}
rules={[{ required: true, message: t('validation:valueRequired') }]}
>
<Input placeholder={t('type:dropdown.valuePlaceholder')} />
</Form.Item>
</Col>
<Col span={4}>
<Button danger onClick={() => remove(index)}>
{t('type:dropdown.removeOption')}
</Button>
</Col>
</Row>
</Form.Item>
))}
<Form.Item
wrapperCol={{
sm: { offset: 6 },
}}
labelCol={{ span: 6 }}
>
<Button type={'dashed'} onClick={() => add()}>
{t('type:dropdown.addOption')}
</Button>
</Form.Item>
</div>
)
}}
</Form.List>
</div>
)
}

View File

@ -1,62 +0,0 @@
import { Form, Select } from 'antd'
import debug from 'debug'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StyledSelect } from '../../../styled/select'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/dropdown')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function DateInput ({
field,
design,
urlValue,
focus,
}) {
const [open, setOpen] = useState(false)
const { t } = useTranslation()
let initialValue = null
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue}
>
<StyledSelect
autoFocus={focus}
design={design}
open={open}
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
onSelect={() => setOpen(false)}
>
{field.options
.filter((option) => option.key === null)
.map((option) => (
<Select.Option value={option.value} key={option.value}>
{option.title || option.value}
</Select.Option>
))}
</StyledSelect>
</Form.Item>
</div>
)
}

View File

@ -1,15 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class DropdownType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./dropdown.admin').then(c => c.DropdownAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./dropdown.input').then(c => c.builder(this)));
}
}

View File

@ -0,0 +1,25 @@
import {Form} from 'antd'
import React from 'react'
import {StyledInput} from '../../styled/input'
import {FieldTypeProps} from './type.props'
export const EmailType: React.FC<FieldTypeProps> = ({field, design}) => {
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ required: field.required, message: 'Please provide Information' },
{ type: 'email', message: 'Must be a valid email' }
]}
initialValue={field.value}
>
<StyledInput
design={design}
allowClear
size={'large'}
/>
</Form.Item>
</div>
)
}

View File

@ -1,21 +0,0 @@
import { Form, Input } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldAdminProps } from '../field.admin.props'
export const EmailAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:email.default')}
name={[props.field.name as string, 'defaultValue']}
rules={[{ type: 'email', message: t('validation:emailRequired') }]}
labelCol={{ span: 6 }}
>
<Input type={'email'} />
</Form.Item>
</div>
)
}

View File

@ -1,49 +0,0 @@
import { Form } from 'antd'
import debug from 'debug'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledInput } from '../../../styled/input'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('email.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function EmailInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation()
let initialValue = null
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id]}
rules={[
{ required: field.required, message: t('validation:valueRequired') },
{ type: 'email', message: t('validation:invalidEmail') },
]}
initialValue={initialValue}
>
<StyledInput autoFocus={focus} design={design} allowClear size={'large'} />
</Form.Item>
</div>
)
}

View File

@ -1,15 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class EmailType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./email.admin').then(c => c.EmailAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./email.input').then(c => c.builder(this)));
}
}

View File

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

View File

@ -1,6 +0,0 @@
import { ComponentType } from 'react'
import { AbstractType } from './abstract.type'
import { FieldInputProps } from './field.input.props'
export type FieldInputBuilderType<A = AbstractType> = (type: A) => ComponentType<FieldInputProps>

View File

@ -1,11 +0,0 @@
import {
FormPublicDesignFragment,
FormPublicFieldFragment,
} from '../../../graphql/fragment/form.public.fragment'
export interface FieldInputProps {
field: FormPublicFieldFragment
design: FormPublicDesignFragment
focus?: boolean
urlValue?: string
}

View File

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

View File

@ -1,15 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class HiddenType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./hidden.admin').then(c => c.HiddenAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return null;
}
}

View File

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

View File

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

View File

@ -1,19 +0,0 @@
/*
TODO
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class ImageType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./dropdown.admin').then(c => c.DropdownAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./dropdown.input').then(c => c.builder(this)));
}
}
*/
export {}

View File

@ -1,34 +1,27 @@
import { AbstractType } from './abstract.type' import React from 'react'
import { CheckboxType } from './checkbox' import {DateType} from './date.type'
import { DateType } from './date' import {DropdownType} from './dropdown.type'
import { DropdownType } from './dropdown' import {EmailType} from './email.type'
import { EmailType } from './email' import {LinkType} from './link.type'
import { HiddenType } from './hidden' import {NumberType} from './number.type'
import { LinkType } from './link' import {RadioType} from './radio.type'
import { LocationType } from './location' import {RatingType} from './rating.type'
import { NumberType } from './number' import {TextType} from './text.type'
import { RadioType } from './radio' import {TextareaType} from './textarea.type'
import { RatingType } from './rating' import {FieldTypeProps} from './type.props'
import { SliderType } from './slider' import {YesNoType} from './yes_no.type'
import { TextareaType } from './textarea'
import { TextfieldType } from './textfield'
import { YesNoType } from './yes_no'
export const fieldTypes: { export const fieldTypes: {
[key: string]: AbstractType [key: string]: React.FC<FieldTypeProps>
} = { } = {
checkbox: new CheckboxType(), 'textfield': TextType,
date: new DateType(), 'date': DateType,
dropdown: new DropdownType(), 'email': EmailType,
email: new EmailType(), 'textarea': TextareaType,
hidden: new HiddenType(), 'link': LinkType,
link: new LinkType(), 'dropdown': DropdownType,
location: new LocationType(), 'rating': RatingType,
number: new NumberType(), 'radio': RadioType,
radio: new RadioType(), 'yes_no': YesNoType,
rating: new RatingType(), 'number': NumberType,
slider: new SliderType(),
textarea: new TextareaType(),
textfield: new TextfieldType(),
yes_no: new YesNoType(),
} }

View File

@ -0,0 +1,25 @@
import {Form} from 'antd'
import React from 'react'
import {StyledInput} from '../../styled/input'
import {FieldTypeProps} from './type.props'
export const LinkType: React.FC<FieldTypeProps> = ({field, design}) => {
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ required: field.required, message: 'Please provide Information' },
{ type: 'url', message: 'Must be a valid URL' }
]}
initialValue={field.value}
>
<StyledInput
design={design}
allowClear
size={'large'}
/>
</Form.Item>
</div>
)
}

View File

@ -1,15 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class LinkType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./link.admin').then(c => c.LinkAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./link.input').then(c => c.builder(this)));
}
}

View File

@ -1,21 +0,0 @@
import { Form, Input } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldAdminProps } from '../field.admin.props'
export const LinkAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:link.default')}
name={[props.field.name as string, 'defaultValue']}
rules={[{ type: 'url', message: t('validation:invalidUrl') }]}
labelCol={{ span: 6 }}
>
<Input type={'url'} />
</Form.Item>
</div>
)
}

View File

@ -1,49 +0,0 @@
import { Form } from 'antd'
import debug from 'debug'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledInput } from '../../../styled/input'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('link.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function LinkInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation()
let initialValue = null
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id]}
rules={[
{ required: field.required, message: t('validation:valueRequired') },
{ type: 'url', message: t('validation:invalidUrl') },
]}
initialValue={initialValue}
>
<StyledInput autoFocus={focus} design={design} allowClear size={'large'} />
</Form.Item>
</div>
)
}

View File

@ -1,34 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class LocationType extends AbstractType<{ lat: number, lng: number }> {
parseUrlValue(raw: string): { lat: number; lng: number } {
if (raw.includes(',')) {
const [lat, lng] = raw.split(',')
return {
lat: parseFloat(lat),
lng: parseFloat(lng),
}
}
throw new Error('no separator found')
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./location.admin').then(c => c.LocationAdmin), { ssr: false });
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./location.input').then(c => c.builder(this)), { ssr: false });
}
stringifyValue(raw: string): string {
const data = this.parseValue(raw)
return `${data.lat}, ${data.lng}`
}
}

View File

@ -1,142 +0,0 @@
import { Alert, Form, Input, InputNumber, Space } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { MapContainer, TileLayer } from 'react-leaflet'
import { DraggableMarker } from '../../../map/draggable.marker'
import { FieldAdminProps } from '../field.admin.props'
export const LocationAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:location:default')}
labelCol={{ span: 6 }}
>
<Space>
<Form.Item
name={[
props.field.name as string,
'defaultValue',
'lat',
]}
noStyle
>
<InputNumber addonAfter={'LAT'} precision={7} step={0.00001} max={90} min={-90} />
</Form.Item>
<Form.Item
name={[
props.field.name as string,
'defaultValue',
'lng',
]}
noStyle
>
<InputNumber addonAfter={'LNG'} precision={7} step={0.00001} max={180} min={-180} />
</Form.Item>
</Space>
</Form.Item>
<Form.Item
label={t('type:location.initialZoom')}
name={[
props.field.name as string,
'optionKeys',
'initialZoom',
]}
labelCol={{ span: 6 }}
initialValue={1}
>
<InputNumber precision={0} min={1} max={18} />
</Form.Item>
<Form.Item
label={t('type:location.tiles')}
name={[
props.field.name as string,
'optionKeys',
'tiles',
]}
labelCol={{ span: 6 }}
initialValue={'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'}
>
<Input placeholder={'https://tile.openstreetmap.org/{z}/{x}/{y}.png'} />
</Form.Item>
<Form.Item shouldUpdate>
{(form) => {
//const prefix = React.useContext(FormItemContext).prefixName
const prefix = (form as any).prefixName
const zoom = form.getFieldValue([
...prefix,
props.field.name as string,
'optionKeys',
'initialZoom',
])
const center = form.getFieldValue([
...prefix,
props.field.name as string,
'defaultValue',
])
const tiles = form.getFieldValue([
...prefix,
props.field.name as string,
'optionKeys',
'tiles',
])
if (!tiles) {
return <Alert message={'Tiles missing!'} />
}
return (
<div>
<MapContainer
center={center}
zoom={zoom}
style={{ height: 300, width: '100%' }}
>
<TileLayer
attribution='&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

@ -1,151 +0,0 @@
import { Alert, Form, InputNumber, Space, Spin } from 'antd'
import debug from 'debug'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { MapContainer, TileLayer } from 'react-leaflet'
import { DraggableMarker } from '../../../map/draggable.marker'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('location.number')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function LocationInput ({
field,
urlValue,
}) {
const [initialZoom, setInitialZoom] = useState<number>(13)
const [tiles, setTiles] = useState<string>()
const [loading, setLoading] = useState(true)
const { t } = useTranslation()
useEffect(() => {
field.options.forEach((option) => {
if (option.key === 'initialZoom') {
try {
setInitialZoom(JSON.parse(option.value))
} catch (e) {
logger('invalid initialZoom value %O', e)
}
}
if (option.key === 'tiles') {
try {
setTiles(JSON.parse(option.value))
} catch (e) {
logger('invalid tiles value %O', e)
}
}
})
setLoading(false)
}, [field])
let initialValue: { lat: number, lng: number } = undefined
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
try {
initialValue = parseUrlValue(urlValue)
} catch (e) {
logger('invalid url value %O', e)
}
}
if (loading) {
return (
<div>
<Spin />
</div>
)
}
if (!tiles) {
return <Alert message={'Tiles missing!'} />
}
return (
<div>
<Form.Item>
<Space>
<Form.Item
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
name={[
field.id,
'lat',
]}
initialValue={initialValue?.lat}
noStyle
>
<InputNumber addonAfter={'LAT'} precision={7} step={0.00001} max={90} min={-90} />
</Form.Item>
<Form.Item
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
name={[
field.id,
'lng',
]}
initialValue={initialValue?.lng}
noStyle
>
<InputNumber addonAfter={'LNG'} precision={7} step={0.00001} max={180} min={-180} />
</Form.Item>
</Space>
</Form.Item>
<Form.Item dependencies={[[field.id, 'lat'], [field.id, 'lng']]}>
{(form) => {
const center = form.getFieldValue([field.id])
return (
<div>
<MapContainer
center={initialValue}
zoom={initialZoom}
style={{ height: 300, width: '100%' }}
>
<TileLayer
attribution='&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

@ -0,0 +1,24 @@
import {Form} from 'antd'
import React from 'react'
import {StyledNumberInput} from '../../styled/number.input'
import {FieldTypeProps} from './type.props'
export const NumberType: React.FC<FieldTypeProps> = ({field, design}) => {
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ type: 'number', message: 'Must be a valid URL' },
{ required: field.required, message: 'Please provide Information' },
]}
initialValue={parseFloat(field.value)}
>
<StyledNumberInput
design={design}
size={'large'}
/>
</Form.Item>
</div>
)
}

View File

@ -1,19 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class NumberType extends AbstractType<number> {
parseUrlValue(raw: string): number {
return parseFloat(raw)
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./number.admin').then(c => c.NumberAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./number.input').then(c => c.builder(this)));
}
}

View File

@ -1,20 +0,0 @@
import { Form, InputNumber } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FieldAdminProps } from '../field.admin.props'
export const NumberAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation()
return (
<div>
<Form.Item
label={t('type:number:default')}
name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }}
>
<InputNumber precision={2} />
</Form.Item>
</div>
)
}

View File

@ -1,49 +0,0 @@
import { Form } from 'antd'
import debug from 'debug'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledNumberInput } from '../../../styled/number.input'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('number.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function NumberInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation()
let initialValue: number = undefined
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id]}
rules={[
{ type: 'number', message: t('validation:invalidNumber') },
{ required: field.required, message: t('validation:valueRequired') },
]}
initialValue={initialValue}
>
<StyledNumberInput autoFocus={focus} design={design} size={'large'} />
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,30 @@
import {Form, Radio} from 'antd'
import React from 'react'
import {StyledRadio} from '../../styled/radio'
import {FieldTypeProps} from './type.props'
export const RadioType: React.FC<FieldTypeProps> = ({field, design}) => {
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ required: field.required, message: 'Please provide Information' },
]}
initialValue={field.options.map(option => option.value).find(value => value === field.value)}
>
<Radio.Group>
{field.options.filter(option => option.key === null).map(option => (
<StyledRadio
design={design}
value={option.value}
key={option.value}
>
{option.title || option.value}
</StyledRadio>
))}
</Radio.Group>
</Form.Item>
</div>
)
}

View File

@ -1,15 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class RadioType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./radio.admin').then(c => c.RadioAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./radio.input').then(c => c.builder(this)));
}
}

View File

@ -1,55 +0,0 @@
import { Form, Radio } from 'antd'
import debug from 'debug'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyledRadio } from '../../../styled/radio'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('radio.input')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function RadioInput ({
field,
design,
urlValue,
}) {
const { t } = useTranslation()
let initialValue: string = undefined
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = parseUrlValue(urlValue)
}
return (
<div>
<Form.Item
name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={field.options
.map((option) => option.value)
.find((value) => value === initialValue)}
>
<Radio.Group>
{field.options
.filter((option) => option.key === null)
.map((option) => (
<StyledRadio design={design} value={option.value} key={option.value}>
{option.title || option.value}
</StyledRadio>
))}
</Radio.Group>
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,21 @@
import {Form, Rate} from 'antd'
import React from 'react'
import {FieldTypeProps} from './type.props'
export const RatingType: React.FC<FieldTypeProps> = ({field}) => {
// TODO add ratings
return (
<div>
<Form.Item
name={[field.id, 'value']}
rules={[
{ required: field.required, message: 'Please provide Information' },
]}
initialValue={parseFloat(field.value)}
>
<Rate allowHalf />
</Form.Item>
</div>
)
}

View File

@ -1,19 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class 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

@ -1,21 +0,0 @@
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

@ -1,43 +0,0 @@
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

@ -1,19 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class SliderType extends AbstractType<number> {
parseUrlValue(raw: string): number {
return parseFloat(raw)
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./slider.admin').then(c => c.SliderAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./slider.input').then(c => c.builder(this)));
}
}

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