mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 01:18:23 +00:00
Main setup for Custom theme
This commit is contained in:
parent
3ae5128281
commit
45c7c60674
7 changed files with 216 additions and 38 deletions
|
|
@ -9,6 +9,7 @@ export const Color = ({
|
|||
value,
|
||||
onChange,
|
||||
pickerStyle = {},
|
||||
colorMap = {},
|
||||
cyLabel,
|
||||
asBoxShadowPopover = true,
|
||||
meta,
|
||||
|
|
@ -112,7 +113,7 @@ export const Color = ({
|
|||
></div>
|
||||
|
||||
<div className="col tj-text-xsm p-0 color-slate12" data-cy={`${String(cyLabel)}-value`}>
|
||||
{value}
|
||||
{colorMap?.[value] ? colorMap?.[value]?.charAt(0).toUpperCase() + colorMap?.[value]?.slice(1) : value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
|
|||
import cx from 'classnames';
|
||||
import { Color } from './Color';
|
||||
import CheckIcon from '@/components/ui/Checkbox/CheckboxUtils/CheckIcon';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
export const ColorSwatches = ({
|
||||
value,
|
||||
|
|
@ -17,10 +19,17 @@ export const ColorSwatches = ({
|
|||
styleDefinition,
|
||||
}) => {
|
||||
const [componentType, setComponentType] = useState('color');
|
||||
const selectedTheme = useStore((state) => state.globalSettings.theme, shallow);
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const brandColors = selectedTheme?.definition?.brand?.colors || {};
|
||||
|
||||
return (
|
||||
<Color
|
||||
value={value}
|
||||
colorMap={Object.keys(brandColors)?.reduce((acc, colorType) => {
|
||||
acc[`var(--${colorType}-brand)`] = colorType;
|
||||
return acc;
|
||||
}, {})}
|
||||
onChange={onChange}
|
||||
pickerStyle={pickerStyle}
|
||||
cyLabel={cyLabel}
|
||||
|
|
@ -37,10 +46,15 @@ export const ColorSwatches = ({
|
|||
)}
|
||||
CustomOptionList={() => (
|
||||
<div style={{ padding: '8px' }}>
|
||||
<CustomOption />
|
||||
<CustomOption />
|
||||
<CustomOption />
|
||||
<CustomOption />
|
||||
{Object.keys(brandColors)?.map((colorType, index) => (
|
||||
<CustomOption
|
||||
color={brandColors[colorType][darkMode ? 'dark' : 'light']}
|
||||
colorType={colorType}
|
||||
key={index}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -69,13 +83,19 @@ const SwatchesToggle = ({ value, onChange }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const CustomOption = () => {
|
||||
const CustomOption = ({ onChange, colorType, color, value }) => {
|
||||
const isSelected = `var(--${colorType}-brand)` === value;
|
||||
return (
|
||||
<div className="codebuilder-color-swatches-options">
|
||||
<div
|
||||
className="codebuilder-color-swatches-options"
|
||||
onClick={() => {
|
||||
onChange(`var(--${colorType}-brand)`);
|
||||
}}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<CheckIcon size="large" fill="#4368E3" />
|
||||
<div className="color-icon" />
|
||||
<span style={{ marginLeft: '5px' }}>Test</span>
|
||||
{isSelected && <CheckIcon size="large" fill="#4368E3" />}
|
||||
<div className="color-icon" style={{ backgroundColor: color, marginLeft: !isSelected && '20px' }} />
|
||||
<span style={{ marginLeft: '5px' }}>{colorType.charAt(0).toUpperCase() + colorType.slice(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,56 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Select from '@/_ui/Select';
|
||||
import CheckMark from '@/_ui/Icon/bulkIcons/CheckMark';
|
||||
import { components } from 'react-select';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getWorkspaceId } from '@/_helpers/utils';
|
||||
import { appThemesService } from '../../../../ee/modules/WorkspaceSettings/pages/ManageThemes/service/app_themes.service';
|
||||
|
||||
const ThemeSelect = ({ darkMode }) => {
|
||||
const [themesList, setThemesList] = useState([]);
|
||||
const selectedTheme = useStore((state) => state.globalSettings.theme, shallow);
|
||||
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
|
||||
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
|
||||
const globalSettingsChanged = useStore((state) => state.globalSettingsChanged, shallow);
|
||||
const workspaceId = getWorkspaceId();
|
||||
const appId = useStore((state) => state.app.appId, shallow);
|
||||
const versionId = useStore((state) => state.currentVersionId, shallow);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchAllThemes = async () => {
|
||||
const themes = await appThemesService.fetchAllThemes();
|
||||
|
||||
const options = themes.map((theme) => ({
|
||||
value: theme.id,
|
||||
name: theme.name,
|
||||
label: theme.name,
|
||||
color: theme?.definition?.brand?.colors?.primary?.[darkMode ? 'dark' : 'light'],
|
||||
isDefault: theme?.isDefault,
|
||||
theme: theme,
|
||||
}));
|
||||
|
||||
setThemesList(options);
|
||||
};
|
||||
|
||||
const setTheme = async (themeId) => {
|
||||
await appThemesService.updateAppTheme(appId, versionId, themeId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllThemes();
|
||||
}, []);
|
||||
|
||||
const customSelectStyles = {
|
||||
control: (provided) => ({
|
||||
...provided,
|
||||
width: '158px',
|
||||
height: '32px',
|
||||
minHeight: '32px',
|
||||
flexWrap: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
input: (provided) => ({
|
||||
...provided,
|
||||
|
|
@ -37,6 +77,10 @@ const ThemeSelect = ({ darkMode }) => {
|
|||
scrollbarWidth: 'none', // Hide scrollbar for Firefox
|
||||
borderRadius: '8px',
|
||||
}),
|
||||
menuPortal: (base) => ({
|
||||
...base,
|
||||
top: base.top + 2, // Adjust the top position
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
backgroundColor: state.isFocused
|
||||
|
|
@ -54,22 +98,44 @@ const ThemeSelect = ({ darkMode }) => {
|
|||
|
||||
const CustomOption = (props) => {
|
||||
const { data, isSelected } = props;
|
||||
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center', // Ensures vertical alignment
|
||||
gap: '10px', // Space between icon and text
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
gap: '2px',
|
||||
height: '30px',
|
||||
}}
|
||||
>
|
||||
{isSelected && (
|
||||
<CheckMark fill="transparent" fillIcon={'var(--primary-brand)'} className="datepicker-select-check" />
|
||||
<CheckMark
|
||||
width="20px"
|
||||
fill="transparent"
|
||||
fillIcon={'var(--primary-brand)'}
|
||||
className="datepicker-select-check"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="color-icon"
|
||||
style={{ backgroundColor: data?.color, marginLeft: isSelected ? '0px' : '22px' }}
|
||||
/>
|
||||
<span style={{ fontSize: '12px', marginLeft: '2px', color: darkMode ? '#fff' : '#000' }}>{data.label}</span>
|
||||
{data?.isDefault && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: '10px',
|
||||
display: 'inline-flex', // Enables flexbox on the span
|
||||
alignItems: 'center', // Vertically centers the text
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
className="theme-default-pill"
|
||||
>
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
<div className="color-icon" />
|
||||
<span style={{ fontSize: '12px', marginLeft: '5px', color: darkMode ? '#fff' : '#000' }}>{data.label}</span>
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
|
|
@ -92,10 +158,12 @@ const ThemeSelect = ({ darkMode }) => {
|
|||
}}
|
||||
>
|
||||
<ButtonSolid
|
||||
onClick={() => {}}
|
||||
onClick={() => {
|
||||
navigate(`/${workspaceId}/workspace-settings/themes`);
|
||||
}}
|
||||
variant="tertiary"
|
||||
leftIcon="addrectangle"
|
||||
fill="var(--primary-brand)"
|
||||
fill="#3e63dd"
|
||||
iconWidth="16"
|
||||
className="tj-text-xsm theme-create-btn"
|
||||
>
|
||||
|
|
@ -112,13 +180,14 @@ const ThemeSelect = ({ darkMode }) => {
|
|||
<p className="tj-text-xsm color-slate12 w-full m-auto">Theme</p>
|
||||
</div>
|
||||
<Select
|
||||
options={[
|
||||
{ name: 'Authorization code', value: 'authorization_code' },
|
||||
{ name: 'Client credentials', value: 'client_credentials' },
|
||||
]}
|
||||
value={'authorization_code'}
|
||||
onChange={(value) => {}}
|
||||
options={themesList}
|
||||
value={selectedTheme?.id}
|
||||
onChange={(themeId) => {
|
||||
setTheme(themeId);
|
||||
globalSettingsChanged({ theme: themesList.find((theme) => theme.value === themeId)?.theme });
|
||||
}}
|
||||
width={'100%'}
|
||||
isDisabled={!licenseValid || !featureAccess?.customThemes}
|
||||
useMenuPortal={true}
|
||||
styles={customSelectStyles}
|
||||
useCustomStyles={true}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { distinctUntilChanged } from 'rxjs';
|
|||
import { convertAllKeysToSnakeCase } from '../_stores/utils';
|
||||
import { getPreviewQueryParams } from '@/_helpers/routes';
|
||||
import { useLocation, useMatch, useParams } from 'react-router-dom';
|
||||
import useThemeAccess from './useThemeAccess';
|
||||
|
||||
/**
|
||||
* this is to normalize the query transformation options to match the expected schema. Takes care of corrupted data.
|
||||
|
|
@ -101,12 +102,14 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
|
|||
const selectedEnvironment = useStore((state) => state.selectedEnvironment);
|
||||
const setIsEditorFreezed = useStore((state) => state.setIsEditorFreezed);
|
||||
const appMode = useStore((state) => state.globalSettings.appMode);
|
||||
const selectedTheme = useStore((state) => state.globalSettings.theme);
|
||||
const previousEnvironmentId = usePrevious(selectedEnvironment?.id);
|
||||
const isComponentLayoutReady = useStore((state) => state.isComponentLayoutReady, shallow);
|
||||
const pageSwitchInProgress = useStore((state) => state.pageSwitchInProgress);
|
||||
const setPageSwitchInProgress = useStore((state) => state.setPageSwitchInProgress);
|
||||
const selectedVersion = useStore((state) => state.selectedVersion);
|
||||
const setIsPublicAccess = useStore((state) => state.setIsPublicAccess);
|
||||
const themeAccess = useThemeAccess();
|
||||
|
||||
const setConversation = useStore((state) => state.ai?.setConversation);
|
||||
const setDocsConversation = useStore((state) => state.ai?.setDocsConversation);
|
||||
|
|
@ -428,6 +431,16 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
|
|||
fetchAndSetWindowTitle({ page: pageTitles.EDITOR, appName: app.appName });
|
||||
}, [app.appName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!themeAccess) return;
|
||||
const root = document.documentElement;
|
||||
const brandColors = selectedTheme?.definition?.brand?.colors || {};
|
||||
Object.keys(brandColors).forEach((colorType) => {
|
||||
const color = brandColors[colorType][darkMode ? 'dark' : 'light'];
|
||||
root.style.setProperty(`--${colorType}-brand`, color);
|
||||
});
|
||||
}, [darkMode, selectedTheme, themeAccess]);
|
||||
|
||||
useEffect(() => {
|
||||
const exposedTheme =
|
||||
appMode && appMode !== 'auto' ? appMode : localStorage.getItem('darkMode') === 'true' ? 'dark' : 'light';
|
||||
|
|
|
|||
10
frontend/src/AppBuilder/_hooks/useThemeAccess.js
Normal file
10
frontend/src/AppBuilder/_hooks/useThemeAccess.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
const useThemeAccess = () => {
|
||||
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
|
||||
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
|
||||
return licenseValid && featureAccess?.customThemes;
|
||||
};
|
||||
|
||||
export default useThemeAccess;
|
||||
|
|
@ -6110,11 +6110,22 @@ fieldset:disabled .btn {
|
|||
vertical-align: text-bottom;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: .75s linear infinite spinner-border;
|
||||
animation: .75s linear infinite spinner-border
|
||||
}
|
||||
|
||||
.spinner-border-filepicker {
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
vertical-align: text-bottom;
|
||||
border: 2px solid var(--primary-brand);
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: .75s linear infinite spinner-border;
|
||||
animation: .75s linear infinite spinner-border
|
||||
}
|
||||
.spinner-light {
|
||||
color: var(--indigo9) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1655,7 +1655,7 @@ button {
|
|||
.pagination {
|
||||
.page-item.active {
|
||||
a.page-link {
|
||||
background-color: $primary-light;
|
||||
background-color: var(--primary-brand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2475,32 +2475,32 @@ body {
|
|||
.CalendarDay__selected,
|
||||
.CalendarDay__selected:active,
|
||||
.CalendarDay__selected:hover {
|
||||
background: $primary;
|
||||
border: 1px double $primary;
|
||||
background: var(--primary-brand);
|
||||
border: 1px double var(--primary-brand);
|
||||
}
|
||||
|
||||
.CalendarDay__selected_span {
|
||||
background: $primary;
|
||||
border: $primary;
|
||||
background: var(--primary-brand);
|
||||
border: var(--primary-brand);
|
||||
}
|
||||
|
||||
.CalendarDay__selected_span:active,
|
||||
.CalendarDay__selected_span:hover {
|
||||
background: $primary;
|
||||
border: 1px double $primary;
|
||||
background: var(--primary-brand);
|
||||
border: 1px double var(--primary-brand);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.CalendarDay__hovered_span:active,
|
||||
.CalendarDay__hovered_span:hover {
|
||||
background: $primary;
|
||||
border: 1px double $primary;
|
||||
background: var(--primary-brand);
|
||||
border: 1px double var(--primary-brand);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.CalendarDay__hovered_span {
|
||||
background: #83b8e7;
|
||||
border: 1px double #83b8e7;
|
||||
background: var(--primary-brand);
|
||||
border: 1px double var(--primary-brand);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
|
@ -3858,7 +3858,7 @@ input[type="text"] {
|
|||
}
|
||||
|
||||
.react-datepicker__day--selected {
|
||||
background-color: $primary-light;
|
||||
background-color: var(--primary-brand);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4111,6 +4111,8 @@ input[type="text"] {
|
|||
.rbc-event-label {
|
||||
display: none;
|
||||
}
|
||||
background-color: var(--primary-brand) !important;
|
||||
border: transparent
|
||||
}
|
||||
|
||||
.rbc-off-range-bg {
|
||||
|
|
@ -4402,6 +4404,14 @@ input[type="text"] {
|
|||
}
|
||||
}
|
||||
|
||||
.color-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
border: 1.5px solid #CCD1D5;
|
||||
background-color: #0091FF;
|
||||
}
|
||||
|
||||
.portal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -18625,4 +18635,48 @@ section.ai-message-prompt-input-wrapper {
|
|||
height:32px;
|
||||
color:#000;
|
||||
border: 1px solid var(--Border-brand-weak, #97AEFC);
|
||||
}
|
||||
|
||||
|
||||
.theme-default-pill {
|
||||
font-size: 11px;
|
||||
background-color: #CCD1D54D;
|
||||
color: #6A727C;
|
||||
width: 49px;
|
||||
height: 18px;
|
||||
border-radius: 20px
|
||||
}
|
||||
|
||||
.no-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.textarea-widget:focus {
|
||||
border-color: var(--primary-brand);
|
||||
}
|
||||
|
||||
.multiselct-widget-option{
|
||||
input:checked {
|
||||
background-color: var(--primary-brand);
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-box {
|
||||
.options{
|
||||
input:checked {
|
||||
background-color: var(--primary-brand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timer-btn {
|
||||
background-color: var(--primary-brand);
|
||||
&:hover {
|
||||
background-color: var(--primary-brand);
|
||||
}
|
||||
}
|
||||
|
||||
.timer-btn-hover:hover {
|
||||
background-color: var(--primary-brand);
|
||||
}
|
||||
Loading…
Reference in a new issue