mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(console): refactor table component (#2738)
This commit is contained in:
parent
8592d22a38
commit
b403f19558
11 changed files with 369 additions and 309 deletions
|
@ -1,27 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
background-color: var(--color-layer-1);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
table {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.bodyTable {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
padding-bottom: _.unit(2);
|
||||
|
||||
tr {
|
||||
&:last-child {
|
||||
td {
|
||||
border-bottom: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import { assert } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import * as styles from './StickyHeaderTable.module.scss';
|
||||
|
||||
type Props = {
|
||||
header: ReactElement<HTMLTableSectionElement>;
|
||||
colGroup?: ReactElement<HTMLTableColElement>;
|
||||
className?: string;
|
||||
children: ReactElement<HTMLTableSectionElement>;
|
||||
};
|
||||
|
||||
const StickyHeaderTable = ({ header, colGroup, className, children: body }: Props) => {
|
||||
assert(header.props.tagName !== 'THEAD', new Error('Expected <thead> for the `header` prop'));
|
||||
|
||||
assert(
|
||||
colGroup?.props.tagName !== 'COLGROUP',
|
||||
new Error('Expected <colgroup> for the `colGroup` prop')
|
||||
);
|
||||
|
||||
assert(body.props.tagName !== 'TBODY', new Error('Expected <tbody> for the `children` prop'));
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<table>
|
||||
{colGroup}
|
||||
{header}
|
||||
</table>
|
||||
<div className={styles.bodyTable}>
|
||||
<table>
|
||||
{colGroup}
|
||||
{body}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StickyHeaderTable;
|
75
packages/console/src/components/Table/index.module.scss
Normal file
75
packages/console/src/components/Table/index.module.scss
Normal file
|
@ -0,0 +1,75 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
table {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.headerTable {
|
||||
background-color: var(--color-layer-1);
|
||||
border-radius: 12px 12px 0 0;
|
||||
padding: 0 _.unit(3);
|
||||
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
font: var(--font-subhead-2);
|
||||
color: var(--color-text);
|
||||
border-bottom: unset;
|
||||
padding: _.unit(3);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bodyTable {
|
||||
overflow-y: auto;
|
||||
padding: 0 _.unit(3) _.unit(3);
|
||||
background-color: var(--color-layer-1);
|
||||
border-radius: 0 0 12px 12px;
|
||||
|
||||
table {
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
font: var(--font-body-medium);
|
||||
border-top: 1px solid var(--color-divider);
|
||||
border-bottom: unset;
|
||||
padding: _.unit(3);
|
||||
}
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-hover);
|
||||
|
||||
td {
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
|
||||
+ tr {
|
||||
td {
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
107
packages/console/src/components/Table/index.tsx
Normal file
107
packages/console/src/components/Table/index.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import type { FieldPath, FieldValues } from 'react-hook-form';
|
||||
|
||||
import TableEmpty from './TableEmpty';
|
||||
import TableError from './TableError';
|
||||
import TableLoading from './TableLoading';
|
||||
import * as styles from './index.module.scss';
|
||||
import type { Column, RowGroup } from './types';
|
||||
|
||||
type Props<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
rowGroups: Array<RowGroup<TFieldValues>>;
|
||||
columns: Array<Column<TFieldValues, TName>>;
|
||||
rowIndexKey: TName;
|
||||
onClickRow?: (row: TFieldValues) => void;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
bodyClassName?: string;
|
||||
isLoading?: boolean;
|
||||
placeholder?: ReactNode;
|
||||
errorMessage?: string;
|
||||
onRetry?: () => void;
|
||||
};
|
||||
|
||||
const Table = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
rowGroups,
|
||||
columns,
|
||||
rowIndexKey,
|
||||
onClickRow,
|
||||
className,
|
||||
headerClassName,
|
||||
bodyClassName,
|
||||
isLoading,
|
||||
placeholder,
|
||||
errorMessage,
|
||||
onRetry,
|
||||
}: Props<TFieldValues, TName>) => {
|
||||
const totalColspan = columns.reduce((result, { colSpan }) => {
|
||||
return result + (colSpan ?? 1);
|
||||
}, 0);
|
||||
|
||||
const hasData = rowGroups.some(({ data }) => data?.length);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<table className={classNames(styles.headerTable, headerClassName)}>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ title, colSpan, dataIndex }) => (
|
||||
<th key={dataIndex} colSpan={colSpan}>
|
||||
{title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div className={classNames(styles.bodyTable, bodyClassName)}>
|
||||
<table>
|
||||
<tbody>
|
||||
{isLoading && <TableLoading columns={columns.length} />}
|
||||
{!hasData && errorMessage && (
|
||||
<TableError columns={columns.length} content={errorMessage} onRetry={onRetry} />
|
||||
)}
|
||||
{!isLoading && !hasData && placeholder && (
|
||||
<TableEmpty columns={columns.length}>{placeholder}</TableEmpty>
|
||||
)}
|
||||
{rowGroups.map(({ key, label, labelClassName, data }) => (
|
||||
<Fragment key={key}>
|
||||
{label && (
|
||||
<tr>
|
||||
<td colSpan={totalColspan} className={labelClassName}>
|
||||
{label}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{data?.map((row) => (
|
||||
<tr
|
||||
key={row[rowIndexKey]}
|
||||
className={classNames(onClickRow && styles.clickable)}
|
||||
onClick={() => {
|
||||
onClickRow?.(row);
|
||||
}}
|
||||
>
|
||||
{columns.map(({ dataIndex, colSpan, className, render }) => (
|
||||
<td key={dataIndex} colSpan={colSpan} className={className}>
|
||||
{render(row[dataIndex], row)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
20
packages/console/src/components/Table/types.ts
Normal file
20
packages/console/src/components/Table/types.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { Key, ReactNode } from 'react';
|
||||
import type { FieldPath, FieldPathValue, FieldValues } from 'react-hook-form';
|
||||
|
||||
export type Column<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
title: ReactNode;
|
||||
dataIndex: TName;
|
||||
render: (value: FieldPathValue<TFieldValues, TName>, row: TFieldValues) => ReactNode;
|
||||
colSpan?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type RowGroup<TFieldValues extends FieldValues = FieldValues> = {
|
||||
key: Key;
|
||||
label?: ReactNode;
|
||||
labelClassName?: string;
|
||||
data?: TFieldValues[];
|
||||
};
|
|
@ -12,14 +12,10 @@ import CardTitle from '@/components/CardTitle';
|
|||
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||
import ItemPreview from '@/components/ItemPreview';
|
||||
import Pagination from '@/components/Pagination';
|
||||
import StickyHeaderTable from '@/components/Table/StickyHeaderTable';
|
||||
import TableEmpty from '@/components/Table/TableEmpty';
|
||||
import TableError from '@/components/Table/TableError';
|
||||
import TableLoading from '@/components/Table/TableLoading';
|
||||
import Table from '@/components/Table';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import * as resourcesStyles from '@/scss/resources.module.scss';
|
||||
import * as tableStyles from '@/scss/table.module.scss';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
|
||||
import CreateForm from './components/CreateForm';
|
||||
|
@ -89,69 +85,50 @@ const Applications = () => {
|
|||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
<StickyHeaderTable
|
||||
<Table
|
||||
className={resourcesStyles.table}
|
||||
header={
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('applications.application_name')}</th>
|
||||
<th>{t('applications.app_id')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
}
|
||||
colGroup={
|
||||
<colgroup>
|
||||
<col className={styles.applicationName} />
|
||||
<col />
|
||||
</colgroup>
|
||||
}
|
||||
>
|
||||
<tbody>
|
||||
{!data && error && (
|
||||
<TableError
|
||||
columns={2}
|
||||
content={error.body?.message ?? error.message}
|
||||
onRetry={async () => mutate(undefined, true)}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <TableLoading columns={2} />}
|
||||
{applications?.length === 0 && (
|
||||
<TableEmpty columns={2}>
|
||||
<Button
|
||||
title="applications.create"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
navigate({
|
||||
pathname: createApplicationPathname,
|
||||
search,
|
||||
});
|
||||
}}
|
||||
rowGroups={[{ key: 'applications', data: applications }]}
|
||||
rowIndexKey="id"
|
||||
isLoading={isLoading}
|
||||
errorMessage={error?.body?.message ?? error?.message}
|
||||
columns={[
|
||||
{
|
||||
title: t('applications.application_name'),
|
||||
dataIndex: 'name',
|
||||
colSpan: 6,
|
||||
render: (name, { type, id }) => (
|
||||
<ItemPreview
|
||||
title={name}
|
||||
subtitle={t(`${applicationTypeI18nKey[type]}.title`)}
|
||||
icon={<ApplicationIcon className={styles.icon} type={type} />}
|
||||
to={buildDetailsPathname(id)}
|
||||
/>
|
||||
</TableEmpty>
|
||||
)}
|
||||
{applications?.map(({ id, name, type }) => (
|
||||
<tr
|
||||
key={id}
|
||||
className={tableStyles.clickable}
|
||||
onClick={() => {
|
||||
navigate(buildDetailsPathname(id));
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<ItemPreview
|
||||
title={name}
|
||||
subtitle={t(`${applicationTypeI18nKey[type]}.title`)}
|
||||
icon={<ApplicationIcon className={styles.icon} type={type} />}
|
||||
to={buildDetailsPathname(id)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<CopyToClipboard value={id} variant="text" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</StickyHeaderTable>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('applications.app_id'),
|
||||
dataIndex: 'id',
|
||||
colSpan: 10,
|
||||
render: (id) => <CopyToClipboard value={id} variant="text" />,
|
||||
},
|
||||
]}
|
||||
placeholder={
|
||||
<Button
|
||||
title="applications.create"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
navigate({
|
||||
pathname: createApplicationPathname,
|
||||
search,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClickRow={({ id }) => {
|
||||
navigate(buildDetailsPathname(id));
|
||||
}}
|
||||
onRetry={async () => mutate(undefined, true)}
|
||||
/>
|
||||
<Pagination
|
||||
pageIndex={pageIndex}
|
||||
totalCount={totalCount}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.sectionTitle {
|
||||
@include _.subhead-cap-small;
|
||||
color: var(--color-neutral-variant-60);
|
||||
background-color: var(--color-layer-light);
|
||||
padding: _.unit(1) 0;
|
||||
}
|
||||
|
||||
.sectionDataKey {
|
||||
padding: _.unit(4) _.unit(5);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.sectionBuiltInText {
|
||||
padding: _.unit(2) _.unit(3);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
background: var(--color-layer-2);
|
||||
}
|
||||
|
||||
.inputCell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sectionInputArea {
|
||||
position: absolute;
|
||||
inset: _.unit(2) _.unit(5);
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import type { Translation } from '@logto/schemas';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import Textarea from '@/components/Textarea';
|
||||
|
||||
import * as style from './EditSection.module.scss';
|
||||
|
||||
type EditSectionProps = {
|
||||
dataKey: string;
|
||||
data: Record<string, string>;
|
||||
};
|
||||
|
||||
const EditSection = ({ dataKey, data }: EditSectionProps) => {
|
||||
const { register } = useFormContext<Translation>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td colSpan={3} className={style.sectionTitle}>
|
||||
{dataKey}
|
||||
</td>
|
||||
</tr>
|
||||
{Object.entries(data).map(([field, value]) => {
|
||||
const fieldKey = `${dataKey}.${field}`;
|
||||
|
||||
return (
|
||||
<tr key={fieldKey}>
|
||||
<td className={style.sectionDataKey}>{field}</td>
|
||||
<td>
|
||||
<div className={style.sectionBuiltInText}>{value}</div>
|
||||
</td>
|
||||
<td className={style.inputCell}>
|
||||
<Textarea className={style.sectionInputArea} {...register(fieldKey)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSection;
|
|
@ -33,16 +33,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid var(--color-divider);
|
||||
overflow: hidden;
|
||||
|
||||
.tableWrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
border-top: 1px solid var(--color-divider);
|
||||
overflow-y: auto;
|
||||
|
||||
.customValuesColumn {
|
||||
display: flex;
|
||||
|
@ -58,32 +61,9 @@
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
> table {
|
||||
border: none;
|
||||
|
||||
> thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
// Note: cells with `position: relative` style will overlap this sticky header, add a z-index to fix it.
|
||||
z-index: 1;
|
||||
|
||||
tr > th {
|
||||
padding: _.unit(1) _.unit(5);
|
||||
font: var(--font-label-large);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-layer-1);
|
||||
}
|
||||
}
|
||||
|
||||
> tbody > tr > td {
|
||||
padding: _.unit(2) _.unit(5);
|
||||
border: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
@include _.subhead-cap;
|
||||
@include _.subhead-cap-small;
|
||||
color: var(--color-neutral-variant-60);
|
||||
background-color: var(--color-layer-light);
|
||||
}
|
||||
|
||||
|
@ -94,7 +74,44 @@
|
|||
}
|
||||
|
||||
.sectionBuiltInText {
|
||||
padding: _.unit(2) 0;
|
||||
padding: _.unit(2) _.unit(3);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
background: var(--color-layer-2);
|
||||
}
|
||||
|
||||
.inputCell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sectionInputArea {
|
||||
position: absolute;
|
||||
inset: _.unit(2) _.unit(5);
|
||||
}
|
||||
|
||||
table {
|
||||
border: none;
|
||||
|
||||
thead {
|
||||
tr > th {
|
||||
padding: _.unit(1) _.unit(5);
|
||||
font: var(--font-label-large);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-layer-1);
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
padding: _.unit(2) _.unit(5);
|
||||
border: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { SignInExperience, Translation } from '@logto/schemas';
|
|||
import cleanDeep from 'clean-deep';
|
||||
import deepmerge from 'deepmerge';
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
|
@ -16,6 +16,8 @@ import Delete from '@/assets/images/delete.svg';
|
|||
import Button from '@/components/Button';
|
||||
import ConfirmModal from '@/components/ConfirmModal';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import Table from '@/components/Table';
|
||||
import Textarea from '@/components/Textarea';
|
||||
import { Tooltip } from '@/components/Tip';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import useUiLanguages from '@/hooks/use-ui-languages';
|
||||
|
@ -25,8 +27,7 @@ import {
|
|||
} from '@/pages/SignInExperience/utils/language';
|
||||
import type { CustomPhraseResponse } from '@/types/custom-phrase';
|
||||
|
||||
import EditSection from './EditSection';
|
||||
import * as style from './LanguageDetails.module.scss';
|
||||
import * as styles from './LanguageDetails.module.scss';
|
||||
import { LanguageEditorContext } from './use-language-editor-context';
|
||||
|
||||
const emptyUiTranslation = createEmptyUiTranslation();
|
||||
|
@ -69,16 +70,15 @@ const LanguageDetails = () => {
|
|||
[customPhrase]
|
||||
);
|
||||
|
||||
const formMethods = useForm<Translation>({
|
||||
defaultValues: defaultFormValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
register,
|
||||
formState: { isSubmitting, isDirty, dirtyFields },
|
||||
} = formMethods;
|
||||
} = useForm<Translation>({
|
||||
defaultValues: defaultFormValues,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
|
@ -133,6 +133,7 @@ const LanguageDetails = () => {
|
|||
}, [api, globalMutate, isDefaultLanguage, languages, selectedLanguage, setSelectedLanguage]);
|
||||
|
||||
const onSubmit = handleSubmit(async (formData: Translation) => {
|
||||
console.log(formData);
|
||||
const updatedCustomPhrase = await upsertCustomPhrase(selectedLanguage, formData);
|
||||
void mutate(updatedCustomPhrase);
|
||||
toast.success(t('general.saved'));
|
||||
|
@ -151,13 +152,13 @@ const LanguageDetails = () => {
|
|||
]);
|
||||
|
||||
return (
|
||||
<div className={style.languageDetails}>
|
||||
<div className={style.title}>
|
||||
<div className={style.languageInfo}>
|
||||
<div className={styles.languageDetails}>
|
||||
<div className={styles.title}>
|
||||
<div className={styles.languageInfo}>
|
||||
{uiLanguageNameMapping[selectedLanguage]}
|
||||
<span>{selectedLanguage}</span>
|
||||
{isBuiltIn && (
|
||||
<span className={style.builtInFlag}>
|
||||
<span className={styles.builtInFlag}>
|
||||
{t('sign_in_exp.others.manage_language.logto_provided')}
|
||||
</span>
|
||||
)}
|
||||
|
@ -174,64 +175,77 @@ const LanguageDetails = () => {
|
|||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
className={style.form}
|
||||
onSubmit={async (event) => {
|
||||
// Note: Avoid propagating the 'submit' event to the outer sign-in-experience form.
|
||||
event.stopPropagation();
|
||||
|
||||
return onSubmit(event);
|
||||
}}
|
||||
>
|
||||
<div className={style.content}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('sign_in_exp.others.manage_language.key')}</th>
|
||||
<th>{t('sign_in_exp.others.manage_language.logto_source_values')}</th>
|
||||
<th>
|
||||
<span className={style.customValuesColumn}>
|
||||
{t('sign_in_exp.others.manage_language.custom_values')}
|
||||
<Tooltip
|
||||
anchorClassName={style.clearButton}
|
||||
content={t('sign_in_exp.others.manage_language.clear_all_tip')}
|
||||
<div className={styles.container}>
|
||||
<Table
|
||||
className={styles.content}
|
||||
headerClassName={styles.tableWrapper}
|
||||
bodyClassName={styles.tableWrapper}
|
||||
rowIndexKey="phraseKey"
|
||||
rowGroups={translationEntries.map(([groupKey, value]) => ({
|
||||
key: groupKey,
|
||||
label: groupKey,
|
||||
labelClassName: styles.sectionTitle,
|
||||
data: Object.entries(flattenTranslation(value)).map(([phraseKey, value]) => ({
|
||||
phraseKey,
|
||||
sourceValue: value,
|
||||
fieldKey: `${groupKey}.${phraseKey}`,
|
||||
})),
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
title: t('sign_in_exp.others.manage_language.key'),
|
||||
dataIndex: 'phraseKey',
|
||||
render: (phraseKey) => phraseKey,
|
||||
className: styles.sectionDataKey,
|
||||
},
|
||||
{
|
||||
title: t('sign_in_exp.others.manage_language.logto_source_values'),
|
||||
dataIndex: 'sourceValue',
|
||||
render: (sourceValue) => (
|
||||
<div className={styles.sectionBuiltInText}>{sourceValue}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<span className={styles.customValuesColumn}>
|
||||
{t('sign_in_exp.others.manage_language.custom_values')}
|
||||
<Tooltip
|
||||
anchorClassName={styles.clearButton}
|
||||
content={t('sign_in_exp.others.manage_language.clear_all_tip')}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
for (const [key, value] of Object.entries(
|
||||
flattenTranslation(emptyUiTranslation)
|
||||
)) {
|
||||
setValue(key, value, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
for (const [key, value] of Object.entries(
|
||||
flattenTranslation(emptyUiTranslation)
|
||||
)) {
|
||||
setValue(key, value, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clear className={style.clearIcon} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<FormProvider {...formMethods}>
|
||||
{translationEntries.map(([key, value]) => (
|
||||
<EditSection key={key} dataKey={key} data={flattenTranslation(value)} />
|
||||
))}
|
||||
</FormProvider>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={style.footer}>
|
||||
<Clear className={styles.clearIcon} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
),
|
||||
dataIndex: 'fieldKey',
|
||||
render: (fieldKey) => (
|
||||
<Textarea className={styles.sectionInputArea} {...register(fieldKey)} />
|
||||
),
|
||||
className: styles.inputCell,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
title="general.save"
|
||||
onClick={async () => onSubmit()}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
isOpen={isDeletionAlertOpen}
|
||||
title={
|
||||
|
|
|
@ -33,11 +33,6 @@ table {
|
|||
padding: _.unit(3);
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
text-align: left;
|
||||
|
||||
&:first-child,
|
||||
&:last-child {
|
||||
padding: _.unit(3) _.unit(8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,12 +40,7 @@ table {
|
|||
td {
|
||||
font: var(--font-body-medium);
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
padding: _.unit(3) _.unit(2);
|
||||
|
||||
&:first-child,
|
||||
&:last-child {
|
||||
padding: _.unit(3) _.unit(8);
|
||||
}
|
||||
padding: _.unit(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue