diff --git a/apps/web/src/api-clients/_common/composables/use-scoped-query.ts b/apps/web/src/api-clients/_common/composables/use-scoped-query.ts index c2d8d12eef..69b834046f 100644 --- a/apps/web/src/api-clients/_common/composables/use-scoped-query.ts +++ b/apps/web/src/api-clients/_common/composables/use-scoped-query.ts @@ -68,10 +68,7 @@ export const useScopedQuery = (() => { const _inheritedEnabled = options?.enabled as MaybeRef | undefined; - - if (_inheritedEnabled !== undefined && !toValue(_inheritedEnabled)) { - return false; - } + if (_inheritedEnabled !== undefined && !toValue(_inheritedEnabled)) return false; return _state.isValidScope && !_state.isLoading; }); diff --git a/apps/web/src/api-clients/dashboard/public-widget/schema/model.ts b/apps/web/src/api-clients/dashboard/public-widget/schema/model.ts index a844fc6f8a..c68d9d61cf 100644 --- a/apps/web/src/api-clients/dashboard/public-widget/schema/model.ts +++ b/apps/web/src/api-clients/dashboard/public-widget/schema/model.ts @@ -16,7 +16,7 @@ export interface PublicWidgetModel { size: WidgetSize; data_table_id: string; widget_type: WidgetType; - options: Record>; + options: Record, WidgetFieldValue>; tags: Tags; workspace_id?: string; domain_id?: string; diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue index cca5982b1d..8f7e5195d4 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue @@ -4,7 +4,9 @@ import { } from 'vue'; import { useMutation } from '@tanstack/vue-query'; -import { isArray, isEqual, uniq } from 'lodash'; +import { + cloneDeep, isArray, isEqual, uniq, +} from 'lodash'; import type { MenuItem } from '@cloudforet/mirinae/src/controls/context-menu/type'; import type { SelectDropdownMenuItem } from '@cloudforet/mirinae/src/controls/dropdown/select-dropdown/type'; @@ -51,12 +53,6 @@ import type { } from '@/common/modules/widgets/types/widget-model'; import { GROUP_BY } from '@/services/cost-explorer/constants/cost-explorer-constant'; - -import { - useDashboardDataTableCascadeUpdate, -} from '@/services/dashboards/composables/use-dashboard-data-table-cascade-update'; -import { useDashboardWidgetFormQuery } from '@/services/dashboards/composables/use-dashboard-widget-form-query'; - import { useDashboardDetailInfoStore } from '@/services/dashboards/stores/dashboard-detail-info-store'; interface Props { @@ -358,11 +354,8 @@ const updateDataTable = async (): Promise => { const result = await updateDataTableAndCascadeUpdate(updateParams); if (widget.value?.state === 'ACTIVE') { - const sanitizedOptions = sanitizeWidgetOptions({ - widgetHeader: { - ...widget.value?.options?.widgetHeader, - }, - }); + const _widgetOptions = cloneDeep(widget.value.options); + const sanitizedOptions = sanitizeWidgetOptions(_widgetOptions, widget.value.widget_type, result); await updateWidget({ widget_id: widgetGenerateState.widgetId, state: 'INACTIVE', diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardTransformContents.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardTransformContents.vue index df9fc3fe3c..9369c9d9d3 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardTransformContents.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardTransformContents.vue @@ -370,11 +370,8 @@ const updateDataTable = async (): Promise => { }; const result = await updateDataTableAndCascadeUpdate(updateParams); if (widget.value?.state === 'ACTIVE') { - const sanitizedOptions = sanitizeWidgetOptions({ - widgetHeader: { - ...widget.value?.options?.widgetHeader, - }, - }); + const _widgetOptions = cloneDeep(widget.value.options); + const sanitizedOptions = sanitizeWidgetOptions(_widgetOptions, widget.value.widget_type, result); await updateWidget({ widget_id: widgetGenerateState.widgetId, state: 'INACTIVE', diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormOverlay.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormOverlay.vue index 9af34020ed..be3fd614d9 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormOverlay.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormOverlay.vue @@ -5,6 +5,7 @@ import { import type { TranslateResult } from 'vue-i18n'; import { useMutation } from '@tanstack/vue-query'; +import { cloneDeep } from 'lodash'; import { PButton, PButtonModal, POverlayLayout, PTextButton, @@ -21,8 +22,10 @@ import ErrorHandler from '@/common/composables/error/errorHandler'; import WidgetFormOverlayStep1 from '@/common/modules/widgets/_components/WidgetFormOverlayStep1.vue'; import WidgetFormOverlayStep2 from '@/common/modules/widgets/_components/WidgetFormOverlayStep2.vue'; import { useWidgetFormQuery } from '@/common/modules/widgets/_composables/use-widget-form-query'; +import { UNSUPPORTED_CHARTS_IN_PIVOT } from '@/common/modules/widgets/_constants/widget-constant'; import { sanitizeWidgetOptions } from '@/common/modules/widgets/_helpers/widget-helper'; import { useWidgetGenerateStore } from '@/common/modules/widgets/_store/widget-generate-store'; +import type { DataTableModel } from '@/common/modules/widgets/types/widget-data-table-type'; import { useDashboardDetailInfoStore } from '@/services/dashboards/stores/dashboard-detail-info-store'; @@ -36,6 +39,7 @@ const widgetGenerateState = widgetGenerateStore.state; /* Query */ const { widget, + dataTableList, api, keys, fetcher, @@ -45,6 +49,7 @@ const { }); const state = reactive({ + selectedDataTable: computed(() => dataTableList.value?.find((item) => item.data_table_id === widgetGenerateState.selectedDataTableId)), sidebarTitle: computed(() => { if (widgetGenerateState.overlayType === 'EXPAND') return undefined; let _title = i18n.t('COMMON.WIDGETS.ADD_WIDGET'); @@ -127,12 +132,13 @@ const handleClickContinue = async () => { if (widget.value?.state === 'ACTIVE') { _updateParams.state = 'INACTIVE'; } - if (widget.value?.options?.widgetHeader) { - _updateParams.options = { - widgetHeader: widget.value?.options?.widgetHeader, - }; + let widgetType = widget.value?.widget_type ?? 'table'; + if (UNSUPPORTED_CHARTS_IN_PIVOT.includes(widgetType)) { + widgetType = 'table'; + _updateParams.widget_type = widgetType; } - const sanitizedOptions = sanitizeWidgetOptions(_updateParams.options ?? {}, _updateParams.widget_type ?? 'table'); + const _widgetOptions = cloneDeep(widget.value?.options ?? {}); + const sanitizedOptions = sanitizeWidgetOptions(_widgetOptions, widgetType, state.selectedDataTable); await updateWidget({ ..._updateParams, options: sanitizedOptions, diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2.vue index f4d4bf4a5f..b4c817e0ae 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2.vue @@ -156,7 +156,7 @@ const { mutateAsync: updateWidgetMutate } = useMutation({ const updateWidget = async () => { if (!widgetGenerateState.widgetId) return; const _isCreating = widget.value?.state === 'CREATING'; - const sanitizedOptions = sanitizeWidgetOptions(state.fieldManager.data, widgetGenerateState.selectedWidgetName); + const sanitizedOptions = sanitizeWidgetOptions(state.fieldManager.data, widgetGenerateState.selectedWidgetName, state.selectedDataTable); const result = await updateWidgetMutate({ widget_id: widgetGenerateState.widgetId, size: widgetGenerateState.size, diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2WidgetForm.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2WidgetForm.vue index cb8cbb803c..1f0d0308f3 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2WidgetForm.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2WidgetForm.vue @@ -32,6 +32,7 @@ import { sanitizeWidgetOptions } from '@/common/modules/widgets/_helpers/widget- import { useWidgetGenerateStore } from '@/common/modules/widgets/_store/widget-generate-store'; import type WidgetFieldValueManager from '@/common/modules/widgets/_widget-field-value-manager'; import WidgetHeaderField from '@/common/modules/widgets/_widget-fields/header/WidgetHeaderField.vue'; +import tableConfig from '@/common/modules/widgets/_widgets/table/widget-config'; import { gray, red } from '@/styles/colors'; @@ -126,8 +127,18 @@ const { mutateAsync: updateWidget } = useMutation({ const handleSelectDataTable = async (dataTableId: string) => { const selectedDataTable = dataTableList.value.find((d) => d.data_table_id === dataTableId); if (!selectedDataTable) return; + + const isPivotDataTable = selectedDataTable.operator === DATA_TABLE_OPERATOR.PIVOT; + let widgetType = widgetGenerateState.selectedWidgetName; + + if (isPivotDataTable && UNSUPPORTED_CHARTS_IN_PIVOT.includes(widgetType)) { + widgetType = 'table'; + widgetGenerateStore.setSelectedWidgetName('table'); + widgetGenerateStore.setSize(tableConfig.meta.sizes[0]); + } + widgetGenerateStore.setSelectedDataTableId(dataTableId); - const sanitizedOptions = sanitizeWidgetOptions(); + const sanitizedOptions = sanitizeWidgetOptions(props.fieldManager.data, widgetType, selectedDataTable); await updateWidget({ widget_id: widgetGenerateState.widgetId, data_table_id: dataTableId, @@ -135,17 +146,13 @@ const handleSelectDataTable = async (dataTableId: string) => { options: sanitizedOptions, }); - props.fieldManager.updateDataTableAndOriginData(selectedDataTable, {}); - - // check if selected chart type is supported in pivot - if (selectedDataTable.operator === DATA_TABLE_OPERATOR.PIVOT && UNSUPPORTED_CHARTS_IN_PIVOT.includes(widgetGenerateState.selectedWidgetName)) { - changeWidgetType('table'); - } + props.fieldManager.updateDataTableAndOriginData(selectedDataTable, sanitizedOptions); }; const handleSelectWidgetName = (widgetName: string) => { changeWidgetType(widgetName); }; + const handleClickEditDataTable = () => { widgetGenerateStore.setOverlayStep(1); state.widgetDefaultValidationModalVisible = false; @@ -185,7 +192,8 @@ const changeWidgetType = (widgetName: string) => { widgetGenerateStore.setSelectedWidgetName(widgetName); widgetGenerateStore.setSize(_config.meta.sizes[0]); - props.fieldManager.updateWidgetType(_config); + const sanitizedOptions = sanitizeWidgetOptions(props.fieldManager.data, widgetName, state.selectedDataTable); + props.fieldManager.updateModifiedData(sanitizedOptions); checkDefaultValidation(); }; diff --git a/apps/web/src/common/modules/widgets/_composables/use-widget-frame.ts b/apps/web/src/common/modules/widgets/_composables/use-widget-frame.ts index 304753b63f..08cd65fa83 100644 --- a/apps/web/src/common/modules/widgets/_composables/use-widget-frame.ts +++ b/apps/web/src/common/modules/widgets/_composables/use-widget-frame.ts @@ -157,7 +157,7 @@ export const useWidgetFrame = ( unitMap: computed>(() => { const _result: Record = {}; dataTableList.value.forEach((d) => { - Object.entries(d.data_info).forEach(([k, v]) => { + Object.entries(d?.data_info ?? {}).forEach(([k, v]) => { if (v?.unit) _result[k] = v.unit; }); }); diff --git a/apps/web/src/common/modules/widgets/_helpers/widget-helper.ts b/apps/web/src/common/modules/widgets/_helpers/widget-helper.ts index d52148ba4d..55209baad7 100644 --- a/apps/web/src/common/modules/widgets/_helpers/widget-helper.ts +++ b/apps/web/src/common/modules/widgets/_helpers/widget-helper.ts @@ -1,11 +1,18 @@ import bytes from 'bytes'; +import { cloneDeep } from 'lodash'; import { byteFormatter, customNumberFormatter, numberFormatter } from '@cloudforet/utils'; +import type { WidgetModel } from '@/api-clients/dashboard/_types/widget-type'; + +import { DATA_TABLE_OPERATOR } from '@/common/modules/widgets/_constants/data-table-constant'; import { DATE_FIELD } from '@/common/modules/widgets/_constants/widget-constant'; import { NUMBER_FORMAT } from '@/common/modules/widgets/_constants/widget-field-constant'; import { getWidgetConfig } from '@/common/modules/widgets/_helpers/widget-config-helper'; +import { integrateFieldsSchema } from '@/common/modules/widgets/_helpers/widget-field-helper'; +import { WIDGET_OPTIONS_AFFECTED_BY_DATA_TABLE, widgetValidatorRegistry } from '@/common/modules/widgets/_widget-field-value-manager/constant/validator-registry'; import type { NumberFormatInfo } from '@/common/modules/widgets/_widget-fields/number-format/type'; +import type { DataTableModel } from '@/common/modules/widgets/types/widget-data-table-type'; import type { WidgetType } from '@/common/modules/widgets/types/widget-model'; import { SIZE_UNITS } from '@/services/asset-inventory-v1/constants/asset-analysis-constant'; @@ -47,21 +54,107 @@ export const getFormattedNumber = (val: number, numberFormatInfo?: NumberFormatI } }; -export const sanitizeWidgetOptions = (options: Record = {}, widgetType: WidgetType = 'table') => { +export const sanitizeWidgetOptions = (options: WidgetModel['options'] = {}, widgetType: WidgetType = 'table', dataTable?: DataTableModel): WidgetModel['options'] => { const currentOptionKeys = Object.keys(options ?? {}); const widgetConfig = getWidgetConfig(widgetType); + const _fieldsSchema = integrateFieldsSchema(widgetConfig?.requiredFieldsSchema ?? {}, widgetConfig?.optionalFieldsSchema ?? {}); const validOptionKeys = [ - ...Object.keys(widgetConfig?.requiredFieldsSchema ?? {}), - ...Object.keys(widgetConfig?.optionalFieldsSchema ?? {}), + ...Object.keys(_fieldsSchema), 'widgetHeader', ]; + if (!widgetConfig) return options; + // Remove keys that are not in the validOptionKeys list currentOptionKeys.forEach((key) => { if (!validOptionKeys.includes(key)) { delete options[key]; } + + const fieldValue = cloneDeep(options[key]).value; + const validator = widgetValidatorRegistry[key]; + const isFieldAffectedByDataTable = WIDGET_OPTIONS_AFFECTED_BY_DATA_TABLE.includes(key); + + if (!dataTable || !fieldValue || !validator || !isFieldAffectedByDataTable) return; + + const fieldOptions = _fieldsSchema[key]?.options ?? {}; + if (!validator(fieldValue, widgetConfig, dataTable, options)) { + const availableFieldKeys = Object.keys(dataTable?.[fieldOptions?.dataTarget || 'data_info'] ?? {}); + console.debug('key', key, dataTable, fieldValue, availableFieldKeys, fieldOptions, validator(fieldValue, widgetConfig, dataTable, options)); + console.debug('availableFieldKeys', key, fieldOptions, availableFieldKeys, fieldValue.data); + if (key === 'dataField') { + const isMultiSelectable = fieldOptions?.multiSelectable; + + const isPivotDataTable = dataTable?.operator === DATA_TABLE_OPERATOR.PIVOT; + if (isPivotDataTable) { + const pivotColumnsField = dataTable?.options.PIVOT?.fields?.column; + options[key] = { value: { ...fieldValue, data: isMultiSelectable ? [pivotColumnsField] : pivotColumnsField } }; + } else if (isMultiSelectable) { + options[key] = { + value: { + ...fieldValue, + data: fieldValue.data?.filter((val) => availableFieldKeys.includes(val)) || [], + }, + }; + } else if (!availableFieldKeys.includes(fieldValue.data)) { + options[key] = { value: { ...fieldValue, data: availableFieldKeys[0] } }; + } + } + if (key === 'groupBy') { + const isMultiSelectable = fieldOptions?.multiSelectable; + if (isMultiSelectable) { + options[key] = { + value: { + ...fieldValue, + data: fieldValue.data?.filter((val) => availableFieldKeys.includes(val)) || [], + }, + }; + } else if (!availableFieldKeys.includes(fieldValue.data)) { + options[key] = { value: { ...fieldValue, data: availableFieldKeys[0] } }; + } + } + if (key === 'categoryBy' || key === 'stackBy' || key === 'xAxis' || key === 'yAxis') { + if (!availableFieldKeys.includes(fieldValue.data)) { + options[key] = { value: { ...fieldValue, data: availableFieldKeys[0] } }; + } + } + if (key === 'sankeyDimensions') { + options[key] = { + value: { + ...fieldValue, + data: fieldValue.data?.filter((val) => availableFieldKeys.includes(val)) || [], + }, + }; + } + if (key === 'formatRules' && fieldOptions.useField) { + if (!availableFieldKeys.includes(fieldValue.field)) { + options[key] = { + value: { + ...fieldValue, + field: availableFieldKeys[0], + }, + }; + } + } + if (key === 'customTableColumnWidth') { + const _availableFieldKeys = [...Object.keys(dataTable?.labels_info ?? {}), ...Object.keys(dataTable?.data_info ?? {})]; + options[key] = { + value: { + widthInfos: fieldValue.widthInfos?.filter((widthInfo) => _availableFieldKeys.includes(widthInfo.fieldKey)) || [], + }, + }; + } + if (key === 'tableColumnComparison') { + options[key] = { + value: { + ...fieldValue, + fields: fieldValue.fields?.filter((field) => availableFieldKeys.includes(field)) || [], + }, + }; + } + } }); + console.debug('options', options); return options; }; diff --git a/apps/web/src/common/modules/widgets/_widget-field-value-manager/constant/default-value-registry.ts b/apps/web/src/common/modules/widgets/_widget-field-value-manager/constant/default-value-registry.ts index 159af87fef..1ce2085e9d 100644 --- a/apps/web/src/common/modules/widgets/_widget-field-value-manager/constant/default-value-registry.ts +++ b/apps/web/src/common/modules/widgets/_widget-field-value-manager/constant/default-value-registry.ts @@ -149,19 +149,19 @@ export const widgetFieldDefaultValueSetterRegistry: WidgetFieldDefaultValueSette dataField: (widgetConfig, dataTable) => { const _fieldsSchema = integrateFieldsSchema(widgetConfig.requiredFieldsSchema, widgetConfig.optionalFieldsSchema); const dataFieldOptions = (_fieldsSchema.dataField?.options ?? {}) as DataFieldOptions; - + const multiSelectable = dataFieldOptions.multiSelectable; const result = cloneDeep(widgetFieldDefaultValueMap.dataField); const isPivotDataTable = dataTable?.operator === DATA_TABLE_OPERATOR.PIVOT; if (isPivotDataTable) { // if pivot dataTable, always multiSelectable return { - data: [dataTable?.options.PIVOT?.fields?.column], + data: multiSelectable ? [dataTable?.options.PIVOT?.fields?.column] : dataTable?.options.PIVOT?.fields?.column, }; } const fieldKeys = sortWidgetTableFields(Object.keys(dataTable?.data_info ?? {})); - if (dataFieldOptions.multiSelectable) { + if (multiSelectable) { result.data = dataFieldOptions.allSelected ? fieldKeys : [fieldKeys?.[0]]; } else { result.data = fieldKeys?.[0]; diff --git a/apps/web/src/common/modules/widgets/_widget-field-value-manager/constant/validator-registry.ts b/apps/web/src/common/modules/widgets/_widget-field-value-manager/constant/validator-registry.ts index e5c8a18b29..a6a3048e22 100644 --- a/apps/web/src/common/modules/widgets/_widget-field-value-manager/constant/validator-registry.ts +++ b/apps/web/src/common/modules/widgets/_widget-field-value-manager/constant/validator-registry.ts @@ -1,3 +1,6 @@ +import { every } from 'lodash'; + +import { DATA_TABLE_OPERATOR } from '@/common/modules/widgets/_constants/data-table-constant'; import { FORMAT_RULE_TYPE } from '@/common/modules/widgets/_constants/widget-field-constant'; import { integrateFieldsSchema } from '@/common/modules/widgets/_helpers/widget-field-helper'; import type { FieldValueValidator } from '@/common/modules/widgets/_widget-field-value-manager/type'; @@ -30,8 +33,26 @@ export interface WidgetValidatorRegistry { [fieldKey: string]: FieldValueValidator; } +const isValidSelectionList = (baseList: string[], selectedList?: string[]): boolean => { + if (!selectedList) return false; + return every(selectedList, (item) => baseList.includes(item)); +}; + +export const WIDGET_OPTIONS_AFFECTED_BY_DATA_TABLE = [ + 'dataField', + 'formatRules', + 'groupBy', + 'stackBy', + 'xAxis', + 'yAxis', + 'categoryBy', + 'sankeyDimensions', + 'customTableColumnWidth', + 'tableColumnComparison', +]; + export const widgetValidatorRegistry: WidgetValidatorRegistry = { - dateRange: (fieldValue: DateRangeValue, widgetConfig, allValueMap) => { + dateRange: (fieldValue: DateRangeValue, widgetConfig, dataTable, allValueMap) => { const _dateRangeType = fieldValue.options?.value; const _granularity = allValueMap?.granularity?.value?.granularity; if (!_dateRangeType || !_granularity) return false; @@ -42,20 +63,38 @@ export const widgetValidatorRegistry: WidgetValidatorRegistry = { return true; }, - dataField: (fieldValue: DataFieldValue, widgetConfig) => { + dataField: (fieldValue: DataFieldValue, widgetConfig, dataTable) => { const _fieldsSchema = integrateFieldsSchema(widgetConfig.requiredFieldsSchema, widgetConfig.optionalFieldsSchema); const dataFieldOptions = (_fieldsSchema.dataField?.options ?? {}) as DataFieldOptions; + const isPivotDataTable = dataTable?.operator === DATA_TABLE_OPERATOR.PIVOT; + const multiSelectable = dataFieldOptions.multiSelectable; + const availableFieldKeys = Object.keys(dataTable?.data_info ?? {}); + + if (isPivotDataTable) { + const pivotColumnsField = dataTable?.options.PIVOT?.fields?.column; + if (!pivotColumnsField) return false; + if (multiSelectable) { + return Array.isArray(fieldValue.data) && !!fieldValue.data.length && isValidSelectionList([pivotColumnsField], fieldValue.data); + } + return fieldValue.data === pivotColumnsField; + } + if (dataFieldOptions.multiSelectable) { - return Array.isArray(fieldValue.data) && !!fieldValue.data.length; + const value = fieldValue.data as string[]|undefined; + return Array.isArray(value) && !!value.length && isValidSelectionList(availableFieldKeys, value); } - return !!fieldValue.data; + + const value = fieldValue.data as string|undefined; + return !!value && availableFieldKeys.includes(value); }, - formatRules: (fieldValue: FormatRulesValue, widgetConfig, allValueMap) => { + formatRules: (fieldValue: FormatRulesValue, widgetConfig, dataTable, allValueMap) => { const _fieldsSchema = integrateFieldsSchema(widgetConfig.requiredFieldsSchema, widgetConfig.optionalFieldsSchema); const formatRulesOptions = (_fieldsSchema.formatRules?.options ?? {}) as FormatRulesOptions; + const availableFieldKeys = Object.keys(dataTable?.[formatRulesOptions?.dataTarget ?? 'labels_info'] ?? {}); const type = formatRulesOptions.formatRulesType; if (formatRulesOptions.useField) { + if (fieldValue.field && !availableFieldKeys.includes(fieldValue.field)) return false; if (formatRulesOptions.dependentField) { const dependentValue: string|string[]|undefined = allValueMap?.[formatRulesOptions.dependentField]?.value?.data; if (!dependentValue) return !!fieldValue.field; @@ -77,28 +116,40 @@ export const widgetValidatorRegistry: WidgetValidatorRegistry = { } return true; }, - categoryBy: (fieldValue: CategoryByValue, widgetConfig) => { + categoryBy: (fieldValue: CategoryByValue, widgetConfig, dataTable) => { const _fieldsSchema = integrateFieldsSchema(widgetConfig.requiredFieldsSchema, widgetConfig.optionalFieldsSchema); const categoryByOptions = (_fieldsSchema.categoryBy?.options ?? {}) as CategoryByOptions; - if (!fieldValue.data || !fieldValue.count || fieldValue.count < 0 || fieldValue.count > categoryByOptions.max) return false; + const availableFieldKeys = Object.keys(dataTable?.[categoryByOptions?.dataTarget ?? 'labels_info'] ?? {}); + + if (!fieldValue.data || !fieldValue.count || fieldValue.count < 0 + || fieldValue.count > categoryByOptions.max || !availableFieldKeys.includes(fieldValue.data)) return false; return true; }, - stackBy: (fieldValue: StackByValue, widgetConfig) => { + stackBy: (fieldValue: StackByValue, widgetConfig, dataTable) => { const _fieldsSchema = integrateFieldsSchema(widgetConfig.requiredFieldsSchema, widgetConfig.optionalFieldsSchema); const stackByOptions = (_fieldsSchema.stackBy?.options ?? {}) as StackByOptions; - if (!fieldValue.data || !fieldValue.count || fieldValue.count < 0 || fieldValue.count > stackByOptions.max) return false; + const availableFieldKeys = Object.keys(dataTable?.[stackByOptions?.dataTarget ?? 'labels_info'] ?? {}); + + if (!fieldValue.data || !fieldValue.count || fieldValue.count < 0 + || fieldValue.count > stackByOptions.max || !availableFieldKeys.includes(fieldValue.data)) return false; return true; }, - xAxis: (fieldValue: XAxisValue, widgetConfig) => { + xAxis: (fieldValue: XAxisValue, widgetConfig, dataTable) => { const _fieldsSchema = integrateFieldsSchema(widgetConfig.requiredFieldsSchema, widgetConfig.optionalFieldsSchema); const xAxisOptions = (_fieldsSchema.xAxis?.options ?? {}) as XAxisOptions; - if (!fieldValue.data || !fieldValue.count || fieldValue.count < 0 || fieldValue.count > xAxisOptions.max) return false; + const availableFieldKeys = Object.keys(dataTable?.[xAxisOptions?.dataTarget ?? 'labels_info'] ?? {}); + + if (!fieldValue.data || !fieldValue.count || fieldValue.count < 0 + || fieldValue.count > xAxisOptions.max || !availableFieldKeys.includes(fieldValue.data)) return false; return true; }, - yAxis: (fieldValue: YAxisValue, widgetConfig) => { + yAxis: (fieldValue: YAxisValue, widgetConfig, dataTable) => { const _fieldsSchema = integrateFieldsSchema(widgetConfig.requiredFieldsSchema, widgetConfig.optionalFieldsSchema); const yAxisOptions = (_fieldsSchema.yAxis?.options ?? {}) as YAxisOptions; - if (!fieldValue.data || !fieldValue.count || fieldValue.count < 0 || fieldValue.count > yAxisOptions.max) return false; + const availableFieldKeys = Object.keys(dataTable?.[yAxisOptions?.dataTarget ?? 'labels_info'] ?? {}); + + if (!fieldValue.data || !fieldValue.count || fieldValue.count < 0 + || fieldValue.count > yAxisOptions.max || !availableFieldKeys.includes(fieldValue.data)) return false; return true; }, colorSchema: (fieldValue: ColorSchemaValue) => { @@ -111,16 +162,24 @@ export const widgetValidatorRegistry: WidgetValidatorRegistry = { } return true; }, - tableColumnComparison: (fieldValue: TableColumnComparisonValue) => { + tableColumnComparison: (fieldValue: TableColumnComparisonValue, widgetConfig, dataTable) => { + const availableFieldKeys = Object.keys(dataTable?.data_info ?? {}); + if (fieldValue.toggleValue) { - return !!fieldValue.decreaseColor && !!fieldValue.increaseColor && !!fieldValue.format && !!fieldValue.fields?.length; + return !!fieldValue.decreaseColor && !!fieldValue.increaseColor && !!fieldValue.format + && !!fieldValue.fields?.length && isValidSelectionList(availableFieldKeys, fieldValue.fields); } return true; }, - customTableColumnWidth: (fieldValue: CustomTableColumnWidthValue) => { + customTableColumnWidth: (fieldValue: CustomTableColumnWidthValue, widgetConfig, dataTable) => { + const availableFieldKeys = [...Object.keys(dataTable?.labels_info ?? {}), ...Object.keys(dataTable?.data_info ?? {})]; + + + if (fieldValue.widthInfos?.length) { return fieldValue.widthInfos.every((d) => !!d.fieldKey && d.width >= 0) - || fieldValue.widthInfos.map((d) => d.fieldKey).length === new Set(fieldValue.widthInfos.map((d) => d.fieldKey)).size; + && fieldValue.widthInfos.map((d) => d.fieldKey).length === new Set(fieldValue.widthInfos.map((d) => d.fieldKey)).size + && isValidSelectionList(availableFieldKeys, fieldValue.widthInfos.map((d) => d.fieldKey)); } return true; }, @@ -139,21 +198,32 @@ export const widgetValidatorRegistry: WidgetValidatorRegistry = { if (!fieldValue.granularity) return false; return true; }, - groupBy: (fieldValue: GroupByValue, widgetConfig) => { + groupBy: (fieldValue: GroupByValue, widgetConfig, dataTable) => { const _fieldsSchema = integrateFieldsSchema(widgetConfig.requiredFieldsSchema, widgetConfig.optionalFieldsSchema); const groupByOptions = (_fieldsSchema.groupBy?.options ?? {}) as GroupByOptions; - if (groupByOptions.fixedValue && fieldValue.data !== groupByOptions.fixedValue) return false; + const availableFieldKeys = Object.keys(dataTable?.[groupByOptions?.dataTarget ?? 'labels_info'] ?? {}); + if (groupByOptions.hideCount && !!fieldValue.count) return false; if (!groupByOptions.hideCount && groupByOptions.max && groupByOptions.defaultMaxCount && (!fieldValue.count || fieldValue.count > groupByOptions.max)) return false; - if (groupByOptions.multiSelectable && !Array.isArray(fieldValue.data)) return false; + if (groupByOptions.multiSelectable) { + if (!Array.isArray(fieldValue.data)) return false; + if (!isValidSelectionList(availableFieldKeys, fieldValue.data as string[]|undefined)) return false; + if (groupByOptions.excludeDateField && fieldValue.data.includes('Date')) return false; + if (groupByOptions.fixedValue && !fieldValue.data.includes(groupByOptions.fixedValue)) return false; + return !!fieldValue.data; + } if (groupByOptions.excludeDateField && fieldValue.data === 'Date') return false; - // return !!fieldValue.data; - return true; + if (groupByOptions.fixedValue && fieldValue.data !== groupByOptions.fixedValue) return false; + return !!fieldValue.data && availableFieldKeys.includes(fieldValue.data as string); }, - sankeyDimensions: (fieldValue: SankeyDimensionsValue, widgetConfig) => { + sankeyDimensions: (fieldValue: SankeyDimensionsValue, widgetConfig, dataTable) => { const _fieldsSchema = integrateFieldsSchema(widgetConfig.requiredFieldsSchema, widgetConfig.optionalFieldsSchema); const sankeyDimensionsOptions = (_fieldsSchema.sankeyDimensions?.options ?? {}) as SankeyDimensionsOptions; - if (!fieldValue.data || fieldValue.data.length !== 2 || !fieldValue.count || fieldValue.count > sankeyDimensionsOptions.max) return false; + const availableFieldKeys = Object.keys(dataTable?.labels_info ?? {}); + + + if (!fieldValue.data || fieldValue.data.length !== 2 || !fieldValue.count + || fieldValue.count > sankeyDimensionsOptions.max || isValidSelectionList(availableFieldKeys, fieldValue.data)) return false; return true; }, widgetHeader: (fieldValue: WidgetHeaderValue) => { diff --git a/apps/web/src/common/modules/widgets/_widget-field-value-manager/index.ts b/apps/web/src/common/modules/widgets/_widget-field-value-manager/index.ts index f29e3d342e..8425d6207d 100644 --- a/apps/web/src/common/modules/widgets/_widget-field-value-manager/index.ts +++ b/apps/web/src/common/modules/widgets/_widget-field-value-manager/index.ts @@ -74,7 +74,7 @@ export default class WidgetFieldValueManager { const validator = widgetValidatorRegistry[key]; if (validator) { - const isValid = validator(this.modifiedData.value[key].value, this.widgetConfig, this.modifiedData.value); + const isValid = validator(this.modifiedData.value[key].value, this.widgetConfig, this.dataTable, this.modifiedData.value); if (!isValid) { this.validationErrors[key as string] = `Invalid value for field "${key}"`; return false; @@ -91,7 +91,7 @@ export default class WidgetFieldValueManager { Object.entries(this.modifiedData.value ?? {}).forEach(([key, field]) => { const validator = widgetValidatorRegistry[key]; - if (validator && !validator(field.value, this.widgetConfig, this.modifiedData.value)) { + if (validator && !validator(field.value, this.widgetConfig, this.dataTable, this.modifiedData.value)) { this.validationErrors[key] = `Invalid value for field "${key}"`; isValid = false; } @@ -110,19 +110,10 @@ export default class WidgetFieldValueManager { this.widgetConfig = widgetConfig; } - private updateModifiedData(data: WidgetFieldValueMap): void { + updateModifiedData(data: WidgetFieldValueMap): void { this.modifiedData.value = { ...data }; } - updateWidgetType(newWidgetConfig: WidgetConfig): void { - this.updateWidgetConfig(newWidgetConfig); - const originDataWithExistingHeaderValue = { - widgetHeader: this.originData.value.widgetHeader, - }; - this.updateModifiedData(WidgetFieldValueManager.applyDefaultValue(originDataWithExistingHeaderValue, newWidgetConfig, this.dataTable)); - this.validationErrors = {}; - } - updateDataTableAndOriginData(dataTable: PublicDataTableModel|PrivateDataTableModel, data: WidgetFieldValueMap): void { this.dataTable = dataTable; this.originData.value = { diff --git a/apps/web/src/common/modules/widgets/_widget-field-value-manager/type.ts b/apps/web/src/common/modules/widgets/_widget-field-value-manager/type.ts index 160658b274..ce60774b6b 100644 --- a/apps/web/src/common/modules/widgets/_widget-field-value-manager/type.ts +++ b/apps/web/src/common/modules/widgets/_widget-field-value-manager/type.ts @@ -34,8 +34,9 @@ import type { WidgetHeightValue } from '@/common/modules/widgets/_widget-fields/ import type { XAxisValue } from '@/common/modules/widgets/_widget-fields/x-axis/type'; import type { YAxisValue } from '@/common/modules/widgets/_widget-fields/y-axis/type'; import type { WidgetConfig } from '@/common/modules/widgets/types/widget-config-type'; +import type { DataTableModel } from '@/common/modules/widgets/types/widget-data-table-type'; -export type FieldValueValidator = (fieldValue: T, widgetConfig: WidgetConfig, allValueMap?: WidgetFieldValueMap) => boolean; +export type FieldValueValidator = (fieldValue: T, widgetConfig: WidgetConfig, dataTable?: DataTableModel, allValueMap?: WidgetFieldValueMap) => boolean; export type FieldDefaultValueConvertor = (widgetConfig: WidgetConfig, dataTable: PublicDataTableModel|PrivateDataTableModel) => WidgetFieldTypeMap[T]['value']; export interface WidgetFieldValueMap { diff --git a/apps/web/src/common/modules/widgets/_widget-fields/data-field/WidgetFieldDataField.vue b/apps/web/src/common/modules/widgets/_widget-fields/data-field/WidgetFieldDataField.vue index 77fd4129ed..da1c28077c 100644 --- a/apps/web/src/common/modules/widgets/_widget-fields/data-field/WidgetFieldDataField.vue +++ b/apps/web/src/common/modules/widgets/_widget-fields/data-field/WidgetFieldDataField.vue @@ -47,7 +47,7 @@ const state = reactive({ label: d, })); }), - invalid: computed(() => !validator(state.fieldValue, props.widgetConfig)), + invalid: computed(() => !validator(state.fieldValue, props.widgetConfig, state.selectedDataTable)), selectedItem: computed(() => { if (state.isPivotDataTable) { const dataName = state.selectedDataTable?.options.PIVOT?.fields?.column || ''; diff --git a/apps/web/src/common/modules/widgets/_widget-fields/format-rules/WidgetFieldFormatRules.vue b/apps/web/src/common/modules/widgets/_widget-fields/format-rules/WidgetFieldFormatRules.vue index 8a00f91e53..8bb93ebebd 100644 --- a/apps/web/src/common/modules/widgets/_widget-fields/format-rules/WidgetFieldFormatRules.vue +++ b/apps/web/src/common/modules/widgets/_widget-fields/format-rules/WidgetFieldFormatRules.vue @@ -48,7 +48,7 @@ const state = reactive({ selectedDataTable: computed(() => dataTableList.value.find((d) => d.data_table_id === widgetGenerateState.selectedDataTableId)), fieldValue: computed(() => props.fieldManager.data[FIELD_KEY].value), type: computed(() => props.widgetFieldSchema?.options?.formatRulesType as FormatRulesType), - invalid: computed(() => !validator(state.fieldValue, props.widgetConfig)), + invalid: computed(() => !validator(state.fieldValue, props.widgetConfig, state.selectedDataTable)), fieldInvalid: computed(() => { if (!props.widgetFieldSchema?.options?.useField) return false; if (state.fieldValue.field === undefined) return true; diff --git a/apps/web/src/common/modules/widgets/_widget-fields/group-by/WidgetFieldGroupBy.vue b/apps/web/src/common/modules/widgets/_widget-fields/group-by/WidgetFieldGroupBy.vue index 4af591fde4..6c35461472 100644 --- a/apps/web/src/common/modules/widgets/_widget-fields/group-by/WidgetFieldGroupBy.vue +++ b/apps/web/src/common/modules/widgets/_widget-fields/group-by/WidgetFieldGroupBy.vue @@ -72,7 +72,7 @@ const state = reactive({ } return state.fieldValue.data; }), - isValid: computed(() => validator(state.fieldValue, props.widgetConfig)), + isValid: computed(() => validator(state.fieldValue, props.widgetConfig, state.selectedDataTable)), max: computed(() => props.widgetFieldSchema?.options?.max), isMaxValid: computed(() => (state.max ? ((state.fieldValue?.count ?? DEFAULT_COUNT) <= state.max) : true)), tooltipDesc: computed(() => i18n.t('COMMON.WIDGETS.MAX_ITEMS_DESC', { diff --git a/apps/web/src/common/modules/widgets/_widget-fields/sankey-dimensions/WidgetFieldSankeyDimensions.vue b/apps/web/src/common/modules/widgets/_widget-fields/sankey-dimensions/WidgetFieldSankeyDimensions.vue index 48a607b674..8fc5b0025e 100644 --- a/apps/web/src/common/modules/widgets/_widget-fields/sankey-dimensions/WidgetFieldSankeyDimensions.vue +++ b/apps/web/src/common/modules/widgets/_widget-fields/sankey-dimensions/WidgetFieldSankeyDimensions.vue @@ -50,8 +50,9 @@ const state = reactive({ fieldValue: computed(() => props.fieldManager.data[FIELD_KEY].value), max: computed(() => props.widgetFieldSchema?.options?.max), menuItems: computed(() => { - if (!state.selectedDataTable) return []; - const dataInfoList = sortWidgetTableFields(Object.keys(state.selectedDataTable?.labels_info ?? {})) ?? []; + const dataTarget = props.widgetFieldSchema?.options?.dataTarget; + if (!state.selectedDataTable || !dataTarget) return []; + const dataInfoList = sortWidgetTableFields(Object.keys(state.selectedDataTable?.[dataTarget] ?? {})) ?? []; return dataInfoList.map((d) => ({ name: d, label: d, @@ -61,7 +62,7 @@ const state = reactive({ if (!state.menuItems.length) return []; return state.menuItems.filter((d) => state.fieldValue.data?.includes(d.name)); }), - isValid: computed(() => validator(state.fieldValue, props.widgetConfig)), + isValid: computed(() => validator(state.fieldValue, props.widgetConfig, state.selectedDataTable)), isMaxValid: computed(() => state.fieldValue?.count <= state.max), tooltipDesc: computed(() => i18n.t('COMMON.WIDGETS.MAX_ITEMS_DESC', { fieldName: i18n.t('DASHBOARDS.WIDGET.OVERLAY.STEP_2.DIMENSIONS'), diff --git a/apps/web/src/common/modules/widgets/_widget-fields/sankey-dimensions/type.ts b/apps/web/src/common/modules/widgets/_widget-fields/sankey-dimensions/type.ts index 536038bed4..0a47e6549e 100644 --- a/apps/web/src/common/modules/widgets/_widget-fields/sankey-dimensions/type.ts +++ b/apps/web/src/common/modules/widgets/_widget-fields/sankey-dimensions/type.ts @@ -1,5 +1,6 @@ export interface SankeyDimensionsOptions { + dataTarget: 'data_info' | 'labels_info'; max: number; defaultMaxCount: number; } diff --git a/apps/web/src/common/modules/widgets/_widgets/sankey-chart/widget-config.ts b/apps/web/src/common/modules/widgets/_widgets/sankey-chart/widget-config.ts index ff39ac69a8..21ed2bae17 100644 --- a/apps/web/src/common/modules/widgets/_widgets/sankey-chart/widget-config.ts +++ b/apps/web/src/common/modules/widgets/_widgets/sankey-chart/widget-config.ts @@ -16,6 +16,7 @@ const sankeyChart: WidgetConfig = { dataField: {}, sankeyDimensions: { options: { + dataTarget: 'labels_info', defaultMaxCount: 10, max: 31, },