Skip to content

Commit

Permalink
add association column type
Browse files Browse the repository at this point in the history
  • Loading branch information
omohokcoj committed Jan 6, 2022
1 parent e2d1ef5 commit 1bdae7a
Show file tree
Hide file tree
Showing 15 changed files with 181 additions and 19 deletions.
1 change: 1 addition & 0 deletions config/locales/en.yml
Expand Up @@ -300,3 +300,4 @@ en:
expand: Expand
add_api: Add API
on_collection: On collection
association: Association
1 change: 1 addition & 0 deletions config/locales/es.yml
Expand Up @@ -300,6 +300,7 @@ es:
expand: Expandir
add_api: De API
on_collection: En la colección
association: Asociación
i:
locale: es
select:
Expand Down
1 change: 1 addition & 0 deletions config/locales/pt.yml
Expand Up @@ -296,6 +296,7 @@ pt:
expand: Expandir
add_api: De API
on_collection: Na coleção
association: Associação
i:
locale: pt
select:
Expand Down
1 change: 1 addition & 0 deletions guides/customizing_resource_table.md
Expand Up @@ -14,6 +14,7 @@ Resource table can be customized via resource columns settings. Additional colum
| Integer | ![image](https://user-images.githubusercontent.com/5418788/132329871-2efd7aa4-3080-455b-8705-fe79e1b61c7d.png) | Number input |
| Decimal | ![image](https://user-images.githubusercontent.com/5418788/132330560-156afeb8-32a2-43b5-8230-e7869792ddf8.png) | Number with floating point |
| Reference | ![image](https://user-images.githubusercontent.com/5418788/132330652-6ccefa3e-b295-4527-b776-86b8cf119914.png) | Link to the related resource |
| Association | ![image](https://user-images.githubusercontent.com/5418788/148389931-ae2c51e1-e8e1-4441-a6c2-b3027f22f15e.png) | Associated resources selector |
| Date and Time | ![image](https://user-images.githubusercontent.com/5418788/132330736-efeb2b7d-4a65-4938-94cf-81794d0537b0.png) | Date selector with time. Time is displayed using browser timezone |
| Date | ![image](https://user-images.githubusercontent.com/5418788/132330973-69f3b428-b801-49a1-bfa0-d7e891a52da7.png) | Date selector |
| Boolean | ![image](https://user-images.githubusercontent.com/5418788/132331042-c4aa005a-e737-42cc-9718-0aab9ebcf7b8.png) | Checkbox |
Expand Down
2 changes: 1 addition & 1 deletion ui/src/api_configs/components/select.vue
Expand Up @@ -16,7 +16,7 @@
import api from 'api'
export default {
name: 'ResourceSelect',
name: 'ApiConfigSelect',
props: {
modelValue: {
type: [String, Number],
Expand Down
2 changes: 1 addition & 1 deletion ui/src/custom_forms/components/select.vue
Expand Up @@ -15,7 +15,7 @@
import { formsStore, loadForms } from '../scripts/store'
export default {
name: 'ResourceSelect',
name: 'FormSelect',
props: {
modelValue: {
type: [String, Number],
Expand Down
3 changes: 2 additions & 1 deletion ui/src/data_forms/components/input.vue
Expand Up @@ -19,7 +19,8 @@
v-else-if="column.reference && column.reference.model_name"
:model-value="modelValue"
:resource-name="column.reference.model_name"
:selected-resource="formData ? formData[column.reference.name] : null"
:selected-resource="formData && !column.is_array ? formData[column.reference.name] : null"
:selected-resources="formData && column.is_array ? formData[column.reference.name] : null"
:multiple="column.is_array"
:primary-key="column.reference.primary_key"
@update:model-value="$emit('update:modelValue', $event)"
Expand Down
31 changes: 30 additions & 1 deletion ui/src/data_resources/components/form.vue
Expand Up @@ -20,6 +20,12 @@
v-model="resourceData[column.name]"
:column="column"
/>
<FormInput
v-else-if="isAssociationColumn(column)"
v-model="resourceData[associationColumn(column).name]"
:column="associationColumn(column)"
:form-data="resource"
/>
<FormInput
v-else
v-model="resourceData[column.name]"
Expand Down Expand Up @@ -61,6 +67,7 @@ import { modelNameMap } from '../scripts/schema'
import { isJsonColumn, buildColumnValidator } from '../scripts/form_utils'
import { scrollToErrors } from 'data_forms/scripts/form_utils'
import { includeParams, fieldsParams } from '../scripts/query_utils'
import singularize from 'inflected/src/singularize'
import FormInput from 'data_forms/components/input'
import FormListInput from 'data_forms/components/list_input'
Expand Down Expand Up @@ -220,13 +227,35 @@ export default {
}
})
},
associationColumn (column) {
const association = this.model.associations.find((a) => a.name === column.format.association_name)
const associationModel = modelNameMap[association.model_name]
const idsColumnName = `${singularize(association.name)}_${associationModel.primary_key}s`
return {
name: idsColumnName,
display_name: column.display_name,
reference: {
name: association.name,
model_name: associationModel.name,
primary_key: associationModel.primary_key
},
is_array: true
}
},
isAssociationColumn (column) {
return column.column_type === 'association'
},
normalizeResourceData (resource) {
const data = {}
this.columns.forEach((column) => {
const value = resource[column.name]
if (isJsonColumn(column, this.resource)) {
if (this.isAssociationColumn(column)) {
const cellColumn = this.associationColumn(column)
data[cellColumn.name] = value.map((item) => item[cellColumn.reference.primary_key])
} else if (isJsonColumn(column, this.resource)) {
data[column.name] = JSON.stringify(value ?? {}, null, ' ')
} else if (value && typeof value === 'object') {
data[column.name] = JSON.parse(JSON.stringify(value))
Expand Down
69 changes: 63 additions & 6 deletions ui/src/data_resources/components/info_cell.vue
@@ -1,7 +1,7 @@
<template>
<div
class="d-flex align-items-center info-cell position-relative"
:class="isRichtext && isEdit ? 'flex-column' : 'flex-row'"
:class="isRichtext && isEdit ? 'flex-column' : (!isEdit && isAssociationColumn ? 'flex-wrap' : 'flex-row')"
>
<Spin
v-if="isLoading"
Expand All @@ -27,9 +27,9 @@
/>
<FormInput
v-else
v-model="resourceData[column.name]"
v-model="resourceData[cellColumn.name]"
:form-data="resource"
:column="column"
:column="cellColumn"
@enter="submit"
/>
</FormItem>
Expand All @@ -52,6 +52,20 @@
:reference-data="resource[column.reference.name]"
:polymorphic-model="polymorphicModel"
/>
<template
v-else-if="column.column_type === 'association' && !isEmpty"
>
<Reference
v-for="item in resource[column.format.association_name]"
:key="item[associationColumnModel.primary_key]"
:resource-id="item[associationColumnModel.primary_key]"
:reference-name="associationColumnModel.name"
:max-length="referenceSize"
class="me-1 mb-1"
:show-popover="false"
:reference-data="item"
/>
</template>
<span
v-else-if="isEmpty"
> - </span>
Expand Down Expand Up @@ -123,6 +137,7 @@ import { modelNameMap } from 'data_resources/scripts/schema'
import { isJsonColumn, buildColumnValidator } from '../scripts/form_utils'
import { includeParams, fieldsParams } from '../scripts/query_utils'
import { underscore } from 'utils/scripts/string'
import singularize from 'inflected/src/singularize'
import api from 'api'
export default {
Expand Down Expand Up @@ -171,6 +186,32 @@ export default {
}
},
computed: {
associationColumn () {
const association = this.associationColumnAssociation
const associationModel = this.associationColumnModel
const idsColumnName = `${singularize(association.name)}_${associationModel.primary_key}s`
return {
name: idsColumnName,
display_name: this.column.display_name,
reference: {
name: association.name,
model_name: associationModel.name,
primary_key: associationModel.primary_key
},
is_array: true
}
},
isAssociationColumn () {
return this.column.column_type === 'association'
},
cellColumn () {
if (this.isAssociationColumn) {
return this.associationColumn
} else {
return this.column
}
},
withClipboard () {
return !!navigator.clipboard
},
Expand Down Expand Up @@ -211,7 +252,11 @@ export default {
return isJsonColumn(this.column, this.resource)
},
value () {
return this.resource[this.column.name]
if (this.isAssociationColumn) {
return this.resource[this.column.format.association_name]
} else {
return this.resource[this.column.name]
}
},
isEditable () {
return this.editable && (this.column.access_type === 'read_write' || this.isActiveStorage)
Expand Down Expand Up @@ -241,7 +286,13 @@ export default {
return this.column.reference?.model_name === 'active_storage/attachment'
},
isEmpty () {
return [null, undefined, ''].includes(this.value)
return [null, undefined, ''].includes(this.value) || (Array.isArray(this.value) && !this.value.length)
},
associationColumnAssociation () {
return this.model.associations.find((a) => a.name === this.column.format.association_name)
},
associationColumnModel () {
return modelNameMap[this.associationColumnAssociation.model_name]
}
},
methods: {
Expand All @@ -252,6 +303,10 @@ export default {
promise = this.$refs.dataCell.$refs.cell.copyToClipboard()
} else if (this.$refs.dataReference) {
promise = this.$refs.dataReference.copyToClipboard()
} else if (this.column.column_type === 'association') {
const text = this.resource[this.column.format.association_name].map((item) => item[this.associationColumnModel.display_column]).join(', ')
promise = navigator.clipboard.writeText(text)
}
promise.then(() => {
Expand All @@ -266,7 +321,9 @@ export default {
this.isEdit = !this.isEdit
},
assignResourceData () {
if (this.isJsonColumn) {
if (this.isAssociationColumn) {
this.resourceData[this.cellColumn.name] = this.value.map((item) => item[this.associationColumn.reference.primary_key])
} else if (this.isJsonColumn) {
this.resourceData[this.column.name] = JSON.stringify(this.value || {}, null, ' ')
} else if (this.value && typeof this.value === 'object') {
this.resourceData[this.column.name] = JSON.parse(JSON.stringify(this.value))
Expand Down
23 changes: 21 additions & 2 deletions ui/src/data_resources/components/select.vue
Expand Up @@ -54,6 +54,11 @@ export default {
type: Object,
required: false,
default: null
},
selectedResources: {
type: Array,
required: false,
default: () => ([])
}
},
emits: ['update:modelValue', 'select'],
Expand Down Expand Up @@ -130,6 +135,9 @@ export default {
selectedResource () {
this.selectedOption = this.selectedResource
},
selectedResources () {
this.selectedOptions = this.selectedResources
},
resourceName () {
this.resetData()
Expand All @@ -141,7 +149,11 @@ export default {
},
created () {
if (this.multiple) {
if (this.value?.length) {
if (this.selectedResources.length) {
this.selectedOptions = [...this.selectedResources]
this.options = [...this.selectedResources]
this.value = this.modelValue
} else if (this.value?.length) {
this.loadMultipleResourceoptionsById(this.value)
} else {
this.value ||= []
Expand Down Expand Up @@ -242,7 +254,14 @@ export default {
})
return this.resourcesRespCache[cacheKey].then((result) => {
this.options = result.data.data
if (!query && this.multiple) {
this.options = [
...this.selectedOptions,
...result.data.data.filter((item) => !this.value.includes(item[this.model.primary_key]))
]
} else {
this.options = result.data.data
}
}).catch((error) => {
console.error(error)
}).finally(() => {
Expand Down
9 changes: 8 additions & 1 deletion ui/src/data_resources/components/table.vue
Expand Up @@ -369,14 +369,21 @@ export default {
return this.model.columns.map((column) => {
if (column.reference?.model_name !== modelNameMap[this.resourceName].name &&
['read_only', 'read_write'].includes(column.access_type)) {
return {
const tableColumn = {
key: column.name,
title: column.display_name,
reference: column.reference,
format: column.format,
sortable: !column.virtual,
type: column.column_type
}
if (column.column_type === 'association') {
tableColumn.format.association_model_name =
this.model.associations.find((assoc) => assoc.name === column.format.association_name).model_name
}
return tableColumn
} else {
return null
}
Expand Down
14 changes: 11 additions & 3 deletions ui/src/data_resources/scripts/query_utils.js
Expand Up @@ -2,16 +2,16 @@ import { modelNameMap } from '../scripts/schema'

function selectReadableColumns (model, accessTypes = ['read_only', 'read_write']) {
return model.columns.map((column) => {
return accessTypes.includes(column.access_type) || model.primary_key === column.name
return column.column_type !== 'association' && (accessTypes.includes(column.access_type) || model.primary_key === column.name)
? column.name
: null
}).filter(Boolean).join(',')
}

function includeParams (model, accessTypes = ['read_only', 'read_write']) {
return model.columns.map((column) => {
return accessTypes.includes(column.access_type) && column.reference?.name
? column.reference.name
return accessTypes.includes(column.access_type) && (column.reference?.name || column.column_type === 'association')
? column.reference?.name || column.format?.association_name
: null
}).filter(Boolean).join(',')
}
Expand All @@ -29,6 +29,14 @@ function fieldsParams (model, accessTypes = ['read_only', 'read_write']) {
fields[column.reference.name] ||= selectReadableColumns(referenceModel, accessTypes)
}
}

if (column.column_type === 'association' && accessTypes.includes(column.access_type)) {
const associationModel = modelNameMap[model.associations.find((assoc) => assoc.name === column.format.association_name)?.model_name]

if (associationModel) {
fields[column.format.association_name] ||= selectReadableColumns(associationModel, accessTypes)
}
}
})

return fields
Expand Down
16 changes: 16 additions & 0 deletions ui/src/data_tables/components/table_column.vue
Expand Up @@ -16,6 +16,19 @@
:always-refer="alwaysRefer"
:polymorphic-model="polymorphicModel"
/>
<template
v-else-if="column.type === 'association'"
>
<Reference
v-for="item in row[column.format.association_name]"
:key="item[associationColumnModel.primary_key]"
:resource-id="item[associationColumnModel.primary_key]"
:reference-name="associationColumnModel.name"
class="me-1 mb-1"
:show-popover="false"
:reference-data="item"
/>
</template>
<DataCell
v-else-if="withHtml && column.type === 'string' && row[column.key]?.match(/^\<.*\>$/)"
:value="row[column.key]"
Expand Down Expand Up @@ -75,6 +88,9 @@ export default {
return null
}
},
associationColumnModel () {
return modelNameMap[this.column.format.association_model_name]
},
referenceId () {
const referenceModel = modelNameMap[this.column.reference.model_name]
Expand Down
2 changes: 1 addition & 1 deletion ui/src/queries/components/select.vue
Expand Up @@ -14,7 +14,7 @@
import { queriesStore, loadQueries } from 'reports/scripts/store'
export default {
name: 'ResourceSelect',
name: 'QuerySelect',
props: {
modelValue: {
type: [String, Number],
Expand Down

0 comments on commit 1bdae7a

Please sign in to comment.