Skip to content

Commit

Permalink
feat: bug fixes, improved types, handle unknown scenarios and fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
ovflowd committed Dec 26, 2024
1 parent 3c8db2a commit c39f620
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 109 deletions.
44 changes: 25 additions & 19 deletions apps/site/components/Common/Select/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,47 @@ import * as ScrollPrimitive from '@radix-ui/react-scroll-area';
import * as SelectPrimitive from '@radix-ui/react-select';
import classNames from 'classnames';
import { useEffect, useId, useMemo, useState } from 'react';
import type { FC, ReactElement } from 'react';
import type { ReactElement, ReactNode } from 'react';

import Skeleton from '@/components/Common/Skeleton';
import type { FormattedMessage } from '@/types';

import styles from './index.module.css';

type SelectValue = {
label: FormattedMessage;
value: string;
export type SelectValue<T extends string> = {
label: FormattedMessage | string;
value: T;
iconImage?: ReactElement<SVGSVGElement>;
disabled?: boolean;
};

type SelectGroup = {
label?: FormattedMessage;
items: Array<SelectValue>;
export type SelectGroup<T extends string> = {
label?: FormattedMessage | string;
items: Array<SelectValue<T>>;
};

const isStringArray = (values: Array<unknown>): values is Array<string> =>
Boolean(values[0] && typeof values[0] === 'string');

const isValuesArray = (values: Array<unknown>): values is Array<SelectValue> =>
const isValuesArray = <T extends string>(
values: Array<unknown>
): values is Array<SelectValue<T>> =>
Boolean(values[0] && typeof values[0] === 'object' && 'value' in values[0]);

type SelectProps = {
values: Array<SelectGroup | string | SelectValue>;
defaultValue?: string;
type SelectProps<T extends string> = {
values: Array<SelectGroup<T>> | Array<T> | Array<SelectValue<T>>;
defaultValue?: T;
placeholder?: string;
label?: string;
inline?: boolean;
onChange?: (value: string) => void;
onChange?: (value: T) => void;
className?: string;
ariaLabel?: string;
loading?: boolean;
disabled?: boolean;
};

const Select: FC<SelectProps> = ({
const Select = <T extends string>({
values = [],
defaultValue,
placeholder,
Expand All @@ -54,7 +56,7 @@ const Select: FC<SelectProps> = ({
ariaLabel,
loading = false,
disabled = false,
}) => {
}: SelectProps<T>): ReactNode => {
const id = useId();
const [value, setValue] = useState(defaultValue);

Expand All @@ -71,7 +73,7 @@ const Select: FC<SelectProps> = ({
return [{ items: mappedValues }];
}

return mappedValues as Array<SelectGroup>;
return mappedValues as Array<SelectGroup<T>>;
}, [values]);

// We render the actual item slotted to fix/prevent the issue
Expand All @@ -85,7 +87,7 @@ const Select: FC<SelectProps> = ({
);

// Both change the internal state and emit the change event
const handleChange = (value: string) => {
const handleChange = (value: T) => {
setValue(value);

if (typeof onChange === 'function') {
Expand All @@ -109,7 +111,7 @@ const Select: FC<SelectProps> = ({
)}

<SelectPrimitive.Root
value={value}
value={currentItem !== undefined ? value : undefined}
onValueChange={handleChange}
disabled={disabled}
>
Expand All @@ -119,8 +121,12 @@ const Select: FC<SelectProps> = ({
id={id}
>
<SelectPrimitive.Value placeholder={placeholder}>
{currentItem?.iconImage}
<span>{currentItem?.label}</span>
{currentItem !== undefined && (
<>
{currentItem.iconImage}
<span>{currentItem.label}</span>
</>
)}
</SelectPrimitive.Value>
<ChevronDownIcon className={styles.icon} />
</SelectPrimitive.Trigger>
Expand Down
35 changes: 18 additions & 17 deletions apps/site/components/Downloads/Release/BitnessDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import { useEffect, useContext, useMemo } from 'react';
import Select from '@/components/Common/Select';
import { useClientContext } from '@/hooks';
import { ReleaseContext } from '@/providers/releaseProvider';
import {
ARCHITECTURES,
getNextItem,
parseCompatibility,
} from '@/util/downloadUtils';
import { ARCHITECTURES, nextItem, parseCompat } from '@/util/downloadUtils';
import { getUserBitnessByArchitecture } from '@/util/getUserBitnessByArchitecture';

const parseNumericBitness = (bitness: string) =>
Expand All @@ -23,15 +19,24 @@ const BitnessDropdown: FC = () => {
const release = useContext(ReleaseContext);
const t = useTranslations();

useEffect(() => {
release.setBitness(getUserBitnessByArchitecture(architecture, bitness));
// Prevents the Bitness from being set during OS loading state
// and always correctly parses the Bitness to a number when needed
const setBitness = (bitness: string) => {
if (release.os !== 'LOADING') {
release.setBitness(parseNumericBitness(bitness));
}
};

useEffect(
() => setBitness(getUserBitnessByArchitecture(architecture, bitness)),
// Only react on the change of the Client Context Architecture and Bitness
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [architecture, bitness]);
[architecture, bitness]
);

// We parse the compatibility of the dropdown items
const parsedArchitectures = useMemo(
() => parseCompatibility(ARCHITECTURES[release.os], release),
() => parseCompat(ARCHITECTURES[release.os], release),
// We only want to react on the change of the OS, Bitness, and Version
// eslint-disable-next-line react-hooks/exhaustive-deps
[release.os, release.bitness, release.version]
Expand All @@ -40,24 +45,20 @@ const BitnessDropdown: FC = () => {
// We set the Bitness to the next available Architecture when the current
// one is not valid anymore due to OS or Version changes
useEffect(
() =>
release.setBitness(
parseNumericBitness(
getNextItem(String(release.bitness), parsedArchitectures)
)
),
() => setBitness(nextItem(String(release.bitness), parsedArchitectures)),
// We only want to react on the change of the OS and Version
// eslint-disable-next-line react-hooks/exhaustive-deps
[release.os, release.version]
[release.os, release.version, release.bitness]
);

return (
<Select
values={parsedArchitectures}
loading={release.os === 'LOADING'}
placeholder={t('layouts.download.dropdown.unknown')}
ariaLabel={t('layouts.download.dropdown.bitness')}
defaultValue={String(release.bitness)}
onChange={bitness => release.setBitness(parseNumericBitness(bitness))}
onChange={bitness => setBitness(bitness)}
className="min-w-20"
inline={true}
/>
Expand Down
31 changes: 14 additions & 17 deletions apps/site/components/Downloads/Release/OperatingSystemDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import Select from '@/components/Common/Select';
import { useClientContext } from '@/hooks';
import { ReleaseContext } from '@/providers/releaseProvider';
import type { UserOS } from '@/types/userOS';
import {
getNextItem,
OPERATING_SYSTEMS,
parseCompatibility,
} from '@/util/downloadUtils';
import { OPERATING_SYSTEMS, parseCompat } from '@/util/downloadUtils';

type OperatingSystemDropdownProps = { exclude?: Array<UserOS> };

Expand All @@ -21,34 +17,35 @@ const OperatingSystemDropdown: FC<OperatingSystemDropdownProps> = () => {
const release = useContext(ReleaseContext);
const t = useTranslations();

// Prevents the OS from being set during OS loading state
const setOS = (newOS: UserOS) => {
if (release.os !== 'LOADING') {
release.setOS(newOS);
}
};

// Reacts on Client Context change of OS
// Only this Hook is allowed to bypass the `setOS` from above
// As this Hook is what defined the initial OS state
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => release.setOS(os), [os]);

// We parse the compatibility of the dropdown items
const parsedOperatingSystems = useMemo(
() => parseCompatibility(OPERATING_SYSTEMS, release),
() => parseCompat(OPERATING_SYSTEMS, release),
// We only want to react on the change of the OS, Bitness, and Version
// eslint-disable-next-line react-hooks/exhaustive-deps
[release.os, release.bitness, release.version]
);

// We set the OS to the next available OS when the current
// one is not valid anymore due to Platform changes
useEffect(
() => release.setOS(getNextItem(release.os, parsedOperatingSystems)),
// We only want to react on the change of the OS and Version
// eslint-disable-next-line react-hooks/exhaustive-deps
[release.platform]
);

return (
<Select
<Select<UserOS>
values={parsedOperatingSystems}
defaultValue={release.os}
loading={release.os === 'LOADING'}
placeholder={t('layouts.download.dropdown.unknown')}
ariaLabel={t('layouts.download.dropdown.os')}
onChange={value => release.setOS(value as UserOS)}
onChange={value => setOS(value)}
className="min-w-[8.5rem]"
inline={true}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ const PackageManagerDropdown: FC = () => {
const t = useTranslations();

return (
<Select
<Select<PackageManager | ''>
values={PACKAGE_MANAGERS}
defaultValue={release.packageManager}
loading={release.os === 'LOADING' || release.platform === ''}
ariaLabel={t('layouts.download.dropdown.packageManager')}
onChange={manager => release.setPackageManager(manager as PackageManager)}
onChange={manager => release.setPackageManager(manager)}
className="min-w-28"
inline={true}
/>
Expand Down
45 changes: 24 additions & 21 deletions apps/site/components/Downloads/Release/PlatformDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ import type { FC } from 'react';
import Select from '@/components/Common/Select';
import { ReleaseContext } from '@/providers/releaseProvider';
import type { InstallationMethod } from '@/types/release';
import {
getNextItem,
INSTALLATION_METHODS,
parseCompatibility,
} from '@/util/downloadUtils';
import { nextItem, INSTALL_METHODS, parseCompat } from '@/util/downloadUtils';

const PlatformDropdown: FC = () => {
const release = useContext(ReleaseContext);
const t = useTranslations();

// Prevents the Platform from being set during OS loading state
// This also prevents the Platform from being set (by Dropdwon or Automatic methods)
// when we haven't yet loaded the OS and defined the initial Platform
const setPlaform = (platform: InstallationMethod | '') => {
if (release.os !== 'LOADING' && release.platform !== '') {
release.setPlatform(platform);
}
};

// We parse the compatibility of the dropdown items
const parsedPlatforms = useMemo(
() => parseCompatibility(INSTALLATION_METHODS, release),
() => parseCompat(INSTALL_METHODS, release),
// We only want to react on the change of the OS and Version
// eslint-disable-next-line react-hooks/exhaustive-deps
[release.os, release.version]
Expand All @@ -43,39 +48,37 @@ const PlatformDropdown: FC = () => {
[parsedPlatforms]
);

// Since in some cases we don't want to have a Platform Dropdown
// This allows us to render the first non-disabled platform
// Or the recommended platform when the Dropdown component is bound
useEffect(() => {
if (!release.platform) {
const initial = parsedPlatforms.find(({ disabled }) => !disabled);

const recommended = parsedPlatforms.find(
({ recommended, disabled }) => recommended && !disabled
// We should only define the initial Platform if the current platform is empty
// (aka has not yet been set) and the OS has finished loading (in the sense that)
// `detectOS` has finished running and decided what platform we are running on.
if (release.os !== 'LOADING' && release.platform === '') {
release.setPlatform(
// Sets either the utmost recommended platform or the first non-disabled one
// Note that the first item of groupped platforms is always the recommended one
nextItem('', grouppedPlatforms[0].items) ||
nextItem('', parsedPlatforms)
);

release.setPlatform(recommended?.value || initial?.value || '');
}
// We are interested only on the initial render of this component
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [parsedPlatforms, release.platform, release.os]);

// We set the Platform to the next available platform when the current
// one is not valid anymore due to OS or Version changes
useEffect(
() => release.setPlatform(getNextItem(release.platform, parsedPlatforms)),
() => setPlaform(nextItem(release.platform, parsedPlatforms)),
// We only want to react on the change of the OS and Version
// eslint-disable-next-line react-hooks/exhaustive-deps
[release.os, release.version]
);

return (
<Select
<Select<InstallationMethod | ''>
values={grouppedPlatforms}
defaultValue={release.platform}
loading={release.os === 'LOADING' || release.platform === ''}
ariaLabel={t('layouts.download.dropdown.platform')}
onChange={platform => release.setPlatform(platform as InstallationMethod)}
onChange={platform => setPlaform(platform)}
className="min-w-28"
inline={true}
/>
Expand Down
8 changes: 4 additions & 4 deletions apps/site/components/Downloads/Release/ReleaseCodeBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import JSXCodeBox from '@/components/JSX/CodeBox';
import { createSval } from '@/next.jsx.compiler.mjs';
import { ReleaseContext, ReleasesContext } from '@/providers/releaseProvider';
import type { ReleaseContextType } from '@/types/release';
import { INSTALLATION_METHODS } from '@/util/downloadUtils';
import { INSTALL_METHODS } from '@/util/downloadUtils';

import LinkWithArrow from './LinkWithArrow';

Expand Down Expand Up @@ -40,7 +40,7 @@ const ReleaseCodeBox: FC = () => {

// Retrieves the current platform (Dropdown Item) based on the selected platform value
const currentPlatform = useMemo(
() => INSTALLATION_METHODS.find(({ value }) => value === platform),
() => INSTALL_METHODS.find(({ value }) => value === platform),
[platform]
);

Expand Down Expand Up @@ -95,15 +95,15 @@ const ReleaseCodeBox: FC = () => {
{t(
currentPlatform?.bottomInfo ??
'layouts.download.codeBox.platformInfo.default',
{ platform: currentPlatform?.label }
{ platform: currentPlatform?.label as string }
)}
</Skeleton>

<br />

<Skeleton loading={renderSkeleton} hide={!currentPlatform}>
{t.rich('layouts.download.codeBox.externalSupportInfo', {
platform: currentPlatform?.label,
platform: currentPlatform?.label as string,
b: chunks => <b>{chunks}</b>,
link: chunks => (
<LinkWithArrow href={currentPlatform?.url}>
Expand Down
4 changes: 2 additions & 2 deletions apps/site/types/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ export type ReleaseAction =
| { type: 'SET_VERSION'; payload: string }
| { type: 'SET_BITNESS'; payload: string | number }
| { type: 'SET_PLATFORM'; payload: InstallationMethod | '' }
| { type: 'SET_MANAGER'; payload: PackageManager };
| { type: 'SET_MANAGER'; payload: PackageManager | '' };

export interface ReleaseDispatchActions {
setVersion: (version: string) => void;
setOS: (os: UserOS) => void;
setBitness: (bitness: string | number) => void;
setPlatform: (platform: InstallationMethod | '') => void;
setPackageManager: (packageManager: PackageManager) => void;
setPackageManager: (packageManager: PackageManager | '') => void;
}

export interface ReleasesContextType {
Expand Down
Loading

0 comments on commit c39f620

Please sign in to comment.