diff --git a/frontend/components/dialog/crudRecordDialog.vue b/frontend/components/dialog/crudRecordDialog.vue
new file mode 100644
index 0000000..51d9761
--- /dev/null
+++ b/frontend/components/dialog/crudRecordDialog.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+ mdi-close
+
+
+
+
+
+
+
+
+ Close
+
+
+
+
+
+
diff --git a/frontend/components/input/genericInput.vue b/frontend/components/input/genericInput.vue
index 1fb0243..acbbb8a 100644
--- a/frontend/components/input/genericInput.vue
+++ b/frontend/components/input/genericInput.vue
@@ -3,6 +3,59 @@
+
- mdi-account
+
{{
+ item.fieldInfo.inputOptions
+ ? item.fieldInfo.inputOptions.fallbackIcon
+ : null
+ }}
+
+ >
+
+
+
+ {{ icon }}
+
+ {{ data.item.name }}
+
+
+
+
+
+ {{ icon }}
+
+ {{ data.item.name }}
+
+
+
+ >
+
+
+
+ {{ icon }}
+
+ {{ data.item.name }}
+
+
+
+
+
+ {{ icon }}
+
+ {{ data.item.name }}
+
+
+
import Draggable from 'vuedraggable'
// import { uploadFile } from '~/services/file'
-import { capitalizeString, isObject, handleError } from '~/services/base'
+import {
+ capitalizeString,
+ isObject,
+ handleError,
+ getIcon,
+} from '~/services/base'
import { executeGiraffeql } from '~/services/giraffeql'
+// import FileChip from '~/components/chip/fileChip.vue'
export default {
components: {
Draggable,
+ // FileChip,
},
props: {
item: {
@@ -457,6 +601,11 @@ export default {
isReadonly() {
return this.item.readonly
},
+ icon() {
+ return this.item.fieldInfo.typename
+ ? getIcon(this.item.fieldInfo.typename)
+ : null
+ },
},
methods: {
addRow() {
@@ -522,18 +671,20 @@ export default {
}
},
- /* handleMultipleFileInputChange(inputObject) {
+ /* handleMultipleFileInputChange(inputObject, removeFromInput = true) {
this.$set(inputObject, 'loading', true)
// inputObject.input expected to be array
inputObject.input.forEach((currentFile) => {
uploadFile(this, currentFile, (file) => {
- // add finished URL to value
- inputObject.value.push(file.fileUploadObject.servingUrl)
+ // add finished fileRecord ID to value
+ inputObject.value.push(file.fileUploadObject.fileRecord)
// remove file from input
- const index = inputObject.input.indexOf(file)
- if (index !== -1) inputObject.input.splice(index, 1)
+ if (removeFromInput) {
+ const index = inputObject.input.indexOf(file)
+ if (index !== -1) inputObject.input.splice(index, 1)
+ }
// if no files left, finish up
if (inputObject.input.length < 1) {
@@ -545,7 +696,7 @@ export default {
}
})
})
- },
+ }, */
handleSingleFileInputClear(inputObject) {
inputObject.value = null
@@ -559,7 +710,7 @@ export default {
inputObject.loading = false
},
- handleSingleFileInputChange(inputObject) {
+ /* handleSingleFileInputChange(inputObject) {
if (!inputObject.input) {
this.handleSingleFileInputClear(inputObject)
return
diff --git a/frontend/components/interface/crud/crudRecordInterface.vue b/frontend/components/interface/crud/crudRecordInterface.vue
index 33fddb0..6178e17 100644
--- a/frontend/components/interface/crud/crudRecordInterface.vue
+++ b/frontend/components/interface/crud/crudRecordInterface.vue
@@ -19,7 +19,7 @@
@update:page="setTableOptionsUpdatedTrigger('page')"
>
-
+
{{ icon || recordInfo.icon || 'mdi-domain' }}
{{
title || `${recordInfo.pluralName}`
@@ -34,7 +34,6 @@
v-if="recordInfo.addOptions"
color="primary"
darks
- class="mb-2"
@click="openAddRecordDialog()"
>
mdi-plus
@@ -58,6 +57,12 @@
+
+ >
+
+
+
+ {{
+ getIcon(item.fieldInfo.typename)
+ }}
+
+ {{ data.item.name }}
+
+
+
+
+
+ {{ getIcon(item.fieldInfo.typename) }}
+
+
+ {{ data.item.name }}
+
+
+
+ >
+
+
+
+ {{
+ getIcon(item.fieldInfo.typename)
+ }}
+
+ {{ data.item.name }}
+
+
+
+
+
+ {{
+ getIcon(item.fieldInfo.typename)
+ }}
+
+ {{ data.item.name }}
+
+
+
+
-
+
{{ icon || recordInfo.icon || 'mdi-domain' }}
{{
title || `${recordInfo.pluralName}`
@@ -34,7 +34,6 @@
v-if="recordInfo.addOptions"
color="primary"
darks
- class="mb-2"
@click="openAddRecordDialog()"
>
mdi-plus
@@ -58,6 +57,12 @@
+
+ >
+
+
+
+ {{
+ getIcon(item.fieldInfo.typename)
+ }}
+
+ {{ data.item.name }}
+
+
+
+
+
+ {{ getIcon(item.fieldInfo.typename) }}
+
+
+ {{ data.item.name }}
+
+
+
+ >
+
+
+
+ {{
+ getIcon(item.fieldInfo.typename)
+ }}
+
+ {{ data.item.name }}
+
+
+
+
+
+ {{
+ getIcon(item.fieldInfo.typename)
+ }}
+
+ {{ data.item.name }}
+
+
+
+
{{ expandObject.name || item.recordInfo.name }}
+ >{{ expandObject.name || expandObject.recordInfo.name }}
mdi-open-in-new
diff --git a/frontend/components/page/crudRecordPage.vue b/frontend/components/page/crudRecordPage.vue
index bb066f0..08c3ef1 100644
--- a/frontend/components/page/crudRecordPage.vue
+++ b/frontend/components/page/crudRecordPage.vue
@@ -12,8 +12,10 @@
:hidden-headers="hiddenHeaders"
:title="title"
:icon="icon"
+ :poll-interval="pollInterval"
dense
@pageOptions-updated="handlePageOptionsUpdated"
+ @record-changed="$emit('record-changed', $event)"
>
@@ -55,6 +57,10 @@ export default {
type: Object,
default: () => null,
},
+ pollInterval: {
+ type: Number,
+ default: 0,
+ },
},
computed: {
interfaceComponent() {
diff --git a/frontend/components/page/preset/pbPagePreset.vue b/frontend/components/page/preset/pbPagePreset.vue
index 36e59f6..0c73d99 100644
--- a/frontend/components/page/preset/pbPagePreset.vue
+++ b/frontend/components/page/preset/pbPagePreset.vue
@@ -218,7 +218,6 @@ export default {
this.inputs.event = this.events.find(
(event) => event.id === eventFilterObject.value
)
- console.log(this.inputs.event)
} else {
this.inputs.event = null
}
diff --git a/frontend/mixins/crud.js b/frontend/mixins/crud.js
index 2e3cc1f..89dd556 100644
--- a/frontend/mixins/crud.js
+++ b/frontend/mixins/crud.js
@@ -18,6 +18,7 @@ import {
serializeNestedProperty,
getPaginatorData,
collectPaginatorData,
+ getIcon,
} from '~/services/base'
export default {
@@ -83,6 +84,10 @@ export default {
type: Boolean,
default: false,
},
+ pollInterval: {
+ type: Number,
+ default: 0,
+ },
},
data() {
@@ -126,6 +131,9 @@ export default {
tableOptionsUpdatedTrigger: null,
+ pollIntervalTimer: null,
+ isPolling: false,
+
processedUpdateSort: false,
previousPage: null,
@@ -265,6 +273,15 @@ export default {
},
watch: {
+ isPolling(val, prevVal) {
+ if (val === prevVal) return
+ if (val) {
+ this.startPolling()
+ } else {
+ this.stopPolling()
+ }
+ },
+
'$vuetify.breakpoint.name'(value) {
if (value === 'xs') {
// when switching to mobile view, un-expand all
@@ -283,6 +300,16 @@ export default {
})
},
+ // this should trigger if the locked filters gets updated
+ lockedFilters() {
+ this.reset({
+ resetSubscription: true,
+ initFilters: true,
+ resetSort: true,
+ resetCursor: true,
+ })
+ },
+
// this triggers when pageOptions get updated on parent element
// this also triggers when parent element switches to a different item
pageOptions() {
@@ -303,20 +330,61 @@ export default {
},
created() {
+ if (this.pollInterval > 0) this.isPolling = true
+
this.reset({
resetSubscription: true,
initFilters: true,
resetSort: true,
})
+
+ document.addEventListener(
+ 'visibilitychange',
+ this.handleVisibilityChange,
+ false
+ )
},
destroyed() {
// unsubscribe from channels on this page
if (this.useSubscription) unsubscribeChannels(this.subscriptionChannels)
+
+ this.stopPolling()
+ document.removeEventListener(
+ 'visibilitychange',
+ this.handleVisibilityChange
+ )
},
methods: {
generateTimeAgoString,
+ getIcon,
+
+ startPolling() {
+ // set the interval for refreshing, if pollInterval > 0 and not polling
+ if (this.pollInterval > 0 && !this.pollIntervalTimer) {
+ this.pollIntervalTimer = setInterval(() => {
+ this.reset()
+ }, this.pollInterval)
+ }
+ },
+
+ stopPolling() {
+ clearInterval(this.pollIntervalTimer)
+ this.pollIntervalTimer = null
+ },
+
+ handleVisibilityChange() {
+ if (!this.isPolling) return
+ if (document.hidden) {
+ // clear the pollIntervalTimer
+ this.stopPolling()
+ } else {
+ // start the pollIntervalTimer again
+ this.startPolling()
+ }
+ },
+
setTableOptionsUpdatedTrigger(trigger) {
this.tableOptionsUpdatedTrigger = trigger
},
@@ -369,6 +437,9 @@ export default {
node: {
id: true,
name: true,
+ ...(inputObject.fieldInfo.inputOptions?.hasAvatar && {
+ avatar: true,
+ }),
},
},
__args: {
@@ -764,8 +835,8 @@ export default {
if (matchingInputObject) {
// if inputType is server-X, do not apply the value unless init
if (
- matchingInputObject.fieldInfo.inputType !== 'server-autocomplete' &&
- matchingInputObject.fieldInfo.inputType !== 'server-combobox'
+ matchingInputObject.inputType !== 'server-autocomplete' &&
+ matchingInputObject.inputType !== 'server-combobox'
) {
matchingInputObject.value = ele.value
}
@@ -773,9 +844,8 @@ export default {
if (init) {
// if inputType === 'server-X', only populate the options with the specific entry, if any
if (
- matchingInputObject.fieldInfo.inputType ===
- 'server-autocomplete' ||
- matchingInputObject.fieldInfo.inputType === 'server-combobox'
+ matchingInputObject.inputType === 'server-autocomplete' ||
+ matchingInputObject.inputType === 'server-combobox'
) {
matchingInputObject.value = ele.value
if (matchingInputObject.value) {
@@ -785,6 +855,8 @@ export default {
)}`]: {
id: true,
name: true,
+ ...(matchingInputObject.fieldInfo.inputOptions
+ ?.hasAvatar && { avatar: true }),
__args: {
id: ele.value,
},
@@ -833,6 +905,7 @@ export default {
resetCursor = false,
resetExpanded = true,
reloadData = true,
+ resetPolling = true,
} = {}) {
let actuallyReloadData = reloadData
@@ -842,6 +915,11 @@ export default {
if (this.useSubscription) this.subscribeEvents()
}
+ if (resetPolling && this.isPolling) {
+ this.stopPolling()
+ this.startPolling()
+ }
+
if (initFilters) {
this.filterInputsArray = this.recordInfo.paginationOptions.filters.map(
(ele) => {
@@ -855,6 +933,7 @@ export default {
fieldInfo,
title: ele.title,
operator: ele.operator,
+ inputType: ele.inputType ?? fieldInfo.inputType,
options: [],
value: null,
loading: false,
diff --git a/frontend/mixins/editRecordInterface.js b/frontend/mixins/editRecordInterface.js
index fc88125..d7b9c84 100644
--- a/frontend/mixins/editRecordInterface.js
+++ b/frontend/mixins/editRecordInterface.js
@@ -313,68 +313,101 @@ export default {
this.mode === 'copy' ? this.recordInfo.addOptions.fields : fields
// build inputs Array
- this.inputsArray = inputFields.map((fieldKey) => {
- const fieldInfo = this.recordInfo.fields[fieldKey]
+ this.inputsArray = await Promise.all(
+ inputFields.map(async (fieldKey) => {
+ const fieldInfo = this.recordInfo.fields[fieldKey]
- // field unknown, abort
- if (!fieldInfo) throw new Error('Unknown field: ' + fieldKey)
+ // field unknown, abort
+ if (!fieldInfo) throw new Error('Unknown field: ' + fieldKey)
- let fieldValue
+ let fieldValue
- // if copy mode and fieldKey not in original fields, use default
- if (this.mode === 'copy' && !fields.includes(fieldKey)) {
- fieldValue = fieldInfo.default ? fieldInfo.default(this) : null
- } else {
- fieldValue = fieldInfo.hidden
- ? null
- : getNestedProperty(data, fieldKey)
- }
-
- const inputObject = {
- field: fieldKey.split(/\+/)[0],
- fieldInfo,
- value: fieldValue, // already serialized
- options: [],
- readonly:
- this.mode === 'view'
- ? true
- : this.mode === 'copy'
- ? fields.includes(fieldKey)
- : false,
- }
+ // if copy mode and fieldKey not in original fields, use default
+ if (this.mode === 'copy' && !fields.includes(fieldKey)) {
+ fieldValue = fieldInfo.default ? fieldInfo.default(this) : null
+ } else {
+ fieldValue = fieldInfo.hidden
+ ? null
+ : getNestedProperty(data, fieldKey)
+ }
- // if inputType === 'server-autocomplete', only populate the options with the specific entry, if any, and if inputObject.value not null
- if (
- fieldInfo.inputType === 'server-autocomplete' ||
- fieldInfo.inputType === 'server-combobox'
- ) {
- inputObject.value = null // set this to null initially while the results load, to prevent console error
- if (fieldValue) {
- executeGiraffeql(this, {
- [`get${capitalizeString(fieldInfo.typename)}`]: {
- id: true,
- name: true,
+ // if field is 'multiple-file' inputType, retrieve the file data from api
+ if (
+ fieldInfo.inputType === 'multiple-file' &&
+ fieldValue.length > 0
+ ) {
+ const fileData = await executeGiraffeql(this, {
+ getFilePaginator: {
+ edges: {
+ node: {
+ id: true,
+ name: true,
+ size: true,
+ location: true,
+ },
+ },
__args: {
- id: fieldValue,
+ first: 100,
+ filterBy: [
+ {
+ id: {
+ in: fieldValue,
+ },
+ },
+ ],
},
},
})
- .then((res) => {
- // change value to object
- inputObject.value = res
+ fieldValue = fileData.edges.map((ele) => ele.node)
+ }
- inputObject.options = [res]
+ const inputObject = {
+ field: fieldKey.split(/\+/)[0],
+ fieldInfo,
+ value: fieldValue, // already serialized
+ options: [],
+ readonly:
+ this.mode === 'view'
+ ? true
+ : this.mode === 'copy'
+ ? fields.includes(fieldKey)
+ : false,
+ }
+
+ // if inputType === 'server-autocomplete', only populate the options with the specific entry, if any, and if inputObject.value not null
+ if (
+ fieldInfo.inputType === 'server-autocomplete' ||
+ fieldInfo.inputType === 'server-combobox'
+ ) {
+ inputObject.value = null // set this to null initially while the results load, to prevent console error
+ if (fieldValue) {
+ executeGiraffeql(this, {
+ [`get${capitalizeString(fieldInfo.typename)}`]: {
+ id: true,
+ name: true,
+ ...(fieldInfo.inputOptions?.hasAvatar && { avatar: true }),
+ __args: {
+ id: fieldValue,
+ },
+ },
})
- .catch((e) => e)
+ .then((res) => {
+ // change value to object
+ inputObject.value = res
+
+ inputObject.options = [res]
+ })
+ .catch((e) => e)
+ }
+ } else {
+ fieldInfo.getOptions &&
+ fieldInfo
+ .getOptions(this)
+ .then((res) => (inputObject.options = res))
}
- } else {
- fieldInfo.getOptions &&
- fieldInfo
- .getOptions(this)
- .then((res) => (inputObject.options = res))
- }
- return inputObject
- })
+ return inputObject
+ })
+ )
} catch (err) {
handleError(this, err)
}
@@ -443,6 +476,7 @@ export default {
[`get${capitalizeString(fieldInfo.typename)}`]: {
id: true,
name: true,
+ ...(fieldInfo.inputOptions?.hasAvatar && { avatar: true }),
__args: {
id: inputObject.value,
},
diff --git a/frontend/models/user.ts b/frontend/models/user.ts
index 3e52f2e..16aa893 100644
--- a/frontend/models/user.ts
+++ b/frontend/models/user.ts
@@ -34,6 +34,9 @@ export const User = >{
avatar: {
text: 'Avatar URL',
inputType: 'avatar',
+ inputOptions: {
+ fallbackIcon: 'mdi-account',
+ },
},
country: {
text: 'Country',
diff --git a/frontend/services/base.ts b/frontend/services/base.ts
index c698e4a..84e22a2 100644
--- a/frontend/services/base.ts
+++ b/frontend/services/base.ts
@@ -1,9 +1,14 @@
import { format } from 'timeago.js'
import { convertArrayToCSV } from 'convert-array-to-csv'
import { executeGiraffeql } from '~/services/giraffeql'
+import * as models from '~/models'
type StringKeyObject = { [x: string]: any }
+export function getIcon(typename: string) {
+ return models[capitalizeString(typename)]?.icon
+}
+
export function generateTimeAgoString(unixTimestamp: number | null) {
if (!unixTimestamp) return 'None'
diff --git a/frontend/types/index.ts b/frontend/types/index.ts
index 6c3c9d4..99df8c6 100644
--- a/frontend/types/index.ts
+++ b/frontend/types/index.ts
@@ -30,21 +30,16 @@ export type RecordInfo = {
// which field should the primary field, for sort purposes
primaryField: string
}
- inputType?:
- | 'html'
- | 'single-image'
- | 'multiple-image'
- | 'key-value-array'
- | 'avatar'
- | 'datepicker'
- | 'switch'
- | 'textarea'
- | 'combobox' // combobox allows the user to add new inputs on the fly (will change to autocomplete in filter interfaces)
- | 'server-combobox' // server-combobox allows the user to add new inputs on the fly with getOptions optional, and fetching results from server (will change to autocomplete in filter interfaces)
- | 'autocomplete' // same as combobox but cannot add new inputs
- | 'server-autocomplete' // if there's lots of entries, may not want to fetch all of the entries at once. getOptions will be optional
- | 'select' // standard select
- | 'text'
+ inputType?: InputType
+
+ // special options pertaining to the specific inputType
+ inputOptions?: {
+ // for server-autocomplete and server-combobox
+ hasAvatar?: boolean
+ // for avatar
+ fallbackIcon?: string
+ }
+
inputRules?: any[]
getOptions?: (that) => Promise
typename?: string
@@ -199,4 +194,23 @@ type FilterObject = {
type RecordFilter = {
field: keyof T
operator: keyof FilterByField
+ inputType?: InputType
}
+
+type InputType =
+ | 'html'
+ | 'single-image'
+ | 'multiple-image'
+ | 'multiple-file'
+ | 'key-value-array'
+ | 'avatar'
+ | 'datepicker'
+ | 'switch'
+ | 'textarea'
+ | 'combobox' // combobox allows the user to add new inputs on the fly (will change to autocomplete in filter interfaces)
+ | 'server-combobox' // server-combobox allows the user to add new inputs on the fly with getOptions optional, and fetching results from server (will change to autocomplete in filter interfaces)
+ | 'autocomplete' // same as combobox but cannot add new inputs
+ | 'server-autocomplete' // if there's lots of entries, may not want to fetch all of the entries at once. getOptions will be optional
+ | 'select' // standard select
+ | 'multiple-select' // multiple select
+ | 'text'