UNPKG

@activecollab/components

Version:

ActiveCollab Components

926 lines (891 loc) 37.5 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; const _excluded = ["open", "className", "children"], _excluded2 = ["value", "tooltip"], _excluded3 = ["labels"], _excluded4 = ["value", "tooltip", "overdue"]; import React, { createContext, forwardRef, useCallback, useContext, useMemo, useState } from "react"; import moment from "moment-timezone"; import styled, { css } from "styled-components"; import { Button } from "../../components/Button"; import { Checkbox } from "../../components/Checkbox"; import { Chip } from "../../components/Chip"; import { DatePicker } from "../../components/DatePicker"; import { Dot } from "../../components/Dot"; import { CalendarIcon, ClockAddIcon, CollapseExpandSingleIcon } from "../../components/Icons"; import { InputHours } from "../../components/Input"; import { Label } from "../../components/Label"; import { Modal } from "../../components/Modal"; import { ProgressPie } from "../../components/ProgressPie"; import { Select } from "../../components/Select"; import { SelectTrigger } from "../../components/SelectTrigger"; import { Textarea } from "../../components/Textarea"; import { SlideFromTop } from "../../components/Transitions"; import { Body2, Caption1, Caption2, Header3 } from "../../components/Typography"; import { formatHours } from "../../utils"; /** * Interactive list-view column cells for the `Presentation/List view` and * `Presentation/List View ID Column` stories — the Label, Date and Time cells * with their editors, mirroring * `new-activecollab-front/src/components/ListView/columns/*`, plus an * Identifier cell that only the ID-column story uses (it has no counterpart in * the app yet): * * - **Identifier** (no app equivalent): `#<id>` as a Copy Link trigger, styled * like the Date trigger. One typography at every length — the column's width * is what adapts to how large the identifiers get. * - **Label** (`columns/Label.js` + `LabelTrigger`/`LabelItems`): chips for 1–2 * labels, dots from 3 up, `+N` past 8, and a DS `Select` picker (search, * colored dot + label name in the label's own colour, checkbox, "+ Add new"). * - **Date** (`columns/Date.js`): a range trigger that formats like the app * (`formatDateRange`), turns red when overdue, and opens the DS `DatePicker` * in atomic mode so it gets the Save / ✕ / Clear footer. * - **Time** (`columns/Time.js`): `ProgressPie` + `tracked/estimated`, coloured * by how much of the estimate is used, opening the "Add Time Record" dialog. * * Everything is local state — there is no store, so edits live as long as the * story is mounted. */ const SANS = '-apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Helvetica Neue", Arial, sans-serif'; /** * CSS-only tooltip, mirroring the app's `.c-cooltip.c-cooltip--top` (see * `styles/c_cooltip.less`). The list renders hundreds of these, and the DS * `Tooltip` mounts a Portal + Popper per instance — far too heavy per cell — so * the cells use `data-tooltip` exactly like the real list view does. */ const CooltipStyle = css(["&[data-tooltip],[data-tooltip]{position:relative;}&[data-tooltip]::after,[data-tooltip]::after{pointer-events:none;content:attr(data-tooltip);position:absolute;bottom:calc(100% + 0.5rem);left:50%;transform:scale(0.85) translateX(-50%);transform-origin:0;opacity:0;max-width:12.5rem;padding:0.25rem 0.625rem;border-radius:0.5rem;background-color:rgba(0,0,0,0.9);color:#fff;font-family:", ";font-size:0.75rem;font-weight:500;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}@media (hover:hover){&[data-tooltip]:not(:focus):hover::after,[data-tooltip]:not(:focus):hover::after{transition:opacity 0.15s ease,transform 0.15s ease;transition-delay:0.6s;transform:scale(1) translateX(-50%);opacity:1;}}"], SANS); /* ================================================================== */ /* Shared trigger button (mirrors ListView/ListButton.js) */ /* ================================================================== */ /** * `Menu` clones its `target` and injects `open`, so — like the app's * `ListButton` — the trigger has to swallow that prop and turn it into the DS * `active` state plus an `.open` class the row CSS uses to keep the trigger * visible while its menu is open. */ const ListButtonBase = /*#__PURE__*/forwardRef((_ref, ref) => { let open = _ref.open, className = _ref.className, children = _ref.children, rest = _objectWithoutPropertiesLoose(_ref, _excluded); return /*#__PURE__*/React.createElement(Button, _extends({}, rest, { ref: ref, type: "button", variant: "text gray", active: open, className: "list-button" + (className ? " " + className : "") + (open ? " open" : "") }), children); }); ListButtonBase.displayName = "ListButtonBase"; /* * The DS Button treats a single element child as "icon only" — inline-flex, * centered, a fixed 32px wide and `overflow: hidden`, which squashes a date or * a chip. `&&` doubles the specificity so these rules win over that block; the * button then hugs its content and the column's `justify-content: flex-end` * parks it against the right edge. */ const StyledListButton = styled(ListButtonBase).withConfig({ displayName: "columns__StyledListButton", componentId: "sc-9f4385-0" })(["&&{display:inline-flex;align-items:center;justify-content:flex-end;width:auto;min-width:0;max-width:100%;padding:4px 8px;overflow:visible;text-align:right;}", ""], CooltipStyle); /* ================================================================== */ /* Identifier (task ID) */ /* ================================================================== */ const StyledIdValue = styled.span.withConfig({ displayName: "columns__StyledIdValue", componentId: "sc-9f4385-1" })(["font-family:", ";white-space:nowrap;font-variant-numeric:tabular-nums;"], SANS); /** * Same trigger the Date cell uses, so the id gets the identical hover outline * and 8px inset — it is a link, not an editor, so it is always visible * (`has-value`) rather than appearing on row hover. * * The typography is Caption 1 bold at every identifier length, like the Date * value next to it; long identifiers are handled by the column's width tier, * not by shrinking the text. */ const IdTrigger = /*#__PURE__*/forwardRef((_ref2, ref) => { let value = _ref2.value, tooltip = _ref2.tooltip, rest = _objectWithoutPropertiesLoose(_ref2, _excluded2); return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, { ref: ref, className: "show-on-hover has-value", "aria-label": "Copy link to task " + value, "data-tooltip": tooltip }), /*#__PURE__*/React.createElement(StyledIdValue, null, /*#__PURE__*/React.createElement(Caption1, { weight: "bold", color: "secondary" }, value))); }); IdTrigger.displayName = "IdTrigger"; /** * Task identifier cell — the `#<id>` value as a copy-link control, mirroring * how `tasks/Metadata.js` turns `Task #248` into the task's Copy Link action: * the click writes the task URL to the clipboard and reports back so the page * can raise the app's "Link copied to clipboard." toast (utils/copyLinkToClipboard). * It never navigates — the row itself is the navigation target. */ export const IdentifierCell = _ref3 => { let identifier = _ref3.identifier, link = _ref3.link, onCopy = _ref3.onCopy; const handleClick = useCallback(event => { var _navigator$clipboard; /* the whole row navigates to the task, so copying must not also open it */ event.preventDefault(); event.stopPropagation(); if ((_navigator$clipboard = navigator.clipboard) != null && _navigator$clipboard.writeText) { navigator.clipboard.writeText(link).then(() => onCopy == null ? void 0 : onCopy(true)).catch(() => onCopy == null ? void 0 : onCopy(false)); return; } onCopy == null || onCopy(false); }, [link, onCopy]); return /*#__PURE__*/React.createElement("div", { className: "col col-identifier" }, /*#__PURE__*/React.createElement(IdTrigger, { value: "#" + identifier, tooltip: "Copy Link", onClick: handleClick })); }; /* ================================================================== */ /* Labels */ /* ================================================================== */ /* The project's label catalogue. Names are uppercase, like in the app. */ const INITIAL_LABELS = [{ id: "in-progress", name: "IN PROGRESS", color: "#CDE8F5", textColor: "#3D7F9E" }, { id: "needs-review", name: "NEEDS REVIEW", color: "#F9E79F", textColor: "#A98A17" }, { id: "approved", name: "APPROVED", color: "#A9DFBF", textColor: "#3D9A6A" }, { id: "blocked", name: "BLOCKED", color: "#F1948A", textColor: "#B5544C" }, { id: "on-hold", name: "ON HOLD", color: "#F5CBA7", textColor: "#B4713A" }, { id: "idea", name: "IDEA", color: "#FAD7A0", textColor: "#B98A2B" }, { id: "todo", name: "TODO", color: "#D6EAF8", textColor: "#4A87B5" }, { id: "research", name: "RESEARCH", color: "#D7BDE2", textColor: "#8155A0" }, { id: "accessibility", name: "ACCESSIBILITY", color: "#AED6F1", textColor: "#3E7CA6" }, { id: "copy", name: "COPY", color: "#F5B7B1", textColor: "#B5544C" }, { id: "design-debt", name: "DESIGN DEBT", color: "#E8DAEF", textColor: "#7D5A99" }, { id: "qa", name: "QA", color: "#A3E4D7", textColor: "#34907C" }, { id: "client-input", name: "CLIENT INPUT", color: "#FDEBD0", textColor: "#B08640" }, { id: "mobile", name: "MOBILE", color: "#D1F2EB", textColor: "#2E8B7A" }, { id: "performance", name: "PERFORMANCE", color: "#FCF3CF", textColor: "#A38B1E" }, { id: "asset", name: "ASSET", color: "#D5D8DC", textColor: "#6B7378" }]; /* Colours handed out to labels created from the picker's "+ Add new" row. */ const NEW_LABEL_COLORS = [{ color: "#F9E0F2", textColor: "#A4569B" }, { color: "#DDEBFB", textColor: "#4E7BA8" }, { color: "#E4F6D8", textColor: "#5F9440" }, { color: "#FDE2D5", textColor: "#B36A45" }]; const LabelsContext = /*#__PURE__*/createContext({ labels: INITIAL_LABELS, labelsById: {}, addLabel: () => "" }); /** * Holds the label catalogue for the whole list, so a label created in one * task's picker shows up in every other picker too. */ export const LabelsProvider = _ref4 => { let children = _ref4.children; const _useState = useState(INITIAL_LABELS), labels = _useState[0], setLabels = _useState[1]; const addLabel = useCallback(name => { const trimmed = name.trim().toUpperCase(); const id = "label-" + trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "-"); setLabels(prev => { if (prev.some(label => label.id === id)) { return prev; } const palette = NEW_LABEL_COLORS[prev.length % NEW_LABEL_COLORS.length]; return [...prev, _extends({ id, name: trimmed }, palette)]; }); return id; }, []); const value = useMemo(() => ({ labels, labelsById: labels.reduce((acc, label) => _extends({}, acc, { [label.id]: label }), {}), addLabel }), [addLabel, labels]); return /*#__PURE__*/React.createElement(LabelsContext.Provider, { value: value }, children); }; const useTaskLabels = () => useContext(LabelsContext); /* Empty state pill (mirrors EntityLabel + .entity-label in list-view.less) */ const StyledLabelPlaceholder = styled.span.withConfig({ displayName: "columns__StyledLabelPlaceholder", componentId: "sc-9f4385-2" })(["display:inline-flex;align-items:center;padding:1px 6px;border:1px solid var(--color-theme-700);border-radius:4px;.list-button.open &,.list-button:hover &{border-color:var(--color-primary-700,var(--color-primary));}"]); const StyledLabelItems = styled.span.withConfig({ displayName: "columns__StyledLabelItems", componentId: "sc-9f4385-3" })(["display:inline-flex;align-items:center;min-width:0;> *:not(:last-child){margin-right:2px;}.rest-count{margin-left:2px;}", ""], CooltipStyle); const MAX_SHOWN_LABELS = 8; const LabelItems = _ref5 => { let labels = _ref5.labels; const showAsDot = labels.length > 2; let shown = labels; let restCount = 0; if (labels.length > MAX_SHOWN_LABELS) { restCount = Math.min(labels.length - MAX_SHOWN_LABELS, 99); shown = labels.slice(0, MAX_SHOWN_LABELS - 1); } return /*#__PURE__*/React.createElement(StyledLabelItems, null, shown.map(label => showAsDot ? /*#__PURE__*/React.createElement(Dot, { key: label.id, color: label.color, size: 20, "data-tooltip": label.name }) : /*#__PURE__*/React.createElement(Chip, { key: label.id, label: label.name, backgroundColor: label.color, color: "#303037", style: { maxWidth: "90px" }, typographyProps: { variant: "Caption 2", weight: "bold" }, "data-tooltip": label.name })), restCount > 0 ? /*#__PURE__*/React.createElement(Body2, { weight: "bold", className: "rest-count" }, "+", restCount) : null); }; const LabelTrigger = /*#__PURE__*/forwardRef((_ref6, ref) => { let labels = _ref6.labels, rest = _objectWithoutPropertiesLoose(_ref6, _excluded3); if (labels.length === 0) { return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, { ref: ref, className: "show-on-hover", "aria-label": "Add labels", "data-tooltip": "Add Labels" }), /*#__PURE__*/React.createElement(StyledLabelPlaceholder, null, /*#__PURE__*/React.createElement(Caption2, { weight: "bold", color: "tertiary" }, "LABEL"))); } return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, { ref: ref, "aria-label": "Change labels" }), /*#__PURE__*/React.createElement(LabelItems, { labels: labels })); }); LabelTrigger.displayName = "LabelTrigger"; export const LabelCell = _ref7 => { let labelIds = _ref7.labelIds, _ref7$completed = _ref7.completed, completed = _ref7$completed === void 0 ? false : _ref7$completed; const _useTaskLabels = useTaskLabels(), catalog = _useTaskLabels.labels, labelsById = _useTaskLabels.labelsById, addLabel = _useTaskLabels.addLabel; const _useState2 = useState(labelIds != null ? labelIds : []), selected = _useState2[0], setSelected = _useState2[1]; /* alphabetical, like LabelTrigger sorts before rendering */ const selectedLabels = useMemo(() => selected.map(id => labelsById[id]).filter(label => Boolean(label)).sort((a, b) => a.name.localeCompare(b.name)), [labelsById, selected]); const options = useMemo(() => catalog.map(label => ({ id: label.id, name: label.name, color: label.color, textColor: label.textColor })), [catalog]); const handleChange = useCallback(value => { setSelected(Array.isArray(value) ? value.map(String) : [String(value)].filter(Boolean)); }, []); const handleAddNew = useCallback(name => { if (!name || !name.trim()) { return; } const id = addLabel(name); setSelected(prev => prev.includes(id) ? prev : [...prev, id]); }, [addLabel]); /* Completed tasks show their labels read-only (mirrors Label.js) */ if (completed) { return /*#__PURE__*/React.createElement(StyledReadOnlyCell, { className: "col col-label" }, selectedLabels.length > 0 ? /*#__PURE__*/React.createElement(LabelItems, { labels: selectedLabels }) : null); } return /*#__PURE__*/React.createElement("div", { className: "col col-label" }, /*#__PURE__*/React.createElement(Select, { type: "multiple", options: options, selected: selected, onChange: handleChange, emptyValue: "Add new", emptyAction: handleAddNew, autoHeightMax: 278, position: "bottom-end", target: /*#__PURE__*/React.createElement(LabelTrigger, { labels: selectedLabels }) })); }; /* ================================================================== */ /* Date */ /* ================================================================== */ /** The personalized date format the app defaults to (`MMM D. YYYY`). */ const DATE_FORMAT = "MMM D. YYYY"; /** Midnight UTC today — the reference point for the mock data and overdue. */ const todayUnix = () => moment.utc().startOf("day").unix(); /** Build a mock timestamp N days from today (negative = in the past). */ export const dayOffset = offset => moment.utc().startOf("day").add(offset, "days").unix(); /** mirrors utils/date.js → formatDateRange (month-first format) */ export const formatDateRange = (from, to) => { if (from === undefined || from === null) { return undefined; } const start = moment.unix(from).utc(); const due = moment.unix(to != null ? to : from).utc(); const withoutYear = DATE_FORMAT.replace(/YYYY|,$/gi, "").trim(); const withoutYearAndMonth = withoutYear.replace(/MMM|,$/gi, "").trim(); if (from === to || to === undefined) { return start.format(withoutYear); } if (start.year() === due.year() && start.month() === due.month()) { return start.format(withoutYear) + " - " + due.format(withoutYearAndMonth); } return start.format(withoutYear) + " - " + due.format(withoutYear); }; /** mirrors utils/date.js → formatDateSmart(…, long) */ const formatDateLong = value => value ? moment.unix(value).utc().format(DATE_FORMAT) : ""; export const isOverdue = (dueOn, completed) => Boolean(dueOn && !completed && dueOn < todayUnix()); const StyledDateValue = styled.span.withConfig({ displayName: "columns__StyledDateValue", componentId: "sc-9f4385-4" })(["font-family:", ";white-space:nowrap;"], SANS); /* completed rows render values without a trigger, so the cell itself hosts the CSS tooltip */ const StyledReadOnlyCell = styled.div.withConfig({ displayName: "columns__StyledReadOnlyCell", componentId: "sc-9f4385-5" })(["padding-right:8px;", ""], CooltipStyle); /** * `DatePicker` wraps itself in a `width: 100%` inline-block, which would stretch * across the column and left-align the trigger the moment the picker mounts — * the value would visibly jump and the popper would anchor to the wrong spot. * Keeping that wrapper right-aligned makes the open state geometrically * identical to the closed one. */ const StyledPickerCell = styled.div.withConfig({ displayName: "columns__StyledPickerCell", componentId: "sc-9f4385-6" })(["> div{text-align:right;}"]); const DateTrigger = /*#__PURE__*/forwardRef((_ref8, ref) => { let value = _ref8.value, tooltip = _ref8.tooltip, overdue = _ref8.overdue, rest = _objectWithoutPropertiesLoose(_ref8, _excluded4); return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, { ref: ref, className: value ? "show-on-hover has-value" : "show-on-hover", "aria-label": value ? "Change dates" : "Add date", "data-tooltip": tooltip }), value ? /*#__PURE__*/React.createElement(StyledDateValue, null, /*#__PURE__*/React.createElement(Caption1, { weight: "bold", color: overdue ? "alert" : "secondary" }, value)) : /*#__PURE__*/React.createElement(CalendarIcon, null)); }); DateTrigger.displayName = "DateTrigger"; export const DateCell = _ref9 => { var _dates$from, _dates$to2; let startOn = _ref9.startOn, dueOn = _ref9.dueOn, _ref9$completed = _ref9.completed, completed = _ref9$completed === void 0 ? false : _ref9$completed; const _useState3 = useState({ from: startOn, to: dueOn }), dates = _useState3[0], setDates = _useState3[1]; /** * The DS `DatePicker` does real work on mount (timezone guess, month grids, * key handlers) — with one per row that is the single most expensive thing on * this page. So the closed state is just the trigger; the picker is mounted * on first click, already open, and unmounted again when it closes. */ const _useState4 = useState(false), pickerMounted = _useState4[0], setPickerMounted = _useState4[1]; /* moment isn't cheap either; only reformat when the dates actually change */ const _useMemo = useMemo(() => { var _dates$to; const formatted = formatDateRange(dates.from, dates.to); let hint = "Add Date"; if (dates.from) { hint = !dates.to || dates.to === dates.from ? formatDateLong(dates.from) : formatDateLong(dates.from) + " - " + formatDateLong(dates.to); } return { value: formatted, overdue: isOverdue((_dates$to = dates.to) != null ? _dates$to : dates.from, completed), tooltip: hint }; }, [completed, dates.from, dates.to]), value = _useMemo.value, overdue = _useMemo.overdue, tooltip = _useMemo.tooltip; const handleSave = useCallback(range => { if (!range || range.from === null || range.from === undefined) { setDates({}); return; } setDates({ from: Number(range.from), to: range.to ? Number(range.to) : Number(range.from) }); }, []); const handleClear = useCallback(() => setDates({}), []); const handleCalendarToggle = useCallback(next => { if (!next) { setPickerMounted(false); } }, []); /* Completed tasks show the date read-only (mirrors Date.js) */ if (completed) { return /*#__PURE__*/React.createElement(StyledReadOnlyCell, { className: "col col-date" }, value ? /*#__PURE__*/React.createElement(StyledDateValue, { "data-tooltip": tooltip }, /*#__PURE__*/React.createElement(Caption1, { weight: "bold", color: overdue ? "alert" : "secondary" }, value)) : null); } if (!pickerMounted) { return /*#__PURE__*/React.createElement("div", { className: "col col-date" }, /*#__PURE__*/React.createElement(DateTrigger, { value: value, tooltip: tooltip, overdue: overdue, onClick: () => setPickerMounted(true) })); } return /*#__PURE__*/React.createElement(StyledPickerCell, { className: "col col-date" }, /*#__PURE__*/React.createElement(DatePicker, { open: true, mode: "custom", instant: false, maxRange: 366, firstDayOfWeek: 1, position: "bottom-end", selected: { from: (_dates$from = dates.from) != null ? _dates$from : null, to: (_dates$to2 = dates.to) != null ? _dates$to2 : null }, onSave: handleSave, onClear: handleClear, onCalendarToggle: handleCalendarToggle, target: /*#__PURE__*/React.createElement(DateTrigger, { value: value, tooltip: tooltip, overdue: overdue }) })); }; /* ================================================================== */ /* Time */ /* ================================================================== */ /** mirrors utils/utils.jsx → calculateTimeToEstimatePercentage */ const timeToEstimatePercentage = (tracked, estimated) => { if (estimated > 0) { return tracked > 0 ? Math.round(tracked / estimated * 100) : 0; } return null; }; /** mirrors utils/utils.jsx → formatTrackedAndEstimatedTime */ const formatTrackedAndEstimated = (tracked, estimated) => estimated > 0 ? formatHours(tracked, false, false, "short") + "/" + formatHours(estimated, false, false, "short") : formatHours(tracked, false, false, "short"); /* mirrors Time.js → progressColor() */ const progressColor = percent => { if (percent >= 100) { return "var(--red-alert)"; } if (percent >= 75) { return "var(--warning)"; } return "var(--color-sucess-green)"; }; const StyledTimeValue = styled.span.withConfig({ displayName: "columns__StyledTimeValue", componentId: "sc-9f4385-7" })(["display:inline-flex;align-items:center;font-family:", ";white-space:nowrap;.c-progress-pie,svg{flex-shrink:0;}.time-pie{margin-right:8px;}"], SANS); const TimeValue = _ref0 => { let tracked = _ref0.tracked, estimated = _ref0.estimated; const percent = timeToEstimatePercentage(tracked, estimated); return /*#__PURE__*/React.createElement(StyledTimeValue, null, estimated > 0 ? /*#__PURE__*/React.createElement(ProgressPie, { className: "time-pie", radius: 10, progress: percent != null ? percent : 0, progressColor: progressColor(percent != null ? percent : 0), backgroundColor: "var(--color-theme-300)" }) : null, /*#__PURE__*/React.createElement(Caption1, { weight: "bold" }, formatTrackedAndEstimated(tracked, estimated))); }; const TimeTrigger = _ref1 => { let tracked = _ref1.tracked, estimated = _ref1.estimated, onClick = _ref1.onClick; const hasTime = tracked > 0 || estimated > 0; return /*#__PURE__*/React.createElement(StyledListButton, { onClick: onClick, className: hasTime ? "show-on-hover open" : "show-on-hover", "aria-label": hasTime ? "Edit time" : "Add time", "data-tooltip": hasTime ? "Edit Time" : "Add Time" }, hasTime ? /*#__PURE__*/React.createElement(TimeValue, { tracked: tracked, estimated: estimated }) : /*#__PURE__*/React.createElement(ClockAddIcon, null)); }; export const TimeCell = _ref10 => { let taskName = _ref10.taskName, _ref10$tracked = _ref10.tracked, tracked = _ref10$tracked === void 0 ? 0 : _ref10$tracked, _ref10$estimated = _ref10.estimated, estimated = _ref10$estimated === void 0 ? 0 : _ref10$estimated, users = _ref10.users, currentUserId = _ref10.currentUserId, _ref10$completed = _ref10.completed, completed = _ref10$completed === void 0 ? false : _ref10$completed; const _useState5 = useState(false), open = _useState5[0], setOpen = _useState5[1]; const _useState6 = useState(() => { var _users$find$name, _users$find; return tracked > 0 ? [{ id: 1, date: dayOffset(-3), userId: currentUserId, userName: (_users$find$name = (_users$find = users.find(user => user.id === currentUserId)) == null ? void 0 : _users$find.name) != null ? _users$find$name : "You", jobType: "Design", hours: tracked, billable: true }] : []; }), records = _useState6[0], setRecords = _useState6[1]; const total = useMemo(() => records.reduce((sum, record) => sum + record.hours, 0), [records]); const handleAdd = useCallback(record => { setRecords(prev => [_extends({}, record, { id: prev.length + 1 }), ...prev]); setOpen(false); }, []); /* Completed tasks show the value without the "add" affordance (mirrors Time.js passing hideIcon for completed tasks) */ if (completed) { return /*#__PURE__*/React.createElement(StyledReadOnlyCell, { className: "col col-time" }, total > 0 || estimated > 0 ? /*#__PURE__*/React.createElement(TimeValue, { tracked: total, estimated: estimated }) : null); } return /*#__PURE__*/React.createElement("div", { className: "col col-time" }, /*#__PURE__*/React.createElement(TimeTrigger, { tracked: total, estimated: estimated, onClick: () => setOpen(true) }), open ? /*#__PURE__*/React.createElement(TimeRecordDialog, { open: open, taskName: taskName, users: users, currentUserId: currentUserId, records: records, total: total, onSubmit: handleAdd, onClose: () => setOpen(false) }) : null); }; /* ------------------------------------------------------------------ */ /* Add Time Record dialog (mirrors TimeRecordForm/*) */ /* ------------------------------------------------------------------ */ const JOB_TYPES = [{ id: "design", name: "Design" }, { id: "development", name: "Development" }, { id: "devops", name: "Devops" }, { id: "consulting", name: "Consulting" }, { id: "qa", name: "QA" }]; /* The dialog paper — the app builds this one by hand rather than with DS `Dialog` because it is 770px wide (`.time_record_form_modal_wrapper`). */ const StyledTimeRecordPaper = styled.div.withConfig({ displayName: "columns__StyledTimeRecordPaper", componentId: "sc-9f4385-8" })(["position:relative;width:770px;max-width:95vw;max-height:calc(100vh - 156px);margin:126px auto 30px auto;padding:20px 30px;overflow:auto;border-radius:8px;background-color:var(--page-paper-main);color:var(--color-theme-900);box-shadow:var(--shadow-primary);box-sizing:border-box;font-family:", ";*{box-sizing:border-box;}@media (max-width:830px){width:calc(100vw - 60px);}.trd-header{display:flex;align-items:center;justify-content:space-between;}.trd-title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin-right:16px;font-size:1rem;font-weight:700;}.trd-date button{width:auto;}.trd-date > div{text-align:right;}.trd-divider{margin:20px 0 32px 0;border-bottom:1px solid var(--border-primary);height:1px;}.trd-body{display:flex;justify-content:space-between;position:relative;}.trd-form{width:100%;}.trd-fields{display:flex;align-items:flex-end;}.trd-hours{width:144px;margin-right:16px;flex-shrink:0;}.trd-hours .c-input-wrapper,.trd-hours input{width:100%;}.trd-field{margin-right:16px;}.trd-field:last-of-type{margin-right:20px;}.trd-field .c-label{display:block;margin-bottom:2px;}.trd-field button{width:144px;}.trd-billable{display:flex;align-items:center;margin-top:24px;}.trd-billable .c-label{margin-left:8px;cursor:pointer;}.trd-description-trigger{display:inline-flex;align-items:center;margin-top:16px;color:var(--color-secondary);font-size:14px;font-weight:500;background:none;border:none;padding:0;cursor:pointer;font-family:", ";}.trd-description-trigger svg{margin-right:4px;fill:var(--color-primary);transition:transform 200ms ease;}.trd-description-trigger.collapsed svg{transform:rotate(180deg);}.trd-textarea{width:360px;max-width:100%;margin-top:8px;font-size:12px;resize:none;}.trd-actions{display:flex;margin-top:24px;}.trd-actions .c-button:first-child{margin-right:12px;}.trd-total{position:absolute;right:0;bottom:0;display:inline-flex;align-items:center;align-self:flex-end;background:none;border:none;padding:0;cursor:pointer;}.trd-total svg{margin-right:4px;fill:var(--color-primary);transition:transform 200ms ease;}.trd-total.collapsed svg{transform:rotate(180deg);}.trd-records{margin-top:32px;border-top:1px solid var(--border-primary);}.trd-record{display:flex;align-items:center;justify-content:space-between;height:36px;border-bottom:1px solid var(--color-theme-300);}.trd-record-left{display:flex;align-items:center;gap:12px;min-width:0;}"], SANS, SANS); const TimeRecordDialog = _ref11 => { let open = _ref11.open, taskName = _ref11.taskName, users = _ref11.users, currentUserId = _ref11.currentUserId, records = _ref11.records, total = _ref11.total, onSubmit = _ref11.onSubmit, onClose = _ref11.onClose; const _useState7 = useState(""), value = _useState7[0], setValue = _useState7[1]; const _useState8 = useState(todayUnix()), date = _useState8[0], setDate = _useState8[1]; const _useState9 = useState(currentUserId), userId = _useState9[0], setUserId = _useState9[1]; const _useState0 = useState("devops"), jobTypeId = _useState0[0], setJobTypeId = _useState0[1]; const _useState1 = useState(false), billable = _useState1[0], setBillable = _useState1[1]; const _useState10 = useState(true), showDescription = _useState10[0], setShowDescription = _useState10[1]; const _useState11 = useState(""), description = _useState11[0], setDescription = _useState11[1]; const _useState12 = useState(false), showRecords = _useState12[0], setShowRecords = _useState12[1]; const user = users.find(item => item.id === userId); const jobType = JOB_TYPES.find(item => item.id === jobTypeId); /** "1:30" / "1.5" → decimal hours */ const hours = useMemo(() => { const trimmed = value.trim(); if (!trimmed) { return 0; } if (trimmed.includes(":")) { const _trimmed$split = trimmed.split(":"), h = _trimmed$split[0], m = _trimmed$split[1]; return Number(h || 0) + Number(m || 0) / 60; } return Number(trimmed) || 0; }, [value]); const handleSubmit = useCallback(() => { var _user$name, _jobType$name; onSubmit({ date, userId, userName: (_user$name = user == null ? void 0 : user.name) != null ? _user$name : "", jobType: (_jobType$name = jobType == null ? void 0 : jobType.name) != null ? _jobType$name : "", hours, billable, description }); }, [billable, date, description, hours, jobType, onSubmit, user, userId]); return /*#__PURE__*/React.createElement(Modal, { open: open, onClose: onClose }, /*#__PURE__*/React.createElement(SlideFromTop, { in: open }, /*#__PURE__*/React.createElement(StyledTimeRecordPaper, { role: "dialog", "aria-label": "Add time record" }, /*#__PURE__*/React.createElement("div", { className: "trd-header" }, /*#__PURE__*/React.createElement("span", { className: "trd-title" }, taskName), /*#__PURE__*/React.createElement("span", { className: "trd-date" }, /*#__PURE__*/React.createElement(DatePicker, { mode: "daily", required: true, firstDayOfWeek: 1, position: "bottom-end", selected: { from: date, to: date }, onSave: range => range != null && range.from ? setDate(Number(range.from)) : undefined, target: /*#__PURE__*/React.createElement(SelectTrigger, null, formatDateRange(date, date)) }))), /*#__PURE__*/React.createElement("div", { className: "trd-divider" }), /*#__PURE__*/React.createElement("div", { className: "trd-body" }, /*#__PURE__*/React.createElement("div", { className: "trd-form" }, /*#__PURE__*/React.createElement("div", { className: "trd-fields" }, /*#__PURE__*/React.createElement("span", { className: "trd-hours" }, /*#__PURE__*/React.createElement(InputHours, { autoFocus: true, allowEmptyValue: true, size: "biggest", placeholder: "00:00", value: value, onChange: setValue })), /*#__PURE__*/React.createElement("span", { className: "trd-field" }, /*#__PURE__*/React.createElement(Label, { size: "small" }, "User"), /*#__PURE__*/React.createElement(Select, { options: users, selected: userId, forceCloseMenu: true, onChange: next => setUserId(String(next)), target: /*#__PURE__*/React.createElement(SelectTrigger, { id: "trd-user" }, user == null ? void 0 : user.name) })), /*#__PURE__*/React.createElement("span", { className: "trd-field" }, /*#__PURE__*/React.createElement(Label, { size: "small" }, "Job Type"), /*#__PURE__*/React.createElement(Select, { options: JOB_TYPES, selected: jobTypeId, forceCloseMenu: true, onChange: next => setJobTypeId(String(next)), target: /*#__PURE__*/React.createElement(SelectTrigger, { id: "trd-job-type" }, jobType == null ? void 0 : jobType.name) })), /*#__PURE__*/React.createElement("span", { className: "trd-billable" }, /*#__PURE__*/React.createElement(Checkbox, { id: "trd-billable", checked: billable, onChange: () => setBillable(prev => !prev) }), /*#__PURE__*/React.createElement(Label, { htmlFor: "trd-billable", weight: "regular" }, "Billable"))), /*#__PURE__*/React.createElement("button", { type: "button", className: showDescription ? "trd-description-trigger" : "trd-description-trigger collapsed", onClick: () => setShowDescription(prev => !prev) }, /*#__PURE__*/React.createElement(CollapseExpandSingleIcon, null), /*#__PURE__*/React.createElement("span", null, "Description")), showDescription ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Textarea, { className: "trd-textarea", value: description, onChange: event => setDescription(event.target.value) })) : null, /*#__PURE__*/React.createElement("div", { className: "trd-actions" }, /*#__PURE__*/React.createElement(Button, { variant: "primary", disabled: hours <= 0, onClick: handleSubmit }, "Add Time Record"), /*#__PURE__*/React.createElement(Button, { variant: "secondary", onClick: onClose }, "Cancel"))), total > 0 ? /*#__PURE__*/React.createElement("button", { type: "button", className: showRecords ? "trd-total" : "trd-total collapsed", onClick: () => setShowRecords(prev => !prev) }, /*#__PURE__*/React.createElement(CollapseExpandSingleIcon, null), /*#__PURE__*/React.createElement(Header3, { color: "theme-secondary", tabularNums: true }, "Total: ", formatHours(total, false, true), "h")) : null), showRecords && records.length > 0 ? /*#__PURE__*/React.createElement("div", { className: "trd-records" }, records.map(record => /*#__PURE__*/React.createElement("div", { key: record.id, className: "trd-record" }, /*#__PURE__*/React.createElement("span", { className: "trd-record-left" }, /*#__PURE__*/React.createElement(Caption1, { weight: "bold", color: "secondary" }, formatDateRange(record.date, record.date)), /*#__PURE__*/React.createElement(Body2, null, record.userName), /*#__PURE__*/React.createElement(Caption1, { color: "secondary" }, record.jobType)), /*#__PURE__*/React.createElement(Body2, { weight: "bold", tabularNums: true }, formatHours(record.hours), "h")))) : null))); }; //# sourceMappingURL=columns.js.map