3
0

Add ability to manage custom chart color schemes

This commit is contained in:
Jože Fortun 2023-10-04 16:12:22 +02:00
parent 6f455148ec
commit 2508b60dc0
20 changed files with 342 additions and 39 deletions

View File

@ -53,6 +53,7 @@
data-test-id="input-text-color"
:translations="{
modalTitle: $t('colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
/>
@ -67,6 +68,7 @@
data-test-id="input-background-color"
:translations="{
modalTitle: $t('colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
/>

View File

@ -168,6 +168,7 @@ export default {
}
data.labels = data.labels.map(l => l === 'undefined' ? this.$t('chart:undefined') : l)
data.customColorSchemes = this.$Settings.get('ui.charts.colorSchemes', [])
this.renderer = chart.makeOptions(data)
} else {

View File

@ -166,6 +166,7 @@
v-model="f.options.backgroundColor"
:translations="{
modalTitle: $t('kind.file.view.colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
/>

View File

@ -34,6 +34,7 @@
v-model="feed.options.color"
:translations="{
modalTitle: $t('calendar.recordFeed.colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
/>

View File

@ -10,6 +10,7 @@
v-model="feed.options.color"
:translations="{
modalTitle: $t('calendar.recordFeed.colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
/>

View File

@ -172,6 +172,7 @@
v-model="options.backgroundColor"
:translations="{
modalTitle: $t('kind.file.view.colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
/>

View File

@ -89,6 +89,7 @@
v-model="feed.options.color"
:translations="{
modalTitle: $t('geometry.recordFeed.colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
/>

View File

@ -10,6 +10,7 @@
v-model="options.color"
:translations="{
modalTitle: $t('metric.editStyle.colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
class="mb-1"
@ -23,6 +24,7 @@
v-model="options.backgroundColor"
:translations="{
modalTitle: $t('geometry.recordFeed.colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
class="mb-1"

View File

@ -134,6 +134,7 @@
v-model="item.options.textColor"
:translations="{
modalTitle: $t('navigation.colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
class="w-100"
@ -144,6 +145,7 @@
v-model="item.options.backgroundColor"
:translations="{
modalTitle: $t('navigation.colorPicker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
class="w-100"

View File

@ -93,31 +93,58 @@
:label="$t('colorScheme.label')"
label-class="text-primary"
>
<vue-select
v-model="chart.config.colorScheme"
:options="colorSchemes"
:reduce="cs => cs.value"
label="label"
option-text="label"
option-value="value"
:placeholder="$t('colorScheme.placeholder')"
:calculate-position="calculateDropdownPosition"
class="bg-white w-100"
>
<template #option="option">
<p
class="mb-1"
<b-input-group class="d-flex w-100">
<vue-select
v-model="chart.config.colorScheme"
:options="colorSchemes"
:reduce="cs => cs.id"
label="name"
:get-option-key="o => o.id"
:placeholder="$t('colorScheme.placeholder')"
:calculate-position="calculateDropdownPosition"
class="bg-white"
>
<template #option="option">
<p
class="mb-1"
>
{{ option.name }}
</p>
<div
v-for="(color, index) in option.colors"
:key="index"
:style="`background: ${color};`"
class="d-inline-block color-box mr-1 mb-1"
/>
</template>
<template
v-if="canManageColorSchemes"
#list-header
>
{{ option.label }}
</p>
<div
v-for="(color, index) in option.colors"
:key="`${option.value}-${index}`"
:style="`background: ${color};`"
class="d-inline-block color-box mr-1 mb-1"
/>
</template>
</vue-select>
<li class="border-bottom text-center mb-1">
<b-button
variant="link"
class="text-decoration-none"
@click="createColorScheme"
>
{{ $t('colorScheme.custom.add') }}
</b-button>
</li>
</template>
</vue-select>
<b-input-group-append v-if="showEditColorSchemeButton">
<b-button
:title="$t('colorScheme.custom.edit')"
variant="light"
class="d-flex align-items-center"
@click="editColorScheme()"
>
<font-awesome-icon :icon="['far', 'edit']" />
</b-button>
</b-input-group-append>
</b-input-group>
<template
v-if="currentColorScheme"
@ -295,6 +322,90 @@
</b-row>
</b-container>
<b-modal
v-model="colorSchemeModal.show"
:title="colorSchemeModalTitle"
:ok-title="$t('general:label.saveAndClose')"
centered
size="md"
cancel-variant="link"
no-fade
>
<b-form-group
:label="$t('colorScheme.custom.modal.name.label')"
label-class="text-primary"
>
<b-form-input
v-model="colorSchemeModal.colorscheme.name"
/>
</b-form-group>
<b-form-group
label-class="text-primary"
class="mb-0"
>
<template #label>
{{ $t('colorScheme.custom.modal.colors.label') }}
<b-button
variant="outline-light"
size="sm"
class="text-primary border-0"
@click="addColor"
>
<font-awesome-icon :icon="['fa', 'plus']" />
</b-button>
</template>
<c-input-color-picker
v-for="(color, index) in colorSchemeModal.colorscheme.colors"
:key="index"
v-model="colorSchemeModal.colorscheme.colors[index]"
data-test-id="input-scheme-color"
:translations="{
modalTitle: $t('colorScheme.pickAColor'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
class="d-inline-flex mr-1"
>
<template #footer>
<c-input-confirm
variant="danger"
size="md"
@confirmed="removeColor(index)"
/>
</template>
</c-input-color-picker>
</b-form-group>
<template #modal-footer>
<c-input-confirm
v-if="colorSchemeModal.edit"
variant="danger"
size="md"
:disabled="colorSchemeModal.processing"
@confirmed="deleteColorScheme()"
/>
<b-button
variant="link"
class="ml-auto"
:disabled="colorSchemeModal.processing"
@click="closeColorSchemeModal"
>
{{ $t('general:label.cancel') }}
</b-button>
<b-button
variant="primary"
:disabled="!colorSchemeModal.colorscheme.name || !colorSchemeModal.colorscheme.colors.length || colorSchemeModal.processing"
@click="saveColorScheme"
>
{{ $t('general:label.saveAndClose') }}
</b-button>
</template>
</b-modal>
<portal to="admin-toolbar">
<editor-toolbar
:processing="processing"
@ -325,7 +436,7 @@ import { chartConstructor } from 'corteza-webapp-compose/src/lib/charts'
import VueSelect from 'vue-select'
import { evaluatePrefilter } from 'corteza-webapp-compose/src/lib/record-filter'
const { CInputCheckbox } = components
const { CInputConfirm, CInputCheckbox, CInputColorPicker } = components
const { colorschemes } = shared
const defaultReport = {
@ -347,6 +458,8 @@ export default {
ReportItem,
VueSelect,
CInputCheckbox,
CInputColorPicker,
CInputConfirm,
},
props: {
@ -375,6 +488,15 @@ export default {
processing: false,
editReportIndex: undefined,
customColorSchemes: [],
colorSchemeModal: {
show: false,
processing: false,
edit: false,
colorscheme: {},
},
checkboxLabel: {
on: this.$t('general:label.yes'),
off: this.$t('general:label.no'),
@ -387,6 +509,7 @@ export default {
modules: 'module/set',
modByID: 'module/getByID',
previousPage: 'ui/previousPage',
can: 'rbac/can',
}),
colorSchemes () {
@ -399,15 +522,15 @@ export default {
}
}
const rr = []
const rr = [...this.customColorSchemes]
for (const g in colorschemes) {
for (const sc in colorschemes[g]) {
const gn = splicer(sc)
rr.push({
label: `${capitalize(g)}: ${capitalize(gn.label)} (${this.$t('colorLabel', gn)})`,
id: `${g}.${sc}`,
name: `${capitalize(g)}: ${capitalize(gn.label)} (${this.$t('colorLabel', gn)})`,
colors: [...colorschemes[g][sc]],
value: `${g}.${sc}`,
})
}
}
@ -416,7 +539,15 @@ export default {
},
currentColorScheme () {
return this.colorSchemes.find(({ value }) => value === this.chart.config.colorScheme)
return this.colorSchemes.find(({ id }) => id === this.chart.config.colorScheme)
},
canManageColorSchemes () {
return this.can('system/', 'settings.manage')
},
colorSchemeModalTitle () {
return this.$t(`colorScheme.custom.${this.colorSchemeModal.edit ? 'edit' : 'add'}`)
},
defaultReport () {
@ -515,6 +646,11 @@ export default {
{ value: 'xy', text: this.$t('edit.toolbox.timeline.options.xy') },
]
},
showEditColorSchemeButton () {
const { config = {} } = this.chart || {}
return config.colorScheme && config.colorScheme.includes('custom') && this.canManageColorSchemes
},
},
watch: {
@ -526,6 +662,10 @@ export default {
const { namespaceID } = this.namespace
if (this.canManageColorSchemes) {
this.fetchCustomColorSchemes()
}
if (chartID === NoID) {
let c = new compose.Chart({ namespaceID: this.namespace.namespaceID })
switch (this.category) {
@ -670,6 +810,101 @@ export default {
this.reports.push(this.chart.defReport())
},
async fetchCustomColorSchemes () {
return this.$SystemAPI.settingsList({ prefix: 'ui.charts.colorSchemes' })
.then(settings => {
const { value = [] } = settings[0] || {}
this.customColorSchemes = value
})
.catch(this.toastErrorHandler(this.$t('notification:chart.colorScheme.fetch.failed')))
},
async saveColorScheme () {
this.colorSchemeModal.processing = true
const action = this.colorSchemeModal.edit ? 'update' : 'create'
if (this.colorSchemeModal.edit) {
const index = this.customColorSchemes.findIndex(({ id }) => id === this.colorSchemeModal.colorscheme.id)
this.customColorSchemes.splice(index, 1, this.colorSchemeModal.colorscheme)
} else {
this.customColorSchemes.push(this.colorSchemeModal.colorscheme)
}
const values = [
{ name: 'ui.charts.colorSchemes', value: this.customColorSchemes },
]
return this.$SystemAPI.settingsUpdate({ values })
.then(() => {
this.chart.config.colorScheme = this.colorSchemeModal.colorscheme.id
this.closeColorSchemeModal()
this.toastSuccess(this.$t(`notification:chart.colorScheme.${action}.success`))
return this.$Settings.fetch().then(() => {
return this.update()
})
})
.catch(this.toastErrorHandler(this.$t(`notification:chart.colorScheme.${action}.failed`)))
.finally(() => {
this.colorSchemeModal.processing = false
})
},
async deleteColorScheme () {
this.colorSchemeModal.processing = true
const value = this.customColorSchemes.filter(({ id }) => id !== this.colorSchemeModal.colorscheme.id)
const values = [
{ name: 'ui.charts.colorSchemes', value },
]
return this.$SystemAPI.settingsUpdate({ values })
.then(() => {
this.chart.config.colorScheme = undefined
this.customColorSchemes = value
this.closeColorSchemeModal()
this.toastSuccess(this.$t('notification:chart.colorScheme.delete.success'))
return this.$Settings.fetch()
})
.catch(this.toastErrorHandler(this.$t('notification:chart.colorScheme.delete.success')))
.finally(() => {
this.colorSchemeModal.processing = false
})
},
createColorScheme () {
this.colorSchemeModal.edit = false
this.colorSchemeModal.colorscheme = {
id: `custom-${Date.now()}`,
name: '',
colors: ['#6C757D', '#000000'],
}
this.colorSchemeModal.show = true
},
editColorScheme () {
this.colorSchemeModal.edit = true
this.colorSchemeModal.colorscheme = {
id: this.currentColorScheme.id,
name: this.currentColorScheme.name,
colors: [...this.currentColorScheme.colors],
}
this.colorSchemeModal.show = true
},
closeColorSchemeModal () {
this.colorSchemeModal.show = false
},
addColor () {
this.colorSchemeModal.colorscheme.colors.push('#000000')
},
removeColor (index) {
this.colorSchemeModal.colorscheme.colors.splice(index, 1)
},
getOptionKey ({ value }) {
return value
},

View File

@ -57,6 +57,7 @@
v-model="options.color"
:translations="{
modalTitle: $t('color.picker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
/>
@ -70,6 +71,7 @@
v-model="options.backgroundColor"
:translations="{
modalTitle: $t('color.picker'),
cancelBtnLabel: $t('general:label.cancel'),
saveBtnLabel: $t('general:label.saveAndClose')
}"
/>

View File

@ -275,7 +275,7 @@ export default class Chart extends BaseChart {
return {
color: getColorschemeColors(colorScheme),
color: getColorschemeColors(colorScheme, data.customColorSchemes),
textStyle: {
fontFamily: 'Poppins-Regular',
overflow: 'break',

View File

@ -90,7 +90,7 @@ export default class FunnelChart extends BaseChart {
const { labels, datasets = [], tooltip } = data
const { legend: l } = reports[0] || {}
const colors = getColorschemeColors(colorScheme)
const colors = getColorschemeColors(colorScheme, data.customColorSchemes)
const tooltipFormatter = `{b}<br />{c} ${tooltip.relative ? ' ({d}%)' : ''}`
const labelFormatter = `{c}${tooltip.relative ? ' ({d}%)' : ''}`

View File

@ -79,7 +79,7 @@ export default class GaugeChart extends BaseChart {
const { datasets = [] } = data
const { steps = [], name, value, max, tooltip, startAngle, endAngle } = datasets.find(({ value }: any) => value) || datasets[0]
const colors = getColorschemeColors(colorScheme)
const colors = getColorschemeColors(colorScheme, data.customColorSchemes)
const color = steps.map((s: any, i: number) => {
return [s.value / max, colors[i]]

View File

@ -46,7 +46,7 @@ export default class RadarChart extends BaseChart {
})
return {
color: getColorschemeColors(colorScheme),
color: getColorschemeColors(colorScheme, data.customColorSchemes),
animation: !noAnimation,
textStyle: {
fontFamily: 'Poppins-Regular',

View File

@ -1,10 +1,14 @@
import { get } from 'lodash'
import colorschemes from './colorschemes'
export const getColorschemeColors = (colorscheme?: string): string[] => {
export const getColorschemeColors = (colorscheme?: string, customColorSchemes?: any[]): string[] => {
if (!colorscheme) {
return []
}
return [...get(colorschemes, colorscheme)]
if (colorscheme.includes('custom') && customColorSchemes) {
return customColorSchemes.find(({ id }) => id === colorscheme)?.colors || []
}
return get(colorschemes, colorscheme)
}

View File

@ -51,13 +51,10 @@
<b-modal
:visible="showModal"
:title="translations.modalTitle"
:ok-title="translations.saveBtnLabel"
centered
size="sm"
size="md"
body-class="p-0"
cancel-variant="link"
no-fade
@ok="$emit('input', currentColor || value)"
@hide="closeMenu"
>
<chrome
@ -65,6 +62,25 @@
class="w-100 shadow-none"
@input="updateColor"
/>
<template #modal-footer>
<slot name="footer" />
<b-button
variant="link"
class="ml-auto"
@click="closeMenu"
>
{{ translations.cancelBtnLabel }}
</b-button>
<b-button
variant="primary"
@click="saveColor"
>
{{ translations.saveBtnLabel }}
</b-button>
</template>
</b-modal>
</div>
</template>
@ -123,6 +139,11 @@ export default {
}
},
saveColor () {
this.$emit('input', this.currentColor || this.value)
this.closeMenu()
},
openMenu () {
this.showModal = true
},

View File

@ -2,6 +2,15 @@ colorLabel: '{{count}} colors'
colorScheme:
label: Color scheme
placeholder: Pick a color scheme
pickAColor: Pick a color
custom:
add: Add color scheme
edit: Edit color scheme
modal:
name:
label: Name
colors:
label: Colors
configure:
reportsLabel: Reports
general:

View File

@ -18,6 +18,18 @@ chart:
saveFailed: Could not save this chart
saved: Chart saved
unsupportedRenderer: Unsupported renderer
colorScheme:
fetch:
failed: Failed to fetch custom color schemes
create:
success: Color scheme created
failed: Failed to create color scheme
update:
success: Color scheme updated
failed: Failed to update color scheme
delete:
success: Color scheme deleted
failed: Failed to delete color scheme
color:
RGBA:
invalid: Invalid RGBA color format

View File

@ -272,6 +272,14 @@ type (
NewTab bool `json:"newTab"`
} `json:"profileLinks"`
} `kv:"topbar,final" json:"topbar"`
Charts struct {
ColorSchemes []struct {
ID string `json:"id"`
Name string `json:"name"`
Colors []string `json:"colors"`
} `kv:"colorSchemes" json:"colorSchemes"`
} `kv:"charts" json:"charts"`
} `kv:"ui" json:"ui"`
ResourceTranslations struct {