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