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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
*.sw*
.run
node_modules
ignore
ignore
.kilo*
.claude*
161 changes: 161 additions & 0 deletions client/web/compose/src/components/ModuleFields/Configurator/basic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,62 @@
{{ $t('defaultValue') }}
</b-form-checkbox>

<b-form-checkbox
:checked="isGlobalFieldMaster"
@change="handleGlobalMasterToggle"
>
{{ $t('globalField.makeGlobal') }}
</b-form-checkbox>

<b-form-group
v-if="hasGlobalFields && !isGlobalFieldMaster"
label-class="text-primary"
class="mt-2"
>
<template #label>
{{ $t('globalField.selector.label') }}
<span
v-if="isLinkedToGlobal"
class="small text-info ml-1"
>
<font-awesome-icon
:icon="['far', 'copy']"
/>
{{ $t('globalField.linked') }}
</span>
</template>
<b-input-group>
<b-form-select
v-model="selectedGlobalField"
:options="globalFieldOptions"
>
<template #first>
<b-form-select-option :value="null">
{{ $t('globalField.selector.placeholder') }}
</b-form-select-option>
</template>
</b-form-select>
<b-input-group-append>
<b-button
id="button-link-global"
variant="outline-primary"
:disabled="!selectedGlobalField"
@click="handleGlobalFieldSelect"
>
<font-awesome-icon
:icon="['far', 'copy']"
/>
</b-button>
<b-tooltip
target="button-link-global"
:triggers="['hover', 'focus']"
>
{{ $t('globalField.selector.load') }}
</b-tooltip>
</b-input-group-append>
</b-input-group>
</b-form-group>

<hr>

