├── .env.production ├── .gitignore ├── src ├── react-app-env.d.ts ├── assets │ ├── img-logo.png │ └── webfonts │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.ttf │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.ttf │ │ ├── fa-solid-900.woff │ │ ├── fa-solid-900.woff2 │ │ ├── inspiro-icons.ttf │ │ ├── inspiro-icons.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.woff │ │ └── fa-regular-400.woff2 ├── utils │ ├── common.ts │ ├── routes.tsx │ ├── demoFormLayouts.ts │ └── formBuilderUtils.ts ├── redux │ ├── entities.ts │ ├── common.ts │ ├── uireducers.ts │ ├── hooks.ts │ ├── store.ts │ ├── uireducers │ │ ├── progress.ts │ │ └── modalstrip.ts │ └── entities │ │ └── formBuilderEntity.ts ├── setupTests.ts ├── pages │ ├── Error404.tsx │ ├── FormBuilderPage.tsx │ └── TemplatesPage.tsx ├── layouts │ └── MainLayout.tsx ├── App.test.tsx ├── components │ ├── FormBuilder │ │ ├── styles.scss │ │ ├── hooks │ │ │ ├── useFormPreview.ts │ │ │ └── useFormBuilder.ts │ │ ├── subcomponents │ │ │ ├── styles.scss │ │ │ ├── FormPreview.tsx │ │ │ ├── ControlDragComponent.tsx │ │ │ ├── ManageItemsListComponent.tsx │ │ │ ├── form-preview │ │ │ │ ├── StepperFormPreview.tsx │ │ │ │ └── RenderItem.tsx │ │ │ ├── DropContainerComponent.tsx │ │ │ ├── ControlViewComponent.tsx │ │ │ └── EditPropertiesComponent.tsx │ │ ├── LeftSidebar.tsx │ │ └── FormBuilder.tsx │ ├── FormTemplates │ │ ├── styles.scss │ │ ├── FormLayoutComponent.tsx │ │ └── NewFormDialogComponent.tsx │ ├── BackdropCircularProgressComponent.tsx │ ├── ModalStrip.tsx │ └── Navbar.tsx ├── reportWebVitals.ts ├── index.tsx ├── App.tsx ├── global-hooks │ └── useModalStrip.tsx ├── index.css └── types │ └── FormTemplateTypes.d.ts ├── public ├── favicon.ico ├── logo192.png ├── logo512.png └── index.html ├── .vscode └── launch.json ├── tsconfig.json ├── webpack.prod.js ├── webpack.dev.js ├── README.md ├── webpack.common.js └── package.json /.env.production: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env.localhost -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/img-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/img-logo.png -------------------------------------------------------------------------------- /src/assets/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /src/assets/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /src/assets/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /src/assets/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /src/assets/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /src/assets/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /src/assets/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /src/assets/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /src/assets/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /src/assets/webfonts/inspiro-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/inspiro-icons.ttf -------------------------------------------------------------------------------- /src/assets/webfonts/inspiro-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/inspiro-icons.woff -------------------------------------------------------------------------------- /src/assets/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /src/assets/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /src/assets/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtomato0129/FormBuilder/HEAD/src/assets/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | export const generateID = () => { 2 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); 3 | }; -------------------------------------------------------------------------------- /src/redux/entities.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "@reduxjs/toolkit"; 2 | import formBuildeeEntity from "./entities/formBuilderEntity"; 3 | 4 | export default combineReducers({ 5 | formBuilder: formBuildeeEntity 6 | }); -------------------------------------------------------------------------------- /src/redux/common.ts: -------------------------------------------------------------------------------- 1 | export const saveToLocalStorage = (key: string, value:string)=>{ 2 | window.localStorage.setItem(key,value); 3 | } 4 | 5 | export const getFromLocalStorage = (key: string)=>{ 6 | return window.localStorage.getItem(key) as string; 7 | } -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/redux/uireducers.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "@reduxjs/toolkit"; 2 | import progressReducer from "./uireducers/progress"; 3 | import modalstripReducer from "./uireducers/modalstrip"; 4 | 5 | export default combineReducers({ 6 | progress: progressReducer, 7 | modalstrip: modalstripReducer 8 | }); -------------------------------------------------------------------------------- /src/pages/Error404.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | 3 | interface Error404Props { 4 | 5 | } 6 | 7 | const Error404: FunctionComponent = () => { 8 | return ( 9 | <> 10 |

404 Page

11 | 12 | ); 13 | } 14 | 15 | export default Error404; -------------------------------------------------------------------------------- /src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Outlet } from 'react-router-dom'; 3 | import Navbar from '../components/Navbar'; 4 | 5 | function MainLayout() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default MainLayout; -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/redux/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' 2 | import type { RootState, AppDispatch } from './store' 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch: () => AppDispatch = useDispatch 6 | export const useAppSelector: TypedUseSelectorHook = useSelector -------------------------------------------------------------------------------- /src/components/FormBuilder/styles.scss: -------------------------------------------------------------------------------- 1 | .created-form-layout, .new-form-layout{ 2 | max-width: 150px; 3 | border-radius: 9px; 4 | div{ 5 | transition: background-color 0.4s ease; 6 | 7 | &:hover{ 8 | background-color: rgba(235, 227, 227, 0.5); 9 | border-radius: 9px; 10 | } 11 | 12 | &:active{ 13 | background-color: rgba(0,0,0,0.1); 14 | border-radius: 9px; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/FormTemplates/styles.scss: -------------------------------------------------------------------------------- 1 | .created-form-layout, .new-form-layout{ 2 | max-width: 150px; 3 | border-radius: 9px; 4 | div{ 5 | transition: background-color 0.4s ease; 6 | 7 | &:hover{ 8 | background-color: rgba(235, 227, 227, 0.5); 9 | border-radius: 9px; 10 | } 11 | 12 | &:active{ 13 | background-color: rgba(0,0,0,0.1); 14 | border-radius: 9px; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/components/FormBuilder/hooks/useFormPreview.ts: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | const useFormPreview = ()=>{ 4 | 5 | const [showPreview, setShowPreview] = useState(false); 6 | 7 | const openPreviewDrawer = ()=>{ 8 | setShowPreview(true); 9 | } 10 | 11 | const closePreviewDrawer = ()=>{ 12 | setShowPreview(false); 13 | } 14 | 15 | return { 16 | showPreview, 17 | openPreviewDrawer, 18 | closePreviewDrawer 19 | } 20 | }; 21 | 22 | export default useFormPreview; -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import entitiesReducer from './entities' 3 | import uireducers from './uireducers' 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | entities: entitiesReducer, 8 | uielements: uireducers 9 | }, 10 | }) 11 | 12 | // Infer the `RootState` and `AppDispatch` types from the store itself 13 | export type RootState = ReturnType 14 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 15 | export type AppDispatch = typeof store.dispatch -------------------------------------------------------------------------------- /src/redux/uireducers/progress.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const slice = createSlice({ 4 | name: "progressReducer", 5 | initialState: { 6 | isCircularProgressOpen: false 7 | }, 8 | reducers: { 9 | openCircularProgress: (state)=>{ 10 | state.isCircularProgressOpen = true; 11 | }, 12 | closeCircularProgress: (state)=>{ 13 | state.isCircularProgressOpen = false 14 | } 15 | }, 16 | }); 17 | 18 | export const { openCircularProgress, closeCircularProgress } = slice.actions; 19 | 20 | export default slice.reducer; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": false, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | import './assets/css/plugins.css'; 6 | import './assets/css/style.css'; 7 | import './index.css'; 8 | 9 | const root = ReactDOM.createRoot( 10 | document.getElementById('root') as HTMLElement 11 | ); 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | 18 | // If you want to start measuring performance in your app, pass a function 19 | // to log results (for example: reportWebVitals(console.log)) 20 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 21 | reportWebVitals(); 22 | -------------------------------------------------------------------------------- /src/utils/routes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { RouteObject } from "react-router-dom"; 3 | import MainLayout from "../layouts/MainLayout"; 4 | import FormBuilderPage from "../pages/FormBuilderPage"; 5 | import TemplatesPage from "../pages/TemplatesPage"; 6 | import Error404 from '../pages/Error404'; 7 | 8 | 9 | const routes: RouteObject[] = [ 10 | { 11 | path: "/", 12 | element: , 13 | errorElement: , 14 | children: [ 15 | { 16 | path: "formbuilder/:formId", 17 | element: , 18 | }, 19 | { 20 | path: "/", 21 | element: , 22 | }, 23 | ], 24 | }, 25 | ]; 26 | 27 | export default routes; -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import routes from './utils/routes'; 3 | import { RouterProvider, createBrowserRouter } from 'react-router-dom'; 4 | import { store } from "./redux/store"; 5 | import { Provider } from 'react-redux' 6 | import BackdropCircularProgressComponent from './components/BackdropCircularProgressComponent'; 7 | import ModalStrip from './components/ModalStrip'; 8 | 9 | function App() { 10 | 11 | const router = createBrowserRouter(routes) 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/global-hooks/useModalStrip.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useAppDispatch } from "../redux/hooks"; 3 | import { closeModal, openModal } from "../redux/uireducers/modalstrip"; 4 | 5 | function useModalStrip() { 6 | 7 | const [timeInterval, setTimeInterval] = useState(null); 8 | const dispatch = useAppDispatch(); 9 | 10 | const showModalStrip = async (modalType: string, message: string, time: number)=>{ 11 | if(timeInterval){ 12 | clearTimeout(timeInterval); 13 | } 14 | dispatch(openModal({modalType, message})); 15 | 16 | setTimeInterval( 17 | setTimeout(() => { 18 | dispatch(closeModal()); 19 | }, time) 20 | ); 21 | } 22 | 23 | return { showModalStrip }; 24 | } 25 | 26 | export default useModalStrip; -------------------------------------------------------------------------------- /src/redux/uireducers/modalstrip.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | 4 | interface ModalStripProp{ 5 | modalType: string | null; 6 | message: string | null; 7 | } 8 | 9 | const initialState: ModalStripProp = { 10 | modalType: null, 11 | message: null, 12 | } 13 | 14 | const slice = createSlice({ 15 | name: "modalStripReducer", 16 | initialState: initialState, 17 | reducers: { 18 | openModal: (state,action:PayloadAction)=>{ 19 | state.modalType = action.payload.modalType; 20 | state.message = action.payload.message; 21 | }, 22 | closeModal: (state)=>{ 23 | state.modalType = null; 24 | state.message = null; 25 | } 26 | }, 27 | }); 28 | 29 | export const { openModal, closeModal } = slice.actions; 30 | 31 | export default slice.reducer; -------------------------------------------------------------------------------- /src/components/BackdropCircularProgressComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, {FunctionComponent} from 'react'; 2 | import { Backdrop, CircularProgress } from '@mui/material'; 3 | import { useAppSelector } from '../redux/hooks'; 4 | 5 | interface BackdropCircularProgressComponentProps { 6 | 7 | } 8 | 9 | const BackdropCircularProgressComponent: FunctionComponent = (prop) => { 10 | 11 | const isCircularProgressOpen = useAppSelector((state)=>state.uielements.progress.isCircularProgressOpen); 12 | 13 | return ( 14 | <> 15 | theme.zIndex.drawer + 1 }} 17 | open={isCircularProgressOpen} 18 | > 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default BackdropCircularProgressComponent; -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const common = require("./webpack.common"); 3 | const { merge } = require("webpack-merge"); 4 | const Dotenv = require("dotenv-webpack"); 5 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 6 | const PACKAGE = require("./package.json"); 7 | const version = PACKAGE.version; 8 | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); 9 | 10 | module.exports = merge(common, { 11 | devtool: "source-map", 12 | mode: "production", 13 | output: { 14 | path: path.join(__dirname, "build"), 15 | filename: `bundle-${version}.js`, 16 | publicPath: '/' 17 | }, 18 | optimization: { 19 | minimizer: [new CssMinimizerPlugin()], 20 | }, 21 | plugins: [ 22 | new Dotenv({ 23 | path: "./.env.production", 24 | safe: true, 25 | allowEmptyValues: true, 26 | }), 27 | new HtmlWebpackPlugin({ 28 | title: "Form Builder", 29 | template: "./public/index.html", 30 | minify: false, 31 | }), 32 | ], 33 | }); -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: white; 3 | } 4 | 5 | .sidebar { 6 | max-height: 100%; 7 | overflow-y: scroll; 8 | overflow-x: hidden; 9 | background-color: #f8f9fa; 10 | border: 1px solid #f8f9fa; 11 | border-radius: 4px; 12 | padding: 20px !important; 13 | } 14 | 15 | .button-form { 16 | border-radius: 2px; 17 | margin-left: 15px; 18 | margin-bottom: 15px; 19 | padding-bottom: 20px; 20 | } 21 | 22 | .main-form-title { 23 | font-size: 14px; 24 | font-style: normal; 25 | font-weight: 600; 26 | text-transform: uppercase; 27 | line-height: 24px; 28 | letter-spacing: 1px; 29 | display: block; 30 | margin-inline-start: 0px; 31 | margin-inline-end: 0px; 32 | font-family: poppins,sans-serif; 33 | color: #1f1f1f; 34 | } 35 | 36 | .main-form-subtitle { 37 | font-size: 12px; 38 | font-style: normal; 39 | font-weight: 600; 40 | text-transform: uppercase; 41 | line-height: 24px; 42 | letter-spacing: 1px; 43 | display: block; 44 | margin-inline-start: 0px; 45 | margin-inline-end: 0px; 46 | font-family: poppins,sans-serif; 47 | color: #1f1f1f; 48 | } -------------------------------------------------------------------------------- /src/components/ModalStrip.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { useAppSelector } from "../redux/hooks"; 3 | 4 | interface ModalStripProps {} 5 | 6 | const ModalStrip: FunctionComponent = (props) => { 7 | const { modalType, message } = useAppSelector( 8 | (state) => state.uielements.modalstrip 9 | ); 10 | 11 | return ( 12 | <> 13 |
31 |
32 |
{message}
33 |
34 |
35 | 36 | ); 37 | }; 38 | 39 | export default ModalStrip; 40 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const common = require("./webpack.common"); 3 | const { merge } = require("webpack-merge"); 4 | const webpack = require("webpack"); 5 | const Dotenv = require("dotenv-webpack"); 6 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 7 | 8 | module.exports = merge(common, { 9 | devtool: "inline-source-map", 10 | mode: "development", 11 | output: { 12 | path: path.join(__dirname, "public/dist"), 13 | filename: `bundle.js`, 14 | publicPath: '/' 15 | }, 16 | devServer: { 17 | static: { 18 | directory: path.join(__dirname, "public"), 19 | }, 20 | port: 3000, 21 | hot: true, 22 | historyApiFallback: true 23 | }, 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | "process.env.NODE_ENV": JSON.stringify("development"), 27 | }), 28 | new HtmlWebpackPlugin({ 29 | title: "Form Builder", 30 | template: "./public/index.html", 31 | minify: false, 32 | }), 33 | new Dotenv({ 34 | path: "./.env.localhost", 35 | safe: true, 36 | allowEmptyValues: true, 37 | }), //in order for environment variable to work 38 | ], 39 | }); 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Form Builder React (Drag & Drop) 2 | 3 | ### Check out the [Live App](https://release.d2xczvacbbtxrp.amplifyapp.com/) 4 | 5 | ## About 6 | This application is a drag & drop form builder built in React and bundled with Webpack. It allows you to create multi-step forms by adding containers, with each container representing a new step. The elements within the containers are displayed on the UI based on the current step the user is in. 7 | 8 | Key features of the app include: 9 | 10 | - Create a new Template 11 | - Add Elements to templates by Dragging & Dropping elements to the form area. 12 | - Sort Elements inside containers by Dragging & Dropping. 13 | - Click Containers/Elements to edit their properties. 14 | - Save & Publish changes 15 | - Preview Changes on how it will look on a mobile phone. 16 | 17 | With these capabilities, the drag & drop form builder empowers you to easily create, customize, save, publish, and preview multi-step forms, streamlining the form-building process for your application or website. 18 | 19 | ## Dev Server 20 | This project uses npm version 18.1 21 | 22 | ### Available Scripts 23 | 24 | In the project directory, you can run: 25 | 26 | #### `npm start` 27 | 28 | In the project directory, you can run: 29 | 30 | #### `npm start` 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | <%= htmlWebpackPlugin.options.title %> 18 | 19 | 20 | 21 |
22 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/FormBuilder/subcomponents/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | html{ 3 | overflow-y: hidden !important; 4 | } 5 | 6 | .container-drop{ 7 | height: 100%; 8 | transition: 0.3s; 9 | margin-bottom: 20px; 10 | position: relative; 11 | padding: 30px; 12 | // padding-top: 0px; 13 | } 14 | 15 | .control-drop{ 16 | transition: 0.3s; 17 | } 18 | 19 | .container-actions{ 20 | // position: absolute; 21 | // border-radius: 0px 0px 0px 4px; 22 | // right: -3px; 23 | // top: -3px; 24 | // z-index: 23; 25 | transition: 0.2s; 26 | 27 | span{ 28 | cursor: pointer; 29 | padding: 0px 12px; 30 | } 31 | } 32 | 33 | .control-actions{ 34 | // position: absolute; 35 | // border-radius: 0px 0px 0px 4px; 36 | // right: 0px; 37 | // top: 0px; 38 | // z-index: 23; 39 | transition: 0.2s; 40 | 41 | span{ 42 | cursor: pointer; 43 | padding: 0px 12px; 44 | } 45 | } 46 | 47 | .control-input-trigger-buttons { 48 | height: 100px; 49 | width: 100px; 50 | display: flex; 51 | align-items: center; 52 | border: 1px solid rgba(0,0,0,0.1); 53 | justify-content: center; 54 | font-size: 1.5rem; 55 | cursor: pointer; 56 | background-color: #f8f9fa; 57 | border-radius: 6px; 58 | 59 | .sign-label{ 60 | letter-spacing: 3px; 61 | font-size: 2rem; 62 | font-weight: 800; 63 | color: rgba(0,0,0,0.3); 64 | border-bottom: 1px solid; 65 | padding: 20px 16px; 66 | } 67 | } -------------------------------------------------------------------------------- /src/types/FormTemplateTypes.d.ts: -------------------------------------------------------------------------------- 1 | export interface TemplateType{ 2 | formName: string, 3 | id: string, 4 | createdAt: number, 5 | updatedAt: number, 6 | lastPublishedAt: number, 7 | publishStatus: string, 8 | formLayoutComponents: FormLayoutComponentsType[], 9 | publishHistory: FormLayoutHistoryType[], 10 | creator: string 11 | } 12 | 13 | export interface FormLayoutComponentsType{ 14 | container:FormLayoutComponentContainerType, 15 | children: FormLayoutComponentChildrenType[] 16 | } 17 | 18 | export interface FormLayoutHistoryType{ 19 | lastPublishedAt: number, 20 | formLayoutComponents: FormLayoutComponentsType[] 21 | } 22 | 23 | interface FormLayoutComponentContainerType{ 24 | controlName: string, 25 | displayText: string, 26 | itemType: string, 27 | icon: string, 28 | heading: string, 29 | subHeading: string, 30 | id: string, 31 | desktopWidth?: number 32 | } 33 | 34 | interface FormLayoutComponentChildrenType{ 35 | controlName: string, 36 | displayText: string, 37 | description: string, 38 | labelName: string, 39 | itemType: string, 40 | icon: string, 41 | required: boolean, 42 | items?: FormLayoutCoponentChildrenItemsType[], 43 | category: string, 44 | index?: number, 45 | id: string, 46 | containerId: string, 47 | placeholder?: string, 48 | rows?: number, 49 | dataType?: string 50 | position?: number 51 | } 52 | 53 | interface FormLayoutCoponentChildrenItemsType{ 54 | id: string 55 | value: string 56 | label: string 57 | } -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./src/index.tsx", 3 | 4 | module: { 5 | rules: [ 6 | //babel loader -> JSX into JS 7 | { 8 | test: /\.(js|jsx|ts|tsx)$/, 9 | exclude: /node_modules/, 10 | resolve: { 11 | extensions: [".js", ".jsx",".ts", ".tsx"], 12 | }, 13 | use: { 14 | loader: "babel-loader", 15 | options: { 16 | presets: ["@babel/preset-typescript","@babel/preset-env", "@babel/preset-react"], 17 | }, 18 | }, 19 | }, 20 | // TS Loader 21 | // { 22 | // test:/\.(ts|tsx)$/, 23 | // exclude: /node_modules/, 24 | // resolve: { 25 | // extensions: [".ts", ".tsx"], 26 | // }, 27 | // use: 'ts-loader', 28 | // }, 29 | //CSS style loader 30 | { 31 | test: /\.css/, 32 | use: ["style-loader", "css-loader"], 33 | }, 34 | { 35 | test: /\.s[ac]ss$/i, 36 | use: [ 37 | // Creates `style` nodes from JS strings 38 | "style-loader", 39 | // Translates CSS into CommonJS 40 | "css-loader", 41 | // Compiles Sass to CSS 42 | "sass-loader", 43 | ], 44 | }, 45 | { 46 | test: /\.(png|jpe?g|gif|svg)$/i, 47 | use: [ 48 | { 49 | loader: "file-loader", 50 | options: { 51 | name: "[name].[ext]", 52 | outputPath: "assets/", 53 | }, 54 | }, 55 | ], 56 | }, 57 | ], 58 | }, 59 | 60 | stats: { 61 | errorDetails: true, 62 | }, 63 | plugins: [], 64 | }; 65 | -------------------------------------------------------------------------------- /src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { 3 | isMobile as libIsMobile, 4 | isTablet as libIsTablet 5 | } from "react-device-detect"; 6 | let isMobile:Boolean; 7 | if (process.env.NODE_ENV === "localhost") { 8 | isMobile = window.innerWidth < 1024; 9 | } else { 10 | isMobile = libIsMobile || libIsTablet || window.innerWidth < 1024; 11 | }; 12 | interface NavbarProps { 13 | window?: ()=>Window 14 | } 15 | import logo from './../assets/img-logo.png'; 16 | 17 | const Navbar: FunctionComponent = (props) => { 18 | 19 | return ( 20 | <> 21 | 51 | 52 | ); 53 | } 54 | 55 | export default Navbar; -------------------------------------------------------------------------------- /src/components/FormBuilder/subcomponents/FormPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import StepperFormPreview from './form-preview/StepperFormPreview'; 3 | import { Drawer } from '@mui/material'; 4 | import { FormLayoutComponentsType } from '../../../types/FormTemplateTypes'; 5 | 6 | interface FormPreviewProps{ 7 | screenType: string; 8 | showPreview: boolean; 9 | closePreviewDrawer: ()=>void; 10 | formLayoutComponents: FormLayoutComponentsType[] 11 | } 12 | 13 | interface FormPreviewStates { 14 | screenType: string; 15 | } 16 | 17 | class FormPreview extends Component { 18 | constructor(props: FormPreviewProps) { 19 | super(props); 20 | this.state = { 21 | screenType: this.props.screenType || 'mobile' 22 | } 23 | this.handleCloseClick = this.handleCloseClick.bind(this); 24 | } 25 | 26 | handleCloseClick(){ 27 | this.props.closePreviewDrawer(); 28 | } 29 | 30 | render() { 31 | 32 | const { showPreview, formLayoutComponents } = this.props; 33 | 34 | return ( 35 | <> 36 | 37 |
38 |
39 |
40 |
41 | this.handleCloseClick()} 45 | > 46 |

