UNPKG

@activecollab/components

Version:

ActiveCollab Components

1,042 lines (1,007 loc) 46.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isOverdue = exports.formatDateRange = exports.dayOffset = exports.TimeCell = exports.LabelsProvider = exports.LabelCell = exports.IdentifierCell = exports.DateCell = void 0; var _react = _interopRequireWildcard(require("react")); var _momentTimezone = _interopRequireDefault(require("moment-timezone")); var _styledComponents = _interopRequireWildcard(require("styled-components")); var _Button = require("../../components/Button"); var _Checkbox = require("../../components/Checkbox"); var _Chip = require("../../components/Chip"); var _DatePicker = require("../../components/DatePicker"); var _Dot = require("../../components/Dot"); var _Icons = require("../../components/Icons"); var _Input = require("../../components/Input"); var _Label = require("../../components/Label"); var _Modal = require("../../components/Modal"); var _ProgressPie = require("../../components/ProgressPie"); var _Select = require("../../components/Select"); var _SelectTrigger = require("../../components/SelectTrigger"); var _Textarea = require("../../components/Textarea"); var _Transitions = require("../../components/Transitions"); var _Typography = require("../../components/Typography"); var _utils = require("../../utils"); var _excluded = ["open", "className", "children"], _excluded2 = ["value", "tooltip"], _excluded3 = ["labels"], _excluded4 = ["value", "tooltip", "overdue"]; 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 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); } 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 _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; } function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; } /** * 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. */ var 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. */ var CooltipStyle = (0, _styledComponents.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. */ var ListButtonBase = /*#__PURE__*/(0, _react.forwardRef)(function (_ref, ref) { var open = _ref.open, className = _ref.className, children = _ref.children, rest = _objectWithoutProperties(_ref, _excluded); return /*#__PURE__*/_react.default.createElement(_Button.Button, _extends({}, rest, { ref: ref, type: "button", variant: "text gray", active: open, className: "list-button".concat(className ? " ".concat(className) : "").concat(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. */ var StyledListButton = (0, _styledComponents.default)(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) */ /* ================================================================== */ var StyledIdValue = _styledComponents.default.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. */ var IdTrigger = /*#__PURE__*/(0, _react.forwardRef)(function (_ref2, ref) { var value = _ref2.value, tooltip = _ref2.tooltip, rest = _objectWithoutProperties(_ref2, _excluded2); return /*#__PURE__*/_react.default.createElement(StyledListButton, _extends({}, rest, { ref: ref, className: "show-on-hover has-value", "aria-label": "Copy link to task ".concat(value), "data-tooltip": tooltip }), /*#__PURE__*/_react.default.createElement(StyledIdValue, null, /*#__PURE__*/_react.default.createElement(_Typography.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. */ var IdentifierCell = exports.IdentifierCell = function IdentifierCell(_ref3) { var identifier = _ref3.identifier, link = _ref3.link, onCopy = _ref3.onCopy; var handleClick = (0, _react.useCallback)(function (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 !== void 0 && _navigator$clipboard.writeText) { navigator.clipboard.writeText(link).then(function () { return onCopy === null || onCopy === void 0 ? void 0 : onCopy(true); }).catch(function () { return onCopy === null || onCopy === void 0 ? void 0 : onCopy(false); }); return; } onCopy === null || onCopy === void 0 || onCopy(false); }, [link, onCopy]); return /*#__PURE__*/_react.default.createElement("div", { className: "col col-identifier" }, /*#__PURE__*/_react.default.createElement(IdTrigger, { value: "#".concat(identifier), tooltip: "Copy Link", onClick: handleClick })); }; /* ================================================================== */ /* Labels */ /* ================================================================== */ /* The project's label catalogue. Names are uppercase, like in the app. */ var 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. */ var NEW_LABEL_COLORS = [{ color: "#F9E0F2", textColor: "#A4569B" }, { color: "#DDEBFB", textColor: "#4E7BA8" }, { color: "#E4F6D8", textColor: "#5F9440" }, { color: "#FDE2D5", textColor: "#B36A45" }]; var LabelsContext = /*#__PURE__*/(0, _react.createContext)({ labels: INITIAL_LABELS, labelsById: {}, addLabel: function addLabel() { return ""; } }); /** * Holds the label catalogue for the whole list, so a label created in one * task's picker shows up in every other picker too. */ var LabelsProvider = exports.LabelsProvider = function LabelsProvider(_ref4) { var children = _ref4.children; var _useState = (0, _react.useState)(INITIAL_LABELS), _useState2 = _slicedToArray(_useState, 2), labels = _useState2[0], setLabels = _useState2[1]; var addLabel = (0, _react.useCallback)(function (name) { var trimmed = name.trim().toUpperCase(); var id = "label-".concat(trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "-")); setLabels(function (prev) { if (prev.some(function (label) { return label.id === id; })) { return prev; } var palette = NEW_LABEL_COLORS[prev.length % NEW_LABEL_COLORS.length]; return [...prev, _objectSpread({ id, name: trimmed }, palette)]; }); return id; }, []); var value = (0, _react.useMemo)(function () { return { labels, labelsById: labels.reduce(function (acc, label) { return _objectSpread(_objectSpread({}, acc), {}, { [label.id]: label }); }, {}), addLabel }; }, [addLabel, labels]); return /*#__PURE__*/_react.default.createElement(LabelsContext.Provider, { value: value }, children); }; var useTaskLabels = function useTaskLabels() { return (0, _react.useContext)(LabelsContext); }; /* Empty state pill (mirrors EntityLabel + .entity-label in list-view.less) */ var StyledLabelPlaceholder = _styledComponents.default.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));}"]); var StyledLabelItems = _styledComponents.default.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); var MAX_SHOWN_LABELS = 8; var LabelItems = function LabelItems(_ref5) { var labels = _ref5.labels; var showAsDot = labels.length > 2; var shown = labels; var 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.default.createElement(StyledLabelItems, null, shown.map(function (label) { return showAsDot ? /*#__PURE__*/_react.default.createElement(_Dot.Dot, { key: label.id, color: label.color, size: 20, "data-tooltip": label.name }) : /*#__PURE__*/_react.default.createElement(_Chip.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.default.createElement(_Typography.Body2, { weight: "bold", className: "rest-count" }, "+", restCount) : null); }; var LabelTrigger = /*#__PURE__*/(0, _react.forwardRef)(function (_ref6, ref) { var labels = _ref6.labels, rest = _objectWithoutProperties(_ref6, _excluded3); if (labels.length === 0) { return /*#__PURE__*/_react.default.createElement(StyledListButton, _extends({}, rest, { ref: ref, className: "show-on-hover", "aria-label": "Add labels", "data-tooltip": "Add Labels" }), /*#__PURE__*/_react.default.createElement(StyledLabelPlaceholder, null, /*#__PURE__*/_react.default.createElement(_Typography.Caption2, { weight: "bold", color: "tertiary" }, "LABEL"))); } return /*#__PURE__*/_react.default.createElement(StyledListButton, _extends({}, rest, { ref: ref, "aria-label": "Change labels" }), /*#__PURE__*/_react.default.createElement(LabelItems, { labels: labels })); }); LabelTrigger.displayName = "LabelTrigger"; var LabelCell = exports.LabelCell = function LabelCell(_ref7) { var labelIds = _ref7.labelIds, _ref7$completed = _ref7.completed, completed = _ref7$completed === void 0 ? false : _ref7$completed; var _useTaskLabels = useTaskLabels(), catalog = _useTaskLabels.labels, labelsById = _useTaskLabels.labelsById, addLabel = _useTaskLabels.addLabel; var _useState3 = (0, _react.useState)(labelIds !== null && labelIds !== void 0 ? labelIds : []), _useState4 = _slicedToArray(_useState3, 2), selected = _useState4[0], setSelected = _useState4[1]; /* alphabetical, like LabelTrigger sorts before rendering */ var selectedLabels = (0, _react.useMemo)(function () { return selected.map(function (id) { return labelsById[id]; }).filter(function (label) { return Boolean(label); }).sort(function (a, b) { return a.name.localeCompare(b.name); }); }, [labelsById, selected]); var options = (0, _react.useMemo)(function () { return catalog.map(function (label) { return { id: label.id, name: label.name, color: label.color, textColor: label.textColor }; }); }, [catalog]); var handleChange = (0, _react.useCallback)(function (value) { setSelected(Array.isArray(value) ? value.map(String) : [String(value)].filter(Boolean)); }, []); var handleAddNew = (0, _react.useCallback)(function (name) { if (!name || !name.trim()) { return; } var id = addLabel(name); setSelected(function (prev) { return prev.includes(id) ? prev : [...prev, id]; }); }, [addLabel]); /* Completed tasks show their labels read-only (mirrors Label.js) */ if (completed) { return /*#__PURE__*/_react.default.createElement(StyledReadOnlyCell, { className: "col col-label" }, selectedLabels.length > 0 ? /*#__PURE__*/_react.default.createElement(LabelItems, { labels: selectedLabels }) : null); } return /*#__PURE__*/_react.default.createElement("div", { className: "col col-label" }, /*#__PURE__*/_react.default.createElement(_Select.Select, { type: "multiple", options: options, selected: selected, onChange: handleChange, emptyValue: "Add new", emptyAction: handleAddNew, autoHeightMax: 278, position: "bottom-end", target: /*#__PURE__*/_react.default.createElement(LabelTrigger, { labels: selectedLabels }) })); }; /* ================================================================== */ /* Date */ /* ================================================================== */ /** The personalized date format the app defaults to (`MMM D. YYYY`). */ var DATE_FORMAT = "MMM D. YYYY"; /** Midnight UTC today — the reference point for the mock data and overdue. */ var todayUnix = function todayUnix() { return _momentTimezone.default.utc().startOf("day").unix(); }; /** Build a mock timestamp N days from today (negative = in the past). */ var dayOffset = exports.dayOffset = function dayOffset(offset) { return _momentTimezone.default.utc().startOf("day").add(offset, "days").unix(); }; /** mirrors utils/date.js → formatDateRange (month-first format) */ var formatDateRange = exports.formatDateRange = function formatDateRange(from, to) { if (from === undefined || from === null) { return undefined; } var start = _momentTimezone.default.unix(from).utc(); var due = _momentTimezone.default.unix(to !== null && to !== void 0 ? to : from).utc(); var withoutYear = DATE_FORMAT.replace(/YYYY|,$/gi, "").trim(); var 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 "".concat(start.format(withoutYear), " - ").concat(due.format(withoutYearAndMonth)); } return "".concat(start.format(withoutYear), " - ").concat(due.format(withoutYear)); }; /** mirrors utils/date.js → formatDateSmart(…, long) */ var formatDateLong = function formatDateLong(value) { return value ? _momentTimezone.default.unix(value).utc().format(DATE_FORMAT) : ""; }; var isOverdue = exports.isOverdue = function isOverdue(dueOn, completed) { return Boolean(dueOn && !completed && dueOn < todayUnix()); }; var StyledDateValue = _styledComponents.default.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 */ var StyledReadOnlyCell = _styledComponents.default.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. */ var StyledPickerCell = _styledComponents.default.div.withConfig({ displayName: "columns__StyledPickerCell", componentId: "sc-9f4385-6" })(["> div{text-align:right;}"]); var DateTrigger = /*#__PURE__*/(0, _react.forwardRef)(function (_ref8, ref) { var value = _ref8.value, tooltip = _ref8.tooltip, overdue = _ref8.overdue, rest = _objectWithoutProperties(_ref8, _excluded4); return /*#__PURE__*/_react.default.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.default.createElement(StyledDateValue, null, /*#__PURE__*/_react.default.createElement(_Typography.Caption1, { weight: "bold", color: overdue ? "alert" : "secondary" }, value)) : /*#__PURE__*/_react.default.createElement(_Icons.CalendarIcon, null)); }); DateTrigger.displayName = "DateTrigger"; var DateCell = exports.DateCell = function DateCell(_ref9) { var _dates$from, _dates$to2; var startOn = _ref9.startOn, dueOn = _ref9.dueOn, _ref9$completed = _ref9.completed, completed = _ref9$completed === void 0 ? false : _ref9$completed; var _useState5 = (0, _react.useState)({ from: startOn, to: dueOn }), _useState6 = _slicedToArray(_useState5, 2), dates = _useState6[0], setDates = _useState6[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. */ var _useState7 = (0, _react.useState)(false), _useState8 = _slicedToArray(_useState7, 2), pickerMounted = _useState8[0], setPickerMounted = _useState8[1]; /* moment isn't cheap either; only reformat when the dates actually change */ var _useMemo = (0, _react.useMemo)(function () { var _dates$to; var formatted = formatDateRange(dates.from, dates.to); var hint = "Add Date"; if (dates.from) { hint = !dates.to || dates.to === dates.from ? formatDateLong(dates.from) : "".concat(formatDateLong(dates.from), " - ").concat(formatDateLong(dates.to)); } return { value: formatted, overdue: isOverdue((_dates$to = dates.to) !== null && _dates$to !== void 0 ? _dates$to : dates.from, completed), tooltip: hint }; }, [completed, dates.from, dates.to]), value = _useMemo.value, overdue = _useMemo.overdue, tooltip = _useMemo.tooltip; var handleSave = (0, _react.useCallback)(function (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) }); }, []); var handleClear = (0, _react.useCallback)(function () { return setDates({}); }, []); var handleCalendarToggle = (0, _react.useCallback)(function (next) { if (!next) { setPickerMounted(false); } }, []); /* Completed tasks show the date read-only (mirrors Date.js) */ if (completed) { return /*#__PURE__*/_react.default.createElement(StyledReadOnlyCell, { className: "col col-date" }, value ? /*#__PURE__*/_react.default.createElement(StyledDateValue, { "data-tooltip": tooltip }, /*#__PURE__*/_react.default.createElement(_Typography.Caption1, { weight: "bold", color: overdue ? "alert" : "secondary" }, value)) : null); } if (!pickerMounted) { return /*#__PURE__*/_react.default.createElement("div", { className: "col col-date" }, /*#__PURE__*/_react.default.createElement(DateTrigger, { value: value, tooltip: tooltip, overdue: overdue, onClick: function onClick() { return setPickerMounted(true); } })); } return /*#__PURE__*/_react.default.createElement(StyledPickerCell, { className: "col col-date" }, /*#__PURE__*/_react.default.createElement(_DatePicker.DatePicker, { open: true, mode: "custom", instant: false, maxRange: 366, firstDayOfWeek: 1, position: "bottom-end", selected: { from: (_dates$from = dates.from) !== null && _dates$from !== void 0 ? _dates$from : null, to: (_dates$to2 = dates.to) !== null && _dates$to2 !== void 0 ? _dates$to2 : null }, onSave: handleSave, onClear: handleClear, onCalendarToggle: handleCalendarToggle, target: /*#__PURE__*/_react.default.createElement(DateTrigger, { value: value, tooltip: tooltip, overdue: overdue }) })); }; /* ================================================================== */ /* Time */ /* ================================================================== */ /** mirrors utils/utils.jsx → calculateTimeToEstimatePercentage */ var timeToEstimatePercentage = function timeToEstimatePercentage(tracked, estimated) { if (estimated > 0) { return tracked > 0 ? Math.round(tracked / estimated * 100) : 0; } return null; }; /** mirrors utils/utils.jsx → formatTrackedAndEstimatedTime */ var formatTrackedAndEstimated = function formatTrackedAndEstimated(tracked, estimated) { return estimated > 0 ? "".concat((0, _utils.formatHours)(tracked, false, false, "short"), "/").concat((0, _utils.formatHours)(estimated, false, false, "short")) : (0, _utils.formatHours)(tracked, false, false, "short"); }; /* mirrors Time.js → progressColor() */ var progressColor = function progressColor(percent) { if (percent >= 100) { return "var(--red-alert)"; } if (percent >= 75) { return "var(--warning)"; } return "var(--color-sucess-green)"; }; var StyledTimeValue = _styledComponents.default.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); var TimeValue = function TimeValue(_ref0) { var tracked = _ref0.tracked, estimated = _ref0.estimated; var percent = timeToEstimatePercentage(tracked, estimated); return /*#__PURE__*/_react.default.createElement(StyledTimeValue, null, estimated > 0 ? /*#__PURE__*/_react.default.createElement(_ProgressPie.ProgressPie, { className: "time-pie", radius: 10, progress: percent !== null && percent !== void 0 ? percent : 0, progressColor: progressColor(percent !== null && percent !== void 0 ? percent : 0), backgroundColor: "var(--color-theme-300)" }) : null, /*#__PURE__*/_react.default.createElement(_Typography.Caption1, { weight: "bold" }, formatTrackedAndEstimated(tracked, estimated))); }; var TimeTrigger = function TimeTrigger(_ref1) { var tracked = _ref1.tracked, estimated = _ref1.estimated, onClick = _ref1.onClick; var hasTime = tracked > 0 || estimated > 0; return /*#__PURE__*/_react.default.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.default.createElement(TimeValue, { tracked: tracked, estimated: estimated }) : /*#__PURE__*/_react.default.createElement(_Icons.ClockAddIcon, null)); }; var TimeCell = exports.TimeCell = function TimeCell(_ref10) { var 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; var _useState9 = (0, _react.useState)(false), _useState0 = _slicedToArray(_useState9, 2), open = _useState0[0], setOpen = _useState0[1]; var _useState1 = (0, _react.useState)(function () { var _users$find$name, _users$find; return tracked > 0 ? [{ id: 1, date: dayOffset(-3), userId: currentUserId, userName: (_users$find$name = (_users$find = users.find(function (user) { return user.id === currentUserId; })) === null || _users$find === void 0 ? void 0 : _users$find.name) !== null && _users$find$name !== void 0 ? _users$find$name : "You", jobType: "Design", hours: tracked, billable: true }] : []; }), _useState10 = _slicedToArray(_useState1, 2), records = _useState10[0], setRecords = _useState10[1]; var total = (0, _react.useMemo)(function () { return records.reduce(function (sum, record) { return sum + record.hours; }, 0); }, [records]); var handleAdd = (0, _react.useCallback)(function (record) { setRecords(function (prev) { return [_objectSpread(_objectSpread({}, 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.default.createElement(StyledReadOnlyCell, { className: "col col-time" }, total > 0 || estimated > 0 ? /*#__PURE__*/_react.default.createElement(TimeValue, { tracked: total, estimated: estimated }) : null); } return /*#__PURE__*/_react.default.createElement("div", { className: "col col-time" }, /*#__PURE__*/_react.default.createElement(TimeTrigger, { tracked: total, estimated: estimated, onClick: function onClick() { return setOpen(true); } }), open ? /*#__PURE__*/_react.default.createElement(TimeRecordDialog, { open: open, taskName: taskName, users: users, currentUserId: currentUserId, records: records, total: total, onSubmit: handleAdd, onClose: function onClose() { return setOpen(false); } }) : null); }; /* ------------------------------------------------------------------ */ /* Add Time Record dialog (mirrors TimeRecordForm/*) */ /* ------------------------------------------------------------------ */ var 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`). */ var StyledTimeRecordPaper = _styledComponents.default.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); var TimeRecordDialog = function TimeRecordDialog(_ref11) { var open = _ref11.open, taskName = _ref11.taskName, users = _ref11.users, currentUserId = _ref11.currentUserId, records = _ref11.records, total = _ref11.total, onSubmit = _ref11.onSubmit, onClose = _ref11.onClose; var _useState11 = (0, _react.useState)(""), _useState12 = _slicedToArray(_useState11, 2), value = _useState12[0], setValue = _useState12[1]; var _useState13 = (0, _react.useState)(todayUnix()), _useState14 = _slicedToArray(_useState13, 2), date = _useState14[0], setDate = _useState14[1]; var _useState15 = (0, _react.useState)(currentUserId), _useState16 = _slicedToArray(_useState15, 2), userId = _useState16[0], setUserId = _useState16[1]; var _useState17 = (0, _react.useState)("devops"), _useState18 = _slicedToArray(_useState17, 2), jobTypeId = _useState18[0], setJobTypeId = _useState18[1]; var _useState19 = (0, _react.useState)(false), _useState20 = _slicedToArray(_useState19, 2), billable = _useState20[0], setBillable = _useState20[1]; var _useState21 = (0, _react.useState)(true), _useState22 = _slicedToArray(_useState21, 2), showDescription = _useState22[0], setShowDescription = _useState22[1]; var _useState23 = (0, _react.useState)(""), _useState24 = _slicedToArray(_useState23, 2), description = _useState24[0], setDescription = _useState24[1]; var _useState25 = (0, _react.useState)(false), _useState26 = _slicedToArray(_useState25, 2), showRecords = _useState26[0], setShowRecords = _useState26[1]; var user = users.find(function (item) { return item.id === userId; }); var jobType = JOB_TYPES.find(function (item) { return item.id === jobTypeId; }); /** "1:30" / "1.5" → decimal hours */ var hours = (0, _react.useMemo)(function () { var trimmed = value.trim(); if (!trimmed) { return 0; } if (trimmed.includes(":")) { var _trimmed$split = trimmed.split(":"), _trimmed$split2 = _slicedToArray(_trimmed$split, 2), h = _trimmed$split2[0], m = _trimmed$split2[1]; return Number(h || 0) + Number(m || 0) / 60; } return Number(trimmed) || 0; }, [value]); var handleSubmit = (0, _react.useCallback)(function () { var _user$name, _jobType$name; onSubmit({ date, userId, userName: (_user$name = user === null || user === void 0 ? void 0 : user.name) !== null && _user$name !== void 0 ? _user$name : "", jobType: (_jobType$name = jobType === null || jobType === void 0 ? void 0 : jobType.name) !== null && _jobType$name !== void 0 ? _jobType$name : "", hours, billable, description }); }, [billable, date, description, hours, jobType, onSubmit, user, userId]); return /*#__PURE__*/_react.default.createElement(_Modal.Modal, { open: open, onClose: onClose }, /*#__PURE__*/_react.default.createElement(_Transitions.SlideFromTop, { in: open }, /*#__PURE__*/_react.default.createElement(StyledTimeRecordPaper, { role: "dialog", "aria-label": "Add time record" }, /*#__PURE__*/_react.default.createElement("div", { className: "trd-header" }, /*#__PURE__*/_react.default.createElement("span", { className: "trd-title" }, taskName), /*#__PURE__*/_react.default.createElement("span", { className: "trd-date" }, /*#__PURE__*/_react.default.createElement(_DatePicker.DatePicker, { mode: "daily", required: true, firstDayOfWeek: 1, position: "bottom-end", selected: { from: date, to: date }, onSave: function onSave(range) { return range !== null && range !== void 0 && range.from ? setDate(Number(range.from)) : undefined; }, target: /*#__PURE__*/_react.default.createElement(_SelectTrigger.SelectTrigger, null, formatDateRange(date, date)) }))), /*#__PURE__*/_react.default.createElement("div", { className: "trd-divider" }), /*#__PURE__*/_react.default.createElement("div", { className: "trd-body" }, /*#__PURE__*/_react.default.createElement("div", { className: "trd-form" }, /*#__PURE__*/_react.default.createElement("div", { className: "trd-fields" }, /*#__PURE__*/_react.default.createElement("span", { className: "trd-hours" }, /*#__PURE__*/_react.default.createElement(_Input.InputHours, { autoFocus: true, allowEmptyValue: true, size: "biggest", placeholder: "00:00", value: value, onChange: setValue })), /*#__PURE__*/_react.default.createElement("span", { className: "trd-field" }, /*#__PURE__*/_react.default.createElement(_Label.Label, { size: "small" }, "User"), /*#__PURE__*/_react.default.createElement(_Select.Select, { options: users, selected: userId, forceCloseMenu: true, onChange: function onChange(next) { return setUserId(String(next)); }, target: /*#__PURE__*/_react.default.createElement(_SelectTrigger.SelectTrigger, { id: "trd-user" }, user === null || user === void 0 ? void 0 : user.name) })), /*#__PURE__*/_react.default.createElement("span", { className: "trd-field" }, /*#__PURE__*/_react.default.createElement(_Label.Label, { size: "small" }, "Job Type"), /*#__PURE__*/_react.default.createElement(_Select.Select, { options: JOB_TYPES, selected: jobTypeId, forceCloseMenu: true, onChange: function onChange(next) { return setJobTypeId(String(next)); }, target: /*#__PURE__*/_react.default.createElement(_SelectTrigger.SelectTrigger, { id: "trd-job-type" }, jobType === null || jobType === void 0 ? void 0 : jobType.name) })), /*#__PURE__*/_react.default.createElement("span", { className: "trd-billable" }, /*#__PURE__*/_react.default.createElement(_Checkbox.Checkbox, { id: "trd-billable", checked: billable, onChange: function onChange() { return setBillable(function (prev) { return !prev; }); } }), /*#__PURE__*/_react.default.createElement(_Label.Label, { htmlFor: "trd-billable", weight: "regular" }, "Billable"))), /*#__PURE__*/_react.default.createElement("button", { type: "button", className: showDescription ? "trd-description-trigger" : "trd-description-trigger collapsed", onClick: function onClick() { return setShowDescription(function (prev) { return !prev; }); } }, /*#__PURE__*/_react.default.createElement(_Icons.CollapseExpandSingleIcon, null), /*#__PURE__*/_react.default.createElement("span", null, "Description")), showDescription ? /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(_Textarea.Textarea, { className: "trd-textarea", value: description, onChange: function onChange(event) { return setDescription(event.target.value); } })) : null, /*#__PURE__*/_react.default.createElement("div", { className: "trd-actions" }, /*#__PURE__*/_react.default.createElement(_Button.Button, { variant: "primary", disabled: hours <= 0, onClick: handleSubmit }, "Add Time Record"), /*#__PURE__*/_react.default.createElement(_Button.Button, { variant: "secondary", onClick: onClose }, "Cancel"))), total > 0 ? /*#__PURE__*/_react.default.createElement("button", { type: "button", className: showRecords ? "trd-total" : "trd-total collapsed", onClick: function onClick() { return setShowRecords(function (prev) { return !prev; }); } }, /*#__PURE__*/_react.default.createElement(_Icons.CollapseExpandSingleIcon, null), /*#__PURE__*/_react.default.createElement(_Typography.Header3, { color: "theme-secondary", tabularNums: true }, "Total: ", (0, _utils.formatHours)(total, false, true), "h")) : null), showRecords && records.length > 0 ? /*#__PURE__*/_react.default.createElement("div", { className: "trd-records" }, records.map(function (record) { return /*#__PURE__*/_react.default.createElement("div", { key: record.id, className: "trd-record" }, /*#__PURE__*/_react.default.createElement("span", { className: "trd-record-left" }, /*#__PURE__*/_react.default.createElement(_Typography.Caption1, { weight: "bold", color: "secondary" }, formatDateRange(record.date, record.date)), /*#__PURE__*/_react.default.createElement(_Typography.Body2, null, record.userName), /*#__PURE__*/_react.default.createElement(_Typography.Caption1, { color: "secondary" }, record.jobType)), /*#__PURE__*/_react.default.createElement(_Typography.Body2, { weight: "bold", tabularNums: true }, (0, _utils.formatHours)(record.hours), "h")); })) : null))); }; //# sourceMappingURL=columns.js.map