mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-25 03:15:24 +00:00
feat: metric config
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -71,7 +71,6 @@ local.properties
|
||||
# Cloud9 IDE
|
||||
# =========
|
||||
.c9/
|
||||
data/
|
||||
mongod
|
||||
|
||||
# Visual Studio
|
||||
|
||||
@@ -8,50 +8,57 @@ const { activeDashboard } = storeToRefs(dashboardStore)
|
||||
|
||||
const getDefaultConfig = (widgetType: WidgetTypes, type?: ChartTypes) => {
|
||||
switch (widgetType) {
|
||||
case 'metric':
|
||||
case WidgetTypes.METRIC:
|
||||
return {
|
||||
component: {
|
||||
title: 'Number Widget',
|
||||
description: '',
|
||||
dataSource: {
|
||||
type: 'model',
|
||||
fk_model_id: '',
|
||||
},
|
||||
appearance: {
|
||||
fontSize: 'large',
|
||||
textColor: '',
|
||||
backgroundColor: '',
|
||||
},
|
||||
source: {
|
||||
tableId: '',
|
||||
viewId: '',
|
||||
type: 'all_records',
|
||||
metric: {
|
||||
aggregation: 'count',
|
||||
columnId: '',
|
||||
filters: [],
|
||||
},
|
||||
}
|
||||
default:
|
||||
case WidgetTypes.CHART:
|
||||
return {
|
||||
chartType: type,
|
||||
component: {
|
||||
title: 'Chart Widget',
|
||||
description: '',
|
||||
dataSource: {
|
||||
type: 'model',
|
||||
fk_model_id: '',
|
||||
},
|
||||
source: {
|
||||
tableId: '',
|
||||
viewId: '',
|
||||
type: 'all_records',
|
||||
aggregation: 'count',
|
||||
columnId: '',
|
||||
filters: [],
|
||||
xAxis: {
|
||||
column_id: '',
|
||||
label: 'X Axis',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
column_id: '',
|
||||
aggregation: 'count',
|
||||
label: 'Y Axis',
|
||||
},
|
||||
],
|
||||
}
|
||||
case WidgetTypes.TEXT:
|
||||
return {
|
||||
content: 'Enter your text here...',
|
||||
format: 'plain',
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const createWidget = async (widgetType: WidgetTypes, type?: ChartTypes) => {
|
||||
if (!activeDashboard.value?.id) return
|
||||
|
||||
const getWidgetTitle = (widgetType: WidgetTypes, chartType?: ChartTypes) => {
|
||||
if (widgetType === WidgetTypes.CHART && chartType) {
|
||||
return `${chartType.charAt(0).toUpperCase() + chartType.slice(1)} Chart`
|
||||
}
|
||||
return `${widgetType.charAt(0).toUpperCase() + widgetType.slice(1)}`
|
||||
}
|
||||
|
||||
const newWidget: Partial<WidgetType> = {
|
||||
title: `${type} Widget`,
|
||||
title: getWidgetTitle(widgetType, type),
|
||||
type: widgetType,
|
||||
position: {
|
||||
x: 0,
|
||||
|
||||
@@ -1,23 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import MetricsWidgetConfig from './Widgets/Metrics/Config.vue'
|
||||
const widgetStore = useWidgetStore()
|
||||
const dashboardStore = useDashboardStore()
|
||||
const { selectedWidget } = storeToRefs(widgetStore)
|
||||
const { activeDashboard } = storeToRefs(dashboardStore)
|
||||
|
||||
// Handle config updates
|
||||
const handleConfigUpdate = async (config: any) => {
|
||||
if (selectedWidget.value && activeDashboard.value?.id) {
|
||||
await widgetStore.updateWidget(activeDashboard.value.id, selectedWidget.value.id!, { config })
|
||||
}
|
||||
}
|
||||
|
||||
// Close editor
|
||||
const closeEditor = () => {
|
||||
selectedWidget.value = null
|
||||
}
|
||||
|
||||
// Get config component based on widget type
|
||||
const getConfigComponent = () => {
|
||||
if (!selectedWidget.value) return null
|
||||
|
||||
@@ -25,7 +10,6 @@ const getConfigComponent = () => {
|
||||
case 'metric':
|
||||
return MetricsWidgetConfig
|
||||
case 'chart':
|
||||
// Will be implemented later for chart widgets
|
||||
return null
|
||||
default:
|
||||
return null
|
||||
@@ -38,7 +22,7 @@ const getConfigComponent = () => {
|
||||
v-if="selectedWidget"
|
||||
class="widget-editor-panel w-80 bg-white border-l border-nc-content-gray-300 h-full overflow-hidden flex flex-col"
|
||||
>
|
||||
<component :is="getConfigComponent()" :widget="selectedWidget" @update:config="handleConfigUpdate" />
|
||||
<component :is="getConfigComponent()" :widget="selectedWidget" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -11,8 +11,15 @@ const chartLabel = computed(() => {
|
||||
return WidgetChartLabelMap[selectedWidget.value?.type as WidgetTypes]
|
||||
})
|
||||
|
||||
const { updateWidget } = useWidgetStore()
|
||||
const { activeDashboard } = storeToRefs(useDashboardStore())
|
||||
|
||||
const handleConfigUpdate = async (config: any) => {
|
||||
console.log('handleConfigUpdate', config)
|
||||
if (selectedWidget.value && activeDashboard.value?.id) {
|
||||
await updateWidget(activeDashboard.value.id, selectedWidget.value.id, {
|
||||
config: { ...selectedWidget.value.config, ...config },
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import TabbedSelect from '../TabbedSelect.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:aggregation': [aggregation: any]
|
||||
}>()
|
||||
|
||||
const { selectedWidget } = storeToRefs(useWidgetStore())
|
||||
|
||||
const selectedValue = ref(selectedWidget.value?.config?.metric?.aggregation === 'count' ? 'count' : 'summary')
|
||||
const selectedAggregationType = ref(selectedWidget.value?.config?.metric?.aggregation || 'count')
|
||||
const selectedFieldId = ref(selectedWidget.value?.config?.metric?.column_id || '')
|
||||
|
||||
const modelId = computed(() => selectedWidget.value?.config?.dataSource?.fk_model_id || '')
|
||||
|
||||
const aggregationMap = {
|
||||
count: 'Record Count',
|
||||
summary: 'Field Summary',
|
||||
} as const
|
||||
|
||||
const aggregationOptions = [
|
||||
{ value: 'distinct', label: 'Distinct' },
|
||||
{ value: 'sum', label: 'Sum' },
|
||||
{ value: 'avg', label: 'Average' },
|
||||
{ value: 'median', label: 'Median' },
|
||||
{ value: 'min', label: 'Minimum' },
|
||||
{ value: 'max', label: 'Maximum' },
|
||||
]
|
||||
|
||||
const handleChange = (type: 'field' | 'aggregation') => {
|
||||
const aggregation = {
|
||||
type: selectedValue.value,
|
||||
}
|
||||
if (type === 'field') {
|
||||
aggregation.aggregation = null
|
||||
}
|
||||
|
||||
if (type === 'aggregation') {
|
||||
aggregation.column_id = selectedFieldId.value
|
||||
aggregation.aggregation = selectedAggregationType.value
|
||||
}
|
||||
|
||||
emit('update:aggregation', aggregation)
|
||||
}
|
||||
|
||||
watch(selectedValue, () => {
|
||||
const aggregation = {
|
||||
aggregation: selectedValue.value,
|
||||
}
|
||||
if (selectedValue.value === 'count') {
|
||||
aggregation.column_id = null
|
||||
aggregation.aggregation = null
|
||||
} else if (selectedValue.value === 'summary') {
|
||||
aggregation.column_id = selectedFieldId.value
|
||||
aggregation.aggregation = selectedAggregationType.value
|
||||
}
|
||||
|
||||
emit('update:aggregation', aggregation)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabbedSelect v-model:model-value="selectedValue" :values="['count', 'summary']">
|
||||
<template #default="{ value }">
|
||||
{{ aggregationMap[value] }}
|
||||
</template>
|
||||
</TabbedSelect>
|
||||
|
||||
<div v-if="selectedValue === 'summary'" class="flex gap-2 flex-1 min-w-0">
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<label>Field</label>
|
||||
<NSelectField v-model:value="selectedFieldId" v-model:table-id="modelId" @update:value="handleChange('field')" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<label>Type</label>
|
||||
<a-select
|
||||
v-model:value="selectedAggregationType"
|
||||
:options="aggregationOptions"
|
||||
class="nc-select-shadow"
|
||||
@update:value="handleChange('aggregation')"
|
||||
>
|
||||
<template #suffixIcon>
|
||||
<GeneralIcon icon="arrowDown" class="text-gray-700" />
|
||||
</template>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import GroupedSettings from '../GroupedSettings.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:source': [source: any]
|
||||
}>()
|
||||
|
||||
const { selectedWidget } = storeToRefs(useWidgetStore())
|
||||
|
||||
const isConditionDropdownOpen = ref(false)
|
||||
|
||||
const selectedDataSourceType = ref(selectedWidget.value?.config?.dataSource?.type || 'model')
|
||||
const selectedModelId = ref(selectedWidget.value?.config?.dataSource?.fk_model_id || '')
|
||||
const selectedViewId = ref(selectedWidget.value?.config?.dataSource?.fk_view_id || '')
|
||||
|
||||
const filters = ref([])
|
||||
|
||||
const updateDataSource = () => {
|
||||
const dataSource = { type: selectedDataSourceType.value }
|
||||
if (selectedDataSourceType.value === 'model') {
|
||||
if (selectedModelId.value) {
|
||||
dataSource.fk_model_id = selectedModelId.value
|
||||
}
|
||||
} else if (selectedDataSourceType.value === 'view') {
|
||||
if (selectedModelId.value) {
|
||||
dataSource.fk_model_id = selectedModelId.value
|
||||
}
|
||||
if (selectedViewId.value) {
|
||||
dataSource.fk_view_id = selectedViewId.value
|
||||
}
|
||||
} else if (selectedDataSourceType.value === 'filter') {
|
||||
if (selectedModelId.value) {
|
||||
dataSource.fk_model_id = selectedModelId.value
|
||||
}
|
||||
}
|
||||
|
||||
console.log(dataSource)
|
||||
emit('update:source', dataSource)
|
||||
}
|
||||
|
||||
const onDataSourceTypeChange = (newValue) => {
|
||||
if (newValue !== 'view') {
|
||||
selectedViewId.value = ''
|
||||
}
|
||||
updateDataSource()
|
||||
}
|
||||
|
||||
const onDataChange = (type: 'model' | 'view') => {
|
||||
if (type === 'model') {
|
||||
selectedViewId.value = ''
|
||||
}
|
||||
updateDataSource()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GroupedSettings title="Source">
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<label>Table</label>
|
||||
<NSelectTable v-model:value="selectedModelId" @update:value="onDataChange('model')" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<label>Records</label>
|
||||
<a-radio-group
|
||||
v-model:value="selectedDataSourceType"
|
||||
class="record-filter-type w-full"
|
||||
@update:value="onDataSourceTypeChange"
|
||||
>
|
||||
<a-radio value="model">All Records</a-radio>
|
||||
<a-radio value="view">Records from a view</a-radio>
|
||||
<a-radio value="filter">Specific records</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedDataSourceType === 'view'" class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<label>View</label>
|
||||
<NSelectView v-model:value="selectedViewId" :table-id="selectedModelId" @update:value="onDataChange('view')" />
|
||||
</div>
|
||||
|
||||
<div v-if="selectedDataSourceType === 'filter'" class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<label>Conditions</label>
|
||||
<NcDropdown
|
||||
v-model:visible="isConditionDropdownOpen"
|
||||
placement="bottomLeft"
|
||||
overlay-class-name="nc-datasource-conditions-dropdown"
|
||||
>
|
||||
<div
|
||||
class="h-9 border-1 rounded-lg py-1 px-3 flex items-center justify-between gap-2 !min-w-[170px] transition-all cursor-pointer select-none text-sm"
|
||||
:class="{
|
||||
'!border-brand-500 shadow-selected': isConditionDropdownOpen,
|
||||
'border-gray-200': !isConditionDropdownOpen,
|
||||
'bg-[#F0F3FF]': filters.length,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="nc-datasource-conditions-count flex-1"
|
||||
:class="{
|
||||
'text-brand-500 ': filters.length,
|
||||
}"
|
||||
>
|
||||
{{ filters.length ? `${filters.length} condition${filters.length !== 1 ? 's' : ''}` : 'No conditions' }}
|
||||
</div>
|
||||
|
||||
<GeneralIcon
|
||||
icon="settings"
|
||||
class="flex-none w-4 h-4"
|
||||
:class="{
|
||||
'text-brand-500 ': filters.length,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #overlay>
|
||||
<div
|
||||
class="nc-datasource-conditions-dropdown-container"
|
||||
:class="{
|
||||
'py-2': !filters.length,
|
||||
}"
|
||||
>
|
||||
<SmartsheetToolbarColumnFilter
|
||||
ref="fieldVisibilityRef"
|
||||
:value="filters"
|
||||
class="w-full"
|
||||
:auto-save="true"
|
||||
data-testid="nc-filter-menu"
|
||||
:show-loading="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</NcDropdown>
|
||||
</div>
|
||||
</GroupedSettings>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.record-filter-type {
|
||||
:deep(.ant-radio-input:focus + .ant-radio-inner) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
:deep(.ant-radio-wrapper) {
|
||||
> span {
|
||||
@apply text-nc-content-gray leading-5;
|
||||
}
|
||||
@apply flex py-2 m-0;
|
||||
.ant-radio-checked .ant-radio-inner {
|
||||
@apply !bg-nc-fill-primary !border-nc-fill-primary;
|
||||
&::after {
|
||||
@apply bg-nc-bg-default;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-top: -6px;
|
||||
margin-left: -6px;
|
||||
}
|
||||
}
|
||||
&:first-child {
|
||||
@apply rounded-tl-lg rounded-tr-lg;
|
||||
}
|
||||
&:last-child {
|
||||
@apply border-t-0 rounded-bl-lg rounded-br-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import GroupedSettings from '../GroupedSettings.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:widget': [updates: any]
|
||||
}>()
|
||||
|
||||
const { selectedWidget } = useWidgetStore()
|
||||
|
||||
const widgetData = reactive({
|
||||
title: selectedWidget?.title || '',
|
||||
description: selectedWidget?.description || '',
|
||||
})
|
||||
|
||||
watch(widgetData, () => {
|
||||
emit('update:widget', widgetData)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GroupedSettings title="Text">
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<label>Title</label>
|
||||
<a-input v-model:value="widgetData.title" class="nc-input-sm nc-input-shadow" placeholder="Title" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<label>Description</label>
|
||||
<a-textarea
|
||||
v-model:value="widgetData.description"
|
||||
class="nc-input-sm nc-input-text-area nc-input-shadow px-3 !text-gray-800 max-h-[150px] min-h-[100px]"
|
||||
placeholder="Description"
|
||||
/>
|
||||
</div>
|
||||
</GroupedSettings>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -1,27 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { WidgetType } from 'nocodb-sdk'
|
||||
import GroupedSettings from '../Common/GroupedSettings.vue'
|
||||
interface Props {
|
||||
widget: WidgetType
|
||||
|
||||
const widgetStore = useWidgetStore()
|
||||
const { selectedWidget } = storeToRefs(widgetStore)
|
||||
const { updateWidget } = useWidgetStore()
|
||||
const { activeDashboard } = storeToRefs(useDashboardStore())
|
||||
|
||||
const handleConfigUpdate = async (type: string, updates: any) => {
|
||||
if (type === 'text') {
|
||||
await updateWidget(activeDashboard.value.id, selectedWidget.value.id, updates)
|
||||
} else if (type === 'dataSource') {
|
||||
await updateWidget(activeDashboard.value.id, selectedWidget.value.id, {
|
||||
config: {
|
||||
...selectedWidget.value.config,
|
||||
dataSource: {
|
||||
...selectedWidget.value.config.dataSource,
|
||||
...updates,
|
||||
},
|
||||
},
|
||||
})
|
||||
} else if (type === 'metric') {
|
||||
await updateWidget(activeDashboard.value.id, selectedWidget.value.id, {
|
||||
config: {
|
||||
...selectedWidget.value.config,
|
||||
metric: {
|
||||
...selectedWidget.value.config.metric,
|
||||
...updates,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:config': [config: any]
|
||||
}>()
|
||||
|
||||
// Watch for changes and emit updates
|
||||
watchEffect(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SmartsheetDashboardWidgetsCommonConfig>
|
||||
<template #data>
|
||||
<SmartsheetDashboardWidgetsCommonDataText />
|
||||
<SmartsheetDashboardWidgetsCommonDataSource />
|
||||
<SmartsheetDashboardWidgetsCommonDataText @update:widget="handleConfigUpdate('text', $event)" />
|
||||
<SmartsheetDashboardWidgetsCommonDataSource @update:source="handleConfigUpdate('dataSource', $event)" />
|
||||
<GroupedSettings title="Display">
|
||||
<SmartsheetDashboardWidgetsCommonDataAggregation />
|
||||
<SmartsheetDashboardWidgetsCommonDataAggregation @update:aggregation="handleConfigUpdate('metric', $event)" />
|
||||
</GroupedSettings>
|
||||
</template>
|
||||
</SmartsheetDashboardWidgetsCommonConfig>
|
||||
|
||||
@@ -5,8 +5,6 @@ const { isEditingDashboard } = storeToRefs(useDashboardStore())
|
||||
<template>
|
||||
<div v-if="isEditingDashboard" class="flex gap-2 items-center justify-center">
|
||||
<NcButton type="secondary" size="small" @click="isEditingDashboard = false"> Cancel </NcButton>
|
||||
|
||||
<NcButton type="secondary" class="!text-nc-content-brand" size="small"> Save changes </NcButton>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex gap-2 items-center justify-center">
|
||||
|
||||
@@ -22,86 +22,6 @@ export const useWidgetStore = defineStore('widget', () => {
|
||||
|
||||
const selectedWidget = ref<WidgetType | null>(null)
|
||||
|
||||
// Create placeholder data for testing
|
||||
const createPlaceholderWidgets = (dashboardId: string): WidgetType[] => {
|
||||
return [
|
||||
{
|
||||
id: 'widget-1',
|
||||
title: 'Total Records',
|
||||
type: 'metric',
|
||||
fk_dashboard_id: dashboardId,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
config: {
|
||||
component: {
|
||||
title: 'Total Records',
|
||||
description: 'Total number of records in the database',
|
||||
},
|
||||
appearance: {
|
||||
fontSize: 'large',
|
||||
textColor: '#1f2937',
|
||||
backgroundColor: '',
|
||||
},
|
||||
source: {
|
||||
type: 'all_records',
|
||||
aggregation: 'count',
|
||||
viewId: '',
|
||||
columnId: '',
|
||||
filters: [],
|
||||
},
|
||||
display: {
|
||||
showLabel: true,
|
||||
showComparison: true,
|
||||
comparisonPeriod: 'previous_period',
|
||||
},
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'widget-2',
|
||||
title: 'Average Revenue',
|
||||
type: 'metric',
|
||||
fk_dashboard_id: dashboardId,
|
||||
position: {
|
||||
x: 2,
|
||||
y: 0,
|
||||
w: 2,
|
||||
h: 2,
|
||||
},
|
||||
config: {
|
||||
component: {
|
||||
title: 'Average Revenue',
|
||||
description: 'Average revenue per customer',
|
||||
},
|
||||
appearance: {
|
||||
fontSize: 'large',
|
||||
textColor: '#059669',
|
||||
backgroundColor: '#f0fdf4',
|
||||
},
|
||||
source: {
|
||||
type: 'all_records',
|
||||
aggregation: 'avg',
|
||||
viewId: '',
|
||||
columnId: 'revenue',
|
||||
filters: [],
|
||||
},
|
||||
display: {
|
||||
showLabel: true,
|
||||
showComparison: false,
|
||||
comparisonPeriod: 'previous_period',
|
||||
},
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Actions
|
||||
const loadWidgets = async ({ dashboardId, force = false }: { dashboardId: string; force?: boolean }) => {
|
||||
if (!activeWorkspaceId.value || !openedProject.value?.id) {
|
||||
@@ -183,7 +103,7 @@ export const useWidgetStore = defineStore('widget', () => {
|
||||
}
|
||||
|
||||
const updateWidget = async (
|
||||
dashboardId: string,
|
||||
dashboardId = activeDashboardId.value,
|
||||
widgetId: string,
|
||||
updates: Partial<WidgetType>,
|
||||
options?: {
|
||||
@@ -221,6 +141,10 @@ export const useWidgetStore = defineStore('widget', () => {
|
||||
widgets.value.set(dashboardId, dashboardWidgets)
|
||||
}
|
||||
|
||||
if (selectedWidget.value?.id === widgetId) {
|
||||
selectedWidget.value = updated as unknown as WidgetType
|
||||
}
|
||||
|
||||
return updated
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -83,11 +83,10 @@ export interface TableWidgetConfig {
|
||||
export interface MetricWidgetConfig {
|
||||
dataSource?: WidgetDataSource;
|
||||
metric: {
|
||||
type: 'count' | 'summary';
|
||||
column_id?: string;
|
||||
aggregation: 'sum' | 'avg' | 'count' | 'min' | 'max';
|
||||
label?: string;
|
||||
};
|
||||
filters?: any[];
|
||||
}
|
||||
|
||||
export interface TextWidgetConfig {
|
||||
|
||||
@@ -86,13 +86,14 @@ export default class Widget implements IWidget {
|
||||
for (let widget of widgetsList) {
|
||||
widget = prepareForResponse(widget, ['config', 'meta', 'position']);
|
||||
}
|
||||
widgetsList.sort(
|
||||
(a, b) =>
|
||||
(a.order != null ? a.order : Infinity) -
|
||||
(b.order != null ? b.order : Infinity),
|
||||
);
|
||||
await NocoCache.setList(CacheScope.WIDGET, [dashboardId], widgetsList);
|
||||
}
|
||||
widgetsList.sort(
|
||||
(a, b) =>
|
||||
(a.order != null ? a.order : Infinity) -
|
||||
(b.order != null ? b.order : Infinity),
|
||||
);
|
||||
|
||||
return widgetsList?.map((w) => new Widget(w));
|
||||
}
|
||||
|
||||
@@ -118,18 +119,18 @@ export default class Widget implements IWidget {
|
||||
|
||||
insertObj = prepareForDb(insertObj, ['config', 'meta', 'position']);
|
||||
|
||||
const { id } = await ncMeta.metaInsert2(
|
||||
const insertRes = await ncMeta.metaInsert2(
|
||||
context.workspace_id,
|
||||
context.base_id,
|
||||
MetaTable.WIDGETS,
|
||||
insertObj,
|
||||
);
|
||||
|
||||
return Widget.get(context, id, ncMeta).then(async (widget) => {
|
||||
return Widget.get(context, insertRes.id, ncMeta).then(async (widget) => {
|
||||
await NocoCache.appendToList(
|
||||
CacheScope.WIDGET,
|
||||
[widget.fk_dashboard_id],
|
||||
`${CacheScope.WIDGET}:${id}`,
|
||||
`${CacheScope.WIDGET}:${insertRes.id}`,
|
||||
);
|
||||
return widget;
|
||||
});
|
||||
@@ -162,7 +163,10 @@ export default class Widget implements IWidget {
|
||||
widgetId,
|
||||
);
|
||||
|
||||
await NocoCache.update(`${CacheScope.WIDGET}:${widgetId}`, updateObj);
|
||||
await NocoCache.update(
|
||||
`${CacheScope.WIDGET}:${widgetId}`,
|
||||
prepareForResponse(updateObj, ['config', 'meta', 'position']),
|
||||
);
|
||||
|
||||
return await this.get(context, widgetId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user