Preview

47 |
48 | 49 |
50 |
51 |
52 |
53 | 54 | ); 55 | } 56 | } 57 | 58 | export default FormPreview; -------------------------------------------------------------------------------- /src/pages/FormBuilderPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect } from "react"; 2 | import FormBuilder from "../components/FormBuilder/FormBuilder"; 3 | import { useAppDispatch, useAppSelector } from "../redux/hooks"; 4 | import { useNavigate, useParams } from "react-router-dom"; 5 | import { 6 | getSingleTemplate, 7 | setSelectedTemplateNull, 8 | } from "../redux/entities/formBuilderEntity"; 9 | import useModalStrip from "../global-hooks/useModalStrip"; 10 | 11 | interface FormBuilderPageProps {} 12 | 13 | const FormBuilderPage: FunctionComponent = () => { 14 | const template = useAppSelector( 15 | (state) => state.entities.formBuilder.selectedTemplate 16 | ); 17 | const dispatch = useAppDispatch(); 18 | const navigate = useNavigate(); 19 | const { showModalStrip } = useModalStrip(); 20 | const { formId } = useParams(); 21 | 22 | useEffect(() => { 23 | (async () => { 24 | try { 25 | const template = await dispatch( 26 | getSingleTemplate(formId as string) 27 | ).unwrap(); 28 | console.log(template); 29 | if(!template){ 30 | throw new Error('Not found'); 31 | } 32 | } catch (ex) { 33 | showModalStrip("danger", "The form id is not correct", 5000); 34 | navigate("/"); 35 | } 36 | })(); 37 | 38 | return () => { 39 | // Setting template to null when unmounting. 40 | dispatch(setSelectedTemplateNull()); 41 | }; 42 | }, []); 43 | 44 | const defaultForm = { 45 | id: "0", 46 | formName: "", 47 | createdAt: 0, 48 | creator: "", 49 | formLayoutComponents: [], 50 | lastPublishedAt: 0, 51 | publishHistory: [], 52 | publishStatus: "draft", 53 | updatedAt: 0, 54 | }; 55 | 56 | return ( 57 | <> 58 | {template ? ( 59 | 60 | ) : null} 61 | 62 | ); 63 | }; 64 | 65 | export default FormBuilderPage; 66 | -------------------------------------------------------------------------------- /src/pages/TemplatesPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useState } from "react"; 2 | import { useAppDispatch, useAppSelector } from "../redux/hooks"; 3 | import { getAllTemplates } from "../redux/entities/formBuilderEntity"; 4 | import { useNavigate } from "react-router-dom"; 5 | import NewFormDialogComponent from "../components/FormTemplates/NewFormDialogComponent"; 6 | import FormLayoutComponent from "../components/FormTemplates/FormLayoutComponent"; 7 | 8 | interface TemplatesPageProps {} 9 | 10 | const TemplatesPage: FunctionComponent = () => { 11 | const templates = useAppSelector( 12 | (state) => state.entities.formBuilder.allTemplates 13 | ); 14 | const dispatch = useAppDispatch(); 15 | 16 | const [openDialog, setOpenDialog] = useState(false); 17 | 18 | useEffect(() => { 19 | if (templates.length === 0) { 20 | dispatch(getAllTemplates()); 21 | } 22 | }, []); 23 | 24 | const newFormLayout = { 25 | border: "1px dashed", 26 | width: "150px", 27 | height: "150px", 28 | fontSize: "2.7rem", 29 | display: "flex", 30 | alignItems: "center", 31 | justifyContent: "center", 32 | cursor: "pointer", 33 | borderRadius: "9px", 34 | }; 35 | 36 | return ( 37 | <> 38 |
39 |

All Form Templates

40 |
41 | 45 | {templates.map((template) => ( 46 | 51 | ))} 52 |
53 |
54 | 58 | 59 | ); 60 | }; 61 | 62 | export default TemplatesPage; 63 | -------------------------------------------------------------------------------- /src/components/FormBuilder/subcomponents/ControlDragComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect } from "react"; 2 | import { useDrag } from "react-dnd"; 3 | import { 4 | FormLayoutComponentChildrenType, 5 | FormLayoutComponentContainerType, 6 | FormLayoutComponentsType, 7 | } from "../../../types/FormTemplateTypes"; 8 | 9 | interface ControlDragComponentProps { 10 | handleItemAdded: ( 11 | item: FormLayoutComponentChildrenType | FormLayoutComponentContainerType, 12 | containerId?: string 13 | ) => void; 14 | item: FormLayoutComponentChildrenType | FormLayoutComponentContainerType; 15 | formLayoutComponents: FormLayoutComponentsType[] 16 | } 17 | 18 | const ControlDragComponent: FunctionComponent = ( 19 | props 20 | ) => { 21 | const { item, handleItemAdded } = props; 22 | 23 | const [{ isDragging }, drag] = useDrag(() => ({ 24 | type: item.itemType, 25 | item: item, 26 | end: (item: FormLayoutComponentChildrenType | FormLayoutComponentContainerType, monitor: any) => { 27 | const dropResult: FormLayoutComponentContainerType = monitor.getDropResult(); 28 | if (item && dropResult) { 29 | if (item.itemType === "container") { 30 | handleItemAdded(item); 31 | } else if (item.itemType === "control") { 32 | handleItemAdded(item, dropResult.id); 33 | } 34 | } 35 | }, 36 | collect: (monitor: any) => ({ 37 | isDragging: monitor.isDragging(), 38 | handlerId: monitor.getHandlerId(), 39 | }), 40 | }),[props.formLayoutComponents]); // Need to add this dependency for dragging elements. 41 | const opacity = isDragging ? 0.4 : 1; 42 | 43 | return ( 44 | <> 45 |
50 | 53 | 54 | 55 | {item.displayText} 56 |
57 | 58 | ); 59 | }; 60 | 61 | export default ControlDragComponent; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.11.0", 7 | "@emotion/styled": "^11.11.0", 8 | "@mui/icons-material": "^5.11.16", 9 | "@mui/material": "^5.13.2", 10 | "@mui/x-date-pickers": "^6.6.0", 11 | "@reduxjs/toolkit": "^1.9.5", 12 | "@testing-library/jest-dom": "^5.16.5", 13 | "@testing-library/react": "^13.4.0", 14 | "@testing-library/user-event": "^13.5.0", 15 | "@types/jest": "^27.5.2", 16 | "@types/react": "^18.0.28", 17 | "@types/react-dom": "^18.0.11", 18 | "lodash": "^4.17.21", 19 | "moment": "^2.29.4", 20 | "react": "^18.2.0", 21 | "react-device-detect": "^2.2.3", 22 | "react-dnd": "^16.0.1", 23 | "react-dnd-html5-backend": "^16.0.1", 24 | "react-dom": "^18.2.0", 25 | "react-redux": "^8.0.7", 26 | "react-router-dom": "^6.11.2", 27 | "tss-react": "^4.8.4", 28 | "typescript": "^4.9.5", 29 | "web-vitals": "^2.1.4" 30 | }, 31 | "scripts": { 32 | "start": "cross-env webpack-dev-server --config webpack.dev.js --open", 33 | "build:staging": "cross-env webpack --config webpack.staging.js", 34 | "build:prod": "npm version patch && cross-env webpack --config webpack.prod.js" 35 | }, 36 | "eslintConfig": { 37 | "extends": [ 38 | "react-app", 39 | "react-app/jest" 40 | ] 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "^7.21.0", 56 | "@babel/preset-env": "^7.20.2", 57 | "@babel/preset-react": "^7.18.6", 58 | "@babel/preset-typescript": "^7.21.0", 59 | "babel-loader": "^9.1.2", 60 | "cross": "^1.0.0", 61 | "cross-env": "^7.0.3", 62 | "css-loader": "^6.7.3", 63 | "css-minimizer-webpack-plugin": "^4.2.2", 64 | "dotenv-webpack": "^8.0.1", 65 | "env": "^0.0.2", 66 | "file-loader": "^6.2.0", 67 | "html-webpack-plugin": "^5.5.0", 68 | "sass": "^1.58.3", 69 | "sass-loader": "^13.3.0", 70 | "style-loader": "^3.3.1", 71 | "webpack": "^5.76.0", 72 | "webpack-cli": "^5.0.1", 73 | "webpack-dev-server": "^4.11.1", 74 | "webpack-merge": "^5.8.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/FormTemplates/FormLayoutComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { TemplateType } from "../../types/FormTemplateTypes"; 3 | import { useNavigate } from "react-router-dom"; 4 | import "./styles.scss"; 5 | import { FormPublishStatus } from "../../utils/formBuilderUtils"; 6 | import { IconButton } from "@mui/material"; 7 | import DeleteIcon from '@mui/icons-material/Delete'; 8 | import { useAppDispatch } from "../../redux/hooks"; 9 | import { deleteTemplate } from "../../redux/entities/formBuilderEntity"; 10 | 11 | interface FormLayoutComponentProps { 12 | template?: TemplateType; 13 | createdFormLayout: boolean; 14 | setOpenDialog?: (arg: boolean)=>void; 15 | } 16 | 17 | const newFormLayout = { 18 | border: "1px dashed", 19 | width: "150px", 20 | height: "150px", 21 | fontSize: "2.7rem", 22 | display: "flex", 23 | alignItems: "center", 24 | justifyContent: "center", 25 | cursor: "pointer", 26 | borderRadius: "9px", 27 | }; 28 | 29 | const FormLayoutComponent: FunctionComponent< 30 | FormLayoutComponentProps 31 | > = (props) => { 32 | 33 | 34 | const navigate = useNavigate(); 35 | const dispatch = useAppDispatch(); 36 | const { template, createdFormLayout, setOpenDialog } = props; 37 | 38 | return ( 39 | <> 40 |
41 |
46 |
47 |
{ 58 | if (createdFormLayout && template) { 59 | navigate(`/formbuilder/${template.id}`); 60 | } else { 61 | if (setOpenDialog) { 62 | setOpenDialog(true); 63 | } 64 | } 65 | }} 66 | > 67 | 72 |
73 |
74 |
81 | {createdFormLayout 82 | ? (template as TemplateType).formName 83 | : "Blank Form"} 84 |
85 | {createdFormLayout ? ( 86 | <> 87 | { 88 | if(confirm('Are you sure you want to delete the template?')){ 89 | dispatch(deleteTemplate(template?.id as string)); 90 | } 91 | }}> 92 | 93 | 94 | 95 | ) : null} 96 |
97 |
98 | 99 | ); 100 | }; 101 | 102 | export default FormLayoutComponent; 103 | -------------------------------------------------------------------------------- /src/components/FormBuilder/subcomponents/ManageItemsListComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC, useEffect, useState} from 'react' 2 | import { FormLayoutCoponentChildrenItemsType } from '../../../types/FormTemplateTypes'; 3 | import { IconButton, List, ListItem, ListItemSecondaryAction, ListItemText, TextField } from '@mui/material'; 4 | import { Delete, Edit } from '@mui/icons-material'; 5 | import { generateID } from '../../../utils/common'; 6 | 7 | interface ManageItemsListComponentProps{ 8 | items: FormLayoutCoponentChildrenItemsType[] | undefined; 9 | addItemInList: (item:FormLayoutCoponentChildrenItemsType)=>void; 10 | deleteItemFromList: (item: FormLayoutCoponentChildrenItemsType)=>void; 11 | editIteminList: (item: FormLayoutCoponentChildrenItemsType)=>void; 12 | } 13 | 14 | const ManageItemsListComponent: FC = (props)=> { 15 | 16 | // const [runningItemId, setRunningItemId] = useState(3); 17 | const [editMode, setEditMode] = useState(false); 18 | const [itemName, setItemName] = useState(''); 19 | const [editItemId, setEditItemId] = useState(undefined) 20 | 21 | const { items, addItemInList, deleteItemFromList, editIteminList } = props; 22 | 23 | useEffect(() => { 24 | cancelEditing(); 25 | }, [items]) 26 | 27 | 28 | const handleChange: React.ChangeEventHandler = (e)=>{ 29 | const { name, value } = e.target; 30 | setItemName(value); 31 | } 32 | 33 | const changeToEditMode = (item: FormLayoutCoponentChildrenItemsType)=>{ 34 | setItemName(item.label); 35 | setEditItemId(item.id); 36 | setEditMode(true); 37 | } 38 | 39 | const onSubmit: React.MouseEventHandler = (event)=>{ 40 | if(itemName !== null && itemName !== ''){ 41 | if(!editMode){ 42 | addItemInList({ 43 | id: generateID(), 44 | value: itemName.replace(" ","__-"), 45 | label: itemName 46 | }); 47 | } else{ 48 | editIteminList({ 49 | id: editItemId as string, 50 | value: itemName.replace(" ","__-"), 51 | label: itemName 52 | }) 53 | } 54 | } 55 | } 56 | 57 | const cancelEditing = ()=>{ 58 | setEditMode(false); 59 | setItemName(''); 60 | setEditItemId(undefined); 61 | } 62 | 63 | return ( <> 64 |
65 | 72 | 78 | { 79 | editMode && 85 | } 86 | 87 | {items?.map((item,ind)=>{ 88 | return 89 | 90 | 91 | changeToEditMode(item)}> 92 | 93 | 94 | deleteItemFromList(item)} edge="end"> 95 | 96 | 97 | 98 | 99 | })} 100 | 101 |
102 | ); 103 | } 104 | 105 | export default ManageItemsListComponent; -------------------------------------------------------------------------------- /src/components/FormBuilder/subcomponents/form-preview/StepperFormPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | import RenderItem from "./RenderItem"; 3 | import { FormLayoutComponentsType } from "../../../../types/FormTemplateTypes"; 4 | 5 | const previewWindowStyle = { 6 | backgroundColor: 'white', 7 | height: '81vh', 8 | overflowY:'scroll', 9 | border: '1px solid rgba(0,0,0,0.1)', 10 | borderRadius: '9px', 11 | boxShadow: '0 9px 90px #efefef', 12 | marginLeft: 'auto', 13 | marginRight: 'auto' 14 | } 15 | 16 | interface StepperFormPreviewProps{ 17 | formLayoutComponents: FormLayoutComponentsType[]; 18 | screenType: string 19 | } 20 | 21 | const StepperFormPreview: FC = (props)=> { 22 | const [componentIndex, setComponentIndex] = useState(0); 23 | const { formLayoutComponents, screenType } = props; 24 | 25 | const component = formLayoutComponents[componentIndex]; 26 | 27 | const nextPrevIndex = (val: number)=>{ 28 | setComponentIndex((prev)=>prev + val); 29 | } 30 | 31 | const isMobile = screenType === 'mobile'; 32 | 33 | return ( 34 | <> 35 | {formLayoutComponents.length > 0 ? ( 36 | <> 37 |
38 |
39 |
40 |
{ 42 | e.preventDefault(); 43 | }} 44 | style={{ minWidth: "100%" }} 45 | > 46 |
47 | Step {componentIndex + 1} 48 |
49 | 50 |
51 |

{component.container.heading}

52 |

{component.container.subHeading}

53 |
54 | 55 | {component.children.map((child, ind) => ( 56 |
57 |
{child.labelName + (child.required ? " *" : "")}
58 | {child.description !== "" ? ( 59 | <> 60 |
61 |

{child.description}

62 |
63 | 64 | ) : null} 65 | 66 | {/* {renderItem(child)} */} 67 |
68 | ))} 69 | 70 | {componentIndex !== 0 && ( 71 | { 76 | nextPrevIndex(-1); 77 | }} 78 | /> 79 | )} 80 | {componentIndex < formLayoutComponents.length - 1 && ( 81 | { 86 | nextPrevIndex(1); 87 | }} 88 | /> 89 | )} 90 | {componentIndex + 1 === formLayoutComponents.length && ( 91 | 96 | )} 97 |
98 |
99 |
100 |
101 | 102 | ) : ( 103 | <> 104 |
105 |

You need to add Containers and Controls to see output here.

106 |
107 | 108 | )} 109 | 110 | ); 111 | } 112 | 113 | export default StepperFormPreview; 114 | -------------------------------------------------------------------------------- /src/components/FormBuilder/LeftSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect } from "react"; 2 | import ControlDragComponent from "./subcomponents/ControlDragComponent"; 3 | import { 4 | FormContainerList, 5 | FormControlList, 6 | } from "../../utils/formBuilderUtils"; 7 | import { 8 | FormLayoutComponentChildrenType, 9 | FormLayoutComponentContainerType, 10 | FormLayoutComponentsType, 11 | } from "../../types/FormTemplateTypes"; 12 | 13 | interface LeftSidebarProps { 14 | handleItemAdded: ( 15 | item: FormLayoutComponentChildrenType | FormLayoutComponentContainerType, 16 | containerId?: string 17 | ) => void; 18 | formLayoutComponents: FormLayoutComponentsType[] 19 | } 20 | 21 | const LeftSidebar: FunctionComponent = (props) => { 22 | 23 | return ( 24 | <> 25 |
26 |
Containers
27 | {FormContainerList.map((container, ind) => { 28 | return ( 29 | 35 | ); 36 | })} 37 |
38 |
Text Elements
39 |
40 | {FormControlList.filter( 41 | (control) => control.category === "text-elements" 42 | ).map((control, ind) => { 43 | return ( 44 |
45 | 51 |
52 | ); 53 | })} 54 |
55 |
56 |
Date Elements
57 |
58 | {FormControlList.filter( 59 | (control) => control.category === "date-elements" 60 | ).map((control, ind) => { 61 | return ( 62 |
63 | 69 |
70 | ); 71 | })} 72 |
73 |
74 |
Other Elements
75 |
76 | {FormControlList.filter( 77 | (control) => control.category === "other-elements" 78 | ).map((control, ind) => { 79 | return ( 80 |
81 | 87 |
88 | ); 89 | })} 90 |
91 |
92 |
Media Elements
93 |
94 | {FormControlList.filter( 95 | (control) => control.category === "media-elements" 96 | ).map((control, ind) => { 97 | return ( 98 |
99 | 105 |
106 | ); 107 | })} 108 |
109 |
110 | 111 | ); 112 | }; 113 | 114 | export default LeftSidebar; 115 | -------------------------------------------------------------------------------- /src/components/FormTemplates/NewFormDialogComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEventHandler, FormEventHandler, FunctionComponent, useEffect, useState } from 'react'; 2 | import { Dialog, DialogContent, DialogTitle, TextField } from "@mui/material"; 3 | import { useAppDispatch, useAppSelector } from '../../redux/hooks'; 4 | import { addTemplate } from '../../redux/entities/formBuilderEntity'; 5 | import { useNavigate } from 'react-router-dom'; 6 | import useModalStrip from '../../global-hooks/useModalStrip'; 7 | import { TemplateType } from '../../types/FormTemplateTypes'; 8 | 9 | interface NewFormDialogComponentProps { 10 | openDialog: boolean, 11 | setOpenDialog: (arg: boolean)=>void 12 | } 13 | 14 | interface NewFormDataType{ 15 | formName: string 16 | } 17 | 18 | const textboxStyle = { 19 | minWidth: "100%", 20 | maxWidth: "100%", 21 | marginTop: "20px", 22 | }; 23 | 24 | const NewFormDialogComponent: FunctionComponent = (props) => { 25 | const dispatch = useAppDispatch(); 26 | const navigate = useNavigate(); 27 | const { showModalStrip } = useModalStrip(); 28 | 29 | const [creatingForm, setCreatingForm] = useState(false); 30 | 31 | const [newFormData, setNewFormData] = useState({ 32 | formName: '' 33 | }); 34 | 35 | const handleInputChange:ChangeEventHandler = (e)=>{ 36 | const {name , value} = e.target; 37 | setNewFormData((prev) => ({ ...prev, [name]: value })); 38 | } 39 | 40 | const handleFormSubmit: FormEventHandler = async (e)=>{ 41 | e.preventDefault(); 42 | if(newFormData.formName === ''){ 43 | showModalStrip("danger", "Form name cannot be empty", 5000); 44 | return; 45 | } 46 | setCreatingForm(true); 47 | try{ 48 | const template: TemplateType = await dispatch(addTemplate(newFormData)).unwrap(); 49 | navigate(`/formbuilder/${template.id}`); 50 | } catch(ex){ 51 | showModalStrip("danger", "Error occured while creating a new Form", 5000); 52 | } 53 | } 54 | 55 | return ( 56 | <> 57 | { 62 | props.setOpenDialog(false); 63 | }} 64 | > 65 | 66 |
67 | 68 | 69 | 70 | props.setOpenDialog(false)} 73 | > 74 | 75 | 76 |
77 |
78 | 79 |
80 |
88 |

