diff --git a/packages/examples/src/examples/employee-registration.ts b/packages/examples/src/examples/employee-registration.ts new file mode 100644 index 0000000000..a9c62e6749 --- /dev/null +++ b/packages/examples/src/examples/employee-registration.ts @@ -0,0 +1,688 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +import { registerExamples } from '../register'; + +export const schema = { + type: 'object', + description: + 'Demonstrates deeply nested vertical, horizontal, and group layouts with conditional visibility', + properties: { + showPersonalInfo: { + type: 'boolean', + default: true, + }, + showAddress: { + type: 'boolean', + default: true, + }, + showEmployment: { + type: 'boolean', + default: true, + }, + showEmergencyContact: { + type: 'boolean', + default: true, + }, + showBenefits: { + type: 'boolean', + default: true, + }, + person: { + type: 'object', + properties: { + firstName: { + type: 'string', + title: 'First Name', + }, + middleName: { + type: 'string', + title: 'Middle Name', + }, + lastName: { + type: 'string', + title: 'Last Name', + }, + dateOfBirth: { + type: 'string', + format: 'date', + title: 'Date of Birth', + }, + email: { + type: 'string', + format: 'email', + title: 'Email', + }, + phone: { + type: 'string', + title: 'Phone Number', + }, + showIdDetails: { + type: 'boolean', + title: 'Include ID Details', + default: true, + }, + showSsn: { + type: 'boolean', + title: 'Include SSN', + default: true, + }, + showDriversLicense: { + type: 'boolean', + title: "Include Driver's License", + default: true, + }, + ssn: { + type: 'string', + title: 'Social Security Number', + }, + driversLicense: { + type: 'string', + title: "Driver's License", + }, + }, + }, + address: { + type: 'object', + properties: { + street: { + type: 'string', + title: 'Street Address', + }, + apartment: { + type: 'string', + title: 'Apt/Suite', + }, + city: { + type: 'string', + title: 'City', + }, + state: { + type: 'string', + title: 'State', + }, + zipCode: { + type: 'string', + title: 'ZIP Code', + }, + country: { + type: 'string', + title: 'Country', + default: 'USA', + }, + }, + }, + employment: { + type: 'object', + properties: { + department: { + type: 'string', + title: 'Department', + enum: ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance'], + }, + position: { + type: 'string', + title: 'Position', + }, + startDate: { + type: 'string', + format: 'date', + title: 'Start Date', + }, + employmentType: { + type: 'string', + title: 'Employment Type', + enum: ['Full-time', 'Part-time', 'Contract', 'Intern'], + }, + salary: { + type: 'number', + title: 'Annual Salary', + }, + showManagerInfo: { + type: 'boolean', + title: 'Assign Manager', + default: true, + }, + managerName: { + type: 'string', + title: 'Manager Name', + }, + managerEmail: { + type: 'string', + format: 'email', + title: 'Manager Email', + }, + }, + }, + emergencyContact: { + type: 'object', + properties: { + name: { + type: 'string', + title: 'Contact Name', + }, + relationship: { + type: 'string', + title: 'Relationship', + }, + primaryPhone: { + type: 'string', + title: 'Primary Phone', + }, + showSecondaryContact: { + type: 'boolean', + title: 'Add Secondary Phone', + default: false, + }, + secondaryPhone: { + type: 'string', + title: 'Secondary Phone', + }, + address: { + type: 'string', + title: 'Address', + }, + }, + }, + benefits: { + type: 'object', + properties: { + healthInsurance: { + type: 'boolean', + title: 'Health Insurance', + }, + dentalInsurance: { + type: 'boolean', + title: 'Dental Insurance', + }, + visionInsurance: { + type: 'boolean', + title: 'Vision Insurance', + }, + show401k: { + type: 'boolean', + title: 'Enroll in 401(k)', + default: false, + }, + contribution401k: { + type: 'number', + title: '401(k) Contribution %', + minimum: 0, + maximum: 100, + }, + }, + }, + }, +}; + +export const uischema = { + type: 'VerticalLayout', + elements: [ + { + type: 'HorizontalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/showPersonalInfo', + label: 'Show Personal Info', + }, + { + type: 'Control', + scope: '#/properties/showAddress', + label: 'Show Address', + }, + { + type: 'Control', + scope: '#/properties/showEmployment', + label: 'Show Employment', + }, + { + type: 'Control', + scope: '#/properties/showEmergencyContact', + label: 'Show Emergency Contact', + }, + { + type: 'Control', + scope: '#/properties/showBenefits', + label: 'Show Benefits', + }, + ], + }, + { + type: 'Group', + label: 'Personal Information', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/showPersonalInfo', + schema: { const: true }, + }, + }, + elements: [ + { + type: 'HorizontalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/person/properties/firstName', + }, + { + type: 'Control', + scope: '#/properties/person/properties/middleName', + }, + { + type: 'Control', + scope: '#/properties/person/properties/lastName', + }, + ], + }, + { + type: 'VerticalLayout', + elements: [ + { + type: 'HorizontalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/person/properties/dateOfBirth', + }, + { + type: 'Control', + scope: '#/properties/person/properties/email', + }, + { + type: 'Control', + scope: '#/properties/person/properties/phone', + }, + ], + }, + { + type: 'HorizontalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/person/properties/showIdDetails', + }, + { + type: 'Control', + scope: '#/properties/person/properties/showSsn', + }, + { + type: 'Control', + scope: '#/properties/person/properties/showDriversLicense', + }, + ], + }, + { + type: 'HorizontalLayout', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/person/properties/showIdDetails', + schema: { const: true }, + }, + }, + elements: [ + { + type: 'Control', + scope: '#/properties/person/properties/ssn', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/person/properties/showSsn', + schema: { const: true }, + }, + }, + }, + { + type: 'Control', + scope: '#/properties/person/properties/driversLicense', + rule: { + effect: 'SHOW', + condition: { + scope: + '#/properties/person/properties/showDriversLicense', + schema: { const: true }, + }, + }, + }, + ], + }, + ], + }, + ], + }, + { + type: 'VerticalLayout', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/showAddress', + schema: { const: true }, + }, + }, + elements: [ + { + type: 'Group', + label: 'Address', + elements: [ + { + type: 'Control', + scope: '#/properties/address/properties/street', + }, + { + type: 'HorizontalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/address/properties/apartment', + }, + { + type: 'Control', + scope: '#/properties/address/properties/city', + }, + ], + }, + { + type: 'HorizontalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/address/properties/state', + }, + { + type: 'Control', + scope: '#/properties/address/properties/zipCode', + }, + { + type: 'Control', + scope: '#/properties/address/properties/country', + }, + ], + }, + ], + }, + ], + }, + { + type: 'HorizontalLayout', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/showEmployment', + schema: { const: true }, + }, + }, + elements: [ + { + type: 'Group', + label: 'Employment Details', + elements: [ + { + type: 'VerticalLayout', + elements: [ + { + type: 'HorizontalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/employment/properties/department', + }, + { + type: 'Control', + scope: '#/properties/employment/properties/position', + }, + ], + }, + { + type: 'HorizontalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/employment/properties/startDate', + }, + { + type: 'Control', + scope: + '#/properties/employment/properties/employmentType', + }, + { + type: 'Control', + scope: '#/properties/employment/properties/salary', + }, + ], + }, + ], + }, + ], + }, + { + type: 'VerticalLayout', + elements: [ + { + type: 'Group', + label: 'Manager Information', + elements: [ + { + type: 'Control', + scope: '#/properties/employment/properties/showManagerInfo', + }, + { + type: 'VerticalLayout', + rule: { + effect: 'SHOW', + condition: { + scope: + '#/properties/employment/properties/showManagerInfo', + schema: { const: true }, + }, + }, + elements: [ + { + type: 'Control', + scope: '#/properties/employment/properties/managerName', + }, + { + type: 'Control', + scope: '#/properties/employment/properties/managerEmail', + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + type: 'Group', + label: 'Emergency Contact', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/showEmergencyContact', + schema: { const: true }, + }, + }, + elements: [ + { + type: 'HorizontalLayout', + elements: [ + { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/emergencyContact/properties/name', + }, + { + type: 'Control', + scope: + '#/properties/emergencyContact/properties/relationship', + }, + ], + }, + { + type: 'Group', + label: 'Contact Numbers', + elements: [ + { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: + '#/properties/emergencyContact/properties/primaryPhone', + }, + { + type: 'Control', + scope: + '#/properties/emergencyContact/properties/showSecondaryContact', + }, + { + type: 'Control', + scope: + '#/properties/emergencyContact/properties/secondaryPhone', + rule: { + effect: 'SHOW', + condition: { + scope: + '#/properties/emergencyContact/properties/showSecondaryContact', + schema: { const: true }, + }, + }, + }, + ], + }, + ], + }, + ], + }, + { + type: 'Control', + scope: '#/properties/emergencyContact/properties/address', + }, + ], + }, + { + type: 'VerticalLayout', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/showBenefits', + schema: { const: true }, + }, + }, + elements: [ + { + type: 'HorizontalLayout', + elements: [ + { + type: 'Group', + label: 'Insurance Options', + elements: [ + { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/benefits/properties/healthInsurance', + }, + { + type: 'Control', + scope: '#/properties/benefits/properties/dentalInsurance', + }, + { + type: 'Control', + scope: '#/properties/benefits/properties/visionInsurance', + }, + ], + }, + ], + }, + { + type: 'Group', + label: 'Retirement', + elements: [ + { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/benefits/properties/show401k', + }, + { + type: 'Control', + scope: + '#/properties/benefits/properties/contribution401k', + rule: { + effect: 'SHOW', + condition: { + scope: '#/properties/benefits/properties/show401k', + schema: { const: true }, + }, + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +}; + +export const data = { + showPersonalInfo: true, + showAddress: true, + showEmployment: true, + showEmergencyContact: true, + showBenefits: true, + person: { + showIdDetails: true, + showSsn: true, + showDriversLicense: true, + }, + employment: { + showManagerInfo: true, + }, + emergencyContact: { + showSecondaryContact: true, + }, + benefits: { + show401k: true, + }, +}; + +registerExamples([ + { + name: 'employee-registration', + label: 'Employee Registration', + data, + schema, + uischema, + }, +]); diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index e200dc8e32..047468720f 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -72,6 +72,7 @@ import * as readonly from './examples/readonly'; import * as listWithDetailPrimitives from './examples/list-with-detail-primitives'; import * as conditionalSchemaComposition from './examples/conditional-schema-compositions'; import * as additionalErrors from './examples/additional-errors'; +import * as employeeRegistration from './examples/employee-registration'; import * as multiEnumWithLabelAndDesc from './examples/enum-multi-with-label-and-desc'; import * as additionalProperties from './examples/additional-properties'; import * as login from './examples/login'; @@ -133,6 +134,7 @@ export { enumExample, radioGroupExample, multiEnum, + employeeRegistration, multiEnumWithLabelAndDesc, enumInArray, readonly, diff --git a/packages/vue-vuetify/dev/components/ExampleForm.vue b/packages/vue-vuetify/dev/components/ExampleForm.vue index 25a2e4f6bc..3d6cd481dc 100644 --- a/packages/vue-vuetify/dev/components/ExampleForm.vue +++ b/packages/vue-vuetify/dev/components/ExampleForm.vue @@ -96,42 +96,46 @@ const properties = computed(() => ({ diff --git a/packages/vue-vuetify/dev/plugins/vuetify.ts b/packages/vue-vuetify/dev/plugins/vuetify.ts index 07c26095c3..a0b7e3eb52 100644 --- a/packages/vue-vuetify/dev/plugins/vuetify.ts +++ b/packages/vue-vuetify/dev/plugins/vuetify.ts @@ -131,6 +131,22 @@ function toBlueprint(value: string): Blueprint { return md1; } +const baseDefaults = { + VField: { hideDetails: 'auto' }, + VTextField: { hideDetails: 'auto' }, + VCombobox: { hideDetails: 'auto' }, + VSelect: { hideDetails: 'auto' }, + VAutocomplete: { hideDetails: 'auto' }, + VTextarea: { hideDetails: 'auto' }, + VNumberInput: { density: 'comfortable', hideDetails: 'auto' }, + VDateInput: { hideDetails: 'auto' }, + VCheckbox: { + density: 'comfortable', + hideDetails: 'auto', + color: 'primary', + }, +}; + function createVuetifyInstance( dark: boolean, blueprint: string, @@ -139,36 +155,13 @@ function createVuetifyInstance( locale: string, ) { const defaults = variant - ? { - VField: { - variant: variant, - }, - VTextField: { - variant: variant, - }, - VCombobox: { - variant: variant, - }, - VSelect: { - variant: variant, - }, - VAutocomplete: { - variant: variant, - }, - VTextarea: { - variant: variant, - }, - VNumberInput: { - variant: variant, - }, - VDateInput: { - variant: variant, - }, - VCheckbox: { color: 'primary' }, - } - : { - VCheckbox: { color: 'primary' }, - }; + ? Object.fromEntries( + Object.entries(baseDefaults).map(([key, props]) => [ + key, + { ...props, variant }, + ]), + ) + : baseDefaults; dayjs.locale(locale); diff --git a/packages/vue-vuetify/src/complex/ArrayControlRenderer.vue b/packages/vue-vuetify/src/complex/ArrayControlRenderer.vue index 303b4f9ef5..bd3b3b7f08 100644 --- a/packages/vue-vuetify/src/complex/ArrayControlRenderer.vue +++ b/packages/vue-vuetify/src/complex/ArrayControlRenderer.vue @@ -1,194 +1,210 @@ diff --git a/packages/vue-vuetify/src/controls/IntegerControlRenderer.vue b/packages/vue-vuetify/src/controls/IntegerControlRenderer.vue index 42b9c380ec..90fc5aaa6f 100644 --- a/packages/vue-vuetify/src/controls/IntegerControlRenderer.vue +++ b/packages/vue-vuetify/src/controls/IntegerControlRenderer.vue @@ -20,7 +20,7 @@ :error-messages="control.errors" :model-value="value" :clearable="clearable" - v-bind="vuetifyProps('v-text-field')" + v-bind="vuetifyProps('v-number-input')" @update:model-value="onChange" @focus="handleFocus" @blur="handleBlur" diff --git a/packages/vue-vuetify/src/controls/OneOfRadioGroupControlRenderer.vue b/packages/vue-vuetify/src/controls/OneOfRadioGroupControlRenderer.vue index 02fe10608f..5a5878c6ea 100644 --- a/packages/vue-vuetify/src/controls/OneOfRadioGroupControlRenderer.vue +++ b/packages/vue-vuetify/src/controls/OneOfRadioGroupControlRenderer.vue @@ -18,6 +18,7 @@ :persistent-hint="persistentHint()" :required="control.required" :error-messages="control.errors" + :inline="inline" v-bind="vuetifyProps('v-radio-group')" :model-value="control.data" @update:model-value="onChange" @@ -67,6 +68,16 @@ const controlRenderer = defineComponent({ setup(props: RendererProps) { return useVuetifyControl(useJsonFormsOneOfEnumControl(props)); }, + computed: { + inline(): boolean { + let inline = this.vuetifyProps('v-radio-group')?.inline; + if (typeof inline === 'boolean') { + return inline; + } + + return this.appliedOptions?.['orientation'] !== 'vertical'; + }, + }, }); export default controlRenderer; diff --git a/packages/vue-vuetify/src/controls/PasswordControlRenderer.vue b/packages/vue-vuetify/src/controls/PasswordControlRenderer.vue index 593d2a87cf..ad18adc38b 100644 --- a/packages/vue-vuetify/src/controls/PasswordControlRenderer.vue +++ b/packages/vue-vuetify/src/controls/PasswordControlRenderer.vue @@ -54,7 +54,7 @@ import { } from '@jsonforms/vue'; import { defineComponent, ref } from 'vue'; import { VTextField } from 'vuetify/components'; -import { useIcons, useVuetifyControl } from '../util'; +import { determineClearValue, useIcons, useVuetifyControl } from '../util'; import { default as ControlWrapper } from './ControlWrapper.vue'; const controlRenderer = defineComponent({ @@ -67,13 +67,14 @@ const controlRenderer = defineComponent({ ...rendererProps(), }, setup(props: RendererProps) { + const clearValue = determineClearValue(''); const showPassword = ref(false); const icons = useIcons(); return { ...useVuetifyControl( useJsonFormsControl(props), - (value) => value || undefined, + (value) => value || clearValue, 300, ), showPassword, diff --git a/packages/vue-vuetify/src/controls/RadioGroupControlRenderer.vue b/packages/vue-vuetify/src/controls/RadioGroupControlRenderer.vue index b9f03f228f..4fbce3d81b 100644 --- a/packages/vue-vuetify/src/controls/RadioGroupControlRenderer.vue +++ b/packages/vue-vuetify/src/controls/RadioGroupControlRenderer.vue @@ -19,6 +19,7 @@ :required="control.required" :error-messages="control.errors" :model-value="control.data" + :inline="inline" v-bind="vuetifyProps('v-radio-group')" @update:model-value="onChange" @focus="handleFocus" @@ -67,6 +68,16 @@ const controlRenderer = defineComponent({ setup(props: RendererProps) { return useVuetifyControl(useJsonFormsEnumControl(props)); }, + computed: { + inline(): boolean { + let inline = this.vuetifyProps('v-radio-group')?.inline; + if (typeof inline === 'boolean') { + return inline; + } + + return this.appliedOptions?.['orientation'] !== 'vertical'; + }, + }, }); export default controlRenderer; diff --git a/packages/vue-vuetify/src/layouts/ArrayLayoutRenderer.vue b/packages/vue-vuetify/src/layouts/ArrayLayoutRenderer.vue index b0263a00c3..ce1220c3f7 100644 --- a/packages/vue-vuetify/src/layouts/ArrayLayoutRenderer.vue +++ b/packages/vue-vuetify/src/layouts/ArrayLayoutRenderer.vue @@ -1,271 +1,296 @@