diff --git a/frontend/public/images/example.png b/frontend/public/images/example.png deleted file mode 100644 index 9e8f37e73..000000000 Binary files a/frontend/public/images/example.png and /dev/null differ diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js index 30e83916e..b3c67ec83 100644 --- a/frontend/rollup.config.js +++ b/frontend/rollup.config.js @@ -7,6 +7,7 @@ import replace from '@rollup/plugin-replace'; import typescript from '@rollup/plugin-typescript'; import path from 'path'; import cleanup from 'rollup-plugin-cleanup'; + export default { input: 'src/task.ts', // 打包入口 output: { @@ -36,6 +37,7 @@ export default { cleanup(), replace({ 'console.log': '//console.log', + 'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL), }), ], }; diff --git a/frontend/src/app/components/Avatar.tsx b/frontend/src/app/components/Avatar.tsx index 3262924fc..4aeb4465e 100644 --- a/frontend/src/app/components/Avatar.tsx +++ b/frontend/src/app/components/Avatar.tsx @@ -1,4 +1,5 @@ import { Avatar as AntdAvatar, AvatarProps } from 'antd'; +import endsWith from 'lodash/endsWith'; import { CSSProperties, useState } from 'react'; import styled from 'styled-components/macro'; @@ -12,7 +13,7 @@ export function Avatar(props: AvatarProps) { } if ( typeof safeSrc === 'string' && - (safeSrc.endsWith('null') || safeSrc.endsWith('undefined')) + (endsWith(safeSrc, 'null') || endsWith(safeSrc, 'undefined')) ) { setSafeSrc(''); } diff --git a/frontend/src/app/migration/BoardConfig/migrateWidgetChartConfig.ts b/frontend/src/app/migration/BoardConfig/migrateWidgetChartConfig.ts index 4ad2e2153..8e8623694 100644 --- a/frontend/src/app/migration/BoardConfig/migrateWidgetChartConfig.ts +++ b/frontend/src/app/migration/BoardConfig/migrateWidgetChartConfig.ts @@ -28,6 +28,14 @@ const migrateWidgetChartConfig = (widgets: Widget[]): Widget[] => { } return widgets .map(widget => { + // ------------------------------------------------ + // FIXME 1.0.0-rc.3 hotfix, will remove in next version. + if (typeof widget?.config?.content?.dataChart?.config === 'string') { + widget.config.content.dataChart.config = JSON.parse( + widget.config.content.dataChart.config, + ); + } + // ------------------------------------------------ if (widget?.config?.content?.dataChart?.config) { const event_rc_2 = new MigrationEvent(APP_VERSION_RC_2, RC2); const dispatcher_rc_2 = new MigrationEventDispatcher(event_rc_2); diff --git a/frontend/src/app/pages/DashBoardPage/components/WidgetManager/utils/init.ts b/frontend/src/app/pages/DashBoardPage/components/WidgetManager/utils/init.ts index 7fd6f7d3c..90840a201 100644 --- a/frontend/src/app/pages/DashBoardPage/components/WidgetManager/utils/init.ts +++ b/frontend/src/app/pages/DashBoardPage/components/WidgetManager/utils/init.ts @@ -27,7 +27,7 @@ import type { import { Widget } from 'app/pages/DashBoardPage/types/widgetTypes'; import type { ChartStyleConfig } from 'app/types/ChartConfig'; import { getInitialLocale } from 'locales/utils'; -import { uuidv4 } from 'utils/utils'; +import { universalUUID } from 'utils/utils'; export const initTitleTpl = () => { const titleTpl: ChartStyleConfig = { @@ -472,7 +472,7 @@ export const initFreeWidgetRect = (): RectConfig => ({ export const widgetTpl = (): Widget => { return { - id: uuidv4(), + id: universalUUID(), dashboardId: '', datachartId: '', relations: [], @@ -502,7 +502,7 @@ export const widgetTpl = (): Widget => { }; export const initClientId = () => { - return 'client_' + uuidv4(); + return 'client_' + universalUUID(); }; export const initWidgetName = (i18nMap: object, local?: string) => { if (local && i18nMap[local]) { diff --git a/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/HiddenUploader.tsx b/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/HiddenUploader.tsx new file mode 100644 index 000000000..4fc289602 --- /dev/null +++ b/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/HiddenUploader.tsx @@ -0,0 +1,71 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Upload } from 'antd'; +import { uploadBoardImage } from 'app/pages/DashBoardPage/pages/BoardEditor/slice/thunk'; +import { + forwardRef, + useCallback, + useContext, + useImperativeHandle, + useRef, +} from 'react'; +import { useDispatch } from 'react-redux'; +import { BoardContext } from '../../BoardProvider/BoardProvider'; + +interface HiddenUploaderProps { + onChange: (url: string) => void; +} + +export const HiddenUploader = forwardRef( + ({ onChange }: HiddenUploaderProps, ref) => { + const dispatch = useDispatch(); + const uploadRef = useRef(); + const { boardId } = useContext(BoardContext); + + useImperativeHandle(ref, () => uploadRef.current?.upload.uploader); + + const beforeUpload = useCallback( + async info => { + const formData = new FormData(); + formData.append('file', info); + dispatch( + uploadBoardImage({ + boardId, + fileName: info.name, + formData: formData, + resolve: onChange, + }), + ); + return false; + }, + [boardId, dispatch, onChange], + ); + + return ( + + ); + }, +); diff --git a/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/ImageWidget.tsx b/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/ImageWidget.tsx index 1624073b3..2765079aa 100644 --- a/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/ImageWidget.tsx +++ b/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/ImageWidget.tsx @@ -15,9 +15,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Space } from 'antd'; import { WidgetContext } from 'app/pages/DashBoardPage/components/WidgetProvider/WidgetProvider'; -import { memo, useContext } from 'react'; +import { + editBoardStackActions, + editWidgetInfoActions, +} from 'app/pages/DashBoardPage/pages/BoardEditor/slice'; +import { memo, useCallback, useContext, useEffect, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components/macro'; import { BoardContext } from '../../BoardProvider/BoardProvider'; import { FlexStyle, ZIndexStyle } from '../../WidgetComponents/constants'; import { EditMask } from '../../WidgetComponents/EditMask'; @@ -30,38 +37,99 @@ import { getWidgetBaseStyle, getWidgetTitle, } from '../../WidgetManager/utils/utils'; +import { WidgetInfoContext } from '../../WidgetProvider/WidgetInfoProvider'; +import { HiddenUploader } from './HiddenUploader'; import { ImageWidgetCore } from './ImageWidgetCore'; +import { Picture } from './Picture'; export const ImageWidget: React.FC<{ hideTitle: boolean }> = memo( ({ hideTitle }) => { + const dispatch = useDispatch(); const widget = useContext(WidgetContext); + const widgetInfo = useContext(WidgetInfoContext); const { editing } = useContext(BoardContext); const title = getWidgetTitle(widget.config.customConfig.props); title.title = widget.config.name; const { background, border, padding } = getWidgetBaseStyle( widget.config.customConfig.props, ); + const showBackground = + !background.image && background.color === 'transparent'; + const uploaderRef = useRef(); + + useEffect(() => { + if (widgetInfo.editing) { + uploaderRef.current?.onClick(); + } + }, [widgetInfo.editing]); + + const uploaderChange = useCallback( + (url: string) => { + dispatch( + editBoardStackActions.updateWidgetStyleConfigByPath({ + ancestors: [0, 0], + configItem: { + key: 'background', + comType: 'background', + label: 'background.background', + value: { ...background, image: url }, + }, + wid: widget.id, + }), + ); + dispatch(editWidgetInfoActions.closeWidgetEditing(widget.id)); + }, + [dispatch, widget.id, background], + ); + return ( - -
- {!hideTitle && } + <> + +
+ {!hideTitle && } -
- +
+ +
-
- {editing && } - - - - - - -
+ {editing && } + + + + + + + + {editing && ( + + )} + {editing && showBackground && ( + + + + )} + ); }, ); + +const ImageWidgetBackground = styled.div` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; diff --git a/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/Picture.tsx b/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/Picture.tsx new file mode 100644 index 000000000..e96c1add4 --- /dev/null +++ b/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/Picture.tsx @@ -0,0 +1,51 @@ +/** + * Datart + * + * Copyright 2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import useI18NPrefix from 'app/hooks/useI18NPrefix'; +import styled from 'styled-components/macro'; + +export function Picture() { + const t = useI18NPrefix(`viz.board.setting`); + + return ( + + + + {t('dbClickToUpload')} + + + ); +} + +const Svg = styled.svg` + width: 60%; + height: 60%; + + path { + fill: ${p => p.theme.borderColorEmphasis}; + } + + text { + font-size: 120px; + fill: ${p => p.theme.borderColorBase}; + } +`; diff --git a/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/imageConfig.ts b/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/imageConfig.ts index 70f67e386..52a9dfb8b 100644 --- a/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/imageConfig.ts +++ b/frontend/src/app/pages/DashBoardPage/components/Widgets/ImageWidget/imageConfig.ts @@ -96,15 +96,6 @@ export const widgetToolkit: ImageToolkit = { { ...initPaddingTpl() }, { ...initBorderTpl() }, ]; - widget.config.customConfig.props?.forEach(ele => { - if (ele.key === 'backgroundGroup') { - ele.rows?.forEach(row => { - if (row.key === 'background') { - row.value.image = '/images/example.png'; - } - }); - } - }); return widget; }, diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ImageUpload.tsx b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ImageUpload.tsx index ffefa97b6..7f173af27 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ImageUpload.tsx +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/components/SlideSetting/SettingItem/BasicSet/ImageUpload.tsx @@ -63,6 +63,7 @@ export const UploadDragger: React.FC<{ return ( ) { const id = action.payload; if (id) { - state[id].selected = false; state[id].editing = false; } else { for (let key of Object.keys(state)) { diff --git a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/thunk.ts b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/thunk.ts index 3fc4fc831..5e8d39a20 100644 --- a/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/thunk.ts +++ b/frontend/src/app/pages/DashBoardPage/pages/BoardEditor/slice/thunk.ts @@ -269,7 +269,7 @@ export const addDataChartWidgets = createAsyncThunk< 'editBoard/addDataChartWidgets', async ({ boardId, chartIds, boardType }, { getState, dispatch }) => { const { - data: { datacharts, views, viewVariables }, + data: { datacharts: serverDataCharts, views, viewVariables }, } = await request2<{ datacharts: ServerDatachart[]; views: View[]; @@ -278,13 +278,17 @@ export const addDataChartWidgets = createAsyncThunk< url: `viz/datacharts?datachartIds=${chartIds.join()}`, method: 'get', }); - const dataCharts: DataChart[] = getDataChartsByServer(datacharts, views); - const dashboardDataChartMap = datacharts.reduce< - Record - >((acc, cur) => { - acc[cur.id] = cur; - return acc; - }, {}); + const dataCharts: DataChart[] = getDataChartsByServer( + serverDataCharts, + views, + ); + const dashboardDataChartMap = dataCharts.reduce>( + (acc, cur) => { + acc[cur.id] = cur; + return acc; + }, + {}, + ); const viewViews = getChartDataView(views, dataCharts); dispatch( boardActions.setDataChartToMap({ dashboardId: boardId, dataCharts }), diff --git a/frontend/src/app/pages/DashBoardPage/utils/index.ts b/frontend/src/app/pages/DashBoardPage/utils/index.ts index da75d5432..9b18d6d8d 100644 --- a/frontend/src/app/pages/DashBoardPage/utils/index.ts +++ b/frontend/src/app/pages/DashBoardPage/utils/index.ts @@ -43,6 +43,7 @@ import { PUBLIC_URL, TIME_FORMATTER, } from 'globalConstants'; +import startsWith from 'lodash/startsWith'; import moment from 'moment'; import { CloneValueDeep } from 'utils/object'; import { boardDrillManager } from '../components/BoardDrillManager/BoardDrillManager'; @@ -73,7 +74,7 @@ export const dateFormatObj = { }; export const convertImageUrl = (urlKey: string = ''): string => { - if (urlKey.startsWith(BOARD_FILE_IMG_PREFIX)) { + if (startsWith(urlKey, BOARD_FILE_IMG_PREFIX)) { return `${window.location.origin}${PUBLIC_URL}/${urlKey}`; } return urlKey; @@ -88,7 +89,7 @@ export const getBackgroundImage = (url: string = ''): string => { */ export const adaptBoardImageUrl = (url: string = '', curBoardId: string) => { const splitter = BOARD_FILE_IMG_PREFIX; - if (!url.startsWith(splitter)) return url; + if (!startsWith(url, splitter)) return url; if (!curBoardId) return url; const originalBoardId = url.split(splitter)[1].split('/')[0]; const nextUrl = url.replace(originalBoardId, curBoardId); diff --git a/frontend/src/globalConstants.ts b/frontend/src/globalConstants.ts index cb9d607d9..682884170 100644 --- a/frontend/src/globalConstants.ts +++ b/frontend/src/globalConstants.ts @@ -37,7 +37,7 @@ export enum StorageKeys { Theme = 'THEME', } -export const PUBLIC_URL = process?.env?.PUBLIC_URL || ''; +export const PUBLIC_URL = process.env.PUBLIC_URL || ''; export const BASE_API_URL = `${PUBLIC_URL}/api/v1`; export const BASE_RESOURCE_URL = `${PUBLIC_URL}/`; // 1 hour diff --git a/frontend/src/locales/en/translation.json b/frontend/src/locales/en/translation.json index bfe926bdb..eefa32b46 100644 --- a/frontend/src/locales/en/translation.json +++ b/frontend/src/locales/en/translation.json @@ -999,6 +999,7 @@ "color": "Color", "image": "Image", "uploadTip": "Click to Upload", + "dbClickToUpload": "Double Click To Upload", "padding": "Padding", "paddingTop": "Padding Top", "paddingRight": "Padding Right", diff --git a/frontend/src/locales/zh/translation.json b/frontend/src/locales/zh/translation.json index 02422862a..546d7fddc 100644 --- a/frontend/src/locales/zh/translation.json +++ b/frontend/src/locales/zh/translation.json @@ -997,6 +997,7 @@ "color": "颜色", "image": "图片", "uploadTip": "点击上传", + "dbClickToUpload": "双击上传图片", "padding": "内边距", "paddingTop": "上边距", "paddingRight": "右边距", diff --git a/frontend/src/utils/@reduxjs/toolkit.tsx b/frontend/src/utils/@reduxjs/toolkit.tsx index 2babacd7a..590a383ce 100644 --- a/frontend/src/utils/@reduxjs/toolkit.tsx +++ b/frontend/src/utils/@reduxjs/toolkit.tsx @@ -1,7 +1,6 @@ import { createSlice as createSliceOriginal, CreateSliceOptions, - isRejected, SliceCaseReducers, } from '@reduxjs/toolkit'; import { RootStateKeyType } from '../types/injector-typings'; @@ -9,6 +8,7 @@ import { RootStateKeyType } from '../types/injector-typings'; /* Wrap createSlice with stricter Name options */ /* istanbul ignore next */ +// TODO: Check if this is still needed export const createSlice = < State, CaseReducers extends SliceCaseReducers, @@ -16,13 +16,3 @@ export const createSlice = < >( options: CreateSliceOptions, ) => createSliceOriginal(options); - -export function isMySliceAction(action, targetSliceName) { - return action?.type?.startsWith(targetSliceName); -} - -export function isRejectedScopedSlice(sliceNames: string[]) { - return action => - isRejected(action) && - sliceNames.some(sliceName => isMySliceAction(action, sliceName)); -} diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts index 733230bc2..f646071d0 100644 --- a/frontend/src/utils/utils.ts +++ b/frontend/src/utils/utils.ts @@ -10,10 +10,18 @@ import { FONT_WEIGHT_REGULAR, } from 'styles/StyleConstants'; import { APIResponse } from 'types'; +import { default as uuidv4 } from 'uuid/dist/umd/uuidv4.min'; import { SaveFormModel } from '../app/pages/MainPage/pages/VizPage/SaveFormContext'; import { removeToken } from './auth'; -export { default as uuidv4 } from 'uuid/dist/umd/uuidv4.min'; +export { uuidv4 }; + +// For environments that do not support crypto.getRandomValues, such as nashorn. +export function universalUUID() { + return typeof crypto === 'undefined' + ? `_${Math.random().toString(36).substring(2)}` + : uuidv4(); +} export function errorHandle(error) { if (error?.response) {