Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 77 additions & 96 deletions src/components/graphqlFormGenerator/FormInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,113 +20,94 @@ This is a convenience component that wraps an input component, allowing
dynamically created inputs.
-->

<script>
import { h, mergeProps } from 'vue'
<template>
<component
:is="inputProps.is"
v-bind="inputProps"
v-model="model"
:gqlType="gqlType"
:types="types"
v-mask="inputProps.mask"
>
<template
v-if="help"
v-slot:append-inner
>
<v-tooltip>
<template v-slot:activator="{ props }">
<v-icon
v-bind="props"
style="cursor: default"
:icon="mdiHelpCircleOutline"
/>
</template>
<Markdown :markdown="help" />
</v-tooltip>
</template>
<!-- pass the "append" slot onto the child component -->
<template v-slot:append="slotProps">
<slot
name="append"
v-bind="slotProps"
/>
</template>
</component>
</template>

<script setup>
import { mergeProps, useAttrs } from 'vue'
import { mask } from 'vue-the-mask'
import Markdown from '@/components/Markdown.vue'
import { formElement } from '@/components/graphqlFormGenerator/mixins'
import { formElementProps, useFormElement } from '@/components/graphqlFormGenerator/mixins'
import VuetifyConfig, { getComponentProps } from '@/components/graphqlFormGenerator/components/vuetify'
import { mdiHelpCircleOutline } from '@mdi/js'
import { VIcon } from 'vuetify/components/VIcon'
import { VTooltip } from 'vuetify/components/VTooltip'
import { upperFirst } from 'lodash'

/**
* Render help icon with tooltip containing help text.
*
* @param {string} helpText - (supports markdown)
*/
export const renderHelpIcon = (helpText) => h(
VTooltip,
{ location: 'bottom' },
{
activator: ({ props }) => h(
VIcon,
{
...props,
style: {
cursor: 'default'
}
},
() => mdiHelpCircleOutline
),
default: () => h(Markdown, { markdown: helpText })
}
)

export default {
name: 'g-form-input',

defineOptions({
// Prevent fallthrough attrs overriding the supplied props for the input
// https://github.com/vuejs/core/issues/6504
inheritAttrs: false,
})

mixins: [formElement],

components: {
Markdown
},

directives: {
mask: (el, binding) => {
// only use the mask if one is provided, this allows us to use the
// mask directive on elements which it doesn't support
if (binding.value) {
mask(el, binding)
}
}
},

props: {
// dictionary of props for overriding default values
propOverrides: {
type: Object,
default: () => { Object() }
}
},

beforeCreate () {
// Set the props to pass to the form input. Note, this includes the "is"
// prop which tells Vue which component class to use.
// TODO: move to rule based system to allow changing
// of parent components based on child types?

// get the default props for this graphQL type
const componentProps = getComponentProps(this.gqlType, VuetifyConfig.namedTypes, VuetifyConfig.kinds)

// merge this in with default and override props
const propGroups = [
componentProps,
this.propOverrides || {}
]
// rules is a list so needs special treatment
const rules = propGroups.flatMap(({ rules }) => rules ?? [])

this.inputProps = mergeProps(this.$attrs, ...propGroups, { rules })
},

render () {
// Some components implement custom v-model
// (https://v2.vuejs.org/v2/guide/components-custom-events.html#Customizing-Component-v-model)
const vModel = this.inputProps.is.options?.model || { prop: 'modelValue', event: 'update:modelValue' }
return h(
this.inputProps.is,
{
...this.inputProps,
[vModel.prop]: this.model,
[`on${upperFirst(vModel.event)}`]: (value) => {
this.model = value
},
gqlType: this.gqlType,
types: this.types
},
{
'append-inner': this.help ? () => renderHelpIcon(this.help) : null,
// pass the "append" slot onto the child component
append: (slotProps) => this.$slots.append?.(slotProps)
}
)
}
const attrs = useAttrs()

const vMask = (el, binding) => {
// only use the mask if one is provided, this allows us to use the
// mask directive on elements which it doesn't support
if (binding.value) {
mask(el, binding)
}
}

const props = defineProps({
...formElementProps,
// dictionary of props for overriding default values
propOverrides: {
type: Object,
default: () => ({})
}
})

const model = defineModel()

// Set the props to pass to the form input. Note, this includes the "is"
// prop which tells Vue which component class to use.
// TODO: move to rule based system to allow changing
// of parent components based on child types?

// get the default props for this graphQL type
const componentProps = getComponentProps(props.gqlType, VuetifyConfig.namedTypes, VuetifyConfig.kinds)

// merge this in with default and override props
const propGroups = [
componentProps,
props.propOverrides || {}
]
// rules is a list so needs special treatment
const rules = propGroups.flatMap(({ rules }) => rules ?? [])

const inputProps = mergeProps(attrs, ...propGroups, { rules })

const { help } = useFormElement(props)
</script>
173 changes: 95 additions & 78 deletions src/components/graphqlFormGenerator/components/BroadcastSetting.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,95 +16,112 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->

<script>
import { h } from 'vue'
import { h, ref } from 'vue'
import { VTextField } from 'vuetify/components/VTextField'
import { formElement } from '@/components/graphqlFormGenerator/mixins'
import { renderHelpIcon } from '@/components/graphqlFormGenerator/FormInput.vue'
import { nonNullRule } from '@/components/graphqlFormGenerator/components/NonNull.vue'
import { VIcon } from 'vuetify/components/VIcon'
import { VTooltip } from 'vuetify/components/VTooltip'
import { mdiHelpCircleOutline } from '@mdi/js'
import Markdown from '@/components/Markdown.vue'
import { formElementProps, useFormElement } from '@/components/graphqlFormGenerator/mixins'
import { RULES } from '@/components/graphqlFormGenerator/components/vuetify'

/**
* Split a given string from the left.
*
* @param {string} string_ - The string to split.
* @param {string} separator - The string to split it by.
* @param {number} n - The maximum number of times to split the string.
* @returns {string[]}
*/
function lsplit (string_, separator, n) {
const split = string_.split(separator)
if (split.length <= n) {
return split
}
return [split.shift(), split.join(separator)]
}

/** Convert a string 'x=y' into an object { 'x': 'y' } */
function fromString (string_) {
const [lhs, rhs] = lsplit(string_, '=', 2)
if (rhs === undefined) {
return null
}
// const [lhs, rhs] = JavaSplit(string_, '=', 2)
if (lhs === 'inherit') {
return null
// return 'ERROR: cannot broadcast inheritance'
}
const rdict = {}
let tail = lhs
const re = /^\[([^\]]*)\](.*)$/
let sect = null
let curDict = rdict
let match = null
while (tail) {
match = tail.match(re)
if (match) {
sect = match[1]
tail = match[2]
if (tail) {
curDict[sect.trim()] = {}
curDict = curDict[sect.trim()]
} else {
curDict[sect.trim()] = rhs.trim()
}
} else {
curDict[tail.trim()] = rhs.trim()
tail = null
}
}
return rdict
}

