@activecollab/components
Version:
ActiveCollab Components
440 lines (429 loc) • 17.7 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import React, { useCallback, useMemo, useState } from "react";
import styled, { createGlobalStyle } from "styled-components";
import { Button } from "../../components/Button";
import { Checkbox } from "../../components/Checkbox";
import { ConfirmDialog } from "../../components/ConfirmDialog";
import { CounterButton } from "../../components/CounterButton";
import { Dialog } from "../../components/Dialog";
import { IconButton } from "../../components/IconButton";
import { AddCrossTinyIcon, CloseSmallIcon, CrownBlankIcon, CrownSelectedIcon, PeopleIcon, SortGeneralIcon } from "../../components/Icons";
import { Label } from "../../components/Label";
import { List, ListItem } from "../../components/List";
import { Menu, MenuHeader } from "../../components/Menu";
import { Select } from "../../components/Select";
import { SelectTrigger } from "../../components/SelectTrigger";
import { Textarea } from "../../components/Textarea";
import { Tooltip } from "../../components/Tooltip";
import { Body2, Caption1 } from "../../components/Typography";
import { AVATAR_COLORS, RoundAvatar } from "../shared";
/* ------------------------------------------------------------------ */
/* People menu (copy of the project header People popover) */
/* ------------------------------------------------------------------ */
/* 12 people (9 members + 3 clients) so the 330px list visibly scrolls.
Jasna (id 1) starts as the board manager. */
export const BOARD_MEMBERS = [{
id: 1,
name: "Jasna Simic",
initials: "JS",
color: AVATAR_COLORS.purple
}, {
id: 2,
name: "Marko Antic",
initials: "MA",
color: AVATAR_COLORS.teal
}, {
id: 3,
name: "Dario Kovac",
initials: "DK",
color: AVATAR_COLORS.orange
}, {
id: 4,
name: "Ivana Lukic",
initials: "IL",
color: AVATAR_COLORS.green
}, {
id: 5,
name: "Petar Novak",
initials: "PN",
color: AVATAR_COLORS.blue
}, {
id: 6,
name: "Sara Milic",
initials: "SM",
color: AVATAR_COLORS.yellow
}, {
id: 7,
name: "Luka Peric",
initials: "LP",
color: AVATAR_COLORS.purple
}, {
id: 8,
name: "Mia Radic",
initials: "MR",
color: AVATAR_COLORS.teal
}, {
id: 9,
name: "Nikola Vukic",
initials: "NV",
color: AVATAR_COLORS.orange
}, {
id: 10,
name: "Ana Klein",
initials: "AK",
color: AVATAR_COLORS.green,
isClient: true
}, {
id: 11,
name: "Tom Becker",
initials: "TB",
color: AVATAR_COLORS.blue,
isClient: true
}, {
id: 12,
name: "Eva Fischer",
initials: "EF",
color: AVATAR_COLORS.yellow,
isClient: true
}];
/* Mirrors ProjectPeopleWrapper + view.project_top_bar.less:
Menu 260px, "People" MenuHeader with an (inert) "+", 330px scroll area,
Members/Clients Caption1 section labels, rows with hover-only options. */
export const StyledPeopleMenu = styled.div.withConfig({
displayName: "peopleMenu__StyledPeopleMenu",
componentId: "sc-uidi4w-0"
})([".people-scroll{max-height:330px;overflow-y:auto;overflow-x:hidden;padding-bottom:8px;}.people-section-label{display:block;margin:8px 0;padding:0 16px;}.member-item{justify-content:space-between;margin:4px 8px;padding:0 8px;border-radius:4px;cursor:auto;}.member-options{display:none;align-items:center;}.member-item:hover .member-options{display:flex;}.member-side{display:flex;align-items:center;flex-shrink:0;}.member-user{display:inline-flex;align-items:center;gap:8px;padding:2px 0;overflow:hidden;.member-name{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}}.manager-avatar{outline:2px #ffcc6b solid;outline-offset:-2px;}"]);
/* Rows sorted by name. Manager: gold crown + ring, no hover options (the
role must be handed over before removal). Crown option only on non-client
rows; X on everyone else. */
export const PeopleMenuSection = _ref => {
let label = _ref.label,
people = _ref.people,
managerId = _ref.managerId,
onMakeManager = _ref.onMakeManager,
onRemove = _ref.onRemove;
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Caption1, {
weight: "bold",
className: "people-section-label"
}, label), /*#__PURE__*/React.createElement(List, null, [...people].sort((a, b) => a.name.localeCompare(b.name)).map(person => {
const isManager = person.id === managerId;
return /*#__PURE__*/React.createElement(ListItem, {
key: person.id,
className: "member-item"
}, /*#__PURE__*/React.createElement("span", {
className: "member-user"
}, /*#__PURE__*/React.createElement(RoundAvatar, {
$bg: person.color,
$bordered: false,
className: isManager ? "manager-avatar" : undefined
}, person.initials), /*#__PURE__*/React.createElement(Body2, {
className: "member-name"
}, person.name)), /*#__PURE__*/React.createElement("span", {
className: "member-side"
}, isManager ? /*#__PURE__*/React.createElement(CrownSelectedIcon, {
fill: "#FFCC6B"
}) : null, /*#__PURE__*/React.createElement("span", {
className: "member-options"
}, !isManager && !person.isClient ? /*#__PURE__*/React.createElement(Tooltip, {
title: "Set as Whiteboard Manager"
}, /*#__PURE__*/React.createElement(IconButton, {
size: "small",
variant: "text gray",
onClick: () => onMakeManager(person)
}, /*#__PURE__*/React.createElement(CrownBlankIcon, null))) : null, !isManager ? /*#__PURE__*/React.createElement(Tooltip, {
title: "Remove from Whiteboard"
}, /*#__PURE__*/React.createElement(IconButton, {
size: "small",
variant: "text gray",
onClick: () => onRemove(person)
}, /*#__PURE__*/React.createElement(CloseSmallIcon, null))) : null)));
})));
};
/* ------------------------------------------------------------------ */
/* Add-members dialog (people "+" → DS Dialog + invite-style picker) */
/* ------------------------------------------------------------------ */
/* Owner company first, remaining companies sorted by name; people sorted
by name within each company — same ordering as the real user picker.
Members of non-owner companies join the board as clients. */
export const INVITE_COMPANIES = [{
name: "ActiveCollab",
isOwnerCompany: true,
people: [{
id: 101,
name: "Ana Ilic",
initials: "AI",
color: AVATAR_COLORS.green
}, {
id: 102,
name: "Bojan Peric",
initials: "BP",
color: AVATAR_COLORS.blue
}, {
id: 103,
name: "Goran Simovic",
initials: "GS",
color: AVATAR_COLORS.orange
}, {
id: 104,
name: "Vera Antic",
initials: "VA",
color: AVATAR_COLORS.purple
}]
}, {
name: "Bitlab Studio",
people: [{
id: 201,
name: "Elena Weber",
initials: "EW",
color: AVATAR_COLORS.teal
}, {
id: 202,
name: "Jonas Richter",
initials: "JR",
color: AVATAR_COLORS.yellow
}]
}, {
name: "Nova Consulting",
people: [{
id: 301,
name: "Iris Novak",
initials: "IN",
color: AVATAR_COLORS.blue
}, {
id: 302,
name: "Karlo Babic",
initials: "KB",
color: AVATAR_COLORS.green
}, {
id: 303,
name: "Tea Grgic",
initials: "TG",
color: AVATAR_COLORS.purple
}]
}];
/* The Select panel and the Dialog render through portals, so they can only
be styled via real global classes — wrapper descendant selectors can't
reach them. Actions gap 12px = the app's "tw-flex tw-gap-3" on
Dialog.Actions (InviteToProjectsDialog). */
export const AddMembersDialogStyle = createGlobalStyle([".wb-user-select{width:480px;max-width:calc(100vw - 32px);}.wb-dialog-actions{display:flex;gap:12px;}"]);
export const StyledCandidateRow = styled.span.withConfig({
displayName: "peopleMenu__StyledCandidateRow",
componentId: "sc-uidi4w-1"
})(["display:inline-flex;align-items:center;gap:8px;min-width:0;flex-grow:1;.candidate-name{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}"]);
/* same offset the Select's built-in row checkbox has */
export const StyledRowCheckbox = styled(Checkbox).withConfig({
displayName: "peopleMenu__StyledRowCheckbox",
componentId: "sc-uidi4w-2"
})(["margin-left:12px;"]);
export const StyledDialogFields = styled.div.withConfig({
displayName: "peopleMenu__StyledDialogFields",
componentId: "sc-uidi4w-3"
})(["display:flex;flex-direction:column;.field{display:flex;flex-direction:column;gap:4px;}.field + .field{margin-top:16px;}button.wb-user-trigger{width:100%;}textarea.wb-invite-message{width:100%;box-sizing:border-box;}"]);
/* Closes on Esc (DS Dialog default) and Cancel; background click stays
disabled (enableBackgroundClick not set). Add is disabled until at least
one user is selected; on Add the selection joins the members list (in the
real app this also sends the invitation email with the optional message,
and Add stays disabled while the request is in flight). */
export const AddMembersDialog = _ref2 => {
let open = _ref2.open,
onClose = _ref2.onClose,
existingIds = _ref2.existingIds,
onAdd = _ref2.onAdd;
const _useState = useState([]),
selectedIds = _useState[0],
setSelectedIds = _useState[1];
const _useState2 = useState(""),
message = _useState2[0],
setMessage = _useState2[1];
/* current members drop out of the picker; companies left empty disappear */
const options = useMemo(() => INVITE_COMPANIES.map((company, index) => ({
id: 1000 + index,
name: company.name,
options: company.people.filter(person => !existingIds.includes(person.id)).map(person => _extends({}, person))
})).filter(company => company.options.length > 0), [existingIds]);
const triggerText = selectedIds.length === 0 ? "Choose users" : selectedIds.length + " " + (selectedIds.length === 1 ? "user" : "users") + " selected";
const handleAdd = useCallback(() => {
const added = INVITE_COMPANIES.flatMap(company => company.people.filter(person => selectedIds.includes(person.id)).map(person => ({
id: person.id,
name: person.name,
initials: person.initials,
color: person.color,
isClient: !company.isOwnerCompany
})));
onAdd(added);
setSelectedIds([]);
setMessage("");
onClose();
}, [onAdd, onClose, selectedIds]);
return /*#__PURE__*/React.createElement(Dialog, {
open: open,
onClose: onClose
}, /*#__PURE__*/React.createElement(AddMembersDialogStyle, null), /*#__PURE__*/React.createElement(Dialog.Title, null, "Add Users"), /*#__PURE__*/React.createElement(Dialog.ContentDivider, null), /*#__PURE__*/React.createElement(Dialog.Content, null, /*#__PURE__*/React.createElement(StyledDialogFields, null, /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement(Label, {
htmlFor: "wb-users",
required: true
}, "Users"), /*#__PURE__*/React.createElement(Select, {
type: "multiple",
options: options,
selected: selectedIds,
onChange: value => setSelectedIds(Array.isArray(value) ? value : []),
noResultText: "No results",
selectClassName: "wb-user-select",
target: /*#__PURE__*/React.createElement(SelectTrigger, {
id: "wb-users",
className: "wb-user-trigger",
typographyProps: {
color: selectedIds.length > 0 ? "primary" : "quaternary"
}
}, triggerText),
endAdornment: /*#__PURE__*/React.createElement(Tooltip, {
title: "Group by"
}, /*#__PURE__*/React.createElement(IconButton, {
variant: "text gray"
}, /*#__PURE__*/React.createElement(SortGeneralIcon, null))),
renderOption: (option, data) => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StyledCandidateRow, null, /*#__PURE__*/React.createElement(RoundAvatar, {
$bg: String(option.color),
$size: 24,
$bordered: false
}, option.initials), /*#__PURE__*/React.createElement(Body2, {
className: "candidate-name"
}, option.name)), /*#__PURE__*/React.createElement(StyledRowCheckbox, data))
})), /*#__PURE__*/React.createElement("div", {
className: "field"
}, /*#__PURE__*/React.createElement(Label, {
htmlFor: "wb-invite-message"
}, "Message to Users"), /*#__PURE__*/React.createElement(Textarea, {
id: "wb-invite-message",
className: "wb-invite-message",
placeholder: "Short note to invited users",
value: message,
onChange: event => setMessage(event.target.value)
}), /*#__PURE__*/React.createElement(Caption1, {
color: "tertiary"
}, "Let people know what this whiteboard is for and why you're inviting them. If provided, this message is included in the invitation email.")))), /*#__PURE__*/React.createElement(Dialog.ContentDivider, null), /*#__PURE__*/React.createElement(Dialog.Actions, {
className: "wb-dialog-actions"
}, /*#__PURE__*/React.createElement(Button, {
variant: "primary",
disabled: selectedIds.length === 0,
onClick: handleAdd
}, "Add"), /*#__PURE__*/React.createElement(Button, {
variant: "secondary",
onClick: onClose
}, "Cancel")));
};
/* People trigger (icon + member count) + the popover + the dialogs.
Flows mirror the implementation spec. The mock acts as a non-owner
manager, so a crown click always shows the confirm dialog (owners would
transfer immediately), and hover options stay visible after a transfer so
the flows can be replayed. */
export const WhiteboardPeopleMenu = () => {
var _removeTarget$name$sp;
const _useState3 = useState(false),
open = _useState3[0],
setOpen = _useState3[1];
const handleOpen = useCallback(() => setOpen(true), []);
const handleClose = useCallback(() => setOpen(false), []);
const _useState4 = useState(BOARD_MEMBERS),
boardMembers = _useState4[0],
setBoardMembers = _useState4[1];
const _useState5 = useState(1),
managerId = _useState5[0],
setManagerId = _useState5[1];
const _useState6 = useState(null),
managerTarget = _useState6[0],
setManagerTarget = _useState6[1];
const _useState7 = useState(null),
removeTarget = _useState7[0],
setRemoveTarget = _useState7[1];
const _useState8 = useState(false),
dialogOpen = _useState8[0],
setDialogOpen = _useState8[1];
const handleDialogOpen = useCallback(() => {
setOpen(false);
setDialogOpen(true);
}, []);
const handleDialogClose = useCallback(() => setDialogOpen(false), []);
const handleAddMembers = useCallback(added => {
setBoardMembers(current => [...current, ...added]);
}, []);
/* close the menu before opening any dialog (menus-that-open-dialogs rule) */
const handleMakeManager = useCallback(person => {
setOpen(false);
setManagerTarget(person);
}, []);
const handleRemove = useCallback(person => {
setOpen(false);
setRemoveTarget(person);
}, []);
const handleManagerConfirm = useCallback(() => {
if (managerTarget) {
setManagerId(managerTarget.id);
}
setManagerTarget(null);
}, [managerTarget]);
const handleRemoveConfirm = useCallback(() => {
if (removeTarget) {
setBoardMembers(current => current.filter(person => person.id !== removeTarget.id));
}
setRemoveTarget(null);
}, [removeTarget]);
const members = boardMembers.filter(person => !person.isClient);
const clients = boardMembers.filter(person => person.isClient);
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AddMembersDialog, {
open: dialogOpen,
onClose: handleDialogClose,
existingIds: boardMembers.map(person => person.id),
onAdd: handleAddMembers
}), /*#__PURE__*/React.createElement(ConfirmDialog, {
open: managerTarget !== null,
dialogTitle: "Change Whiteboard Manager",
dialogContent: "By changing the Whiteboard Manager, you'll lose the ability to manage this whiteboard.",
confirmBtnText: "Change",
shouldShowCancelButton: true,
onConfirm: handleManagerConfirm,
onCancel: () => setManagerTarget(null)
}), /*#__PURE__*/React.createElement(ConfirmDialog, {
open: removeTarget !== null,
dialogTitle: "Remove from Whiteboard",
dialogContent: "Remove " + ((_removeTarget$name$sp = removeTarget == null ? void 0 : removeTarget.name.split(" ")[0]) != null ? _removeTarget$name$sp : "") + " from this whiteboard? They will immediately lose access to it.",
confirmBtnText: "Remove",
shouldShowCancelButton: true,
onConfirm: handleRemoveConfirm,
onCancel: () => setRemoveTarget(null)
}), /*#__PURE__*/React.createElement(Menu, {
open: open,
onOpen: handleOpen,
onClose: handleClose,
position: "bottom-start",
target: /*#__PURE__*/React.createElement(CounterButton, {
active: open,
label: String(boardMembers.length),
icon: /*#__PURE__*/React.createElement(PeopleIcon, null),
className: open ? "open" : undefined,
onClearAll: () => undefined
})
}, /*#__PURE__*/React.createElement(StyledPeopleMenu, null, /*#__PURE__*/React.createElement(MenuHeader, {
title: "People",
rightElement: /*#__PURE__*/React.createElement(IconButton, {
size: "small",
variant: "text gray",
onClick: handleDialogOpen
}, /*#__PURE__*/React.createElement(AddCrossTinyIcon, null))
}), /*#__PURE__*/React.createElement("div", {
className: "people-scroll"
}, /*#__PURE__*/React.createElement(PeopleMenuSection, {
label: "Members",
people: members,
managerId: managerId,
onMakeManager: handleMakeManager,
onRemove: handleRemove
}), clients.length > 0 ? /*#__PURE__*/React.createElement(PeopleMenuSection, {
label: "Clients",
people: clients,
managerId: managerId,
onMakeManager: handleMakeManager,
onRemove: handleRemove
}) : null))));
};
//# sourceMappingURL=peopleMenu.js.map