Main setup for Custom theme

This commit is contained in:
Shaurya Sharma 2025-03-04 02:00:19 +05:30
parent 3ae5128281
commit 45c7c60674
7 changed files with 216 additions and 38 deletions

View file

@ -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>
);

View file

@ -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>
);

View file

@ -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}

View file

@ -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';

View 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;

View file

@ -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;
}

View file

@ -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);
}