@activecollab/components
Version:
ActiveCollab Components
1,312 lines (1,284 loc) • 46.2 kB
JavaScript
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
import _extends from "@babel/runtime/helpers/esm/extends";
const _excluded = ["size", "bg", "initials", "alt"];
import React, { useState } from "react";
import styled, { css } from "styled-components";
import { LinkBranchOrPullRequestDialog } from "./LinkBranchOrPullRequestDialog";
import { LinkingCommandsButton } from "./LinkingCommandsButton";
import { RoundAvatar } from "./RoundAvatar";
import { AVATAR_COLORS } from "./tokens";
import { AvatarGroup } from "../../components/AvatarGroup";
import { Button } from "../../components/Button";
import { AddToListButton } from "../../components/Button/AddToListButton";
import { Chip } from "../../components/Chip";
import { Dialog } from "../../components/Dialog";
import { IconButton } from "../../components/IconButton";
import { ArrowRightIcon, CancelCrossIcon, CheckboxBlankTogglerIcon, CheckIcon, CircleDotIcon, CloseIcon, CloudDeployFailedIcon, CloudDeployIcon, EyeIcon, GearFailedIcon, GearIcon, GitBranchIcon, GitCommitIcon, GitMergeIcon, GitPullRequestClosedIcon, GitPullRequestDraftIcon, GitPullRequestIcon, GitRepositoryIcon, InfoSmallIcon, MessageIcon, MessageSmallIcon, TrashIcon } from "../../components/Icons";
import { StepSequence } from "../../components/StepSequence";
import { Tooltip } from "../../components/Tooltip";
import { Body1, Caption1, Header3 } from "../../components/Typography";
import { colors } from "../../utils";
/**
* Source Code section of a task — the shared subsystem behind the
* `Presentation/Source/Task Source` stories (Mix / All Open / All Completed).
* Holds the row types (issue / pull-or-merge request / branch), the pipeline
* spine, the history dialog, and the section that lays them out. Extracted into
* `shared/` so all three stories render the same components; editing a row type
* here updates every story at once.
*/
const SANS = '-apple-system, BlinkMacSystemFont, "Roboto", "Helvetica Neue", Arial, sans-serif';
/* Icons in the collection colour either via `stroke` (outline glyphs whose root
is `fill="none"`) or via `fill`. To tint a history glyph by tone we must set
the right one — this set marks the stroke-based glyphs used in the stream. */
const STROKE_ICONS = new Set([GitBranchIcon, GitCommitIcon, GitPullRequestIcon, GitPullRequestDraftIcon, GitPullRequestClosedIcon, GitMergeIcon, GearIcon, GearFailedIcon, CloudDeployIcon, CloudDeployFailedIcon, GitRepositoryIcon]);
/** Every event glyph renders in the standard muted gray; success / failure is
carried by a trailing check / cross after the title instead of the glyph. */
const iconColorProps = icon => STROKE_ICONS.has(icon) ? {
stroke: "var(--color-theme-600)"
} : {
fill: "var(--color-theme-600)"
};
/* ================================================================== */
/* Issue state glyphs (open / closed) — mirror the app's IssueStateIcon */
/* (`CircleDotIcon` when open, filled `CheckboxBlankTogglerIcon` when */
/* closed). Kept as a helper so the row lead and the status chip share */
/* the same glyph. */
/* ================================================================== */
const IssueStateIcon = _ref => {
let closed = _ref.closed,
_ref$size = _ref.size,
size = _ref$size === void 0 ? 16 : _ref$size;
return closed ? /*#__PURE__*/React.createElement(CheckboxBlankTogglerIcon, {
width: size,
height: size,
fill: "var(--color-primary)"
}) : /*#__PURE__*/React.createElement(CircleDotIcon, {
width: size,
height: size
});
};
/* ================================================================== */
/* Source Code section — data model */
/* ================================================================== */
const STAGE_NAMES = ["Branch", "Commits", "Pull request", "CI / Build", "Deploy"];
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const pad = n => String(n).padStart(2, "0");
/**
* Reproduces ActiveCollab's standard `Ago` component (`Angie.functions.ago()`)
* for the mock: "Just now", "N minutes ago", "Today HH:mm", "Yesterday HH:mm",
* "MMM D. HH:mm", else "MMM D. YYYY HH:mm". Production must use the real one,
* which also carries the absolute timestamp in a tooltip and auto-refreshes.
*/
const formatAgo = minutesAgo => {
const now = new Date();
const then = new Date(now.getTime() - minutesAgo * 60000);
if (minutesAgo < 2) {
return "Just now";
}
if (minutesAgo < 60) {
return Math.floor(minutesAgo) + " minutes ago";
}
const time = pad(then.getHours()) + ":" + pad(then.getMinutes());
if (then.toDateString() === now.toDateString()) {
return "Today " + time;
}
const yesterday = new Date(now.getTime() - 24 * 60 * 60000);
if (then.toDateString() === yesterday.toDateString()) {
return "Yesterday " + time;
}
const monthDay = MONTHS[then.getMonth()] + " " + then.getDate() + ".";
if (then.getFullYear() === now.getFullYear()) {
return monthDay + " " + time;
}
return monthDay + " " + then.getFullYear() + " " + time;
};
/** A VCS issue. Weakly referenced by any number of branches / PRs. */
/** A branch or pull request read from the VCS. */
const mkStages = defs => defs.map((d, i) => _extends({
name: STAGE_NAMES[i]
}, d));
const issueCompleted = i => i.state === "closed";
const codeCompleted = e => e.status !== "open";
const byRecency = (a, b) => a.updatedMinsAgo - b.updatedMinsAgo;
/** External URL for an element — opens the object in the provider (new tab). */
const issueUrl = i => "https://github.com/" + i.repoFull + "/issues/" + i.ref.replace("#", "");
const codeUrl = e => {
if (e.kind === "pr") {
var _e$title$match$, _e$title$match;
const number = (_e$title$match$ = (_e$title$match = e.title.match(/#(\d+)/)) == null ? void 0 : _e$title$match[1]) != null ? _e$title$match$ : "";
return "https://github.com/" + e.repoFull + "/pull/" + number;
}
return "https://github.com/" + e.repoFull + "/tree/" + e.title;
};
/* ---- Issues -------------------------------------------------------- */
const ISSUES = [{
ref: "#482",
title: "#482 Ranks reset when a contract is archived",
repo: "backend",
repoFull: "activecollab/backend",
state: "open",
updatedLabel: "20m ago",
updatedMinsAgo: 20
}, {
ref: "#455",
title: "#455 Retainer totals off by a rounding cent",
repo: "backend",
repoFull: "activecollab/backend",
state: "open",
updatedLabel: "3h ago",
updatedMinsAgo: 180
}, {
ref: "#470",
title: "#470 Cannot set a rank above 100",
repo: "backend",
repoFull: "activecollab/backend",
state: "closed",
updatedLabel: "4d ago",
updatedMinsAgo: 5760
}];
/* ---- Branches / pull requests -------------------------------------- */
const CODE_ENTRIES = [{
// Open PR with a pipeline mid-run: the CI / Build stage is "progress"
// (blue node, white dot) while Deploy is still "todo". This is the one
// state note 02 describes but the other examples never show — a healthy
// task that is currently *doing something*, visible at a glance on the
// spine. The connector into Build turns green (movement into the stage);
// the connector into Deploy stays muted.
kind: "pr",
title: "#119921 Recalculate ranks when a contract is archived",
headSha: "3d5a8f1c9b7e2604a1f8c3d5e6079b2a4c6d8e0f",
repo: "backend",
repoFull: "activecollab/backend",
status: "open",
sourceBranch: "fix/rank-archive-482",
targetBranch: "main",
closing: true,
contributors: [{
initials: "DA",
bg: AVATAR_COLORS.green
}],
commits: {
value: 8,
tip: "8 commits · last 1h ago by Dragana A."
},
diff: {
add: 96,
del: 12,
tip: "96 additions, 12 deletions across 5 files"
},
comments: {
value: 2,
tip: "2 comments on the pull request"
},
updatedLabel: "10m ago",
updatedMinsAgo: 10,
references: ["#482"],
stages: mkStages([{
state: "done",
details: ["fix/rank-archive-482", "on activecollab/backend"]
}, {
state: "done",
details: ["8 commits", "last 1h ago by Dragana A."]
}, {
state: "done",
details: ["#119921 opened", "awaiting review"]
}, {
state: "progress",
details: ["Build running", "CI pipeline #501 in progress"]
}, {
state: "todo",
details: ["Not deployed"]
}]),
pipelineSupported: true,
history: [{
icon: GitBranchIcon,
title: "Branch created",
detail: "fix/rank-archive-482 by Dragana A.",
minutesAgo: 600
}, {
icon: GitCommitIcon,
title: "8 commits pushed",
detail: "Last by Dragana A.: “Recalculate ranks on contract archive”",
minutesAgo: 90
}, {
icon: GitPullRequestIcon,
title: "Pull request opened",
detail: "Target: main by Dragana A.",
minutesAgo: 40,
tone: "progress"
}, {
icon: EyeIcon,
title: "Review requested",
detail: "Requested by Ilija S. and Marko K.",
minutesAgo: 30
}, {
icon: GearIcon,
title: "Build started",
detail: "CI pipeline #501",
minutesAgo: 10,
tone: "progress"
}]
}, {
kind: "branch",
title: "spike/rank-cache",
headSha: "9f3c1a2b7d4e5f60819a2b3c4d5e6f7081920304",
repo: "backend",
repoFull: "activecollab/backend",
status: "open",
contributors: [{
initials: "IS",
bg: AVATAR_COLORS.purple
}],
commits: {
value: 1,
tip: "1 commit · 30m ago by Ilija S."
},
diff: {
add: 38,
del: 4,
tip: "38 additions, 4 deletions across 2 files"
},
noPrLabel: "no pull request yet",
updatedLabel: "30m ago",
updatedMinsAgo: 30,
references: [],
stages: mkStages([{
state: "done",
details: ["spike/rank-cache", "on activecollab/backend"]
}, {
state: "done",
details: ["1 commit", "30m ago by Ilija S."]
}, {
state: "todo",
details: ["No pull request opened yet"]
}]),
pipelineSupported: false,
history: [{
icon: GitBranchIcon,
title: "Branch created",
detail: "spike/rank-cache by Ilija Studen",
minutesAgo: 120
}, {
icon: GitCommitIcon,
title: "One commit pushed",
detail: "Ilija Studen: “Prototype rank cache”",
minutesAgo: 30
}]
}, {
kind: "branch",
title: "feature/retainers-ui-34",
headSha: "4a7b9c2d1e3f5061728394a5b6c7d8e9f0a1b2c3",
repo: "frontend",
repoFull: "activecollab/frontend",
status: "open",
contributors: [{
initials: "DA",
bg: AVATAR_COLORS.green
}],
commits: {
value: 6,
tip: "6 commits · last 1h ago by Dragana A."
},
diff: {
add: 214,
del: 52,
tip: "214 additions, 52 deletions across 9 files"
},
noPrLabel: "no pull request yet",
updatedLabel: "1h ago",
updatedMinsAgo: 60,
references: ["#482"],
stages: mkStages([{
state: "done",
details: ["feature/retainers-ui-34", "on activecollab/frontend"]
}, {
state: "done",
details: ["6 commits", "last 1h ago by Dragana A."]
}, {
state: "todo",
details: ["No pull request opened yet"]
}, {
state: "todo",
details: ["No build yet"]
}, {
state: "todo",
details: ["Not deployed"]
}]),
pipelineSupported: true,
history: [{
icon: GitBranchIcon,
title: "Branch created",
detail: "feature/retainers-ui-34 by Dragana A.",
minutesAgo: 480
}, {
icon: GitCommitIcon,
title: "6 commits pushed",
detail: "Last by Dragana A.: “Retainer settings screen”",
minutesAgo: 60
}]
}, {
kind: "pr",
title: "#119915 Contract UI rank controls",
headSha: "b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5",
repo: "frontend",
repoFull: "activecollab/frontend",
status: "merged",
sourceBranch: "feature/contracts-ui-34",
targetBranch: "main",
closing: true,
contributors: [{
initials: "MK",
bg: AVATAR_COLORS.blue
}],
commits: {
value: 11,
tip: "11 commits · last 5h ago by Marko K."
},
diff: {
add: 168,
del: 24,
tip: "168 additions, 24 deletions across 6 files"
},
comments: {
value: 5,
tip: "5 comments on the pull request"
},
updatedLabel: "5h ago",
updatedMinsAgo: 300,
references: ["#455"],
stages: mkStages([{
state: "done",
details: ["feature/contracts-ui-34", "on activecollab/frontend"]
}, {
state: "done",
details: ["11 commits", "last 5h ago by Marko K."]
}, {
state: "done",
details: ["#119915 merged", "approved by 1 reviewer"]
}, {
state: "failed",
details: ["Build failing", "3 of 12 checks failed"]
}, {
state: "todo",
details: ["Not deployed"]
}]),
pipelineSupported: true,
history: [{
icon: GitBranchIcon,
title: "Branch created",
detail: "feature/contracts-ui-34 by Marko K.",
minutesAgo: 11520
}, {
icon: GitCommitIcon,
title: "6 commits pushed",
detail: "Last by Marko K.: “Wire rank controls to contracts API”",
minutesAgo: 10080
}, {
icon: GitPullRequestDraftIcon,
title: "Draft pull request opened",
detail: "Target: main by Marko K.",
minutesAgo: 8640
}, {
icon: GearIcon,
title: "Build started",
detail: "CI pipeline #479",
minutesAgo: 8639,
tone: "progress"
}, {
icon: GearFailedIcon,
title: "Build failed",
detail: "3 of 12 checks failed",
minutesAgo: 8635,
tone: "danger"
}, {
icon: GitCommitIcon,
title: "2 commits pushed",
detail: "Last by Marko K.: “Fix failing snapshot tests”",
minutesAgo: 7200
}, {
icon: GearIcon,
title: "Build passed",
detail: "All 12 checks passed · CI pipeline #482",
minutesAgo: 7195,
tone: "success"
}, {
icon: GitPullRequestIcon,
title: "Marked ready for review",
author: "Marko K.",
minutesAgo: 5760,
tone: "progress"
}, {
icon: EyeIcon,
title: "Review requested",
detail: "Requested by Ana P., Ilija S. and 2 more",
minutesAgo: 5755
}, {
icon: MessageIcon,
title: "Changes requested",
detail: "Request on 4 files by Ana P.",
minutesAgo: 4320
}, {
icon: GitCommitIcon,
title: "One commit pushed",
detail: "Marko K.: “Address review feedback”",
minutesAgo: 4000
}, {
icon: GitPullRequestClosedIcon,
title: "Pull request closed",
detail: "Closed without merging by Marko K.",
minutesAgo: 2880
}, {
icon: GitPullRequestIcon,
title: "Pull request reopened",
detail: "Target: main by Marko K.",
minutesAgo: 1680,
tone: "progress"
}, {
icon: GitCommitIcon,
title: "4 commits pushed",
detail: "Last by Marko K.: “Rebuild rank controls with a new segmented layout, keyboard navigation, and analytics events”",
minutesAgo: 1560
}, {
icon: CloudDeployIcon,
title: "Deployed to staging",
detail: "Release ci-4821",
minutesAgo: 300,
tone: "success"
}, {
icon: CheckIcon,
title: "Approved",
detail: "2 approvals · Ana P., Ilija S.",
minutesAgo: 180,
tone: "success"
}, {
icon: GitMergeIcon,
title: "Pull request merged",
detail: "Target: main by Marko K.",
minutesAgo: 38,
tone: "success"
}, {
icon: CloudDeployFailedIcon,
title: "Deploy failed",
detail: "Target: production by Marko K.",
minutesAgo: 24,
tone: "danger"
}, {
icon: CloudDeployIcon,
title: "Deployed to production",
detail: "Release v2026.7.3",
minutesAgo: 1,
tone: "success"
}]
}, {
kind: "branch",
title: "feature/retainers-34",
headSha: "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d",
repo: "backend",
repoFull: "activecollab/backend",
status: "merged",
contributors: [{
initials: "IS",
bg: AVATAR_COLORS.purple
}],
commits: {
value: 14,
tip: "14 commits · merged 2d ago by Ilija S."
},
diff: {
add: 405,
del: 37,
tip: "405 additions, 37 deletions across 11 files"
},
updatedLabel: "2d ago",
updatedMinsAgo: 2880,
references: ["#482", "#470"],
stages: mkStages([{
state: "done",
details: ["feature/retainers-34", "on activecollab/backend"]
}, {
state: "done",
details: ["14 commits", "merged 2d ago by Ilija S."]
}, {
state: "done",
details: ["Merged via #119909", "into main"]
}]),
pipelineSupported: false,
history: [{
icon: GitBranchIcon,
title: "Branch created",
detail: "feature/retainers-34 by Ilija Studen",
minutesAgo: 4320
}, {
icon: GitCommitIcon,
title: "14 commits pushed",
detail: "Last by Ilija Studen: “Rank model + datastore updates”",
minutesAgo: 3000
}, {
icon: GitMergeIcon,
title: "Branch merged",
detail: "Target: main by Ilija Studen",
minutesAgo: 2880,
tone: "success"
}]
}, {
kind: "pr",
title: "#119909 Allow and update ranks across datastores",
headSha: "c4d5e6f70819202a3b4c5d6e7f8091a2b3c4d5e6",
repo: "backend",
repoFull: "activecollab/backend",
status: "merged",
sourceBranch: "feature/retainers-34",
targetBranch: "main",
contributors: [{
initials: "IS",
bg: AVATAR_COLORS.purple
}, {
initials: "MK",
bg: AVATAR_COLORS.blue
}],
commits: {
value: 14,
tip: "14 commits · merged 2d ago by Ilija S."
},
diff: {
add: 405,
del: 37,
tip: "405 additions, 37 deletions across 11 files"
},
comments: {
value: 12,
tip: "12 comments on the pull request"
},
updatedLabel: "2d ago",
updatedMinsAgo: 2900,
references: ["#482", "#470"],
stages: mkStages([{
state: "done",
details: ["feature/retainers-34", "on activecollab/backend"]
}, {
state: "done",
details: ["14 commits", "last 2d ago by Ilija S."]
}, {
state: "done",
details: ["#119909 merged", "approved by 2 reviewers"]
}]),
pipelineSupported: false,
history: [{
icon: GitPullRequestIcon,
title: "Pull request opened",
detail: "Target: main by Ilija Studen",
minutesAgo: 3000,
tone: "progress"
}, {
icon: CheckIcon,
title: "Approved",
detail: "2 approvals · Marko K., Janko S.",
minutesAgo: 2905,
tone: "success"
}, {
icon: GitMergeIcon,
title: "Pull request merged",
detail: "Target: main by Ilija Studen",
minutesAgo: 2880,
tone: "success"
}]
}, {
kind: "branch",
title: "hotfix/rank-typo",
headSha: "7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9012",
repo: "backend",
repoFull: "activecollab/backend",
status: "deleted",
contributors: [{
initials: "MK",
bg: AVATAR_COLORS.blue
}],
commits: {
value: 2,
tip: "2 commits · deleted 5d ago"
},
diff: {
add: 6,
del: 3,
tip: "6 additions, 3 deletions across 1 file"
},
updatedLabel: "5d ago",
updatedMinsAgo: 7000,
references: ["#470"],
stages: mkStages([{
state: "done",
details: ["hotfix/rank-typo", "on activecollab/backend"]
}, {
state: "done",
details: ["2 commits", "by Marko K."]
}, {
state: "todo",
details: ["No pull request opened"]
}]),
pipelineSupported: false,
history: [{
icon: GitBranchIcon,
title: "Branch created",
detail: "hotfix/rank-typo by Marko K.",
minutesAgo: 8640
}, {
icon: GitCommitIcon,
title: "2 commits pushed",
detail: "Last by Marko K.: “Fix rank label typo”",
minutesAgo: 8600
}, {
icon: TrashIcon,
title: "Branch deleted",
detail: "Soft deleted by Marko K.",
minutesAgo: 7000
}]
}, {
kind: "pr",
title: "#119870 Clamp rank to valid range",
headSha: "d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f701",
repo: "backend",
repoFull: "activecollab/backend",
status: "merged",
sourceBranch: "fix/rank-clamp-33",
targetBranch: "main",
contributors: [{
initials: "MK",
bg: AVATAR_COLORS.blue
}],
commits: {
value: 4,
tip: "4 commits · merged 5d ago by Marko K."
},
diff: {
add: 52,
del: 8,
tip: "52 additions, 8 deletions across 3 files"
},
updatedLabel: "5d ago",
updatedMinsAgo: 7200,
references: ["#470"],
stages: mkStages([{
state: "done",
details: ["fix/rank-clamp-33", "on activecollab/backend"]
}, {
state: "done",
details: ["4 commits", "merged 5d ago by Marko K."]
}, {
state: "done",
details: ["#119870 merged", "approved by 1 reviewer"]
}]),
pipelineSupported: false,
history: [{
icon: GitBranchIcon,
title: "Branch created",
detail: "fix/rank-clamp-33 by Marko K.",
minutesAgo: 8640
}, {
icon: GitPullRequestIcon,
title: "Pull request opened",
detail: "Target: main by Marko K.",
minutesAgo: 8600,
tone: "progress"
}, {
icon: GitMergeIcon,
title: "Pull request merged",
detail: "Target: main by Marko K.",
minutesAgo: 7200,
tone: "success"
}]
}, {
kind: "pr",
title: "#119888 Cache ranks per datastore",
headSha: "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4",
repo: "backend",
repoFull: "activecollab/backend",
status: "closed",
sourceBranch: "feature/rank-datastore-cache",
targetBranch: "main",
contributors: [{
initials: "IS",
bg: AVATAR_COLORS.purple
}],
commits: {
value: 5,
tip: "5 commits · closed 3d ago by Ilija S."
},
diff: {
add: 120,
del: 15,
tip: "120 additions, 15 deletions across 4 files"
},
comments: {
value: 3,
tip: "3 comments on the pull request"
},
updatedLabel: "3d ago",
updatedMinsAgo: 4320,
references: ["#455"],
stages: mkStages([{
state: "done",
details: ["feature/rank-datastore-cache", "on activecollab/backend"]
}, {
state: "done",
details: ["5 commits", "last 3d ago by Ilija S."]
}, {
state: "todo",
details: ["#119888 closed without merging"]
}]),
pipelineSupported: false,
history: [{
icon: GitPullRequestIcon,
title: "Pull request opened",
detail: "Target: main by Ilija Studen",
minutesAgo: 5760,
tone: "progress"
}, {
icon: GitPullRequestClosedIcon,
title: "Pull request closed",
detail: "Closed without merging by Ilija Studen",
minutesAgo: 4320
}]
}];
/* ================================================================== */
/* Spine + history dialog (for branches / pull requests) */
/* ================================================================== */
const StageTip = _ref2 => {
let name = _ref2.name,
details = _ref2.details;
return /*#__PURE__*/React.createElement("span", {
style: {
display: "block",
textAlign: "left",
lineHeight: 1.5
}
}, /*#__PURE__*/React.createElement("span", {
style: {
display: "block",
fontWeight: 700
}
}, "Stage: ", name), details.map((d, i) => /*#__PURE__*/React.createElement("span", {
key: name + "-" + i,
style: {
display: "block"
}
}, d)));
};
const StyledSpineSequence = styled(StepSequence).withConfig({
displayName: "SourceCodeSection__StyledSpineSequence",
componentId: "sc-rvqbh5-0"
})(["align-self:center;flex-shrink:0;"]);
/** Stage state → the generic look `StepSequence` renders it with. */
const STAGE_STEP = {
done: {
tone: "success",
fill: "solid",
icon: /*#__PURE__*/React.createElement(CheckIcon, {
fill: "var(--only-white)",
width: 14,
height: 14
})
},
progress: {
tone: "info",
fill: "solid"
},
failed: {
tone: "danger",
fill: "solid",
icon: /*#__PURE__*/React.createElement(CloseIcon, {
fill: "var(--only-white)",
width: 12,
height: 12
})
},
todo: {
tone: "neutral",
fill: "outline"
}
};
const Spine = _ref3 => {
let items = _ref3.items,
onClick = _ref3.onClick;
return /*#__PURE__*/React.createElement(StyledSpineSequence, {
ariaLabel: "Open source code history",
onClick: onClick,
steps: items.map(s => _extends({}, STAGE_STEP[s.state], {
key: s.name,
tooltip: /*#__PURE__*/React.createElement(StageTip, s)
}))
});
};
/* The whole content scrolls together (header + stream + note) once the stream
is long — the dialog title bar stays fixed, everything below scrolls. */
const StyledHistory = styled.div.withConfig({
displayName: "SourceCodeSection__StyledHistory",
componentId: "sc-rvqbh5-1"
})(["font-family:", ";max-height:60vh;overflow-y:auto;padding-right:4px;.hist-header{padding-bottom:20px;margin-bottom:20px;border-bottom:1px solid var(--border-primary);}.hist-name{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}.hist-branches{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:8px;font-size:13px;color:var(--color-theme-600);}.hist-branch{display:inline-flex;align-items:center;gap:4px;}.hist-repo{display:inline-flex;align-items:center;gap:4px;margin-top:6px;font-size:13px;color:var(--color-theme-600);}.sc-chip{display:inline-flex;align-items:center;gap:3px;height:20px;padding:0 8px;border-radius:6px;font-size:11px;font-weight:700;letter-spacing:0.02em;flex-shrink:0;}.sc-chip.done{color:var(--color-primary);background:color-mix(in srgb,var(--color-primary) 15%,transparent);}.sc-chip.gone{color:var(--color-theme-900);background:var(--color-theme-200);border:1px solid var(--border-primary);}.sc-chip.fail{color:var(--red-alert);background:color-mix(in srgb,var(--red-alert) 15%,transparent);}.hist-event{display:flex;align-items:center;gap:12px;padding-bottom:20px;}.hist-event:last-child{padding-bottom:0;}.hist-icon{flex-shrink:0;width:22px;height:22px;display:flex;align-items:center;justify-content:center;}.hist-body{flex:1 1 auto;min-width:0;}.hist-title{display:flex;align-items:center;gap:6px;min-width:0;}.hist-title > *:first-child{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.hist-status{flex-shrink:0;display:inline-flex;}.hist-line{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:13px;line-height:1.5;color:var(--color-theme-600);}.hist-meta{flex-shrink:0;align-self:flex-start;text-align:right;white-space:nowrap;}.hist-note{display:flex;align-items:center;gap:6px;margin-top:16px;color:var(--color-theme-500);}.hist-note svg{flex-shrink:0;}"], SANS);
const HistoryDialog = _ref4 => {
let entry = _ref4.entry,
onClose = _ref4.onClose;
return /*#__PURE__*/React.createElement(Dialog, {
open: !!entry,
onClose: onClose,
enableBackgroundClick: true
}, /*#__PURE__*/React.createElement(Dialog.Title, {
disableDefaultHeading: true
}, /*#__PURE__*/React.createElement("div", {
style: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12
}
}, /*#__PURE__*/React.createElement(Header3, null, (entry == null ? void 0 : entry.kind) === "branch" ? "Branch History" : "Pull Request History"), /*#__PURE__*/React.createElement(IconButton, {
type: "button",
variant: "text gray",
onClick: onClose
}, /*#__PURE__*/React.createElement(CancelCrossIcon, null)))), /*#__PURE__*/React.createElement(Dialog.ContentDivider, null), /*#__PURE__*/React.createElement(Dialog.Content, null, entry && /*#__PURE__*/React.createElement(StyledHistory, null, /*#__PURE__*/React.createElement("div", {
className: "hist-header"
}, /*#__PURE__*/React.createElement("div", {
className: "hist-name"
}, /*#__PURE__*/React.createElement(Body1, {
weight: "medium"
}, entry.status === "deleted" ? entry.title : /*#__PURE__*/React.createElement(TitleLink, {
href: codeUrl(entry),
target: "_blank",
rel: "noopener noreferrer"
}, entry.title)), /*#__PURE__*/React.createElement(CodeStatusChip, {
entry: entry
})), entry.kind === "pr" && entry.sourceBranch && entry.targetBranch && /*#__PURE__*/React.createElement("div", {
className: "hist-branches"
}, /*#__PURE__*/React.createElement("span", {
className: "hist-branch"
}, /*#__PURE__*/React.createElement(GitBranchIcon, {
width: 14,
height: 14
}), entry.sourceBranch), /*#__PURE__*/React.createElement(ArrowRightIcon, {
width: 14,
height: 14
}), /*#__PURE__*/React.createElement("span", {
className: "hist-branch"
}, /*#__PURE__*/React.createElement(GitBranchIcon, {
width: 14,
height: 14
}), entry.targetBranch)), /*#__PURE__*/React.createElement("span", {
className: "hist-repo"
}, /*#__PURE__*/React.createElement(GitRepositoryIcon, {
width: 14,
height: 14
}), entry.repoFull)), [...entry.history].sort((a, b) => a.minutesAgo - b.minutesAgo).map((ev, i) => /*#__PURE__*/React.createElement("div", {
className: "hist-event",
key: ev.title + "-" + i
}, /*#__PURE__*/React.createElement("span", {
className: "hist-icon"
}, /*#__PURE__*/React.createElement(ev.icon, _extends({
width: 22,
height: 22
}, iconColorProps(ev.icon)))), /*#__PURE__*/React.createElement("div", {
className: "hist-body"
}, /*#__PURE__*/React.createElement("div", {
className: "hist-title"
}, /*#__PURE__*/React.createElement(Body1, {
weight: "medium"
}, ev.title), ev.tone === "success" && /*#__PURE__*/React.createElement("span", {
className: "hist-status"
}, /*#__PURE__*/React.createElement(CheckIcon, {
width: 16,
height: 16,
fill: "var(--color-sucess-green)"
})), ev.tone === "danger" && /*#__PURE__*/React.createElement("span", {
className: "sc-chip fail"
}, /*#__PURE__*/React.createElement(CloseIcon, {
width: 12,
height: 12,
fill: "var(--red-alert)"
}), "Failed")), ev.detail && /*#__PURE__*/React.createElement("span", {
className: "hist-line"
}, ev.detail), ev.author && /*#__PURE__*/React.createElement("span", {
className: "hist-line"
}, ev.author)), /*#__PURE__*/React.createElement("div", {
className: "hist-meta"
}, /*#__PURE__*/React.createElement("span", {
className: "hist-line"
}, formatAgo(ev.minutesAgo))))), !entry.pipelineSupported && /*#__PURE__*/React.createElement("div", {
className: "hist-note"
}, /*#__PURE__*/React.createElement(InfoSmallIcon, {
width: 15,
height: 15
}), /*#__PURE__*/React.createElement(Caption1, {
color: "secondary"
}, "No CI or deployment pipeline is configured for this repository.")))));
};
/* ================================================================== */
/* Source Code section — flat list, split open / completed */
/* ================================================================== */
const StyledSourceCode = styled.div.withConfig({
displayName: "SourceCodeSection__StyledSourceCode",
componentId: "sc-rvqbh5-2"
})(["font-family:", ";margin:0 0 16px;padding-bottom:16px;border-bottom:1px solid var(--border-primary);", " .sc-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:4px;}.sc-completed-row{display:flex;align-items:center;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-primary);}.sc-add{margin-top:8px;padding-top:12px;border-top:1px solid var(--border-primary);}.sc-add .c--add-to-list-btn svg{box-sizing:content-box;}.sc-item{padding:12px 0;}.sc-item + .sc-item{border-top:1px solid var(--border-primary);}.sc-row{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;}.sc-main{flex:1 1 0%;min-width:0;}.sc-name{display:flex;align-items:center;gap:8px;flex-wrap:nowrap;min-width:0;}.sc-lead{flex-shrink:0;display:inline-flex;color:var(--color-theme-600);}.sc-title{flex:0 1 auto;min-width:0;display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.sc-title.strike{text-decoration:line-through;}.sc-title.strike-link{text-decoration:line-through;}.sc-title.strike-link:hover{text-decoration:none;}.sc-sigs{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:10px;font-size:12px;color:var(--color-theme-600);}.sc-sig{display:inline-flex;align-items:center;gap:4px;}.sc-closing{display:inline-flex;align-items:center;flex-shrink:0;cursor:default;}.sc-sha{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}.sc-muted{color:var(--color-theme-600);}.sc-refs{display:inline-flex;align-items:center;gap:6px;}.sc-refs .ref-link{color:var(--color-primary);font-weight:500;}"], SANS, p => p.$flush && css(["margin:0;padding-bottom:0;border-bottom:none;"]));
/* Element name — a link that opens the object in a new tab. Keeps the row's
typography (inherits color + weight), turns primary on hover, link cursor. */
const TitleLink = styled.a.withConfig({
displayName: "SourceCodeSection__TitleLink",
componentId: "sc-rvqbh5-3"
})(["color:inherit;text-decoration:none;cursor:pointer;transition:color 200ms ease;&:hover{color:var(--color-primary);}"]);
/* Status label chip — the DS `Chip` styled to match the app's
`SourceCodeStatusChip`: a "done" tone (primary text on a translucent-primary
fill) and a "gone" tone (muted text on a bordered theme-200 fill). */
const CHIP_BASE_STYLE = {
height: "1.25rem",
paddingLeft: "0.5rem",
paddingRight: "0.5rem",
borderRadius: "0.375rem",
gap: "3px",
flexShrink: 0
};
const CHIP_DONE_STYLE = _extends({}, CHIP_BASE_STYLE, {
backgroundColor: "color-mix(in srgb, var(--color-primary) 15%, transparent)"
});
const CHIP_GONE_STYLE = _extends({}, CHIP_BASE_STYLE, {
backgroundColor: "var(--color-theme-200)",
border: "1px solid var(--border-primary)"
});
/* Info tone — the default (blue) InfoBox recipe: blueSky tint + solid border. */
const CHIP_INFO_STYLE = _extends({}, CHIP_BASE_STYLE, {
backgroundColor: colors.blueSky.color + "1A",
border: "1px solid " + colors.blueSky.color
});
const StatusChip = _ref5 => {
let label = _ref5.label,
tone = _ref5.tone,
glyph = _ref5.glyph;
return /*#__PURE__*/React.createElement(Chip, {
label: label,
startAdornment: glyph,
color: tone === "done" ? "var(--color-primary)" : tone === "info" ? colors.blueSky.lighter : undefined,
style: tone === "done" ? CHIP_DONE_STYLE : tone === "info" ? CHIP_INFO_STYLE : CHIP_GONE_STYLE,
typographyProps: {
variant: "Caption 1",
weight: "bold",
align: "center"
}
});
};
/* Contributor avatars — the app renders real user images through the DS
`AvatarGroup` (`ContributorAvatars.jsx`). The mock feeds that same component
initials avatars via `MockAvatar`, which honours AvatarGroup's cloneElement
contract (it injects `size`, `alt` and `style` — the overlap margin + paper
ring), so the grouping, overlap and "+N" overflow behave identically. */
const CONTRIBUTOR_NAMES = {
DA: "Dragana A.",
IS: "Ilija Studen",
MK: "Marko K."
};
const MockAvatar = /*#__PURE__*/React.forwardRef((_ref6, ref) => {
let _ref6$size = _ref6.size,
size = _ref6$size === void 0 ? 20 : _ref6$size,
bg = _ref6.bg,
initials = _ref6.initials,
alt = _ref6.alt,
rest = _objectWithoutPropertiesLoose(_ref6, _excluded);
return (
/*#__PURE__*/
// `...rest` carries the props AvatarGroup and its Tooltip inject via
// cloneElement — `style` (overlap margin + paper ring) and the hover handlers
// (`onMouseOver` / `onMouseLeave`) — plus the forwarded `ref`, all of which
// must reach the DOM node for the tooltip to open.
React.createElement(RoundAvatar, _extends({
ref: ref,
$bg: bg,
$size: size,
$bordered: false
}, rest), initials)
);
});
MockAvatar.displayName = "MockAvatar";
/* The app removes AvatarGroup's default padding with `tw-p-0`; the mock does
the same through a styled wrapper (`tw-` classes don't run in Storybook). */
const StyledAvatarGroup = styled(AvatarGroup).withConfig({
displayName: "SourceCodeSection__StyledAvatarGroup",
componentId: "sc-rvqbh5-4"
})(["&&{padding:0;}"]);
const ContributorAvatars = _ref7 => {
let contributors = _ref7.contributors;
return /*#__PURE__*/React.createElement(StyledAvatarGroup, {
limit: 4,
size: 20,
hasTooltip: true
}, contributors.map(c => {
var _CONTRIBUTOR_NAMES$c$;
return /*#__PURE__*/React.createElement(MockAvatar, {
key: c.initials,
bg: c.bg,
initials: c.initials,
alt: (_CONTRIBUTOR_NAMES$c$ = CONTRIBUTOR_NAMES[c.initials]) != null ? _CONTRIBUTOR_NAMES$c$ : c.initials
});
}));
};
const IssueRow = _ref8 => {
let issue = _ref8.issue;
const closed = issue.state === "closed";
return /*#__PURE__*/React.createElement("div", {
className: "sc-row"
}, /*#__PURE__*/React.createElement("div", {
className: "sc-main"
}, /*#__PURE__*/React.createElement("div", {
className: "sc-name"
}, /*#__PURE__*/React.createElement("span", {
className: "sc-lead",
style: {
color: closed ? "var(--color-primary)" : undefined
}
}, /*#__PURE__*/React.createElement(IssueStateIcon, {
closed: closed
})), /*#__PURE__*/React.createElement(Body1, {
weight: "medium",
className: "sc-title"
}, /*#__PURE__*/React.createElement(TitleLink, {
href: issueUrl(issue),
target: "_blank",
rel: "noopener noreferrer"
}, issue.title)), closed && /*#__PURE__*/React.createElement(StatusChip, {
label: "Closed",
tone: "done",
glyph: /*#__PURE__*/React.createElement(IssueStateIcon, {
closed: true,
size: 12
})
})), /*#__PURE__*/React.createElement("div", {
className: "sc-sigs"
}, /*#__PURE__*/React.createElement(Tooltip, {
title: "Repository: " + issue.repoFull
}, /*#__PURE__*/React.createElement("span", {
className: "sc-sig"
}, /*#__PURE__*/React.createElement(GitRepositoryIcon, {
width: 14,
height: 14
}), issue.repo)), /*#__PURE__*/React.createElement("span", {
className: "sc-muted"
}, issue.updatedLabel))));
};
const CodeStatusChip = _ref9 => {
let entry = _ref9.entry;
if (entry.status === "merged") {
return /*#__PURE__*/React.createElement(StatusChip, {
label: "Merged",
tone: "done",
glyph: /*#__PURE__*/React.createElement(GitPullRequestIcon, {
width: 12,
height: 12,
fill: "var(--color-primary)"
})
});
}
if (entry.status === "deleted") {
return /*#__PURE__*/React.createElement(StatusChip, {
label: "Deleted",
tone: "gone",
glyph: /*#__PURE__*/React.createElement(TrashIcon, {
width: 12,
height: 12,
fill: "var(--color-theme-900)"
})
});
}
if (entry.status === "closed") {
return /*#__PURE__*/React.createElement(StatusChip, {
label: "Closed",
tone: "gone"
});
}
// Open branches / PRs show no status label.
return null;
};
const CodeRow = _ref0 => {
let entry = _ref0.entry,
onOpenHistory = _ref0.onOpenHistory;
const deleted = entry.status === "deleted";
// A closed-without-merge PR is still a live link, but its name is struck
// through to signal the dead-end; the strike lifts on hover so it reads as a
// normal PR link.
const closedPr = entry.kind === "pr" && entry.status === "closed";
return /*#__PURE__*/React.createElement("div", {
className: "sc-row"
}, /*#__PURE__*/React.createElement("div", {
className: "sc-main"
}, /*#__PURE__*/React.createElement("div", {
className: "sc-name"
}, /*#__PURE__*/React.createElement("span", {
className: "sc-lead"
}, entry.kind === "pr" ? /*#__PURE__*/React.createElement(GitPullRequestIcon, {
width: 16,
height: 16
}) : /*#__PURE__*/React.createElement(GitBranchIcon, {
width: 16,
height: 16
})), deleted ? /*#__PURE__*/React.createElement(Body1, {
weight: "medium",
className: "sc-title strike"
}, entry.title) : /*#__PURE__*/React.createElement(Body1, {
weight: "medium",
className: closedPr ? "sc-title strike-link" : "sc-title"
}, /*#__PURE__*/React.createElement(TitleLink, {
href: codeUrl(entry),
target: "_blank",
rel: "noopener noreferrer"
}, entry.title)), /*#__PURE__*/React.createElement(CodeStatusChip, {
entry: entry
}), entry.kind === "pr" && entry.status === "open" && entry.closing && /*#__PURE__*/React.createElement(Tooltip, {
title: "Marks the task as completed when this pull request is merged."
}, /*#__PURE__*/React.createElement("span", {
className: "sc-closing"
}, /*#__PURE__*/React.createElement(StatusChip, {
label: "Closing",
tone: "info",
glyph: /*#__PURE__*/React.createElement(CheckIcon, {
width: 12,
height: 12,
fill: colors.blueSky.lighter
})
})))), /*#__PURE__*/React.createElement("div", {
className: "sc-sigs"
}, /*#__PURE__*/React.createElement(Tooltip, {
title: "Repository: " + entry.repoFull
}, /*#__PURE__*/React.createElement("span", {
className: "sc-sig"
}, /*#__PURE__*/React.createElement(GitRepositoryIcon, {
width: 14,
height: 14
}), entry.repo)), /*#__PURE__*/React.createElement(ContributorAvatars, {
contributors: entry.contributors
}), /*#__PURE__*/React.createElement(Tooltip, {
title: entry.commits.tip
}, /*#__PURE__*/React.createElement(Caption1, {
color: "tertiary",
className: "sc-sig"
}, /*#__PURE__*/React.createElement(GitCommitIcon, {
width: 14,
height: 14
}), entry.commits.value)), /*#__PURE__*/React.createElement(Tooltip, {
title: "Head commit: " + entry.headSha
}, /*#__PURE__*/React.createElement(Caption1, {
color: "tertiary",
className: "sc-sig sc-sha"
}, entry.headSha.slice(0, 8))), entry.diff && /*#__PURE__*/React.createElement(Tooltip, {
title: entry.diff.tip
}, /*#__PURE__*/React.createElement("span", {
className: "sc-sig sc-diff"
}, /*#__PURE__*/React.createElement(Caption1, {
color: "success"
}, "+", entry.diff.add), /*#__PURE__*/React.createElement(Caption1, {
color: "alert"
}, "\u2212", entry.diff.del))), entry.comments && /*#__PURE__*/React.createElement(Tooltip, {
title: entry.comments.tip
}, /*#__PURE__*/React.createElement(Caption1, {
color: "tertiary",
className: "sc-sig"
}, /*#__PURE__*/React.createElement(MessageSmallIcon, null), entry.comments.value)), /*#__PURE__*/React.createElement("span", {
className: "sc-muted"
}, entry.updatedLabel))), /*#__PURE__*/React.createElement(Spine, {
items: entry.stages,
onClick: onOpenHistory
}));
};
const CompletedHeader = _ref1 => {
let expanded = _ref1.expanded,
count = _ref1.count,
onToggle = _ref1.onToggle;
return /*#__PURE__*/React.createElement("div", {
className: "sc-completed-row"
}, /*#__PURE__*/React.createElement(Button, {
type: "button",
variant: "text colored",
onClick: onToggle,
"aria-expanded": expanded,
style: {
marginLeft: -8
}
}, expanded ? "Hide completed elements" : "Show " + count + " more completed elements"));
};
const SourceCodeSection = _ref10 => {
let _ref10$variant = _ref10.variant,
variant = _ref10$variant === void 0 ? "mix" : _ref10$variant,
issues = _ref10.issues,
code = _ref10.code,
_ref10$showHeader = _ref10.showHeader,
showHeader = _ref10$showHeader === void 0 ? true : _ref10$showHeader,
_ref10$showLink = _ref10.showLink,
showLink = _ref10$showLink === void 0 ? true : _ref10$showLink,
_ref10$flush = _ref10.flush,
flush = _ref10$flush === void 0 ? false : _ref10$flush;
const _useState = useState(null),
openEntry = _useState[0],
setOpenEntry = _useState[1];
const _useState2 = useState(false),
completedExpanded = _useState2[0],
setCompletedExpanded = _useState2[1];
const _useState3 = useState(false),
linkOpen = _useState3[0],
setLinkOpen = _useState3[1];
const issuesData = issues != null ? issues : ISSUES;
const codeData = code != null ? code : CODE_ENTRIES;
const openIssues = issuesData.filter(i => !issueCompleted(i)).sort(byRecency);
const openCode = codeData.filter(e => !codeCompleted(e)).sort(byRecency);
const completed = [...issuesData.filter(issueCompleted).map(issue => ({
key: issue.ref,
updatedMinsAgo: issue.updatedMinsAgo,
node: /*#__PURE__*/React.createElement(IssueRow, {
issue: issue
})
})), ...codeData.filter(codeCompleted).map(entry => ({
key: entry.repo + "-" + entry.title,
updatedMinsAgo: entry.updatedMinsAgo,
node: /*#__PURE__*/React.createElement(CodeRow, {
entry: entry,
onOpenHistory: () => setOpenEntry(entry)
})
}))].sort(byRecency);
const showOpen = variant === "mix" || variant === "open";
const showCompleted = variant === "mix" || variant === "completed";
// In the "completed" variant there is no collapsible header and everything is
// shown; in "mix" the section is collapsed by default.
const completedVisible = variant === "completed" ? true : completedExpanded;
return /*#__PURE__*/React.createElement(StyledSourceCode, {
$flush: flush
}, showHeader && /*#__PURE__*/React.createElement("div", {
className: "sc-header"
}, /*#__PURE__*/React.createElement(Body1, {
weight: "bold"
}, "Source Code"), /*#__PURE__*/React.createElement(LinkingCommandsButton, null)), showOpen && openIssues.map(issue => /*#__PURE__*/React.createElement("div", {
className: "sc-item",
key: issue.ref
}, /*#__PURE__*/React.createElement(IssueRow, {
issue: issue
}))), showOpen && openCode.map(entry => /*#__PURE__*/React.createElement("div", {
className: "sc-item",
key: entry.repo + "-" + entry.title
}, /*#__PURE__*/React.createElement(CodeRow, {
entry: entry,
onOpenHistory: () => setOpenEntry(entry)
}))), showCompleted && completedVisible && completed.map(c => /*#__PURE__*/React.createElement("div", {
className: "sc-item",
key: c.key
}, c.node)), showCompleted && completed.length > 0 && variant === "mix" && /*#__PURE__*/React.createElement(CompletedHeader, {
expanded: completedExpanded,
count: completed.length,
onToggle: () => setCompletedExpanded(v => !v)
}), showLink && /*#__PURE__*/React.createElement("div", {
className: "sc-add"
}, /*#__PURE__*/React.createElement(AddToListButton, {
text: "Link Branch or Pull Request",
style: {
marginLeft: -4
},
onClick: () => setLinkOpen(true)
})), /*#__PURE__*/React.createElement(HistoryDialog, {
entry: openEntry,
onClose: () => setOpenEntry(null)
}), /*#__PURE__*/React.createElement(LinkBranchOrPullRequestDialog, {
open: linkOpen,
onClose: () => setLinkOpen(false)
}));
};
export { Spine, HistoryDialog, IssueRow, CodeRow, CompletedHeader, SourceCodeSection };
//# sourceMappingURL=SourceCodeSection.js.map