@activecollab/components 2.0.365 → 2.0.367

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,926 @@
1
+ import _extends from "@babel/runtime/helpers/esm/extends";
2
+ import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
3
+ const _excluded = ["open", "className", "children"],
4
+ _excluded2 = ["value", "tooltip"],
5
+ _excluded3 = ["labels"],
6
+ _excluded4 = ["value", "tooltip", "overdue"];
7
+ import React, { createContext, forwardRef, useCallback, useContext, useMemo, useState } from "react";
8
+ import moment from "moment-timezone";
9
+ import styled, { css } from "styled-components";
10
+ import { Button } from "../../components/Button";
11
+ import { Checkbox } from "../../components/Checkbox";
12
+ import { Chip } from "../../components/Chip";
13
+ import { DatePicker } from "../../components/DatePicker";
14
+ import { Dot } from "../../components/Dot";
15
+ import { CalendarIcon, ClockAddIcon, CollapseExpandSingleIcon } from "../../components/Icons";
16
+ import { InputHours } from "../../components/Input";
17
+ import { Label } from "../../components/Label";
18
+ import { Modal } from "../../components/Modal";
19
+ import { ProgressPie } from "../../components/ProgressPie";
20
+ import { Select } from "../../components/Select";
21
+ import { SelectTrigger } from "../../components/SelectTrigger";
22
+ import { Textarea } from "../../components/Textarea";
23
+ import { SlideFromTop } from "../../components/Transitions";
24
+ import { Body2, Caption1, Caption2, Header3 } from "../../components/Typography";
25
+ import { formatHours } from "../../utils";
26
+
27
+ /**
28
+ * Interactive list-view column cells for the `Presentation/List view` and
29
+ * `Presentation/List View ID Column` stories — the Label, Date and Time cells
30
+ * with their editors, mirroring
31
+ * `new-activecollab-front/src/components/ListView/columns/*`, plus an
32
+ * Identifier cell that only the ID-column story uses (it has no counterpart in
33
+ * the app yet):
34
+ *
35
+ * - **Identifier** (no app equivalent): `#<id>` as a Copy Link trigger, styled
36
+ * like the Date trigger. One typography at every length — the column's width
37
+ * is what adapts to how large the identifiers get.
38
+ * - **Label** (`columns/Label.js` + `LabelTrigger`/`LabelItems`): chips for 1–2
39
+ * labels, dots from 3 up, `+N` past 8, and a DS `Select` picker (search,
40
+ * colored dot + label name in the label's own colour, checkbox, "+ Add new").
41
+ * - **Date** (`columns/Date.js`): a range trigger that formats like the app
42
+ * (`formatDateRange`), turns red when overdue, and opens the DS `DatePicker`
43
+ * in atomic mode so it gets the Save / ✕ / Clear footer.
44
+ * - **Time** (`columns/Time.js`): `ProgressPie` + `tracked/estimated`, coloured
45
+ * by how much of the estimate is used, opening the "Add Time Record" dialog.
46
+ *
47
+ * Everything is local state — there is no store, so edits live as long as the
48
+ * story is mounted.
49
+ */
50
+
51
+ const SANS = '-apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Helvetica Neue", Arial, sans-serif';
52
+
53
+ /**
54
+ * CSS-only tooltip, mirroring the app's `.c-cooltip.c-cooltip--top` (see
55
+ * `styles/c_cooltip.less`). The list renders hundreds of these, and the DS
56
+ * `Tooltip` mounts a Portal + Popper per instance — far too heavy per cell — so
57
+ * the cells use `data-tooltip` exactly like the real list view does.
58
+ */
59
+ 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);
60
+
61
+ /* ================================================================== */
62
+ /* Shared trigger button (mirrors ListView/ListButton.js) */
63
+ /* ================================================================== */
64
+
65
+ /**
66
+ * `Menu` clones its `target` and injects `open`, so — like the app's
67
+ * `ListButton` — the trigger has to swallow that prop and turn it into the DS
68
+ * `active` state plus an `.open` class the row CSS uses to keep the trigger
69
+ * visible while its menu is open.
70
+ */
71
+ const ListButtonBase = /*#__PURE__*/forwardRef((_ref, ref) => {
72
+ let open = _ref.open,
73
+ className = _ref.className,
74
+ children = _ref.children,
75
+ rest = _objectWithoutPropertiesLoose(_ref, _excluded);
76
+ return /*#__PURE__*/React.createElement(Button, _extends({}, rest, {
77
+ ref: ref,
78
+ type: "button",
79
+ variant: "text gray",
80
+ active: open,
81
+ className: "list-button" + (className ? " " + className : "") + (open ? " open" : "")
82
+ }), children);
83
+ });
84
+ ListButtonBase.displayName = "ListButtonBase";
85
+
86
+ /*
87
+ * The DS Button treats a single element child as "icon only" — inline-flex,
88
+ * centered, a fixed 32px wide and `overflow: hidden`, which squashes a date or
89
+ * a chip. `&&` doubles the specificity so these rules win over that block; the
90
+ * button then hugs its content and the column's `justify-content: flex-end`
91
+ * parks it against the right edge.
92
+ */
93
+ const StyledListButton = styled(ListButtonBase).withConfig({
94
+ displayName: "columns__StyledListButton",
95
+ componentId: "sc-9f4385-0"
96
+ })(["&&{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);
97
+
98
+ /* ================================================================== */
99
+ /* Identifier (task ID) */
100
+ /* ================================================================== */
101
+
102
+ const StyledIdValue = styled.span.withConfig({
103
+ displayName: "columns__StyledIdValue",
104
+ componentId: "sc-9f4385-1"
105
+ })(["font-family:", ";white-space:nowrap;font-variant-numeric:tabular-nums;"], SANS);
106
+ /**
107
+ * Same trigger the Date cell uses, so the id gets the identical hover outline
108
+ * and 8px inset — it is a link, not an editor, so it is always visible
109
+ * (`has-value`) rather than appearing on row hover.
110
+ *
111
+ * The typography is Caption 1 bold at every identifier length, like the Date
112
+ * value next to it; long identifiers are handled by the column's width tier,
113
+ * not by shrinking the text.
114
+ */
115
+ const IdTrigger = /*#__PURE__*/forwardRef((_ref2, ref) => {
116
+ let value = _ref2.value,
117
+ tooltip = _ref2.tooltip,
118
+ rest = _objectWithoutPropertiesLoose(_ref2, _excluded2);
119
+ return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, {
120
+ ref: ref,
121
+ className: "show-on-hover has-value",
122
+ "aria-label": "Copy link to task " + value,
123
+ "data-tooltip": tooltip
124
+ }), /*#__PURE__*/React.createElement(StyledIdValue, null, /*#__PURE__*/React.createElement(Caption1, {
125
+ weight: "bold",
126
+ color: "secondary"
127
+ }, value)));
128
+ });
129
+ IdTrigger.displayName = "IdTrigger";
130
+
131
+ /**
132
+ * Task identifier cell — the `#<id>` value as a copy-link control, mirroring
133
+ * how `tasks/Metadata.js` turns `Task #248` into the task's Copy Link action:
134
+ * the click writes the task URL to the clipboard and reports back so the page
135
+ * can raise the app's "Link copied to clipboard." toast (utils/copyLinkToClipboard).
136
+ * It never navigates — the row itself is the navigation target.
137
+ */
138
+ export const IdentifierCell = _ref3 => {
139
+ let identifier = _ref3.identifier,
140
+ link = _ref3.link,
141
+ onCopy = _ref3.onCopy;
142
+ const handleClick = useCallback(event => {
143
+ var _navigator$clipboard;
144
+ /* the whole row navigates to the task, so copying must not also open it */
145
+ event.preventDefault();
146
+ event.stopPropagation();
147
+ if ((_navigator$clipboard = navigator.clipboard) != null && _navigator$clipboard.writeText) {
148
+ navigator.clipboard.writeText(link).then(() => onCopy == null ? void 0 : onCopy(true)).catch(() => onCopy == null ? void 0 : onCopy(false));
149
+ return;
150
+ }
151
+ onCopy == null || onCopy(false);
152
+ }, [link, onCopy]);
153
+ return /*#__PURE__*/React.createElement("div", {
154
+ className: "col col-identifier"
155
+ }, /*#__PURE__*/React.createElement(IdTrigger, {
156
+ value: "#" + identifier,
157
+ tooltip: "Copy Link",
158
+ onClick: handleClick
159
+ }));
160
+ };
161
+
162
+ /* ================================================================== */
163
+ /* Labels */
164
+ /* ================================================================== */
165
+
166
+ /* The project's label catalogue. Names are uppercase, like in the app. */
167
+ const INITIAL_LABELS = [{
168
+ id: "in-progress",
169
+ name: "IN PROGRESS",
170
+ color: "#CDE8F5",
171
+ textColor: "#3D7F9E"
172
+ }, {
173
+ id: "needs-review",
174
+ name: "NEEDS REVIEW",
175
+ color: "#F9E79F",
176
+ textColor: "#A98A17"
177
+ }, {
178
+ id: "approved",
179
+ name: "APPROVED",
180
+ color: "#A9DFBF",
181
+ textColor: "#3D9A6A"
182
+ }, {
183
+ id: "blocked",
184
+ name: "BLOCKED",
185
+ color: "#F1948A",
186
+ textColor: "#B5544C"
187
+ }, {
188
+ id: "on-hold",
189
+ name: "ON HOLD",
190
+ color: "#F5CBA7",
191
+ textColor: "#B4713A"
192
+ }, {
193
+ id: "idea",
194
+ name: "IDEA",
195
+ color: "#FAD7A0",
196
+ textColor: "#B98A2B"
197
+ }, {
198
+ id: "todo",
199
+ name: "TODO",
200
+ color: "#D6EAF8",
201
+ textColor: "#4A87B5"
202
+ }, {
203
+ id: "research",
204
+ name: "RESEARCH",
205
+ color: "#D7BDE2",
206
+ textColor: "#8155A0"
207
+ }, {
208
+ id: "accessibility",
209
+ name: "ACCESSIBILITY",
210
+ color: "#AED6F1",
211
+ textColor: "#3E7CA6"
212
+ }, {
213
+ id: "copy",
214
+ name: "COPY",
215
+ color: "#F5B7B1",
216
+ textColor: "#B5544C"
217
+ }, {
218
+ id: "design-debt",
219
+ name: "DESIGN DEBT",
220
+ color: "#E8DAEF",
221
+ textColor: "#7D5A99"
222
+ }, {
223
+ id: "qa",
224
+ name: "QA",
225
+ color: "#A3E4D7",
226
+ textColor: "#34907C"
227
+ }, {
228
+ id: "client-input",
229
+ name: "CLIENT INPUT",
230
+ color: "#FDEBD0",
231
+ textColor: "#B08640"
232
+ }, {
233
+ id: "mobile",
234
+ name: "MOBILE",
235
+ color: "#D1F2EB",
236
+ textColor: "#2E8B7A"
237
+ }, {
238
+ id: "performance",
239
+ name: "PERFORMANCE",
240
+ color: "#FCF3CF",
241
+ textColor: "#A38B1E"
242
+ }, {
243
+ id: "asset",
244
+ name: "ASSET",
245
+ color: "#D5D8DC",
246
+ textColor: "#6B7378"
247
+ }];
248
+
249
+ /* Colours handed out to labels created from the picker's "+ Add new" row. */
250
+ const NEW_LABEL_COLORS = [{
251
+ color: "#F9E0F2",
252
+ textColor: "#A4569B"
253
+ }, {
254
+ color: "#DDEBFB",
255
+ textColor: "#4E7BA8"
256
+ }, {
257
+ color: "#E4F6D8",
258
+ textColor: "#5F9440"
259
+ }, {
260
+ color: "#FDE2D5",
261
+ textColor: "#B36A45"
262
+ }];
263
+ const LabelsContext = /*#__PURE__*/createContext({
264
+ labels: INITIAL_LABELS,
265
+ labelsById: {},
266
+ addLabel: () => ""
267
+ });
268
+
269
+ /**
270
+ * Holds the label catalogue for the whole list, so a label created in one
271
+ * task's picker shows up in every other picker too.
272
+ */
273
+ export const LabelsProvider = _ref4 => {
274
+ let children = _ref4.children;
275
+ const _useState = useState(INITIAL_LABELS),
276
+ labels = _useState[0],
277
+ setLabels = _useState[1];
278
+ const addLabel = useCallback(name => {
279
+ const trimmed = name.trim().toUpperCase();
280
+ const id = "label-" + trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "-");
281
+ setLabels(prev => {
282
+ if (prev.some(label => label.id === id)) {
283
+ return prev;
284
+ }
285
+ const palette = NEW_LABEL_COLORS[prev.length % NEW_LABEL_COLORS.length];
286
+ return [...prev, _extends({
287
+ id,
288
+ name: trimmed
289
+ }, palette)];
290
+ });
291
+ return id;
292
+ }, []);
293
+ const value = useMemo(() => ({
294
+ labels,
295
+ labelsById: labels.reduce((acc, label) => _extends({}, acc, {
296
+ [label.id]: label
297
+ }), {}),
298
+ addLabel
299
+ }), [addLabel, labels]);
300
+ return /*#__PURE__*/React.createElement(LabelsContext.Provider, {
301
+ value: value
302
+ }, children);
303
+ };
304
+ const useTaskLabels = () => useContext(LabelsContext);
305
+
306
+ /* Empty state pill (mirrors EntityLabel + .entity-label in list-view.less) */
307
+ const StyledLabelPlaceholder = styled.span.withConfig({
308
+ displayName: "columns__StyledLabelPlaceholder",
309
+ componentId: "sc-9f4385-2"
310
+ })(["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));}"]);
311
+ const StyledLabelItems = styled.span.withConfig({
312
+ displayName: "columns__StyledLabelItems",
313
+ componentId: "sc-9f4385-3"
314
+ })(["display:inline-flex;align-items:center;min-width:0;> *:not(:last-child){margin-right:2px;}.rest-count{margin-left:2px;}", ""], CooltipStyle);
315
+ const MAX_SHOWN_LABELS = 8;
316
+ const LabelItems = _ref5 => {
317
+ let labels = _ref5.labels;
318
+ const showAsDot = labels.length > 2;
319
+ let shown = labels;
320
+ let restCount = 0;
321
+ if (labels.length > MAX_SHOWN_LABELS) {
322
+ restCount = Math.min(labels.length - MAX_SHOWN_LABELS, 99);
323
+ shown = labels.slice(0, MAX_SHOWN_LABELS - 1);
324
+ }
325
+ return /*#__PURE__*/React.createElement(StyledLabelItems, null, shown.map(label => showAsDot ? /*#__PURE__*/React.createElement(Dot, {
326
+ key: label.id,
327
+ color: label.color,
328
+ size: 20,
329
+ "data-tooltip": label.name
330
+ }) : /*#__PURE__*/React.createElement(Chip, {
331
+ key: label.id,
332
+ label: label.name,
333
+ backgroundColor: label.color,
334
+ color: "#303037",
335
+ style: {
336
+ maxWidth: "90px"
337
+ },
338
+ typographyProps: {
339
+ variant: "Caption 2",
340
+ weight: "bold"
341
+ },
342
+ "data-tooltip": label.name
343
+ })), restCount > 0 ? /*#__PURE__*/React.createElement(Body2, {
344
+ weight: "bold",
345
+ className: "rest-count"
346
+ }, "+", restCount) : null);
347
+ };
348
+ const LabelTrigger = /*#__PURE__*/forwardRef((_ref6, ref) => {
349
+ let labels = _ref6.labels,
350
+ rest = _objectWithoutPropertiesLoose(_ref6, _excluded3);
351
+ if (labels.length === 0) {
352
+ return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, {
353
+ ref: ref,
354
+ className: "show-on-hover",
355
+ "aria-label": "Add labels",
356
+ "data-tooltip": "Add Labels"
357
+ }), /*#__PURE__*/React.createElement(StyledLabelPlaceholder, null, /*#__PURE__*/React.createElement(Caption2, {
358
+ weight: "bold",
359
+ color: "tertiary"
360
+ }, "LABEL")));
361
+ }
362
+ return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, {
363
+ ref: ref,
364
+ "aria-label": "Change labels"
365
+ }), /*#__PURE__*/React.createElement(LabelItems, {
366
+ labels: labels
367
+ }));
368
+ });
369
+ LabelTrigger.displayName = "LabelTrigger";
370
+ export const LabelCell = _ref7 => {
371
+ let labelIds = _ref7.labelIds,
372
+ _ref7$completed = _ref7.completed,
373
+ completed = _ref7$completed === void 0 ? false : _ref7$completed;
374
+ const _useTaskLabels = useTaskLabels(),
375
+ catalog = _useTaskLabels.labels,
376
+ labelsById = _useTaskLabels.labelsById,
377
+ addLabel = _useTaskLabels.addLabel;
378
+ const _useState2 = useState(labelIds != null ? labelIds : []),
379
+ selected = _useState2[0],
380
+ setSelected = _useState2[1];
381
+
382
+ /* alphabetical, like LabelTrigger sorts before rendering */
383
+ const selectedLabels = useMemo(() => selected.map(id => labelsById[id]).filter(label => Boolean(label)).sort((a, b) => a.name.localeCompare(b.name)), [labelsById, selected]);
384
+ const options = useMemo(() => catalog.map(label => ({
385
+ id: label.id,
386
+ name: label.name,
387
+ color: label.color,
388
+ textColor: label.textColor
389
+ })), [catalog]);
390
+ const handleChange = useCallback(value => {
391
+ setSelected(Array.isArray(value) ? value.map(String) : [String(value)].filter(Boolean));
392
+ }, []);
393
+ const handleAddNew = useCallback(name => {
394
+ if (!name || !name.trim()) {
395
+ return;
396
+ }
397
+ const id = addLabel(name);
398
+ setSelected(prev => prev.includes(id) ? prev : [...prev, id]);
399
+ }, [addLabel]);
400
+
401
+ /* Completed tasks show their labels read-only (mirrors Label.js) */
402
+ if (completed) {
403
+ return /*#__PURE__*/React.createElement(StyledReadOnlyCell, {
404
+ className: "col col-label"
405
+ }, selectedLabels.length > 0 ? /*#__PURE__*/React.createElement(LabelItems, {
406
+ labels: selectedLabels
407
+ }) : null);
408
+ }
409
+ return /*#__PURE__*/React.createElement("div", {
410
+ className: "col col-label"
411
+ }, /*#__PURE__*/React.createElement(Select, {
412
+ type: "multiple",
413
+ options: options,
414
+ selected: selected,
415
+ onChange: handleChange,
416
+ emptyValue: "Add new",
417
+ emptyAction: handleAddNew,
418
+ autoHeightMax: 278,
419
+ position: "bottom-end",
420
+ target: /*#__PURE__*/React.createElement(LabelTrigger, {
421
+ labels: selectedLabels
422
+ })
423
+ }));
424
+ };
425
+
426
+ /* ================================================================== */
427
+ /* Date */
428
+ /* ================================================================== */
429
+
430
+ /** The personalized date format the app defaults to (`MMM D. YYYY`). */
431
+ const DATE_FORMAT = "MMM D. YYYY";
432
+
433
+ /** Midnight UTC today — the reference point for the mock data and overdue. */
434
+ const todayUnix = () => moment.utc().startOf("day").unix();
435
+
436
+ /** Build a mock timestamp N days from today (negative = in the past). */
437
+ export const dayOffset = offset => moment.utc().startOf("day").add(offset, "days").unix();
438
+
439
+ /** mirrors utils/date.js → formatDateRange (month-first format) */
440
+ export const formatDateRange = (from, to) => {
441
+ if (from === undefined || from === null) {
442
+ return undefined;
443
+ }
444
+ const start = moment.unix(from).utc();
445
+ const due = moment.unix(to != null ? to : from).utc();
446
+ const withoutYear = DATE_FORMAT.replace(/YYYY|,$/gi, "").trim();
447
+ const withoutYearAndMonth = withoutYear.replace(/MMM|,$/gi, "").trim();
448
+ if (from === to || to === undefined) {
449
+ return start.format(withoutYear);
450
+ }
451
+ if (start.year() === due.year() && start.month() === due.month()) {
452
+ return start.format(withoutYear) + " - " + due.format(withoutYearAndMonth);
453
+ }
454
+ return start.format(withoutYear) + " - " + due.format(withoutYear);
455
+ };
456
+
457
+ /** mirrors utils/date.js → formatDateSmart(…, long) */
458
+ const formatDateLong = value => value ? moment.unix(value).utc().format(DATE_FORMAT) : "";
459
+ export const isOverdue = (dueOn, completed) => Boolean(dueOn && !completed && dueOn < todayUnix());
460
+ const StyledDateValue = styled.span.withConfig({
461
+ displayName: "columns__StyledDateValue",
462
+ componentId: "sc-9f4385-4"
463
+ })(["font-family:", ";white-space:nowrap;"], SANS);
464
+
465
+ /* completed rows render values without a trigger, so the cell itself hosts the
466
+ CSS tooltip */
467
+ const StyledReadOnlyCell = styled.div.withConfig({
468
+ displayName: "columns__StyledReadOnlyCell",
469
+ componentId: "sc-9f4385-5"
470
+ })(["padding-right:8px;", ""], CooltipStyle);
471
+
472
+ /**
473
+ * `DatePicker` wraps itself in a `width: 100%` inline-block, which would stretch
474
+ * across the column and left-align the trigger the moment the picker mounts —
475
+ * the value would visibly jump and the popper would anchor to the wrong spot.
476
+ * Keeping that wrapper right-aligned makes the open state geometrically
477
+ * identical to the closed one.
478
+ */
479
+ const StyledPickerCell = styled.div.withConfig({
480
+ displayName: "columns__StyledPickerCell",
481
+ componentId: "sc-9f4385-6"
482
+ })(["> div{text-align:right;}"]);
483
+ const DateTrigger = /*#__PURE__*/forwardRef((_ref8, ref) => {
484
+ let value = _ref8.value,
485
+ tooltip = _ref8.tooltip,
486
+ overdue = _ref8.overdue,
487
+ rest = _objectWithoutPropertiesLoose(_ref8, _excluded4);
488
+ return /*#__PURE__*/React.createElement(StyledListButton, _extends({}, rest, {
489
+ ref: ref,
490
+ className: value ? "show-on-hover has-value" : "show-on-hover",
491
+ "aria-label": value ? "Change dates" : "Add date",
492
+ "data-tooltip": tooltip
493
+ }), value ? /*#__PURE__*/React.createElement(StyledDateValue, null, /*#__PURE__*/React.createElement(Caption1, {
494
+ weight: "bold",
495
+ color: overdue ? "alert" : "secondary"
496
+ }, value)) : /*#__PURE__*/React.createElement(CalendarIcon, null));
497
+ });
498
+ DateTrigger.displayName = "DateTrigger";
499
+ export const DateCell = _ref9 => {
500
+ var _dates$from, _dates$to2;
501
+ let startOn = _ref9.startOn,
502
+ dueOn = _ref9.dueOn,
503
+ _ref9$completed = _ref9.completed,
504
+ completed = _ref9$completed === void 0 ? false : _ref9$completed;
505
+ const _useState3 = useState({
506
+ from: startOn,
507
+ to: dueOn
508
+ }),
509
+ dates = _useState3[0],
510
+ setDates = _useState3[1];
511
+ /**
512
+ * The DS `DatePicker` does real work on mount (timezone guess, month grids,
513
+ * key handlers) — with one per row that is the single most expensive thing on
514
+ * this page. So the closed state is just the trigger; the picker is mounted
515
+ * on first click, already open, and unmounted again when it closes.
516
+ */
517
+ const _useState4 = useState(false),
518
+ pickerMounted = _useState4[0],
519
+ setPickerMounted = _useState4[1];
520
+
521
+ /* moment isn't cheap either; only reformat when the dates actually change */
522
+ const _useMemo = useMemo(() => {
523
+ var _dates$to;
524
+ const formatted = formatDateRange(dates.from, dates.to);
525
+ let hint = "Add Date";
526
+ if (dates.from) {
527
+ hint = !dates.to || dates.to === dates.from ? formatDateLong(dates.from) : formatDateLong(dates.from) + " - " + formatDateLong(dates.to);
528
+ }
529
+ return {
530
+ value: formatted,
531
+ overdue: isOverdue((_dates$to = dates.to) != null ? _dates$to : dates.from, completed),
532
+ tooltip: hint
533
+ };
534
+ }, [completed, dates.from, dates.to]),
535
+ value = _useMemo.value,
536
+ overdue = _useMemo.overdue,
537
+ tooltip = _useMemo.tooltip;
538
+ const handleSave = useCallback(range => {
539
+ if (!range || range.from === null || range.from === undefined) {
540
+ setDates({});
541
+ return;
542
+ }
543
+ setDates({
544
+ from: Number(range.from),
545
+ to: range.to ? Number(range.to) : Number(range.from)
546
+ });
547
+ }, []);
548
+ const handleClear = useCallback(() => setDates({}), []);
549
+ const handleCalendarToggle = useCallback(next => {
550
+ if (!next) {
551
+ setPickerMounted(false);
552
+ }
553
+ }, []);
554
+
555
+ /* Completed tasks show the date read-only (mirrors Date.js) */
556
+ if (completed) {
557
+ return /*#__PURE__*/React.createElement(StyledReadOnlyCell, {
558
+ className: "col col-date"
559
+ }, value ? /*#__PURE__*/React.createElement(StyledDateValue, {
560
+ "data-tooltip": tooltip
561
+ }, /*#__PURE__*/React.createElement(Caption1, {
562
+ weight: "bold",
563
+ color: overdue ? "alert" : "secondary"
564
+ }, value)) : null);
565
+ }
566
+ if (!pickerMounted) {
567
+ return /*#__PURE__*/React.createElement("div", {
568
+ className: "col col-date"
569
+ }, /*#__PURE__*/React.createElement(DateTrigger, {
570
+ value: value,
571
+ tooltip: tooltip,
572
+ overdue: overdue,
573
+ onClick: () => setPickerMounted(true)
574
+ }));
575
+ }
576
+ return /*#__PURE__*/React.createElement(StyledPickerCell, {
577
+ className: "col col-date"
578
+ }, /*#__PURE__*/React.createElement(DatePicker, {
579
+ open: true,
580
+ mode: "custom",
581
+ instant: false,
582
+ maxRange: 366,
583
+ firstDayOfWeek: 1,
584
+ position: "bottom-end",
585
+ selected: {
586
+ from: (_dates$from = dates.from) != null ? _dates$from : null,
587
+ to: (_dates$to2 = dates.to) != null ? _dates$to2 : null
588
+ },
589
+ onSave: handleSave,
590
+ onClear: handleClear,
591
+ onCalendarToggle: handleCalendarToggle,
592
+ target: /*#__PURE__*/React.createElement(DateTrigger, {
593
+ value: value,
594
+ tooltip: tooltip,
595
+ overdue: overdue
596
+ })
597
+ }));
598
+ };
599
+
600
+ /* ================================================================== */
601
+ /* Time */
602
+ /* ================================================================== */
603
+
604
+ /** mirrors utils/utils.jsx → calculateTimeToEstimatePercentage */
605
+ const timeToEstimatePercentage = (tracked, estimated) => {
606
+ if (estimated > 0) {
607
+ return tracked > 0 ? Math.round(tracked / estimated * 100) : 0;
608
+ }
609
+ return null;
610
+ };
611
+
612
+ /** mirrors utils/utils.jsx → formatTrackedAndEstimatedTime */
613
+ const formatTrackedAndEstimated = (tracked, estimated) => estimated > 0 ? formatHours(tracked, false, false, "short") + "/" + formatHours(estimated, false, false, "short") : formatHours(tracked, false, false, "short");
614
+
615
+ /* mirrors Time.js → progressColor() */
616
+ const progressColor = percent => {
617
+ if (percent >= 100) {
618
+ return "var(--red-alert)";
619
+ }
620
+ if (percent >= 75) {
621
+ return "var(--warning)";
622
+ }
623
+ return "var(--color-sucess-green)";
624
+ };
625
+ const StyledTimeValue = styled.span.withConfig({
626
+ displayName: "columns__StyledTimeValue",
627
+ componentId: "sc-9f4385-7"
628
+ })(["display:inline-flex;align-items:center;font-family:", ";white-space:nowrap;.c-progress-pie,svg{flex-shrink:0;}.time-pie{margin-right:8px;}"], SANS);
629
+ const TimeValue = _ref0 => {
630
+ let tracked = _ref0.tracked,
631
+ estimated = _ref0.estimated;
632
+ const percent = timeToEstimatePercentage(tracked, estimated);
633
+ return /*#__PURE__*/React.createElement(StyledTimeValue, null, estimated > 0 ? /*#__PURE__*/React.createElement(ProgressPie, {
634
+ className: "time-pie",
635
+ radius: 10,
636
+ progress: percent != null ? percent : 0,
637
+ progressColor: progressColor(percent != null ? percent : 0),
638
+ backgroundColor: "var(--color-theme-300)"
639
+ }) : null, /*#__PURE__*/React.createElement(Caption1, {
640
+ weight: "bold"
641
+ }, formatTrackedAndEstimated(tracked, estimated)));
642
+ };
643
+ const TimeTrigger = _ref1 => {
644
+ let tracked = _ref1.tracked,
645
+ estimated = _ref1.estimated,
646
+ onClick = _ref1.onClick;
647
+ const hasTime = tracked > 0 || estimated > 0;
648
+ return /*#__PURE__*/React.createElement(StyledListButton, {
649
+ onClick: onClick,
650
+ className: hasTime ? "show-on-hover open" : "show-on-hover",
651
+ "aria-label": hasTime ? "Edit time" : "Add time",
652
+ "data-tooltip": hasTime ? "Edit Time" : "Add Time"
653
+ }, hasTime ? /*#__PURE__*/React.createElement(TimeValue, {
654
+ tracked: tracked,
655
+ estimated: estimated
656
+ }) : /*#__PURE__*/React.createElement(ClockAddIcon, null));
657
+ };
658
+ export const TimeCell = _ref10 => {
659
+ let taskName = _ref10.taskName,
660
+ _ref10$tracked = _ref10.tracked,
661
+ tracked = _ref10$tracked === void 0 ? 0 : _ref10$tracked,
662
+ _ref10$estimated = _ref10.estimated,
663
+ estimated = _ref10$estimated === void 0 ? 0 : _ref10$estimated,
664
+ users = _ref10.users,
665
+ currentUserId = _ref10.currentUserId,
666
+ _ref10$completed = _ref10.completed,
667
+ completed = _ref10$completed === void 0 ? false : _ref10$completed;
668
+ const _useState5 = useState(false),
669
+ open = _useState5[0],
670
+ setOpen = _useState5[1];
671
+ const _useState6 = useState(() => {
672
+ var _users$find$name, _users$find;
673
+ return tracked > 0 ? [{
674
+ id: 1,
675
+ date: dayOffset(-3),
676
+ userId: currentUserId,
677
+ userName: (_users$find$name = (_users$find = users.find(user => user.id === currentUserId)) == null ? void 0 : _users$find.name) != null ? _users$find$name : "You",
678
+ jobType: "Design",
679
+ hours: tracked,
680
+ billable: true
681
+ }] : [];
682
+ }),
683
+ records = _useState6[0],
684
+ setRecords = _useState6[1];
685
+ const total = useMemo(() => records.reduce((sum, record) => sum + record.hours, 0), [records]);
686
+ const handleAdd = useCallback(record => {
687
+ setRecords(prev => [_extends({}, record, {
688
+ id: prev.length + 1
689
+ }), ...prev]);
690
+ setOpen(false);
691
+ }, []);
692
+
693
+ /* Completed tasks show the value without the "add" affordance (mirrors
694
+ Time.js passing hideIcon for completed tasks) */
695
+ if (completed) {
696
+ return /*#__PURE__*/React.createElement(StyledReadOnlyCell, {
697
+ className: "col col-time"
698
+ }, total > 0 || estimated > 0 ? /*#__PURE__*/React.createElement(TimeValue, {
699
+ tracked: total,
700
+ estimated: estimated
701
+ }) : null);
702
+ }
703
+ return /*#__PURE__*/React.createElement("div", {
704
+ className: "col col-time"
705
+ }, /*#__PURE__*/React.createElement(TimeTrigger, {
706
+ tracked: total,
707
+ estimated: estimated,
708
+ onClick: () => setOpen(true)
709
+ }), open ? /*#__PURE__*/React.createElement(TimeRecordDialog, {
710
+ open: open,
711
+ taskName: taskName,
712
+ users: users,
713
+ currentUserId: currentUserId,
714
+ records: records,
715
+ total: total,
716
+ onSubmit: handleAdd,
717
+ onClose: () => setOpen(false)
718
+ }) : null);
719
+ };
720
+
721
+ /* ------------------------------------------------------------------ */
722
+ /* Add Time Record dialog (mirrors TimeRecordForm/*) */
723
+ /* ------------------------------------------------------------------ */
724
+
725
+ const JOB_TYPES = [{
726
+ id: "design",
727
+ name: "Design"
728
+ }, {
729
+ id: "development",
730
+ name: "Development"
731
+ }, {
732
+ id: "devops",
733
+ name: "Devops"
734
+ }, {
735
+ id: "consulting",
736
+ name: "Consulting"
737
+ }, {
738
+ id: "qa",
739
+ name: "QA"
740
+ }];
741
+
742
+ /* The dialog paper — the app builds this one by hand rather than with DS
743
+ `Dialog` because it is 770px wide (`.time_record_form_modal_wrapper`). */
744
+ const StyledTimeRecordPaper = styled.div.withConfig({
745
+ displayName: "columns__StyledTimeRecordPaper",
746
+ componentId: "sc-9f4385-8"
747
+ })(["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);
748
+ const TimeRecordDialog = _ref11 => {
749
+ let open = _ref11.open,
750
+ taskName = _ref11.taskName,
751
+ users = _ref11.users,
752
+ currentUserId = _ref11.currentUserId,
753
+ records = _ref11.records,
754
+ total = _ref11.total,
755
+ onSubmit = _ref11.onSubmit,
756
+ onClose = _ref11.onClose;
757
+ const _useState7 = useState(""),
758
+ value = _useState7[0],
759
+ setValue = _useState7[1];
760
+ const _useState8 = useState(todayUnix()),
761
+ date = _useState8[0],
762
+ setDate = _useState8[1];
763
+ const _useState9 = useState(currentUserId),
764
+ userId = _useState9[0],
765
+ setUserId = _useState9[1];
766
+ const _useState0 = useState("devops"),
767
+ jobTypeId = _useState0[0],
768
+ setJobTypeId = _useState0[1];
769
+ const _useState1 = useState(false),
770
+ billable = _useState1[0],
771
+ setBillable = _useState1[1];
772
+ const _useState10 = useState(true),
773
+ showDescription = _useState10[0],
774
+ setShowDescription = _useState10[1];
775
+ const _useState11 = useState(""),
776
+ description = _useState11[0],
777
+ setDescription = _useState11[1];
778
+ const _useState12 = useState(false),
779
+ showRecords = _useState12[0],
780
+ setShowRecords = _useState12[1];
781
+ const user = users.find(item => item.id === userId);
782
+ const jobType = JOB_TYPES.find(item => item.id === jobTypeId);
783
+
784
+ /** "1:30" / "1.5" → decimal hours */
785
+ const hours = useMemo(() => {
786
+ const trimmed = value.trim();
787
+ if (!trimmed) {
788
+ return 0;
789
+ }
790
+ if (trimmed.includes(":")) {
791
+ const _trimmed$split = trimmed.split(":"),
792
+ h = _trimmed$split[0],
793
+ m = _trimmed$split[1];
794
+ return Number(h || 0) + Number(m || 0) / 60;
795
+ }
796
+ return Number(trimmed) || 0;
797
+ }, [value]);
798
+ const handleSubmit = useCallback(() => {
799
+ var _user$name, _jobType$name;
800
+ onSubmit({
801
+ date,
802
+ userId,
803
+ userName: (_user$name = user == null ? void 0 : user.name) != null ? _user$name : "",
804
+ jobType: (_jobType$name = jobType == null ? void 0 : jobType.name) != null ? _jobType$name : "",
805
+ hours,
806
+ billable,
807
+ description
808
+ });
809
+ }, [billable, date, description, hours, jobType, onSubmit, user, userId]);
810
+ return /*#__PURE__*/React.createElement(Modal, {
811
+ open: open,
812
+ onClose: onClose
813
+ }, /*#__PURE__*/React.createElement(SlideFromTop, {
814
+ in: open
815
+ }, /*#__PURE__*/React.createElement(StyledTimeRecordPaper, {
816
+ role: "dialog",
817
+ "aria-label": "Add time record"
818
+ }, /*#__PURE__*/React.createElement("div", {
819
+ className: "trd-header"
820
+ }, /*#__PURE__*/React.createElement("span", {
821
+ className: "trd-title"
822
+ }, taskName), /*#__PURE__*/React.createElement("span", {
823
+ className: "trd-date"
824
+ }, /*#__PURE__*/React.createElement(DatePicker, {
825
+ mode: "daily",
826
+ required: true,
827
+ firstDayOfWeek: 1,
828
+ position: "bottom-end",
829
+ selected: {
830
+ from: date,
831
+ to: date
832
+ },
833
+ onSave: range => range != null && range.from ? setDate(Number(range.from)) : undefined,
834
+ target: /*#__PURE__*/React.createElement(SelectTrigger, null, formatDateRange(date, date))
835
+ }))), /*#__PURE__*/React.createElement("div", {
836
+ className: "trd-divider"
837
+ }), /*#__PURE__*/React.createElement("div", {
838
+ className: "trd-body"
839
+ }, /*#__PURE__*/React.createElement("div", {
840
+ className: "trd-form"
841
+ }, /*#__PURE__*/React.createElement("div", {
842
+ className: "trd-fields"
843
+ }, /*#__PURE__*/React.createElement("span", {
844
+ className: "trd-hours"
845
+ }, /*#__PURE__*/React.createElement(InputHours, {
846
+ autoFocus: true,
847
+ allowEmptyValue: true,
848
+ size: "biggest",
849
+ placeholder: "00:00",
850
+ value: value,
851
+ onChange: setValue
852
+ })), /*#__PURE__*/React.createElement("span", {
853
+ className: "trd-field"
854
+ }, /*#__PURE__*/React.createElement(Label, {
855
+ size: "small"
856
+ }, "User"), /*#__PURE__*/React.createElement(Select, {
857
+ options: users,
858
+ selected: userId,
859
+ forceCloseMenu: true,
860
+ onChange: next => setUserId(String(next)),
861
+ target: /*#__PURE__*/React.createElement(SelectTrigger, {
862
+ id: "trd-user"
863
+ }, user == null ? void 0 : user.name)
864
+ })), /*#__PURE__*/React.createElement("span", {
865
+ className: "trd-field"
866
+ }, /*#__PURE__*/React.createElement(Label, {
867
+ size: "small"
868
+ }, "Job Type"), /*#__PURE__*/React.createElement(Select, {
869
+ options: JOB_TYPES,
870
+ selected: jobTypeId,
871
+ forceCloseMenu: true,
872
+ onChange: next => setJobTypeId(String(next)),
873
+ target: /*#__PURE__*/React.createElement(SelectTrigger, {
874
+ id: "trd-job-type"
875
+ }, jobType == null ? void 0 : jobType.name)
876
+ })), /*#__PURE__*/React.createElement("span", {
877
+ className: "trd-billable"
878
+ }, /*#__PURE__*/React.createElement(Checkbox, {
879
+ id: "trd-billable",
880
+ checked: billable,
881
+ onChange: () => setBillable(prev => !prev)
882
+ }), /*#__PURE__*/React.createElement(Label, {
883
+ htmlFor: "trd-billable",
884
+ weight: "regular"
885
+ }, "Billable"))), /*#__PURE__*/React.createElement("button", {
886
+ type: "button",
887
+ className: showDescription ? "trd-description-trigger" : "trd-description-trigger collapsed",
888
+ onClick: () => setShowDescription(prev => !prev)
889
+ }, /*#__PURE__*/React.createElement(CollapseExpandSingleIcon, null), /*#__PURE__*/React.createElement("span", null, "Description")), showDescription ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Textarea, {
890
+ className: "trd-textarea",
891
+ value: description,
892
+ onChange: event => setDescription(event.target.value)
893
+ })) : null, /*#__PURE__*/React.createElement("div", {
894
+ className: "trd-actions"
895
+ }, /*#__PURE__*/React.createElement(Button, {
896
+ variant: "primary",
897
+ disabled: hours <= 0,
898
+ onClick: handleSubmit
899
+ }, "Add Time Record"), /*#__PURE__*/React.createElement(Button, {
900
+ variant: "secondary",
901
+ onClick: onClose
902
+ }, "Cancel"))), total > 0 ? /*#__PURE__*/React.createElement("button", {
903
+ type: "button",
904
+ className: showRecords ? "trd-total" : "trd-total collapsed",
905
+ onClick: () => setShowRecords(prev => !prev)
906
+ }, /*#__PURE__*/React.createElement(CollapseExpandSingleIcon, null), /*#__PURE__*/React.createElement(Header3, {
907
+ color: "theme-secondary",
908
+ tabularNums: true
909
+ }, "Total: ", formatHours(total, false, true), "h")) : null), showRecords && records.length > 0 ? /*#__PURE__*/React.createElement("div", {
910
+ className: "trd-records"
911
+ }, records.map(record => /*#__PURE__*/React.createElement("div", {
912
+ key: record.id,
913
+ className: "trd-record"
914
+ }, /*#__PURE__*/React.createElement("span", {
915
+ className: "trd-record-left"
916
+ }, /*#__PURE__*/React.createElement(Caption1, {
917
+ weight: "bold",
918
+ color: "secondary"
919
+ }, formatDateRange(record.date, record.date)), /*#__PURE__*/React.createElement(Body2, null, record.userName), /*#__PURE__*/React.createElement(Caption1, {
920
+ color: "secondary"
921
+ }, record.jobType)), /*#__PURE__*/React.createElement(Body2, {
922
+ weight: "bold",
923
+ tabularNums: true
924
+ }, formatHours(record.hours), "h")))) : null)));
925
+ };
926
+ //# sourceMappingURL=columns.js.map