<b-form-group
Expand Down Expand Up @@ -225,6 +281,7 @@ export default {
data () {
return {
showValueExpr: false,
selectedGlobalField: null,

mock: {
show: true,
Expand All @@ -240,6 +297,7 @@ export default {
computed: {
...mapGetters({
getModuleByID: 'module/getByID',
getGlobalFieldsByKind: 'namespace/getGlobalFieldsByKind',
}),

noDescriptionEdit () {
Expand Down Expand Up @@ -271,6 +329,31 @@ export default {
isNew () {
return this.field.fieldID === NoID
},

isGlobalFieldMaster () {
const gf = this.field.config && this.field.config.globalField
return !!(gf && gf.namespaceID && gf.fieldID && gf.fieldID === this.field.fieldID)
},

isLinkedToGlobal () {
const gf = this.field.config && this.field.config.globalField
return !!(gf && gf.namespaceID && gf.fieldID && gf.fieldID !== this.field.fieldID)
},

globalFields () {
return this.getGlobalFieldsByKind(this.namespace.namespaceID, this.field.kind) || []
},

hasGlobalFields () {
return this.globalFields.length > 0
},

globalFieldOptions () {
return this.globalFields.map(gf => ({
value: gf.config.globalField.fieldID,
text: gf.name || gf.config.globalField.fieldID,
}))
},
},

watch: {
Expand Down Expand Up @@ -391,6 +474,84 @@ export default {
this.initMocks()
}
},

handleGlobalMasterToggle (value) {
if (!this.field.config) {
this.$set(this.field, 'config', {})
}

if (value) {
this.$set(this.field.config, 'globalField', {
namespaceID: this.namespace.namespaceID,
fieldID: this.field.fieldID,
})
} else {
this.$delete(this.field.config, 'globalField')
}
},

handleGlobalFieldSelect () {
const fieldID = this.selectedGlobalField
if (!fieldID) {
if (this.field.config && this.field.config.globalField) {
this.$delete(this.field.config, 'globalField')
}
return
}

const ns = this.$store.getters['namespace/getByID'](this.namespace.namespaceID)
if (!ns) return

const globalFields = ns.fields || []
const selectedField = globalFields.find(gf => gf.config.globalField.fieldID === fieldID)

if (!selectedField) {
return
}

const localHint = this.field.options.hint
const localDescription = this.field.options.description

this.field.isRequired = selectedField.isRequired !== undefined ? selectedField.isRequired : this.field.isRequired
this.field.isMulti = selectedField.isMulti !== undefined ? selectedField.isMulti : this.field.isMulti
this.field.defaultValue = (selectedField.defaultValue && selectedField.defaultValue.length)
? [...selectedField.defaultValue]
: this.field.defaultValue
this.field.expressions = selectedField.expressions
? { ...selectedField.expressions }
: this.field.expressions

const globalOptions = selectedField.options || {}
this.field.options = {
...globalOptions,
hint: localHint,
description: localDescription,
}

if (selectedField.config) {
this.$set(this.field, 'config', {
...selectedField.config,
globalField: {
namespaceID: this.namespace.namespaceID,
fieldID: selectedField.config.globalField.fieldID,
},
})
} else {
if (!this.field.config) {
this.$set(this.field, 'config', {})
}
this.$set(this.field.config, 'globalField', {
namespaceID: this.namespace.namespaceID,
fieldID: selectedField.fieldID,
})
}

if (this.field.defaultValue.length) {
this.initMocks(this.field.defaultValue)
}

this.showValueExpr = !!(this.field.expressions && this.field.expressions.value)
},
},
}
</script>
10 changes: 10 additions & 0 deletions client/web/compose/src/store/namespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export default function (ComposeAPI) {
set (state) {
return state.set
},

getGlobalFieldsByKind (state, { getByID }) {
return (ID, kind) => {
const ns = getByID(ID)
if (!ns || !ns.fields) {
return []
}
return ns.fields.filter(f => f.kind === kind)
}
},
},

actions: {
Expand Down
90 changes: 89 additions & 1 deletion client/web/compose/src/views/Admin/Modules/Edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ export default {
...mapGetters({
pages: 'page/set',
previousPage: 'ui/previousPage',
namespaces: 'namespace/set',
}),

title () {
Expand Down Expand Up @@ -763,6 +764,7 @@ export default {
createModule: 'module/create',
deleteModule: 'module/delete',
deletePage: 'page/delete',
updateNamespace: 'namespace/update',
}),

handleNewField () {
Expand All @@ -785,6 +787,60 @@ export default {
}
},

async updateNamespaceGlobalFields (savedModule, namespaceSource) {
const { fields = [] } = savedModule
const ns = namespaceSource || this.namespace

const globalFields = fields.filter(f => {
return f.config && f.config.globalField
})

for (const field of globalFields) {
const globalFieldConfig = field.config.globalField
if (!globalFieldConfig.fieldID) {
console.error('Global field missing fieldID:', field.name)
throw new Error(this.$t('notification:module.globalField.invalid'))
}
}

const existingGlobalFields = ns.fields || []

const fieldsMap = new Map()
existingGlobalFields.forEach(field => {
if (field.fieldID !== NoID) {
fieldsMap.set(field.fieldID, field)
}
})

globalFields.forEach(field => {
const globalFieldConfig = field.config.globalField
const fieldID = globalFieldConfig.fieldID

const newGlobalField = {
fieldID,
name: field.label || field.name,
kind: field.kind,
options: field.options,
isRequired: field.isRequired,
isMulti: field.isMulti,
defaultValue: field.defaultValue ? [...field.defaultValue] : [],
expressions: field.expressions ? { ...field.expressions } : {},
config: field.config ? { ...field.config } : {},
}
fieldsMap.set(fieldID, newGlobalField)
})

const combinedFields = Array.from(fieldsMap.values())

return this.updateNamespace({
...ns,
fields: combinedFields,
}).then((updatedNs) => {
this.$store.dispatch('namespace/load', { force: true })
return updatedNs.fields
})
},

onDiscoverySettingsSave (changes) {
this.module.config = { ...this.module.config, ...changes }
},
Expand Down Expand Up @@ -840,6 +896,22 @@ export default {
module = await this.updateModule({ ...module, fields })
}

await Promise.all([
this.$store.dispatch('namespace/load', {
namespaceID: this.namespace.namespaceID,
force: true,
}),
this.findModuleByID({
...module,
namespace: this.namespace,
force: true,
}),
])

const currentNamespace = this.namespaces.find(n => n.namespaceID === this.namespace.namespaceID)

await this.updateNamespaceGlobalFields(module, currentNamespace || this.namespace)

this.loading = true

this.module = new compose.Module({ ...module }, this.namespace)
Expand All @@ -862,7 +934,23 @@ export default {
toggleProcessing(false)
})
} else {
this.updateModule({ ...module, resourceTranslationLanguage }).then(module => {
this.updateModule({ ...module, resourceTranslationLanguage }).then(async module => {
await Promise.all([
this.$store.dispatch('namespace/load', {
namespaceID: this.namespace.namespaceID,
force: true,
}),
this.findModuleByID({
...module,
namespace: this.namespace,
force: true,
}),
])

const currentNamespace = this.namespaces.find(n => n.namespaceID === this.namespace.namespaceID)

await this.updateNamespaceGlobalFields(module, currentNamespace || this.namespace)

this.module = new compose.Module({ ...module }, this.namespace)
this.initialModuleState = this.module.clone()

Expand Down
2 changes: 1 addition & 1 deletion client/web/compose/src/views/Admin/Modules/List.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"<template>
<template>
<b-container
fluid="xl"
class="d-flex flex-column py-3"
Expand Down
4 changes: 3 additions & 1 deletion client/web/compose/src/views/Namespace/Edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ export default {
* instructs store layer to add content-language header to the API request
*/
const resourceTranslationLanguage = this.currentLanguage
let { namespaceID, name, slug, enabled, meta } = this.namespace
let { namespaceID, name, slug, enabled, meta, fields, labels } = this.namespace
let assets

// Firstly handle any new namespace assets
Expand All @@ -592,6 +592,8 @@ export default {
slug,
enabled,
meta,
fields,
labels,
resourceTranslationLanguage,
}

Expand Down
Loading