mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 13:14:58 +08:00
Make space hierarchy a treeview
This commit is contained in:
parent
6fddfe0d59
commit
09f20bcda7
@ -150,13 +150,14 @@ const reducer = (state: IState, action: IAction) => {
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
handleHomeEnd?: boolean;
|
handleHomeEnd?: boolean;
|
||||||
|
handleUpDown?: boolean;
|
||||||
children(renderProps: {
|
children(renderProps: {
|
||||||
onKeyDownHandler(ev: React.KeyboardEvent);
|
onKeyDownHandler(ev: React.KeyboardEvent);
|
||||||
});
|
});
|
||||||
onKeyDown?(ev: React.KeyboardEvent, state: IState);
|
onKeyDown?(ev: React.KeyboardEvent, state: IState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, onKeyDown }) => {
|
export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeEnd, handleUpDown, onKeyDown }) => {
|
||||||
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
|
const [state, dispatch] = useReducer<Reducer<IState, IAction>>(reducer, {
|
||||||
activeRef: null,
|
activeRef: null,
|
||||||
refs: [],
|
refs: [],
|
||||||
@ -167,21 +168,50 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
|||||||
const onKeyDownHandler = useCallback((ev) => {
|
const onKeyDownHandler = useCallback((ev) => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
// Don't interfere with input default keydown behaviour
|
// Don't interfere with input default keydown behaviour
|
||||||
if (handleHomeEnd && ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
if (ev.target.tagName !== "INPUT" && ev.target.tagName !== "TEXTAREA") {
|
||||||
// check if we actually have any items
|
// check if we actually have any items
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.HOME:
|
case Key.HOME:
|
||||||
handled = true;
|
if (handleHomeEnd) {
|
||||||
// move focus to first item
|
handled = true;
|
||||||
if (context.state.refs.length > 0) {
|
// move focus to first item
|
||||||
context.state.refs[0].current.focus();
|
if (context.state.refs.length > 0) {
|
||||||
|
context.state.refs[0].current.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Key.END:
|
case Key.END:
|
||||||
handled = true;
|
if (handleHomeEnd) {
|
||||||
// move focus to last item
|
handled = true;
|
||||||
if (context.state.refs.length > 0) {
|
// move focus to last item
|
||||||
context.state.refs[context.state.refs.length - 1].current.focus();
|
if (context.state.refs.length > 0) {
|
||||||
|
context.state.refs[context.state.refs.length - 1].current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.ARROW_UP:
|
||||||
|
if (handleUpDown) {
|
||||||
|
handled = true;
|
||||||
|
if (context.state.refs.length > 0) {
|
||||||
|
const idx = context.state.refs.indexOf(context.state.activeRef);
|
||||||
|
if (idx > 1) {
|
||||||
|
context.state.refs[idx - 1].current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.ARROW_DOWN:
|
||||||
|
if (handleUpDown) {
|
||||||
|
handled = true;
|
||||||
|
if (context.state.refs.length > 0) {
|
||||||
|
const idx = context.state.refs.indexOf(context.state.activeRef);
|
||||||
|
if (idx < context.state.refs.length - 1) {
|
||||||
|
context.state.refs[idx + 1].current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -193,7 +223,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({ children, handleHomeE
|
|||||||
} else if (onKeyDown) {
|
} else if (onKeyDown) {
|
||||||
return onKeyDown(ev, context.state);
|
return onKeyDown(ev, context.state);
|
||||||
}
|
}
|
||||||
}, [context.state, onKeyDown, handleHomeEnd]);
|
}, [context.state, onKeyDown, handleHomeEnd, handleUpDown]);
|
||||||
|
|
||||||
return <RovingTabIndexContext.Provider value={context}>
|
return <RovingTabIndexContext.Provider value={context}>
|
||||||
{ children({ onKeyDownHandler }) }
|
{ children({ onKeyDownHandler }) }
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ReactNode, useMemo, useState } from "react";
|
import React, { ReactNode, KeyboardEvent, useMemo, useState } from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
|
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
|
||||||
@ -46,6 +46,8 @@ import { getDisplayAliasForAliasSet } from "../../Rooms";
|
|||||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
import { useDispatcher } from "../../hooks/useDispatcher";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
|
import { Key } from "../../Keyboard";
|
||||||
|
import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
|
||||||
|
|
||||||
interface IHierarchyProps {
|
interface IHierarchyProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
@ -80,6 +82,7 @@ const Tile: React.FC<ITileProps> = ({
|
|||||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||||
|
|
||||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||||
|
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||||
|
|
||||||
const onPreviewClick = (ev: ButtonEvent) => {
|
const onPreviewClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@ -94,11 +97,21 @@ const Tile: React.FC<ITileProps> = ({
|
|||||||
|
|
||||||
let button;
|
let button;
|
||||||
if (joinedRoom) {
|
if (joinedRoom) {
|
||||||
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
button = <AccessibleButton
|
||||||
|
onClick={onPreviewClick}
|
||||||
|
kind="primary_outline"
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
>
|
||||||
{ _t("View") }
|
{ _t("View") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
} else if (onJoinClick) {
|
} else if (onJoinClick) {
|
||||||
button = <AccessibleButton onClick={onJoinClick} kind="primary">
|
button = <AccessibleButton
|
||||||
|
onClick={onJoinClick}
|
||||||
|
kind="primary"
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
>
|
||||||
{ _t("Join") }
|
{ _t("Join") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
@ -106,13 +119,13 @@ const Tile: React.FC<ITileProps> = ({
|
|||||||
let checkbox;
|
let checkbox;
|
||||||
if (onToggleClick) {
|
if (onToggleClick) {
|
||||||
if (hasPermissions) {
|
if (hasPermissions) {
|
||||||
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} />;
|
checkbox = <StyledCheckbox checked={!!selected} onChange={onToggleClick} tabIndex={isActive ? 0 : -1} />;
|
||||||
} else {
|
} else {
|
||||||
checkbox = <TextWithTooltip
|
checkbox = <TextWithTooltip
|
||||||
tooltip={_t("You don't have permission")}
|
tooltip={_t("You don't have permission")}
|
||||||
onClick={ev => { ev.stopPropagation(); }}
|
onClick={ev => { ev.stopPropagation(); }}
|
||||||
>
|
>
|
||||||
<StyledCheckbox disabled={true} />
|
<StyledCheckbox disabled={true} tabIndex={isActive ? 0 : -1} />
|
||||||
</TextWithTooltip>;
|
</TextWithTooltip>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,19 +198,65 @@ const Tile: React.FC<ITileProps> = ({
|
|||||||
toggleShowChildren();
|
toggleShowChildren();
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
if (showChildren) {
|
if (showChildren) {
|
||||||
childSection = <div className="mx_SpaceRoomDirectory_subspace_children">
|
const onChildrenKeyDown = (e) => {
|
||||||
|
if (e.key === Key.ARROW_LEFT) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
ref.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
childSection = <div
|
||||||
|
className="mx_SpaceRoomDirectory_subspace_children"
|
||||||
|
onKeyDown={onChildrenKeyDown}
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
{ children }
|
{ children }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onKeyDown = children ? (e) => {
|
||||||
|
let handled = false;
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case Key.ARROW_LEFT:
|
||||||
|
if (showChildren) {
|
||||||
|
handled = true;
|
||||||
|
toggleShowChildren();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Key.ARROW_RIGHT:
|
||||||
|
handled = true;
|
||||||
|
if (showChildren) {
|
||||||
|
(ref.current?.nextElementSibling?.firstElementChild as HTMLElement)?.focus();
|
||||||
|
} else {
|
||||||
|
toggleShowChildren();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className={classNames("mx_SpaceRoomDirectory_roomTile", {
|
className={classNames("mx_SpaceRoomDirectory_roomTile", {
|
||||||
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
|
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
|
||||||
})}
|
})}
|
||||||
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
inputRef={ref}
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
aria-expanded={children ? showChildren : undefined}
|
||||||
|
role="treeitem"
|
||||||
>
|
>
|
||||||
{ content }
|
{ content }
|
||||||
{ childToggle }
|
{ childToggle }
|
||||||
@ -414,176 +473,191 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
|||||||
return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
|
return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content;
|
const onKeyDown = (ev: KeyboardEvent, state: IState) => {
|
||||||
if (roomsMap) {
|
if (ev.key === Key.ARROW_DOWN && ev.currentTarget.classList.contains("mx_SpaceRoomDirectory_search")) {
|
||||||
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
state.refs[0]?.current?.focus();
|
||||||
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
|
||||||
|
|
||||||
let countsStr;
|
|
||||||
if (numSpaces > 1) {
|
|
||||||
countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces });
|
|
||||||
} else if (numSpaces > 0) {
|
|
||||||
countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces });
|
|
||||||
} else {
|
|
||||||
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
let manageButtons;
|
|
||||||
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
|
||||||
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
|
||||||
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
|
||||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
|
||||||
});
|
|
||||||
|
|
||||||
const disabled = !selectedRelations.length || removing || saving;
|
|
||||||
|
|
||||||
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
|
||||||
let props = {};
|
|
||||||
if (!selectedRelations.length) {
|
|
||||||
Button = AccessibleTooltipButton;
|
|
||||||
props = {
|
|
||||||
tooltip: _t("Select a room below first"),
|
|
||||||
yOffset: -40,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
manageButtons = <>
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
onClick={async () => {
|
|
||||||
setRemoving(true);
|
|
||||||
try {
|
|
||||||
for (const [parentId, childId] of selectedRelations) {
|
|
||||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
|
||||||
parentChildMap.get(parentId).delete(childId);
|
|
||||||
if (parentChildMap.get(parentId).size > 0) {
|
|
||||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
|
||||||
} else {
|
|
||||||
parentChildMap.delete(parentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setError(_t("Failed to remove some rooms. Try again later"));
|
|
||||||
}
|
|
||||||
setRemoving(false);
|
|
||||||
}}
|
|
||||||
kind="danger_outline"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{ removing ? _t("Removing...") : _t("Remove") }
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
onClick={async () => {
|
|
||||||
setSaving(true);
|
|
||||||
try {
|
|
||||||
for (const [parentId, childId] of selectedRelations) {
|
|
||||||
const suggested = !selectionAllSuggested;
|
|
||||||
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
|
|
||||||
if (!existingContent || existingContent.suggested === suggested) continue;
|
|
||||||
|
|
||||||
const content = {
|
|
||||||
...existingContent,
|
|
||||||
suggested: !selectionAllSuggested,
|
|
||||||
};
|
|
||||||
|
|
||||||
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
|
|
||||||
|
|
||||||
parentChildMap.get(parentId).get(childId).content = content;
|
|
||||||
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setError("Failed to update some suggestions. Try again later");
|
|
||||||
}
|
|
||||||
setSaving(false);
|
|
||||||
setSelected(new Map());
|
|
||||||
}}
|
|
||||||
kind="primary_outline"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{ saving
|
|
||||||
? _t("Saving...")
|
|
||||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let results;
|
|
||||||
if (roomsMap.size) {
|
|
||||||
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
|
||||||
|
|
||||||
results = <>
|
|
||||||
<HierarchyLevel
|
|
||||||
spaceId={space.roomId}
|
|
||||||
rooms={roomsMap}
|
|
||||||
relations={parentChildMap}
|
|
||||||
parents={new Set()}
|
|
||||||
selectedMap={selected}
|
|
||||||
onToggleClick={hasPermissions ? (parentId, childId) => {
|
|
||||||
setError("");
|
|
||||||
if (!selected.has(parentId)) {
|
|
||||||
setSelected(new Map(selected.set(parentId, new Set([childId]))));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentSet = selected.get(parentId);
|
|
||||||
if (!parentSet.has(childId)) {
|
|
||||||
setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId]))));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
parentSet.delete(childId);
|
|
||||||
setSelected(new Map(selected.set(parentId, new Set(parentSet))));
|
|
||||||
} : undefined}
|
|
||||||
onViewRoomClick={(roomId, autoJoin) => {
|
|
||||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{ children && <hr /> }
|
|
||||||
</>;
|
|
||||||
} else {
|
|
||||||
results = <div className="mx_SpaceRoomDirectory_noResults">
|
|
||||||
<h3>{ _t("No results found") }</h3>
|
|
||||||
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
content = <>
|
|
||||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
|
||||||
{ countsStr }
|
|
||||||
<span>
|
|
||||||
{ additionalButtons }
|
|
||||||
{ manageButtons }
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
|
||||||
{ error }
|
|
||||||
</div> }
|
|
||||||
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
|
|
||||||
{ results }
|
|
||||||
{ children }
|
|
||||||
</AutoHideScrollbar>
|
|
||||||
</>;
|
|
||||||
} else {
|
|
||||||
content = <Spinner />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO loading state/error state
|
// TODO loading state/error state
|
||||||
return <>
|
return <RovingTabIndexProvider onKeyDown={onKeyDown} handleHomeEnd handleUpDown>
|
||||||
<SearchBox
|
{ ({ onKeyDownHandler }) => {
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
let content;
|
||||||
placeholder={_t("Search names and descriptions")}
|
if (roomsMap) {
|
||||||
onSearch={setQuery}
|
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
||||||
autoFocus={true}
|
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
||||||
initialValue={initialText}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{ content }
|
let countsStr;
|
||||||
</>;
|
if (numSpaces > 1) {
|
||||||
|
countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces });
|
||||||
|
} else if (numSpaces > 0) {
|
||||||
|
countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces });
|
||||||
|
} else {
|
||||||
|
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||||
|
}
|
||||||
|
|
||||||
|
let manageButtons;
|
||||||
|
if (space.getMyMembership() === "join" &&
|
||||||
|
space.currentState.maySendStateEvent(EventType.SpaceChild, userId)
|
||||||
|
) {
|
||||||
|
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||||
|
return [
|
||||||
|
...selected.get(parentId).values(),
|
||||||
|
].map(childId => [parentId, childId]) as [string, string][];
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||||
|
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabled = !selectedRelations.length || removing || saving;
|
||||||
|
|
||||||
|
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
||||||
|
let props = {};
|
||||||
|
if (!selectedRelations.length) {
|
||||||
|
Button = AccessibleTooltipButton;
|
||||||
|
props = {
|
||||||
|
tooltip: _t("Select a room below first"),
|
||||||
|
yOffset: -40,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
manageButtons = <>
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
onClick={async () => {
|
||||||
|
setRemoving(true);
|
||||||
|
try {
|
||||||
|
for (const [parentId, childId] of selectedRelations) {
|
||||||
|
await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId);
|
||||||
|
parentChildMap.get(parentId).delete(childId);
|
||||||
|
if (parentChildMap.get(parentId).size > 0) {
|
||||||
|
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||||
|
} else {
|
||||||
|
parentChildMap.delete(parentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(_t("Failed to remove some rooms. Try again later"));
|
||||||
|
}
|
||||||
|
setRemoving(false);
|
||||||
|
}}
|
||||||
|
kind="danger_outline"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{ removing ? _t("Removing...") : _t("Remove") }
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
onClick={async () => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
for (const [parentId, childId] of selectedRelations) {
|
||||||
|
const suggested = !selectionAllSuggested;
|
||||||
|
const existingContent = parentChildMap.get(parentId)?.get(childId)?.content;
|
||||||
|
if (!existingContent || existingContent.suggested === suggested) continue;
|
||||||
|
|
||||||
|
const content = {
|
||||||
|
...existingContent,
|
||||||
|
suggested: !selectionAllSuggested,
|
||||||
|
};
|
||||||
|
|
||||||
|
await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId);
|
||||||
|
|
||||||
|
parentChildMap.get(parentId).get(childId).content = content;
|
||||||
|
parentChildMap.set(parentId, new Map(parentChildMap.get(parentId)));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError("Failed to update some suggestions. Try again later");
|
||||||
|
}
|
||||||
|
setSaving(false);
|
||||||
|
setSelected(new Map());
|
||||||
|
}}
|
||||||
|
kind="primary_outline"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{ saving
|
||||||
|
? _t("Saving...")
|
||||||
|
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let results;
|
||||||
|
if (roomsMap.size) {
|
||||||
|
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||||
|
|
||||||
|
results = <>
|
||||||
|
<HierarchyLevel
|
||||||
|
spaceId={space.roomId}
|
||||||
|
rooms={roomsMap}
|
||||||
|
relations={parentChildMap}
|
||||||
|
parents={new Set()}
|
||||||
|
selectedMap={selected}
|
||||||
|
onToggleClick={hasPermissions ? (parentId, childId) => {
|
||||||
|
setError("");
|
||||||
|
if (!selected.has(parentId)) {
|
||||||
|
setSelected(new Map(selected.set(parentId, new Set([childId]))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentSet = selected.get(parentId);
|
||||||
|
if (!parentSet.has(childId)) {
|
||||||
|
setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId]))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parentSet.delete(childId);
|
||||||
|
setSelected(new Map(selected.set(parentId, new Set(parentSet))));
|
||||||
|
} : undefined}
|
||||||
|
onViewRoomClick={(roomId, autoJoin) => {
|
||||||
|
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{ children && <hr /> }
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
results = <div className="mx_SpaceRoomDirectory_noResults">
|
||||||
|
<h3>{ _t("No results found") }</h3>
|
||||||
|
<div>{ _t("You may want to try a different search or check for typos.") }</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = <>
|
||||||
|
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||||
|
{ countsStr }
|
||||||
|
<span>
|
||||||
|
{ additionalButtons }
|
||||||
|
{ manageButtons }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||||
|
{ error }
|
||||||
|
</div> }
|
||||||
|
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list" onKeyDown={onKeyDownHandler} role="tree">
|
||||||
|
{ results }
|
||||||
|
{ children }
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
content = <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<SearchBox
|
||||||
|
className="mx_SpaceRoomDirectory_search mx_textinput_icon mx_textinput_search"
|
||||||
|
placeholder={_t("Search names and descriptions")}
|
||||||
|
onSearch={setQuery}
|
||||||
|
autoFocus={true}
|
||||||
|
initialValue={initialText}
|
||||||
|
onKeyDown={onKeyDownHandler}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ content }
|
||||||
|
</>;
|
||||||
|
} }
|
||||||
|
</RovingTabIndexProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -272,6 +272,7 @@ const SpacePanel = () => {
|
|||||||
<ul
|
<ul
|
||||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
|
role="tree"
|
||||||
>
|
>
|
||||||
<Droppable droppableId="top-level-spaces">
|
<Droppable droppableId="top-level-spaces">
|
||||||
{ (provided, snapshot) => (
|
{ (provided, snapshot) => (
|
||||||
|
@ -328,7 +328,7 @@ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
|
|||||||
isNested,
|
isNested,
|
||||||
parents,
|
parents,
|
||||||
}) => {
|
}) => {
|
||||||
return <ul className="mx_SpaceTreeLevel">
|
return <ul className="mx_SpaceTreeLevel" role="group">
|
||||||
{ spaces.map(s => {
|
{ spaces.map(s => {
|
||||||
return (<SpaceItem
|
return (<SpaceItem
|
||||||
key={s.roomId}
|
key={s.roomId}
|
||||||
|
Loading…
Reference in New Issue
Block a user