[Glitch] Mark pending accounts in collection editor

Port 41a3679d83f5f767c013dd4a29c153869469b03a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion 2026-04-29 12:35:09 +02:00 committed by Claire
parent 49f60c0b76
commit 26cee35f50
4 changed files with 84 additions and 44 deletions

View File

@ -116,7 +116,7 @@ const RevokeControls: React.FC<{
);
};
const PendingNote: React.FC = () => {
export const PendingNote: React.FC = () => {
return (
<Callout
variant='subtle'

View File

@ -6,11 +6,11 @@ import { useHistory } from 'react-router-dom';
import type { Map as ImmutableMap } from 'immutable';
import { useComboboxItemProps } from '@/flavours/glitch/components/form_fields/combobox_field';
import type { ApiMutedAccountJSON } from 'flavours/glitch/api_types/accounts';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { AccountListItem } from 'flavours/glitch/components/account_list_item';
import { Avatar } from 'flavours/glitch/components/avatar';
import { PendingBadge } from 'flavours/glitch/components/badge';
import { Button } from 'flavours/glitch/components/button';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { useAccountHandle } from 'flavours/glitch/components/display_name/default';
@ -19,6 +19,7 @@ import {
FormStack,
ComboboxField,
} from 'flavours/glitch/components/form_fields';
import { useComboboxItemProps } from 'flavours/glitch/components/form_fields/combobox_field';
import {
ListItemContent,
ListItemWrapper,
@ -34,12 +35,14 @@ import { domain } from 'flavours/glitch/initial_state';
import type { Relationship } from 'flavours/glitch/models/relationship';
import {
addCollectionItem,
getCollectionItemIds,
getEditorCollectionItems,
removeCollectionItem,
updateCollectionEditorField,
} from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { PendingNote } from '../detail';
import classes from './styles.module.scss';
import { WizardStepTitle } from './wizard_step_title';
@ -47,8 +50,9 @@ const MAX_ACCOUNT_COUNT = 25;
const AddedAccountItem: React.FC<{
accountId: string;
pending?: boolean;
onRemove: (id: string) => void;
}> = ({ accountId, onRemove }) => {
}> = ({ accountId, pending, onRemove }) => {
const handleRemoveAccount = useCallback(() => {
onRemove(accountId);
}, [accountId, onRemove]);
@ -65,7 +69,13 @@ const AddedAccountItem: React.FC<{
[handleRemoveAccount],
);
return <AccountListItem accountId={accountId} renderButton={renderButton} />;
return (
<AccountListItem
accountId={accountId}
badge={pending && <PendingBadge />}
renderButton={renderButton}
/>
);
};
const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => {
@ -186,22 +196,26 @@ export const CollectionAccounts: React.FC<{
const { id, items: collectionItems } = collection ?? {};
const isEditMode = !!id;
const addedAccountIds = useAppSelector(
(state) => state.collections.editor.accountIds,
const editorItemsFromState = useAppSelector(
(state) => state.collections.editor.items,
);
// In edit mode, we're bypassing state and just return collection items directly,
// since they're edited "live", saving after each addition/deletion
const accountIds = useMemo(
// In edit mode, we're bypassing our Redux state and just work on the
// collection items directly since they're edited "live", saving right
// after each addition/deletion
const editorItems = useMemo(
() =>
isEditMode ? getCollectionItemIds(collectionItems) : addedAccountIds,
[isEditMode, collectionItems, addedAccountIds],
isEditMode
? getEditorCollectionItems(collectionItems)
: editorItemsFromState,
[isEditMode, collectionItems, editorItemsFromState],
);
const hasPendingItems = editorItems.some((item) => item.state === 'pending');
const [searchValue, setSearchValue] = useState('');
const hasAccounts = accountIds.length > 0;
const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT;
const hasItems = editorItems.length > 0;
const hasMaxItems = editorItems.length === MAX_ACCOUNT_COUNT;
const {
accounts: suggestedAccounts,
@ -211,7 +225,8 @@ export const CollectionAccounts: React.FC<{
} = useSearchAccounts({
withRelationships: true,
// Don't suggest accounts that were already added
filterResults: (account) => !accountIds.includes(account.id),
filterResults: (account) =>
!editorItems.find((item) => item.account_id === account.id),
});
const relationships = useAppSelector((state) => state.relationships);
@ -239,24 +254,33 @@ export const CollectionAccounts: React.FC<{
(accountId: string) => {
dispatch(
updateCollectionEditorField({
field: 'accountIds',
value: accountIds.filter((id) => id !== accountId),
field: 'items',
value: editorItems.filter((item) => item.account_id !== accountId),
}),
);
},
[accountIds, dispatch],
[editorItems, dispatch],
);
const addAccountItem = useCallback(
(item: ApiMutedAccountJSON) => {
dispatch(
updateCollectionEditorField({
field: 'accountIds',
value: [...accountIds, item.id],
field: 'items',
value: [
...editorItems,
{
account_id: item.id,
state:
item.feature_approval.current_user === 'manual'
? 'pending'
: 'accepted',
},
],
}),
);
},
[accountIds, dispatch],
[editorItems, dispatch],
);
const instantRemoveAccountItem = useCallback(
@ -322,12 +346,10 @@ export const CollectionAccounts: React.FC<{
e.preventDefault();
if (!id) {
history.push(`/collections/new/details`, {
account_ids: accountIds,
});
history.push('/collections/new/details');
}
},
[id, history, accountIds],
[id, history],
);
const inputId = useId();
@ -348,16 +370,17 @@ export const CollectionAccounts: React.FC<{
}
/>
)}
{hasPendingItems && <PendingNote />}
<ComboboxField
id={inputId}
label={intl.formatMessage({
id: 'collections.search_accounts_label',
defaultMessage: 'Search for an account to add',
})}
value={hasMaxAccounts ? '' : searchValue}
value={hasMaxItems ? '' : searchValue}
onChange={handleSearchValueChange}
onKeyDown={handleSearchKeyDown}
disabled={hasMaxAccounts}
disabled={hasMaxItems}
isLoading={isLoadingSuggestions}
items={groupedItems}
getItemId={getItemId}
@ -366,7 +389,7 @@ export const CollectionAccounts: React.FC<{
renderGroupTitle={renderGroupTitle}
onSelectItem={handleSelectItem}
status={
hasMaxAccounts
hasMaxItems
? {
variant: 'warning',
message: intl.formatMessage({
@ -381,12 +404,12 @@ export const CollectionAccounts: React.FC<{
</header>
<div>
{hasAccounts && (
{hasItems && (
<AccountsHeadingElement className={classes.listHeading}>
<FormattedMessage
id='collections.hints.accounts_counter'
defaultMessage='{count}/{max} accounts'
values={{ count: accountIds.length, max: MAX_ACCOUNT_COUNT }}
values={{ count: editorItems.length, max: MAX_ACCOUNT_COUNT }}
/>
</AccountsHeadingElement>
)}
@ -413,14 +436,15 @@ export const CollectionAccounts: React.FC<{
/>
}
>
{accountIds.map((accountId, index) => (
{editorItems.map(({ account_id, state }, index) => (
<Article
key={accountId}
key={account_id}
aria-posinset={index}
aria-setsize={accountIds.length}
aria-setsize={editorItems.length}
>
<AddedAccountItem
accountId={accountId}
accountId={account_id}
pending={state === 'pending'}
onRemove={handleRemoveAccountItem}
/>
</Article>
@ -429,7 +453,7 @@ export const CollectionAccounts: React.FC<{
</Scrollable>
</div>
</FormStack>
{!isEditMode && hasAccounts && (
{!isEditMode && hasItems && (
<div className={classes.stickyFooter}>
<Button type='submit'>
{id ? (

View File

@ -46,7 +46,7 @@ import { WizardStepTitle } from './wizard_step_title';
export const CollectionDetails: React.FC = () => {
const dispatch = useAppDispatch();
const history = useHistory();
const { id, name, description, topic, discoverable, sensitive, accountIds } =
const { id, name, description, topic, discoverable, sensitive, items } =
useAppSelector((state) => state.collections.editor);
const handleNameChange = useCallback(
@ -123,7 +123,7 @@ export const CollectionDetails: React.FC = () => {
description,
discoverable,
sensitive,
account_ids: accountIds,
account_ids: items.map((item) => item.account_id),
};
if (topic) {
payload.tag_name = topic;
@ -152,7 +152,7 @@ export const CollectionDetails: React.FC = () => {
sensitive,
dispatch,
history,
accountIds,
items,
currentUserName,
],
);

View File

@ -16,6 +16,7 @@ import type {
ApiCollectionJSON,
ApiCreateCollectionPayload,
ApiUpdateCollectionPayload,
CollectionAccountItem,
} from '@/flavours/glitch/api_types/collections';
import { me } from '@/flavours/glitch/initial_state';
import {
@ -41,6 +42,16 @@ interface CollectionState {
editor: EditorState;
}
/**
* This is a subset of the `CollectionAccountItem` type
* for use in the editor. Here, `account_id` is always defined
* and `state` is more limited.
*/
export interface EditorCollectionItem {
account_id: string;
state: 'pending' | 'accepted';
}
interface EditorState {
id: string | null;
name: string;
@ -49,7 +60,7 @@ interface EditorState {
language: string | null;
discoverable: boolean;
sensitive: boolean;
accountIds: string[];
items: EditorCollectionItem[];
}
interface UpdateEditorFieldPayload<K extends keyof EditorState> {
@ -68,7 +79,7 @@ const initialState: CollectionState = {
language: null,
discoverable: true,
sensitive: false,
accountIds: [],
items: [],
},
};
@ -87,7 +98,7 @@ const collectionSlice = createSlice({
language: collection?.language ?? '',
discoverable: collection?.discoverable ?? true,
sensitive: collection?.sensitive ?? false,
accountIds: getCollectionItemIds(collection?.items ?? []),
items: getEditorCollectionItems(collection?.items ?? []),
};
},
reset(state) {
@ -338,7 +349,12 @@ export const selectAccountCollections = createAppSelector(
},
);
const onlyExistingIds = (id?: string): id is string => !!id;
const isEditorItem = (
item: Partial<CollectionAccountItem>,
): item is EditorCollectionItem =>
!!item.account_id && (item.state === 'accepted' || item.state === 'pending');
export const getCollectionItemIds = (items?: ApiCollectionJSON['items']) =>
items?.map((item) => item.account_id).filter(onlyExistingIds) ?? [];
export const getEditorCollectionItems = (items?: CollectionAccountItem[]) =>
items
?.map(({ account_id, state }) => ({ account_id, state }))
.filter(isEditorItem) ?? [];