@activecollab/components
Version:
ActiveCollab Components
883 lines (844 loc) • 42 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.taskDisplayName = exports.projectElementTitle = exports.projectClientLabel = exports.projectCardWidth = exports.isProjectTool = exports.groupTasks = exports.groupProjects = exports.TaskElementCard = exports.TASK_CARD_WIDTH = exports.StyledResolving = exports.StyledProjectElementBox = exports.StyledProjectDraft = exports.StyledPlaceholder = exports.StyledLimitedCard = exports.SEEDED_PROJECT_ELEMENT_POSITIONS = exports.SEEDED_PROJECT_ELEMENTS = exports.RESOLVE_MS = exports.ProjectElementPicker = exports.ProjectElementNode = exports.ProjectElementCard = exports.PROJECT_CARD_WIDTH = exports.PLACEHOLDER_MIN_WIDTH = exports.PLACEHOLDER_MIN_HEIGHT = exports.PICKER_PROJECT_LABELS = exports.PICKER_PROJECTS = exports.PICKER_PEOPLE = exports.PICKER_LABELS = exports.LimitedElementCard = exports.LIMITED_ACCESS_MESSAGE = exports.LIMITED_ACCESS_LABEL = exports.INTERNAL_CLIENT_LABEL = void 0;
var _react = _interopRequireWildcard(require("react"));
var _styledComponents = _interopRequireDefault(require("styled-components"));
var _elements = require("./elements");
var _IconButton = require("../../components/IconButton");
var _Icons = require("../../components/Icons");
var _Input = require("../../components/Input");
var _Loaders = require("../../components/Loaders");
var _Modal = require("../../components/Modal");
var _Paper = require("../../components/Paper");
var _RadioButton = require("../../components/RadioButton");
var _StackedCard = require("../../components/StackedCard");
var _Tooltip = require("../../components/Tooltip");
var _Typography = require("../../components/Typography");
var _shared = require("../shared");
var _content = require("../stackedCard/content");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/* ------------------------------------------------------------------ */
/* Project elements — placeholder → picker → resolved card */
/* ------------------------------------------------------------------ */
/**
* The board's third "draw a box, then say what goes in it" element, after the
* link and the file card. The gesture ladder is deliberately the same one those
* two already established, because it is the only one the dock teaches:
*
* armed the Project Elements dock button opens a submenu (Project /
* Task); picking one arms the tool and puts the canvas in
* crosshair mode.
* drawn mouse down → drag → up leaves a dashed PLACEHOLDER at the drawn
* box (clamped to a card-sized minimum), exactly where the real
* card will sit.
* picking the release opens the picker — a centred popover replicating the
* app's `MenuSelector/ProjectList` (and, for tasks, the two-step
* `ProjectTaskMenuSelector`: projects, then that project's tasks).
* resolving a short skeleton flash in the placeholder's own box, standing in
* for "the board is fetching this record's details".
* resolved the placeholder becomes the real card, anchored at the
* placeholder's top-left and grown to the card's natural size.
*
* Escape or a click on the overlay cancels: the picker closes and the
* placeholder is thrown away, so a cancelled gesture leaves no debris.
*/
/* A drawn box smaller than this reads as "I just want a card here", so the
placeholder takes the minimum rather than the (unusable) drawn size. */
var PLACEHOLDER_MIN_WIDTH = exports.PLACEHOLDER_MIN_WIDTH = 260;
var PLACEHOLDER_MIN_HEIGHT = exports.PLACEHOLDER_MIN_HEIGHT = 120;
/* The natural width each resolved card grows to. Heights follow the content,
like every other card on this board. */
var PROJECT_CARD_WIDTH = exports.PROJECT_CARD_WIDTH = 300;
var TASK_CARD_WIDTH = exports.TASK_CARD_WIDTH = 260;
/* How long the "details are being resolved from the backend" flash lasts. */
var RESOLVE_MS = exports.RESOLVE_MS = 480;
/** True for the two board tools this module owns. */
var isProjectTool = exports.isProjectTool = function isProjectTool(tool) {
return tool === "project" || tool === "task";
};
/* ------------------------------------------------------------------ */
/* Mock data */
/* ------------------------------------------------------------------ */
/**
* The people and labels the board carries are exactly what the shared content
* cards read, so the picker's types ARE the cards' types — one shape, so a card
* cannot be fed something it does not understand.
*/
/**
* A project label. The app renders it as a FILLED pill whose background is the
* label's own colour and whose text is a darker shade of it
* (`ProjectLabel.jsx`: `EntityLabel` with `backgroundColor: label.color` and
* `color: label.darker_text_color`), which is the design system's `Chip` — not
* the colour-text `Tag` the column cards use. Both colours are data. The shape
* is the shared `ProjectCard`'s own.
*/
/**
* What an internal project's client line says. The word belongs to the card (it
* is the card that has to decide what to print for a project with no client), so
* the board re-exports the card's own constant rather than keeping a second copy
* that could drift.
*/
var INTERNAL_CLIENT_LABEL = exports.INTERNAL_CLIENT_LABEL = _content.INTERNAL_CLIENT_LABEL;
/* One rule for both the card's client line and the picker's group header, so a
project can never be filed under one name and labelled with another. */
var projectClientLabel = exports.projectClientLabel = function projectClientLabel(project) {
var _project$client;
return (_project$client = project.client) !== null && _project$client !== void 0 ? _project$client : INTERNAL_CLIENT_LABEL;
};
/* Avatar colours are DATA, not theme — they stand in for real user avatars. */
var PICKER_PEOPLE = exports.PICKER_PEOPLE = {
jasmina: {
name: "Jasmina Sekulić",
initials: "JS",
color: _shared.AVATAR_COLORS.purple
},
marko: {
name: "Marko Antić",
initials: "MA",
color: _shared.AVATAR_COLORS.teal
},
dina: {
name: "Dina Kovač",
initials: "DK",
color: _shared.AVATAR_COLORS.orange
},
elena: {
name: "Elena Petrović",
initials: "EP",
color: _shared.AVATAR_COLORS.yellow
},
tomas: {
name: "Tomas Slavik",
initials: "TS",
color: _shared.AVATAR_COLORS.blue
}
};
/* Label colours are data too: they are the label's own colour in the app. */
var PICKER_LABELS = exports.PICKER_LABELS = {
assigned: {
text: "ASSIGNED",
color: "#E08B8B"
},
blocked: {
text: "BLOCKED",
color: "#D9534F"
},
inProgress: {
text: "IN PROGRESS",
color: "#9BA0A8"
},
review: {
text: "REVIEW",
color: "#D4A017"
}
};
/* Project labels carry a background AND a darker text colour, the pair the app
stores as `color` / `darker_text_color`. Uppercase because that is how the
app saves a new project label (ProjectLabel.jsx upper-cases the name). */
var PICKER_PROJECT_LABELS = exports.PICKER_PROJECT_LABELS = {
client: {
text: "CLIENT WORK",
color: "#FBBB75",
textColor: "#6B4416"
},
internal: {
text: "INTERNAL",
color: "#B7D8B0",
textColor: "#2F5B28"
},
urgent: {
text: "URGENT",
color: "#F3A7A7",
textColor: "#7A2222"
},
research: {
text: "RESEARCH",
color: "#B8CDEB",
textColor: "#274974"
}
};
/* A compact constructor so every mocked task stays one readable row. */
var mockTask = function mockTask(number, name, taskList) {
var extra = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
return _objectSpread({
id: "task-".concat(number),
number,
name,
taskList,
assignees: [PICKER_PEOPLE.elena],
date: "16. Jul"
}, extra);
};
var PICKER_PROJECTS = exports.PICKER_PROJECTS = [{
id: "p-ama",
name: "AMA Webinar",
client: "ActiveCollab Inc.",
description: "Quarterly live session with the community. Collect questions up front, keep the run of show to forty minutes, and publish the recap the same week.",
lead: PICKER_PEOPLE.jasmina,
label: PICKER_PROJECT_LABELS.client,
tasks: [mockTask(184, "Draft the run of show", "Preparation", {
subtasks: 3
}), mockTask(185, "Collect questions from the community", "Preparation", {
label: PICKER_LABELS.assigned,
comments: 4
}), mockTask(186, "Record the intro segment", "Production", {
assignees: [PICKER_PEOPLE.marko]
}), mockTask(187, "Publish the recap post", "Follow-up", {
label: PICKER_LABELS.review,
date: "24. Jul"
})]
}, {
id: "p-care",
/* Internal project WITH a description and a leader, but no label. */
name: "Customer Care Team",
client: null,
description: "Where the support team plans its week. Macros, escalation paths and the weekly churn review all live here.",
lead: PICKER_PEOPLE.dina,
tasks: [mockTask(201, "Rewrite the macro replies", "Inbox", {
label: PICKER_LABELS.inProgress,
comments: 2
}), mockTask(202, "Weekly churn review", "Reporting", {
subtasks: 5
}), mockTask(203, "Escalation path for billing", "Inbox", {
assignees: [PICKER_PEOPLE.tomas]
}), mockTask(204, "Onboarding call script", "Reporting", {
date: "2. Aug"
})]
}, {
id: "p-backlog",
/* No description and NO leader — the meta row is the label alone, pushed
to the right of an otherwise empty row. */
name: "Design Backlog",
client: null,
label: PICKER_PROJECT_LABELS.internal,
tasks: [mockTask(311, "Whiteboard element inspector", "Ideas", {
label: PICKER_LABELS.assigned,
subtasks: 2,
comments: 3
}), mockTask(312, "Empty states for the board list", "Ideas"), mockTask(313, "Dark theme audit of the dock", "Ready", {
label: PICKER_LABELS.blocked,
assignees: [PICKER_PEOPLE.jasmina, PICKER_PEOPLE.marko]
}), mockTask(314, "Icon set for board elements", "Ready", {
date: "9. Aug"
})]
}, {
id: "p-design-team",
/* Leader only: no description, no label. */
name: "Design Team",
client: null,
lead: PICKER_PEOPLE.elena,
tasks: [mockTask(402, "Design system spring cleanup", "This Sprint", {
subtasks: 8
}), mockTask(403, "Retro board template", "This Sprint", {
label: PICKER_LABELS.inProgress,
assignees: [PICKER_PEOPLE.dina]
}), mockTask(404, "Handoff checklist", "Next Sprint", {
comments: 1
}), mockTask(405, "Component audit: cards", "Next Sprint", {
date: "30. Jul"
})]
}, {
id: "p-foo",
/* The bare minimum a project card can be: name and client line only. No
description, no leader, no label — so no meta row at all. */
name: "Foo Kanban",
client: null,
tasks: [mockTask(77, "Set up the columns", "Task List", {
subtasks: 1
}), mockTask(78, "Import the old cards", "Task List", {
label: PICKER_LABELS.blocked
}), mockTask(79, "Invite the team", "Task List 2", {
assignees: [PICKER_PEOPLE.marko]
}), mockTask(80, "Archive stale swimlanes", "Task List 2", {
date: "5. Aug"
})]
}, {
id: "p-marketing",
name: "Marketing Site",
client: "ActiveCollab Inc.",
description: "Pricing, case studies and the homepage. Copy passes go through content, performance budgets through engineering.",
lead: PICKER_PEOPLE.marko,
label: PICKER_PROJECT_LABELS.urgent,
tasks: [mockTask(510, "Pricing page copy pass", "Content", {
label: PICKER_LABELS.review,
comments: 6
}), mockTask(511, "Case study: Bright Lane", "Content", {
subtasks: 4
}), mockTask(512, "Lighthouse budget for the homepage", "Engineering", {
assignees: [PICKER_PEOPLE.tomas]
}), mockTask(513, "Cookie banner rework", "Engineering", {
date: "11. Aug"
})]
}, {
id: "p-research",
name: "Product Research",
client: null,
description: "Interviews, synthesis and concept tests for the board work. Findings are shared with design at the end of each round.",
lead: PICKER_PEOPLE.jasmina,
label: PICKER_PROJECT_LABELS.research,
tasks: [mockTask(620, "Interview script for board users", "Discovery", {
subtasks: 3
}), mockTask(621, "Synthesis of the June calls", "Discovery", {
label: PICKER_LABELS.inProgress,
comments: 2
}), mockTask(622, "Concept test: project cards", "Validation", {
assignees: [PICKER_PEOPLE.elena, PICKER_PEOPLE.dina]
}), mockTask(623, "Share findings with design", "Validation", {
date: "19. Aug"
})]
}, {
id: "p-brand",
name: "Brand Refresh",
client: "Bright Lane Studio",
description: "Identity work: logo lockups, a typography scale and the photography direction, collected into a brand book.",
lead: PICKER_PEOPLE.dina,
label: PICKER_PROJECT_LABELS.client,
tasks: [mockTask(701, "Logo lockup variations", "Identity", {
subtasks: 6
}), mockTask(702, "Typography scale", "Identity", {
label: PICKER_LABELS.assigned
}), mockTask(703, "Photography direction", "Guidelines", {
comments: 3
}), mockTask(704, "Brand book layout", "Guidelines", {
date: "28. Jul"
})]
}, {
id: "p-portal",
/* Leader AND label, but no description. */
name: "Client Portal",
client: "Bright Lane Studio",
lead: PICKER_PEOPLE.tomas,
label: PICKER_PROJECT_LABELS.urgent,
tasks: [mockTask(810, "Invoice list pagination", "Backend", {
label: PICKER_LABELS.blocked,
subtasks: 2
}), mockTask(811, "Single sign-on for clients", "Backend", {
comments: 5
}), mockTask(812, "Dashboard tiles", "Frontend", {
assignees: [PICKER_PEOPLE.jasmina]
}), mockTask(813, "Mobile navigation", "Frontend", {
date: "3. Sep"
})]
}];
/* The picker groups projects by client and tasks by task list, exactly the way
`ProjectList` / `TaskList` group them in the app. Order is the data's own
order — the first project of a client decides where its group sits. Internal
projects group under the same word their card shows, so the header and the
card cannot say different things about the same project. */
var groupProjects = exports.groupProjects = function groupProjects(projects) {
return projects.reduce(function (groups, project) {
var client = projectClientLabel(project);
var group = groups.find(function (entry) {
return entry.client === client;
});
if (group) {
group.projects.push(project);
return groups;
}
return [...groups, {
client,
projects: [project]
}];
}, []);
};
var groupTasks = exports.groupTasks = function groupTasks(tasks) {
return tasks.reduce(function (groups, task) {
var group = groups.find(function (entry) {
return entry.taskList === task.taskList;
});
if (group) {
group.tasks.push(task);
return groups;
}
return [...groups, {
taskList: task.taskList,
tasks: [task]
}];
}, []);
};
var matchesQuery = function matchesQuery(value, query) {
return value.toLowerCase().includes(query.trim().toLowerCase());
};
/** `#123: Name` — `utils/taskDisplayName`, the app's own task display name. */
var taskDisplayName = exports.taskDisplayName = function taskDisplayName(task) {
return "#".concat(task.number, ": ").concat(task.name);
};
/* ------------------------------------------------------------------ */
/* Limited access */
/* ------------------------------------------------------------------ */
/**
* A board is shared with people who do not all see the same projects. When a
* card points at a task or project the current viewer cannot open, the board
* cannot just drop it: the element is part of everyone else's layout, and a
* hole would be a worse lie than a blank. So the card stays and says exactly
* one thing — something is here, and it is not yours to see.
*
* This follows the app's existing limited-access language (Workload):
* - the wording is "Limited Access", the phrase `EntityPlaceholder`,
* `WorkloadCalendarEvent`, Expenses and Timelist all already use;
* - nothing else is rendered. Workload's own limited cell
* (`.workload_null_project_cell`) shows an aggregate and no name, and the
* shape it is fed is literally `{ hoursOccupied, leftOffset }` — a quantity
* and a position, never an identity;
* - the surface is muted and hatched, the treatment the app reserves for
* "this area is not live" (weekend / day-off cells, `--muted-surface`);
* - no affordance suggests opening: Workload points the link at "#", disables
* the name tooltip so the title cannot leak, and marks blocked interactions
* `cursor: not-allowed`.
*/
var LIMITED_ACCESS_LABEL = exports.LIMITED_ACCESS_LABEL = "Limited Access";
/* The one sentence a limited card is allowed to say, in the app's own voice.
It states the viewer's situation as a fact and stops — Workload's secondary
line is "with your limited access" (WorkloadProjectContainer.js), and its
status sentences read "Work scheduled for a day off." and "task doesn't have
an estimate": second person, sentence case, contraction, no apology, and
never a word about why the thing is on the screen. */
var LIMITED_ACCESS_MESSAGE = exports.LIMITED_ACCESS_MESSAGE = "You don't have access to this item.";
/* ------------------------------------------------------------------ */
/* The element on the board */
/* ------------------------------------------------------------------ */
/** A project / task element on the board. Position lives in `positions`. */
/** The natural width a resolved card of this kind grows to. */
var projectCardWidth = exports.projectCardWidth = function projectCardWidth(kind) {
return kind === "project" ? PROJECT_CARD_WIDTH : TASK_CARD_WIDTH;
};
/**
* What the card is OF — the task's or the project's own name.
*
* These cards are a view of a record that lives in the project, so the board
* does not get to name them: the title is the entity's, and it is read-only
* wherever it appears. A placeholder has no entity yet, so it says what it is
* still waiting for.
*/
var projectElementTitle = exports.projectElementTitle = function projectElementTitle(element) {
if (element.limited) {
return LIMITED_ACCESS_LABEL;
}
if (element.kind === "project") {
var _element$project$name, _element$project;
return (_element$project$name = (_element$project = element.project) === null || _element$project === void 0 ? void 0 : _element$project.name) !== null && _element$project$name !== void 0 ? _element$project$name : "Project";
}
return element.task ? taskDisplayName(element.task) : "Task";
};
/* ------------------------------------------------------------------ */
/* Seeded board state */
/* ------------------------------------------------------------------ */
/**
* Two cards the board starts with, both limited.
*
* They are seeded rather than reachable through the picker on purpose: limited
* access is a property of the VIEWER, and you cannot pick a card you are not
* allowed to see. Someone else placed these; this session is simply the one
* that cannot open them. Seeding is also the whole demonstration — no "view as"
* switch is needed to show what the state looks like.
*
* Both are `resolved` with `project` and `task` left null: the record was never
* withheld pending a fetch, it is withheld full stop.
*/
var SEEDED = [{
element: {
id: "limited-task",
kind: "task",
width: TASK_CARD_WIDTH,
height: PLACEHOLDER_MIN_HEIGHT,
status: "resolved",
limited: true,
project: null,
task: null
},
x: 120,
y: 200
}, {
element: {
id: "limited-project",
kind: "project",
width: PROJECT_CARD_WIDTH,
height: PLACEHOLDER_MIN_HEIGHT,
status: "resolved",
limited: true,
project: null,
task: null
},
x: 120,
y: 380
}];
/* Derived from one list, the way INITIAL_POSITIONS is derived from
CANVAS_ELEMENTS — a seed's position is written once, not twice. */
var SEEDED_PROJECT_ELEMENTS = exports.SEEDED_PROJECT_ELEMENTS = SEEDED.map(function (seed) {
return seed.element;
});
var SEEDED_PROJECT_ELEMENT_POSITIONS = exports.SEEDED_PROJECT_ELEMENT_POSITIONS = SEEDED.reduce(function (acc, seed) {
acc[seed.element.id] = {
x: seed.x,
y: seed.y
};
return acc;
}, {});
/* The board wrapper. It is the canvas element box, only rounded — the shared
box is square because shapes are, and a square selection ring around a
rounded card reads as a second, wrong border. The transition is what makes
the placeholder GROW into its card instead of being replaced by one. */
var StyledProjectElementBox = exports.StyledProjectElementBox = (0, _styledComponents.default)(_elements.StyledCanvasElement).withConfig({
displayName: "projectElements__StyledProjectElementBox",
componentId: "sc-1cayxi5-0"
})(["border-radius:8px;transition:width 160ms ease;&.limited{cursor:not-allowed;}"]);
/* The rubber band and the armed tool's hover preview — the same blue dashed
box the link tool draws, so the gesture reads as one board convention. */
var StyledProjectDraft = exports.StyledProjectDraft = _styledComponents.default.div.withConfig({
displayName: "projectElements__StyledProjectDraft",
componentId: "sc-1cayxi5-1"
})(["position:absolute;box-sizing:border-box;border:1.5px dashed ", ";border-radius:8px;background-color:rgba(59,130,246,0.08);pointer-events:none;"], _elements.SELECTION_BLUE);
/* The placeholder that waits for the picker's answer. Card-like on purpose:
rounded, dashed, tinted — a card-shaped hole, not a selection marquee. */
var StyledPlaceholder = exports.StyledPlaceholder = _styledComponents.default.div.withConfig({
displayName: "projectElements__StyledPlaceholder",
componentId: "sc-1cayxi5-2"
})(["box-sizing:border-box;width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:12px;border:1.5px dashed var(--border-primary);border-radius:8px;background-color:var(--color-theme-200);user-select:none;"]);
/* The resolving flash: the placeholder's box, with the card's rows sketched in
so the swap to the real card does not read as a different element. */
var StyledResolving = exports.StyledResolving = (0, _styledComponents.default)(StyledPlaceholder).withConfig({
displayName: "projectElements__StyledResolving",
componentId: "sc-1cayxi5-3"
})(["flex-direction:column;align-items:stretch;justify-content:center;gap:10px;.c-loader{height:12px;}"]);
/**
* The limited-access face — the card-shaped translation of Workload's black
* `.workload_null_project_cell`.
*
* The app hardcodes that cell `#000000` on `#ffffff` text, which works there
* because it always sits on light paper. A canvas is not always light: on the
* dark board a black slab would sink into the background instead of reading as
* something withheld. So the same idea is expressed with the design system's
* own "not live" tokens — `--muted-surface` / `--muted-surface-text` are the
* pair the app itself substitutes for muted surfaces in dark themes, and they
* are a legible, deliberately flat combination in every theme.
*
* The -45° hatch is copied stroke for stroke from the app's unavailable
* surfaces (weekend and day-off cells): `transparent 5px, <line> 5px, <line>
* 5.7px`, with `--stripe-pattern` as the line — the token workload.less uses
* for exactly this in dark themes.
*/
var StyledLimitedCard = exports.StyledLimitedCard = _styledComponents.default.div.withConfig({
displayName: "projectElements__StyledLimitedCard",
componentId: "sc-1cayxi5-4"
})(["box-sizing:border-box;width:100%;height:100%;min-height:96px;display:flex;align-items:center;justify-content:center;padding:12px;border:1px solid var(--border-primary);border-radius:8px;background-color:var(--muted-surface);background-image:repeating-linear-gradient( -45deg,transparent,transparent 5px,var(--stripe-pattern) 5px,var(--stripe-pattern) 5.7px );color:var(--muted-surface-text);cursor:not-allowed;user-select:none;.limited-label{color:var(--muted-surface-text);}"]);
/**
* Both resolved faces are the design system's `StackedCard` — the same shell the
* board's link and file cards already stand on — filled with the shared typed
* content cards from `../stackedCard/content`. The board owns only the width and
* the selection state; everything inside the card is the card's business.
*
* Identical to `StyledLinkCard`: the shell's reference 260px width gives way to
* the box the board drew, and the height stays auto.
*/
var StyledEntityCard = (0, _styledComponents.default)(_StackedCard.StackedCard).withConfig({
displayName: "projectElements__StyledEntityCard",
componentId: "sc-1cayxi5-5"
})(["&&{width:100%;}user-select:none;"]);
/**
* The task face — the shared `TaskCard`, with the project line switched on.
*
* The board writes no anatomy of its own: the title clamp, the info line (labels
* as colour text, the counter signifiers and their 2px dot separators) and the
* date / assignees row all live in the content card, where the column-view
* baseline they preserve is documented. The one thing the board asks for is the
* project line above the title: on a canvas a task card has lost the column that
* used to say which project it came from.
*
* The title is the app's own display name (`#123: Name`, `taskDisplayName`), so
* the card's separate task-number prefix stays off.
*/
var TaskElementCard = exports.TaskElementCard = function TaskElementCard(_ref) {
var _task$subtasks, _task$comments, _task$date;
var project = _ref.project,
task = _ref.task,
selected = _ref.selected;
return /*#__PURE__*/_react.default.createElement(StyledEntityCard, {
selected: selected
}, /*#__PURE__*/_react.default.createElement(_content.TaskCard, {
title: taskDisplayName(task),
showProjectName: true,
projectName: project.name,
labels: task.label ? [task.label] : [],
subtaskCount: (_task$subtasks = task.subtasks) !== null && _task$subtasks !== void 0 ? _task$subtasks : 0,
commentCount: (_task$comments = task.comments) !== null && _task$comments !== void 0 ? _task$comments : 0,
dateLabel: (_task$date = task.date) !== null && _task$date !== void 0 ? _task$date : null,
assigned: task.assignees.length > 0,
assignees: task.assignees
}));
};
/**
* The project face — the shared `ProjectCard`.
*
* The whiteboard's face spec and the content type's are the SAME spec, so it is
* implemented once, in the card: name and client, an optional description
* excerpt, and one row carrying the leader on the left and the label on the
* right. No progress indicator, and no row at all when the project has neither a
* leader nor a label.
*/
var ProjectElementCard = exports.ProjectElementCard = function ProjectElementCard(_ref2) {
var project = _ref2.project,
selected = _ref2.selected;
return /*#__PURE__*/_react.default.createElement(StyledEntityCard, {
selected: selected
}, /*#__PURE__*/_react.default.createElement(_content.ProjectCard, {
name: project.name,
client: project.client,
description: project.description,
leader: project.lead,
label: project.label
}));
};
/**
* The limited-access card. Deliberately the shortest component in this file:
* one phrase, no icon, no entity data of any kind.
*
* No icon because the app uses none — Expenses and Timelist both render the
* bare `Caption1 weight="bold"` "Limited Access", and an icon here would be a
* second signal competing with the hatch. Even the KIND (task vs project) is
* withheld: the viewer is told that a card exists, not what sort of thing it
* points at.
*
* The tooltip is allowed to exist because it carries the viewer's situation,
* not the entity's identity — the opposite of the tooltip Workload disables on
* a limited event, which would have leaked the name.
*/
var LimitedElementCard = exports.LimitedElementCard = function LimitedElementCard() {
return /*#__PURE__*/_react.default.createElement(_Tooltip.Tooltip, {
title: LIMITED_ACCESS_MESSAGE
}, /*#__PURE__*/_react.default.createElement(StyledLimitedCard, {
"data-testid": "wb-limited-card"
}, /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
weight: "bold",
className: "limited-label"
}, LIMITED_ACCESS_LABEL)));
};
/** One board element, in whichever of its three states it currently is. */
var ProjectElementNode = exports.ProjectElementNode = function ProjectElementNode(_ref3) {
var element = _ref3.element,
selected = _ref3.selected;
/* Checked before anything else: a limited element must never be able to fall
through to a branch that renders entity data. */
if (element.limited) {
return /*#__PURE__*/_react.default.createElement(LimitedElementCard, null);
}
if (element.status === "placeholder") {
return /*#__PURE__*/_react.default.createElement(StyledPlaceholder, null, /*#__PURE__*/_react.default.createElement(_Typography.Caption1, {
color: "tertiary"
}, element.kind === "project" ? "Pick a project…" : "Pick a task…"));
}
if (element.status === "resolving") {
return /*#__PURE__*/_react.default.createElement(StyledResolving, null, /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
width: "80%"
}
}), /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
width: "55%"
}
}), /*#__PURE__*/_react.default.createElement(_Loaders.SkeletonLoader, {
style: {
width: "40%"
}
}));
}
if (element.kind === "project" && element.project) {
return /*#__PURE__*/_react.default.createElement(ProjectElementCard, {
project: element.project,
selected: selected
});
}
if (element.project && element.task) {
return /*#__PURE__*/_react.default.createElement(TaskElementCard, {
project: element.project,
task: element.task,
selected: selected
});
}
return /*#__PURE__*/_react.default.createElement(StyledPlaceholder, null);
};
/* ------------------------------------------------------------------ */
/* The picker */
/* ------------------------------------------------------------------ */
/* The popover, centred over the board's own overlay. It is deliberately NOT a
DS `Dialog`: the app's picker has no title and no action buttons — you search,
you pick, it closes. `Modal` is used only for what a dialog and a popover do
share: the portal, the overlay, the focus lock and the Esc handler. */
var StyledPickerPanel = (0, _styledComponents.default)(_Paper.Paper).withConfig({
displayName: "projectElements__StyledPickerPanel",
componentId: "sc-1cayxi5-6"
})(["position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;flex-direction:column;width:470px;max-width:calc(100vw - 32px);max-height:70vh;overflow:hidden;&&{border-radius:12px;border:1px solid var(--border-primary);box-shadow:0 18px 48px rgba(0,0,0,0.24);}"]);
/* Search + adornments, mirroring `Select`'s own form row: the back arrow is a
start adornment (TaskList), the group-by sort a end adornment (ProjectList). */
var StyledPickerHeader = _styledComponents.default.div.withConfig({
displayName: "projectElements__StyledPickerHeader",
componentId: "sc-1cayxi5-7"
})(["display:flex;align-items:center;gap:8px;flex-shrink:0;padding:12px 12px 8px;.picker-search{width:100%;border-radius:8px;}.picker-search:focus-within{border-color:var(--color-primary);outline:1px solid var(--color-primary);}"]);
var StyledPickerList = _styledComponents.default.div.withConfig({
displayName: "projectElements__StyledPickerList",
componentId: "sc-1cayxi5-8"
})(["flex:1 1 auto;min-height:0;overflow-y:auto;padding:4px 0 12px;.picker-group-header{box-sizing:border-box;height:36px;display:flex;align-items:center;padding:0 20px;margin-top:4px;}.picker-empty{padding:24px 20px;text-align:center;}"]);
/* A generously spaced row: the name on the left, an empty radio on the right —
the shape `Select` gives a single-choice option, at picker scale. */
var StyledPickerRow = _styledComponents.default.div.withConfig({
displayName: "projectElements__StyledPickerRow",
componentId: "sc-1cayxi5-9"
})(["box-sizing:border-box;height:56px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 20px;cursor:pointer;outline:none;&:hover,&:focus-visible{background-color:var(--color-theme-200);}.row-name{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.c-radio-btn{flex-shrink:0;}"]);
var PickerRow = function PickerRow(_ref4) {
var id = _ref4.id,
name = _ref4.name,
onSelect = _ref4.onSelect;
return /*#__PURE__*/_react.default.createElement(StyledPickerRow, {
role: "button",
tabIndex: 0,
onClick: onSelect,
onKeyDown: function onKeyDown(event) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onSelect();
}
}
}, /*#__PURE__*/_react.default.createElement("span", {
className: "row-name"
}, /*#__PURE__*/_react.default.createElement(_Typography.Body2, null, name)), /*#__PURE__*/_react.default.createElement(_RadioButton.RadioButton, {
id: "wb-picker-".concat(id),
tabIndex: -1
}));
};
/**
* One picker, two flows — the same split the app makes between
* `MenuSelector/ProjectList` (one step) and `ProjectTaskMenuSelector` (project,
* then task). The task flow deliberately drops the app's "Directly on project"
* default option: a task card has to be a task.
*/
var ProjectElementPicker = exports.ProjectElementPicker = function ProjectElementPicker(_ref5) {
var kind = _ref5.kind,
onSelectProject = _ref5.onSelectProject,
onSelectTask = _ref5.onSelectTask,
onCancel = _ref5.onCancel;
var _useState = (0, _react.useState)(null),
_useState2 = _slicedToArray(_useState, 2),
project = _useState2[0],
setProject = _useState2[1];
var _useState3 = (0, _react.useState)(""),
_useState4 = _slicedToArray(_useState3, 2),
query = _useState4[0],
setQuery = _useState4[1];
/* The picker opens on a mouseUP — the end of the draw gesture — so the click
that closes that gesture arrives when the overlay is already on screen. A
dismissal therefore has to be a WHOLE click on the overlay: press, then
release. Without this the placeholder could be cancelled by the very
gesture that created it. */
var pressedInsideRef = (0, _react.useRef)(false);
var onTaskStep = kind === "task" && project !== null;
var projectGroups = (0, _react.useMemo)(function () {
return groupProjects(PICKER_PROJECTS.filter(function (entry) {
return matchesQuery(entry.name, query);
}));
}, [query]);
var taskGroups = (0, _react.useMemo)(function () {
return project ? groupTasks(project.tasks.filter(function (task) {
return matchesQuery(taskDisplayName(task), query);
})) : [];
}, [project, query]);
var handleProject = function handleProject(picked) {
if (kind === "project") {
onSelectProject(picked);
return;
}
setProject(picked);
setQuery("");
};
var handleBack = function handleBack() {
setProject(null);
setQuery("");
};
var isEmpty = onTaskStep ? taskGroups.length === 0 : projectGroups.length === 0;
return /*#__PURE__*/_react.default.createElement(_Modal.Modal, {
open: true,
onClose: onCancel,
disableBackgroundClick: true,
onMouseDown: function onMouseDown() {
pressedInsideRef.current = true;
},
onClickOutside: function onClickOutside() {
if (pressedInsideRef.current) {
onCancel();
}
}
}, /*#__PURE__*/_react.default.createElement(StyledPickerPanel, {
type: "paper-2"
}, /*#__PURE__*/_react.default.createElement(StyledPickerHeader, null, onTaskStep ? /*#__PURE__*/_react.default.createElement(_Tooltip.Tooltip, {
title: "Back to projects"
}, /*#__PURE__*/_react.default.createElement(_IconButton.IconButton, {
type: "button",
variant: "text gray",
onClick: handleBack,
"aria-label": "Back to projects"
}, /*#__PURE__*/_react.default.createElement(_Icons.ArrowLeftIcon, null))) : null, /*#__PURE__*/_react.default.createElement(_Input.Input, {
className: "picker-search",
autoFocus: true,
value: query,
placeholder: onTaskStep ? "Search a task" : "Search a project",
onChange: function onChange(event) {
return setQuery(event.target.value);
}
}), onTaskStep ? null : /*#__PURE__*/_react.default.createElement(_Tooltip.Tooltip, {
title: "Group by"
}, /*#__PURE__*/_react.default.createElement(_IconButton.IconButton, {
type: "button",
variant: "text gray"
}, /*#__PURE__*/_react.default.createElement(_Icons.SortGeneralIcon, null)))), /*#__PURE__*/_react.default.createElement(StyledPickerList, null, isEmpty ? /*#__PURE__*/_react.default.createElement("div", {
className: "picker-empty"
}, /*#__PURE__*/_react.default.createElement(_Typography.Body2, {
color: "tertiary"
}, onTaskStep ? "No Result" : "No Projects")) : null, onTaskStep && project ? taskGroups.map(function (group) {
return /*#__PURE__*/_react.default.createElement("div", {
key: group.taskList
}, /*#__PURE__*/_react.default.createElement("div", {
className: "picker-group-header"
}, /*#__PURE__*/_react.default.createElement(_Typography.Body2, {
weight: "bold"
}, group.taskList)), group.tasks.map(function (task) {
return /*#__PURE__*/_react.default.createElement(PickerRow, {
key: task.id,
id: task.id,
name: taskDisplayName(task),
onSelect: function onSelect() {
return onSelectTask(project, task);
}
});
}));
}) : null, onTaskStep ? null : projectGroups.map(function (group) {
return /*#__PURE__*/_react.default.createElement("div", {
key: group.client
}, /*#__PURE__*/_react.default.createElement("div", {
className: "picker-group-header"
}, /*#__PURE__*/_react.default.createElement(_Typography.Body2, {
weight: "bold"
}, group.client)), group.projects.map(function (entry) {
return /*#__PURE__*/_react.default.createElement(PickerRow, {
key: entry.id,
id: entry.id,
name: entry.name,
onSelect: function onSelect() {
return handleProject(entry);
}
});
}));
}))));
};
//# sourceMappingURL=projectElements.js.map