Skip to content

Commit

Permalink
Improve accessibility on external properties tab, fix erroneous empty…
Browse files Browse the repository at this point in the history
… messages on preview

Allow non-subscribers to preview external properties
  • Loading branch information
synzen committed Feb 16, 2025
1 parent cb1a035 commit 20cb82a
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 345 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const SubscriberBlockText = ({ alternateText, onClick, feature, supporter

return (
<Stack>
<Alert rounded="md" colorScheme="purple">
<Alert rounded="md" colorScheme="purple" status="info" role={undefined}>
<AlertDescription>
<Box>
<Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { GroupBase, StylesConfig } from "react-select";
import { theme } from "@chakra-ui/react";
import getChakraColor from "@/utils/getChakraColor";

interface SelectOption {
export interface SelectOption {
value: string;
label: string;
icon?: string | React.ReactNode;
description?: string;
}

type SelectStyles = StylesConfig<SelectOption, false, GroupBase<SelectOption>> | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
UpdateUserFeedOutput,
} from "../api";

export const useUpdateUserFeed = () => {
interface Props {
queryKeyStringsToIgnoreValidation?: string[];
}

export const useUpdateUserFeed = (props?: Props) => {
const queryClient = useQueryClient();

return useMutation<UpdateUserFeedOutput, ApiAdapterError, UpdateUserFeedInput>(
Expand All @@ -16,6 +20,19 @@ export const useUpdateUserFeed = () => {
onSuccess: async (data, inputData) => {
await queryClient.invalidateQueries({
predicate: (query) => {
const queryKeyStringsToIgnoreValidation = new Set(
props?.queryKeyStringsToIgnoreValidation
);

if (
queryKeyStringsToIgnoreValidation &&
query.queryKey.some(
(item) => typeof item === "string" && queryKeyStringsToIgnoreValidation.has(item)
)
) {
return false;
}

return query.queryKey[0] === "user-feeds" || query.queryKey.includes(inputData.feedId);
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ export interface UseUserFeedArticlesProps {
};
onSuccess?: (data: GetUserFeedArticlesOutput) => void;
disabled?: boolean;
queryKeyFields?: string[];
}

export const useUserFeedArticles = ({
feedId,
data: inputData,
onSuccess,
disabled,
queryKeyFields,
}: UseUserFeedArticlesProps) => {
const queryKey = [
"user-feed-articles",
Expand All @@ -25,6 +27,7 @@ export const useUserFeedArticles = ({
inputData,
},
feedId,
...(queryKeyFields || []),
];

const { data, status, error, refetch, fetchStatus } = useQuery<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import {
Box,
Button,
Code,
Flex,
HStack,
Link,
Modal,
ModalBody,
ModalCloseButton,
Expand All @@ -14,7 +12,7 @@ import {
ModalOverlay,
Radio,
RadioGroup,
Skeleton,
Spinner,
Stack,
Table,
TableContainer,
Expand All @@ -24,13 +22,12 @@ import {
Text,
Th,
Thead,
Tooltip,
Tr,
chakra,
useDisclosure,
} from "@chakra-ui/react";
import { cloneElement, useEffect, useState } from "react";
import { ExternalLinkIcon, RepeatIcon } from "@chakra-ui/icons";
import { RepeatIcon, StarIcon } from "@chakra-ui/icons";
import { SelectArticlePropertyType, useUserFeedArticles } from "../../../feed";
import { useUserFeedContext } from "../../../../contexts/UserFeedContext";
import { InlineErrorAlert } from "../../../../components";
Expand All @@ -40,7 +37,7 @@ interface Props {
onSubmitted: (data: { sourceField: string }) => void;
}

const CreateArticleInjectionModal = ({ trigger, onSubmitted }: Props) => {
const CreateExternalPropertyModal = ({ trigger, onSubmitted }: Props) => {
const { userFeed, articleFormatOptions } = useUserFeedContext();
const { isOpen, onClose, onOpen } = useDisclosure();
const [selected, setSelected] = useState("");
Expand All @@ -49,6 +46,7 @@ const CreateArticleInjectionModal = ({ trigger, onSubmitted }: Props) => {
error,
refetch,
fetchStatus,
status,
} = useUserFeedArticles({
feedId: userFeed.id,
data: {
Expand All @@ -59,6 +57,7 @@ const CreateArticleInjectionModal = ({ trigger, onSubmitted }: Props) => {
random: true,
formatOptions: articleFormatOptions,
},
disabled: !isOpen,
});

// @ts-ignore
Expand All @@ -68,6 +67,10 @@ const CreateArticleInjectionModal = ({ trigger, onSubmitted }: Props) => {
const articleObjectEntries = Object.entries(article ?? {});

const onClickRandomize = async () => {
if (fetchStatus === "fetching") {
return;
}

await refetch();
};

Expand All @@ -79,7 +82,7 @@ const CreateArticleInjectionModal = ({ trigger, onSubmitted }: Props) => {

useEffect(() => {
if (isOpen && linkExists && !selected) {
setSelected("link");
// setSelected("link");
}
}, [linkExists, selected, isOpen]);

Expand All @@ -92,100 +95,94 @@ const CreateArticleInjectionModal = ({ trigger, onSubmitted }: Props) => {
<ModalHeader>Create a new external property</ModalHeader>
<ModalCloseButton />
<ModalBody tabIndex={-1}>
<Box srOnly aria-live="polite" aria-busy={status === "loading"}>
{status === "success" &&
`Finished loading ${articleObjectEntries.length} article properties`}
</Box>
<Stack spacing={4} paddingBottom={4}>
<Text>
Select the source property containing the URL that references the page with the
desired content.
</Text>
{error && (
<InlineErrorAlert
title="Failed to get article properties"
description={error.message}
/>
)}
{!error && (
<Box bg="gray.800" p={2} rounded="lg">
<RadioGroup onChange={setSelected} value={selected}>
<TableContainer overflow="auto">
<Table size="sm">
<Thead>
<Tr>
<Th />
<Th>Article Property</Th>
<Th>
Sample Article Value
<Tooltip label="See another random article's values">
<Button
size="xs"
ml={2}
isLoading={fetchStatus === "fetching"}
onClick={onClickRandomize}
variant="outline"
leftIcon={<RepeatIcon />}
aria-label="See another random article's values"
>
<span>Randomize sample article</span>
</Button>
</Tooltip>
</Th>
</Tr>
</Thead>
<Tbody>
{articleObjectEntries.map(([field, value]) => {
if (field === "id" || field === "idHash" || !value) {
return null;
}
{status === "loading" && (
<Stack alignItems="center" width="100%" aria-busy>
<Spinner />
<Text>Loading article properties...</Text>
</Stack>
)}
{status === "success" && !error && (
<Stack spacing={4}>
<Text>
Select the source property containing the URL that references the page with the
desired content. If you don&apos;t see a property that fits, you can randomize
the article to get a new sample by clicking the randomize button. The
recommended source property is the link property.
</Text>
<Box>
<Button
isLoading={fetchStatus === "fetching"}
aria-disabled={fetchStatus === "fetching"}
onClick={onClickRandomize}
leftIcon={<RepeatIcon />}
>
<span>Randomize sample article</span>
</Button>
</Box>
<Box bg="gray.800" p={2} rounded="lg" overflow="auto">
<chakra.fieldset>
<chakra.legend srOnly>Source Property</chakra.legend>
<RadioGroup onChange={setSelected} value={selected}>
<TableContainer role="presentation">
<Table size="sm">
<Thead>
<Tr>
<Th>Article Property</Th>
<Th>Sample Article Value</Th>
</Tr>
</Thead>
<Tbody>
{articleObjectEntries.map(([field, value]) => {
if (field === "id" || field === "idHash" || !value) {
return null;
}

return (
<Tr key={field}>
<Td width="min-content">
<Radio
value={field}
id={`field-${field}`}
name="field"
isDisabled={fetchStatus !== "idle"}
/>
</Td>
<Td>
<Skeleton isLoaded={fetchStatus === "idle"}>
<chakra.label htmlFor={`field-${field}`}>
<Code>{field}</Code>
</chakra.label>
{field === "link" && (
<Tag ml={3} colorScheme="blue" size="sm">
Recommended
</Tag>
)}
</Skeleton>
</Td>
<Td whiteSpace="nowrap">
<Skeleton isLoaded={fetchStatus === "idle"}>
<Flex
as={chakra.label}
alignItems="center"
htmlFor={`field-${field}`}
gap={2}
>
{value}
<Link
color="blue.300"
href={value}
target="_blank"
rel="noopener noreferrer"
return (
<Tr key={field}>
<Td>
<Radio
value={field}
id={`field-${field}`}
aria-labelledby={`field-${field}-label`}
mr={2}
/>
<chakra.label
htmlFor={`field-${field}`}
id={`field-${field}-label`}
>
<ExternalLinkIcon />
</Link>
</Flex>
</Skeleton>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</RadioGroup>
</Box>
<Code>{field}</Code>
{field === "link" && (
<Tag ml={3} colorScheme="green" size="sm">
<StarIcon mr={1} aria-hidden />
Recommended source property
</Tag>
)}
</chakra.label>
</Td>
<Td whiteSpace="normal" wordBreak="break-all">
{value}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</RadioGroup>
</chakra.fieldset>
</Box>
</Stack>
)}
</Stack>
</ModalBody>
Expand All @@ -197,7 +194,12 @@ const CreateArticleInjectionModal = ({ trigger, onSubmitted }: Props) => {
<Button
colorScheme="blue"
isDisabled={!selected}
aria-disabled={!selected}
onClick={() => {
if (!selected) {
return;
}

onSubmitted({ sourceField: selected });
onClose();
}}
Expand All @@ -212,4 +214,4 @@ const CreateArticleInjectionModal = ({ trigger, onSubmitted }: Props) => {
);
};

export default CreateArticleInjectionModal;
export default CreateExternalPropertyModal;
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,13 @@ import { chakra, Text } from "@chakra-ui/react";

export const CssSelectorFormattedOption = ({
label,
description,
isSelected,
}: {
label: string;
description?: string;
isSelected: boolean;
}) => {
let description = "";

switch (label) {
case "img":
description = "Targets all images on the page.";
break;
case "a":
description = "Targets all links on the page.";
break;
case 'meta[property="og:image"]':
description = "Targets the image used when sharing the page on social media.";
break;
default:
break;
}

return (
<div>
<chakra.span fontFamily="mono">{label}</chakra.span>
Expand Down
Loading

0 comments on commit 20cb82a

Please sign in to comment.