diff --git a/entrypoints/sidepanel/components/CollectionView.styles.ts b/entrypoints/sidepanel/components/CollectionView.styles.ts index c214b74..a684b11 100644 --- a/entrypoints/sidepanel/components/CollectionView.styles.ts +++ b/entrypoints/sidepanel/components/CollectionView.styles.ts @@ -19,6 +19,11 @@ export const useStyles_CollectionView = makeStyles({ "&:hover": { boxShadow: tokens.shadow4 + }, + + "&:not(:focus-within) .compact": + { + display: "none" } }, color: diff --git a/entrypoints/sidepanel/components/CollectionView.tsx b/entrypoints/sidepanel/components/CollectionView.tsx index 40a5b69..d7ee416 100644 --- a/entrypoints/sidepanel/components/CollectionView.tsx +++ b/entrypoints/sidepanel/components/CollectionView.tsx @@ -12,7 +12,12 @@ import { useStyles_CollectionView } from "./CollectionView.styles"; import GroupView from "./GroupView"; import TabView from "./TabView"; -export default function CollectionView({ collection, index: collectionIndex, dragOverlay }: CollectionViewProps): ReactElement +export default function CollectionView({ + collection, + index: collectionIndex, + dragOverlay, + compact +}: CollectionViewProps): ReactElement { const { tilesView } = useCollections(); const { @@ -53,12 +58,12 @@ export default function CollectionView({ collection, index: collectionIndex, dra { (!activeItem || activeItem.item.type !== "collection") && !dragOverlay && <> { collection.items.length < 1 ? -
+
{ i18n.t("collections.empty") }
: -
+
[collectionIndex, index].join("/")) } strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy } @@ -66,9 +71,12 @@ export default function CollectionView({ collection, index: collectionIndex, dra { collection.items.map((i, index) => i.type === "group" ? + key={ index } group={ i } indices={ [collectionIndex, index] } + collectionId={ collection.timestamp } /> : - + ) }
@@ -85,4 +93,5 @@ export type CollectionViewProps = collection: CollectionItem; index: number; dragOverlay?: boolean; + compact?: boolean | null; }; diff --git a/entrypoints/sidepanel/components/EditDialog.tsx b/entrypoints/sidepanel/components/EditDialog.tsx index cc3c80a..1eb0594 100644 --- a/entrypoints/sidepanel/components/EditDialog.tsx +++ b/entrypoints/sidepanel/components/EditDialog.tsx @@ -87,7 +87,7 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement value={ color === "pinned" ? i18n.t("groups.pinned") : title } onChange={ (_, e) => setTitle(e.value) } /> - +
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) && { group.items.map((i, index) => - + ) }
@@ -117,4 +119,5 @@ export type GroupViewProps = group: GroupItem; indices: number[]; dragOverlay?: boolean; + collectionId: number; }; diff --git a/entrypoints/sidepanel/components/TabEditDialog.tsx b/entrypoints/sidepanel/components/TabEditDialog.tsx new file mode 100644 index 0000000..9a32192 --- /dev/null +++ b/entrypoints/sidepanel/components/TabEditDialog.tsx @@ -0,0 +1,69 @@ +import { track } from "@/features/analytics"; +import { TabItem } from "@/models/CollectionModels"; +import { Button, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, DialogTrigger, Field, Input, makeStyles, tokens } from "@fluentui/react-components"; + +export default function TabEditDialog({ tab, onSave }: TabEditDialogProps): React.ReactElement +{ + const cls = useStyles(); + + const [title, setTitle] = useState(tab.title ?? ""); + const [url, setUrl] = useState(tab.url); + const isValid = useMemo(() => url.trim().length > 0, [url]); + + const onSubmit = (e: React.FormEvent) => + { + e.preventDefault(); + track("item_edited", { type: "tab" }); + onSave({ + ...tab, + title: title.trim().length > 0 ? title : undefined, + url: url.trim() + }); + }; + + return ( + +
+ + { i18n.t("dialogs.edit.title.edit_tab") } + + setTitle(e.value) } + placeholder={ i18n.t("dialogs.edit.collection_title") } /> + + setUrl(e.value) } + placeholder="URL" /> + + + + + + + + + + + + +
+
+ ); +} + +const useStyles = makeStyles({ + content: + { + display: "flex", + flexFlow: "column", + gap: tokens.spacingVerticalMNudge + } +}); + +export type TabEditDialogProps = +{ + tab: TabItem; + onSave: (updatedTab: TabItem) => void; +}; diff --git a/entrypoints/sidepanel/components/TabMoreButton.tsx b/entrypoints/sidepanel/components/TabMoreButton.tsx new file mode 100644 index 0000000..decd49f --- /dev/null +++ b/entrypoints/sidepanel/components/TabMoreButton.tsx @@ -0,0 +1,42 @@ +import { useDangerStyles } from "@/hooks/useDangerStyles"; +import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components"; +import { bundleIcon, Delete20Filled, Delete20Regular, Edit20Filled, Edit20Regular, MoreHorizontal20Regular } from "@fluentui/react-icons"; +import { ButtonHTMLAttributes } from "react"; + +export default function TabMoreButton({ onEdit, onDelete, ...props }: TabMoreButtonProps): React.ReactElement +{ + const EditIcon = bundleIcon(Edit20Filled, Edit20Regular); + const DeleteIcon = bundleIcon(Delete20Filled, Delete20Regular); + const dangerCls = useDangerStyles(); + + return ( + + + + + ); +} + +export type TabMoreButtonProps = + ButtonHTMLAttributes & + { + onDelete?: () => void; + onEdit?: () => void; + }; diff --git a/entrypoints/sidepanel/components/TabView.tsx b/entrypoints/sidepanel/components/TabView.tsx index 9937b63..15b87b0 100644 --- a/entrypoints/sidepanel/components/TabView.tsx +++ b/entrypoints/sidepanel/components/TabView.tsx @@ -4,16 +4,17 @@ import { useDialog } from "@/contexts/DialogProvider"; import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider"; import useDndItem from "@/entrypoints/sidepanel/hooks/useDndItem"; import useSettings from "@/hooks/useSettings"; -import { TabItem } from "@/models/CollectionModels"; -import { Button, Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-components"; -import { Dismiss20Regular } from "@fluentui/react-icons"; +import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels"; +import { Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-components"; import { MouseEventHandler, ReactElement } from "react"; import { useStyles_TabView } from "./TabView.styles"; import CollectionContext, { CollectionContextType } from "../contexts/CollectionContext"; +import TabMoreButton from "./TabMoreButton"; +import TabEditDialog from "./TabEditDialog"; -export default function TabView({ tab, indices, dragOverlay }: TabViewProps): ReactElement +export default function TabView({ tab, indices, dragOverlay, collectionId }: TabViewProps): ReactElement { - const { removeItem, graphics, tilesView } = useCollections(); + const { removeItem, graphics, tilesView, collections, updateCollection } = useCollections(); const { collection } = useContext(CollectionContext); const { setNodeRef, setActivatorNodeRef, @@ -26,11 +27,8 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re const cls = useStyles_TabView(); - const handleDelete: MouseEventHandler = (args) => + const handleDelete = (): void => { - args.preventDefault(); - args.stopPropagation(); - const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)]; if (deletePrompt) @@ -45,6 +43,26 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re removeItem(...removeIndex); }; + const handleEdit = (): void => + { + if (collectionId < 0) + return; + + const updateTab = async (updatedTab: TabItem): Promise => + { + const collection: CollectionItem = collections!.find(i => i.timestamp === collectionId)!; + + if (indices.length > 2) + (collection.items[indices[1]] as GroupItem).items[indices[2]] = updatedTab; + else + collection.items[indices[1]] = updatedTab; + + await updateCollection(collection, collection.timestamp); + }; + + dialog.pushCustom(); + }; + const handleClick: MouseEventHandler = (args) => { args.preventDefault(); @@ -91,12 +109,10 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re - -
); @@ -107,4 +123,5 @@ export type TabViewProps = tab: TabItem; indices: number[]; dragOverlay?: boolean; + collectionId: number; }; diff --git a/entrypoints/sidepanel/components/collections/CollectionHeader.tsx b/entrypoints/sidepanel/components/collections/CollectionHeader.tsx index c4a1efa..62a0a96 100644 --- a/entrypoints/sidepanel/components/collections/CollectionHeader.tsx +++ b/entrypoints/sidepanel/components/collections/CollectionHeader.tsx @@ -2,7 +2,7 @@ import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionT import useSettings from "@/hooks/useSettings"; import { TabItem } from "@/models/CollectionModels"; import { Button, Caption1, makeStyles, mergeClasses, Subtitle2, tokens, Tooltip } from "@fluentui/react-components"; -import { Add20Filled, Add20Regular, bundleIcon } from "@fluentui/react-icons"; +import { Add20Filled, Add20Regular, bundleIcon, EyeOff16Regular } from "@fluentui/react-icons"; import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext"; import { useCollections } from "../../contexts/CollectionsProvider"; import CollectionMoreButton from "./CollectionMoreButton"; @@ -23,7 +23,7 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col const handleAddSelected = async () => { - const [newTabs, skipCount] = await getTabsToSaveAsync(); + const [newTabs, skipCount] = await getTabsToSaveAsync(true); if (newTabs.length > 0) await updateCollection({ @@ -45,9 +45,12 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col content={ getCollectionTitle(collection) } positioning="above-start" > - - { getCollectionTitle(collection) } - +
+ { collection.hidden && } + + { getCollectionTitle(collection) } + +
@@ -112,5 +115,11 @@ const useStyles = makeStyles({ showToolbar: { display: "flex" + }, + titleContainer: + { + display: "flex", + alignItems: "center", + gap: tokens.spacingHorizontalS } }); diff --git a/entrypoints/sidepanel/components/collections/CollectionMoreButton.tsx b/entrypoints/sidepanel/components/collections/CollectionMoreButton.tsx index db0fc24..1dba8cc 100644 --- a/entrypoints/sidepanel/components/collections/CollectionMoreButton.tsx +++ b/entrypoints/sidepanel/components/collections/CollectionMoreButton.tsx @@ -22,6 +22,8 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co const EditIcon = ic.bundleIcon(ic.Edit20Filled, ic.Edit20Regular); const DeleteIcon = ic.bundleIcon(ic.Delete20Filled, ic.Delete20Regular); const BookmarkIcon = ic.bundleIcon(ic.BookmarkAdd20Filled, ic.BookmarkAdd20Regular); + const ShowIcon = ic.bundleIcon(ic.Eye20Filled, ic.Eye20Regular); + const HideIcon = ic.bundleIcon(ic.EyeOff20Filled, ic.EyeOff20Regular); const dangerCls = useDangerStyles(); @@ -39,6 +41,11 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co removeItem(collection.timestamp); }; + const toggleHidden = () => + { + updateCollection({ ...collection, hidden: !collection.hidden }, collection.timestamp); + }; + const handleEdit = () => dialog.pushCustom( } onClick={ handleEdit }> { i18n.t("collections.menu.edit") } + : } onClick={ toggleHidden }> + { collection.hidden ? i18n.t("collections.menu.unhide") : i18n.t("collections.menu.hide") } + } className={ dangerCls.menuItem } onClick={ handleDelete }> { i18n.t("collections.menu.delete") } diff --git a/entrypoints/sidepanel/components/collections/GroupMoreMenu.tsx b/entrypoints/sidepanel/components/collections/GroupMoreMenu.tsx index fd5fbf0..55d9755 100644 --- a/entrypoints/sidepanel/components/collections/GroupMoreMenu.tsx +++ b/entrypoints/sidepanel/components/collections/GroupMoreMenu.tsx @@ -67,7 +67,7 @@ export default function GroupMoreMenu(): ReactElement const handleAddSelected = async () => { - const [newTabs, skipCount] = await getTabsToSaveAsync(); + const [newTabs, skipCount] = await getTabsToSaveAsync(true); if (newTabs.length > 0) await updateGroup({ diff --git a/entrypoints/sidepanel/layouts/collections/CollectionListView.styles.ts b/entrypoints/sidepanel/layouts/collections/CollectionListView.styles.ts index 4faa635..5e3d55f 100644 --- a/entrypoints/sidepanel/layouts/collections/CollectionListView.styles.ts +++ b/entrypoints/sidepanel/layouts/collections/CollectionListView.styles.ts @@ -51,5 +51,9 @@ export const useStyles_CollectionListView = makeStyles({ { gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))" } + }, + compactList: + { + alignItems: "baseline" } }); diff --git a/entrypoints/sidepanel/layouts/collections/CollectionListView.tsx b/entrypoints/sidepanel/layouts/collections/CollectionListView.tsx index 7b2f9e1..d84c361 100644 --- a/entrypoints/sidepanel/layouts/collections/CollectionListView.tsx +++ b/entrypoints/sidepanel/layouts/collections/CollectionListView.tsx @@ -18,10 +18,10 @@ import CollectionContext from "../../contexts/CollectionContext"; import { useCollections } from "../../contexts/CollectionsProvider"; import applyReorder from "../../utils/dnd/applyReorder"; import { collisionDetector } from "../../utils/dnd/collisionDetector"; +import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor"; import { useStyles_CollectionListView } from "./CollectionListView.styles"; import SearchBar from "./SearchBar"; import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage"; -import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor"; export default function CollectionListView(): ReactElement { @@ -30,17 +30,19 @@ export default function CollectionListView(): ReactElement const [sortMode, setSortMode] = useSettings("sortMode"); const [query, setQuery] = useState(""); const [colors, setColors] = useState([]); + const [showHidden, setShowHidden] = useState(false); + const [compactView] = useSettings("compactView"); const [active, setActive] = useState(null); const sensors = useSensors( - useSensor(MouseSensor, { activationConstraint: { delay: 10, tolerance: 20 } }), + useSensor(MouseSensor, { activationConstraint: { delay: 150, tolerance: 20 } }), useSensor(TouchSensor, { activationConstraint: { delay: 300, tolerance: 20 } }) ); const resultList = useMemo( - () => sortCollections(filterCollections(collections, { query, colors }), sortMode), - [query, colors, sortMode, collections] + () => sortCollections(filterCollections(collections, { query, colors, showHidden }), sortMode), + [query, colors, sortMode, collections, showHidden] ); const cls = useStyles_CollectionListView(); @@ -49,6 +51,13 @@ export default function CollectionListView(): ReactElement { setQuery(""); setColors([]); + setShowHidden(false); + }, []); + + const updateFilter = useCallback((newColors: CollectionFilterType["colors"], newShowHidden: boolean) => + { + setColors(newColors); + setShowHidden(newShowHidden); }, []); const handleDragStart = (event: DragStartEvent): void => @@ -87,8 +96,9 @@ export default function CollectionListView(): ReactElement
@@ -105,7 +115,7 @@ export default function CollectionListView(): ReactElement
: -
+
{ resultList.map((collection, index) => - + ) } @@ -135,9 +145,9 @@ export default function CollectionListView(): ReactElement } } > { active.item.type === "group" ? - + : - + } : diff --git a/entrypoints/sidepanel/layouts/collections/FilterCollectionsButton.tsx b/entrypoints/sidepanel/layouts/collections/FilterCollectionsButton.tsx index 772ffcf..a33a4db 100644 --- a/entrypoints/sidepanel/layouts/collections/FilterCollectionsButton.tsx +++ b/entrypoints/sidepanel/layouts/collections/FilterCollectionsButton.tsx @@ -3,32 +3,48 @@ import * as fui from "@fluentui/react-components"; import * as ic from "@fluentui/react-icons"; import { CollectionFilterType } from "../../utils/filterCollections"; -export default function FilterCollectionsButton({ value, onChange }: FilterCollectionsButtonProps): React.ReactElement +export default function FilterCollectionsButton({ value, onChange, showHidden }: FilterCollectionsButtonProps): React.ReactElement { const cls = useStyles(); const colorCls = useGroupColors(); - const ColorFilterIcon = ic.bundleIcon(ic.Color20Filled, ic.Color20Regular); + const FilterIcon = ic.bundleIcon(ic.Filter20Filled, ic.Filter20Regular); const ColorIcon = ic.bundleIcon(ic.Circle20Filled, ic.CircleHalfFill20Regular); const NoColorIcon = ic.bundleIcon(ic.CircleOffFilled, ic.CircleOffRegular); const AnyColorIcon = ic.bundleIcon(ic.PhotoFilter20Filled, ic.PhotoFilter20Regular); + const HiddenIcon = ic.bundleIcon(ic.EyeOff20Filled, ic.EyeOff20Regular); + + const values: Record = useMemo(() => ({ + default: !value || value.length < 1 ? ["any"] : [], + colors: value || [], + hidden: showHidden ? ["show"] : [] + }), [value, showHidden]); + + const onCheckedValueChange = useCallback((_: fui.MenuCheckedValueChangeEvent, e: fui.MenuCheckedValueChangeData) => + { + if (e.name === "hidden") + onChange?.(value ?? [], e.checkedItems.includes("show")); + else + onChange?.(e.checkedItems.includes("any") ? [] : e.checkedItems as CollectionFilterType["colors"], showHidden ?? false); + }, [onChange, showHidden, value]); return ( - onChange?.(e.checkedItems.includes("any") ? [] : e.checkedItems as CollectionFilterType["colors"]) - } + checkedValues={ values } + onCheckedValueChange={ onCheckedValueChange } > - } /> + } /> + }> + { i18n.t("main.list.searchbar.show_hidden") } + }> { i18n.t("colors.any") } @@ -60,7 +76,8 @@ export default function FilterCollectionsButton({ value, onChange }: FilterColle export type FilterCollectionsButtonProps = { value?: CollectionFilterType["colors"]; - onChange?: (value: CollectionFilterType["colors"]) => void; + showHidden?: boolean; + onChange?: (value: CollectionFilterType["colors"], showHidden: boolean) => void; }; const useStyles = fui.makeStyles({ diff --git a/entrypoints/sidepanel/layouts/collections/SearchBar.tsx b/entrypoints/sidepanel/layouts/collections/SearchBar.tsx index a525e19..6b45bbe 100644 --- a/entrypoints/sidepanel/layouts/collections/SearchBar.tsx +++ b/entrypoints/sidepanel/layouts/collections/SearchBar.tsx @@ -25,7 +25,7 @@ export default function SearchBar(props: SearchBarProps): React.ReactElement