0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00
This commit is contained in:
Gao Sun 2024-07-06 17:50:39 +08:00
parent 1fa9f85e14
commit 275312698f
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
11 changed files with 165 additions and 4 deletions

View file

@ -4,6 +4,9 @@
border: 1px dashed var(--color-border);
border-radius: 8px;
padding: _.unit(3.25);
transition-property: outline, border;
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
> input {
display: none;
@ -18,6 +21,9 @@
width: 20px;
height: 20px;
color: var(--color-text-secondary);
transition-property: color;
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
}
.uploadingIcon {
@ -60,4 +66,3 @@
border-color: var(--color-error);
}
}

View file

@ -0,0 +1,71 @@
import { Theme } from '@logto/schemas';
import { noop } from '@silverhand/essentials';
import { Controller, type UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import FormCard from '@/components/FormCard';
import ColorPicker from '@/ds-components/ColorPicker';
import FormField from '@/ds-components/FormField';
import ImageUploader from '@/ds-components/Uploader/ImageUploader';
import * as styles from './index.module.scss';
import { type FormData } from './utils';
type Props = {
readonly form: UseFormReturn<FormData>;
};
function Branding({ form }: Props) {
const {
control,
formState: { errors },
setError,
clearErrors,
watch,
} = form;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<FormCard
title="organization_details.branding.title"
description="organization_details.branding.description"
>
<div className={styles.branding}>
{Object.values(Theme).map((theme) => (
<section key={theme}>
<FormField title={`organization_details.branding.${theme}.primary_color`}>
<Controller
name="branding.light.primaryColor"
control={control}
render={({ field: { onChange, value } }) => (
<ColorPicker value={value} onChange={onChange} />
)}
/>
</FormField>
<FormField title={`organization_details.branding.${theme}.logo`}>
<Controller
control={control}
name="branding.light.logoUrl"
render={({ field: { onChange, value, name } }) => (
<ImageUploader
name={name}
value={value ?? ''}
actionDescription={t('organization_details.branding.logo_upload_description')}
onCompleted={onChange}
// OnUploadErrorChange={setUploadLogoError}
onUploadErrorChange={noop}
onDelete={() => {
onChange('');
}}
/>
)}
/>
</FormField>
</section>
))}
</div>
</FormCard>
);
}
export default Branding;

View file

@ -22,6 +22,12 @@
gap: _.unit(3);
}
.branding {
section + section {
margin-top: _.unit(6);
}
}
.mfaWarning {
margin-top: _.unit(3);
}

View file

@ -20,6 +20,7 @@ import { trySubmitSafe } from '@/utils/form';
import { type OrganizationDetailsOutletContext } from '../types';
import Branding from './Branding';
import JitSettings from './JitSettings';
import * as styles from './index.module.scss';
import { assembleData, isJsonObject, normalizeData, type FormData } from './utils';
@ -136,6 +137,7 @@ function Settings() {
)}
</FormField>
</FormCard>
<Branding form={form} />
<JitSettings form={form} />
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleting && isDirty} />
</DetailsForm>

View file

@ -38,6 +38,20 @@ const organization_details = {
custom_data_tip:
'Custom data is a JSON object that can be used to store additional data associated with the organization.',
invalid_json_object: 'Invalid JSON object.',
branding: {
title: 'Branding',
description:
'Customize the branding of the organization. The branding can be used in the sign-in experience or for your own reference.',
light: {
primary_color: 'Primary color',
logo: 'Organization logo',
},
dark: {
primary_color: 'Primary color (dark)',
logo: 'Organization logo (dark)',
},
logo_upload_description: 'Click or drop an image to upload',
},
jit: {
title: 'Just-in-time provisioning',
description:

View file

@ -23,7 +23,7 @@ const sign_in_exp = {
color: {
title: 'COLOR',
primary_color: 'Brand color',
dark_primary_color: 'Brand color (Dark)',
dark_primary_color: 'Brand color (dark)',
dark_mode: 'Enable dark mode',
dark_mode_description:
'Your app will have an auto-generated dark mode theme based on your brand color and Logto algorithm. You are free to customize.',
@ -36,7 +36,7 @@ const sign_in_exp = {
favicon: 'Favicon',
logo_image_url: 'App logo image URL',
logo_image_url_placeholder: 'https://your.cdn.domain/logo.png',
dark_logo_image_url: 'App logo image URL (Dark)',
dark_logo_image_url: 'App logo image URL (dark)',
dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png',
logo_image: 'App logo',
dark_logo_image: 'App logo (Dark)',

View file

@ -0,0 +1,18 @@
import { sql } from '@silverhand/slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
alter table organizations add column branding jsonb not null default '{}'::jsonb;
`);
},
down: async (pool) => {
await pool.query(sql`
alter table organizations drop column branding;
`);
},
};
export default alteration;

View file

@ -8,6 +8,7 @@ export * from './sentinel.js';
export * from './users.js';
export * from './sso-connector.js';
export * from './applications.js';
export * from './organization.js';
export {
configurableConnectorMetadataGuard,

View file

@ -0,0 +1,43 @@
import { hexColorRegEx } from '@logto/core-kit';
import { z } from 'zod';
import { type Theme } from '../../types/theme.js';
import { type ToZodObject } from '../../utils/zod.js';
/** Organization branding data for a specific theme. */
export type OrganizationBrandingTheme = Partial<{
logoUrl: string;
/**
* The primary color of the organization. Should be a hex color.
*
* @example
* '#ff0000'
* '#f00'
*/
primaryColor: string;
}>;
/** Zod representation of {@link OrganizationBrandingTheme}. */
export const organizationBrandingThemeGuard = z
.object({
logoUrl: z.string().url(),
primaryColor: z.string().regex(hexColorRegEx),
})
.partial() satisfies ToZodObject<OrganizationBrandingTheme>;
/** Organization branding data for all themes. */
export type OrganizationBranding = Partial<
Record<Theme, OrganizationBrandingTheme> & {
/** URL to the favicon of the organization. */
faviconUrl: string;
}
>;
/** Zod representation of {@link OrganizationBranding}. */
export const organizationBrandingGuard = z
.object({
light: organizationBrandingThemeGuard,
dark: organizationBrandingThemeGuard,
faviconUrl: z.string().url(),
})
.partial() satisfies ToZodObject<OrganizationBranding>;

View file

@ -10,6 +10,5 @@ create table application_sign_in_experiences (
terms_of_use_url varchar(2048),
privacy_policy_url varchar(2048),
display_name varchar(256),
primary key (tenant_id, application_id)
);

View file

@ -14,6 +14,8 @@ create table organizations (
custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb,
/** Whether multi-factor authentication configuration is required for the members of the organization. */
is_mfa_required boolean not null default false,
/** The organization's branding configuration. */
branding jsonb /* @use OrganizationBranding */ not null default '{}'::jsonb,
/** When the organization was created. */
created_at timestamptz not null default(now()),
primary key (id)