@activecollab/components
Version:
ActiveCollab Components
1,042 lines (1,007 loc) • 46.9 kB
JavaScript
"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