mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
refactor: improve guide responsive rules (#4436)
This commit is contained in:
parent
9995154832
commit
f1ded1168b
22 changed files with 346 additions and 315 deletions
packages/console/src
assets/docs/guides/spa-vanilla
mdx-components
pages
ApplicationDetails
Applications
scss
|
@ -115,7 +115,6 @@ After signing out, it'll be great to redirect user back to your website. Let's a
|
|||
|
||||
<UriInputField
|
||||
appId={props.app.id}
|
||||
isSingle={!props.isCompact}
|
||||
name="postLogoutRedirectUris"
|
||||
title="application_details.post_sign_out_redirect_uri"
|
||||
/>
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
@use '@/scss/dimensions' as dim;
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navigationAnchor {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
transform: translateX(-100%);
|
||||
inset: _.unit(6) auto _.unit(6) _.unit(6);
|
||||
}
|
||||
|
||||
.navigation {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
top: _.unit(6);
|
||||
flex-shrink: 0;
|
||||
margin-right: _.unit(4);
|
||||
margin-right: _.unit(7.5);
|
||||
width: 220px;
|
||||
|
||||
> :not(:last-child) {
|
||||
|
@ -27,11 +23,21 @@
|
|||
}
|
||||
|
||||
.content {
|
||||
max-width: 858px;
|
||||
width: 100%;
|
||||
min-width: dim.$guide-content-min-width;
|
||||
max-width: dim.$guide-content-max-width;
|
||||
padding: dim.$guide-content-padding calc(dim.$guide-sidebar-width + dim.$guide-panel-gap + dim.$guide-content-padding);
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
|
||||
> :not(:last-child) {
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
|
||||
&.compact {
|
||||
min-width: 652px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stepper {
|
||||
|
@ -51,3 +57,11 @@
|
|||
background: var(--color-focused-variant);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: dim.$guide-content-max-width) {
|
||||
.content {
|
||||
margin: 0;
|
||||
padding-right: dim.$guide-content-padding;
|
||||
max-width: calc(dim.$guide-main-content-max-width + dim.$guide-sidebar-width + dim.$guide-panel-gap + 2 * dim.$guide-content-padding);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ export default function Steps({ children: reactChildren }: Props) {
|
|||
: [reactChildren, furtherReadings],
|
||||
[furtherReadings, reactChildren]
|
||||
);
|
||||
|
||||
const { isCompact } = useContext(GuideContext);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -85,30 +86,30 @@ export default function Steps({ children: reactChildren }: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.wrapper, isCompact && styles.fullWidth)}>
|
||||
{!isCompact && (
|
||||
<div className={styles.navigationAnchor}>
|
||||
<nav className={styles.navigation}>
|
||||
{children.map((component, index) => (
|
||||
<div
|
||||
key={component.props.title}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={classNames(styles.stepper, index === activeIndex && styles.active)}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
navigateToStep(index);
|
||||
})}
|
||||
onClick={() => {
|
||||
navigateToStep(index);
|
||||
}}
|
||||
>
|
||||
{index + 1}. {component.props.title}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
<div ref={contentRef} className={styles.content}>
|
||||
<div className={styles.wrapper}>
|
||||
<div ref={contentRef} className={classNames(styles.content, isCompact && styles.compact)}>
|
||||
{!isCompact && (
|
||||
<div className={styles.navigationAnchor}>
|
||||
<nav className={styles.navigation}>
|
||||
{children.map((component, index) => (
|
||||
<div
|
||||
key={component.props.title}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={classNames(styles.stepper, index === activeIndex && styles.active)}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
navigateToStep(index);
|
||||
})}
|
||||
onClick={() => {
|
||||
navigateToStep(index);
|
||||
}}
|
||||
>
|
||||
{index + 1}. {component.props.title}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
<Sample />
|
||||
{children.map((component, index) =>
|
||||
React.cloneElement(component, {
|
||||
|
|
|
@ -10,12 +10,10 @@ import useSWR from 'swr';
|
|||
|
||||
import MultiTextInputField from '@/components/MultiTextInputField';
|
||||
import Button from '@/ds-components/Button';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import {
|
||||
convertRhfErrorMessage,
|
||||
createValidatorForRhf,
|
||||
} from '@/ds-components/MultiTextInput/utils';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { GuideContext } from '@/pages/Applications/components/Guide';
|
||||
|
@ -42,9 +40,7 @@ function UriInputField({ name, defaultValue }: Props) {
|
|||
} = methods;
|
||||
const {
|
||||
app: { id: appId },
|
||||
isCompact,
|
||||
} = useContext(GuideContext);
|
||||
const isSingle = !isCompact;
|
||||
const { data, mutate } = useSWR<Application, RequestError>(`api/applications/${appId}`);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
@ -93,10 +89,7 @@ function UriInputField({ name, defaultValue }: Props) {
|
|||
defaultValue={defaultValueArray}
|
||||
rules={{
|
||||
validate: createValidatorForRhf({
|
||||
required: t(
|
||||
isSingle ? 'errors.required_field_missing' : 'errors.required_field_missing_plural',
|
||||
{ field: title }
|
||||
),
|
||||
required: t('errors.required_field_missing_plural', { field: title }),
|
||||
pattern: {
|
||||
verify: (value) => !value || uriValidator(value),
|
||||
message: t('errors.invalid_uri_format'),
|
||||
|
@ -108,39 +101,18 @@ function UriInputField({ name, defaultValue }: Props) {
|
|||
|
||||
return (
|
||||
<div ref={ref} className={styles.wrapper}>
|
||||
{isSingle && (
|
||||
<FormField
|
||||
isRequired={name === 'redirectUris'}
|
||||
className={styles.field}
|
||||
title={title}
|
||||
>
|
||||
<TextInput
|
||||
className={styles.field}
|
||||
value={value[0]}
|
||||
error={errorObject?.required ?? errorObject?.inputs?.[0]}
|
||||
onChange={({ currentTarget: { value } }) => {
|
||||
onChange([value]);
|
||||
}}
|
||||
onKeyPress={(event) => {
|
||||
onKeyPress(event, value);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
{!isSingle && (
|
||||
<MultiTextInputField
|
||||
isRequired={name === 'redirectUris'}
|
||||
formFieldClassName={styles.field}
|
||||
title={title}
|
||||
value={value}
|
||||
error={errorObject}
|
||||
className={styles.multiTextInput}
|
||||
onChange={onChange}
|
||||
onKeyPress={(event) => {
|
||||
onKeyPress(event, value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<MultiTextInputField
|
||||
isRequired={name === 'redirectUris'}
|
||||
formFieldClassName={styles.field}
|
||||
title={title}
|
||||
value={value}
|
||||
error={errorObject}
|
||||
className={styles.multiTextInput}
|
||||
onChange={onChange}
|
||||
onKeyPress={(event) => {
|
||||
onKeyPress(event, value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className={styles.saveButton}
|
||||
disabled={!isDirty}
|
||||
|
|
|
@ -32,6 +32,6 @@
|
|||
|
||||
.guide {
|
||||
flex: 1;
|
||||
height: unset;
|
||||
overflow: hidden;
|
||||
padding: _.unit(6);
|
||||
}
|
||||
|
|
|
@ -74,7 +74,6 @@ function GuideDrawer({ app, onClose }: Props) {
|
|||
</div>
|
||||
{!selectedGuide && (
|
||||
<GuideGroup
|
||||
isCompact
|
||||
className={styles.cardGroup}
|
||||
categoryName={t(`categories.${app.type}`)}
|
||||
guides={structuredMetadata[app.type]}
|
||||
|
|
|
@ -36,7 +36,7 @@ import useTenantPathname from '@/hooks/use-tenant-pathname';
|
|||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import GuideModal from '../Applications/components/Guide/GuideModal';
|
||||
import GuideModal from '../Applications/components/GuideModal';
|
||||
|
||||
import AdvancedSettings from './components/AdvancedSettings';
|
||||
import GuideDrawer from './components/GuideDrawer';
|
||||
|
|
|
@ -1,60 +1,47 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
@use '@/scss/dimensions' as dim;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-base);
|
||||
height: 100vh;
|
||||
.content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-y: auto;
|
||||
padding: _.unit(6) _.unit(6) max(10vh, 120px);
|
||||
section p {
|
||||
font: var(--font-body-2);
|
||||
margin: _.unit(4) 0;
|
||||
}
|
||||
|
||||
section p {
|
||||
font: var(--font-body-2);
|
||||
margin: _.unit(4) 0;
|
||||
section ul > li,
|
||||
section ol > li {
|
||||
font: var(--font-body-2);
|
||||
margin-block: _.unit(2);
|
||||
padding-inline-start: _.unit(1);
|
||||
}
|
||||
|
||||
section table {
|
||||
border-spacing: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
font: var(--font-body-2);
|
||||
|
||||
tr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section ul > li,
|
||||
section ol > li {
|
||||
font: var(--font-body-2);
|
||||
margin-block: _.unit(2);
|
||||
padding-inline-start: _.unit(1);
|
||||
td,
|
||||
th {
|
||||
padding: _.unit(2) _.unit(4);
|
||||
}
|
||||
|
||||
section table {
|
||||
border-spacing: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
font: var(--font-body-2);
|
||||
thead {
|
||||
font: var(--font-title-3);
|
||||
}
|
||||
|
||||
tr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: _.unit(2) _.unit(4);
|
||||
}
|
||||
|
||||
thead {
|
||||
font: var(--font-title-3);
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
tbody td {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.markdownContent {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.notFound {
|
||||
width: 100%;
|
||||
|
||||
|
@ -64,16 +51,15 @@
|
|||
}
|
||||
|
||||
.actionBar {
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
padding: _.unit(4);
|
||||
flex-shrink: 0;
|
||||
padding: _.unit(4) _.unit(6);
|
||||
background-color: var(--color-layer-1);
|
||||
box-shadow: var(--shadow-3);
|
||||
z-index: 1;
|
||||
|
||||
.layout {
|
||||
margin: 0 auto;
|
||||
max-width: 858px;
|
||||
max-width: dim.$guide-main-content-max-width;
|
||||
|
||||
> button {
|
||||
margin-right: 0;
|
||||
|
|
|
@ -9,12 +9,12 @@ import { type GuideMetadata } from '@/assets/docs/guides/types';
|
|||
import { AppDataContext } from '@/contexts/AppDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import CodeEditor from '@/ds-components/CodeEditor';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useCustomDomain from '@/hooks/use-custom-domain';
|
||||
import DetailsSummary from '@/mdx-components/DetailsSummary';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
import GuideHeader from '../GuideHeader';
|
||||
import StepsSkeleton from '../StepsSkeleton';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -86,9 +86,8 @@ function Guide({ className, guideId, app, isCompact, onClose }: Props) {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
{!isCompact && <GuideHeader onClose={onClose} />}
|
||||
<div className={styles.content}>
|
||||
<>
|
||||
<OverlayScrollbar className={classNames(styles.content, className)}>
|
||||
{memorizedContext ? (
|
||||
<GuideContext.Provider value={memorizedContext}>
|
||||
<MDXProvider
|
||||
|
@ -125,20 +124,20 @@ function Guide({ className, guideId, app, isCompact, onClose }: Props) {
|
|||
) : (
|
||||
<NotFound className={styles.notFound} />
|
||||
)}
|
||||
{!isCompact && memorizedContext && (
|
||||
<nav className={styles.actionBar}>
|
||||
<div className={styles.layout}>
|
||||
<Button
|
||||
size="large"
|
||||
title="applications.guide.finish_and_done"
|
||||
type="primary"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
{memorizedContext && (
|
||||
<nav className={styles.actionBar}>
|
||||
<div className={styles.layout}>
|
||||
<Button
|
||||
size="large"
|
||||
title="applications.guide.finish_and_done"
|
||||
type="primary"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,9 +11,10 @@
|
|||
min-width: 220px;
|
||||
max-width: 460px;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
|
||||
&.compact {
|
||||
cursor: pointer;
|
||||
&.hasButton {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.hasBorder {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ApplicationType } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { Suspense, useContext } from 'react';
|
||||
import { Suspense, useCallback, useContext } from 'react';
|
||||
|
||||
import { type Guide, type GuideMetadata } from '@/assets/docs/guides/types';
|
||||
import ProTag from '@/components/ProTag';
|
||||
|
@ -24,10 +24,10 @@ type Props = {
|
|||
data: Guide;
|
||||
onClick: (data: SelectedGuide) => void;
|
||||
hasBorder?: boolean;
|
||||
isCompact?: boolean;
|
||||
hasButton?: boolean;
|
||||
};
|
||||
|
||||
function GuideCard({ data, onClick, hasBorder, isCompact }: Props) {
|
||||
function GuideCard({ data, onClick, hasBorder, hasButton }: Props) {
|
||||
const { navigate } = useTenantPathname();
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { data: currentPlan } = useSubscriptionPlan(currentTenantId);
|
||||
|
@ -41,26 +41,26 @@ function GuideCard({ data, onClick, hasBorder, isCompact }: Props) {
|
|||
metadata: { target, name, description },
|
||||
} = data;
|
||||
|
||||
const onClickCard = () => {
|
||||
if (!isCompact) {
|
||||
return;
|
||||
const handleClick = useCallback(() => {
|
||||
if (isSubscriptionRequired) {
|
||||
navigate(subscriptionPage);
|
||||
} else {
|
||||
onClick({ id, target, name });
|
||||
}
|
||||
|
||||
onClick({ id, target, name });
|
||||
};
|
||||
}, [id, isSubscriptionRequired, name, target, navigate, onClick]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.card,
|
||||
hasBorder && styles.hasBorder,
|
||||
isCompact && styles.compact
|
||||
hasButton && styles.hasButton
|
||||
)}
|
||||
{...(isCompact && {
|
||||
{...(!hasButton && {
|
||||
tabIndex: 0,
|
||||
role: 'button',
|
||||
onKeyDown: onKeyDownHandler(onClickCard),
|
||||
onClick: onClickCard,
|
||||
onKeyDown: onKeyDownHandler(handleClick),
|
||||
onClick: handleClick,
|
||||
})}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
|
@ -77,19 +77,13 @@ function GuideCard({ data, onClick, hasBorder, isCompact }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isCompact && (
|
||||
{hasButton && (
|
||||
<Button
|
||||
title={
|
||||
isSubscriptionRequired ? 'upsell.upgrade_plan' : 'applications.guide.start_building'
|
||||
}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (isSubscriptionRequired) {
|
||||
navigate(subscriptionPage);
|
||||
} else {
|
||||
onClick({ id, target, name });
|
||||
}
|
||||
}}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -12,12 +12,12 @@ type GuideGroupProps = {
|
|||
categoryName?: string;
|
||||
guides?: readonly Guide[];
|
||||
hasCardBorder?: boolean;
|
||||
isCompact?: boolean;
|
||||
hasCardButton?: boolean;
|
||||
onClickGuide: (data: SelectedGuide) => void;
|
||||
};
|
||||
|
||||
function GuideGroup(
|
||||
{ className, categoryName, guides, hasCardBorder, isCompact, onClickGuide }: GuideGroupProps,
|
||||
{ className, categoryName, guides, hasCardBorder, hasCardButton, onClickGuide }: GuideGroupProps,
|
||||
ref: Ref<HTMLDivElement>
|
||||
) {
|
||||
if (!guides?.length) {
|
||||
|
@ -31,8 +31,8 @@ function GuideGroup(
|
|||
{guides.map((guide) => (
|
||||
<GuideCard
|
||||
key={guide.id}
|
||||
isCompact={isCompact}
|
||||
hasBorder={hasCardBorder}
|
||||
hasButton={hasCardButton}
|
||||
data={guide}
|
||||
onClick={onClickGuide}
|
||||
/>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-base);
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
padding: 0 _.unit(6);
|
||||
flex-shrink: 0;
|
||||
|
@ -33,6 +34,12 @@
|
|||
}
|
||||
|
||||
.requestSdkButton {
|
||||
margin-right: _.unit(2);
|
||||
margin-right: _.unit(15);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 918px) {
|
||||
.header .requestSdkButton {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Box from '@/assets/icons/box.svg';
|
||||
import Close from '@/assets/icons/close.svg';
|
||||
|
@ -9,19 +8,15 @@ import Button from '@/ds-components/Button';
|
|||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import IconButton from '@/ds-components/IconButton';
|
||||
import Spacer from '@/ds-components/Spacer';
|
||||
import Tooltip from '@/ds-components/Tip/Tooltip';
|
||||
|
||||
import RequestGuide from './RequestGuide';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
isCompact?: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function GuideHeader({ isCompact = false, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
function GuideHeader({ onClose }: Props) {
|
||||
const [isRequestGuideOpen, setIsRequestGuideOpen] = useState(false);
|
||||
const onRequestGuideClose = useCallback(() => {
|
||||
setIsRequestGuideOpen(false);
|
||||
|
@ -29,51 +24,29 @@ function GuideHeader({ isCompact = false, onClose }: Props) {
|
|||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
{isCompact && (
|
||||
<>
|
||||
<CardTitle
|
||||
size="small"
|
||||
title="applications.guide.modal_header_title"
|
||||
subtitle="applications.guide.header_subtitle"
|
||||
/>
|
||||
<Spacer />
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
anchorClassName={styles.githubToolTipAnchor}
|
||||
content={t('applications.guide.get_sample_file')}
|
||||
/>
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
{!isCompact && (
|
||||
<>
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
<div className={styles.separator} />
|
||||
<CardTitle
|
||||
size="small"
|
||||
title="applications.guide.modal_header_title"
|
||||
subtitle="applications.guide.header_subtitle"
|
||||
/>
|
||||
<Spacer />
|
||||
<Button
|
||||
className={styles.requestSdkButton}
|
||||
type="outline"
|
||||
icon={<Box />}
|
||||
title="applications.guide.cannot_find_guide"
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
setIsRequestGuideOpen(true);
|
||||
} else {
|
||||
window.open(githubIssuesLink, '_blank');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
<div className={styles.separator} />
|
||||
<CardTitle
|
||||
size="small"
|
||||
title="applications.guide.modal_header_title"
|
||||
subtitle="applications.guide.header_subtitle"
|
||||
/>
|
||||
<Spacer />
|
||||
<Button
|
||||
className={styles.requestSdkButton}
|
||||
type="outline"
|
||||
icon={<Box />}
|
||||
title="applications.guide.cannot_find_guide"
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
setIsRequestGuideOpen(true);
|
||||
} else {
|
||||
window.open(githubIssuesLink, '_blank');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isCloud && <RequestGuide isOpen={isRequestGuideOpen} onClose={onRequestGuideClose} />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,17 +1,36 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
@use '@/scss/dimensions' as dim;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
gap: _.unit(7);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
min-width: dim.$guide-content-min-width;
|
||||
max-width: dim.$guide-content-max-width;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
|
||||
&.hasFilters {
|
||||
padding: dim.$guide-content-padding calc(dim.$guide-sidebar-width + dim.$guide-panel-gap + dim.$guide-content-padding);
|
||||
}
|
||||
}
|
||||
|
||||
.filterAnchor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.filters {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: dim.$guide-sidebar-width;
|
||||
gap: _.unit(4);
|
||||
padding: _.unit(8) 0 _.unit(8) _.unit(11);
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
margin-right: dim.$guide-panel-gap;
|
||||
|
||||
label {
|
||||
font: var(--font-label-2);
|
||||
|
@ -45,16 +64,23 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: _.unit(8);
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
|
||||
> div {
|
||||
flex: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper.hasFilters .groups {
|
||||
max-width: dim.$guide-main-content-max-width;
|
||||
}
|
||||
|
||||
.guideGroup {
|
||||
flex: 1;
|
||||
margin: _.unit(8) _.unit(8) 0 0;
|
||||
|
||||
+ .guideGroup {
|
||||
margin-top: _.unit(8);
|
||||
}
|
||||
}
|
||||
|
||||
.emptyPlaceholder {
|
||||
|
@ -63,3 +89,11 @@
|
|||
width: 100%;
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: dim.$guide-content-max-width) {
|
||||
.wrapper.hasFilters {
|
||||
margin-left: 0;
|
||||
padding-right: dim.$guide-content-padding;
|
||||
max-width: calc(dim.$guide-main-content-max-width + dim.$guide-sidebar-width + dim.$guide-panel-gap + 2 * dim.$guide-content-padding);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,11 @@ import * as styles from './index.module.scss';
|
|||
type Props = {
|
||||
className?: string;
|
||||
hasCardBorder?: boolean;
|
||||
hasCardButton?: boolean;
|
||||
hasFilters?: boolean;
|
||||
};
|
||||
|
||||
function GuideLibrary({ className, hasCardBorder, hasFilters }: Props) {
|
||||
function GuideLibrary({ className, hasCardBorder, hasCardButton, hasFilters }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.applications.guide' });
|
||||
const { navigate } = useTenantPathname();
|
||||
const [keyword, setKeyword] = useState<string>('');
|
||||
|
@ -69,66 +70,71 @@ function GuideLibrary({ className, hasCardBorder, hasFilters }: Props) {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
{hasFilters && (
|
||||
<div className={styles.filters}>
|
||||
<label>{t('filter.title')}</label>
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
icon={<SearchIcon />}
|
||||
placeholder={t('filter.placeholder')}
|
||||
value={keyword}
|
||||
onChange={(event) => {
|
||||
setKeyword(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.checkboxGroupContainer}>
|
||||
<CheckboxGroup
|
||||
className={styles.checkboxGroup}
|
||||
options={allAppGuideCategories.map((category) => ({
|
||||
title: `applications.guide.categories.${category}`,
|
||||
value: category,
|
||||
}))}
|
||||
value={filterCategories}
|
||||
onChange={(value) => {
|
||||
const sortedValue = allAppGuideCategories.filter((category) =>
|
||||
value.includes(category)
|
||||
);
|
||||
setFilterCategories(sortedValue);
|
||||
}}
|
||||
/>
|
||||
{isM2mDisabledForCurrentPlan && <ProTag className={styles.proTag} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{keyword &&
|
||||
(filteredMetadata?.length ? (
|
||||
<GuideGroup
|
||||
className={styles.guideGroup}
|
||||
hasCardBorder={hasCardBorder}
|
||||
guides={filteredMetadata}
|
||||
onClickGuide={onClickGuide}
|
||||
/>
|
||||
) : (
|
||||
<EmptyDataPlaceholder className={styles.emptyPlaceholder} size="large" />
|
||||
))}
|
||||
{!keyword && (
|
||||
<OverlayScrollbar className={styles.groups}>
|
||||
{(filterCategories.length > 0 ? filterCategories : allAppGuideCategories).map(
|
||||
(category) =>
|
||||
structuredMetadata[category].length > 0 && (
|
||||
<GuideGroup
|
||||
key={category}
|
||||
className={styles.guideGroup}
|
||||
hasCardBorder={hasCardBorder}
|
||||
categoryName={t(`categories.${category}`)}
|
||||
guides={structuredMetadata[category]}
|
||||
onClickGuide={onClickGuide}
|
||||
<OverlayScrollbar className={classNames(styles.container, className)}>
|
||||
<div className={classNames(styles.wrapper, hasFilters && styles.hasFilters)}>
|
||||
<div className={styles.groups}>
|
||||
{hasFilters && (
|
||||
<div className={styles.filterAnchor}>
|
||||
<div className={styles.filters}>
|
||||
<label>{t('filter.title')}</label>
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
icon={<SearchIcon />}
|
||||
placeholder={t('filter.placeholder')}
|
||||
value={keyword}
|
||||
onChange={(event) => {
|
||||
setKeyword(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
<div className={styles.checkboxGroupContainer}>
|
||||
<CheckboxGroup
|
||||
className={styles.checkboxGroup}
|
||||
options={allAppGuideCategories.map((category) => ({
|
||||
title: `applications.guide.categories.${category}`,
|
||||
value: category,
|
||||
}))}
|
||||
value={filterCategories}
|
||||
onChange={(value) => {
|
||||
const sortedValue = allAppGuideCategories.filter((category) =>
|
||||
value.includes(category)
|
||||
);
|
||||
setFilterCategories(sortedValue);
|
||||
}}
|
||||
/>
|
||||
{isM2mDisabledForCurrentPlan && <ProTag className={styles.proTag} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</OverlayScrollbar>
|
||||
)}
|
||||
{keyword &&
|
||||
(filteredMetadata?.length ? (
|
||||
<GuideGroup
|
||||
className={styles.guideGroup}
|
||||
hasCardBorder={hasCardBorder}
|
||||
hasCardButton={hasCardButton}
|
||||
guides={filteredMetadata}
|
||||
onClickGuide={onClickGuide}
|
||||
/>
|
||||
) : (
|
||||
<EmptyDataPlaceholder className={styles.emptyPlaceholder} size="large" />
|
||||
))}
|
||||
{!keyword &&
|
||||
(filterCategories.length > 0 ? filterCategories : allAppGuideCategories).map(
|
||||
(category) =>
|
||||
structuredMetadata[category].length > 0 && (
|
||||
<GuideGroup
|
||||
key={category}
|
||||
className={styles.guideGroup}
|
||||
hasCardBorder={hasCardBorder}
|
||||
hasCardButton={hasCardButton}
|
||||
categoryName={t(`categories.${category}`)}
|
||||
guides={structuredMetadata[category]}
|
||||
onClickGuide={onClickGuide}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedGuide?.target !== 'API' && showCreateForm && (
|
||||
<CreateForm
|
||||
defaultCreateType={selectedGuide?.target}
|
||||
|
@ -136,7 +142,7 @@ function GuideLibrary({ className, hasCardBorder, hasFilters }: Props) {
|
|||
onClose={onCloseCreateForm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,38 +1,50 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
@use '@/scss/dimensions' as dim;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-base);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow-x: auto;
|
||||
|
||||
> * {
|
||||
min-width: dim.$guide-content-min-width;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actionBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
inset: auto 0 0 0;
|
||||
padding: _.unit(4) _.unit(8);
|
||||
width: 100%;
|
||||
padding: _.unit(4) _.unit(6);
|
||||
background-color: var(--color-layer-1);
|
||||
box-shadow: var(--shadow-3);
|
||||
z-index: 1;
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: dim.$guide-main-content-max-width;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.text {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text);
|
||||
margin-left: _.unit(62.5);
|
||||
margin-right: _.unit(4);
|
||||
margin-right: _.unit(3);
|
||||
@include _.multi-line-ellipsis(2);
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: dim.$guide-content-max-width) {
|
||||
.container .actionBar .wrapper {
|
||||
margin: 0 0 0 _.unit(62.5);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,18 +30,19 @@ function GuideLibraryModal({ isOpen, onClose }: Props) {
|
|||
>
|
||||
<div className={styles.container}>
|
||||
<GuideHeader onClose={onClose} />
|
||||
<GuideLibrary hasFilters className={styles.content} />
|
||||
<GuideLibrary hasFilters hasCardButton className={styles.content} />
|
||||
<nav className={styles.actionBar}>
|
||||
<span className={styles.text}>{t('do_not_need_tutorial')}</span>
|
||||
<Button
|
||||
className={styles.button}
|
||||
size="large"
|
||||
title="applications.guide.create_without_framework"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
setShowCreateForm(true);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.wrapper}>
|
||||
<span className={styles.text}>{t('do_not_need_tutorial')}</span>
|
||||
<Button
|
||||
size="large"
|
||||
title="applications.guide.create_without_framework"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
setShowCreateForm(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
{showCreateForm && (
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
@use '@/scss/dimensions' as dim;
|
||||
|
||||
.modalContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: var(--color-base);
|
||||
overflow-x: auto;
|
||||
|
||||
> * {
|
||||
min-width: dim.$guide-content-min-width;
|
||||
}
|
||||
}
|
||||
|
||||
.guide {
|
||||
flex: 1;
|
||||
}
|
|
@ -3,7 +3,10 @@ import Modal from 'react-modal';
|
|||
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import Guide from '.';
|
||||
import Guide from '../Guide';
|
||||
import GuideHeader from '../GuideHeader';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
guideId: string;
|
||||
|
@ -27,7 +30,10 @@ function GuideModal({ guideId, app, onClose }: Props) {
|
|||
className={modalStyles.fullScreen}
|
||||
onRequestClose={closeModal}
|
||||
>
|
||||
<Guide guideId={guideId} app={app} onClose={closeModal} />
|
||||
<div className={styles.modalContainer}>
|
||||
<GuideHeader onClose={closeModal} />
|
||||
<Guide className={styles.guide} guideId={guideId} app={app} onClose={closeModal} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -85,7 +85,7 @@ function Applications() {
|
|||
title="applications.guide.header_title"
|
||||
subtitle="applications.guide.header_subtitle"
|
||||
/>
|
||||
<GuideLibrary hasCardBorder className={styles.library} />
|
||||
<GuideLibrary hasCardBorder hasCardButton className={styles.library} />
|
||||
</OverlayScrollbar>
|
||||
)}
|
||||
{(isLoading || !!applications?.length) && (
|
||||
|
|
|
@ -6,3 +6,11 @@ $modal-layout-grid-large: 850px;
|
|||
$modal-layout-grid-medium: 668px;
|
||||
$modal-layout-grid-small: 500px;
|
||||
$form-text-field-width: 556px;
|
||||
|
||||
// Guide related dimensions
|
||||
$guide-main-content-max-width: 858px;
|
||||
$guide-sidebar-width: 220px;
|
||||
$guide-panel-gap: 30px;
|
||||
$guide-content-padding: 24px;
|
||||
$guide-content-max-width: calc($guide-main-content-max-width + 2 * ($guide-sidebar-width + $guide-panel-gap + $guide-content-padding));
|
||||
$guide-content-min-width: 750px;
|
||||
|
|
Loading…
Add table
Reference in a new issue