Skip to content

Instantly share code, notes, and snippets.

@tatsuyasusukida
Last active June 21, 2022 22:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tatsuyasusukida/d113b27ac717f7c65f36f138a0d49182 to your computer and use it in GitHub Desktop.
Save tatsuyasusukida/d113b27ac717f7c65f36f138a0d49182 to your computer and use it in GitHub Desktop.
🌷 Node.js tutorial to make Google Forms within 3 hours | Hana Forms Admin (a subsystem for users to manage forms) [demo video available]

🌹 Node.js tutorial to make Google Forms within 3 hours | Hana Forms Admin (a subsystem for users to manage forms) [demo video available]

Demo video thumbnail

About this article

This article describes how to make a online form creator like Google Forms Forms in Node.js. The online form creator implemented in this article consists of the following two subsystems.

Hana Forms Public: a subsystem for users to enter their answers Hana Forms Admin: a subsystem for users to manage forms

This article covers Hana Forms Admin. See Hana Forms Public for the other subsystem. The resources related to this artice are as follows:

Workflow

The workflow is as follows:

  1. Preparing for the database
  2. Preparing for coding
  3. Coding
  4. Operation check

Preparing for the database

Execute the following SQL statements to prepare for the database.

Click to go to db.sql

Run the following command in your terminal to execute SQL statements. If you have not set a password, you do not need the -p option.

mysql -u root -p 

Preparing for coding

Run the following commands in the terminal to prepare for coding.

mkdir hana-forms-admin
cd hana-forms-admin
npm init -y
npm install --save dotenv ejs express express-openid-connect morgan mysql2 nocache sequelize
touch .env api-answer-submit-delete.js api-form-initialize-add.js api-form-initialize-edit.js api-form-submit-add.js api-form-submit-delete.js api-form-submit-edit.js api-form-validate.js api-item-initialize-add.js api-item-initialize-edit.js api-item-submit-add.js api-item-submit-delete.js api-item-submit-edit.js api-item-validate.js api-option-initialize-add.js api-option-initialize-edit.js api-option-submit-add.js api-option-submit-delete.js api-option-submit-edit.js api-option-validate.js find-answer.js find-form.js find-item.js find-option.js fixture.js form.js main.js model.js render-answer-detail.js render-form-detail.js render-form-list.js render-item-detail.js render-option-detail.js static-js-app.js static-js-ui-delete.js static-js-ui-form.js validate-form.js validate-item.js validate-option.js validate.js view-answer-delete.ejs view-answer-detail.ejs view-form-add.ejs view-form-delete.ejs view-form-detail.ejs view-form-edit.ejs view-form-list.ejs view-home.ejs view-item-add.ejs view-item-delete.ejs view-item-detail.ejs view-item-edit.ejs view-layout-footer.ejs view-layout-header.ejs view-option-add.ejs view-option-delete.ejs view-option-detail.ejs view-option-edit.ejs view-partial-form.ejs view-partial-item.ejs view-partial-option.ejs

Coding

A list of source code is shown below.

Operation check

Run the following command in your terminal to create tables in the database and insert records.

node -r dotenv/config fixture.js

Run the following command in your terminal to start the server.

node -r dotenv/config main.js

Go to http://localhost:3001/ in your browser.

When the login page is displayed, log in by clicking the "Continue with Google" button.

When the top page is displayed, click the "Forms" link.

When the form list page is displayed, Click the Add link.

When the form add page is displayed, enter the contents of the form and then click the "Add" button.

When the form details page is displayed, Click the Add item link.

When the form item add page is displayed, enter the contents of the form and then click the "Add" button.

When the form item details page is displayed, Click the Back link.

When the form details page is displayed, click the URL of the answer page.

When the answer page is displayed, fill in the form and then click the "Submit" button.

When the completion page is displayed, return to the form details page and reload the page.

When the form details page has reloaded, Click the answer link.

When the answer details page is displayed, check that the answer content is displayed.

