0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

Merge branch 'master' into gao-add-admin-tenant

This commit is contained in:
Gao Sun 2023-02-15 15:00:04 +08:00
commit 40173bb5e0
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
350 changed files with 5252 additions and 6936 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/connector-kit": minor
---
`getSession` and `setSession` are actually used as REQUIRED parameters, update interface definition.

View file

@ -0,0 +1,7 @@
---
"@logto/connector-kit": minor
---
Add optional `formItems` to connector's metadata.
If set, the admin console's connector page (both create and update) will use it to generate a form to input config instead of raw JSON.

View file

@ -18,6 +18,7 @@
"@logto/language-kit": "1.0.0-rc.0"
},
"changesets": [
"big-turkeys-invite",
"breezy-socks-joke",
"clever-panthers-lay",
"curly-hornets-end",

View file

@ -42,12 +42,18 @@ Boringly, we call it "[customer identity access management](https://en.wikipedia
- Visit our 🎨 [website](https://logto.io) for a brief introduction if you are new to Logto.
- A step-by-step guide is available on 📖 [docs.logto.io](https://docs.logto.io).
### Interactive demo
[![Uffizzi](https://cdn.uffizzi.com/demo-button.svg)](https://app.uffizzi.com/demo/github.com/logto-io/logto)
Recommended. Click and wait for a few seconds to start exploring Logto in your own browser!
[![GitPod](https://raw.githubusercontent.com/gitpod-io/gitpod/30da76375c996109f243491b23e47feefab7217f/components/dashboard/public/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/logto-io/demo)
If you launch Logto via GitPod, please wait until you see the message like `App is running at https://3001-...gitpod.io` in the terminal, press Cmd (or Ctrl on Windows) and click the URL to continue your Logto journey.
### Launch Logto
#### Online demo (GitPod)
[Click here](https://gitpod.io/#https://github.com/logto-io/demo) to launch Logto via GitPod. Once you see the message like `App is running at https://3001-...gitpod.io` in the terminal, press Cmd (or Ctrl) and click the URL to continue your Logto journey.
#### Docker Compose
Docker Compose CLI usually comes with [Docker Desktop](https://www.docker.com/products/docker-desktop).
@ -71,6 +77,14 @@ npm init @logto
const languages = ['Deutsch', 'English', 'Français', 'Português', '简体中文', 'Türkçe', '한국어'];
```
## Web compatibility
Logto uses the [default browserlist config](https://github.com/browserslist/browserslist#full-list) to compile frontend projects, which is:
```
> 0.5%, last 2 versions, Firefox ESR, not dead
```
## Bug report, feature request, feedback
- Our team takes security seriously, especially when it relates to identity. If you find any existing or potential security issues, please do not hesitate to email 🔒 [security@logto.io](mailto:security@logto.io).

View file

@ -0,0 +1,37 @@
# This compose file is for demonstration only, do not use in prod.
version: "3.9"
x-uffizzi:
ingress:
service: app
port: 3001
services:
app:
depends_on:
- "postgres"
image: registry.uffizzi.com/logto-io/logto:prerelease
ports:
- 3001:3001
environment:
TRUST_PROXY_HEADER: 1
DB_URL: postgres://postgres:p0stgr3s@localhost:5432/logto
deploy:
resources:
limits:
memory: 2000M
entrypoint: /bin/sh
command:
- "-c"
- "npm run cli db seed -- --swe && ENDPOINT=$$UFFIZZI_URL npm start"
postgres:
image: postgres:14-alpine
user: postgres
environment:
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "p0stgr3s"
deploy:
resources:
limits:
memory: 500M

View file

@ -1,6 +1,6 @@
import type { CommandModule } from 'yargs';
import { log } from '../../utilities.js';
import { log } from '../../utils.js';
import { addConnectors, addOfficialConnectors, inquireInstancePath } from './utils.js';
const add: CommandModule<

View file

@ -1,7 +1,7 @@
import chalk from 'chalk';
import type { CommandModule } from 'yargs';
import type { ConnectorPackage } from '../../utilities.js';
import type { ConnectorPackage } from '../../utils.js';
import { getConnectorPackagesFrom, isOfficialConnector } from './utils.js';
const logConnectorNames = (type: string, packages: ConnectorPackage[]) => {

View file

@ -3,7 +3,7 @@ import fs from 'fs/promises';
import chalk from 'chalk';
import type { CommandModule } from 'yargs';
import { log } from '../../utilities.js';
import { log } from '../../utils.js';
import { getConnectorPackagesFrom } from './utils.js';
const remove: CommandModule<{ path?: string }, { path?: string; packages?: string[] }> = {

View file

@ -14,7 +14,7 @@ import tar from 'tar';
import { z } from 'zod';
import { connectorDirectory } from '../../constants.js';
import { getConnectorPackagesFromDirectory, isTty, log, oraPromise } from '../../utilities.js';
import { getConnectorPackagesFromDirectory, isTty, log, oraPromise } from '../../utils.js';
import { defaultPath } from '../install/utils.js';
const coreDirectory = 'packages/core';

View file

@ -9,7 +9,7 @@ import {
getCurrentDatabaseAlterationTimestamp,
updateDatabaseTimestamp,
} from '../../../queries/system.js';
import { log } from '../../../utilities.js';
import { log } from '../../../utils.js';
import type { AlterationFile } from './type.js';
import { getAlterationFiles, getTimestampFromFilename } from './utils.js';
import { chooseAlterationsByVersion, chooseRevertAlterationsByVersion } from './version.js';

View file

@ -5,7 +5,7 @@ import path from 'path';
import { findPackage } from '@logto/shared';
import { getPathInModule } from '../../../utilities.js';
import { getPathInModule } from '../../../utils.js';
import type { AlterationFile } from './type.js';
const currentDirname = path.dirname(fileURLToPath(import.meta.url));

View file

@ -3,7 +3,7 @@ import chalk from 'chalk';
import inquirer from 'inquirer';
import { SemVer, compare, eq, gt } from 'semver';
import { findLastIndex, isTty, log } from '../../../utilities.js';
import { findLastIndex, isTty, log } from '../../../utils.js';
import type { AlterationFile } from './type.js';
const getVersionStringFromFilename = (filename: string) =>

View file

@ -11,8 +11,8 @@ import type { CommandModule } from 'yargs';
import { createPoolFromConfig } from '../../database.js';
import { getRowsByKeys, updateValueByKey } from '../../queries/logto-config.js';
import { log } from '../../utilities.js';
import { generateOidcCookieKey, generateOidcPrivateKey } from './utilities.js';
import { log } from '../../utils.js';
import { generateOidcCookieKey, generateOidcPrivateKey } from './utils.js';
const validKeysDisplay = chalk.green(logtoConfigKeys.join(', '));

View file

@ -4,7 +4,7 @@ import type { CommandModule } from 'yargs';
import { createPoolAndDatabaseIfNeeded } from '../../../database.js';
import { doesConfigsTableExist } from '../../../queries/logto-config.js';
import { log, oraPromise } from '../../../utilities.js';
import { log, oraPromise } from '../../../utils.js';
import { getLatestAlterationTimestamp } from '../alteration/index.js';
import { getAlterationDirectory } from '../alteration/utils.js';
import { createTables, seedTables } from './tables.js';

View file

@ -8,8 +8,8 @@ import type { DatabaseTransactionConnection } from 'slonik';
import { z } from 'zod';
import { getRowsByKeys, updateValueByKey } from '../../../queries/logto-config.js';
import { log } from '../../../utilities.js';
import { generateOidcCookieKey, generateOidcPrivateKey } from '../utilities.js';
import { log } from '../../../utils.js';
import { generateOidcCookieKey, generateOidcPrivateKey } from '../utils.js';
const isBase64FormatPrivateKey = (key: string) => !key.includes('-');

View file

@ -20,7 +20,7 @@ import { raw } from 'slonik-sql-tag-raw';
import { insertInto } from '../../../database.js';
import { getDatabaseName } from '../../../queries/database.js';
import { updateDatabaseTimestamp } from '../../../queries/system.js';
import { getPathInModule } from '../../../utilities.js';
import { getPathInModule } from '../../../utils.js';
import { seedOidcConfigs } from './oidc-config.js';
import { createTenant, seedAdminData } from './tenant.js';

View file

@ -2,7 +2,7 @@ import chalk from 'chalk';
import type { CommandModule } from 'yargs';
import { getDatabaseUrlFromConfig } from '../../database.js';
import { log } from '../../utilities.js';
import { log } from '../../utils.js';
import {
validateNodeVersion,
inquireInstancePath,

View file

@ -20,7 +20,7 @@ import {
log,
oraPromise,
safeExecSync,
} from '../../utilities.js';
} from '../../utils.js';
import { seedByPool } from '../database/seed/index.js';
export const defaultPath = path.join(os.homedir(), 'logto');

View file

@ -6,7 +6,7 @@ import { createPool, parseDsn, sql, stringifyDsn } from 'slonik';
import { createInterceptors } from 'slonik-interceptor-preset';
import { z } from 'zod';
import { ConfigKey, getCliConfigWithPrompt, log } from './utilities.js';
import { ConfigKey, getCliConfigWithPrompt, log } from './utils.js';
export const defaultDatabaseUrl = 'postgresql://localhost:5432/logto';

View file

@ -7,7 +7,7 @@ import connector from './commands/connector/index.js';
import database from './commands/database/index.js';
import install from './commands/install/index.js';
import { packageJson } from './package-json.js';
import { cliConfig, ConfigKey } from './utilities.js';
import { cliConfig, ConfigKey } from './utils.js';
void yargs(hideBin(process.argv))
.version(false)

View file

@ -2,8 +2,8 @@ import { AlterationStateKey, Systems } from '@logto/schemas';
import { convertToIdentifiers } from '@logto/shared';
import { createMockPool, createMockQueryResult, sql } from 'slonik';
import type { QueryType } from '../test-utilities.js';
import { expectSqlAssert } from '../test-utilities.js';
import type { QueryType } from '../test-utils.js';
import { expectSqlAssert } from '../test-utils.js';
import { updateDatabaseTimestamp, getCurrentDatabaseAlterationTimestamp } from './system.js';
const { jest } = import.meta;

View file

@ -19,6 +19,7 @@
},
"devDependencies": {
"@fontsource/roboto-mono": "^4.5.7",
"@logto/connector-kit": "workspace:*",
"@logto/core-kit": "workspace:*",
"@logto/language-kit": "workspace:*",
"@logto/phrases": "workspace:*",
@ -43,6 +44,7 @@
"@types/react-dom": "^18.0.0",
"@types/react-modal": "^3.13.1",
"@types/react-syntax-highlighter": "^15.5.1",
"buffer": "^5.7.1",
"classnames": "^2.3.1",
"clean-deep": "^3.4.0",
"cross-env": "^7.0.3",

View file

@ -52,13 +52,15 @@ import AppEndpointsProvider, { AppEndpointsContext } from './containers/AppEndpo
import ApiResourcePermissions from './pages/ApiResourceDetails/ApiResourcePermissions';
import ApiResourceSettings from './pages/ApiResourceDetails/ApiResourceSettings';
import CloudPreview from './pages/CloudPreview';
import CloudPreviewWelcome from './pages/CloudPreview/pages/Welcome';
import { CloudPreviewPage } from './pages/CloudPreview/types';
import RolePermissions from './pages/RoleDetails/RolePermissions';
import RoleSettings from './pages/RoleDetails/RoleSettings';
import RoleUsers from './pages/RoleDetails/RoleUsers';
import UserLogs from './pages/UserDetails/UserLogs';
import UserRoles from './pages/UserDetails/UserRoles';
import UserSettings from './pages/UserDetails/UserSettings';
import { getBasename } from './utilities/router';
import { getBasename } from './utils/router';
void initI18n();
@ -79,7 +81,10 @@ const Main = () => {
<Route path="callback" element={<Callback />} />
<Route path="welcome" element={<Welcome />} />
<Route element={<AppLayout />}>
<Route path="/cloud-preview" element={<CloudPreview />} />
<Route path="cloud-preview" element={<CloudPreview />}>
<Route index element={<Navigate replace to={CloudPreviewPage.Welcome} />} />
<Route path={CloudPreviewPage.Welcome} element={<CloudPreviewWelcome />} />
</Route>
<Route element={<AppContent />}>
<Route path="*" element={<NotFound />} />
<Route path="get-started" element={<GetStarted />} />

View file

@ -0,0 +1,3 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.1665 6.66675H12.9998C13.2209 6.66675 13.4328 6.57895 13.5891 6.42267C13.7454 6.26639 13.8332 6.05443 13.8332 5.83342C13.8332 5.6124 13.7454 5.40044 13.5891 5.24416C13.4328 5.08788 13.2209 5.00008 12.9998 5.00008H12.1665C11.9455 5.00008 11.7335 5.08788 11.5772 5.24416C11.421 5.40044 11.3332 5.6124 11.3332 5.83342C11.3332 6.05443 11.421 6.26639 11.5772 6.42267C11.7335 6.57895 11.9455 6.66675 12.1665 6.66675ZM12.1665 10.0001H12.9998C13.2209 10.0001 13.4328 9.91228 13.5891 9.756C13.7454 9.59972 13.8332 9.38776 13.8332 9.16675C13.8332 8.94573 13.7454 8.73377 13.5891 8.57749C13.4328 8.42121 13.2209 8.33342 12.9998 8.33342H12.1665C11.9455 8.33342 11.7335 8.42121 11.5772 8.57749C11.421 8.73377 11.3332 8.94573 11.3332 9.16675C11.3332 9.38776 11.421 9.59972 11.5772 9.756C11.7335 9.91228 11.9455 10.0001 12.1665 10.0001ZM7.99984 6.66675H8.83317C9.05418 6.66675 9.26615 6.57895 9.42243 6.42267C9.57871 6.26639 9.6665 6.05443 9.6665 5.83342C9.6665 5.6124 9.57871 5.40044 9.42243 5.24416C9.26615 5.08788 9.05418 5.00008 8.83317 5.00008H7.99984C7.77882 5.00008 7.56686 5.08788 7.41058 5.24416C7.2543 5.40044 7.1665 5.6124 7.1665 5.83342C7.1665 6.05443 7.2543 6.26639 7.41058 6.42267C7.56686 6.57895 7.77882 6.66675 7.99984 6.66675ZM7.99984 10.0001H8.83317C9.05418 10.0001 9.26615 9.91228 9.42243 9.756C9.57871 9.59972 9.6665 9.38776 9.6665 9.16675C9.6665 8.94573 9.57871 8.73377 9.42243 8.57749C9.26615 8.42121 9.05418 8.33342 8.83317 8.33342H7.99984C7.77882 8.33342 7.56686 8.42121 7.41058 8.57749C7.2543 8.73377 7.1665 8.94573 7.1665 9.16675C7.1665 9.38776 7.2543 9.59972 7.41058 9.756C7.56686 9.91228 7.77882 10.0001 7.99984 10.0001ZM17.9998 16.6667H17.1665V2.50008C17.1665 2.27907 17.0787 2.06711 16.9224 1.91083C16.7661 1.75455 16.5542 1.66675 16.3332 1.66675H4.6665C4.44549 1.66675 4.23353 1.75455 4.07725 1.91083C3.92097 2.06711 3.83317 2.27907 3.83317 2.50008V16.6667H2.99984C2.77882 16.6667 2.56686 16.7545 2.41058 16.9108C2.2543 17.0671 2.1665 17.2791 2.1665 17.5001C2.1665 17.7211 2.2543 17.9331 2.41058 18.0893C2.56686 18.2456 2.77882 18.3334 2.99984 18.3334H17.9998C18.2209 18.3334 18.4328 18.2456 18.5891 18.0893C18.7454 17.9331 18.8332 17.7211 18.8332 17.5001C18.8332 17.2791 18.7454 17.0671 18.5891 16.9108C18.4328 16.7545 18.2209 16.6667 17.9998 16.6667ZM11.3332 16.6667H9.6665V13.3334H11.3332V16.6667ZM15.4998 16.6667H12.9998V12.5001C12.9998 12.2791 12.912 12.0671 12.7558 11.9108C12.5995 11.7545 12.3875 11.6667 12.1665 11.6667H8.83317C8.61216 11.6667 8.4002 11.7545 8.24392 11.9108C8.08764 12.0671 7.99984 12.2791 7.99984 12.5001V16.6667H5.49984V3.33341H15.4998V16.6667Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,3 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.85 7.67495C15.3403 6.50479 14.4608 5.53411 13.3465 4.91174C12.2321 4.28938 10.9444 4.04967 9.68076 4.22938C8.41711 4.40909 7.24728 4.9983 6.35063 5.90666C5.45398 6.81503 4.88 7.9924 4.7167 9.25828C3.92219 9.44855 3.22522 9.92397 2.75815 10.5943C2.29108 11.2646 2.08643 12.0831 2.18308 12.8943C2.27973 13.7055 2.67094 14.453 3.28242 14.9948C3.8939 15.5366 4.68306 15.835 5.50003 15.8333H14.6667C15.6662 15.8305 16.6313 15.4686 17.3862 14.8135C18.141 14.1584 18.6353 13.2538 18.7787 12.2647C18.9222 11.2756 18.7053 10.2679 18.1677 9.42533C17.6301 8.58278 16.8075 7.96155 15.85 7.67495ZM14.6667 14.1666H5.50003C5.058 14.1666 4.63408 13.991 4.32152 13.6785C4.00896 13.3659 3.83336 12.942 3.83336 12.4999C3.83336 12.0579 4.00896 11.634 4.32152 11.3214C4.63408 11.0089 5.058 10.8333 5.50003 10.8333C5.72104 10.8333 5.93301 10.7455 6.08929 10.5892C6.24557 10.4329 6.33336 10.221 6.33336 9.99995C6.33549 9.01435 6.68694 8.06141 7.32525 7.31043C7.96357 6.55946 8.84744 6.05907 9.81982 5.89817C10.7922 5.73728 11.7901 5.92628 12.6363 6.43162C13.4825 6.93696 14.1222 7.7259 14.4417 8.65828C14.4906 8.79993 14.5768 8.92577 14.6911 9.02256C14.8055 9.11936 14.9439 9.18351 15.0917 9.20828C15.7025 9.32099 16.2495 9.65705 16.6261 10.151C17.0027 10.645 17.182 11.2614 17.1289 11.8803C17.0759 12.4992 16.7943 13.0761 16.3392 13.4988C15.884 13.9214 15.2878 14.1595 14.6667 14.1666Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.66683 14.1666C6.50201 14.1666 6.3409 14.2155 6.20385 14.3071C6.06681 14.3986 5.96 14.5288 5.89693 14.6811C5.83386 14.8333 5.81735 15.0009 5.84951 15.1625C5.88166 15.3242 5.96103 15.4727 6.07757 15.5892C6.19412 15.7058 6.3426 15.7851 6.50425 15.8173C6.6659 15.8494 6.83346 15.8329 6.98573 15.7699C7.138 15.7068 7.26815 15.6 7.35972 15.4629C7.45129 15.3259 7.50016 15.1648 7.50016 15C7.50016 14.7789 7.41236 14.567 7.25608 14.4107C7.0998 14.2544 6.88784 14.1666 6.66683 14.1666ZM6.66683 9.16663C6.50201 9.16663 6.3409 9.2155 6.20385 9.30707C6.06681 9.39864 5.96 9.52878 5.89693 9.68106C5.83386 9.83333 5.81735 10.0009 5.84951 10.1625C5.88166 10.3242 5.96103 10.4727 6.07757 10.5892C6.19412 10.7058 6.3426 10.7851 6.50425 10.8173C6.6659 10.8494 6.83346 10.8329 6.98573 10.7699C7.138 10.7068 7.26815 10.6 7.35972 10.4629C7.45129 10.3259 7.50016 10.1648 7.50016 9.99996C7.50016 9.77895 7.41236 9.56698 7.25608 9.4107C7.0998 9.25442 6.88784 9.16663 6.66683 9.16663ZM13.3335 1.66663H6.66683C5.78277 1.66663 4.93493 2.01782 4.30981 2.64294C3.68469 3.26806 3.3335 4.1159 3.3335 4.99996V15C3.3335 15.884 3.68469 16.7319 4.30981 17.357C4.93493 17.9821 5.78277 18.3333 6.66683 18.3333H13.3335C14.2176 18.3333 15.0654 17.9821 15.6905 17.357C16.3156 16.7319 16.6668 15.884 16.6668 15V4.99996C16.6668 4.1159 16.3156 3.26806 15.6905 2.64294C15.0654 2.01782 14.2176 1.66663 13.3335 1.66663ZM15.0002 15C15.0002 15.442 14.8246 15.8659 14.512 16.1785C14.1994 16.491 13.7755 16.6666 13.3335 16.6666H6.66683C6.2248 16.6666 5.80088 16.491 5.48832 16.1785C5.17576 15.8659 5.00016 15.442 5.00016 15V12.8666C5.50349 13.1696 6.07936 13.3308 6.66683 13.3333H13.3335C13.921 13.3308 14.4968 13.1696 15.0002 12.8666V15ZM15.0002 9.99996C15.0002 10.442 14.8246 10.8659 14.512 11.1785C14.1994 11.491 13.7755 11.6666 13.3335 11.6666H6.66683C6.2248 11.6666 5.80088 11.491 5.48832 11.1785C5.17576 10.8659 5.00016 10.442 5.00016 9.99996V7.86663C5.50349 8.16959 6.07936 8.33083 6.66683 8.33329H13.3335C13.921 8.33083 14.4968 8.16959 15.0002 7.86663V9.99996ZM13.3335 6.66663H6.66683C6.2248 6.66663 5.80088 6.49103 5.48832 6.17847C5.17576 5.86591 5.00016 5.44199 5.00016 4.99996C5.00016 4.55793 5.17576 4.13401 5.48832 3.82145C5.80088 3.50889 6.2248 3.33329 6.66683 3.33329H13.3335C13.7755 3.33329 14.1994 3.50889 14.512 3.82145C14.8246 4.13401 15.0002 4.55793 15.0002 4.99996C15.0002 5.44199 14.8246 5.86591 14.512 6.17847C14.1994 6.49103 13.7755 6.66663 13.3335 6.66663Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.09178 10.4C6.92696 10.4 6.76585 10.4488 6.62881 10.5404C6.49177 10.632 6.38495 10.7621 6.32188 10.9144C6.25881 11.0667 6.24231 11.2342 6.27446 11.3959C6.30661 11.5575 6.38598 11.706 6.50253 11.8226C6.61907 11.9391 6.76756 12.0185 6.92921 12.0506C7.09086 12.0828 7.25841 12.0663 7.41068 12.0032C7.56296 11.9401 7.6931 11.8333 7.78467 11.6963C7.87624 11.5592 7.92511 11.3981 7.92511 11.2333C7.92511 11.0123 7.83732 10.8003 7.68104 10.6441C7.52476 10.4878 7.3128 10.4 7.09178 10.4ZM7.09178 6.66664C6.92696 6.66664 6.76585 6.71552 6.62881 6.80708C6.49177 6.89865 6.38495 7.0288 6.32188 7.18107C6.25881 7.33334 6.24231 7.5009 6.27446 7.66255C6.30661 7.8242 6.38598 7.97269 6.50253 8.08923C6.61907 8.20577 6.76756 8.28514 6.92921 8.3173C7.09086 8.34945 7.25841 8.33295 7.41068 8.26988C7.56296 8.2068 7.6931 8.09999 7.78467 7.96295C7.87624 7.82591 7.92511 7.66479 7.92511 7.49998C7.92511 7.27896 7.83732 7.067 7.68104 6.91072C7.52476 6.75444 7.3128 6.66664 7.09178 6.66664ZM10.0001 8.33331C9.7791 8.33331 9.56714 8.42111 9.41086 8.57739C9.25458 8.73367 9.16678 8.94563 9.16678 9.16664V9.99998C9.16678 10.221 9.25458 10.433 9.41086 10.5892C9.56714 10.7455 9.7791 10.8333 10.0001 10.8333C10.2211 10.8333 10.4331 10.7455 10.5894 10.5892C10.7457 10.433 10.8334 10.221 10.8334 9.99998V9.16664C10.8334 8.94563 10.7457 8.73367 10.5894 8.57739C10.4331 8.42111 10.2211 8.33331 10.0001 8.33331ZM17.0834 7.14164C16.796 6.97624 16.4785 6.86969 16.1494 6.8282C15.8203 6.78671 15.4863 6.81111 15.1668 6.89998C14.8342 6.98905 14.5245 7.14816 14.2584 7.36664L4.59178 1.77498C4.4651 1.70184 4.3214 1.66333 4.17511 1.66333C4.02883 1.66333 3.88513 1.70184 3.75845 1.77498C3.62973 1.84733 3.52254 1.95259 3.44786 2.07998C3.37319 2.20737 3.3337 2.35231 3.33345 2.49998V13.475C2.84166 13.6409 2.41526 13.9586 2.11572 14.3825C1.81618 14.8063 1.65898 15.3144 1.66678 15.8333C1.66655 16.485 1.92077 17.1109 2.37529 17.5779C2.82981 18.0449 3.4487 18.3159 4.10011 18.3333H4.58345C7.31406 18.3269 9.99436 17.5981 12.3522 16.2208C14.7101 14.8436 16.6615 12.867 18.0084 10.4916C18.3195 9.92331 18.3978 9.2564 18.2268 8.63147C18.0558 8.00655 17.6489 7.47241 17.0918 7.14164H17.0834ZM5.00011 3.94164L13.2418 8.69998C12.3274 10.0643 11.1051 11.1945 9.67344 11.9994C8.24178 12.8043 6.64092 13.2612 5.00011 13.3333V3.94164ZM16.5584 9.67498C15.3213 11.8547 13.5146 13.6569 11.3318 14.8885C9.14892 16.1201 6.67218 16.7347 4.16678 16.6666C3.94577 16.6666 3.73381 16.5788 3.57753 16.4226C3.42125 16.2663 3.33345 16.0543 3.33345 15.8333C3.3328 15.7217 3.35459 15.611 3.39754 15.5079C3.44048 15.4049 3.5037 15.3115 3.58345 15.2333C3.74 15.0824 3.94934 14.9987 4.16678 15H4.59178C6.73154 14.9991 8.8327 14.43 10.6805 13.351C12.5282 12.2719 14.0563 10.7215 15.1084 8.85831C15.159 8.7669 15.2282 8.68707 15.3114 8.62392C15.3946 8.56078 15.4901 8.51573 15.5918 8.49164C15.8019 8.43397 16.0263 8.46089 16.2168 8.56664C16.408 8.66941 16.5515 8.84273 16.6169 9.04972C16.6823 9.25671 16.6643 9.48104 16.5668 9.67498H16.5584Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -7,7 +7,7 @@ import Error from '@/assets/images/error.svg';
import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg';
import KeyboardArrowUp from '@/assets/images/keyboard-arrow-up.svg';
import { useTheme } from '@/hooks/use-theme';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';

View file

@ -10,7 +10,7 @@ import UserName from '@/components/UserName';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl } from '@/utilities/url';
import { buildUrl } from '@/utils/url';
import Table from '../Table';
import type { Column } from '../Table/types';

View file

@ -2,7 +2,7 @@ import classNames from 'classnames';
import type { ReactNode } from 'react';
import { useLayoutEffect, useState } from 'react';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import { Tooltip } from '../Tip';
import * as styles from './index.module.scss';

View file

@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import Copy from '@/assets/images/copy.svg';
import EyeClosed from '@/assets/images/eye-closed.svg';
import Eye from '@/assets/images/eye.svg';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import IconButton from '../IconButton';
import { Tooltip } from '../Tip';

View file

@ -1,7 +1,7 @@
import classNames from 'classnames';
import type { MouseEvent, KeyboardEvent, ReactNode } from 'react';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './DropdownItem.module.scss';

View file

@ -5,7 +5,7 @@ import ReactModal from 'react-modal';
import usePosition from '@/hooks/use-position';
import type { HorizontalAlignment } from '@/types/positioning';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import OverlayScrollbar from '../OverlayScrollbar';
import * as styles from './index.module.scss';

View file

@ -1,6 +1,6 @@
import { t } from 'i18next';
import { safeParseJson } from '@/utilities/json';
import { safeParseJson } from '@/utils/json';
import type { MultiTextInputError, MultiTextInputRule } from './types';
import { multiTextInputErrorGuard } from './types';

View file

@ -0,0 +1,254 @@
@use '@/scss/underscore' as _;
// Default Styles
.radio {
user-select: none;
cursor: pointer;
font: var(--font-body-2);
&:not(:last-child) {
margin-bottom: _.unit(2);
}
.content {
display: flex;
align-items: center;
.indicator {
border-radius: 50%;
border: 2px solid var(--color-neutral-60);
display: inline-block;
margin-right: _.unit(2);
&::before {
content: '';
background: var(--color-layer-1);
width: 10px;
height: 10px;
display: block;
border-radius: 50%;
border: 2px solid var(--color-layer-1);
}
}
.icon {
margin-right: _.unit(2);
color: var(--color-text-secondary);
> svg {
display: block;
}
}
}
}
.card {
padding: _.unit(3);
border-radius: _.unit(4);
border: 1px solid transparent;
outline: 1px solid var(--color-neutral-90);
&:not(:last-child) {
margin-bottom: unset;
}
.content {
position: relative;
display: block;
.indicator {
border-radius: unset;
border: unset;
display: block;
margin-right: unset;
position: absolute;
right: 0;
top: 0;
svg {
opacity: 0%;
}
&::before {
display: none;
}
}
.icon {
margin-right: _.unit(2);
vertical-align: middle;
color: var(--color-text-secondary);
> svg {
display: unset;
}
}
.disabledLabel {
background: var(--color-neutral-90);
padding: _.unit(0.5) _.unit(2);
border-radius: 10px;
font: var(--font-label-3);
color: var(--color-text);
}
}
}
.compact {
position: relative;
border: 1px solid var(--color-border);
flex: 1;
font: var(--font-label-2);
&:first-child {
border-radius: 12px 0 0 12px;
}
&:last-child {
border-radius: 0 12px 12px 0;
}
&:not(:first-child) {
border-left: none;
}
&:not(:last-child) {
margin-bottom: unset;
}
.content {
padding: _.unit(5);
.icon {
margin-right: _.unit(4);
}
}
}
// Checked Styles
.radio.checked {
.content {
.indicator {
border-color: var(--color-primary);
&::before {
background: var(--color-primary);
}
}
}
}
.card.checked {
border-color: var(--color-primary);
outline: 1px solid var(--color-primary);
.content {
.indicator {
svg {
opacity: 100%;
}
}
}
}
.compact.checked {
color: var(--color-text-link);
border-color: var(--color-primary);
.content {
.icon {
color: var(--color-primary);
}
}
&:not(:first-child)::before {
position: absolute;
content: '';
width: 1px;
top: -1px;
left: -1px;
bottom: -1px;
background-color: var(--color-primary);
}
}
// Disabled Styles
.radio.disabled {
cursor: not-allowed;
color: var(--color-disabled);
.content {
.indicator {
border-color: var(--color-disabled);
&::before {
background: var(--color-layer-1);
}
}
}
}
.card.disabled {
background-color: var(--color-layer-2);
border-color: var(--color-layer-2);
outline: unset;
}
.compact.disabled {
cursor: not-allowed;
background-color: var(--color-layer-2);
.content {
.icon {
color: var(--color-text-secondary);
}
}
}
// Not Disabled Behaviors
.card:not(.disabled) {
&:focus {
outline: 1px solid var(--color-primary);
box-shadow: var(--shadow-2);
}
&:hover {
box-shadow: var(--shadow-2);
}
}
.compact:not(.disabled) {
&:focus {
color: var(--color-text-link);
border-color: var(--color-primary);
.content {
.icon {
color: var(--color-primary);
}
}
}
&:hover {
color: var(--color-text-link);
border-color: var(--color-primary);
background-color: var(--color-hover-variant);
.content {
.icon {
color: var(--color-primary);
}
}
&:not(:first-child)::before {
position: absolute;
content: '';
width: 1px;
top: -1px;
left: -1px;
bottom: -1px;
background-color: var(--color-primary);
}
}
}

View file

@ -4,7 +4,7 @@ import type { KeyboardEventHandler, ReactNode } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import * as styles from './index.module.scss';
import * as styles from './Radio.module.scss';
const Check = () => (
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -24,9 +24,10 @@ export type Props = {
isChecked?: boolean;
onClick?: () => void;
tabIndex?: number;
type?: 'card' | 'plain';
type?: 'card' | 'plain' | 'compact';
isDisabled?: boolean;
disabledLabel?: AdminConsoleKey;
icon?: ReactNode;
};
const Radio = ({
@ -38,9 +39,10 @@ const Radio = ({
isChecked,
onClick,
tabIndex,
type,
type = 'plain',
isDisabled,
disabledLabel,
icon,
}: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -62,6 +64,7 @@ const Radio = ({
<div
className={classNames(
styles.radio,
styles[type],
isChecked && styles.checked,
isDisabled && styles.disabled,
className
@ -81,6 +84,7 @@ const Radio = ({
)}
{children}
{type === 'plain' && <div className={styles.indicator} />}
{icon && <span className={styles.icon}>{icon}</span>}
{title && t(title)}
{isDisabled && disabledLabel && (
<div className={classNames(styles.indicator, styles.disabledLabel)}>

View file

@ -10,125 +10,8 @@
}
}
.card {
> .radio {
padding: _.unit(3);
border-radius: _.unit(4);
border: 1px solid transparent;
outline: 1px solid var(--color-neutral-90);
user-select: none;
cursor: pointer;
&.disabled {
cursor: not-allowed;
background-color: var(--color-layer-2);
border-color: var(--color-layer-2);
outline: unset;
}
&:not(.disabled):focus {
outline: 1px solid var(--color-primary);
box-shadow: var(--shadow-2);
}
&:not(.disabled):hover {
box-shadow: var(--shadow-2);
}
.content {
position: relative;
.indicator {
position: absolute;
right: 0;
top: 0;
svg {
opacity: 0%;
}
}
.disabledLabel {
background: var(--color-neutral-90);
padding: _.unit(0.5) _.unit(2);
border-radius: 10px;
font: var(--font-label-3);
color: var(--color-text);
}
}
&.checked {
border-color: var(--color-primary);
outline: 1px solid var(--color-primary);
.indicator {
svg {
opacity: 100%;
}
}
}
}
}
.plain {
font: var(--font-body-2);
/* stylelint-disable-next-line no-descending-specificity */
> .radio {
cursor: pointer;
/* stylelint-disable-next-line no-descending-specificity */
&:not(:last-child) {
margin-bottom: _.unit(2);
}
.content {
display: flex;
align-items: center;
.indicator {
border-radius: 50%;
border: 2px solid var(--color-neutral-60);
display: inline-block;
margin-right: _.unit(2);
&::before {
content: '';
background: var(--color-layer-1);
width: 10px;
height: 10px;
display: block;
border-radius: 50%;
border: 2px solid var(--color-layer-1);
}
}
}
&.checked {
.content {
.indicator {
border-color: var(--color-primary);
&::before {
background: var(--color-primary);
}
}
}
}
&.disabled {
cursor: not-allowed;
color: var(--color-disabled);
.content {
.indicator {
border-color: var(--color-disabled);
&::before {
background: var(--color-layer-1);
}
}
}
}
}
.compact {
display: flex;
flex-wrap: nowrap;
align-items: center;
}

View file

@ -19,7 +19,7 @@ type Props = {
name: string;
children: RadioElement | RadioElement[];
value?: string;
type?: 'card' | 'plain';
type?: 'card' | 'plain' | 'compact';
className?: string;
onChange?: (value: string) => void;
};

View file

@ -7,7 +7,7 @@ import CaretExpanded from '@/assets/images/caret-expanded.svg';
import CaretFolded from '@/assets/images/caret-folded.svg';
import Checkbox from '@/components/Checkbox';
import IconButton from '@/components/IconButton';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import type { DetailedResourceResponse } from '../../types';
import SourceScopeItem from '../SourceScopeItem';

View file

@ -1,7 +1,7 @@
import type { ScopeResponse } from '@logto/schemas';
import Checkbox from '@/components/Checkbox';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';

View file

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import Checkbox from '@/components/Checkbox';
import UserAvatar from '@/components/UserAvatar';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';

View file

@ -14,7 +14,7 @@ import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api';
import useDebounce from '@/hooks/use-debounce';
import * as transferLayout from '@/scss/transfer.module.scss';
import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import SourceUserItem from '../SourceUserItem';
import * as styles from './index.module.scss';

View file

@ -5,7 +5,7 @@ import { useRef, useState } from 'react';
import Close from '@/assets/images/close.svg';
import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg';
import KeyboardArrowUp from '@/assets/images/keyboard-arrow-up.svg';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import Dropdown, { DropdownItem } from '../Dropdown';
import IconButton from '../IconButton';

View file

@ -2,7 +2,7 @@ import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Link, useLocation } from 'react-router-dom';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './TabNavItem.module.scss';

View file

@ -11,6 +11,14 @@
outline-color: var(--color-focused-variant);
}
&.error {
border-color: var(--color-error);
&:focus-within {
outline-color: var(--color-danger-focused);
}
}
textarea {
width: 100%;
height: 100%;
@ -23,7 +31,7 @@
padding: 0;
&::placeholder {
color: var(--color-caption);
color: var(--color-placeholder);
}
}
}

View file

@ -6,11 +6,15 @@ import * as styles from './index.module.scss';
type Props = HTMLProps<HTMLTextAreaElement> & {
className?: string;
hasError?: boolean;
};
const Textarea = ({ className, ...rest }: Props, reference: ForwardedRef<HTMLTextAreaElement>) => {
const Textarea = (
{ className, hasError, ...rest }: Props,
reference: ForwardedRef<HTMLTextAreaElement>
) => {
return (
<div className={classNames(styles.container, className)}>
<div className={classNames(styles.container, hasError && styles.error, className)}>
<textarea {...rest} ref={reference} />
</div>
);

View file

@ -4,7 +4,7 @@ import ReactModal from 'react-modal';
import usePosition from '@/hooks/use-position';
import type { HorizontalAlignment } from '@/types/positioning';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import type { TipBubblePlacement } from '../TipBubble';
import TipBubble from '../TipBubble';

View file

@ -2,7 +2,7 @@ import type { RoleResponse } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
import Checkbox from '@/components/Checkbox';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';

View file

@ -14,7 +14,7 @@ import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api';
import useDebounce from '@/hooks/use-debounce';
import * as transferLayout from '@/scss/transfer.module.scss';
import { buildUrl } from '@/utilities/url';
import { buildUrl } from '@/utils/url';
import SourceRoleItem from '../SourceRoleItem';
import * as styles from './index.module.scss';

View file

@ -8,7 +8,7 @@ import SignOut from '@/assets/images/sign-out.svg';
import Dropdown, { DropdownItem } from '@/components/Dropdown';
import { Ring as Spinner } from '@/components/Spinner';
import UserAvatar from '@/components/UserAvatar';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import UserInfoSkeleton from '../UserInfoSkeleton';
import * as styles from './index.module.scss';

View file

@ -5,7 +5,7 @@ import type { Height } from 'react-animate-height';
import AnimateHeight from 'react-animate-height';
import ArrowRight from '@/assets/images/triangle-right.svg';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';

View file

@ -12,7 +12,7 @@ import DangerousRaw from '@/components/DangerousRaw';
import IconButton from '@/components/IconButton';
import Index from '@/components/Index';
import Spacer from '@/components/Spacer';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';

View file

@ -15,7 +15,7 @@ import TextInput from '@/components/TextInput';
import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import type { GuideForm } from '@/types/guide';
import { uriValidator } from '@/utilities/validator';
import { uriValidator } from '@/utils/validator';
import * as styles from './index.module.scss';

View file

@ -12,7 +12,7 @@ import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import type { ApiResourceDetailsOutletContext } from '../types';
import CreatePermissionModal from './components/CreatePermissionModal';

View file

@ -26,7 +26,7 @@ import { useTheme } from '@/hooks/use-theme';
import * as detailsStyles from '@/scss/details.module.scss';
import * as styles from './index.module.scss';
import type { ApiResourceDetailsOutletContext } from './types';
import { ApiResourceDetailsOutletContext } from './types';
const ApiResourceDetails = () => {
const { pathname } = useLocation();
@ -132,7 +132,6 @@ const ApiResourceDetails = () => {
</TabNav>
<Outlet
context={
// eslint-disable-next-line no-restricted-syntax
{
resource: data,
isDeleting,
@ -140,7 +139,7 @@ const ApiResourceDetails = () => {
onResourceUpdated: (resource: Resource) => {
void mutate(resource);
},
} as ApiResourceDetailsOutletContext
} satisfies ApiResourceDetailsOutletContext
}
/>
</>

View file

@ -22,7 +22,7 @@ import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { useTheme } from '@/hooks/use-theme';
import * as modalStyles from '@/scss/modal.module.scss';
import * as resourcesStyles from '@/scss/resources.module.scss';
import { buildUrl } from '@/utilities/url';
import { buildUrl } from '@/utils/url';
import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss';

View file

@ -12,7 +12,7 @@ import MultiTextInputField from '@/components/MultiTextInputField';
import TextInput from '@/components/TextInput';
import TextLink from '@/components/TextLink';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import { uriOriginValidator } from '@/utilities/validator';
import { uriOriginValidator } from '@/utils/validator';
import * as styles from '../index.module.scss';

View file

@ -19,7 +19,7 @@ import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import * as modalStyles from '@/scss/modal.module.scss';
import * as resourcesStyles from '@/scss/resources.module.scss';
import { applicationTypeI18nKey } from '@/types/applications';
import { buildUrl } from '@/utilities/url';
import { buildUrl } from '@/utils/url';
import CreateForm from './components/CreateForm';
import * as styles from './index.module.scss';

View file

@ -0,0 +1 @@
$questionnaire-content-width: 858px;

View file

@ -0,0 +1,11 @@
@use '@/scss/underscore' as _;
.container {
height: 92px;
background-color: var(--color-layer-1);
padding: 0 _.unit(17);
display: flex;
align-items: center;
flex-direction: row-reverse;
justify-content: space-between;
}

View file

@ -0,0 +1,11 @@
import type { ReactNode } from 'react';
import * as styles from './index.module.scss';
type Props = {
children: ReactNode;
};
const ActionBar = ({ children }: Props) => <div className={styles.container}>{children}</div>;
export default ActionBar;

View file

@ -0,0 +1,20 @@
import RadioGroup, { Radio } from '@/components/RadioGroup';
import type { Option } from './types';
type Props = {
name: string;
value: string;
options: Option[];
onChange: (value: string) => void;
};
const CardSelector = ({ name, value, options, onChange }: Props) => (
<RadioGroup type="compact" value={value} name={name} onChange={onChange}>
{options.map(({ value: optionValue, title, icon }) => (
<Radio key={optionValue} icon={icon} title={title} value={optionValue} />
))}
</RadioGroup>
);
export default CardSelector;

View file

@ -0,0 +1,50 @@
@use '@/scss/underscore' as _;
.selector {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: _.unit(4);
}
.option {
border: 1px solid var(--color-border);
border-radius: 12px;
min-height: 80px;
padding: _.unit(5);
font: var(--font-label-2);
cursor: pointer;
user-select: none;
background-color: var(--color-layer-1);
color: var(--color-text);
display: flex;
align-items: center;
.icon {
color: var(--color-text-secondary);
margin-right: _.unit(4);
vertical-align: middle;
> svg {
display: block;
}
}
&.selected {
border-color: var(--color-primary);
color: var(--color-text-link);
.icon {
color: var(--color-primary);
}
}
&:hover {
background-color: var(--color-hover-variant);
color: var(--color-text-link);
.icon {
color: var(--color-primary);
}
}
}

View file

@ -0,0 +1,49 @@
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { onKeyDownHandler } from '@/utils/a11y';
import type { Option } from '../types';
import * as styles from './index.module.scss';
type Props = {
options: Option[];
value: string[];
onChange: (value: string[]) => void;
};
const MultiCardSelector = ({ options, value: selectedValues, onChange }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const onToggle = (value: string) => {
onChange(
selectedValues.includes(value)
? selectedValues.filter((selected) => selected !== value)
: [...selectedValues, value]
);
};
return (
<div className={styles.selector}>
{options.map(({ icon, title, value }) => (
<div
key={value}
role="button"
tabIndex={0}
className={classNames(styles.option, selectedValues.includes(value) && styles.selected)}
onClick={() => {
onToggle(value);
}}
onKeyDown={onKeyDownHandler(() => {
onToggle(value);
})}
>
{icon && <span className={styles.icon}>{icon}</span>}
{t(title)}
</div>
))}
</div>
);
};
export default MultiCardSelector;

View file

@ -0,0 +1,3 @@
export type { Option } from './types';
export { default as CardSelector } from './CardSelector';
export { default as MultiCardSelector } from './MultiCardSelector';

View file

@ -0,0 +1,8 @@
import type { AdminConsoleKey } from '@logto/phrases';
import type { ReactNode } from 'react';
export type Option = {
icon?: ReactNode;
title: AdminConsoleKey;
value: string;
};

View file

@ -0,0 +1,4 @@
.cloudPreview {
flex-grow: 1;
overflow: hidden;
}

View file

@ -1,5 +1,11 @@
const CloudPreview = () => {
return <div>CloudPreview(WIP)</div>;
};
import { Outlet } from 'react-router-dom';
import * as styles from './index.module.scss';
const CloudPreview = () => (
<div className={styles.cloudPreview}>
<Outlet />
</div>
);
export default CloudPreview;

View file

@ -0,0 +1,11 @@
.page {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.contentContainer {
flex: 1;
overflow-y: auto;
}

View file

@ -0,0 +1,39 @@
@use '@/scss/underscore' as _;
@use '@/pages/CloudPreview/cloud-page-size.scss' as size;
.content {
margin: 0 auto;
max-width: size.$questionnaire-content-width;
border-radius: 16px;
padding: _.unit(12);
background-color: var(--color-layer-1);
margin-bottom: _.unit(4);
display: flex;
flex-direction: column;
align-items: center;
}
.congrats {
width: 160px;
height: 160px;
}
.title {
margin-top: _.unit(6);
font: var(--font-title-1);
}
.description {
margin-top: _.unit(3);
text-align: center;
font: var(--font-body-2);
}
.form {
width: 100%;
margin-top: _.unit(6);
.cardFieldHeadline {
margin-bottom: _.unit(2);
}
}

View file

@ -0,0 +1,90 @@
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import Congrats from '@/assets/images/congrats.svg';
import Button from '@/components/Button';
import FormField from '@/components/FormField';
import OverlayScrollbar from '@/components/OverlayScrollbar';
import * as pageLayout from '@/pages/CloudPreview/layout.module.scss';
import ActionBar from '../../components/ActionBar';
import { CardSelector } from '../../components/CardSelector';
import type { Questionnaire } from '../../types';
import * as styles from './index.module.scss';
import { deploymentTypeOptions, projectOptions } from './options';
const Welcome = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
control,
handleSubmit,
formState: { isSubmitting, isValid },
} = useForm<Questionnaire>({ mode: 'onChange' });
const onSubmit = handleSubmit(async (formData) => {
// TODO @xiaoyijun send data to the backend
console.log(formData);
// TODO @xiaoyijun navigate to the about users page
});
return (
<div className={pageLayout.page}>
<OverlayScrollbar className={pageLayout.contentContainer}>
<div className={styles.content}>
<Congrats className={styles.congrats} />
<div className={styles.title}>{t('cloud_preview.welcome.title')}</div>
<div className={styles.description}>{t('cloud_preview.welcome.description')}</div>
<form className={styles.form}>
<FormField
title="cloud_preview.welcome.project_field"
headlineClassName={styles.cardFieldHeadline}
>
<Controller
control={control}
name="project"
rules={{ required: true }}
render={({ field: { onChange, value, name } }) => (
<CardSelector
name={name}
value={value}
options={projectOptions}
onChange={onChange}
/>
)}
/>
</FormField>
<FormField
title="cloud_preview.welcome.deployment_type_field"
headlineClassName={styles.cardFieldHeadline}
>
<Controller
control={control}
name="deploymentType"
rules={{ required: true }}
render={({ field: { onChange, value, name } }) => (
<CardSelector
name={name}
value={value}
options={deploymentTypeOptions}
onChange={onChange}
/>
)}
/>
</FormField>
</form>
</div>
</OverlayScrollbar>
<ActionBar>
<Button
title="general.next"
type="primary"
disabled={isSubmitting || !isValid}
onClick={onSubmit}
/>
</ActionBar>
</div>
);
};
export default Welcome;

View file

@ -0,0 +1,33 @@
import Building from '@/assets/images/building.svg';
import Cloud from '@/assets/images/cloud.svg';
import Database from '@/assets/images/database.svg';
import Pizza from '@/assets/images/pizza.svg';
import type { Option as SelectorOption } from '@/pages/CloudPreview/components/CardSelector';
import { DeploymentType, Project } from '../../types';
export const projectOptions: SelectorOption[] = [
{
icon: <Pizza />,
title: 'cloud_preview.welcome.project_personal',
value: Project.Personal,
},
{
icon: <Building />,
title: 'cloud_preview.welcome.project_company',
value: Project.Company,
},
];
export const deploymentTypeOptions: SelectorOption[] = [
{
icon: <Database />,
title: 'cloud_preview.welcome.deployment_type_opensource',
value: DeploymentType.Opensource,
},
{
icon: <Cloud />,
title: 'cloud_preview.welcome.deployment_type_cloud',
value: DeploymentType.Cloud,
},
];

View file

@ -0,0 +1,20 @@
export enum CloudPreviewPage {
Welcome = 'welcome',
About = 'about',
SignInExperience = 'sign-in-experience',
}
export enum Project {
Personal = 'personal',
Company = 'company',
}
export enum DeploymentType {
Opensource = 'opensource',
Cloud = 'cloud',
}
export type Questionnaire = {
project: Project;
deploymentType: DeploymentType;
};

View file

@ -11,9 +11,10 @@ import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import useApi from '@/hooks/use-api';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import ConnectorForm from '@/pages/Connectors/components/ConnectorForm';
import { useConfigParser } from '@/pages/Connectors/components/ConnectorForm/hooks';
import { initFormData, parseFormConfig } from '@/pages/Connectors/components/ConnectorForm/utils';
import type { ConnectorFormType } from '@/pages/Connectors/types';
import { SyncProfileMode } from '@/pages/Connectors/types';
import { safeParseJson } from '@/utilities/json';
import * as styles from '../index.module.scss';
import SenderTester from './SenderTester';
@ -27,6 +28,7 @@ type Props = {
const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();
const parseJsonConfig = useConfigParser();
const api = useApi();
const methods = useForm<ConnectorFormType>({
reValidateMode: 'onBlur',
@ -42,9 +44,11 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
} = methods;
useEffect(() => {
const { name, logo, logoDark, target } = connectorData.metadata;
const { config, syncProfile } = connectorData;
const { formItems, metadata, config, syncProfile } = connectorData;
const { name, logo, logoDark, target } = metadata;
reset({
...(formItems ? initFormData(formItems, config) : {}),
target,
logo,
logoDark: logoDark ?? '',
@ -54,36 +58,26 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
});
}, [connectorData, reset]);
const onSubmit = handleSubmit(async ({ config, syncProfile, ...metadata }) => {
if (!config) {
toast.error(t('connector_details.save_error_empty_config'));
return;
}
const result = safeParseJson(config);
if (!result.success) {
toast.error(result.error);
return;
}
const onSubmit = handleSubmit(async (data) => {
const { formItems, isStandard, id } = connectorData;
const config = formItems ? parseFormConfig(data, formItems) : parseJsonConfig(data.config);
const { syncProfile, name, logo, logoDark, target } = data;
const payload =
connectorData.type === ConnectorType.Social
? {
config: result.data,
config,
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
}
: { config: result.data };
: { config };
const standardConnectorPayload = {
...payload,
metadata: { ...metadata, name: { en: metadata.name } },
metadata: { name: { en: name }, logo, logoDark, target },
};
const body = connectorData.isStandard ? standardConnectorPayload : payload;
const body = isStandard ? standardConnectorPayload : payload;
const updatedConnector = await api
.patch(`api/connectors/${connectorData.id}`, {
.patch(`api/connectors/${id}`, {
json: body,
})
.json<ConnectorResponse>();
@ -109,6 +103,7 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
connectorType={connectorData.type}
isStandard={connectorData.isStandard}
isDarkDefaultVisible={Boolean(connectorData.metadata.logoDark)}
formItems={connectorData.formItems}
/>
{connectorData.type !== ConnectorType.Social && (
<SenderTester

View file

@ -12,8 +12,8 @@ import FormField from '@/components/FormField';
import TextInput from '@/components/TextInput';
import { Tooltip } from '@/components/Tip';
import useApi from '@/hooks/use-api';
import { onKeyDownHandler } from '@/utilities/a11y';
import { safeParseJson } from '@/utilities/json';
import { onKeyDownHandler } from '@/utils/a11y';
import { safeParseJson } from '@/utils/json';
import * as styles from './index.module.scss';

View file

@ -0,0 +1,151 @@
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
import { useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import CodeEditor from '@/components/CodeEditor';
import DangerousRaw from '@/components/DangerousRaw';
import FormField from '@/components/FormField';
import Select from '@/components/Select';
import Switch from '@/components/Switch';
import TextInput from '@/components/TextInput';
import Textarea from '@/components/Textarea';
import { jsonValidator } from '@/utils/validator';
import type { ConnectorFormType } from '../../types';
type Props = {
formItems: ConnectorConfigFormItem[];
};
const ConfigForm = ({ formItems }: Props) => {
const {
watch,
register,
control,
formState: { errors },
} = useFormContext<ConnectorFormType>();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const values = watch();
const filteredFormItems = useMemo(() => {
return formItems.filter((item) => {
if (!item.showConditions) {
return true;
}
return item.showConditions.every(({ expectValue, targetKey }) => {
const targetValue = values[targetKey];
return targetValue === expectValue;
});
});
}, [formItems, values]);
const renderFormItem = (item: ConnectorConfigFormItem) => {
const hasError = Boolean(errors[item.key]);
const errorMessage = errors[item.key]?.message;
const commonProperties = {
...register(item.key, { required: item.required }),
placeholder: item.placeholder,
hasError,
};
if (item.type === ConnectorConfigFormItemType.Text) {
return <TextInput {...commonProperties} />;
}
if (item.type === ConnectorConfigFormItemType.MultilineText) {
return <Textarea rows={5} {...commonProperties} />;
}
if (item.type === ConnectorConfigFormItemType.Number) {
return <TextInput type="number" {...commonProperties} />;
}
return (
<Controller
name={item.key}
control={control}
rules={
item.type === ConnectorConfigFormItemType.Json
? {
validate: (value) =>
(typeof value === 'string' && jsonValidator(value)) ||
t('errors.invalid_json_format'),
}
: undefined
}
render={({ field: { onChange, value } }) => {
if (item.type === ConnectorConfigFormItemType.Switch) {
return (
<Switch
label={item.label}
checked={typeof value === 'boolean' ? value : false}
onChange={({ currentTarget: { checked } }) => {
onChange(checked);
}}
/>
);
}
if (item.type === ConnectorConfigFormItemType.Select) {
return (
<Select
options={item.selectItems}
value={typeof value === 'string' ? value : undefined}
hasError={hasError}
onChange={onChange}
/>
);
}
if (item.type === ConnectorConfigFormItemType.Json) {
return (
<CodeEditor
language="json"
hasError={hasError}
errorMessage={errorMessage}
value={typeof value === 'string' ? value : '{}'}
onChange={onChange}
/>
);
}
// Default (unknown) type is "Text"
// This will happen when connector's version is ahead of AC
return (
<TextInput
hasError={hasError}
value={typeof value === 'string' ? value : ''}
onChange={onChange}
/>
);
}}
/>
);
};
return (
<>
{filteredFormItems.map((item) => (
<FormField
key={item.key}
isRequired={item.required}
title={
<DangerousRaw>
{item.type !== ConnectorConfigFormItemType.Switch && item.label}
</DangerousRaw>
}
>
{renderFormItem(item)}
</FormField>
))}
</>
);
};
export default ConfigForm;

View file

@ -0,0 +1,26 @@
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { safeParseJson } from '@/utils/json';
export const useConfigParser = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (config: string) => {
if (!config) {
toast.error(t('connector_details.save_error_empty_config'));
return;
}
const result = safeParseJson(config);
if (!result.success) {
toast.error(result.error);
return;
}
return result.data;
};
};

View file

@ -1,3 +1,4 @@
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
import type { ConnectorFactoryResponse } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas';
import { useState } from 'react';
@ -13,10 +14,11 @@ import Select from '@/components/Select';
import TextInput from '@/components/TextInput';
import TextLink from '@/components/TextLink';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import { uriValidator, jsonValidator } from '@/utilities/validator';
import { uriValidator, jsonValidator } from '@/utils/validator';
import type { ConnectorFormType } from '../../types';
import { SyncProfileMode } from '../../types';
import ConfigForm from '../ConfigForm';
import * as styles from './index.module.scss';
type Props = {
@ -25,6 +27,7 @@ type Props = {
configTemplate?: ConnectorFactoryResponse['configTemplate'];
isAllowEditTarget?: boolean;
isDarkDefaultVisible?: boolean;
formItems?: ConnectorConfigFormItem[];
};
const ConnectorForm = ({
@ -33,6 +36,7 @@ const ConnectorForm = ({
isAllowEditTarget,
isDarkDefaultVisible,
connectorType,
formItems,
}: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getDocumentationUrl } = useDocumentationUrl();
@ -132,25 +136,29 @@ const ConnectorForm = ({
</FormField>
</>
)}
<FormField title="connectors.guide.config">
<Controller
name="config"
control={control}
defaultValue={configTemplate}
rules={{
validate: (value) => jsonValidator(value) || t('errors.invalid_json_format'),
}}
render={({ field: { onChange, value } }) => (
<CodeEditor
hasError={Boolean(errors.config)}
errorMessage={errors.config?.message}
language="json"
value={value}
onChange={onChange}
/>
)}
/>
</FormField>
{formItems ? (
<ConfigForm formItems={formItems} />
) : (
<FormField title="connectors.guide.config">
<Controller
name="config"
control={control}
defaultValue={configTemplate}
rules={{
validate: (value) => jsonValidator(value) || t('errors.invalid_json_format'),
}}
render={({ field: { onChange, value } }) => (
<CodeEditor
hasError={Boolean(errors.config)}
errorMessage={errors.config?.message}
language="json"
value={value}
onChange={onChange}
/>
)}
/>
</FormField>
)}
{connectorType === ConnectorType.Social && (
<FormField title="connectors.guide.sync_profile">
<Controller

View file

@ -0,0 +1,56 @@
import type { ConnectorConfigFormItem } from '@logto/connector-kit';
import { ConnectorConfigFormItemType } from '@logto/connector-kit';
import { safeParseJson } from '@/utils/json';
import type { ConnectorFormType } from '../../types';
export const initFormData = (
formItems: ConnectorConfigFormItem[],
config?: Record<string, unknown>
) => {
const data: Array<[string, unknown]> = formItems.map((item) => {
const value = config?.[item.key] ?? item.defaultValue;
if (item.type === ConnectorConfigFormItemType.Json) {
return [item.key, JSON.stringify(value, null, 2)];
}
return [item.key, value];
});
return Object.fromEntries(data);
};
export const parseFormConfig = (data: ConnectorFormType, formItems: ConnectorConfigFormItem[]) => {
return Object.fromEntries(
Object.entries(data)
.map(([key, value]) => {
// Filter out empty input
if (value === '') {
return null;
}
const formItem = formItems.find((item) => item.key === key);
if (!formItem) {
return null;
}
if (formItem.type === ConnectorConfigFormItemType.Number) {
// The number input my return string value.
return [key, Number(value)];
}
if (formItem.type === ConnectorConfigFormItemType.Json) {
// The JSON validation is done in the form
const result = safeParseJson(typeof value === 'string' ? value : '');
return [key, result.success ? result.data : {}];
}
return [key, value];
})
.filter((item): item is [string, unknown] => Array.isArray(item))
);
};

View file

@ -18,11 +18,12 @@ import { ConnectorsTabs } from '@/consts/page-tabs';
import useApi from '@/hooks/use-api';
import useConfigs from '@/hooks/use-configs';
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
import { safeParseJson } from '@/utilities/json';
import type { ConnectorFormType } from '../../types';
import { SyncProfileMode } from '../../types';
import ConnectorForm from '../ConnectorForm';
import { useConfigParser } from '../ConnectorForm/hooks';
import { initFormData, parseFormConfig } from '../ConnectorForm/utils';
import * as styles from './index.module.scss';
type Props = {
@ -34,8 +35,9 @@ const Guide = ({ connector, onClose }: Props) => {
const api = useApi();
const navigate = useNavigate();
const { updateConfigs } = useConfigs();
const parseJsonConfig = useConfigParser();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { id: connectorId, type: connectorType, name, readme, isStandard } = connector;
const { id: connectorId, type: connectorType, name, readme, isStandard, formItems } = connector;
const { language } = i18next;
const connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en;
const isSocialConnector =
@ -43,6 +45,7 @@ const Guide = ({ connector, onClose }: Props) => {
const methods = useForm<ConnectorFormType>({
reValidateMode: 'onBlur',
defaultValues: {
...(formItems ? initFormData(formItems) : {}),
syncProfile: SyncProfileMode.OnlyAtRegister,
},
});
@ -57,23 +60,18 @@ const Guide = ({ connector, onClose }: Props) => {
return;
}
const { config, name, syncProfile, ...otherData } = data;
const result = safeParseJson(config);
if (!result.success) {
toast.error(result.error);
return;
}
const { id: connectorId } = connector;
const { formItems, isStandard, id: connectorId } = connector;
const config = formItems ? parseFormConfig(data, formItems) : parseJsonConfig(data.config);
const { syncProfile, name, logo, logoDark, target } = data;
const basePayload = {
config: result.data,
config,
connectorId,
metadata: conditional(
isStandard && {
...otherData,
logo,
logoDark,
target,
name: { en: name },
}
),
@ -127,6 +125,7 @@ const Guide = ({ connector, onClose }: Props) => {
connectorType={connector.type}
configTemplate={connector.configTemplate}
isStandard={connector.isStandard}
formItems={connector.formItems}
/>
{!isSocialConnector && (
<SenderTester

View file

@ -5,7 +5,7 @@ export type ConnectorFormType = {
logoDark: string;
target: string;
syncProfile: SyncProfileMode;
};
} & Record<string, unknown>; // Extend custom connector config form
export enum SyncProfileMode {
OnlyAtRegister = 'OnlyAtRegister',

View file

@ -10,7 +10,7 @@ import Card from '@/components/Card';
import IconButton from '@/components/IconButton';
import { ToggleTip } from '@/components/Tip';
import type { Props as ToggleTipProps } from '@/components/Tip/ToggleTip';
import { formatNumberWithComma } from '@/utilities/number';
import { formatNumberWithComma } from '@/utils/number';
import * as styles from './Block.module.scss';

View file

@ -1,4 +1,4 @@
import { formatNumberWithComma } from '@/utilities/number';
import { formatNumberWithComma } from '@/utils/number';
import * as styles from './index.module.scss';

View file

@ -9,7 +9,7 @@ import Dropdown, { DropdownItem } from '@/components/Dropdown';
import Index from '@/components/Index';
import { useTheme } from '@/hooks/use-theme';
import useUserPreferences from '@/hooks/use-user-preferences';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import useGetStartedMetadata from '../../hook';
import * as styles from './index.module.scss';

View file

@ -12,7 +12,7 @@ import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import type { RoleDetailsOutletContext } from '../types';
import AssignPermissionsModal from './components/AssignPermissionsModal';

View file

@ -22,7 +22,7 @@ import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import type { RoleDetailsOutletContext } from '../types';
import AssignUsersModal from './components/AssignUsersModal';

View file

@ -14,7 +14,7 @@ import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import * as pageStyles from '@/scss/resources.module.scss';
import { buildUrl, formatSearchKeyword } from '@/utilities/url';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import AssignedUsers from './components/AssignedUsers';
import CreateRoleModal from './components/CreateRoleModal';

View file

@ -7,7 +7,7 @@ import type { SignInMethod, SignInMethodsObject } from '@/pages/SignInExperience
import DiffSegment from './DiffSegment';
import * as styles from './index.module.scss';
import { convertToSignInMethodsObject } from './utilities';
import { convertToSignInMethodsObject } from './utils';
type Props = {
before: SignInMethod[];

View file

@ -6,7 +6,7 @@ import Card from '@/components/Card';
import FormField from '@/components/FormField';
import RadioGroup, { Radio } from '@/components/RadioGroup';
import TextInput from '@/components/TextInput';
import { uriValidator } from '@/utilities/validator';
import { uriValidator } from '@/utils/validator';
import type { SignInExperienceForm } from '../../types';
import * as styles from '../index.module.scss';

View file

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import Card from '@/components/Card';
import FormField from '@/components/FormField';
import TextInput from '@/components/TextInput';
import { uriValidator } from '@/utilities/validator';
import { uriValidator } from '@/utils/validator';
import type { SignInExperienceForm } from '../../types';
import * as styles from '../index.module.scss';

View file

@ -9,7 +9,7 @@ import SearchIcon from '@/assets/images/search.svg';
import Button from '@/components/Button';
import OverlayScrollbar from '@/components/OverlayScrollbar';
import TextInput from '@/components/TextInput';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as style from './AddLanguageSelector.module.scss';

View file

@ -3,7 +3,7 @@ import { languages } from '@logto/language-kit';
import classNames from 'classnames';
import { useEffect, useRef } from 'react';
import { onKeyDownHandler } from '@/utilities/a11y';
import { onKeyDownHandler } from '@/utils/a11y';
import * as style from './LanguageItem.module.scss';

View file

@ -22,7 +22,7 @@ import {
createSignInMethod,
getSignInMethodPasswordCheckState,
getSignInMethodVerificationCodeCheckState,
} from './utilities';
} from './utils';
const SignUpForm = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });

View file

@ -13,7 +13,7 @@ import {
import type { SignInExperienceForm } from '@/pages/SignInExperience/types';
import { getSignUpRequiredConnectorTypes } from '@/pages/SignInExperience/utils/identifier';
import { createSignInMethod } from '../../utilities';
import { createSignInMethod } from '../../utils';
import AddButton from './AddButton';
import SignInMethodItem from './SignInMethodItem';
import * as styles from './index.module.scss';

View file

@ -7,7 +7,7 @@ import {
hasSignInMethodsChanged,
hasSignUpSettingsChanged,
hasSocialTargetsChanged,
} from '../components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/utilities';
} from '../components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/utils';
import { signUpIdentifiersMapping } from '../constants';
import { SignUpIdentifier } from '../types';
import type { SignInExperienceForm, SignUpForm } from '../types';

Some files were not shown because too many files have changed in this diff Show more