Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multi squad post approval #4111

Merged
merged 25 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c3f4714
feat: make squad id optional for sourcePostModerations
AmarTrebinjac Jan 24, 2025
a9cdeb3
feat: move moderate page to squad folder
AmarTrebinjac Jan 24, 2025
f4f6370
chore: delete old moderate page
AmarTrebinjac Jan 24, 2025
63a901e
feat: make squad optional in useSourceModerationList
AmarTrebinjac Jan 24, 2025
b4f3482
feat: update squad header bar link
AmarTrebinjac Jan 24, 2025
8f6782b
feat: add squad icon on moderation item
AmarTrebinjac Jan 26, 2025
4c971f6
feat: Update fetching logic
AmarTrebinjac Jan 26, 2025
3b0f1db
feat: add pending posts to sidebar
AmarTrebinjac Jan 26, 2025
70150f4
feat: add pending button to my
AmarTrebinjac Jan 26, 2025
9adabe8
feat: bulk approval for multiple squads
AmarTrebinjac Jan 26, 2025
a2ba4bd
feat: update squad tabs
AmarTrebinjac Jan 27, 2025
ea7ca1e
feat: update squadtab props
AmarTrebinjac Jan 27, 2025
94dc603
Merge branch 'main' into MI-635
AmarTrebinjac Jan 27, 2025
e08a8fa
Merge branch 'main' into MI-635
AmarTrebinjac Jan 27, 2025
d4e15d5
fix: pass correct prop
AmarTrebinjac Jan 27, 2025
7960c8f
fix: pass squad correctly
AmarTrebinjac Jan 27, 2025
fb4c6bd
Merge branch 'main' into MI-635
AmarTrebinjac Jan 27, 2025
3063fa5
Merge branch 'main' into MI-635
AmarTrebinjac Jan 28, 2025
e430803
feat: add SSR
AmarTrebinjac Jan 30, 2025
85d791b
feat: updated logic to not fetch from sidebar
AmarTrebinjac Feb 2, 2025
5e16656
Merge branch 'main' into MI-635
AmarTrebinjac Feb 4, 2025
e45a357
fix: remove default disabled in sidebar
AmarTrebinjac Feb 4, 2025
e2a44e1
Merge branch 'main' into MI-635
sshanzel Feb 4, 2025
b4ee2ca
Merge branch 'main' into MI-635
AmarTrebinjac Feb 6, 2025
209ef09
chore: add redirect
AmarTrebinjac Feb 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { ReactElement } from 'react';
import React, { useMemo } from 'react';
import type { SidebarMenuItem } from '../common';
import { ListIcon } from '../common';
import { DefaultSquadIcon, NewSquadIcon, SourceIcon } from '../../icons';
import {
DefaultSquadIcon,
NewSquadIcon,
SourceIcon,
TimerIcon,
} from '../../icons';
import { Section } from '../Section';
import { Origin } from '../../../lib/log';
import { useSquadNavigation } from '../../../hooks';
Expand All @@ -11,13 +16,17 @@ import { SquadImage } from '../../squads/SquadImage';
import { SidebarSettingsFlags } from '../../../graphql/settings';
import { webappUrl } from '../../../lib/constants';
import type { SidebarSectionProps } from './common';
import { useSquadPendingPosts } from '../../../hooks/squads/useSquadPendingPosts';
import { Typography, TypographyColor } from '../../typography/Typography';