PORT=3001
DB_URL=mysql://hana_forms_user:hana_forms_pass@localhost:3306/hana_forms_db
DB_IS_SSL=0
DB_IS_VITESS=0
PUBLIC_URL=http://localhost:3000
FIXTURE_OWNER=google-oauth2|000000000000000000000
FIXTURE_SKIP=0
AUTH0_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AUTH0_BASE_URL=https://localhost:3001
AUTH0_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AUTH0_ISSUER_BASE_URL=https://xxxx.jp.auth0.com
/node_modules/
/.env
/package-lock.json
# Do not ignore package-lock.json other than gist
const model = require('./model')
exports.apiAnswerSubmitDelete = async (req, res, next) => {
try {
await model.sequelize.transaction(async (transaction) => {
await req.locals.answer.destroy({transaction})
const ok = true
const redirect = '../../../'
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const {makeForm} = require('./form')
const {makeValidationForm} = require('./validate-form')
exports.apiFormInitializeAdd = async (req, res, next) => {
try {
const form = makeForm()
const validation = makeValidationForm()
res.send({form, validation})
} catch (err) {
next(err)
}
}
const {makeForm} = require('./form')
const {makeValidationForm} = require('./validate-form')
exports.apiFormInitializeEdit = async (req, res, next) => {
try {
const form = makeForm()
const validation = makeValidationForm()
form.title = req.locals.form.title
res.send({form, validation})
} catch (err) {
next(err)
}
}
const crypto = require('crypto')
const model = require('./model')
const {validateForm} = require('./validate-form')
exports.apiFormSubmitAdd = async (req, res, next) => {
try {
const validation = validateForm(req)
if (!validation.ok) {
res.status(400).end()
return
}
await model.sequelize.transaction(async (transaction) => {
const form = await model.form.create({
code: crypto.randomBytes(4).toString('hex'),
title: req.body.form.title,
owner: req.oidc.user.sub,
}, {transaction})
const ok = true
const redirect = `../${form.code}/`
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const model = require('./model')
exports.apiFormSubmitDelete = async (req, res, next) => {
try {
await model.sequelize.transaction(async (transaction) => {
await req.locals.form.destroy({transaction})
const ok = true
const redirect = '../../'
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const model = require('./model')
const {validateForm} = require('./validate-form')
exports.apiFormSubmitEdit = async (req, res, next) => {
try {
const validation = validateForm(req)
if (!validation.ok) {
res.status(400).end()
return
}
await model.sequelize.transaction(async (transaction) => {
req.locals.form.title = req.body.form.title
await req.locals.form.save({transaction})
const ok = true
const redirect = '../'
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const {validateForm} = require('./validate-form')
exports.apiFormValidate = async (req, res, next) => {
try {
const validation = validateForm(req)
res.send({validation})
} catch (err) {
next(err)
}
}
const model = require('./model')
const {makeFormItem} = require('./form')
const {makeValidationItem} = require('./validate-item')
exports.apiItemInitializeAdd = async (req, res, next) => {
try {
const form = makeFormItem()
const validation = makeValidationItem()
const formItemTypes = await model.formItemType.findAll({
attributes: ['code', 'title'],
order: [['sort', 'asc']],
})
const options = {type: formItemTypes}
res.send({form, validation, options})
} catch (err) {
next(err)
}
}
const {Op} = require('sequelize')
const model = require('./model')
const {makeFormItem} = require('./form')
const {makeValidationItem} = require('./validate-item')
exports.apiItemInitializeEdit = async (req, res, next) => {
try {
const form = makeFormItem()
const validation = makeValidationItem()
const formItemTypes = await model.formItemType.findAll({
attributes: ['code', 'title'],
order: [['sort', 'asc']],
})
const options = {type: formItemTypes}
const formItemType = await model.formItemType.findOne({
where: {
id: {[Op.eq]: req.locals.formItem.formItemTypeId},
},
})
form.sort = req.locals.formItem.sort + ''
form.label = req.locals.formItem.label
form.isRequired = req.locals.formItem.isRequired
form.type = formItemType.code
res.send({form, validation, options})
} catch (err) {
next(err)
}
}
const crypto = require('crypto')
const {Op} = require('sequelize')
const model = require('./model')
const {validateItem} = require('./validate-item')
exports.apiItemSubmitAdd = async (req, res, next) => {
try {
const validation = validateItem(req)
if (!validation.ok) {
res.status(400).end()
return
}
await model.sequelize.transaction(async (transaction) => {
const formItemType = await model.formItemType.findOne({
where: {
code: {[Op.eq]: req.body.form.type},
},
}, {transaction})
const formItem = await model.formItem.create({
code: crypto.randomBytes(4).toString('hex'),
sort: req.body.form.sort,
label: req.body.form.label,
isRequired: req.body.form.isRequired,
formItemTypeId: formItemType.id,
formId: req.locals.form.id,
}, {transaction})
const ok = true
const redirect = `../${formItem.code}/`
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const model = require('./model')
exports.apiItemSubmitDelete = async (req, res, next) => {
try {
await model.sequelize.transaction(async (transaction) => {
await req.locals.formItem.destroy({transaction})
const ok = true
const redirect = '../../../'
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const {Op} = require('sequelize')
const model = require('./model')
const {validateItem} = require('./validate-item')
exports.apiItemSubmitEdit = async (req, res, next) => {
try {
const validation = validateItem(req)
if (!validation.ok) {
res.status(400).end()
return
}
await model.sequelize.transaction(async (transaction) => {
const formItemType = await model.formItemType.findOne({
where: {
code: {[Op.eq]: req.body.form.type},
},
}, {transaction})
req.locals.formItem.sort = req.body.form.sort
req.locals.formItem.label = req.body.form.label
req.locals.formItem.isRequired = req.body.form.isRequired
req.locals.formItem.formItemTypeId = formItemType.id
await req.locals.formItem.save({transaction})
const ok = true
const redirect = '../'
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const {validateItem} = require('./validate-item')
exports.apiItemValidate = async (req, res, next) => {
try {
const validation = validateItem(req)
res.send({validation})
} catch (err) {
next(err)
}
}
const {makeFormItemOption} = require('./form')
const {makeValidationOption} = require('./validate-option')
exports.apiOptionInitializeAdd = async (req, res, next) => {
try {
const form = makeFormItemOption()
const validation = makeValidationOption()
res.send({form, validation})
} catch (err) {
next(err)
}
}
const {makeFormItemOption} = require('./form')
const {makeValidationOption} = require('./validate-option')
exports.apiOptionInitializeEdit = async (req, res, next) => {
try {
const form = makeFormItemOption()
const validation = makeValidationOption()
form.sort = req.locals.formItemOption.sort + ''
form.label = req.locals.formItemOption.label
res.send({form, validation})
} catch (err) {
next(err)
}
}
const crypto = require('crypto')
const model = require('./model')
const {validateOption} = require('./validate-option')
exports.apiOptionSubmitAdd = async (req, res, next) => {
try {
const validation = validateOption(req)
if (!validation.ok) {
res.status(400).end()
return
}
await model.sequelize.transaction(async (transaction) => {
const formItemOption = await model.formItemOption.create({
code: crypto.randomBytes(4).toString('hex'),
sort: req.body.form.sort,
label: req.body.form.label,
formItemId: req.locals.formItem.id,
}, {transaction})
const ok = true
const redirect = `../${formItemOption.code}/`
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const model = require('./model')
exports.apiOptionSubmitDelete = async (req, res, next) => {
try {
await model.sequelize.transaction(async (transaction) => {
await req.locals.formItemOption.destroy({transaction})
const ok = true
const redirect = '../../../'
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const {Op} = require('sequelize')
const model = require('./model')
const {validateOption} = require('./validate-option')
exports.apiOptionSubmitEdit = async (req, res, next) => {
try {
const validation = validateOption(req)
if (!validation.ok) {
res.status(400).end()
return
}
await model.sequelize.transaction(async (transaction) => {
req.locals.formItemOption.sort = req.body.form.sort
req.locals.formItemOption.label = req.body.form.label
await req.locals.formItemOption.save({transaction})
const ok = true
const redirect = '../'
res.send({ok, redirect})
})
} catch (err) {
next(err)
}
}
const {validateOption} = require('./validate-option')
exports.apiOptionValidate = async (req, res, next) => {
try {
const validation = validateOption(req)
res.send({validation})
} catch (err) {
next(err)
}
}
create database hana_forms_db charset utf8;
create user hana_forms_user@localhost identified by 'hana_forms_pass';
grant all privileges on hana_forms_db.* to hana_forms_user@localhost;
const {Op} = require('sequelize')
const model = require('./model')
exports.findAnswer = async (req, res, next) => {
try {
const answer = await model.answer.findOne({
where: {
code: {[Op.eq]: req.params.answerCode},
},
})
if (!answer) {
res.status(404).end()
return
}
req.locals = req.locals || {}
req.locals.answer = answer
next()
} catch (err) {
next(err)
}
}
const {Op} = require('sequelize')
const model = require('./model')
exports.findForm = async (req, res, next) => {
try {
const form = await model.form.findOne({
where: {
code: {[Op.eq]: req.params.formCode},
},
})
if (!form) {
res.status(404).end()
return
}
if (form.owner !== req.oidc.user.sub) {
res.status(401).end()
return
}
req.locals = req.locals || {}
req.locals.form = form
next()
} catch (err) {
next(err)
}
}
const {Op} = require('sequelize')
const model = require('./model')
exports.findItem = async (req, res, next) => {
try {
const formItem = await model.formItem.findOne({
where: {
code: {[Op.eq]: req.params.itemCode},
},
})
if (!formItem) {
res.status(404).end()
return
}
req.locals = req.locals || {}
req.locals.formItem = formItem
next()
} catch (err) {
next(err)
}
}
const {Op} = require('sequelize')
const model = require('./model')
exports.findOption = async (req, res, next) => {
try {
const formItemOption = await model.formItemOption.findOne({
where: {
code: {[Op.eq]: req.params.optionCode},
},
})
if (!formItemOption) {
res.status(404).end()
return
}
req.locals = req.locals || {}
req.locals.formItemOption = formItemOption
next()
} catch (err) {
next(err)
}
}
const model = require('./model')
if (require.main === module) {
main()
}
async function main () {
try {
await model.sequelize.sync({force: true})
const formItemType = {
text: await model.formItemType.create({
code: 'text',
sort: 1,
title: 'Text input',
}),
textarea: await model.formItemType.create({
code: 'textarea',
sort: 2,
title: 'Textarea',
}),
radio: await model.formItemType.create({
code: 'radio',
sort: 3,
title: 'Radio button',
}),
checkbox: await model.formItemType.create({
code: 'checkbox',
sort: 4,
title: 'Checkbox',
}),
}
if (process.env.FIXTURE_SKIP === '1') {
return
}
const form = await model.form.create({
code: '1234abcd',
title: 'TITLE',
owner: process.env.FIXTURE_OWNER,
})
const formItems = [
await model.formItem.create({
code: '12ab',
sort: 1,
label: 'inputText',
isRequired: true,
formId: form.id,
formItemTypeId: formItemType.text.id,
}),
await model.formItem.create({
code: '34cd',
sort: 2,
label: 'inputTextarea',
isRequired: true,
formId: form.id,
formItemTypeId: formItemType.textarea.id,
}),
await model.formItem.create({
code: '56ef',
sort: 3,
label: 'selectRadio',
isRequired: true,
formId: form.id,
formItemTypeId: formItemType.radio.id,
}),
await model.formItem.create({
code: '7890',
sort: 4,
label: 'selectCheckbox',
isRequired: true,
formId: form.id,
formItemTypeId: formItemType.checkbox.id,
}),
]
const formItemOptions = [
await model.formItemOption.create({
code: '1a',
sort: 1,
label: 'selectRadio option 1',
formItemId: formItems[2].id,
}),
await model.formItemOption.create({
code: '2b',
sort: 2,
label: 'selectRadio option 2',
formItemId: formItems[2].id,
}),
await model.formItemOption.create({
code: '3c',
sort: 1,
label: 'selectCheckbox option 1',
formItemId: formItems[3].id,
}),
await model.formItemOption.create({
code: '4d',
sort: 2,
label: 'selectCheckbox option 2',
formItemId: formItems[3].id,
}),
]
const answer = await model.answer.create({
code: '1234abcd',
date: new Date(),
formId: form.id,
})
const answerItems = [
await model.answerItem.create({
sort: formItems[0].sort,
label: formItems[0].label,
input: 'INPUT',
answerId: answer.id,
formItemId: formItems[0].id,
}),
await model.answerItem.create({
sort: formItems[1].sort,
label: formItems[1].label,
input: 'INPUT',
answerId: answer.id,
formItemId: formItems[1].id,
}),
await model.answerItem.create({
sort: formItems[2].sort,
label: formItems[2].label,
input: '',
answerId: answer.id,
formItemId: formItems[2].id,
}),
await model.answerItem.create({
sort: formItems[3].sort,
label: formItems[3].label,
input: '',
answerId: answer.id,
formItemId: formItems[3].id,
}),
]
const answerItemOptions = [
await model.answerItemOption.create({
sort: 1,
label: 'LABEL',
answerItemId: answerItems[2].id,
}),
await model.answerItemOption.create({
sort: 1,
label: 'LABEL',
answerItemId: answerItems[3].id,
}),
]
} catch (err) {
console.error(err)
} finally {
model.sequelize.close()
}
}
exports.makeForm = () => {
return {
title: '',
}
}
exports.makeFormItem = () => {
return {
sort: '',
label: '',
isRequired: false,
type: '',
}
}
exports.makeFormItemOption = () => {
return {
sort: '',
label: '',
}
}
const fs = require('fs')
const path = require('path')
const files = fs.readdirSync(__dirname)
.filter(file => {
return (/\.js$/.test(file) || /\.ejs$/.test(file))
&& file !== 'loc.js'
})
const lines = files.map(file => {
const buffer = fs.readFileSync(path.join(__dirname, file))
const text = buffer.toString()
const lines = text.split('\n')
return lines.length
})
const sum = lines.reduce((memo, line) => memo + line, 0)
console.log({sum})
const path = require('path')
const morgan = require('morgan')
const express = require('express')
const nocache = require('nocache')
const {auth, requiresAuth} = require('express-openid-connect')
const {apiFormInitializeAdd} = require('./api-form-initialize-add')
const {apiFormInitializeEdit} = require('./api-form-initialize-edit')
const {apiFormValidate} = require('./api-form-validate')
const {apiFormSubmitAdd} = require('./api-form-submit-add')
const {apiFormSubmitEdit} = require('./api-form-submit-edit')
const {apiFormSubmitDelete} = require('./api-form-submit-delete')
const {apiItemInitializeAdd} = require('./api-item-initialize-add')
const {apiItemInitializeEdit} = require('./api-item-initialize-edit')
const {apiItemValidate} = require('./api-item-validate')
const {apiItemSubmitAdd} = require('./api-item-submit-add')
const {apiItemSubmitEdit} = require('./api-item-submit-edit')
const {apiItemSubmitDelete} = require('./api-item-submit-delete')
const {apiOptionInitializeAdd} = require('./api-option-initialize-add')
const {apiOptionInitializeEdit} = require('./api-option-initialize-edit')
const {apiOptionValidate} = require('./api-option-validate')
const {apiOptionSubmitAdd} = require('./api-option-submit-add')
const {apiOptionSubmitEdit} = require('./api-option-submit-edit')
const {apiOptionSubmitDelete} = require('./api-option-submit-delete')
const {apiAnswerSubmitDelete} = require('./api-answer-submit-delete')
const {findForm} = require('./find-form')
const {findItem} = require('./find-item')
const {findOption} = require('./find-option')
const {findAnswer} = require('./find-answer')
const {renderFormList} = require('./render-form-list')
const {renderFormDetail} = require('./render-form-detail')
const {renderItemDetail} = require('./render-item-detail')
const {renderOptionDetail} = require('./render-option-detail')
const {renderAnswerDetail} = require('./render-answer-detail')
if (require.main === module) {
main()
}
function main () {
try {
const router = express()
router.set('views', __dirname)
router.set('view engine', 'ejs')
router.set('strict routing', true)
router.use(morgan('dev'))
router.use(auth({
authRequired: false,
auth0Logout: true,
secret: process.env.AUTH0_SECRET,
baseURL: process.env.AUTH0_BASE_URL,
clientID: process.env.AUTH0_CLIENT_ID,
issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL,
}))
router.use(requiresAuth())
router.get('/', render('view-home', {env: process.env}))
router.get('/form/', renderFormList)
router.get('/form/add/', render('view-form-add'))
router.use('/form/:formCode([0-9a-f]+)/', findForm)
router.get('/form/:formCode([0-9a-f]+)/', renderFormDetail)
router.get('/form/:formCode([0-9a-f]+)/edit/', render('view-form-edit'))
router.get('/form/:formCode([0-9a-f]+)/delete/', render('view-form-delete'))
router.get('/form/:formCode([0-9a-f]+)/item/add/', render('view-item-add'))
router.use('/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/', findItem)
router.get('/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/', renderItemDetail)
router.get('/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/edit/', render('view-item-edit'))
router.get('/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/delete/', render('view-item-delete'))
router.get('/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/add/', render('view-option-add'))
router.use('/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/:optionCode([0-9a-f]+)/', findOption)
router.get('/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/:optionCode([0-9a-f]+)/', renderOptionDetail)
router.get('/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/:optionCode([0-9a-f]+)/edit/', render('view-option-edit'))
router.get('/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/:optionCode([0-9a-f]+)/delete/', render('view-option-delete'))
router.get('/form/:formCode([0-9a-f]+)/answer/:answerCode([0-9a-f]+)/', findAnswer)
router.get('/form/:formCode([0-9a-f]+)/answer/:answerCode([0-9a-f]+)/', renderAnswerDetail)
router.get('/form/:formCode([0-9a-f]+)/answer/:answerCode([0-9a-f]+)/delete/', render('view-answer-delete'))
router.get('/js/app.js', sendFile('static-js-app.js'))
router.get('/js/ui/form.js', sendFile('static-js-ui-form.js'))
router.get('/js/ui/delete.js', sendFile('static-js-ui-delete.js'))
router.use('/api/', nocache())
router.use('/api/', express.json())
router.get('/api/form/add/initialize', apiFormInitializeAdd)
router.post('/api/form/add/validate', apiFormValidate)
router.post('/api/form/add/submit', apiFormSubmitAdd)
router.use('/api/form/:formCode([0-9a-f]+)/', findForm)
router.get('/api/form/:formCode([0-9a-f]+)/edit/initialize', apiFormInitializeEdit)
router.put('/api/form/:formCode([0-9a-f]+)/edit/validate', apiFormValidate)
router.put('/api/form/:formCode([0-9a-f]+)/edit/submit', apiFormSubmitEdit)
router.delete('/api/form/:formCode([0-9a-f]+)/delete/submit', apiFormSubmitDelete)
router.get('/api/form/:formCode([0-9a-f]+)/item/add/initialize', apiItemInitializeAdd)
router.post('/api/form/:formCode([0-9a-f]+)/item/add/validate', apiItemValidate)
router.post('/api/form/:formCode([0-9a-f]+)/item/add/submit', apiItemSubmitAdd)
router.use('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/', findItem)
router.get('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/edit/initialize', apiItemInitializeEdit)
router.put('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/edit/validate', apiItemValidate)
router.put('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/edit/submit', apiItemSubmitEdit)
router.delete('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/delete/submit', apiItemSubmitDelete)
router.get('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/add/initialize', apiOptionInitializeAdd)
router.post('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/add/validate', apiOptionValidate)
router.post('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/add/submit', apiOptionSubmitAdd)
router.use('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/:optionCode([0-9a-f]+)/', findOption)
router.get('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/:optionCode([0-9a-f]+)/edit/initialize', apiOptionInitializeEdit)
router.put('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/:optionCode([0-9a-f]+)/edit/validate', apiOptionValidate)
router.put('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/:optionCode([0-9a-f]+)/edit/submit', apiOptionSubmitEdit)
router.delete('/api/form/:formCode([0-9a-f]+)/item/:itemCode([0-9a-f]+)/option/:optionCode([0-9a-f]+)/delete/submit', apiOptionSubmitDelete)
router.use('/api/form/:formCode([0-9a-f]+)/answer/:answerCode([0-9a-f]+)/', findAnswer)
router.delete('/api/form/:formCode([0-9a-f]+)/answer/:answerCode([0-9a-f]+)/delete/submit', apiAnswerSubmitDelete)
router.use((_, res) => res.status(404).end())
router.use((err, _, res, __) => {
res.status(err.status || 500).end()
console.error(err)
})
router.listen(process.env.PORT, () => {
console.info(`Listening on ${process.env.PORT}`)
})
} catch (err) {
console.error(err)
}
}
function sendFile(filename) {
return (_, res) => {
res.sendFile(path.join(__dirname, filename))
}
}
function render(view, locals) {
return (_, res) => {
res.render(view, locals)
}
}
const {Sequelize, DataTypes} = require('sequelize')
const dialectOptions = process.env.DB_IS_SSL === '1'
? {
ssl: {
rejectUnauthorized: true,
},
} : {}
const sequelize = new Sequelize(process.env.DB_URL, {
logQueryParameters: true,
dialectOptions,
})
const model = {
sequelize,
form: sequelize.define('hanaForm', {
code: {type: DataTypes.STRING, allowNull: false, unique: true},
title: {type: DataTypes.STRING, allowNull: false},
owner: {type: DataTypes.STRING, allowNull: false},
}, {freezeTableName: true}),
formItem: sequelize.define('hanaFormItem', {
code: {type: DataTypes.STRING, allowNull: false, unique: true},
sort: {type: DataTypes.INTEGER, allowNull: false},
label: {type: DataTypes.TEXT, allowNull: false},
isRequired: {type: DataTypes.BOOLEAN, allowNull: false},
}, {freezeTableName: true}),
formItemOption: sequelize.define('hanaFormItemOption', {
code: {type: DataTypes.STRING, allowNull: false, unique: true},
sort: {type: DataTypes.INTEGER, allowNull: false},
label: {type: DataTypes.STRING, allowNull: false},
}, {freezeTableName: true}),
formItemType: sequelize.define('hanaFormItemType', {
code: {type: DataTypes.STRING, allowNull: false, unique: true},
sort: {type: DataTypes.INTEGER, allowNull: false},
title: {type: DataTypes.STRING, allowNull: false},
}, {freezeTableName: true}),
answer: sequelize.define('hanaAnswer', {
code: {type: DataTypes.STRING, allowNull: false, unique: true},
date: {type: DataTypes.DATE, allowNull: false},
}, {freezeTableName: true}),
answerItem: sequelize.define('hanaAnswerItem', {
sort: {type: DataTypes.INTEGER, allowNull: false},
label: {type: DataTypes.TEXT, allowNull: false},
input: {type: DataTypes.TEXT, allowNull: false},
}, {freezeTableName: true}),
answerItemOption: sequelize.define('hanaAnswerItemOption', {
sort: {type: DataTypes.INTEGER, allowNull: false},
label: {type: DataTypes.STRING, allowNull: false},
}, {freezeTableName: true}),
}
const options = (as) => process.env.DB_IS_VITESS === '1'
? {
as,
foreignKey: {allowNull: false},
constraints: false,
} : {
as,
foreignKey: {allowNull: false},
onDelete: 'cascade',
onUpdate: 'cascade',
}
model.formItem.belongsTo(model.form, options('form'))
model.formItem.belongsTo(model.formItemType, options('formItemType'))
model.formItemOption.belongsTo(model.formItem, options('formItem'))
model.answer.belongsTo(model.form, options('form'))
model.answerItem.belongsTo(model.answer, options('answer'))
model.answerItemOption.belongsTo(model.answerItem, options('answerItem'))
module.exports = model
{
"name": "hanaformsadmin",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "node main.js",
"dev": "nodemon -r dotenv/config main.js",
"fixture": "node -r dotenv/config fixture.js",
"deploy": "gcloud run deploy hana-forms-admin --source . --region asia-northeast1 --platform managed --allow-unauthenticated"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^16.0.1",
"ejs": "^3.1.8",
"express": "^4.18.1",
"express-openid-connect": "^2.7.2",
"morgan": "^1.10.0",
"mysql2": "^2.3.3",
"nocache": "^3.0.4",
"sequelize": "^6.20.0"
}
}
const {Op, QueryTypes} = require('sequelize')
const model = require('./model')
exports.renderAnswerDetail = async (req, res, next) => {
try {
const {answer} = req.locals
const sql = `
select
answerItem.id as itemId,
answerItem.label as label,
answerItem.input as input,
answerItemOption.label as optionLabel
from hanaAnswerItem as answerItem
left outer join hanaAnswerItemOption as answerItemOption
on answerItem.id = answerItemOption.answerItemId
where answerItem.answerId = ?
order by answerItem.sort asc,
answerItemOption.sort asc
`
const replacements = [answer.id]
const rows = await model.sequelize.query(sql, {
replacements,
type: QueryTypes.SELECT,
})
const answerItems = partitionBy((row) => {
return row.itemId
}, rows)
.map(rows => {
const [first] = rows
return {
label: first.label,
input: first.input,
options: rows.filter((row) => {
return row.optionLabel !== null
})
.map(row => ({
label: row.optionLabel,
}))
}
})
res.locals.answer = answer
res.locals.answerItems = answerItems
res.render('view-answer-detail')
} catch (err) {
next(err)
}
}
function partitionBy (f, coll) {
if (!coll || coll.length === 0) {
return coll
}
const memo = [[coll[0]]]
return partitionByRecurse(f, coll.slice(1), memo)
.map(subcoll => subcoll.reverse())
.reverse()
}
function partitionByRecurse (f, coll, memo) {
if (coll.length === 0) {
return memo
}
const [first] = coll
if (f(first) === f(memo[0][0])) {
memo[0].unshift(first)
} else {
memo.unshift([first])
}
return partitionByRecurse(f, coll.slice(1), memo)
}
const {Op} = require('sequelize')
const model = require('./model')
exports.renderFormDetail = async (req, res, next) => {
try {
const {form} = req.locals
const formItems = await model.formItem.findAll({
where: {
formId: {[Op.eq]: form.id},
},
order: [['sort', 'asc']],
})
const answers = await model.answer.findAll({
where: {
formId: {[Op.eq]: form.id},
},
order: [['date', 'asc']],
})
res.locals.form = form
res.locals.formItems = formItems
res.locals.answers = answers
res.locals.env = process.env
res.render('view-form-detail')
} catch (err) {
next(err)
}
}
const {Op} = require('sequelize')
const model = require('./model')
exports.renderFormList = async (req, res, next) => {
try {
const forms = await model.form.findAll({
where: {
owner: {[Op.eq]: req.oidc.user.sub},
},
order: [['createdAt', 'asc']]
})
res.locals.forms = forms
res.render('view-form-list')
} catch (err) {
next(err)
}
}
const {Op} = require('sequelize')
const model = require('./model')
exports.renderItemDetail = async (req, res, next) => {
try {
const {formItem} = req.locals
const formItemType = await model.formItemType.findOne({
where: {
id: {[Op.eq]: formItem.formItemTypeId},
},
})
const formItemOptions = await model.formItemOption.findAll({
where: {
formItemId: {[Op.eq]: formItem.id},
},
order: [['sort', 'asc']],
})
res.locals.formItem = formItem
res.locals.formItemType = formItemType
res.locals.formItemOptions = formItemOptions
res.render('view-item-detail')
} catch (err) {
next(err)
}
}
exports.renderOptionDetail = async (req, res, next) => {
try {
res.locals.formItemOption = req.locals.formItemOption
res.render('view-option-detail')
} catch (err) {
next(err)
}
}
main()
function main () {
const {pathname} = window.location
if (isPathnameAdd(pathname)) {
Vue.createApp(makeOptionsForm('POST')).mount('#main')
} else if (isPathnameEdit(pathname)) {
Vue.createApp(makeOptionsForm('PUT')).mount('#main')
} else if (isPathnameDelete(pathname)) {
Vue.createApp(makeOptionsDelete()).mount('#main')
}
}
function isPathnameAdd (pathname) {
const patterns = [
'^/form/add/$',
'^/form/[0-9a-f]+/item/add/$',
'^/form/[0-9a-f]+/item/[0-9a-f]+/option/add/$',
]
return patterns.some(pattern => {
return new RegExp(pattern).test(pathname)
})
}
function isPathnameEdit (pathname) {
const patterns = [
'^/form/[0-9a-f]+/edit/$',
'^/form/[0-9a-f]+/item/[0-9a-f]+/edit/$',
'^/form/[0-9a-f]+/item/[0-9a-f]+/option/[0-9a-f]+/edit/$',
]
return patterns.some(pattern => {
return new RegExp(pattern).test(pathname)
})
}
function isPathnameDelete (pathname) {
const patterns = [
'^/form/[0-9a-f]+/delete/$',
'^/form/[0-9a-f]+/item/[0-9a-f]+/delete/$',
'^/form/[0-9a-f]+/item/[0-9a-f]+/option/[0-9a-f]+/delete/$',
'^/form/[0-9a-f]+/answer/[0-9a-f]+/delete/$',
]
return patterns.some(pattern => {
return new RegExp(pattern).test(pathname)
})
}
(function (exports) {
exports.makeOptionsDelete = (method) => {
return {
data () {
return {
api: '/api' + window.location.pathname,
}
},
methods: {
async onClickButtonSubmit () {
try {
const url = this.api + 'submit'
const options = {method: 'DELETE'}
const response = await fetch(url, options)
const {ok, redirect} = await response.json()
if (ok) {
window.location.assign(redirect)
}
} catch (err) {
console.error(err)
}
},
},
}
}
})(window);
(function (exports) {
exports.makeOptionsForm = (method) => {
return {
data () {
return {
api: '/api' + window.location.pathname,
body: null,
}
},
async created () {
try {
const url = this.api + 'initialize'
const response = await fetch(url)
this.body = await response.json()
} catch (err) {
console.error(err)
}
},
methods: {
async onClickButtonSubmit () {
try {
const options = {
method,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: JSON.stringify({form: this.body.form}),
}
const url = this.api + 'validate'
const response = await fetch(url, options)
const {validation} = await response.json()
this.body.validation = validation
if (validation.ok) {
const url = this.api + 'submit'
const response = await fetch(url, options)
const {ok, redirect} = await response.json()
if (ok) {
window.location.assign(redirect)
}
}
} catch (err) {
console.error(err)
}
},
},
}
}
})(window);
const {isValidField, isValidRequest} = require('./validate')
exports.makeValidationForm = () => {
return {
ok: null,
title: {ok: null, isNotEmpty: null},
}
}
exports.validateForm = (req) => {
const validation = exports.makeValidationForm()
if (!req || !req.body || !req.body.form) {
throw new TypeError(!req || !req.body || !req.body.form)
}
const {form} = req.body
validation.title.isNotEmpty = !/^\s*$/.test(form.title)
validation.title.ok = isValidField(validation.title)
validation.ok = isValidRequest(validation)
return validation
}
const {isValidField, isValidRequest} = require('./validate')
exports.makeValidationItem = () => {
return {
ok: null,
sort: {ok: null, isInteger: null},
label: {ok: null, isNotEmpty: null},
type: {ok: null, isNotEmpty: null},
}
}
exports.validateItem = (req) => {
const validation = exports.makeValidationItem()
if (!req || !req.body || !req.body.form) {
throw new TypeError(!req || !req.body || !req.body.form)
}
const {form} = req.body
validation.sort.isInteger = /^\d+$/.test(form.sort)
validation.sort.ok = isValidField(validation.sort)
validation.label.isNotEmpty = !/^\s*$/.test(form.label)
validation.label.ok = isValidField(validation.label)
validation.type.isNotEmpty = !/^\s*$/.test(form.type)
validation.type.ok = isValidField(validation.type)
validation.ok = isValidRequest(validation)
return validation
}
const {isValidField, isValidRequest} = require('./validate')
exports.makeValidationOption = () => {
return {
ok: null,
sort: {ok: null, isInteger: null},
label: {ok: null, isNotEmpty: null},
}
}
exports.validateOption = (req) => {
const validation = exports.makeValidationOption()
if (!req || !req.body || !req.body.form) {
throw new TypeError(!req || !req.body || !req.body.form)
}
const {form} = req.body
validation.sort.isInteger = /^\d+$/.test(form.sort)
validation.sort.ok = isValidField(validation.sort)
validation.label.isNotEmpty = !/^\s*$/.test(form.label)
validation.label.ok = isValidField(validation.label)
validation.ok = isValidRequest(validation)
return validation
}
exports.isValidField = (validationField) => {
return Object.keys(validationField).every((key) => {
return key === 'ok' || validationField[key]
})
}
exports.isValidRequest = (validation) => {
return Object.keys(validation).every((key) => {
return key === 'ok' || validation[key].ok
})
}
<%- include('view-layout-header', {title: 'Answer delete | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="../">Answer</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Delete</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container">
<h1 class="mb-3">Answer delete</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<div class="d-flex flex-wrap gap-3">
<a href="../" class="btn btn-secondary">Cancel</a>
<button class="btn btn-danger" type="submit" v-on:click.prevent="onClickButtonSubmit()">Delete</button>
</div>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Form item detail | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Answer</a>
</li>
</ol>
</div>
</nav>
<main>
<div class="container">
<h1 class="mb-3">Answer detail</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../../" class="btn btn-outline-secondary">Back</a>
<a href="./delete/" class="btn btn-outline-danger">Delete...</a>
</div>
</nav>
<section>
<h2 class="mb-3">About this answer</h2>
<dl class="mb-3">
<dt>Date</dt>
<dd><%= answer.date.toString() %></dd>
<% for (const answerItem of answerItems) { %>
<dt><%= answerItem.label %></dt>
<dd>
<% if (answerItem.options.length >= 1) { %>
<% answerItem.options.forEach((option, i) => { %>
<%- i === 0 ? '' : '<br>' %>
<%= option.label %>
<% }) %>
<% } else { %>
<%= answerItem.input %>
<% } %>
</dd>
<% } %>
</dl>
</section>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Add form | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Add</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container" v-if="body">
<h1 class="mb-3">Add form</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<%- include('./view-partial-form') %>
<button class="btn btn-primary" type="submit" v-on:click.prevent="onClickButtonSubmit()">Add</button>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Add form | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Delete</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container">
<h1 class="mb-3">Delete form</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<div class="d-flex flex-wrap gap-3">
<a href="../" class="btn btn-secondary">Cancel</a>
<button class="btn btn-danger" type="submit" v-on:click.prevent="onClickButtonSubmit()">Delete</button>
</div>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Add form | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Detail</a>
</li>
</ol>
</div>
</nav>
<main>
<div class="container">
<h1 class="mb-3">Form detail</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
<a href="./edit/" class="btn btn-outline-primary">Edit...</a>
<a href="./delete/" class="btn btn-outline-danger">Delete...</a>
<a href="./item/add/" class="btn btn-outline-primary">Add item...</a>
</div>
</nav>
<section>
<h2 class="mb-3">About this form</h2>
<dl class="mb-3">
<dt>Title</dt>
<dd><%= form.title %></dd>
<dt>URL</dt>
<dd>
<a href="<%- env.PUBLIC_URL %>/form/<%- form.code %>/" target="_blank">
<%- env.PUBLIC_URL %>/forms/<%- form.code %>/
</a>
</dd>
</dl>
</section>
<section>
<h2 class="mb-3 border-top pt-3">Items</h2>
<nav aria-label="form items">
<ul class="list-group mb-3">
<% for (const formItem of formItems) { %>
<li class="list-group-item">
<a href="./item/<%- formItem.code %>/"><%= formItem.label %></a>
</li>
<% } %>
</ul>
</nav>
</section>
<section>
<h2 class="mb-3 border-top pt-3">Answers</h2>
<nav aria-label="answers">
<ul class="list-group mb-3">
<% for (const answer of answers) { %>
<li class="list-group-item">
<a href="./answer/<%- answer.code %>/"><%= answer.date.toString() %></a>
</li>
<% } %>
</ul>
</nav>
</section>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Add form | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Edit</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container" v-if="body">
<h1 class="mb-3">Edit form</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<%- include('./view-partial-form') %>
<button class="btn btn-primary" type="submit" v-on:click.prevent="onClickButtonSubmit()">Save</button>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Forms | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Forms</a>
</li>
</ol>
</div>
</nav>
<main>
<div class="container">
<h1 class="mb-3">Forms</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
<a href="./add/" class="btn btn-outline-primary">Add...</a>
</div>
</nav>
<nav class="mb-3" aria-label="forms">
<ul class="list-group">
<% for (const form of forms) { %>
<li class="list-group-item">
<a href="./<%= form.code%>/"><%= form.title %></a>
</li>
<% } %>
</ul>
</nav>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="./" aria-current="page">Home</a>
</li>
</ol>
</div>
</nav>
<main>
<div class="container">
<h1 class="mt-3">Hana Forms Admin</h1>
<nav aria-label="menu">
<ul class="list-unstyled">
<li>
<a href="/form/">Forms</a>
</li>
<li>
<a href="/logout">Sign out</a>
</li>
</ul>
</nav>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Edit item | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="./">Add item</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container" v-if="body">
<h1 class="mb-3">Add item</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<%- include('./view-partial-item') %>
<button class="btn btn-primary" type="submit" v-on:click.prevent="onClickButtonSubmit()">Add</button>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Item delete | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="../">Item</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Delete</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container">
<h1 class="mb-3">Item delete</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<div class="d-flex flex-wrap gap-3">
<a href="../" class="btn btn-secondary">Cancel</a>
<button class="btn btn-danger" type="submit" v-on:click.prevent="onClickButtonSubmit()">Delete</button>
</div>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Item detail | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Item</a>
</li>
</ol>
</div>
</nav>
<main>
<div class="container">
<h1 class="mb-3">Item detail</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../../" class="btn btn-outline-secondary">Back</a>
<a href="./edit/" class="btn btn-outline-primary">Edit...</a>
<a href="./delete/" class="btn btn-outline-danger">Delete...</a>
<a href="./option/add/" class="btn btn-outline-primary">Add option...</a>
</div>
</nav>
<section>
<h2 class="mb-3">About this question</h2>
<dl class="mb-3">
<dt>Order</dt>
<dd><%= formItem.sort %></dd>
<dt>Label</dt>
<dd><%= formItem.label %></dd>
<dt>isRequired</dt>
<dd><%= formItem.isRequired ? 'Yes' : 'No' %></dd>
<dt>Type</dt>
<dd><%= formItemType.title %></dd>
</dl>
</section>
<section>
<h2 class="mb-3 border-top pt-3">Options</h2>
<nav aria-label="options">
<ul class="list-group mb-3">
<% for (const formItemOption of formItemOptions) { %>
<li class="list-group-item">
<a href="./option/<%= formItemOption.code%>/"><%= formItemOption.label %></a>
</li>
<% } %>
</ul>
</nav>
</section>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Edit item | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="../">Item</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Edit</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container" v-if="body">
<h1 class="mb-3">Edit item</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<%- include('./view-partial-item') %>
<button class="btn btn-primary" type="submit" v-on:click.prevent="onClickButtonSubmit()">Save</button>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
</head>
<body>
<header>
<nav class="navbar navbar-light bg-light">
<div class="container-fluid">
<a href="/" class="navbar-brand">Hana Forms Admin</a>
</div>
</nav>
</header>
<%- include('view-layout-header', {title: 'Edit option | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="../../">Item</a>
</li>
<li class="breadcrumb-item">
<a href="./">Add option</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container" v-if="body">
<h1 class="mb-3">Option edit</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<%- include('./view-partial-option') %>
<button class="btn btn-primary" type="submit" v-on:click.prevent="onClickButtonSubmit()">Add</button>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Delete option | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="../../../">Item</a>
</li>
<li class="breadcrumb-item">
<a href="../">Option</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Delete</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container">
<h1 class="mb-3">Option delete</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<div class="d-flex flex-wrap gap-3">
<a href="../" class="btn btn-secondary">Cancel</a>
<button class="btn btn-danger" type="submit" v-on:click.prevent="onClickButtonSubmit()">Delete</button>
</div>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Option detail | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="../../">Item</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Option</a>
</li>
</ol>
</div>
</nav>
<main>
<div class="container">
<h1 class="mb-3">Option detail</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../../" class="btn btn-outline-secondary">Back</a>
<a href="./edit/" class="btn btn-outline-primary">Edit...</a>
<a href="./delete/" class="btn btn-outline-danger">Delete...</a>
</div>
</nav>
<section>
<h2 class="mb-3">About this option</h2>
<dl class="mb-3">
<dt>Order</dt>
<dd><%= formItemOption.sort %></dd>
<dt>Label</dt>
<dd><%= formItemOption.label %></dd>
</dl>
</section>
</div>
</main>
<%- include('view-layout-footer') %>
<%- include('view-layout-header', {title: 'Option edit | Hana Forms Admin'}) %>
<nav class="mt-3 mb-3" aria-label="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="../../../../../../../">Home</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../../../">Forms</a>
</li>
<li class="breadcrumb-item">
<a href="../../../../../">Detail</a>
</li>
<li class="breadcrumb-item">
<a href="../../../">Item</a>
</li>
<li class="breadcrumb-item">
<a href="../">Option</a>
</li>
<li class="breadcrumb-item">
<a href="./" aria-current="page">Edit</a>
</li>
</ol>
</div>
</nav>
<main>
<div id="main">
<div class="container" v-if="body">
<h1 class="mb-3">Option edit</h1>
<nav class="mb-3" aria-label="menu">
<div class="d-flex flex-wrap gap-2">
<a href="../" class="btn btn-outline-secondary">Back</a>
</div>
</nav>
<form class="mb-3" role="form">
<%- include('./view-partial-option') %>
<button class="btn btn-primary" type="submit" v-on:click.prevent="onClickButtonSubmit()">Save</button>
</form>
</div>
</div>
</main>
<%- include('view-layout-footer') %>
<div class="form-group mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" name="title" id="title" v-model="body.form.title" v-bind:class="{'is-invalid': body.validation.title.ok === false}">
<p class="invalid-feedback" v-if="body.validation.title.ok === false">
Title is required
</p>
</div>
<div class="form-group mb-3">
<label for="sort" class="form-label">Order</label>
<input type="number" class="form-control" name="sort" id="sort" v-model="body.form.sort" v-bind:class="{'is-invalid': body.validation.sort.ok === false}">
<p class="invalid-feedback" v-if="body.validation.sort.ok === false">
Order is required
</p>
</div>
<div class="form-group mb-3">
<label for="label" class="form-label">Label</label>
<input type="text" class="form-control" name="label" id="label" v-model="body.form.label" v-bind:class="{'is-invalid': body.validation.label.ok === false}">
<p class="invalid-feedback" v-if="body.validation.label.ok === false">
Label is required
</p>
</div>
<fieldset class="mb-3">
<legend class="fs-6">Is required</legend>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="isRequired" id="isRequired" v-model="body.form.isRequired">
<label for="isRequired" class="form-check-label">This item is required</label>
</div>
</fieldset>
<fieldset class="mb-3">
<legend class="fs-6">Type</legend>
<template v-for="(type, i) of body.options.type">
<div class="form-check">
<input type="radio" class="form-check-input" name="type" v-bind:id="'type' + i" v-bind:value="type.code" v-model="body.form.type" v-bind:class="{'is-invalid': body.validation.type.ok === false}">
<label v-bind:for="'type' + i" class="form-check-label">{{type.title}}</label>
<template v-if="i === body.options.type.length - 1">
<p class="invalid-feedback" v-if="body.validation.type.ok === false">
Type is required
</p>
</template>
</div>
</template>
</fieldset>
<div class="form-group mb-3">
<label for="sort" class="form-label">Order</label>
<input type="text" class="form-control" name="sort" id="sort" v-model="body.form.sort" v-bind:class="{'is-invalid': body.validation.sort.ok === false}">
<p class="invalid-feedback" v-if="body.validation.sort.ok === false">
Order is required
</p>
</div>
<div class="form-group mb-3">
<label for="label" class="form-label">Label</label>
<input type="text" class="form-control" name="label" id="label" v-model="body.form.label" v-bind:class="{'is-invalid': body.validation.label.ok === false}">
<p class="invalid-feedback" v-if="body.validation.label.ok === false">
Label is required
</p>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment