Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions client/packages/lowcoder/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3451,6 +3451,16 @@ export const en = {
"cancelDefaultTheme": "Unset Default Theme",
"setDefaultTheme": "Set as Default Theme",
"copyTheme": "Duplicate Theme",
"exportTheme": "Export Theme",
"importTheme": "Import Theme",
"importSuccessMsg": "Theme Imported Successfully",
"importError": "Failed to Import Theme: {message}",
"importParseError": "Invalid Theme File: Could Not Parse JSON",
"importFormatError": "Invalid Theme File: Missing Required Theme Data",
"importFileError": "Failed to Read Theme File",
"importNameRequired": "Theme Name Is Required",
"importSuffix": " Import",
"importDefaultName": "Imported Theme",
"setSuccessMsg": "Setting Succeeded",
"cancelSuccessMsg": "Unsetting Succeeded",
"deleteSuccessMsg": "Deletion Succeeded",
Expand Down
38 changes: 38 additions & 0 deletions client/packages/lowcoder/src/pages/setting/theme/ThemeImport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { default as AntUpload } from "antd/es/upload";
import React from "react";
import styled from "styled-components";

const Upload = styled(AntUpload)`
.ant-upload-wrapper .ant-upload-select {
display: block;
}
`;

type ThemeImportProps = {
children: React.ReactNode;
disabled?: boolean;
onImport: (file: File) => void | Promise<void>;
};

export function ThemeImport(props: ThemeImportProps) {
const { children, disabled, onImport } = props;

return (
<Upload
accept=".json,application/json"
showUploadList={false}
disabled={disabled}
customRequest={async ({ file, onSuccess, onError }) => {
try {
await onImport(file as File);
onSuccess?.("ok");
} catch (error) {
onError?.(error as Error);
}
}}
multiple={false}
>
{children}
</Upload>
);
}
32 changes: 30 additions & 2 deletions client/packages/lowcoder/src/pages/setting/theme/detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import { Card, Divider, Flex, List, Tooltip } from 'antd';

import { ThemeCompPanel } from "pages/setting/theme/ThemeCompPanel";
import { JSONObject } from "@lowcoder-ee/util/jsonTypes";
import { exportThemeAsJSONFile } from "../themeImportExport";
import _ from "lodash";

const ThemeSettingsView = styled.div`
font-size: 14px;
Expand Down Expand Up @@ -129,7 +131,7 @@ class ThemeDetailPage extends React.Component<ThemeDetailPageProps, ThemeDetailP
}

componentDidUpdate(prevProps: ThemeDetailPageProps, prevState: ThemeDetailPageState) {
if (prevProps.themeList?.length !== this.props.themeList?.length) {
if (prevProps.themeList !== this.props.themeList && prevProps.themeList?.length !== this.props.themeList?.length) {
this.findCurrentTheme();
}
}
Expand Down Expand Up @@ -169,10 +171,17 @@ class ThemeDetailPage extends React.Component<ThemeDetailPageProps, ThemeDetailP
configChange(params: configChangeParams) {
if (!this.state.theme) return;

const { themeSettingKey, ...rest } = params;
const value = _.find(_.values(rest), (v) => v !== undefined);

if (_.isEqual(this.state.theme[themeSettingKey as keyof typeof this.state.theme], value)) {
return;
}

this.setState({
theme: {
...this.state.theme,
[params.themeSettingKey]: params.color || params.radius || params.chart || params.margin || params.padding || params.borderWidth || params.borderStyle || params.fontFamily || params.showComponentLoadingIndicators || params.showDataLoadingIndicators || params.dataLoadingIndicator || params.gridColumns || params.gridRowHeight || params.gridRowCount || params.gridPaddingX || params.gridPaddingY || params.gridBgImage || params.gridBgImageRepeat || params.gridBgImageSize || params.gridBgImagePosition || params.gridBgImageOrigin,
[themeSettingKey]: value,
},
});
}
Expand All @@ -188,6 +197,18 @@ class ThemeDetailPage extends React.Component<ThemeDetailPageProps, ThemeDetailP
history.push(THEME_SETTING);
};

handleExport = () => {
if (!this.state.name || !this.state.theme) {
return;
}
exportThemeAsJSONFile({
name: this.state.name,
id: this.id,
updateTime: Date.now(),
theme: this.state.theme,
});
};

render() {

const colorSettings = [
Expand Down Expand Up @@ -898,6 +919,13 @@ class ThemeDetailPage extends React.Component<ThemeDetailPageProps, ThemeDetailP
>
{trans("theme.saveBtn")}
</SaveButton>
<ResetButton
onClick={this.handleExport}
disabled={!this.state.name || !this.state.theme}
style={{ marginLeft: "auto" }}
>
{trans("theme.exportTheme")}
</ResetButton>
</Footer>
</DetailContainer>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const Footer = styled.div`
padding: 24px;
position: fixed;
bottom: 0;
width: calc(100vw - 492px);
width: calc(100% - 520px);
background-color: #fff;
z-index: 1000;
margin-right: 10px;
Expand Down Expand Up @@ -659,44 +659,43 @@ export const CustomModalStyled = styled(CustomModal)`
}
`;

export const Margin = styled.div<{ $margin: string }>`
> div {
margin: 3px;
overflow: hidden;
> svg {
fill: currentColor;
}
}
}
`;
export const Padding = styled.div<{ $padding: string }>`
> div {
margin: 3px;
overflow: hidden;
> svg {
fill: currentColor;
}
}
}`
export const Margin = styled.div<{ $margin: string }>`
> div {
margin: 3px;
overflow: hidden;
> svg {
fill: currentColor;
}
}
`;

export const Padding = styled.div<{ $padding: string }>`
> div {
margin: 3px;
overflow: hidden;
> svg {
fill: currentColor;
}
}
`;

// Added By Aqib Mirza
export const GridColumns = styled.div<{ $gridColumns: string }>`
export const GridColumns = styled.div<{ $gridColumns: string }>`
> div {
margin: 3px;
overflow: hidden;
> svg {
fill: currentColor;
}
> svg {
fill: currentColor;
}
}
`;

export const BorderStyle = styled.div<{ $borderStyle: string }>`
export const BorderStyle = styled.div<{ $borderStyle: string }>`
> div {
margin: 3px;
overflow: hidden;
> svg {
fill: currentColor;
}
> svg {
fill: currentColor;
}
}
`;
Expand All @@ -705,9 +704,8 @@ export const BorderWidth = styled.div<{ $borderWidth: string }>`
> div {
margin: 3px;
overflow: hidden;
> svg {
fill: currentColor;
}
> svg {
fill: currentColor;
}
}
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum MENU_TYPE {
COPY = "copy",
EDIT = "edit",
RENAME = "rename",
EXPORT = "export",
}

export enum DETAIL_TYPE {
Expand Down
122 changes: 122 additions & 0 deletions client/packages/lowcoder/src/pages/setting/theme/themeImportExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { ThemeDetail, ThemeType } from "api/commonSettingApi";
import { genQueryId } from "comps/utils/idGenerator";
import { trans } from "i18n";
import { saveDataAsFile } from "util/fileUtils";

export const THEME_EXPORT_VERSION = 1;
export const THEME_EXPORT_TYPE = "lowcoder-theme";

export interface ThemeExportPayload {
version: number;
type: typeof THEME_EXPORT_TYPE;
theme: ThemeType;
}

function isThemeDetail(value: unknown): value is ThemeDetail {
return typeof value === "object" && value !== null;
}

function isThemeType(value: unknown): value is ThemeType {
if (typeof value !== "object" || value === null) {
return false;
}
const theme = value as ThemeType;
return typeof theme.name === "string" && isThemeDetail(theme.theme);
}

export function parseThemeImport(content: string): ThemeType {
let data: unknown;
try {
data = JSON.parse(content);
} catch {
throw new Error(trans("theme.importParseError"));
}

if (typeof data !== "object" || data === null) {
throw new Error(trans("theme.importFormatError"));
}

const payload = data as Record<string, unknown>;

if (payload.type === THEME_EXPORT_TYPE && isThemeType(payload.theme)) {
return payload.theme;
}

if (isThemeType(data)) {
return data;
}

throw new Error(trans("theme.importFormatError"));
}

export function resolveUniqueThemeName(baseName: string, existingNames: Set<string>): string {
const trimmed = baseName.trim();
if (!trimmed) {
throw new Error(trans("theme.importNameRequired"));
}
if (!existingNames.has(trimmed)) {
return trimmed;
}

const importSuffix = trans("theme.importSuffix");
let candidate = trimmed + importSuffix;
if (!existingNames.has(candidate)) {
return candidate;
}

let index = 1;
while (existingNames.has(candidate + index)) {
index += 1;
}
return candidate + index;
}

export function prepareImportedTheme(
source: ThemeType,
existingThemes: ThemeType[],
fallbackName?: string
): ThemeType {
const existingNames = new Set(existingThemes.map((theme) => theme.name));
const name = resolveUniqueThemeName(
source.name || fallbackName || trans("theme.importDefaultName"),
existingNames
);

return {
name,
id: genQueryId(),
updateTime: Date.now(),
theme: source.theme,
};
}

export function exportThemeAsJSONFile(theme: ThemeType) {
const exportObj: ThemeExportPayload = {
version: THEME_EXPORT_VERSION,
type: THEME_EXPORT_TYPE,
theme,
};
void saveDataAsFile({
data: exportObj,
filename: `${theme.name}.json`,
fileType: "json",
});
}

export function readThemeFile(file: File): Promise<ThemeType> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = (event) => {
try {
if (!event.target?.result) {
throw new Error(trans("theme.importFileError"));
}
resolve(parseThemeImport(event.target.result.toString()));
} catch (error) {
reject(error);
}
};
reader.onerror = () => reject(new Error(trans("theme.importFileError")));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ function ThemeList(props: ThemeListProp) {
label: trans("theme.copyTheme"),
key: MENU_TYPE.COPY,
},
{
label: trans("theme.exportTheme"),
key: MENU_TYPE.EXPORT,
},
{
label: trans("delete"),
key: MENU_TYPE.DELETE,
Expand Down
Loading
Loading