mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
test
This commit is contained in:
parent
1fa9f85e14
commit
275312698f
11 changed files with 165 additions and 4 deletions
|
@ -4,6 +4,9 @@
|
||||||
border: 1px dashed var(--color-border);
|
border: 1px dashed var(--color-border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: _.unit(3.25);
|
padding: _.unit(3.25);
|
||||||
|
transition-property: outline, border;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
transition-duration: 0.2s;
|
||||||
|
|
||||||
> input {
|
> input {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -18,6 +21,9 @@
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
transition-property: color;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
transition-duration: 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadingIcon {
|
.uploadingIcon {
|
||||||
|
@ -60,4 +66,3 @@
|
||||||
border-color: var(--color-error);
|
border-color: var(--color-error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -22,6 +22,12 @@
|
||||||
gap: _.unit(3);
|
gap: _.unit(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.branding {
|
||||||
|
section + section {
|
||||||
|
margin-top: _.unit(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mfaWarning {
|
.mfaWarning {
|
||||||
margin-top: _.unit(3);
|
margin-top: _.unit(3);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { trySubmitSafe } from '@/utils/form';
|
||||||
|
|
||||||
import { type OrganizationDetailsOutletContext } from '../types';
|
import { type OrganizationDetailsOutletContext } from '../types';
|
||||||
|
|
||||||
|
import Branding from './Branding';
|
||||||
import JitSettings from './JitSettings';
|
import JitSettings from './JitSettings';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
import { assembleData, isJsonObject, normalizeData, type FormData } from './utils';
|
import { assembleData, isJsonObject, normalizeData, type FormData } from './utils';
|
||||||
|
@ -136,6 +137,7 @@ function Settings() {
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
</FormCard>
|
</FormCard>
|
||||||
|
<Branding form={form} />
|
||||||
<JitSettings form={form} />
|
<JitSettings form={form} />
|
||||||
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleting && isDirty} />
|
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleting && isDirty} />
|
||||||
</DetailsForm>
|
</DetailsForm>
|
||||||
|
|
|
@ -38,6 +38,20 @@ const organization_details = {
|
||||||
custom_data_tip:
|
custom_data_tip:
|
||||||
'Custom data is a JSON object that can be used to store additional data associated with the organization.',
|
'Custom data is a JSON object that can be used to store additional data associated with the organization.',
|
||||||
invalid_json_object: 'Invalid JSON object.',
|
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: {
|
jit: {
|
||||||
title: 'Just-in-time provisioning',
|
title: 'Just-in-time provisioning',
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -23,7 +23,7 @@ const sign_in_exp = {
|
||||||
color: {
|
color: {
|
||||||
title: 'COLOR',
|
title: 'COLOR',
|
||||||
primary_color: 'Brand color',
|
primary_color: 'Brand color',
|
||||||
dark_primary_color: 'Brand color (Dark)',
|
dark_primary_color: 'Brand color (dark)',
|
||||||
dark_mode: 'Enable dark mode',
|
dark_mode: 'Enable dark mode',
|
||||||
dark_mode_description:
|
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.',
|
'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',
|
favicon: 'Favicon',
|
||||||
logo_image_url: 'App logo image URL',
|
logo_image_url: 'App logo image URL',
|
||||||
logo_image_url_placeholder: 'https://your.cdn.domain/logo.png',
|
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',
|
dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png',
|
||||||
logo_image: 'App logo',
|
logo_image: 'App logo',
|
||||||
dark_logo_image: 'App logo (Dark)',
|
dark_logo_image: 'App logo (Dark)',
|
||||||
|
|
|
@ -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;
|
|
@ -8,6 +8,7 @@ export * from './sentinel.js';
|
||||||
export * from './users.js';
|
export * from './users.js';
|
||||||
export * from './sso-connector.js';
|
export * from './sso-connector.js';
|
||||||
export * from './applications.js';
|
export * from './applications.js';
|
||||||
|
export * from './organization.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
configurableConnectorMetadataGuard,
|
configurableConnectorMetadataGuard,
|
||||||
|
|
43
packages/schemas/src/foundations/jsonb-types/organization.ts
Normal file
43
packages/schemas/src/foundations/jsonb-types/organization.ts
Normal 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>;
|
|
@ -10,6 +10,5 @@ create table application_sign_in_experiences (
|
||||||
terms_of_use_url varchar(2048),
|
terms_of_use_url varchar(2048),
|
||||||
privacy_policy_url varchar(2048),
|
privacy_policy_url varchar(2048),
|
||||||
display_name varchar(256),
|
display_name varchar(256),
|
||||||
|
|
||||||
primary key (tenant_id, application_id)
|
primary key (tenant_id, application_id)
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,6 +14,8 @@ create table organizations (
|
||||||
custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb,
|
custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb,
|
||||||
/** Whether multi-factor authentication configuration is required for the members of the organization. */
|
/** Whether multi-factor authentication configuration is required for the members of the organization. */
|
||||||
is_mfa_required boolean not null default false,
|
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. */
|
/** When the organization was created. */
|
||||||
created_at timestamptz not null default(now()),
|
created_at timestamptz not null default(now()),
|
||||||
primary key (id)
|
primary key (id)
|
||||||
|
|
Loading…
Reference in a new issue