17
common/resources/client/core/bootstrap-data/bootstrap-data-context.ts
Executable file
17
common/resources/client/core/bootstrap-data/bootstrap-data-context.ts
Executable file
@@ -0,0 +1,17 @@
|
||||
import {BootstrapData} from './bootstrap-data';
|
||||
import {createContext, useContext} from 'react';
|
||||
|
||||
export interface BoostrapDataContextValue<T = BootstrapData> {
|
||||
data: T;
|
||||
setBootstrapData: (data: string | T) => void;
|
||||
mergeBootstrapData: (data: Partial<T>) => void;
|
||||
invalidateBootstrapData: () => void;
|
||||
}
|
||||
|
||||
export const BoostrapDataContext = createContext<BoostrapDataContextValue>(
|
||||
null!
|
||||
);
|
||||
|
||||
export function useBootstrapData() {
|
||||
return useContext(BoostrapDataContext);
|
||||
}
|
||||
33
common/resources/client/core/bootstrap-data/bootstrap-data-provider.tsx
Executable file
33
common/resources/client/core/bootstrap-data/bootstrap-data-provider.tsx
Executable file
@@ -0,0 +1,33 @@
|
||||
import React, {useMemo} from 'react';
|
||||
import {
|
||||
invalidateBootstrapData,
|
||||
mergeBootstrapData,
|
||||
setBootstrapData,
|
||||
useBackendBootstrapData,
|
||||
} from './use-backend-bootstrap-data';
|
||||
import {
|
||||
BoostrapDataContext,
|
||||
BoostrapDataContextValue,
|
||||
} from './bootstrap-data-context';
|
||||
|
||||
interface BootstrapDataProviderProps {
|
||||
children: any;
|
||||
}
|
||||
export function BootstrapDataProvider({children}: BootstrapDataProviderProps) {
|
||||
const {data} = useBackendBootstrapData();
|
||||
|
||||
const value: BoostrapDataContextValue = useMemo(() => {
|
||||
return {
|
||||
data: data,
|
||||
setBootstrapData: setBootstrapData,
|
||||
mergeBootstrapData: mergeBootstrapData,
|
||||
invalidateBootstrapData: invalidateBootstrapData,
|
||||
};
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<BoostrapDataContext.Provider value={value}>
|
||||
{children}
|
||||
</BoostrapDataContext.Provider>
|
||||
);
|
||||
}
|
||||
20
common/resources/client/core/bootstrap-data/bootstrap-data.ts
Executable file
20
common/resources/client/core/bootstrap-data/bootstrap-data.ts
Executable file
@@ -0,0 +1,20 @@
|
||||
import {CssTheme} from '../../ui/themes/css-theme';
|
||||
import {Settings} from '../settings/settings';
|
||||
import {User} from '../../auth/user';
|
||||
import {Role} from '../../auth/role';
|
||||
import {Localization} from '../../i18n/localization';
|
||||
import {MetaTag} from '../../seo/meta-tag';
|
||||
|
||||
export interface BootstrapData {
|
||||
themes: {all: CssTheme[]; selectedThemeId?: number | string | null};
|
||||
sentry_release: string;
|
||||
is_mobile_device: boolean;
|
||||
csrf_token: string;
|
||||
settings: Settings;
|
||||
user: User | null;
|
||||
guest_role: Role | null;
|
||||
i18n: Localization;
|
||||
default_meta_tags: MetaTag[];
|
||||
show_cookie_notice: boolean;
|
||||
rendered_ssr?: boolean;
|
||||
}
|
||||
57
common/resources/client/core/bootstrap-data/use-backend-bootstrap-data.ts
Executable file
57
common/resources/client/core/bootstrap-data/use-backend-bootstrap-data.ts
Executable file
@@ -0,0 +1,57 @@
|
||||
import {apiClient, queryClient} from '../../http/query-client';
|
||||
import {BootstrapData} from './bootstrap-data';
|
||||
import {keepPreviousData, useQuery} from '@tanstack/react-query';
|
||||
|
||||
const queryKey = ['bootstrapData'];
|
||||
|
||||
export function getBootstrapData(): BootstrapData {
|
||||
return queryClient.getQueryData(queryKey)!;
|
||||
}
|
||||
|
||||
export function invalidateBootstrapData() {
|
||||
queryClient.invalidateQueries({queryKey});
|
||||
}
|
||||
|
||||
export function setBootstrapData(data: string | BootstrapData) {
|
||||
queryClient.setQueryData<BootstrapData>(
|
||||
queryKey,
|
||||
typeof data === 'string' ? decodeBootstrapData(data) : data,
|
||||
);
|
||||
}
|
||||
|
||||
export function mergeBootstrapData(partialData: Partial<BootstrapData>) {
|
||||
setBootstrapData({
|
||||
...getBootstrapData(),
|
||||
...partialData,
|
||||
});
|
||||
}
|
||||
|
||||
// set bootstrap data that was provided with initial request from backend
|
||||
const initialBootstrapData = (
|
||||
typeof window !== 'undefined' && window.bootstrapData
|
||||
? decodeBootstrapData(window.bootstrapData)
|
||||
: undefined
|
||||
) as BootstrapData;
|
||||
|
||||
// make sure initial data is available right away when accessing it via "getBootstrapData()"
|
||||
queryClient.setQueryData(queryKey, initialBootstrapData);
|
||||
|
||||
export function useBackendBootstrapData() {
|
||||
return useQuery({
|
||||
queryKey: queryKey,
|
||||
queryFn: () => fetchBootstrapData(),
|
||||
staleTime: Infinity,
|
||||
placeholderData: keepPreviousData,
|
||||
initialData: initialBootstrapData,
|
||||
});
|
||||
}
|
||||
|
||||
const fetchBootstrapData = async (): Promise<BootstrapData> => {
|
||||
return apiClient.get('bootstrap-data').then(response => {
|
||||
return decodeBootstrapData(response.data.data);
|
||||
});
|
||||
};
|
||||
|
||||
function decodeBootstrapData(data: string | BootstrapData): BootstrapData {
|
||||
return typeof data === 'string' ? JSON.parse(data) : data;
|
||||
}
|
||||
32
common/resources/client/core/common-provider.tsx
Executable file
32
common/resources/client/core/common-provider.tsx
Executable file
@@ -0,0 +1,32 @@
|
||||
import React, {StrictMode} from 'react';
|
||||
import {QueryClientProvider} from '@tanstack/react-query';
|
||||
import {domAnimation, LazyMotion} from 'framer-motion';
|
||||
import {queryClient} from '../http/query-client';
|
||||
import {SiteConfigContext} from './settings/site-config-context';
|
||||
import {SiteConfig} from '@app/site-config';
|
||||
import deepMerge from 'deepmerge';
|
||||
import {BaseSiteConfig} from './settings/base-site-config';
|
||||
import {ThemeProvider} from './theme-provider';
|
||||
import {BootstrapDataProvider} from './bootstrap-data/bootstrap-data-provider';
|
||||
|
||||
interface ProvidersProps {
|
||||
children: any;
|
||||
}
|
||||
|
||||
const mergedConfig = deepMerge(BaseSiteConfig, SiteConfig);
|
||||
|
||||
export function CommonProvider({children}: ProvidersProps) {
|
||||
return (
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<LazyMotion features={domAnimation}>
|
||||
<SiteConfigContext.Provider value={mergedConfig}>
|
||||
<BootstrapDataProvider>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</BootstrapDataProvider>
|
||||
</SiteConfigContext.Provider>
|
||||
</LazyMotion>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
14
common/resources/client/core/root-el.ts
Executable file
14
common/resources/client/core/root-el.ts
Executable file
@@ -0,0 +1,14 @@
|
||||
export let rootEl = (
|
||||
typeof document !== 'undefined'
|
||||
? document.getElementById('root') ?? document.body
|
||||
: undefined
|
||||
) as HTMLElement;
|
||||
|
||||
export let themeEl = (
|
||||
typeof document !== 'undefined' ? document.documentElement : undefined
|
||||
) as HTMLElement;
|
||||
|
||||
export function setRootEl(el: HTMLElement) {
|
||||
rootEl = el;
|
||||
themeEl = el;
|
||||
}
|
||||
36
common/resources/client/core/settings/base-site-config.ts
Executable file
36
common/resources/client/core/settings/base-site-config.ts
Executable file
@@ -0,0 +1,36 @@
|
||||
import {SiteConfigContextValue} from './site-config-context';
|
||||
import {WorkspaceInviteNotificationRenderer} from '../../workspace/notifications/workspace-invite-notification-renderer';
|
||||
import {message} from '../../i18n/message';
|
||||
|
||||
const workspaceInviteNotif =
|
||||
'Common\\Workspaces\\Notifications\\WorkspaceInvitation';
|
||||
|
||||
export const BaseSiteConfig: SiteConfigContextValue = {
|
||||
auth: {
|
||||
redirectUri: '/',
|
||||
adminRedirectUri: '/admin',
|
||||
},
|
||||
tags: {
|
||||
types: [{name: 'custom'}],
|
||||
},
|
||||
customPages: {
|
||||
types: [{type: 'default', label: message('Default')}],
|
||||
},
|
||||
notifications: {
|
||||
renderMap: {
|
||||
[workspaceInviteNotif]: WorkspaceInviteNotificationRenderer,
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
ads: [],
|
||||
},
|
||||
demo: {
|
||||
loginPageDefaults: 'singleAccount',
|
||||
},
|
||||
homepage: {
|
||||
options: [
|
||||
{label: message('Login page'), value: 'loginPage'},
|
||||
{label: message('Registration page'), value: 'registerPage'},
|
||||
],
|
||||
},
|
||||
};
|
||||
146
common/resources/client/core/settings/settings.ts
Executable file
146
common/resources/client/core/settings/settings.ts
Executable file
@@ -0,0 +1,146 @@
|
||||
import {IconTree} from '../../icons/create-svg-icon';
|
||||
|
||||
export type RecaptchaAction = 'contact' | 'register' | 'link_creation';
|
||||
|
||||
export interface Settings {
|
||||
version: string;
|
||||
branding: {
|
||||
logo_light: string;
|
||||
logo_dark: string;
|
||||
logo_light_mobile: string;
|
||||
logo_dark_mobile: string;
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
favicon: string;
|
||||
};
|
||||
menus: MenuConfig[];
|
||||
base_url: string;
|
||||
asset_url?: string;
|
||||
html_base_uri: string;
|
||||
cookie_notice: {
|
||||
enable: boolean;
|
||||
position: 'top' | 'bottom';
|
||||
button?: MenuItemConfig;
|
||||
};
|
||||
logging: {
|
||||
sentry_public?: string;
|
||||
};
|
||||
themes?: {
|
||||
default_id?: number | string | null;
|
||||
user_change: boolean;
|
||||
};
|
||||
custom_domains?: {
|
||||
default_host?: string;
|
||||
allow_select?: boolean;
|
||||
allow_all_option?: boolean;
|
||||
};
|
||||
dates: {
|
||||
format: string;
|
||||
default_timezone: string;
|
||||
};
|
||||
i18n: {
|
||||
enable: boolean;
|
||||
default_localization: string;
|
||||
};
|
||||
api?: {
|
||||
integrated: boolean;
|
||||
};
|
||||
billing: {
|
||||
integrated: boolean;
|
||||
enable: boolean;
|
||||
accepted_cards?: string | string[];
|
||||
paypal_test_mode: boolean;
|
||||
stripe_public_key?: string;
|
||||
invoice: {
|
||||
address?: string;
|
||||
notes?: string;
|
||||
};
|
||||
paypal: {
|
||||
public_key: string;
|
||||
enable: boolean;
|
||||
};
|
||||
stripe: {
|
||||
enable: boolean;
|
||||
};
|
||||
};
|
||||
notifications: {
|
||||
integrated: boolean;
|
||||
};
|
||||
notif: {
|
||||
subs: {
|
||||
integrated: boolean;
|
||||
};
|
||||
};
|
||||
site: {
|
||||
hide_docs_button: boolean;
|
||||
has_mobile_app: boolean;
|
||||
demo: boolean;
|
||||
};
|
||||
registration: {
|
||||
disable: boolean;
|
||||
policies?: MenuItemConfig[];
|
||||
};
|
||||
social: {
|
||||
envato: {
|
||||
enable: boolean;
|
||||
};
|
||||
google: {
|
||||
enable: boolean;
|
||||
};
|
||||
twitter: {
|
||||
enable: boolean;
|
||||
};
|
||||
facebook: {
|
||||
enable: boolean;
|
||||
};
|
||||
compact_buttons: boolean;
|
||||
};
|
||||
workspaces: {
|
||||
integrated: boolean;
|
||||
};
|
||||
uploads: {
|
||||
chunk_size: number;
|
||||
max_size: number;
|
||||
available_space: number;
|
||||
allowed_extensions?: string[];
|
||||
blocked_extensions?: string[];
|
||||
public_driver: string;
|
||||
uploads_driver: string;
|
||||
s3_direct_upload: boolean;
|
||||
disable_tus: boolean;
|
||||
};
|
||||
require_email_confirmation: boolean;
|
||||
single_device_login: boolean;
|
||||
mail: {
|
||||
contact_page_address: string;
|
||||
handler: string;
|
||||
};
|
||||
recaptcha?: {
|
||||
enable?: Record<RecaptchaAction, boolean>;
|
||||
site_key: string;
|
||||
};
|
||||
analytics?: {
|
||||
tracking_code?: string;
|
||||
gchart_api_key?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MenuConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
positions: string[];
|
||||
items: MenuItemConfig[];
|
||||
}
|
||||
|
||||
export interface MenuItemConfig {
|
||||
id: string;
|
||||
type: 'route' | 'link';
|
||||
order: number;
|
||||
label: string;
|
||||
action: string;
|
||||
target?: '_blank' | '_self';
|
||||
roles?: number[];
|
||||
permissions?: string[];
|
||||
settings?: Record<string, any>;
|
||||
icon?: IconTree[] | null;
|
||||
}
|
||||
72
common/resources/client/core/settings/site-config-context.ts
Executable file
72
common/resources/client/core/settings/site-config-context.ts
Executable file
@@ -0,0 +1,72 @@
|
||||
import React, {ComponentType} from 'react';
|
||||
import type {NotificationListItemProps} from '../../notifications/notification-list';
|
||||
import {MessageDescriptor} from '../../i18n/message-descriptor';
|
||||
import {User} from '@common/auth/user';
|
||||
import {SvgIconProps} from '@common/icons/svg-icon';
|
||||
|
||||
export interface AdConfig {
|
||||
slot: string;
|
||||
description: MessageDescriptor;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface TagType {
|
||||
name: string;
|
||||
system?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomPageType {
|
||||
type: string;
|
||||
label: MessageDescriptor;
|
||||
}
|
||||
|
||||
export interface HomepageOption {
|
||||
label: MessageDescriptor;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SiteConfigContextValue {
|
||||
auth: {
|
||||
redirectUri: string;
|
||||
// redirect uri to use when homepage is set to login page, to avoid infinite loop
|
||||
secondaryRedirectUri?: string;
|
||||
adminRedirectUri: string;
|
||||
getUserProfileLink?: (user: User) => string;
|
||||
registerFields?: ComponentType;
|
||||
accountSettingsPanels?: {
|
||||
icon: ComponentType<SvgIconProps>;
|
||||
label: MessageDescriptor;
|
||||
id: string;
|
||||
component: ComponentType<{user: User}>;
|
||||
}[];
|
||||
};
|
||||
notifications: {
|
||||
renderMap?: Record<string, ComponentType<NotificationListItemProps>>;
|
||||
};
|
||||
tags: {
|
||||
types: TagType[];
|
||||
};
|
||||
customPages: {
|
||||
types: CustomPageType[];
|
||||
};
|
||||
settings?: {
|
||||
showIncomingMailMethod?: boolean;
|
||||
showRecaptchaLinkSwitch?: boolean;
|
||||
};
|
||||
admin: {
|
||||
ads: AdConfig[];
|
||||
channelsDocsLink?: string;
|
||||
};
|
||||
demo: {
|
||||
loginPageDefaults: 'singleAccount' | 'randomAccount';
|
||||
email?: string;
|
||||
password?: string;
|
||||
};
|
||||
homepage: {
|
||||
options: HomepageOption[];
|
||||
};
|
||||
}
|
||||
|
||||
export const SiteConfigContext = React.createContext<SiteConfigContextValue>(
|
||||
null!,
|
||||
);
|
||||
9
common/resources/client/core/settings/use-settings.ts
Executable file
9
common/resources/client/core/settings/use-settings.ts
Executable file
@@ -0,0 +1,9 @@
|
||||
import {Settings} from './settings';
|
||||
import {useBootstrapData} from '../bootstrap-data/bootstrap-data-context';
|
||||
|
||||
export function useSettings(): Settings {
|
||||
const {
|
||||
data: {settings},
|
||||
} = useBootstrapData();
|
||||
return settings;
|
||||
}
|
||||
69
common/resources/client/core/theme-provider.tsx
Executable file
69
common/resources/client/core/theme-provider.tsx
Executable file
@@ -0,0 +1,69 @@
|
||||
import React, {useMemo} from 'react';
|
||||
import {applyThemeToDom} from '../ui/themes/utils/apply-theme-to-dom';
|
||||
import {
|
||||
ThemeId,
|
||||
ThemeSelectorContext,
|
||||
ThemeSelectorContextValue,
|
||||
} from '../ui/themes/theme-selector-context';
|
||||
import {CssTheme} from '../ui/themes/css-theme';
|
||||
import {useSettings} from './settings/use-settings';
|
||||
import {useBootstrapData} from './bootstrap-data/bootstrap-data-context';
|
||||
import {useCookie} from '@common/utils/hooks/use-cookie';
|
||||
|
||||
const STORAGE_KEY = 'be-active-theme';
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: any;
|
||||
}
|
||||
export function ThemeProvider({children}: ThemeProviderProps) {
|
||||
const {themes} = useSettings();
|
||||
const canChangeTheme = themes?.user_change;
|
||||
const {data} = useBootstrapData();
|
||||
const allThemes = useMemo(() => data.themes.all || [], [data.themes.all]);
|
||||
const initialThemeId = data.themes.selectedThemeId || undefined;
|
||||
|
||||
const [selectedThemeId, setSelectedThemeId] = useCookie(
|
||||
STORAGE_KEY,
|
||||
`${initialThemeId}`,
|
||||
);
|
||||
|
||||
let selectedTheme = canChangeTheme
|
||||
? allThemes.find(t => t.id == selectedThemeId)
|
||||
: allThemes.find(t => t.id == themes?.default_id);
|
||||
if (!selectedTheme) {
|
||||
selectedTheme = allThemes[0];
|
||||
}
|
||||
|
||||
const contextValue: ThemeSelectorContextValue = useMemo(() => {
|
||||
return {
|
||||
allThemes,
|
||||
selectedTheme: selectedTheme!,
|
||||
selectTheme: (id: ThemeId) => {
|
||||
if (!canChangeTheme) return;
|
||||
const theme = findTheme(allThemes, id);
|
||||
if (theme) {
|
||||
setSelectedThemeId(`${theme.id}`);
|
||||
applyThemeToDom(theme);
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [allThemes, selectedTheme, setSelectedThemeId, canChangeTheme]);
|
||||
|
||||
return (
|
||||
<ThemeSelectorContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ThemeSelectorContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function findTheme(themes: CssTheme[], id: ThemeId) {
|
||||
return themes.find(t => {
|
||||
if (id === 'light') {
|
||||
return t.default_light === true;
|
||||
}
|
||||
if (id === 'dark') {
|
||||
return t.default_dark === true;
|
||||
}
|
||||
return t.id === id;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user