const renderHelpIcon = (helpText) => h(
VTooltip,
{ location: 'bottom' },
{
activator: ({ props }) => h(
VIcon,
{
...props,
style: {
cursor: 'default'
}
},
() => mdiHelpCircleOutline
),
default: () => h(Markdown, { markdown: helpText })
}
)

export default {
name: 'g-broadcast-setting',

mixins: [formElement],

inheritAttrs: false,

data: () => ({
localValue: null
}),
props: {
...formElementProps,
modelValue: {
type: Object,
required: false,
}
},

methods: {
/** Split a given string from the left.
*
* @param {string} string_ - The string to split.
* @param {string} separator - The string to split it by.
* @param {number} n - The maximum number of times to split the string.
* @returns {Array<string>}
*/
lsplit (string_, separator, n) {
const split = string_.split(separator)
if (split.length <= n) {
return split
}
return [split.shift(), split.join(separator)]
},
emits: ['update:modelValue'],

fromString (string_) {
const [lhs, rhs] = this.lsplit(string_, '=', 2)
if (rhs === undefined) {
return null
}
// const [lhs, rhs] = JavaSplit(string_, '=', 2)
if (lhs === 'inherit') {
return null
// return 'ERROR: cannot broadcast inheritance'
}
const rdict = {}
let tail = lhs
const re = /^\[([^\]]*)\](.*)$/
let sect = null
let curDict = rdict
let match = null
while (tail) {
match = tail.match(re)
if (match) {
sect = match[1]
tail = match[2]
if (tail) {
curDict[sect.trim()] = {}
curDict = curDict[sect.trim()]
} else {
curDict[sect.trim()] = rhs.trim()
}
} else {
curDict[tail.trim()] = rhs.trim()
tail = null
}
}
return rdict
},
setup (props) {
const { help } = useFormElement(props)

fromObject (object_) {
let ptr = object_
let ret = ''
while (ptr) {
const keys = Object.keys(ptr)
if (keys && keys.length !== 0) {
const key = keys[0]
ptr = ptr[key]
if (typeof ptr === 'object') {
ret += `[${key}]`
} else {
ret += `${key}=${ptr}`
ptr = null
}
}
}
return ret
},
return {
help,
localValue: ref(null),
}
},

methods: {
isValid (val) {
const nonNullOutcome = nonNullRule(val)
const nonNullOutcome = RULES.nonNull(val)
return nonNullOutcome === true
? (this.modelValue != null) || 'Invalid'
: nonNullOutcome
Expand All @@ -119,7 +136,7 @@ export default {
modelValue: this.localValue,
'onUpdate:modelValue': (val) => {
this.localValue = val
this.$emit('update:modelValue', this.fromString(val))
this.$emit('update:modelValue', fromString(val))
},
rules: [this.isValid]
},
Expand Down
Loading