UNPKG

@activecollab/components

Version:

ActiveCollab Components

1,312 lines (1,284 loc) 46.2 kB
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