@activecollab/components
Version:
ActiveCollab Components
926 lines (891 loc) • 37.5 kB
JavaScript
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