export const NetworkSection = ({
isItemsButton,
...defaultRenderSectionProps
}: SidebarSectionProps): ReactElement => {
const { squads } = useAuthContext();
const { count, isModeratorInAnySquad } = useSquadPendingPosts();
const { openNewSquad } = useSquadNavigation();

const menuItems: SidebarMenuItem[] = useMemo(() => {
const squadItems =
squads?.map((squad) => {
Expand All @@ -33,8 +42,22 @@ export const NetworkSection = ({
path: `${webappUrl}squads/${handle}`,
};
}) ?? [];

return [
isModeratorInAnySquad &&
count > 0 && {
icon: () => <ListIcon Icon={() => <TimerIcon />} />,
title: 'Pending Posts',
path: `${webappUrl}squads/moderate`,
rightIcon: () => (
<Typography
color={TypographyColor.Secondary}
bold
className="rounded-6 bg-background-subtle px-1.5"
>
{count}
</Typography>
),
},
{
icon: (active: boolean) => (
<ListIcon Icon={() => <SourceIcon secondary={active} />} />
Expand All @@ -51,7 +74,7 @@ export const NetworkSection = ({
requiresLogin: true,
},
].filter(Boolean);
}, [openNewSquad, squads]);
}, [openNewSquad, squads, count, isModeratorInAnySquad]);

return (
<Section
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/components/squads/SquadHeaderBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ const SquadModerationButton = ({ squad }: SquadBarButtonProps<'a'>) => {
return (
<Button
aria-label={`Check ${count} pending ${postLabel}`}
href={`/squads/${squad.handle}/moderate`}
href={`/squads/moderate?handle=${squad.handle}`}
icon={<TimerIcon aria-hidden role="presentation" />}
size={ButtonSize.Small}
tag="a"
Expand Down
19 changes: 12 additions & 7 deletions packages/shared/src/components/squads/SquadTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { ReactElement } from 'react';
import React from 'react';
import { TabContainer, Tab } from '../tabs/TabContainer';
import { webappUrl } from '../../lib/constants';
import type { Squad } from '../../graphql/sources';
import { SourcePermissions } from '../../graphql/sources';
import { verifyPermission } from '../../graphql/squads';
import { useSquad } from '../../hooks';

export enum SquadTab {
Settings = 'Settings',
Expand All @@ -13,15 +13,17 @@ export enum SquadTab {

interface SquadTabsProps {
active: SquadTab;
squad: Squad;
handle: string;
}

export function SquadTabs({ active, squad }: SquadTabsProps): ReactElement {
const { handle, moderationPostCount } = squad;
export function SquadTabs({ active, handle }: SquadTabsProps): ReactElement {
const { squad } = useSquad({
handle,
});
const isModerator = verifyPermission(squad, SourcePermissions.ModeratePost);
const squadLink = `${webappUrl}squads/${handle}`;
const pendingTabLabel = moderationPostCount
? `${SquadTab.PendingPosts} (${moderationPostCount})`
const pendingTabLabel = squad?.moderationPostCount
? `${SquadTab.PendingPosts} (${squad?.moderationPostCount})`
: SquadTab.PendingPosts;

const links = [
Expand All @@ -33,7 +35,10 @@ export function SquadTabs({ active, squad }: SquadTabsProps): ReactElement {
},
]
: []),
{ label: pendingTabLabel, url: `${squadLink}/moderate` },
{
label: pendingTabLabel,
url: `${webappUrl}squads/moderate?handle=${handle}`,
},
];

const controlledActive =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactElement } from 'react';
import React from 'react';
import { useSearchParams } from 'next/navigation';
import {
Typography,
TypographyColor,
Expand All @@ -21,13 +22,16 @@ import { useTruncatedSummary } from '../../../hooks';
import type { SquadModerationItemProps } from './useSourceModerationItem';
import { useSourceModerationItem } from './useSourceModerationItem';
import { SquadModerationItemContextMenu } from './SquadModerationItemContextMenu';
import SourceProfilePicture from '../../profile/SourceProfilePicture';

export function SquadModerationItem(
props: SquadModerationItemProps,
): ReactElement {
const searchParams = useSearchParams();
const handle = searchParams?.get('handle');
const { context, modal, user } = useSourceModerationItem(props);
const { data, squad, onApprove, onReject, isPending } = props;
const { rejectionReason, createdBy, createdAt, image, status } = data;
const { rejectionReason, createdBy, createdAt, image, status, source } = data;

const IconComponent =
status === SourcePostModerationStatus.Rejected ? WarningIcon : TimerIcon;
Expand All @@ -45,6 +49,21 @@ export function SquadModerationItem(
onClick={modal.open}
type="button"
/>
{!handle && (
<div className="flex gap-2">
<SourceProfilePicture
className="pointer-events-none"
source={source}
size={ProfileImageSize.Small}
/>
<Typography
color={TypographyColor.Tertiary}
type={TypographyType.Callout}
>
{source.name}
</Typography>
</div>
)}
<div className="flex flex-row gap-4">
<ProfilePicture
className="pointer-events-none"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,33 @@ import { useSourceModerationList } from '../../../hooks/squads/useSourceModerati
import { useSquadPendingPosts } from '../../../hooks/squads/useSquadPendingPosts';
import { SquadModerationItem } from './SquadModerationItem';
import type { Squad } from '../../../graphql/sources';
import { SourcePermissions } from '../../../graphql/sources';
import InfiniteScrolling from '../../containers/InfiniteScrolling';
import {
SourcePostModerationStatus,
verifyPermission,
} from '../../../graphql/squads';
import { SourcePostModerationStatus } from '../../../graphql/squads';
import { EmptyModerationList } from './SquadModerationEmptyScreen';
import InfiniteScrolling from '../../containers/InfiniteScrolling';

interface SquadModerationListProps {
squad: Squad;
isModerator: boolean;
}

export function SquadModerationList({
squad,
isModerator,
}: SquadModerationListProps): ReactElement {
const moderate = useSourceModerationList({
squad,
});
const isModerator = verifyPermission(squad, SourcePermissions.ModeratePost);

const { data, isFetched, fetchNextPage, hasNextPage, isPending } =
useSquadPendingPosts(
squad?.id,
isModerator
useSquadPendingPosts({
squadId: squad?.id,
status: isModerator
? [SourcePostModerationStatus.Pending]
: [
SourcePostModerationStatus.Pending,
SourcePostModerationStatus.Rejected,
],
);
});

const list = useMemo(
() =>
Expand All @@ -57,10 +55,7 @@ export function SquadModerationList({
variant={ButtonVariant.Primary}
size={ButtonSize.Small}
onClick={() =>
moderate.onApprove(
list.map((request) => request.id),
squad.id,
)
moderate.onApprove(list.map((request) => request.id))
}
>
Approve all {list.length} posts
Expand All @@ -78,8 +73,8 @@ export function SquadModerationList({
squad={squad}
data={item}
isPending={isPending}
onReject={() => moderate.onReject(item.id, squad.id)}
onApprove={() => moderate.onApprove([item.id], squad.id)}
onReject={() => moderate.onReject(item.id, item.source.id)}
onApprove={() => moderate.onApprove([item.id], item.source.id)}
/>
))}
</InfiniteScrolling>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MouseEventHandler } from 'react';
import { useId } from 'react';
import { useSearchParams } from 'next/navigation';
import type { SourcePostModeration } from '../../../graphql/squads';
import { verifyPermission } from '../../../graphql/squads';
import useContextMenu from '../../../hooks/useContextMenu';
Expand Down Expand Up @@ -35,16 +36,19 @@ interface UseSourceModerationItem {

export const useSourceModerationItem = ({
data,
squad,
onApprove,
onReject,
}: SquadModerationItemProps): UseSourceModerationItem => {
const squad = data.source as Squad;
const searchParams = useSearchParams();
const contextMenuId = useId();
const { isOpen, onMenuClick } = useContextMenu({ id: contextMenuId });

const { openModal, closeModal } = useLazyModal();

const isModerator = verifyPermission(squad, SourcePermissions.ModeratePost);
const isModerator =
verifyPermission(squad, SourcePermissions.ModeratePost) ||
!searchParams?.get('handle');

const { onDelete } = useSourceModerationList({ squad });

Expand Down
14 changes: 4 additions & 10 deletions packages/shared/src/graphql/squads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ const SOURCE_POST_MODERATION_FRAGMENT = gql`

export const SQUAD_PENDING_POSTS_QUERY = gql`
query sourcePostModerations(
$sourceId: ID!
$sourceId: ID
$status: [String]
$first: Int
$after: String
Expand Down Expand Up @@ -662,14 +662,12 @@ export const SQUAD_MODERATE_POST_MUTATION = gql`
mutation ModerateSourcePost(
$postIds: [ID]!
$status: String
$sourceId: ID!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to be careful with deployment. If we are updating the existing mutation, we must merge this PR at the same time the API is deployed. Otherwise, requests might fail.

$rejectionReason: String
$moderatorMessage: String
) {
moderateSourcePosts(
postIds: $postIds
status: $status
sourceId: $sourceId
rejectionReason: $rejectionReason
moderatorMessage: $moderatorMessage
) {
Expand Down Expand Up @@ -702,24 +700,21 @@ export interface SquadPostRejectionProps extends SquadPostModerationProps {
note?: string;
}

export const squadApproveMutation = ({
postIds,
sourceId,
}: SquadPostModerationProps): Promise<SourcePostModeration[]> => {
export const squadApproveMutation = (
postIds: string[],
): Promise<SourcePostModeration[]> => {
return gqlClient
.request<{
moderateSourcePosts: SourcePostModeration[];
}>(SQUAD_MODERATE_POST_MUTATION, {
postIds,
sourceId,
status: SourcePostModerationStatus.Approved,
})
.then((res) => res.moderateSourcePosts);
};

export const squadRejectMutation = ({
postIds,
sourceId,
reason,
note,
}: SquadPostRejectionProps): Promise<SourcePostModeration[]> => {
Expand All @@ -728,7 +723,6 @@ export const squadRejectMutation = ({
moderateSourcePosts: SourcePostModeration[];
}>(SQUAD_MODERATE_POST_MUTATION, {
postIds,
sourceId,
status: SourcePostModerationStatus.Rejected,
rejectionReason: reason,
moderatorMessage: note,
Expand Down
Loading