Enter the following details:

89 |
90 |
91 | 98 |
99 | 117 | props.setOpenDialog(false)} 123 | /> 124 |
125 |
126 |
127 |
128 |
129 | 130 | ); 131 | } 132 | 133 | export default NewFormDialogComponent; -------------------------------------------------------------------------------- /src/utils/demoFormLayouts.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { generateID } from "./common"; 3 | 4 | const currentDateTime = moment().unix()*1000; 5 | 6 | const DemoFormLayouts: any = [ 7 | { 8 | id: generateID(), 9 | formName: "Regular POD Form", 10 | createdAt: currentDateTime, 11 | creator: "Test User", 12 | formLayoutComponents: [ 13 | { 14 | container: { 15 | id: "xdb2ni4u0md3nflqc1qksy", 16 | controlName: "step-container", 17 | displayText: "Workflow Step", 18 | itemType: "container", 19 | icon: "fa fa-building", 20 | heading: "Dropoff Location", 21 | subHeading: "", 22 | }, 23 | children: [ 24 | { 25 | id: "dl0dduqw8s991yf8amgsm", 26 | controlName: "radio-group", 27 | displayText: "Radio", 28 | description: "Enter the dropoff location of the package", 29 | labelName: "Dropoff Location", 30 | itemType: "control", 31 | icon: "far fa-dot-circle", 32 | required: true, 33 | items: [ 34 | { 35 | id: "56azek5q7i2m97yfbmw7j", 36 | value: "Front__-Door", 37 | label: "Front Door", 38 | }, 39 | { 40 | id: "y1d6eq3231v2wsvs1ocy", 41 | value: "Back__-Door", 42 | label: "Back Door", 43 | }, 44 | { 45 | id: "990tgqa51essjkas0gqzgs", 46 | value: "Package__-Locker", 47 | label: "Package Locker", 48 | }, 49 | ], 50 | category: "other-elements", 51 | containerId: "xdb2ni4u0md3nflqc1qksy", 52 | }, 53 | ], 54 | }, 55 | { 56 | container: { 57 | id: "nyqlps3erjkgfng1rxgbvn", 58 | controlName: "step-container", 59 | displayText: "Workflow Step", 60 | itemType: "container", 61 | icon: "fa fa-building", 62 | heading: "Container Heading", 63 | subHeading: "Container SubHeading", 64 | }, 65 | children: [ 66 | { 67 | id: "h3qfythhwh5kbi9l5s0hrn", 68 | controlName: "image-upload", 69 | displayText: "Image", 70 | description: "", 71 | labelName: "Upload Image for POD", 72 | itemType: "control", 73 | icon: "far fa-image", 74 | required: true, 75 | category: "media-elements", 76 | containerId: "nyqlps3erjkgfng1rxgbvn", 77 | }, 78 | ], 79 | }, 80 | ], 81 | lastPublishedAt: 0, 82 | publishHistory: [], 83 | publishStatus: "draft", 84 | updatedAt: currentDateTime, 85 | }, 86 | { 87 | id: generateID(), 88 | formName: "Legal Document Form", 89 | createdAt: currentDateTime, 90 | creator: "Test User", 91 | formLayoutComponents: [ 92 | { 93 | container: { 94 | id: "xdb2ni4u0md3nflqc1qksy", 95 | controlName: "step-container", 96 | displayText: "Workflow Step", 97 | itemType: "container", 98 | icon: "fa fa-building", 99 | heading: "Dropoff Location", 100 | subHeading: "", 101 | }, 102 | children: [ 103 | { 104 | id: "mru48gcfqzxb34ifrdwpf", 105 | controlName: "signature", 106 | displayText: "Signature", 107 | description: "", 108 | labelName: "Customer Signature", 109 | itemType: "control", 110 | icon: "fa fa-signature", 111 | required: true, 112 | category: "other-elements", 113 | containerId: "xdb2ni4u0md3nflqc1qksy", 114 | }, 115 | ], 116 | }, 117 | { 118 | container: { 119 | id: "nyqlps3erjkgfng1rxgbvn", 120 | controlName: "step-container", 121 | displayText: "Workflow Step", 122 | itemType: "container", 123 | icon: "fa fa-building", 124 | heading: "Container Heading", 125 | subHeading: "Container SubHeading", 126 | }, 127 | children: [ 128 | { 129 | id: "h3qfythhwh5kbi9l5s0hrn", 130 | controlName: "image-upload", 131 | displayText: "Image", 132 | description: "", 133 | labelName: "Upload Image for POD", 134 | itemType: "control", 135 | icon: "far fa-image", 136 | required: true, 137 | category: "media-elements", 138 | containerId: "nyqlps3erjkgfng1rxgbvn", 139 | }, 140 | { 141 | id: "n6kf3cyws39v2dyhu3dli", 142 | controlName: "multiline-text-field", 143 | displayText: "Notes", 144 | description: "Addional infomration", 145 | placeholder: "Write your notes here", 146 | labelName: "Notes", 147 | rows: 4, 148 | itemType: "control", 149 | icon: "far fa-file", 150 | required: false, 151 | category: "text-elements", 152 | containerId: "nyqlps3erjkgfng1rxgbvn", 153 | index: 0, 154 | }, 155 | ], 156 | }, 157 | ], 158 | lastPublishedAt: 0, 159 | publishHistory: [], 160 | publishStatus: "draft", 161 | updatedAt: currentDateTime, 162 | }, 163 | ]; 164 | 165 | export default DemoFormLayouts; -------------------------------------------------------------------------------- /src/redux/entities/formBuilderEntity.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { TemplateType } from "../../types/FormTemplateTypes"; 3 | import { getFromLocalStorage, saveToLocalStorage } from "../common"; 4 | import DemoFormLayouts from "../../utils/demoFormLayouts"; 5 | import { 6 | closeCircularProgress, 7 | openCircularProgress, 8 | } from "../uireducers/progress"; 9 | import { generateID } from "../../utils/common"; 10 | import moment from "moment"; 11 | import _ from "lodash"; 12 | 13 | interface AddTemplateType { 14 | formName: string; 15 | } 16 | 17 | // Logic to Get All Templates 18 | export const getAllTemplates = createAsyncThunk( 19 | "formBuilderEntity/getAllTemplates", 20 | async (data, thunkAPI) => { 21 | // Open the Circular Progress 22 | thunkAPI.dispatch(openCircularProgress()); 23 | return await new Promise((resolve, reject) => { 24 | let outputInStorage = JSON.parse(getFromLocalStorage("templates")); 25 | // Check if its null; 26 | if (outputInStorage === null) { 27 | outputInStorage = DemoFormLayouts; 28 | saveToLocalStorage("templates", JSON.stringify(outputInStorage)); 29 | } 30 | setTimeout(() => { 31 | // Close the Circular Progress 32 | thunkAPI.dispatch(closeCircularProgress()); 33 | resolve(outputInStorage); 34 | }, 1000); 35 | }); 36 | } 37 | ); 38 | 39 | // Logic to get Single Template 40 | export const getSingleTemplate = createAsyncThunk( 41 | "formBuilderEntity/getSingleTemplate", 42 | async (data: string, thunkAPI) => { 43 | // Open the Circular Progress 44 | thunkAPI.dispatch(openCircularProgress()); 45 | return await new Promise((resolve, reject) => { 46 | const allTemplates: TemplateType[] = JSON.parse( 47 | getFromLocalStorage("templates") 48 | ); 49 | const singleTemplate = allTemplates.filter((t) => t.id === data)[0]; 50 | setTimeout(() => { 51 | // Close the Circular Progress 52 | thunkAPI.dispatch(closeCircularProgress()); 53 | resolve(singleTemplate); 54 | }, 1000); 55 | }); 56 | } 57 | ); 58 | 59 | // Logic to Add Template 60 | export const addTemplate = createAsyncThunk( 61 | "formBuilderEntity/addTemplate", 62 | async (data: AddTemplateType, thunkAPI) => { 63 | return await new Promise((resolve, reject) => { 64 | const currentDateTime = moment().unix() * 1000; 65 | 66 | const allTemplates: TemplateType[] = JSON.parse( 67 | getFromLocalStorage("templates") 68 | ); 69 | // Create new Template 70 | const template: TemplateType = { 71 | id: generateID(), 72 | formName: data.formName, 73 | createdAt: currentDateTime, 74 | creator: "Test User", 75 | formLayoutComponents: [], 76 | lastPublishedAt: 0, 77 | publishHistory: [], 78 | publishStatus: "draft", 79 | updatedAt: 0, 80 | }; 81 | allTemplates.push(template); 82 | setTimeout(() => { 83 | saveToLocalStorage("templates",JSON.stringify(allTemplates)); 84 | resolve(template); 85 | }, 1000); 86 | }); 87 | } 88 | ); 89 | 90 | 91 | // Logic to delete a template 92 | export const deleteTemplate = createAsyncThunk( 93 | "formBuilderEntity/deleteTemplate", 94 | async (data: string, thunkAPI) => { 95 | // Open the Circular Progress 96 | thunkAPI.dispatch(openCircularProgress()); 97 | return await new Promise((resolve, reject)=>{ 98 | const allTemplates: TemplateType[] = JSON.parse( 99 | getFromLocalStorage("templates") 100 | ); 101 | const deleteIndex = allTemplates.findIndex((t)=>t.id === data); 102 | allTemplates.splice(deleteIndex,1); 103 | setTimeout(() => { 104 | // Close the Circular Progress 105 | thunkAPI.dispatch(closeCircularProgress()); 106 | saveToLocalStorage("templates",JSON.stringify(allTemplates)); 107 | resolve(deleteIndex); 108 | }, 600); 109 | }); 110 | } 111 | ); 112 | 113 | // Logic to save template 114 | export const saveTemplate = createAsyncThunk( 115 | "formBuilderEntity/saveTemplate", 116 | async (data: TemplateType, thunkAPI) => { 117 | // Open the Circular Progress 118 | thunkAPI.dispatch(openCircularProgress()); 119 | return await new Promise((resolve, reject)=>{ 120 | const allTemplates: TemplateType[] = JSON.parse( 121 | getFromLocalStorage("templates") 122 | ); 123 | 124 | let templateIndex = allTemplates.findIndex((t) => t.id === data.id); 125 | allTemplates[templateIndex] = data; 126 | setTimeout(() => { 127 | // Close the Circular Progress 128 | thunkAPI.dispatch(closeCircularProgress()); 129 | saveToLocalStorage("templates",JSON.stringify(allTemplates)); 130 | resolve(data); 131 | }, 1000); 132 | }) 133 | } 134 | ); 135 | 136 | const slice = createSlice({ 137 | name: "formBuilderEntity", 138 | initialState: { 139 | allTemplates: [] as TemplateType[], 140 | selectedTemplate: null as TemplateType | null, 141 | }, 142 | reducers: { 143 | setSelectedTemplateNull: (state) => { 144 | state.selectedTemplate = null; 145 | }, 146 | }, 147 | extraReducers: (builder) => { 148 | builder.addCase(getAllTemplates.fulfilled, (state, action) => { 149 | state.allTemplates = action.payload; 150 | }); 151 | builder.addCase(getSingleTemplate.fulfilled, (state, action) => { 152 | state.selectedTemplate = action.payload; 153 | }); 154 | builder.addCase( 155 | addTemplate.fulfilled, 156 | (state, action: PayloadAction) => { 157 | const updatedState = _.cloneDeep(state.allTemplates); 158 | updatedState.push(action.payload); 159 | state.allTemplates = updatedState; 160 | } 161 | ); 162 | builder.addCase(saveTemplate.fulfilled, (state, action)=>{ 163 | const newStateTemplates = state.allTemplates.slice(); 164 | const newTemplateId = newStateTemplates.findIndex((t)=>t.id === action.payload.id); 165 | newStateTemplates[newTemplateId] = action.payload; 166 | state.allTemplates = newStateTemplates; 167 | }); 168 | builder.addCase(deleteTemplate.fulfilled,(state,action)=>{ 169 | const newStateTemplates = state.allTemplates.slice(); 170 | newStateTemplates.splice(action.payload,1); 171 | state.allTemplates = newStateTemplates; 172 | }) 173 | }, 174 | }); 175 | 176 | export const { setSelectedTemplateNull } = slice.actions; 177 | 178 | export default slice.reducer; 179 | -------------------------------------------------------------------------------- /src/components/FormBuilder/subcomponents/form-preview/RenderItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { FormLayoutComponentChildrenType } from '../../../../types/FormTemplateTypes'; 3 | import { FormControlNames } from '../../../../utils/formBuilderUtils'; 4 | import { Checkbox, FormControl, FormControlLabel, FormGroup, MenuItem, Radio, RadioGroup, Select, Switch, TextField } from '@mui/material'; 5 | import { LocalizationProvider } from '@mui/x-date-pickers'; 6 | import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; 7 | import { DatePicker } from '@mui/x-date-pickers/DatePicker'; 8 | import { TimePicker } from '@mui/x-date-pickers/TimePicker'; 9 | 10 | const dateFormat = 'yyyy, MMM dd'; 11 | 12 | 13 | interface RenderItemProps{ 14 | item: FormLayoutComponentChildrenType 15 | } 16 | 17 | const RenderItem: FC = (props)=> { 18 | const { item } = props; 19 | 20 | switch (item.controlName) { 21 | case FormControlNames.INPUTTEXTFIELD: 22 | return ( 23 | <> 24 | 30 | 31 | ); 32 | 33 | case FormControlNames.INPUTMULTILINE: 34 | return ( 35 | <> 36 | 44 | 45 | ); 46 | case FormControlNames.CHECKBOX: 47 | return ( 48 | <> 49 |
50 | 54 | } 55 | style={{ marginLeft: "0px" }} 56 | label={item.placeholder} 57 | /> 58 |
59 | 60 | ); 61 | 62 | case FormControlNames.RADIOGROUP: 63 | return ( 64 | <> 65 | 66 | {/* {item.labelName + (item.required?" *":"")} */} 67 | 68 | {item.items?.map((i) => ( 69 | } 73 | label={i.label} 74 | /> 75 | ))} 76 | 77 | 78 | 79 | ); 80 | 81 | case FormControlNames.SELECTDROPDOWN: 82 | return ( 83 | <> 84 | 85 | {/* {item.labelName + (item.required?" *":"")} */} 86 | 97 | 98 | 99 | ); 100 | 101 | case FormControlNames.DATEFIELD: 102 | return ( 103 | <> 104 | 105 | 108 | 109 | 110 | ); 111 | 112 | case FormControlNames.TIMEFIELD: 113 | return ( 114 | <> 115 | 116 | 119 | 120 | 121 | ); 122 | 123 | case FormControlNames.FILEUPLOAD: 124 | return ( 125 | <> 126 | 127 | 130 | 131 | ); 132 | 133 | case FormControlNames.IMAGEUPLOAD: 134 | return ( 135 | <> 136 | 137 | 140 | 141 | ); 142 | 143 | case FormControlNames.SCANCODE: 144 | return ( 145 | <> 146 | 147 | 150 | 151 | ); 152 | 153 | case FormControlNames.SCANCODE: 154 | return ( 155 | <> 156 | 157 | 160 | 161 | ); 162 | 163 | case FormControlNames.SIGNATURE: 164 | return ( 165 | <> 166 | 173 | 174 | ); 175 | 176 | case FormControlNames.TOGGLE: 177 | return ( 178 | <> 179 | 180 | 181 | ); 182 | 183 | case FormControlNames.CHECKLIST: 184 | return ( 185 | <> 186 | 187 | {item.items?.map((i, ind) => ( 188 | } 191 | label={i.label} 192 | style={{ marginLeft: "0px" }} 193 | /> 194 | ))} 195 | 196 | 197 | ); 198 | } 199 | return <> 200 | } 201 | 202 | export default RenderItem; -------------------------------------------------------------------------------- /src/utils/formBuilderUtils.ts: -------------------------------------------------------------------------------- 1 | import { generateID } from "./common"; 2 | 3 | export const FormControlNames = { 4 | STEPCONTAINER: "step-container", 5 | INPUTTEXTFIELD: "text-field", 6 | INPUTMULTILINE: "multiline-text-field", 7 | CHECKBOX: "checkbox", 8 | RADIOGROUP: "radio-group", 9 | SELECTDROPDOWN: "select-drop-down", 10 | DATEFIELD: "date-field", 11 | TIMEFIELD: "time-field", 12 | FILEUPLOAD: "file-upload", 13 | IMAGEUPLOAD: "image-upload", 14 | TOGGLE: "toggle", 15 | CHECKLIST: "checklist", 16 | SIGNATURE: "signature", 17 | MULTICHOICES: "multi-choices", 18 | SCANCODE: "scan-code", 19 | VERIFIEDID: "verified-id", 20 | }; 21 | 22 | export const FormTextDataTypes = { 23 | TEXT: "text", 24 | }; 25 | 26 | export const FormItemTypes = { 27 | CONTROL: "control", 28 | CONTAINER: "container", 29 | }; 30 | 31 | export const FormPublishStatus = { 32 | DRAFT: "draft", 33 | PUBLISHED: "published", 34 | }; 35 | 36 | export const FormContainerList = [ 37 | { 38 | id: '', 39 | controlName: FormControlNames.STEPCONTAINER, 40 | displayText: "Workflow Step", 41 | itemType: FormItemTypes.CONTAINER, 42 | icon: "fa fa-building", 43 | heading: "Container Heading", 44 | subHeading: "Container SubHeading", 45 | }, 46 | ]; 47 | 48 | export const FormControlList = [ 49 | { 50 | id: '', 51 | controlName: FormControlNames.INPUTTEXTFIELD, 52 | displayText: "Text Field", 53 | placeholder: "Placeholder for Text Field", 54 | description: "Some Description about the field", 55 | labelName: "Text Field", 56 | itemType: FormItemTypes.CONTROL, 57 | dataType: FormTextDataTypes.TEXT, 58 | icon: "fas fa-text-height", 59 | required: false, 60 | category: "text-elements", 61 | containerId: '', 62 | }, 63 | { 64 | id: '', 65 | controlName: FormControlNames.INPUTMULTILINE, 66 | displayText: "Notes", 67 | description: "Some Description about the field", 68 | placeholder: "Please write your notes here.", 69 | labelName: "Notes", 70 | rows: 4, 71 | itemType: FormItemTypes.CONTROL, 72 | icon: "far fa-file", 73 | required: false, 74 | category: "text-elements", 75 | containerId: '', 76 | }, 77 | { 78 | id: '', 79 | controlName: FormControlNames.RADIOGROUP, 80 | displayText: "Radio", 81 | description: "Some Description about the field", 82 | labelName: "Label for Radio", 83 | itemType: FormItemTypes.CONTROL, 84 | icon: "far fa-dot-circle", 85 | required: false, 86 | items: [ 87 | { 88 | id: generateID(), 89 | value: "Button__-1", 90 | label: "Button 1", 91 | }, 92 | { 93 | id: generateID(), 94 | value: "Button__-2", 95 | label: "Button 2", 96 | }, 97 | ], 98 | category: "other-elements", 99 | containerId: '', 100 | }, 101 | { 102 | id: '', 103 | controlName: FormControlNames.TOGGLE, 104 | displayText: "Toggle", 105 | description: "Some Description about the field", 106 | labelName: "Label for Toggle", 107 | itemType: FormItemTypes.CONTROL, 108 | icon: "fas fa-toggle-on", 109 | required: false, 110 | category: "other-elements", 111 | containerId: '', 112 | }, 113 | { 114 | id: '', 115 | controlName: FormControlNames.CHECKLIST, 116 | displayText: "Checklist", 117 | description: "Some Description about the field", 118 | labelName: "Label for Checklist", 119 | itemType: FormItemTypes.CONTROL, 120 | icon: "fas fa-tasks", 121 | required: false, 122 | items: [ 123 | { 124 | id: generateID(), 125 | value: "Check__-1", 126 | label: "Check 1", 127 | }, 128 | { 129 | id: generateID(), 130 | value: "Check__-2", 131 | label: "Check 2", 132 | }, 133 | ], 134 | category: "other-elements", 135 | containerId: '', 136 | }, 137 | { 138 | id: '', 139 | controlName: FormControlNames.SELECTDROPDOWN, 140 | displayText: "Dropdown", 141 | description: "Some Description about the field", 142 | labelName: "Label for Dropdown", 143 | itemType: FormItemTypes.CONTROL, 144 | icon: "far fa-caret-square-down", 145 | required: false, 146 | items: [ 147 | { 148 | id: generateID(), 149 | value: "Option__-1", 150 | label: "Option 1", 151 | }, 152 | { 153 | id: generateID(), 154 | value: "Option__-2", 155 | label: "Option 2", 156 | }, 157 | ], 158 | category: "other-elements", 159 | containerId: '', 160 | }, 161 | { 162 | id: '', 163 | controlName: FormControlNames.CHECKBOX, 164 | displayText: "Checkbox", 165 | description: "Some Description about the field", 166 | labelName: "Label for Checkbox", 167 | placeholder: "Place Holder Text", 168 | itemType: FormItemTypes.CONTROL, 169 | icon: "far fa-check-square", 170 | required: false, 171 | category: "other-elements", 172 | containerId: '', 173 | }, 174 | { 175 | id: '', 176 | controlName: FormControlNames.DATEFIELD, 177 | displayText: "Date Picker", 178 | description: "Some Description about the field", 179 | labelName: "Label for Date", 180 | itemType: FormItemTypes.CONTROL, 181 | icon: "far fa-calendar", 182 | required: false, 183 | category: "date-elements", 184 | containerId: '', 185 | }, 186 | { 187 | id: '', 188 | controlName: FormControlNames.TIMEFIELD, 189 | displayText: "Time", 190 | description: "Some Description about the field", 191 | labelName: "Label for Time", 192 | itemType: FormItemTypes.CONTROL, 193 | icon: "far fa-clock", 194 | required: false, 195 | category: "date-elements", 196 | containerId: '', 197 | }, 198 | { 199 | id: '', 200 | controlName: FormControlNames.SIGNATURE, 201 | displayText: "Signature", 202 | description: "Some Description about the field", 203 | labelName: "Label for Signature", 204 | itemType: FormItemTypes.CONTROL, 205 | icon: "fa fa-signature", 206 | required: false, 207 | category: "other-elements", 208 | containerId: '', 209 | }, 210 | { 211 | id: '', 212 | controlName: FormControlNames.FILEUPLOAD, 213 | displayText: "Upload", 214 | description: "Some Description about the field", 215 | labelName: "Label for File Upload", 216 | itemType: FormItemTypes.CONTROL, 217 | icon: "fas fa-cloud-upload-alt", 218 | required: false, 219 | category: "media-elements", 220 | containerId: '', 221 | }, 222 | { 223 | id: '', 224 | controlName: FormControlNames.IMAGEUPLOAD, 225 | displayText: "Image", 226 | description: "Some Description about the field", 227 | labelName: "Label for Image Upload", 228 | itemType: FormItemTypes.CONTROL, 229 | icon: "far fa-image", 230 | required: false, 231 | category: "media-elements", 232 | containerId: '', 233 | }, 234 | ]; 235 | -------------------------------------------------------------------------------- /src/components/FormBuilder/FormBuilder.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useEffect, useState } from "react"; 2 | import { Button } from "@mui/material"; 3 | import { makeStyles } from 'tss-react/mui'; 4 | import { DndProvider } from "react-dnd"; 5 | import { HTML5Backend } from "react-dnd-html5-backend"; 6 | import DropContainerComponent from './subcomponents/DropContainerComponent' 7 | import EditPropertiesComponent from './subcomponents/EditPropertiesComponent' 8 | import { 9 | isMobile as libIsMobile, 10 | isTablet as libIsTablet, 11 | } from "react-device-detect"; 12 | import LeftSidebar from "./LeftSidebar"; 13 | import useFormBuilder from "./hooks/useFormBuilder"; 14 | import useFormPreview from './hooks/useFormPreview'; 15 | import { Publish, RemoveRedEye } from "@mui/icons-material"; 16 | import { FormContainerList, FormControlList, FormItemTypes } from "../../utils/formBuilderUtils"; 17 | import FormPreview from './subcomponents/FormPreview'; 18 | import { FormLayoutComponentChildrenType, FormLayoutComponentContainerType, FormLayoutComponentsType, TemplateType } from "../../types/FormTemplateTypes"; 19 | import { generateID } from "../../utils/common"; 20 | import ControlDragComponent from "./subcomponents/ControlDragComponent"; 21 | import { useNavigate } from "react-router-dom"; 22 | 23 | let isMobile: boolean; 24 | if (process.env.NODE_ENV === "localhost") { 25 | isMobile = window.innerWidth < 1024; 26 | } else { 27 | isMobile = libIsMobile || libIsTablet || window.innerWidth < 1024; 28 | } 29 | 30 | interface FormBuilderProps { 31 | template: TemplateType 32 | } 33 | 34 | const useStyles = makeStyles()(() => ({ 35 | textField: { 36 | minWidth: "100%", 37 | maxWidth: "100%", 38 | }, 39 | sidebarHeight: { 40 | height: "calc(100vh - 63px);", 41 | overflowY: "auto", 42 | }, 43 | })); 44 | 45 | 46 | const FormBuilder: FunctionComponent = (props) => { 47 | 48 | const { 49 | handleItemAdded, 50 | saveForm, 51 | deleteContainer, 52 | deleteControl, 53 | editContainerProperties, 54 | editControlProperties, 55 | moveControl, 56 | moveControlFromSide, 57 | publishForm, 58 | selectControl, 59 | selectedTemplate, 60 | formLayoutComponents, 61 | selectedControl, 62 | } = useFormBuilder({ template: props.template }); 63 | 64 | 65 | const { showPreview, openPreviewDrawer, closePreviewDrawer } = 66 | useFormPreview(); 67 | 68 | const {classes} = useStyles(); 69 | 70 | const navigate = useNavigate(); 71 | 72 | return ( 73 | <> 74 | {!isMobile ? ( 75 | <> 76 | 77 |
78 |
79 |
83 |
84 | 85 |
86 |
87 |
88 |
89 | {/* Form Details and Action */} 90 |
91 |
92 |
93 |

{selectedTemplate?.formName}

94 |
95 | 103 |
104 | 107 | 115 | 125 |
126 |
127 |
128 |
129 |
136 |
137 | {formLayoutComponents.map((layout, ind) => { 138 | return ( 139 | 151 | ); 152 | })} 153 |
154 |
155 | 160 |
161 |
162 |
163 |
164 |
165 |
166 | 174 |
175 |
176 |
177 |
178 | {/* Preview Drawer */} 179 | 185 |
186 | 187 | ) : ( 188 | <> 189 |
190 |

191 | Form Builder is only supported on desktop devices for now. Please 192 | switch to a desktop device. 193 |

194 |
195 | 196 | )} 197 | 198 | ); 199 | }; 200 | 201 | export default FormBuilder; 202 | 203 | -------------------------------------------------------------------------------- /src/components/FormBuilder/subcomponents/DropContainerComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | import { FormLayoutComponentChildrenType, FormLayoutComponentContainerType } from '../../../types/FormTemplateTypes'; 3 | import { useDrop } from 'react-dnd' 4 | import { FormContainerList, FormItemTypes } from '../../../utils/formBuilderUtils'; 5 | import { Button } from '@mui/material'; 6 | import ControlViewComponent from './ControlViewComponent'; 7 | import './styles.scss'; 8 | 9 | interface DropContainerComponentProps { 10 | accept: string; 11 | name?: string; 12 | index?: number; 13 | handleItemAdded?: ( 14 | item: FormLayoutComponentChildrenType | FormLayoutComponentContainerType, 15 | containerId?: string 16 | ) => void; 17 | layout?: FormLayoutComponentChildrenType | FormLayoutComponentContainerType; 18 | selectedControl?: 19 | | null 20 | | FormLayoutComponentChildrenType 21 | | FormLayoutComponentContainerType; 22 | childrenComponents?: FormLayoutComponentChildrenType[]; 23 | deleteContainer?: (containerId: string) => void; 24 | deleteControl?: (controlId: string, containerId: string) => void; 25 | selectControl?: ( 26 | layout: 27 | | FormLayoutComponentChildrenType 28 | | FormLayoutComponentContainerType 29 | | undefined 30 | ) => void; 31 | moveControl?: ( 32 | item: FormLayoutComponentChildrenType, 33 | dragIndex: number, 34 | hoverIndex: number, 35 | containerId: string 36 | ) => void; 37 | } 38 | 39 | const DropContainerComponent: FunctionComponent = (props) => { 40 | const {accept,layout, childrenComponents, index, deleteContainer, deleteControl, selectControl, 41 | selectedControl, handleItemAdded, moveControl} = props; 42 | 43 | const [{ canDrop, isOver }, drop] = useDrop(() => ({ 44 | accept: accept, 45 | drop: () => (layout), 46 | collect: (monitor: any) => ({ 47 | isOver: monitor.isOver(), 48 | canDrop: monitor.canDrop(), 49 | }), 50 | })) 51 | 52 | const isActive = canDrop && isOver; 53 | let backgroundColor = accept && accept === FormItemTypes.CONTROL ? 'rgba(0,0,0,0)': 'rgba(0,0,0,0.1)'; 54 | let borderColor= 'rgba(0,0,0,0.1)'; 55 | const borderBase= '1px solid'; 56 | let border; 57 | if (isActive) { 58 | backgroundColor = 'rgba(46,212,182,0.4)' 59 | } else if (canDrop) { 60 | backgroundColor = 'rgba(255,178,15,0.7)' 61 | } 62 | 63 | if(accept === FormItemTypes.CONTROL){ 64 | border = borderBase+ ' ' + borderColor; 65 | } 66 | 67 | // Change border Color 68 | if(selectedControl && selectedControl.itemType === layout?.itemType && selectedControl.id === layout.id){ 69 | borderColor= 'rgb(255, 193, 7)'; 70 | border = borderBase+ ' ' + borderColor; 71 | } 72 | 73 | const handleDeleteContainer: React.MouseEventHandler = (event)=>{ 74 | if(deleteContainer){ 75 | deleteContainer(layout?.id as string); 76 | } 77 | // event.cancelBubble = true; 78 | if (event.stopPropagation) event.stopPropagation(); 79 | } 80 | 81 | return ( 82 | <> 83 |
88 | {accept === FormItemTypes.CONTAINER ? ( 89 | <> 90 |
94 | 106 |
107 | 108 | ) : null} 109 | 110 | {accept === FormItemTypes.CONTROL ? ( 111 | <> 112 |
{ 114 | if (selectControl) { 115 | selectControl(layout); 116 | } 117 | }} 118 | className="container-header d-flex justify-content-between py-3 mb-3" 119 | style={{ 120 | borderBottom: "1px solid rgba(0,0,0,0.1)", 121 | cursor: "pointer", 122 | }} 123 | > 124 |
Step {(index as number) + 1}
125 |
129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 |
137 |
138 |

{(layout as FormLayoutComponentContainerType)?.heading}

139 |

{(layout as FormLayoutComponentContainerType)?.subHeading}

140 |
144 | {childrenComponents?.length === 0 ? ( 145 | <> 146 |
147 | 148 | 149 | 150 | Drop Field 151 |
152 | 153 | ) : ( 154 | <> 155 | {childrenComponents?.map((component, ind) => { 156 | return ( 157 | { 161 | if (deleteControl) { 162 | deleteControl(controlId, containerId); 163 | } 164 | }} 165 | selectControl={(layout) => { 166 | if (selectControl) { 167 | selectControl(layout); 168 | } 169 | }} 170 | selectedControl={selectedControl} 171 | containerId={layout?.id as string} 172 | index={ind} 173 | moveControl={( 174 | item, 175 | dragIndex, 176 | hoverIndex, 177 | containerId 178 | ) => { 179 | if (moveControl) { 180 | moveControl( 181 | item, 182 | dragIndex, 183 | hoverIndex, 184 | containerId 185 | ); 186 | } 187 | }} 188 | /> 189 | ); 190 | })} 191 | 192 | )} 193 |
194 | {/*
195 | selectControl(layout)}> 196 | 197 | 198 | deleteContainer(layout.id)}> 199 |
*/} 200 | 201 | ) : null} 202 |
203 | 204 | ); 205 | } 206 | 207 | export default DropContainerComponent; -------------------------------------------------------------------------------- /src/components/FormBuilder/hooks/useFormBuilder.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { 3 | TemplateType, 4 | FormLayoutComponentsType, 5 | FormLayoutComponentChildrenType, 6 | FormLayoutComponentContainerType, 7 | } from "../../../types/FormTemplateTypes"; 8 | import { FormItemTypes, FormPublishStatus } from "../../../utils/formBuilderUtils"; 9 | import { generateID } from "../../../utils/common"; 10 | import { useAppDispatch } from "../../../redux/hooks"; 11 | import { openModal } from "../../../redux/uireducers/modalstrip"; 12 | import moment from "moment"; 13 | import { saveTemplate } from "../../../redux/entities/formBuilderEntity"; 14 | import useModalStrip from "../../../global-hooks/useModalStrip"; 15 | 16 | 17 | interface useFormBuilderProps{ 18 | template: TemplateType 19 | } 20 | 21 | const useFormBuilder = (props: useFormBuilderProps) => { 22 | const [selectedTemplate, setSelectedTemplate] = useState(props.template); 23 | const [formLayoutComponents, setFormLayoutComponents] = useState< 24 | FormLayoutComponentsType[] 25 | >(props.template.formLayoutComponents); 26 | const [selectedControl, setSelectedControl] = useState< 27 | | undefined 28 | | FormLayoutComponentContainerType 29 | | FormLayoutComponentChildrenType 30 | >(undefined); 31 | 32 | const dispatch = useAppDispatch(); 33 | const {showModalStrip} = useModalStrip(); 34 | 35 | // Handles a Container or a Component added on the form builder 36 | const handleItemAdded = (item: FormLayoutComponentChildrenType | FormLayoutComponentContainerType, containerId?: string) => { 37 | if (item.itemType === FormItemTypes.CONTAINER) { 38 | const newState = formLayoutComponents.slice(); 39 | newState.push({ 40 | container: { ...item as FormLayoutComponentContainerType, id: generateID() }, 41 | children: [], 42 | }); 43 | setFormLayoutComponents(newState); 44 | } else if (item.itemType === FormItemTypes.CONTROL) { 45 | const newState = formLayoutComponents.slice(); 46 | const formContainerId = newState.findIndex((f)=>f.container.id === containerId) 47 | const formContainer = {...newState[formContainerId]}; 48 | const obj = { ...item as FormLayoutComponentChildrenType, id: generateID(), containerId: containerId }; 49 | 50 | // Create a deep copy of items. 51 | const childItem = item as FormLayoutComponentChildrenType 52 | if (childItem.items) { 53 | obj.items = JSON.parse(JSON.stringify(childItem.items)); 54 | } 55 | const newChildren = formContainer.children.slice(); 56 | newChildren.push(obj as FormLayoutComponentChildrenType); 57 | formContainer.children = newChildren; 58 | newState[formContainerId] = formContainer; 59 | setFormLayoutComponents(newState); 60 | } 61 | }; 62 | 63 | // Delete a container from the layout 64 | const deleteContainer = (containerId: string) => { 65 | if (confirm("Are you sure you want to delete container?")) { 66 | const newState = formLayoutComponents.filter( 67 | (comp) => comp.container.id !== containerId, 68 | ); 69 | setFormLayoutComponents(newState); 70 | setSelectedControl((prev) => 71 | prev && 72 | (prev.id === containerId || 73 | (prev as FormLayoutComponentChildrenType).containerId === containerId) 74 | ? undefined 75 | : prev 76 | ); 77 | } 78 | }; 79 | 80 | // Delete a control from the layout 81 | const deleteControl = (controlId: string, containerId: string) => { 82 | const newState = formLayoutComponents.slice(); 83 | const formContainer = newState.filter((comp) => comp.container.id === containerId)[0]; 84 | formContainer.children = formContainer.children.filter((cont) => cont.id !== controlId); 85 | setFormLayoutComponents(newState); 86 | setSelectedControl((prev) => 87 | prev && prev.id === controlId ? undefined : prev 88 | ); 89 | }; 90 | 91 | // Selet a control on click 92 | const selectControl = (item: FormLayoutComponentChildrenType | FormLayoutComponentContainerType | undefined) => { 93 | setSelectedControl(item); 94 | }; 95 | 96 | 97 | // Edit properties of the control from Sidebar 98 | const editControlProperties = (item: FormLayoutComponentChildrenType) => { 99 | const newState = formLayoutComponents.slice(); 100 | const formContainerId = newState.findIndex((comp)=>comp.container.id === item.containerId); 101 | let formContainer = {...newState[formContainerId]}; 102 | formContainer.children.forEach((cont, ind) => { 103 | if (cont.id === item.id) { 104 | const newChildren = formContainer.children.slice(); 105 | newChildren[ind] = item 106 | formContainer.children = newChildren; 107 | return; 108 | } 109 | }); 110 | newState[formContainerId] = formContainer; 111 | setFormLayoutComponents(newState); 112 | }; 113 | 114 | // Edit properties of the container 115 | const editContainerProperties = (item: FormLayoutComponentContainerType) => { 116 | const newState = formLayoutComponents.slice(); 117 | const formContainerId = newState.findIndex((comp)=>comp.container.id === item.id); 118 | const formContainer = {...newState[formContainerId]}; 119 | formContainer.container = { 120 | ...formContainer.container, 121 | heading: item.heading, 122 | subHeading: item.subHeading, 123 | } 124 | newState[formContainerId] = formContainer; 125 | setFormLayoutComponents(newState); 126 | }; 127 | 128 | // Move a control from the sidebar based on the values on Drop Down 129 | const moveControlFromSide = ( 130 | item: FormLayoutComponentChildrenType, 131 | { 132 | containerId, 133 | position, 134 | }: FormLayoutComponentChildrenType 135 | ) => { 136 | let componentsCopy: FormLayoutComponentsType[] = JSON.parse( 137 | JSON.stringify(formLayoutComponents) 138 | ); 139 | 140 | const currentItemContainer = componentsCopy.filter((con, ind) => { 141 | return con.container.id === item.containerId; 142 | })[0]; 143 | 144 | const moveItemToContainer = componentsCopy.filter((con, ind) => { 145 | return con.container.id === containerId; 146 | })[0]; 147 | 148 | const itemIndex = currentItemContainer.children.findIndex( 149 | (con) => con.id === item.id 150 | ); 151 | 152 | const deletedItem = currentItemContainer.children.splice(itemIndex, 1); 153 | deletedItem[0].containerId = containerId; 154 | 155 | if (position !== undefined) { 156 | moveItemToContainer.children.splice(position, 0, deletedItem[0]); 157 | } else { 158 | if (item.containerId !== containerId) { 159 | if(position){ 160 | moveItemToContainer.children.splice(position, 0, deletedItem[0]); 161 | } 162 | } else { 163 | moveItemToContainer.children.splice(itemIndex, 0, deletedItem[0]); 164 | } 165 | } 166 | setSelectedControl(deletedItem[0]); 167 | setFormLayoutComponents(componentsCopy); 168 | }; 169 | 170 | // Move control when dragged to a different position 171 | const moveControl = ( 172 | item: FormLayoutComponentChildrenType, 173 | dragIndex: number, 174 | hoverIndex: number, 175 | containerId: string 176 | ) => { 177 | if (item === undefined) { 178 | return; 179 | } 180 | let componentsCopy: FormLayoutComponentsType[] = JSON.parse(JSON.stringify(formLayoutComponents)); 181 | 182 | if (dragIndex !== undefined && item.id) { 183 | if (item.containerId === containerId) { 184 | const formContainer = componentsCopy.filter((con, ind) => { 185 | return con.container.id === containerId; 186 | })[0]; 187 | const deletedItem = formContainer.children.splice( 188 | formContainer.children.findIndex((con) => con.id === item.id), 189 | 1, 190 | ); 191 | // Just to be sure that there is a item deleted 192 | if (deletedItem.length === 0) { 193 | return; 194 | } 195 | formContainer.children.splice(hoverIndex, 0, deletedItem[0]); 196 | } 197 | setFormLayoutComponents(componentsCopy); 198 | } 199 | }; 200 | 201 | const checkIfControlsInContainer = ()=> { 202 | for (let i = 0; i < formLayoutComponents.length; i++) { 203 | let componentChildren = formLayoutComponents[i].children; 204 | if (componentChildren.length === 0) { 205 | showModalStrip( 206 | "danger", 207 | "You need to have controls inside containers before updating.", 208 | 5000 209 | ); 210 | return false; 211 | } 212 | } 213 | return true; 214 | } 215 | 216 | 217 | const publishForm = () => { 218 | if (formLayoutComponents.length === 0) { 219 | showModalStrip("danger", "Form cannot be empty", 5000); 220 | return; 221 | } 222 | 223 | if (!checkIfControlsInContainer()) { 224 | return; 225 | } 226 | 227 | let currentTemplate:TemplateType = JSON.parse(JSON.stringify(selectedTemplate)); 228 | 229 | // Check if there is a change in the previous published version 230 | if(currentTemplate.publishHistory.length > 0 && JSON.stringify(currentTemplate.publishHistory[0].formLayoutComponents) === JSON.stringify(formLayoutComponents)){ 231 | showModalStrip("info","No Change in current & previous published version.",5000); 232 | return; 233 | } 234 | 235 | let updatedAt = moment().unix() * 1000; 236 | 237 | if(currentTemplate.lastPublishedAt !== 0){ 238 | // Add current layout components to publish history 239 | currentTemplate.publishHistory.splice(0,0,{ 240 | lastPublishedAt: currentTemplate.lastPublishedAt, 241 | formLayoutComponents: currentTemplate.formLayoutComponents 242 | }); 243 | } 244 | currentTemplate.formLayoutComponents = formLayoutComponents; 245 | currentTemplate.publishStatus = FormPublishStatus.PUBLISHED; 246 | currentTemplate.lastPublishedAt = updatedAt; 247 | currentTemplate.updatedAt = updatedAt; 248 | 249 | dispatch(saveTemplate(currentTemplate)).unwrap().then((newTemplate)=>{ 250 | // Adding this so that the current template in the state is updated. 251 | setSelectedTemplate(newTemplate); 252 | showModalStrip( 253 | "success", 254 | "Changes in Form Published.", 255 | 5000 256 | ); 257 | }); 258 | }; 259 | 260 | const saveForm = () => { 261 | if (formLayoutComponents.length === 0) { 262 | showModalStrip("danger", "Form cannot be empty", 5000); 263 | return; 264 | } 265 | 266 | if (!checkIfControlsInContainer()) { 267 | return; 268 | } 269 | const currentTemplate = JSON.parse(JSON.stringify(selectedTemplate)); 270 | 271 | if ( 272 | JSON.stringify(currentTemplate.formLayoutComponents) === 273 | JSON.stringify(formLayoutComponents) 274 | ) { 275 | showModalStrip( 276 | "info", 277 | "No Change in current & previous saved version.", 278 | 5000 279 | ); 280 | return; 281 | } 282 | currentTemplate.formLayoutComponents = formLayoutComponents; 283 | currentTemplate.publishStatus = FormPublishStatus.DRAFT; 284 | currentTemplate.updatedAt = moment().unix() * 1000; 285 | 286 | dispatch(saveTemplate(currentTemplate)).unwrap().then((resolvedvalue)=>{ 287 | showModalStrip( 288 | "success", 289 | "Changes in Form Saved.", 290 | 5000 291 | ); 292 | }); 293 | }; 294 | 295 | return { 296 | handleItemAdded, 297 | deleteContainer, 298 | deleteControl, 299 | selectControl, 300 | editContainerProperties, 301 | editControlProperties, 302 | moveControlFromSide, 303 | moveControl, 304 | publishForm, 305 | saveForm, 306 | selectedTemplate, 307 | formLayoutComponents, 308 | selectedControl, 309 | }; 310 | }; 311 | 312 | export default useFormBuilder; 313 | -------------------------------------------------------------------------------- /src/components/FormBuilder/subcomponents/ControlViewComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { useDrag, useDrop } from 'react-dnd'; 3 | import type { Identifier } from 'dnd-core' 4 | import { FormLayoutComponentChildrenType } from '../../../types/FormTemplateTypes'; 5 | import { FormControlNames, FormItemTypes } from '../../../utils/formBuilderUtils'; 6 | import { Checkbox, FormControl, FormControlLabel, FormGroup, MenuItem, Radio, RadioGroup, Select, Switch, TextField } from '@mui/material'; 7 | import { LocalizationProvider } from '@mui/x-date-pickers'; 8 | import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; 9 | import { DatePicker } from '@mui/x-date-pickers/DatePicker'; 10 | import { TimePicker } from '@mui/x-date-pickers/TimePicker'; 11 | 12 | const selectedColor= 'var(--primary)'; 13 | const nonSelectedColor= 'rgba(0,0,0,0.1)'; 14 | const dateFormat = 'yyyy, MMM dd'; 15 | 16 | const renderItem = (item: FormLayoutComponentChildrenType)=>{ 17 | switch(item.controlName){ 18 | case FormControlNames.INPUTTEXTFIELD: 19 | return <> 20 | 27 | 28 | 29 | case FormControlNames.INPUTMULTILINE: 30 | return <> 31 | 40 | 41 | case FormControlNames.CHECKBOX: 42 | return <> 43 |
44 | } 48 | style={{marginLeft: '0px'}} 49 | label={item.placeholder} 50 | /> 51 |
52 | 53 | 54 | case FormControlNames.RADIOGROUP: 55 | return <> 56 | 57 | {/* {item.labelName + (item.required?" *":"")} */} 58 | 59 | {item.items?.map((i)=>( 60 | } label={i.label} /> 61 | ))} 62 | 63 | 64 | 65 | 66 | case FormControlNames.SELECTDROPDOWN: 67 | return <> 68 | 69 | {/* {item.labelName + (item.required?" *":"")} */} 70 | 80 | 81 | 82 | 83 | case FormControlNames.DATEFIELD: 84 | return ( 85 | <> 86 | 87 | 91 | 92 | 93 | ); 94 | 95 | case FormControlNames.TIMEFIELD: 96 | return ( 97 | <> 98 | 99 | 103 | 104 | 105 | ); 106 | 107 | case FormControlNames.FILEUPLOAD: 108 | return <> 109 | 114 | 117 | 118 | 119 | case FormControlNames.IMAGEUPLOAD: 120 | return <> 121 | 126 | 129 | 130 | 131 | case FormControlNames.SCANCODE: 132 | return <> 133 | 138 | 141 | 142 | 143 | case FormControlNames.SCANCODE: 144 | return <> 145 | 150 | 153 | 154 | 155 | case FormControlNames.SIGNATURE: 156 | return <> 157 | 160 | 161 | 162 | case FormControlNames.TOGGLE: 163 | return <> 164 | 165 | 166 | 167 | case FormControlNames.CHECKLIST: 168 | return <> 169 | 170 | {item.items?.map((i,ind)=>( 171 | } label={i.label} style={{marginLeft: '0px'}} /> 172 | ))} 173 | 174 | 175 | } 176 | } 177 | 178 | interface ControlViewComponentProps { 179 | item: any; 180 | deleteControl: (itemId: string, containerId: string) => void; 181 | containerId: string; 182 | selectControl: (item: FormLayoutComponentChildrenType) => void; 183 | selectedControl: any; 184 | index: number; 185 | moveControl: ( 186 | item: FormLayoutComponentChildrenType, 187 | dragIndex: number, 188 | hoverIndex: number, 189 | containerId: string 190 | ) => void; 191 | } 192 | 193 | function ControlViewComponent(props: ControlViewComponentProps) { 194 | 195 | const {item, deleteControl, containerId, selectControl, 196 | selectedControl, index, moveControl} = props; 197 | 198 | let colBackgroundcolor = nonSelectedColor; 199 | let color= ''; 200 | let wrapperStyle = { 201 | border: '1px solid '+nonSelectedColor, 202 | borderRadius: '9px', 203 | marginBottom: '20px', 204 | backgroundColor: 'white', 205 | cursor: 'pointer', 206 | boxShadow: '0 9px 90px #efefef', 207 | } 208 | 209 | // Check if its the same type and id to change color. 210 | if(selectedControl && item.id === selectedControl.id && item.type === selectedControl.type){ 211 | wrapperStyle.border = '2px solid '+selectedColor; 212 | colBackgroundcolor = selectedColor; 213 | color= 'white'; 214 | } 215 | 216 | const handleDeleteControl: React.MouseEventHandler = (event)=>{ 217 | deleteControl(item.id, containerId); 218 | if (event.stopPropagation) event.stopPropagation(); 219 | } 220 | 221 | 222 | // Drag & Sort Code for functionality 223 | 224 | const ref = useRef(null); 225 | const [{ handlerId }, drop] = useDrop< 226 | FormLayoutComponentChildrenType, 227 | void, 228 | { handlerId: Identifier | null } 229 | >({ 230 | accept: FormItemTypes.CONTROL, 231 | collect(monitor: any) { 232 | return { 233 | handlerId: monitor.getHandlerId(), 234 | }; 235 | }, 236 | hover(item: FormLayoutComponentChildrenType, monitor: any) { 237 | if (!ref.current) { 238 | return; 239 | } 240 | const dragIndex = item.index; 241 | const hoverIndex = index; 242 | // Don't replace items with themselves 243 | if (dragIndex === hoverIndex) { 244 | return; 245 | } 246 | // Determine rectangle on screen 247 | const hoverBoundingRect = ref.current?.getBoundingClientRect(); 248 | // Get vertical middle 249 | const hoverMiddleY = 250 | (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; 251 | // Determine mouse position 252 | const clientOffset = monitor.getClientOffset(); 253 | // Get pixels to the top 254 | const hoverClientY = clientOffset.y - hoverBoundingRect.top; 255 | // Only perform the move when the mouse has crossed half of the items height 256 | // When dragging downwards, only move when the cursor is below 50% 257 | // When dragging upwards, only move when the cursor is above 50% 258 | // Dragging downwards 259 | if (dragIndex && dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 260 | return; 261 | } 262 | // Dragging upwards 263 | if (dragIndex && dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 264 | return; 265 | } 266 | // Time to actually perform the action 267 | moveControl(item, dragIndex as number, hoverIndex, containerId); 268 | item.index = hoverIndex; 269 | }, 270 | }); 271 | 272 | const [{ isDragging }, drag, preview] = useDrag({ 273 | type: FormItemTypes.CONTROL, 274 | item: () => { 275 | return {...item,index} 276 | }, 277 | collect: (monitor:any) => ({ 278 | isDragging: monitor.isDragging(), 279 | }), 280 | }) 281 | 282 | const opacity = isDragging ? 0 : 1 283 | drag(drop(ref)) 284 | 285 | return ( <> 286 |
selectControl(item)} style={{...wrapperStyle,opacity}}> 287 |
288 |
289 |
{item.labelName + (item.required?" *":"")}
290 |
291 | 292 | 293 | 294 | 295 | 296 |
297 |
298 | {item.description !== '' ? <> 299 |
300 |

{item.description}

301 |
302 | :null} 303 |
304 | {renderItem(item)} 305 |
306 |
307 | {/*
308 |
309 |
310 |
311 |
312 | {renderItem(item)} 313 |
314 |
315 | selectControl(item)}> 316 | 317 | 318 | deleteControl(item.id,containerId)}> 319 |
320 |
*/} 321 |
322 | ); 323 | } 324 | 325 | export default ControlViewComponent; -------------------------------------------------------------------------------- /src/components/FormBuilder/subcomponents/EditPropertiesComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from 'react' 2 | import ManageItemsListComponent from './ManageItemsListComponent'; 3 | import { FormLayoutComponentChildrenType, FormLayoutComponentContainerType, FormLayoutCoponentChildrenItemsType } from '../../../types/FormTemplateTypes'; 4 | import { Checkbox, FormControl, FormControlLabel, InputLabel, MenuItem, Select, SelectChangeEvent, TextField } from '@mui/material'; 5 | import { FormControlNames, FormItemTypes } from '../../../utils/formBuilderUtils'; 6 | import { FormLayoutComponentsType } from '../../../types/FormTemplateTypes'; 7 | import _ from "lodash"; 8 | import useModalStrip from '../../../global-hooks/useModalStrip'; 9 | 10 | const textboxStyle={ 11 | minWidth: "100%", 12 | maxWidth: "100%", 13 | marginTop: 10, 14 | } 15 | 16 | interface EditPropertiesComponentProps{ 17 | selectedControl?: 18 | | undefined 19 | | FormLayoutComponentChildrenType 20 | | FormLayoutComponentContainerType; 21 | selectControl?: (layout: FormLayoutComponentChildrenType | FormLayoutComponentContainerType | undefined) => void; 22 | editControlProperties: (updatedItem: FormLayoutComponentChildrenType)=>void; 23 | editContainerProperties: (updatedItem: FormLayoutComponentContainerType)=>void; 24 | formLayoutComponents: FormLayoutComponentsType[]; 25 | moveControlFromSide: (selectedControl: FormLayoutComponentChildrenType, moveControlObj: FormLayoutComponentChildrenType)=>void; 26 | } 27 | 28 | const EditPropertiesComponent: FC = (props)=> { 29 | 30 | const {selectedControl, selectControl, editControlProperties, editContainerProperties} = props; 31 | const [updatedItem, setUpdatedItem] = useState({}); 32 | 33 | const childUpdatedItem = updatedItem as FormLayoutComponentChildrenType; 34 | const containerUpdatedItem = updatedItem as FormLayoutComponentContainerType; 35 | 36 | const [isUpdatedItemRequired, setIsUpdatedItemRequired] = useState(false); 37 | 38 | const [moveControlObj, setMoveControlObj] = useState(null); 39 | const [controlsInContainer, setControlsInContainer] = useState(undefined); 40 | 41 | const {showModalStrip} = useModalStrip(); 42 | 43 | useEffect(()=>{ 44 | if(selectedControl){ 45 | if((selectedControl as FormLayoutComponentChildrenType).items){ 46 | setUpdatedItem({ 47 | ...selectedControl, 48 | items: JSON.parse( 49 | JSON.stringify( 50 | (selectedControl as FormLayoutComponentChildrenType).items 51 | ) 52 | ), 53 | }); 54 | } else{ 55 | setUpdatedItem({...selectedControl}); 56 | } 57 | if(selectedControl.hasOwnProperty('required')){ 58 | setIsUpdatedItemRequired( 59 | (selectedControl as FormLayoutComponentChildrenType).required 60 | ); 61 | } 62 | } 63 | setMoveControlObj(null); 64 | setControlsInContainer(undefined); 65 | },[selectedControl]) 66 | 67 | const handleChange: React.ChangeEventHandler = (e)=>{ 68 | const { name, value } = e.target; 69 | setUpdatedItem((prevState) => ({ 70 | ...prevState, [name]: value 71 | })); 72 | } 73 | 74 | 75 | const addItemInList = (item:FormLayoutCoponentChildrenItemsType)=>{ 76 | const newItems = _.cloneDeep((updatedItem as FormLayoutComponentChildrenType).items); 77 | newItems.push(item); 78 | setUpdatedItem((prevState)=>({ 79 | ...prevState, items: newItems 80 | })); 81 | } 82 | 83 | const deleteItemFromList = (item: FormLayoutCoponentChildrenItemsType)=>{ 84 | const newItems = (updatedItem as FormLayoutComponentChildrenType).items?.filter((i)=>i.id !== item.id); 85 | setUpdatedItem((prevState)=>({ 86 | ...prevState, items: newItems 87 | })); 88 | } 89 | 90 | const editIteminList = (item: FormLayoutCoponentChildrenItemsType)=>{ 91 | const newItems: FormLayoutCoponentChildrenItemsType[] = _.cloneDeep((updatedItem as FormLayoutComponentChildrenType).items); 92 | const itemToBeReplaced = newItems.filter((i)=>i.id === item.id)[0]; 93 | itemToBeReplaced.value = item.value; 94 | itemToBeReplaced.label = item.label; 95 | setUpdatedItem((prevState)=>({ 96 | ...prevState, items: newItems 97 | })); 98 | } 99 | 100 | const handleCheckChange: React.ChangeEventHandler = (e)=>{ 101 | const { name, value } = e.target; 102 | const key = e.currentTarget.checked; 103 | if(name === 'required'){ 104 | setIsUpdatedItemRequired(key); 105 | } 106 | setUpdatedItem((prevState) => ({ 107 | ...prevState, [name]: key 108 | })); 109 | } 110 | 111 | const onFormSubmit: React.FormEventHandler = (event)=>{ 112 | event.preventDefault(); 113 | editControlProperties(updatedItem as FormLayoutComponentChildrenType); 114 | } 115 | 116 | const onContainerFormSubmit: React.FormEventHandler =(event)=>{ 117 | event.preventDefault(); 118 | editContainerProperties((updatedItem as FormLayoutComponentContainerType)); 119 | } 120 | 121 | const handleMoveControlSelectChange: ((event: SelectChangeEvent, child: React.ReactNode)=>void) = (e)=>{ 122 | const {name,value}= e.target; 123 | 124 | if(name === 'containerId'){ 125 | const container = props.formLayoutComponents.filter((con)=>con.container.id === value)[0]; 126 | let stepsInContainer = container.children.length; 127 | if((selectedControl as FormLayoutComponentChildrenType).containerId === value){ 128 | stepsInContainer -= 1; 129 | } 130 | 131 | setControlsInContainer(stepsInContainer); 132 | } 133 | setMoveControlObj((prev)=>({ 134 | ...prev as FormLayoutComponentChildrenType, 135 | [name]:value 136 | })) 137 | } 138 | 139 | const getPositions = ()=>{ 140 | if(controlsInContainer !== undefined){ 141 | return Array.from(Array(controlsInContainer+1).keys()).map((item)=>{ 142 | return {item+1} 143 | }) 144 | } 145 | return null; 146 | } 147 | 148 | const onMoveControlFormSubmit: React.FormEventHandler = (e)=>{ 149 | e.preventDefault(); 150 | 151 | if(!(moveControlObj as FormLayoutComponentChildrenType).containerId){ 152 | showModalStrip("danger","You need to select Step first",5000); 153 | return; 154 | } 155 | props.moveControlFromSide(selectedControl as FormLayoutComponentChildrenType,moveControlObj as FormLayoutComponentChildrenType); 156 | } 157 | 158 | return ( 159 | <> 160 | {selectedControl ? ( 161 | <> 162 | {containerUpdatedItem.itemType === FormItemTypes.CONTAINER ? ( 163 | <> 164 |
165 |
169 |
170 | Edit Container Properties 171 |
172 |
173 | 180 |
181 |
182 | 189 |
190 | 195 | { 200 | if (selectControl) { 201 | selectControl(undefined); 202 | } 203 | }} 204 | /> 205 |
206 |
207 | 208 | ) : ( 209 | <> 210 |
211 |
212 |
Edit Field Properties
213 |
214 | 221 |
222 | {/* {childUpdatedItem.controlName === FormControlNames.INPUTTEXTFIELD ? <> 223 |
224 | 225 | Prefill Data With 226 | 231 | 232 |
233 | : null} */} 234 | {childUpdatedItem.controlName === 235 | FormControlNames.INPUTTEXTFIELD || 236 | childUpdatedItem.controlName === FormControlNames.INPUTMULTILINE || 237 | childUpdatedItem.controlName === FormControlNames.CHECKBOX ? ( 238 | <> 239 |
240 | 247 |
248 | 249 | ) : ( 250 | "" 251 | )} 252 |
253 | 261 |
262 |
263 | 270 | } 271 | label="Required" 272 | /> 273 |
274 | {childUpdatedItem.controlName === FormControlNames.RADIOGROUP || 275 | childUpdatedItem.controlName === FormControlNames.SELECTDROPDOWN || 276 | childUpdatedItem.controlName === FormControlNames.CHECKLIST ? ( 277 | <> 278 |
List Items
279 | 285 | 286 | ) : null} 287 | 292 | { 297 | if (selectControl) { 298 | selectControl(undefined); 299 | } 300 | }} 301 | /> 302 | 303 |
304 |
305 |
306 |
310 |
Move Control to Step
311 |
312 | 313 | Step: 314 | 334 | 335 |
336 |
337 | 338 | Position: 339 | 348 | 349 |
350 | 355 | { 360 | if (selectControl) { 361 | selectControl(undefined); 362 | } 363 | }} 364 | /> 365 |
366 |
367 | 368 | )} 369 | 370 | ) : ( 371 | <> 372 |

Edit Properties

373 |
377 |

Note!

378 | You need to select a container/control to edit properties. 379 |
380 | 381 | )} 382 | 383 | ); 384 | } 385 | 386 | export default EditPropertiesComponent; --------------------------------------------------------------------------------