Testing and types updates (#39657)
This commit is contained in:
29 files changed
+263
-191
No files matched your search
@@ -60,7 +60,7 @@ export interface BaseApiAccountJSON {
|
||||
show_featured: boolean;
|
||||
noindex?: boolean;
|
||||
note: string;
|
||||
roles?: ApiAccountJSON[];
|
||||
roles?: ApiAccountRoleJSON[];
|
||||
statuses_count: number;
|
||||
uri: string;
|
||||
url?: string;
|
||||
|
||||
@@ -19,11 +19,13 @@ interface ApiNestedQuoteJSON {
|
||||
quoted_status_id: string;
|
||||
}
|
||||
|
||||
export type ApiQuotedStatusJSON = Omit<ApiStatusJSON, 'quote'> & {
|
||||
quote?: ApiNestedQuoteJSON | ApiQuoteEmptyJSON;
|
||||
};
|
||||
|
||||
interface ApiQuoteAcceptedJSON {
|
||||
state: 'accepted';
|
||||
quoted_status: Omit<ApiStatusJSON, 'quote'> & {
|
||||
quote?: ApiNestedQuoteJSON | ApiQuoteEmptyJSON;
|
||||
};
|
||||
quoted_status: ApiQuotedStatusJSON;
|
||||
}
|
||||
|
||||
export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON;
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { ComponentProps } from 'react';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
|
||||
import {
|
||||
accountFactoryImmutable,
|
||||
relationshipsFactoryAPI,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import { Account } from './index';
|
||||
|
||||
@@ -69,7 +72,7 @@ const meta = {
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState(),
|
||||
'1': accountFactoryImmutable(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -121,7 +124,7 @@ export const Blocked: Story = {
|
||||
parameters: {
|
||||
state: {
|
||||
relationships: {
|
||||
'1': relationshipsFactory({
|
||||
'1': relationshipsFactoryAPI({
|
||||
blocking: true,
|
||||
}),
|
||||
},
|
||||
@@ -134,7 +137,7 @@ export const Muted: Story = {
|
||||
parameters: {
|
||||
state: {
|
||||
relationships: {
|
||||
'1': relationshipsFactory({
|
||||
'1': relationshipsFactoryAPI({
|
||||
muting: true,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
|
||||
import {
|
||||
accountFactoryImmutable,
|
||||
relationshipsFactoryAPI,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import { PendingBadge } from '../badge';
|
||||
|
||||
@@ -16,7 +19,7 @@ const meta = {
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState(),
|
||||
'1': accountFactoryImmutable(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -32,7 +35,7 @@ export const FollowsYou: Story = {
|
||||
parameters: {
|
||||
state: {
|
||||
relationships: {
|
||||
'1': relationshipsFactory({
|
||||
'1': relationshipsFactoryAPI({
|
||||
followed_by: true,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ComponentProps } from 'react';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { accountFactoryState } from '@/testing/factories';
|
||||
import { accountFactoryImmutable } from '@/testing/factories';
|
||||
|
||||
import { DisplayName, LinkedDisplayName } from './index';
|
||||
|
||||
@@ -23,7 +23,7 @@ const meta = {
|
||||
tags: [],
|
||||
render({ name, username, loading, ...args }) {
|
||||
const account = !loading
|
||||
? accountFactoryState({
|
||||
? accountFactoryImmutable({
|
||||
display_name: name,
|
||||
acct: username,
|
||||
})
|
||||
@@ -69,7 +69,7 @@ export const LocalUser: Story = {
|
||||
export const Linked: Story = {
|
||||
render({ name, username, loading, ...args }) {
|
||||
const account = !loading
|
||||
? accountFactoryState({
|
||||
? accountFactoryImmutable({
|
||||
display_name: name,
|
||||
acct: username,
|
||||
})
|
||||
|
||||
@@ -8,8 +8,9 @@ import type {
|
||||
} from 'react';
|
||||
import { useCallback, useId, useRef } from 'react';
|
||||
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
||||
import type { OmitUnion } from '@/mastodon/utils/types';
|
||||
|
||||
import { CharacterCounter } from '../character_counter';
|
||||
import { EmojiPickerButton } from '../emoji/picker_button';
|
||||
@@ -29,7 +30,7 @@ export type EmojiInputProps = {
|
||||
} & Omit<CommonFieldWrapperProps, 'wrapperClassName'>;
|
||||
|
||||
export const EmojiTextInputField: FC<
|
||||
OmitUnion<ComponentPropsWithoutRef<'input'>, EmojiInputProps>
|
||||
Merge<ComponentPropsWithoutRef<'input'>, EmojiInputProps>
|
||||
> = ({
|
||||
onChange,
|
||||
value,
|
||||
@@ -72,7 +73,7 @@ export const EmojiTextInputField: FC<
|
||||
};
|
||||
|
||||
export const EmojiTextAreaField: FC<
|
||||
OmitUnion<Omit<TextAreaProps, 'style'>, EmojiInputProps>
|
||||
Merge<Omit<TextAreaProps, 'style'>, EmojiInputProps>
|
||||
> = ({
|
||||
onChange,
|
||||
value,
|
||||
|
||||
@@ -3,14 +3,14 @@ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { OmitUnion } from '@/mastodon/utils/types';
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
import { Icon } from '../icon';
|
||||
import type { IconProp } from '../icon';
|
||||
|
||||
import classes from './styles.module.css';
|
||||
|
||||
export type MiniCardProps = OmitUnion<
|
||||
export type MiniCardProps = Merge<
|
||||
ComponentPropsWithoutRef<'div'>,
|
||||
{
|
||||
label: ReactNode;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ComponentPropsWithoutRef, Key } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { OmitUnion } from '@/mastodon/utils/types';
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
import { MiniCard } from '.';
|
||||
import type { MiniCardProps as BaseCardProps } from '.';
|
||||
@@ -19,7 +19,7 @@ interface MiniCardListProps {
|
||||
|
||||
export const MiniCardList = forwardRef<
|
||||
HTMLDListElement,
|
||||
OmitUnion<ComponentPropsWithoutRef<'dl'>, MiniCardListProps>
|
||||
Merge<ComponentPropsWithoutRef<'dl'>, MiniCardListProps>
|
||||
>(({ cards = [], className, children, ...props }, ref) => {
|
||||
if (!cards.length) {
|
||||
return null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import { statusFactoryState } from '@/testing/factories';
|
||||
import { statusFactoryImmutable } from '@/testing/factories';
|
||||
|
||||
import { BoostButton } from './boost_button';
|
||||
|
||||
@@ -50,7 +50,7 @@ function argsToStatus({
|
||||
quoteAllowed,
|
||||
alreadyBoosted,
|
||||
}: StoryProps) {
|
||||
return statusFactoryState({
|
||||
return statusFactoryImmutable({
|
||||
reblogs_count: reblogCount,
|
||||
visibility,
|
||||
reblogged: alreadyBoosted,
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { SetRequired } from 'type-fest';
|
||||
|
||||
import { quoteComposeById } from '@/mastodon/actions/compose_typed';
|
||||
import { toggleReblog } from '@/mastodon/actions/interactions';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
@@ -13,7 +15,6 @@ import { quickBoosting } from '@/mastodon/initial_state';
|
||||
import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu';
|
||||
import type { Status } from '@/mastodon/models/status';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import type { SomeRequired } from '@/mastodon/utils/types';
|
||||
|
||||
import type { RenderItemFn } from '../dropdown_menu';
|
||||
import { Dropdown, DropdownMenuItemContent } from '../dropdown_menu';
|
||||
@@ -91,7 +92,7 @@ interface ReblogButtonProps {
|
||||
counters?: boolean;
|
||||
}
|
||||
|
||||
type ActionMenuItemWithIcon = SomeRequired<ActionMenuItem, 'icon'>;
|
||||
type ActionMenuItemWithIcon = SetRequired<ActionMenuItem, 'icon'>;
|
||||
|
||||
const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { fn } from 'storybook/test';
|
||||
|
||||
import type { StatusTranslation } from '@/mastodon/models/status';
|
||||
import { statusFactoryState } from '@/testing/factories';
|
||||
import { statusFactoryImmutable } from '@/testing/factories';
|
||||
|
||||
import { StatusContent } from './content';
|
||||
|
||||
@@ -57,7 +57,7 @@ const meta = {
|
||||
},
|
||||
},
|
||||
stateFn(args: StatusContentProps) {
|
||||
let status = statusFactoryState();
|
||||
let status = statusFactoryImmutable();
|
||||
if (args.translatedTo) {
|
||||
status = status.set('translation', {
|
||||
contentHtml: `${args.text}<p><em>(in ${args.translatedTo})</em></p>`,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import { HashtagMenuController } from '@/mastodon/features/ui/components/hashtag_menu_controller';
|
||||
import { accountFactoryState } from '@/testing/factories';
|
||||
import { accountFactoryImmutable } from '@/testing/factories';
|
||||
|
||||
import { HoverCardController } from '../hover_card_controller';
|
||||
|
||||
@@ -55,7 +55,7 @@ const meta = {
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState({ id: '1', acct: 'hashtaguser' }),
|
||||
'1': accountFactoryImmutable({ id: '1', acct: 'hashtaguser' }),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,11 +9,11 @@ import { fn } from 'storybook/test';
|
||||
import type { ApiMediaAttachmentJSON } from '@/mastodon/api_types/media_attachments';
|
||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||
import {
|
||||
accountFactoryState,
|
||||
mediaAttachmentFactory,
|
||||
pollFactory,
|
||||
statusFactory,
|
||||
statusFactoryState,
|
||||
accountFactoryImmutable,
|
||||
mediaAttachmentFactoryAPI,
|
||||
pollFactoryImmutable,
|
||||
statusFactoryAPI,
|
||||
statusFactoryImmutable,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import { TypedStatus } from './types';
|
||||
@@ -71,7 +71,7 @@ interface StatusStoryProps {
|
||||
showPrepend?: boolean;
|
||||
}
|
||||
|
||||
const otherAccount = accountFactoryState({
|
||||
const otherAccount = accountFactoryImmutable({
|
||||
id: '2',
|
||||
display_name: 'Another user',
|
||||
});
|
||||
@@ -106,14 +106,14 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
showPrepend = true,
|
||||
} = props;
|
||||
const { account, status } = useMemo(() => {
|
||||
const account = accountFactoryState();
|
||||
const account = accountFactoryImmutable();
|
||||
|
||||
const media_attachments: ApiMediaAttachmentJSON[] = [];
|
||||
switch (attachments) {
|
||||
// Use fall through add attachments depending on count.
|
||||
case 'image-3':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
mediaAttachmentFactoryAPI({
|
||||
id: '2',
|
||||
url: 'https://cataas.com/cat/EbVq9zMc4Xxv7s73',
|
||||
meta: {
|
||||
@@ -129,7 +129,7 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'image-2':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
mediaAttachmentFactoryAPI({
|
||||
id: '3',
|
||||
url: 'https://cataas.com/cat/YFaQ4xWYoWURSz37',
|
||||
meta: {
|
||||
@@ -145,7 +145,7 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'image-1':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
mediaAttachmentFactoryAPI({
|
||||
id: '4',
|
||||
url: 'https://cataas.com/cat/bYBTjiFUqjUPIBUD',
|
||||
meta: {
|
||||
@@ -161,7 +161,7 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
break;
|
||||
case 'video':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
mediaAttachmentFactoryAPI({
|
||||
type: 'video',
|
||||
url: 'https://www.pexels.com/download/video/11760787/',
|
||||
meta: {
|
||||
@@ -175,7 +175,7 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
break;
|
||||
case 'audio':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
mediaAttachmentFactoryAPI({
|
||||
type: 'audio',
|
||||
url: 'https://upload.wikimedia.org/wikipedia/commons/4/40/Elephant_voice_-_trumpeting.ogg',
|
||||
}),
|
||||
@@ -183,7 +183,7 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
break;
|
||||
case 'gifv':
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactory({
|
||||
mediaAttachmentFactoryAPI({
|
||||
type: 'gifv',
|
||||
url: 'https://www.pexels.com/download/video/11760787/',
|
||||
meta: {
|
||||
@@ -196,13 +196,15 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
);
|
||||
break;
|
||||
case 'unknown':
|
||||
media_attachments.push(mediaAttachmentFactory({ type: attachments }));
|
||||
media_attachments.push(
|
||||
mediaAttachmentFactoryAPI({ type: attachments }),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
account,
|
||||
status: statusFactoryState({
|
||||
status: statusFactoryImmutable({
|
||||
text,
|
||||
spoiler_text: contentWarning,
|
||||
visibility,
|
||||
@@ -215,7 +217,7 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
quote: isQuote
|
||||
? {
|
||||
state: 'accepted',
|
||||
quoted_status: { ...statusFactory(), quote: undefined },
|
||||
quoted_status: { ...statusFactoryAPI(), quote: undefined },
|
||||
}
|
||||
: undefined,
|
||||
favourites_count: favouriteCount,
|
||||
@@ -236,7 +238,7 @@ const StatusStoryComponent: FC<StatusStoryProps> = (props) => {
|
||||
if (isReblog) {
|
||||
status.set(
|
||||
'reblog',
|
||||
statusFactoryState({ id: '2' }).set('account', otherAccount),
|
||||
statusFactoryImmutable({ id: '2' }).set('account', otherAccount),
|
||||
);
|
||||
}
|
||||
if (isPoll) {
|
||||
@@ -469,8 +471,8 @@ const meta = {
|
||||
'2': otherAccount,
|
||||
},
|
||||
polls: {
|
||||
'1': pollFactory(),
|
||||
'2': pollFactory({
|
||||
'1': pollFactoryImmutable(),
|
||||
'2': pollFactoryImmutable({
|
||||
voted: true,
|
||||
voters_count: 1,
|
||||
votes_count: 1,
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Map as ImmutableMap } from 'immutable';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import type { ApiQuoteJSON } from '@/mastodon/api_types/quotes';
|
||||
import { accountFactoryState, statusFactoryState } from '@/testing/factories';
|
||||
import {
|
||||
accountFactoryImmutable,
|
||||
statusFactoryImmutable,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import type { StatusQuoteManagerProps } from './status_quoted';
|
||||
import { StatusQuoteManager } from './status_quoted';
|
||||
@@ -16,15 +19,15 @@ const meta = {
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState({ id: '1', acct: 'hashtaguser' }),
|
||||
'1': accountFactoryImmutable({ id: '1', acct: 'hashtaguser' }),
|
||||
},
|
||||
statuses: {
|
||||
'1': statusFactoryState({
|
||||
'1': statusFactoryImmutable({
|
||||
id: '1',
|
||||
language: 'en',
|
||||
text: 'Hello world!',
|
||||
}),
|
||||
'2': statusFactoryState({
|
||||
'2': statusFactoryImmutable({
|
||||
id: '2',
|
||||
language: 'en',
|
||||
text: 'Quote!',
|
||||
@@ -33,19 +36,19 @@ const meta = {
|
||||
quoted_status: '1',
|
||||
}) as unknown as ApiQuoteJSON,
|
||||
}),
|
||||
'1001': statusFactoryState({
|
||||
'1001': statusFactoryImmutable({
|
||||
id: '1001',
|
||||
language: 'mn-Mong',
|
||||
// meaning: Mongolia
|
||||
text: 'ᠮᠤᠩᠭᠤᠯ',
|
||||
}),
|
||||
'1002': statusFactoryState({
|
||||
'1002': statusFactoryImmutable({
|
||||
id: '1002',
|
||||
language: 'mn-Mong',
|
||||
// meaning: All human beings are born free and equal in dignity and rights.
|
||||
text: 'ᠬᠦᠮᠦᠨ ᠪᠦᠷ ᠲᠥᠷᠥᠵᠦ ᠮᠡᠨᠳᠡᠯᠡᠬᠦ ᠡᠷᠬᠡ ᠴᠢᠯᠥᠭᠡ ᠲᠡᠢ᠂ ᠠᠳᠠᠯᠢᠬᠠᠨ ᠨᠡᠷᠡ ᠲᠥᠷᠥ ᠲᠡᠢ᠂ ᠢᠵᠢᠯ ᠡᠷᠬᠡ ᠲᠡᠢ ᠪᠠᠢᠠᠭ᠃',
|
||||
}),
|
||||
'1003': statusFactoryState({
|
||||
'1003': statusFactoryImmutable({
|
||||
id: '1003',
|
||||
language: 'mn-Mong',
|
||||
// meaning: Mongolia
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { OmitUnion } from '@/mastodon/utils/types';
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import type { IconProp } from '../icon';
|
||||
@@ -24,7 +25,7 @@ export interface TagProps {
|
||||
|
||||
export const Tag = forwardRef<
|
||||
HTMLButtonElement,
|
||||
OmitUnion<ComponentPropsWithoutRef<'button'>, TagProps>
|
||||
Merge<ComponentPropsWithoutRef<'button'>, TagProps>
|
||||
>(({ name, active, icon, className, children, ...props }, ref) => {
|
||||
if (!name) {
|
||||
return null;
|
||||
@@ -47,7 +48,7 @@ Tag.displayName = 'Tag';
|
||||
|
||||
export const EditableTag = forwardRef<
|
||||
HTMLSpanElement,
|
||||
OmitUnion<
|
||||
Merge<
|
||||
ComponentPropsWithoutRef<'span'>,
|
||||
TagProps & {
|
||||
onRemove: () => void;
|
||||
|
||||
+4
-3
@@ -1,17 +1,18 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { action } from 'storybook/actions';
|
||||
import type { ConditionalExcept } from 'type-fest';
|
||||
|
||||
import type { AnyFunction, OmitValueType } from '@/mastodon/utils/types';
|
||||
import type { AnyFunction } from '@/mastodon/utils/types';
|
||||
|
||||
import type { AnnualReportAnnouncementProps } from '.';
|
||||
import { AnnualReportAnnouncement } from '.';
|
||||
|
||||
type Props = OmitValueType<
|
||||
type Props = ConditionalExcept<
|
||||
// We can't use the name 'state' here because it's reserved for overriding Redux state.
|
||||
Omit<AnnualReportAnnouncementProps, 'state'> & {
|
||||
reportState: AnnualReportAnnouncementProps['state'];
|
||||
},
|
||||
AnyFunction // Remove any functions, as they can't meaningfully be controlled in Storybook.
|
||||
AnyFunction | undefined // Remove any functions, as they can't meaningfully be controlled in Storybook.
|
||||
>;
|
||||
|
||||
const meta = {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import {
|
||||
accountFactoryState,
|
||||
annualReportFactory,
|
||||
statusFactoryState,
|
||||
accountFactoryImmutable,
|
||||
annualReportFactoryState,
|
||||
statusFactoryImmutable,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import { AnnualReport } from '.';
|
||||
@@ -22,12 +22,12 @@ const meta = {
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState({ display_name: 'Freddie Fruitbat' }),
|
||||
'1': accountFactoryImmutable({ display_name: 'Freddie Fruitbat' }),
|
||||
},
|
||||
statuses: {
|
||||
'1': statusFactoryState(),
|
||||
'1': statusFactoryImmutable(),
|
||||
},
|
||||
annualReport: annualReportFactory({
|
||||
annualReport: annualReportFactoryState({
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
},
|
||||
@@ -54,7 +54,7 @@ export const ArchetypeOracle: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
annualReport: annualReportFactoryState({
|
||||
archetype: 'oracle',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
@@ -66,7 +66,7 @@ export const NoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
annualReport: annualReportFactoryState({
|
||||
archetype: 'booster',
|
||||
}),
|
||||
},
|
||||
@@ -77,7 +77,7 @@ export const NoNewPosts: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
annualReport: annualReportFactoryState({
|
||||
archetype: 'pollster',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
without_posts: true,
|
||||
@@ -90,7 +90,7 @@ export const NoNewPostsNoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
annualReport: annualReportFactoryState({
|
||||
archetype: 'replier',
|
||||
without_posts: true,
|
||||
}),
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
import { unescapeHTML } from 'mastodon/utils/html';
|
||||
|
||||
import { CustomEmojiFactory } from './custom_emoji';
|
||||
import type { CustomEmoji } from './custom_emoji';
|
||||
import type { CustomEmoji, CustomEmojiShape } from './custom_emoji';
|
||||
|
||||
// AccountField
|
||||
export interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
||||
@@ -59,7 +59,7 @@ export type AccountShapeFull = Omit<
|
||||
AccountShape,
|
||||
'emojis' | 'fields' | 'roles'
|
||||
> & {
|
||||
emojis: CustomEmoji[];
|
||||
emojis: CustomEmojiShape[];
|
||||
fields: AccountFieldShape[];
|
||||
roles: AccountRoleShape[];
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Record as ImmutableRecord, isList } from 'immutable';
|
||||
|
||||
import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji';
|
||||
|
||||
type CustomEmojiShape = Required<ApiCustomEmojiJSON>; // no changes from server shape
|
||||
export type CustomEmojiShape = Required<ApiCustomEmojiJSON>; // no changes from server shape
|
||||
export type CustomEmoji = RecordOf<CustomEmojiShape>;
|
||||
|
||||
export const CustomEmojiFactory = ImmutableRecord<CustomEmojiShape>({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import type { CamelCase } from 'type-fest';
|
||||
|
||||
import { fetchAccount } from '@/mastodon/actions/accounts';
|
||||
import {
|
||||
apiDeleteFeaturedTag,
|
||||
@@ -26,13 +28,12 @@ import {
|
||||
createDataLoadingThunk,
|
||||
} from '@/mastodon/store/typed_functions';
|
||||
import { hashObjectArray } from '@/mastodon/utils/hash';
|
||||
import type { SnakeToCamelCase } from '@/mastodon/utils/types';
|
||||
|
||||
type ProfileData = {
|
||||
[Key in keyof Omit<
|
||||
ApiProfileJSON,
|
||||
'note' | 'fields' | 'featured_tags'
|
||||
> as SnakeToCamelCase<Key>]: ApiProfileJSON[Key];
|
||||
> as CamelCase<Key>]: ApiProfileJSON[Key];
|
||||
} & {
|
||||
bio: ApiProfileJSON['note'];
|
||||
fields: FieldData[];
|
||||
@@ -45,7 +46,7 @@ export type TagData = {
|
||||
[Key in keyof Omit<
|
||||
ApiFeaturedTagJSON,
|
||||
'statuses_count'
|
||||
> as SnakeToCamelCase<Key>]: ApiFeaturedTagJSON[Key];
|
||||
> as CamelCase<Key>]: ApiFeaturedTagJSON[Key];
|
||||
} & {
|
||||
statusesCount: number;
|
||||
};
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import { getNestedProperty } from './objects';
|
||||
|
||||
describe('getNestedProperty', () => {
|
||||
const obj = { a: { b: { c: 42 } } } as const;
|
||||
|
||||
test('returns the value of a nested property if it exists', () => {
|
||||
const obj = { a: { b: { c: 42 } } };
|
||||
expect(getNestedProperty(obj, 'a', 'b', 'c')).toBe(42);
|
||||
});
|
||||
|
||||
test('returns undefined if any part of the path does not exist', () => {
|
||||
const obj = { a: { b: { c: 42 } } };
|
||||
expect(getNestedProperty(obj, 'a', 'x', 'c')).toBeUndefined();
|
||||
expect(getNestedProperty(obj, 'a', 'b', 'x')).toBeUndefined();
|
||||
expect(getNestedProperty(obj, 'x', 'b', 'c')).toBeUndefined();
|
||||
@@ -20,8 +20,7 @@ describe('getNestedProperty', () => {
|
||||
expect(getNestedProperty('string', 'a', 'b')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns undefined if no keys are provided', () => {
|
||||
const obj = { a: 1 };
|
||||
expect(getNestedProperty(obj)).toBeUndefined();
|
||||
test('returns the object if no keys are provided', () => {
|
||||
expect(getNestedProperty({ a: 1 })).toStrictEqual({ a: 1 });
|
||||
});
|
||||
});
|
||||
@@ -1,52 +1,37 @@
|
||||
import { isPlainObject } from '@reduxjs/toolkit';
|
||||
|
||||
export type RecordObject = Record<PropertyKey, unknown>;
|
||||
import type { Get, IsUnknown, UnknownRecord } from 'type-fest';
|
||||
|
||||
export function isRecordObject(obj: unknown): obj is RecordObject {
|
||||
export function isRecordObject(obj: unknown): obj is UnknownRecord {
|
||||
return isPlainObject(obj);
|
||||
}
|
||||
|
||||
type NestedProperty<T, K extends readonly PropertyKey[]> = K extends readonly [
|
||||
infer Head,
|
||||
...infer Tail,
|
||||
]
|
||||
? Head extends keyof NonNullable<T>
|
||||
? Tail extends readonly PropertyKey[]
|
||||
? NestedProperty<NonNullable<T>[Head], Tail>
|
||||
: NonNullable<T>[Head]
|
||||
: undefined
|
||||
: T;
|
||||
type NestedProperty<TObject, TKeys extends readonly string[]> =
|
||||
IsUnknown<TObject> extends true
|
||||
? unknown
|
||||
: IsUnknown<Get<TObject, TKeys>> extends true
|
||||
? undefined
|
||||
: Get<TObject, TKeys>;
|
||||
|
||||
export function getNestedProperty<TObject>(object: TObject): TObject;
|
||||
export function getNestedProperty<
|
||||
TObject extends RecordObject,
|
||||
const TKeys extends readonly PropertyKey[],
|
||||
>(object: TObject, ...keys: TKeys): NestedProperty<TObject, TKeys> | undefined;
|
||||
export function getNestedProperty(
|
||||
object: unknown,
|
||||
...keys: PropertyKey[]
|
||||
): unknown;
|
||||
export function getNestedProperty(
|
||||
object: unknown,
|
||||
...keys: PropertyKey[]
|
||||
): unknown {
|
||||
if (!isRecordObject(object) || keys.length === 0) {
|
||||
return undefined;
|
||||
TObject,
|
||||
const TKeys extends readonly string[],
|
||||
>(object: TObject, ...keys: TKeys): NestedProperty<TObject, TKeys>;
|
||||
export function getNestedProperty(object: unknown, ...keys: readonly string[]) {
|
||||
if (keys.length === 0) {
|
||||
return object;
|
||||
}
|
||||
|
||||
const remainingKeys = [...keys];
|
||||
let currentObject: RecordObject = object;
|
||||
while (remainingKeys.length > 0) {
|
||||
const currentKey = remainingKeys.shift();
|
||||
if (currentKey !== undefined && currentKey in currentObject) {
|
||||
const nextObject = currentObject[currentKey];
|
||||
if (isRecordObject(nextObject)) {
|
||||
currentObject = nextObject;
|
||||
continue;
|
||||
} else if (remainingKeys.length === 0) {
|
||||
return nextObject;
|
||||
}
|
||||
let currentValue = object;
|
||||
|
||||
for (const key of keys) {
|
||||
if (!isRecordObject(currentValue) || !(key in currentValue)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
currentValue = currentValue[key];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return currentValue;
|
||||
}
|
||||
@@ -1,41 +1,5 @@
|
||||
/**
|
||||
* Extend an existing type and make some of its properties required or optional.
|
||||
* @example
|
||||
* interface Person {
|
||||
* name: string;
|
||||
* age?: number;
|
||||
* likesIceCream?: boolean;
|
||||
* }
|
||||
*
|
||||
* type PersonWithSomeRequired = SomeRequired<Person, 'age' | 'likesIceCream' >;
|
||||
* type PersonWithSomeOptional = SomeOptional<Person, 'name' >;
|
||||
*/
|
||||
import type { SetOptional } from 'type-fest';
|
||||
|
||||
export type DeepPartial<T> = T extends object
|
||||
? {
|
||||
[K in keyof T]?: DeepPartial<T[K]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
export type SomeRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
||||
export type SomeOptional<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> &
|
||||
Partial<Pick<T, K>>;
|
||||
|
||||
export type RequiredExcept<T, K extends keyof T> = SomeOptional<Required<T>, K>;
|
||||
|
||||
export type OmitValueType<T, V> = {
|
||||
[K in keyof T as T[K] extends V ? never : K]: T[K];
|
||||
};
|
||||
|
||||
export type OmitUnion<TUnion, TBase> = TBase & Omit<TUnion, keyof TBase>;
|
||||
|
||||
export type PickValueType<T, V> = {
|
||||
[K in keyof T as T[K] extends V | undefined ? K : never]: T[K];
|
||||
};
|
||||
export type RequiredExcept<T, K extends keyof T> = SetOptional<Required<T>, K>;
|
||||
|
||||
export type AnyFunction = (...args: never) => unknown;
|
||||
|
||||
export type SnakeToCamelCase<S extends string> =
|
||||
S extends `${infer T}_${infer U}`
|
||||
? `${T}${Capitalize<SnakeToCamelCase<U>>}`
|
||||
: S;
|
||||
Reference in new issue
Block a user