) => {
@@ -70,6 +70,7 @@ describe("Board Header", () => {
showNotesOfOtherUsers: true,
allowStacking: true,
showNoteReactions: true,
+ isLocked: false,
},
},
})
diff --git a/src/components/BoardReaction/BoardReaction.tsx b/src/components/BoardReaction/BoardReaction.tsx
index 8526009540..9f5697e1ff 100644
--- a/src/components/BoardReaction/BoardReaction.tsx
+++ b/src/components/BoardReaction/BoardReaction.tsx
@@ -1,4 +1,4 @@
-import {BOARD_REACTION_EMOJI_MAP, BoardReactionType} from "types/boardReaction";
+import {BOARD_REACTION_EMOJI_MAP, BoardReactionType} from "store/features/boardReactions/types";
import {memo, useEffect, useRef, useState} from "react";
import {getRandomNumberInRange} from "utils/random";
import {useAppSelector} from "store";
@@ -18,8 +18,8 @@ export const BoardReaction = memo((props: BoardReactionProps) => {
const [displayOffset, setDisplayOffset] = useState(-100);
const {t} = useTranslation();
const emoji = BOARD_REACTION_EMOJI_MAP.get(props.reaction.reactionType);
- const me = useAppSelector((state) => state.participants!.self);
- const others = useAppSelector((state) => state.participants!.others);
+ const me = useAppSelector((state) => state.participants!.self)!;
+ const others = useAppSelector((state) => state.participants!.others) ?? [];
const skinTone = useAppSelector((state) => state.skinTone);
const all = [me, ...others];
const reactionUser = all.find((p) => p.user.id === props.reaction.user)!;
diff --git a/src/components/BoardReactionMenu/BoardReactionMenu.tsx b/src/components/BoardReactionMenu/BoardReactionMenu.tsx
index 9d41c7c98c..bd7ea98278 100644
--- a/src/components/BoardReactionMenu/BoardReactionMenu.tsx
+++ b/src/components/BoardReactionMenu/BoardReactionMenu.tsx
@@ -1,16 +1,15 @@
import {ForwardedRef, forwardRef, MouseEvent} from "react";
import {Close} from "components/Icon";
-import {ReactionType} from "types/reaction";
-import {BOARD_REACTION_EMOJI_MAP} from "types/boardReaction";
-import {Actions} from "store/action";
-import {useDispatch} from "react-redux";
+import {ReactionType} from "store/features/reactions/types";
+import {BOARD_REACTION_EMOJI_MAP} from "store/features/boardReactions/types";
import {useHotkeys} from "react-hotkeys-hook";
-import {useAppSelector} from "store";
+import {useAppDispatch, useAppSelector} from "store";
import {Toast} from "utils/Toast";
import {useTranslation} from "react-i18next";
import {useDelayedReset} from "utils/hooks/useDelayedReset";
import {animated, useTransition} from "@react-spring/web";
import "./BoardReactionMenu.scss";
+import {addBoardReaction, setShowBoardReactions} from "store/features";
type BoardReactionMenuProps = {
showMenu: boolean;
@@ -20,7 +19,7 @@ type BoardReactionMenuProps = {
const REACTION_DEBOUNCE_TIME = 300; // milliseconds
export const BoardReactionMenu = forwardRef((props: BoardReactionMenuProps, ref: ForwardedRef) => {
- const dispatch = useDispatch();
+ const dispatch = useAppDispatch();
const {t} = useTranslation();
const boardReactions = [...BOARD_REACTION_EMOJI_MAP];
@@ -37,12 +36,12 @@ export const BoardReactionMenu = forwardRef((props: BoardReactionMenuProps, ref:
Toast.info({
title: t("Toast.boardReactionsDisabled"),
buttons: [t("Toast.enable")],
- firstButtonOnClick: () => dispatch(Actions.setShowBoardReactions(true)),
+ firstButtonOnClick: () => dispatch(setShowBoardReactions(true)),
});
} else if (debounce) {
// not ready yet
} else {
- dispatch(Actions.addBoardReaction(reaction));
+ dispatch(addBoardReaction(reaction));
resetDebounce();
}
};
diff --git a/src/components/BoardUsers/BoardUsers.tsx b/src/components/BoardUsers/BoardUsers.tsx
index fef1fa4df1..93a71d4a31 100644
--- a/src/components/BoardUsers/BoardUsers.tsx
+++ b/src/components/BoardUsers/BoardUsers.tsx
@@ -41,7 +41,7 @@ export const BoardUsers = () => {
const {me, them} = useAppSelector(
(state) => ({
- them: state.participants!.others.filter((participant) => participant.connected),
+ them: state.participants!.others!.filter((participant) => participant.connected),
me: state.participants!.self,
}),
isEqual
diff --git a/src/components/BoardUsers/UserAvatar.tsx b/src/components/BoardUsers/UserAvatar.tsx
index 6fadff3600..0f12614290 100644
--- a/src/components/BoardUsers/UserAvatar.tsx
+++ b/src/components/BoardUsers/UserAvatar.tsx
@@ -1,7 +1,8 @@
import {useEffect, useState, useRef} from "react";
import classNames from "classnames";
import {CheckDone, RaiseHand} from "components/Icon";
-import {AvataaarProps, Avatar} from "../Avatar";
+import {AvataaarProps} from "types/avatar";
+import {Avatar} from "../Avatar";
import {Badge} from "../Badge";
import "./UserAvatar.scss";
diff --git a/src/components/BoardUsers/__tests__/BoardUsers.test.tsx b/src/components/BoardUsers/__tests__/BoardUsers.test.tsx
index 2aae495b87..e1542e5591 100644
--- a/src/components/BoardUsers/__tests__/BoardUsers.test.tsx
+++ b/src/components/BoardUsers/__tests__/BoardUsers.test.tsx
@@ -1,7 +1,7 @@
import {render} from "testUtils";
import {Provider} from "react-redux";
import {BoardUsers} from "components/BoardUsers";
-import {ApplicationState} from "types";
+import {ApplicationState} from "store";
import getTestStore from "utils/test/getTestStore";
import getTestParticipant from "utils/test/getTestParticipant";
@@ -20,12 +20,11 @@ describe("users", () => {
participants: {
self: getTestParticipant(),
others: [
- getTestParticipant({user: {id: "other-1", name: "other-1"}, connected: false}),
- getTestParticipant({user: {id: "other-2", name: "other-2"}}),
- getTestParticipant({user: {id: "other-3", name: "other-3"}}),
- getTestParticipant({user: {id: "other-4", name: "other-4"}}),
+ getTestParticipant({user: {id: "other-1", name: "other-1", isAnonymous: true}, connected: false}),
+ getTestParticipant({user: {id: "other-2", name: "other-2", isAnonymous: true}}),
+ getTestParticipant({user: {id: "other-3", name: "other-3", isAnonymous: true}}),
+ getTestParticipant({user: {id: "other-4", name: "other-4", isAnonymous: true}}),
],
- focusInitiator: null,
},
})
);
@@ -37,12 +36,11 @@ describe("users", () => {
participants: {
self: getTestParticipant(),
others: [
- getTestParticipant({user: {id: "other-1", name: "other-1"}}),
- getTestParticipant({user: {id: "other-2", name: "other-2"}}),
- getTestParticipant({user: {id: "other-3", name: "other-3"}}),
- getTestParticipant({user: {id: "other-4", name: "other-4"}}),
+ getTestParticipant({user: {id: "other-1", name: "other-1", isAnonymous: true}}),
+ getTestParticipant({user: {id: "other-2", name: "other-2", isAnonymous: true}}),
+ getTestParticipant({user: {id: "other-3", name: "other-3", isAnonymous: true}}),
+ getTestParticipant({user: {id: "other-4", name: "other-4", isAnonymous: true}}),
],
- focusInitiator: null,
},
})
);
@@ -54,13 +52,12 @@ describe("users", () => {
participants: {
self: getTestParticipant(),
others: [
- getTestParticipant({user: {id: "other-1", name: "other-1"}}),
- getTestParticipant({user: {id: "other-2", name: "other-2"}}),
- getTestParticipant({user: {id: "other-3", name: "other-3"}}),
- getTestParticipant({user: {id: "other-4", name: "other-4"}}),
- getTestParticipant({user: {id: "other-5", name: "other-5"}}),
+ getTestParticipant({user: {id: "other-1", name: "other-1", isAnonymous: true}}),
+ getTestParticipant({user: {id: "other-2", name: "other-2", isAnonymous: true}}),
+ getTestParticipant({user: {id: "other-3", name: "other-3", isAnonymous: true}}),
+ getTestParticipant({user: {id: "other-4", name: "other-4", isAnonymous: true}}),
+ getTestParticipant({user: {id: "other-5", name: "other-5", isAnonymous: true}}),
],
- focusInitiator: null,
},
})
);
diff --git a/src/components/ColorPicker/ColorPicker.scss b/src/components/ColorPicker/ColorPicker.scss
new file mode 100644
index 0000000000..06663d622d
--- /dev/null
+++ b/src/components/ColorPicker/ColorPicker.scss
@@ -0,0 +1,113 @@
+@import "constants/style";
+
+.color-picker {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding-bottom: $spacing--xxs;
+ box-shadow: 0 0 20px rgba($navy--400, 0.12);
+ background-color: $gray--300;
+ list-style: none;
+ padding-left: 0;
+ margin-top: auto;
+ border-radius: $rounded--full;
+
+ &__item {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &-button {
+ cursor: pointer;
+ border: none;
+ background: none;
+ padding: 0;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:focus-within {
+ > .color-picker__color-option {
+ border: 3px solid var(--accent-color--100);
+ }
+ }
+
+ &:focus-visible {
+ border-radius: $rounded--full;
+ outline: none;
+ }
+ }
+ }
+}
+
+[theme="dark"] {
+ .color-picker {
+ background-color: $navy--600;
+
+ &__item {
+ &__button {
+ &:focus-visible {
+ border-radius: $rounded--full;
+ outline: none;
+ }
+ }
+ }
+ }
+}
+
+.color-picker__color-option {
+ width: 24px;
+ height: 24px;
+ border-radius: $rounded--full;
+ background-color: var(--accent-color--400);
+ border: 3px solid $gray--300;
+
+ &--selected {
+ background-color: var(--accent-color--400);
+ border: 3px solid var(--accent-color--200);
+ }
+
+ &:hover,
+ &:focus-visible {
+ background-color: var(--accent-color--400);
+ border: 3px solid var(--accent-color--100);
+ }
+}
+
+[theme="dark"] {
+ .color-picker__color-option {
+ border: 3px solid $navy--600;
+
+ &--selected {
+ border: 3px solid var(--accent-color--200);
+ }
+
+ &:hover,
+ &:focus-visible {
+ border: 3px solid var(--accent-color--100);
+ }
+ }
+}
+
+.fix-focus-lock-placement {
+ margin-top: auto;
+}
+
+@media #{$smartphone} {
+ .color-picker {
+ flex-direction: row-reverse;
+ position: absolute;
+ right: 0;
+ margin: auto;
+ justify-self: center;
+ padding-right: $spacing--xxs;
+ }
+
+ .fix-focus-lock-placement {
+ margin-top: initial;
+ margin-bottom: auto;
+ }
+}
diff --git a/src/components/ColorPicker/ColorPicker.tsx b/src/components/ColorPicker/ColorPicker.tsx
new file mode 100644
index 0000000000..8d7538a8fd
--- /dev/null
+++ b/src/components/ColorPicker/ColorPicker.tsx
@@ -0,0 +1,70 @@
+import {useEffect} from "react";
+import {uniqueId} from "underscore";
+import ReactFocusLock from "react-focus-lock";
+import {Color, getColorClassName, formatColorName} from "constants/colors";
+import {Tooltip} from "components/Tooltip";
+import "./ColorPicker.scss";
+
+type ColorPickerProps = {
+ open: boolean;
+ colors: Color[];
+ activeColor: Color;
+ selectColor: (color: Color) => void;
+ closeColorPicker: () => void;
+};
+
+export const ColorPicker = (props: ColorPickerProps) => {
+ const colorsWithoutSelectedColor = props.colors.filter((curColor) => curColor !== props.activeColor);
+ const primColorAnchor = uniqueId(`color-picker-${props.activeColor.toString()}`);
+
+ useEffect(() => {
+ const handleKeyPress = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ e.stopPropagation();
+ props.closeColorPicker();
+ }
+ };
+
+ document.addEventListener("keydown", handleKeyPress, true); // trigger in capture phase
+ return () => document.removeEventListener("keydown", handleKeyPress, true);
+ }, [props]);
+
+ if (!props.open) {
+ return ;
+ }
+
+ return (
+
+
+ -
+
+
+
+ {colorsWithoutSelectedColor.map((color) => {
+ const anchor = uniqueId(`color-picker-${color.toString()}`);
+ return (
+ -
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/components/Column/Column.scss b/src/components/Column/Column.scss
index 45573ee433..007e9c08ee 100644
--- a/src/components/Column/Column.scss
+++ b/src/components/Column/Column.scss
@@ -52,6 +52,8 @@
display: flex;
flex-direction: column;
padding: 0 $spacing--xl;
+
+ z-index: $column-header-z-index;
}
.column__header-title {
diff --git a/src/components/Column/Column.tsx b/src/components/Column/Column.tsx
index 025bb5db04..f9ec55ef00 100644
--- a/src/components/Column/Column.tsx
+++ b/src/components/Column/Column.tsx
@@ -1,22 +1,22 @@
-import "./Column.scss";
-import {Color, getColorClassName} from "constants/colors";
-import {NoteInput} from "components/NoteInput";
+import {useTranslation} from "react-i18next";
+import _ from "underscore";
import {useEffect, useRef, useState} from "react";
import classNames from "classnames";
import {Tooltip} from "react-tooltip";
-import {useAppSelector} from "store";
-import {Actions} from "store/action";
-import {Close, MarkAsDone, Hidden, ThreeDots} from "components/Icon";
-import _ from "underscore";
-import {useDispatch} from "react-redux";
-import {useTranslation} from "react-i18next";
+import {useAppDispatch, useAppSelector} from "store";
+import {createColumn, deleteColumnOptimistically, editColumn, editColumnOptimistically} from "store/features";
+import {Color, getColorClassName} from "constants/colors";
import {hotkeyMap} from "constants/hotkeys";
+import {NoteInput} from "components/NoteInput";
import {Droppable} from "components/DragAndDrop/Droppable";
-import {useStripeOffset} from "utils/hooks/useStripeOffset";
import {EmojiSuggestions} from "components/EmojiSuggestions";
+import {ColumnSettings} from "components/Column/ColumnSettings";
+import {Close, MarkAsDone, Hidden, ThreeDots} from "components/Icon";
+import {Note} from "components/Note";
import {useEmojiAutocomplete} from "utils/hooks/useEmojiAutocomplete";
-import {Note} from "../Note";
-import {ColumnSettings} from "./ColumnSettings";
+import {useStripeOffset} from "utils/hooks/useStripeOffset";
+import {useTextOverflow} from "utils/hooks/useTextOverflow";
+import "./Column.scss";
const {SELECT_NOTE_INPUT_FIRST_KEY} = hotkeyMap;
@@ -30,7 +30,9 @@ export interface ColumnProps {
export const Column = ({id, name, color, visible, index}: ColumnProps) => {
const {t} = useTranslation();
- const dispatch = useDispatch();
+ const dispatch = useAppDispatch();
+
+ const {isTextTruncated, textRef} = useTextOverflow(name);
const notes = useAppSelector(
(state) =>
@@ -46,7 +48,7 @@ export const Column = ({id, name, color, visible, index}: ColumnProps) => {
const viewer = useAppSelector((state) => state.participants!.self);
const colorClassName = getColorClassName(color);
- const isModerator = viewer.role === "OWNER" || viewer.role === "MODERATOR";
+ const isModerator = viewer?.role === "OWNER" || viewer?.role === "MODERATOR";
const {value: columnName, ...emoji} = useEmojiAutocomplete({maxInputLength: 32, initialValue: name});
const [columnNameMode, setColumnNameMode] = useState<"VIEW" | "EDIT">("VIEW");
@@ -58,7 +60,17 @@ export const Column = ({id, name, color, visible, index}: ColumnProps) => {
const closeButtonRef = useRef(null);
const toggleVisibilityHandler = () => {
- dispatch(Actions.editColumn(id, {name, color, index, visible: !visible}));
+ dispatch(
+ editColumn({
+ id,
+ column: {
+ name,
+ color,
+ index,
+ visible: !visible,
+ },
+ })
+ );
};
const [localNotes, setLocalNotes] = useState(notes);
@@ -80,14 +92,34 @@ export const Column = ({id, name, color, visible, index}: ColumnProps) => {
const handleEditColumnName = (newName: string) => {
if (isTemporary) {
if (!newName) {
- dispatch(Actions.deleteColumnOptimistically(id));
+ dispatch(deleteColumnOptimistically(id));
} else {
- dispatch(Actions.editColumnOptimistically(id, {name: newName, color, visible, index})); // Prevents flicker when submitting a new column
- dispatch(Actions.createColumn({name: newName, color, visible, index}));
+ dispatch(
+ editColumnOptimistically({
+ id,
+ column: {
+ name: newName,
+ color,
+ visible,
+ index,
+ },
+ })
+ ); // Prevents flicker when submitting a new column
+ dispatch(createColumn({name: newName, color, visible, index}));
setIsTemporary(false);
}
} else {
- dispatch(Actions.editColumn(id, {name: newName, color, visible, index}));
+ dispatch(
+ editColumn({
+ id,
+ column: {
+ name: newName,
+ color,
+ visible,
+ index,
+ },
+ })
+ );
}
setColumnNameMode("VIEW");
};
@@ -97,6 +129,8 @@ export const Column = ({id, name, color, visible, index}: ColumnProps) => {
{!visible && }
{
if (isModerator) {
@@ -107,14 +141,17 @@ export const Column = ({id, name, color, visible, index}: ColumnProps) => {
>
{name}
-
- {name}
-
+ {isTextTruncated && (
+
+ {name}
+
+ )}
) : (
<>
{
@@ -123,7 +160,7 @@ export const Column = ({id, name, color, visible, index}: ColumnProps) => {
if (e.key === "Escape") {
if (isTemporary) {
- dispatch(Actions.deleteColumnOptimistically(id));
+ dispatch(deleteColumnOptimistically(id));
}
setColumnNameMode("VIEW");
} else if (e.key === "Enter") {
@@ -164,7 +201,7 @@ export const Column = ({id, name, color, visible, index}: ColumnProps) => {
ref={closeButtonRef}
onClick={() => {
if (isTemporary) {
- dispatch(Actions.deleteColumnOptimistically(id));
+ dispatch(deleteColumnOptimistically(id));
}
setColumnNameMode("VIEW");
}}
@@ -173,9 +210,9 @@ export const Column = ({id, name, color, visible, index}: ColumnProps) => {