@activecollab/components
Version:
ActiveCollab Components
354 lines (342 loc) • 17.2 kB
JavaScript
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { Button } from "../../components/Button";
import { ChooseV2 } from "../../components/ChooseV2";
import { Dialog } from "../../components/Dialog";
import { IconButton } from "../../components/IconButton";
import { CancelCrossIcon, CommandLineIcon, CopyIcon, HashIcon } from "../../components/Icons";
import { SkeletonLoader } from "../../components/Loaders";
import { Tooltip } from "../../components/Tooltip";
import { Body2, Caption1, Header3 } from "../../components/Typography";
/**
* "Linking and Commands" button — a plain design-system secondary button (small,
* rounded, no icon, no AI gradient: this is not an AI feature). Clicking it opens
* a read-only reference dialog.
*
* This mock mirrors the shipped `activecollab-next` implementation
* (`src/components/tasks/SourceCode/LinkingAndCommandsButton.jsx` +
* `LinkingAndCommandsDialog.jsx`, task #457754): a 640px dialog with a short
* pitch, a centered ChooseV2 to switch conventions (Linking default), and two
* tabs. Linking shows the reference forms for this task and a copyable
* `git checkout -b …` suggestion; Commands shows an API-shaped table of action
* words and slash commands, plus a fixed example commit. Both the reference
* guide and the suggested-branch request are represented by short skeleton
* loads, the way the real dialog fetches them once it is opened.
*/
const SANS = '-apple-system, BlinkMacSystemFont, "Roboto", "Helvetica Neue", Arial, sans-serif';
const MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
/** The example task these instructions are written against. */
const TASK_ID = 4231;
/** Task URL, in production built with `~/utils/urls` `getTaskUrl`. */
const TASK_URL = "https://app.activecollab.com/1/projects/2110/tasks/" + TASK_ID;
/**
* Suggested branch name for this task — in production it is resolved per repo
* via GET .../tasks/{task_id}/source/suggest-branch-name. Mocked here.
*/
const SUGGESTED_BRANCH = "ilija/" + TASK_ID + "-add-cacak-region-support";
/** POSIX-safe single-quote escaping for a value dropped into a shell command. */
const shellQuote = value => "'" + value.replace(/'/g, "'\\''") + "'";
/* ------------------------------------------------------------------ */
/* Reference-guide data — shaped like the GET .../source/reference-guide */
/* payload the real dialog is driven by: a map of action words plus a */
/* flat list of slash commands. Descriptions are localised server-side; */
/* tokens and examples never translate. */
/* ------------------------------------------------------------------ */
/** Action words act on the linked task from a title, commit, or PR body. */
const ACTION_WORDS = {
close: {
primary: "Close",
example: "Close #" + TASK_ID,
description: "Closes the task when its pull request is merged.",
aliases: ["Closes", "Fixes", "Fixed", "Resolves", "Resolved"]
}
};
/** Slash commands act on the task from a commit message or PR description. */
const COMMANDS = [{
name: "spend",
syntax: "/spend {duration} [of {job type}]",
example: "/spend 1h 30m of Design",
description: "Logs the time you spent on the task."
}, {
name: "estimate",
syntax: "/estimate {duration}",
example: "/estimate 4h",
description: "Sets the task's time estimate."
}, {
name: "comment",
syntax: "/comment {text}",
example: "/comment Deployed to staging",
description: "Posts a comment on the task."
}, {
name: "done",
syntax: "/done",
example: "/done",
description: "Marks the task complete.",
aliases: ["/complete"]
}, {
name: "reopen",
syntax: "/reopen",
example: "/reopen",
description: "Reopens a completed task."
}, {
name: "assign",
syntax: "/assign @{member}",
example: "/assign @ilija",
description: "Assigns the task to a team member."
}, {
name: "label",
syntax: "/label {name}",
example: "/label In Progress",
description: "Adds a label to the task."
}];
/** Action words first, then the slash commands — the shipped row order. */
const TABLE_ROWS = [...Object.values(ACTION_WORDS).map(word => ({
key: "action-" + word.primary,
example: word.example,
description: word.description,
aliases: word.aliases
})), ...COMMANDS.map(command => ({
key: "command-" + command.name,
example: command.example,
format: command.syntax,
description: command.description,
aliases: command.aliases
}))];
/**
* Illustrative commit message: closes the task on the subject line, logs time,
* leaves a note. Fixed sample text — only the task id is interpolated, and
* durations are printed verbatim (never reformatted).
*/
const EXAMPLE_COMMIT = "Fix ranks reset when a contract is archived, closes #" + TASK_ID + "\n\n/spend 1h 30m\n/comment Verified on staging, ranks hold after archive";
/* ------------------------------------------------------------------ */
/* Trigger button — plain DS secondary button, rounded to the compact */
/* "Suggest Subtasks" shape (6px), no icon, no gradient. */
/* ------------------------------------------------------------------ */
const TriggerButton = styled(Button).withConfig({
displayName: "LinkingCommandsButton__TriggerButton",
componentId: "sc-11q4kcm-0"
})(["&&{border-radius:6px;}"]);
/** Wider than the default 540px dialog, matching the shipped 640px width. */
const StyledDialog = styled(Dialog).withConfig({
displayName: "LinkingCommandsButton__StyledDialog",
componentId: "sc-11q4kcm-1"
})(["&&{width:640px;max-width:calc(100vw - 64px);}"]);
const StyledTitleRow = styled.div.withConfig({
displayName: "LinkingCommandsButton__StyledTitleRow",
componentId: "sc-11q4kcm-2"
})(["display:flex;align-items:center;justify-content:space-between;gap:12px;"]);
const StyledBody = styled.div.withConfig({
displayName: "LinkingCommandsButton__StyledBody",
componentId: "sc-11q4kcm-3"
})(["font-family:", ";display:flex;flex-direction:column;gap:16px;max-height:60vh;overflow-y:auto;.code{font-family:", ";background:var(--color-theme-200);color:var(--color-theme-900);border-radius:4px;padding:2px 6px;font-size:12px;}.lc-term{font-weight:600;color:var(--color-theme-900);}.lc-leads{display:flex;flex-direction:column;gap:8px;}.lc-lead{display:flex;align-items:flex-start;gap:8px;}.lc-lead svg{flex-shrink:0;width:16px;height:16px;margin-top:2px;color:var(--color-theme-600);}.lc-choosectl > div{justify-content:center;}.lc-tabbody{display:flex;flex-direction:column;gap:20px;}.lc-commands{gap:16px;}.lc-block{display:flex;flex-direction:column;gap:8px;}.lc-list{display:flex;flex-direction:column;gap:6px;margin:0;padding-left:20px;list-style:disc;}.lc-table{display:grid;grid-template-columns:max-content 1fr;column-gap:24px;}.lc-th{padding-bottom:8px;border-bottom:1px solid var(--border-primary);}.lc-td{padding:12px 0;border-bottom:1px solid var(--border-primary);}.lc-format{display:block;margin-top:4px;font-family:", ";}.lc-aliases{margin-top:12px;}.lc-skeleton{display:flex;flex-direction:column;gap:12px;}.lc-skel{border-radius:6px;}.lc-skel-line{height:20px;}.lc-skel-block{height:36px;width:100%;}.lc-skel-2-3{width:66%;}.lc-skel-1-2{width:50%;}.lc-branch-skel{width:100%;height:36px;border-radius:6px;}"], SANS, MONO, MONO);
/* Copyable code snippet — mirrors the shared CopySnippet: a theme-200 <pre>
with a copy control that reveals on hover (hidden when hideCopyButton). */
const StyledSnippet = styled.div.withConfig({
displayName: "LinkingCommandsButton__StyledSnippet",
componentId: "sc-11q4kcm-4"
})(["position:relative;pre{margin:0;background:var(--color-theme-200);color:var(--color-theme-900);border-radius:6px;padding:12px 16px;overflow-x:auto;font-family:", ";font-size:13px;line-height:1.5;}.lc-copy-btn{position:absolute;top:6px;right:6px;opacity:0;transition:opacity 0.15s ease;}&:hover .lc-copy-btn{opacity:1;}.lc-copy-btn svg{width:16px;height:16px;}"], MONO);
const TAB_OPTIONS = [{
id: "linking",
name: "Linking"
}, {
id: "commands",
name: "Commands"
}];
const CopySnippet = _ref => {
let code = _ref.code,
_ref$hideCopyButton = _ref.hideCopyButton,
hideCopyButton = _ref$hideCopyButton === void 0 ? false : _ref$hideCopyButton;
const _useState = useState(false),
copied = _useState[0],
setCopied = _useState[1];
const copy = () => {
var _navigator$clipboard;
// eslint-disable-next-line compat/compat
void ((_navigator$clipboard = navigator.clipboard) == null ? void 0 : _navigator$clipboard.writeText(code));
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
};
return /*#__PURE__*/React.createElement(StyledSnippet, null, /*#__PURE__*/React.createElement("pre", null, /*#__PURE__*/React.createElement("code", null, code)), !hideCopyButton && /*#__PURE__*/React.createElement(Tooltip, {
title: copied ? "Copied!" : "Copy"
}, /*#__PURE__*/React.createElement("button", {
type: "button",
className: "lc-copy-btn",
onClick: copy,
"aria-label": "Copy command",
style: {
border: "none",
background: "transparent",
cursor: "pointer",
padding: 4,
display: "inline-flex",
color: "var(--color-theme-600)"
}
}, /*#__PURE__*/React.createElement(CopyIcon, null))));
};
const LinkingTab = _ref2 => {
let branchLoading = _ref2.branchLoading,
branch = _ref2.branch;
const command = branch ? "git checkout -b " + shellQuote(branch) : "";
return /*#__PURE__*/React.createElement("div", {
className: "lc-tabbody"
}, /*#__PURE__*/React.createElement("div", {
className: "lc-block"
}, /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, "Mention the task in your git activity and it links to this task on its own:"), /*#__PURE__*/React.createElement("ul", {
className: "lc-list"
}, /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, /*#__PURE__*/React.createElement("span", {
className: "lc-term"
}, "In a branch name:"), " the raw task ID", " ", /*#__PURE__*/React.createElement("span", {
className: "code"
}, TASK_ID))), /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, /*#__PURE__*/React.createElement("span", {
className: "lc-term"
}, "In a title, commit message, or description:"), " ", /*#__PURE__*/React.createElement("span", {
className: "code"
}, "#", TASK_ID), " or", " ", /*#__PURE__*/React.createElement("span", {
className: "code"
}, "AC-", TASK_ID))), /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, /*#__PURE__*/React.createElement("span", {
className: "lc-term"
}, "Anywhere:"), " the task URL", " ", /*#__PURE__*/React.createElement("span", {
className: "code"
}, TASK_URL))))), branchLoading || command ? /*#__PURE__*/React.createElement("div", {
className: "lc-block"
}, /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, "Give the branch a name with the task number in it and it links the moment you push. Create one from the suggested name:"), branchLoading ? /*#__PURE__*/React.createElement(SkeletonLoader, {
className: "lc-branch-skel"
}) : /*#__PURE__*/React.createElement(CopySnippet, {
code: command
})) : null);
};
const CommandsTab = () => /*#__PURE__*/React.createElement("div", {
className: "lc-tabbody lc-commands"
}, /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, "The table below lists action words and slash commands that you can use in a commit message or a pull request description to act on the linked task. Example:"), /*#__PURE__*/React.createElement(CopySnippet, {
code: EXAMPLE_COMMIT,
hideCopyButton: true
}), /*#__PURE__*/React.createElement("div", {
className: "lc-table"
}, /*#__PURE__*/React.createElement(Caption1, {
color: "tertiary",
className: "lc-th"
}, "Command"), /*#__PURE__*/React.createElement(Caption1, {
color: "tertiary",
className: "lc-th"
}, "What it does"), TABLE_ROWS.map(row => {
var _row$aliases;
return /*#__PURE__*/React.createElement(React.Fragment, {
key: row.key
}, /*#__PURE__*/React.createElement("div", {
className: "lc-td"
}, /*#__PURE__*/React.createElement("span", {
className: "code"
}, row.example), row.format ? /*#__PURE__*/React.createElement(Caption1, {
color: "tertiary",
className: "lc-format"
}, row.format) : null), /*#__PURE__*/React.createElement("div", {
className: "lc-td"
}, /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, row.description), (_row$aliases = row.aliases) != null && _row$aliases.length ? /*#__PURE__*/React.createElement(Body2, {
color: "secondary",
className: "lc-aliases"
}, "Aliases: ", row.aliases.join(", ")) : null));
})));
export const LinkingCommandsButton = () => {
const _useState2 = useState(false),
open = _useState2[0],
setOpen = _useState2[1];
const _useState3 = useState("linking"),
tab = _useState3[0],
setTab = _useState3[1];
const _useState4 = useState(true),
guideLoading = _useState4[0],
setGuideLoading = _useState4[1];
const _useState5 = useState(true),
branchLoading = _useState5[0],
setBranchLoading = _useState5[1];
const _useState6 = useState(null),
suggestedBranch = _useState6[0],
setSuggestedBranch = _useState6[1];
// Mock the two endpoint loads the real dialog fires once it opens: the
// reference guide resolves first (~0.6s), then the suggested branch name
// (~1.1s), each showing its own skeleton until it lands.
useEffect(() => {
if (!open) return undefined;
setGuideLoading(true);
setBranchLoading(true);
setSuggestedBranch(null);
const guideTimer = window.setTimeout(() => setGuideLoading(false), 600);
const branchTimer = window.setTimeout(() => {
setSuggestedBranch(SUGGESTED_BRANCH);
setBranchLoading(false);
}, 1100);
return () => {
window.clearTimeout(guideTimer);
window.clearTimeout(branchTimer);
};
}, [open]);
const openDialog = () => {
setTab("linking");
setOpen(true);
};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TriggerButton, {
type: "button",
variant: "secondary",
size: "small",
onClick: openDialog
}, "Linking and Commands"), /*#__PURE__*/React.createElement(StyledDialog, {
open: open,
onClose: () => setOpen(false),
enableBackgroundClick: true
}, /*#__PURE__*/React.createElement(Dialog.Title, {
disableDefaultHeading: true
}, /*#__PURE__*/React.createElement(StyledTitleRow, null, /*#__PURE__*/React.createElement(Header3, null, "Linking and Commands"), /*#__PURE__*/React.createElement(IconButton, {
type: "button",
variant: "text gray",
"aria-label": "Close",
onClick: () => setOpen(false)
}, /*#__PURE__*/React.createElement(CancelCrossIcon, null)))), /*#__PURE__*/React.createElement(Dialog.ContentDivider, null), /*#__PURE__*/React.createElement(Dialog.Content, null, /*#__PURE__*/React.createElement(StyledBody, null, guideLoading ? /*#__PURE__*/React.createElement("div", {
className: "lc-skeleton"
}, /*#__PURE__*/React.createElement(SkeletonLoader, {
className: "lc-skel lc-skel-line lc-skel-2-3"
}), /*#__PURE__*/React.createElement(SkeletonLoader, {
className: "lc-skel lc-skel-block"
}), /*#__PURE__*/React.createElement(SkeletonLoader, {
className: "lc-skel lc-skel-block"
}), /*#__PURE__*/React.createElement(SkeletonLoader, {
className: "lc-skel lc-skel-line lc-skel-1-2"
})) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Body2, null, "Keep a task in sync with your code without leaving your editor."), /*#__PURE__*/React.createElement("div", {
className: "lc-leads"
}, /*#__PURE__*/React.createElement("div", {
className: "lc-lead"
}, /*#__PURE__*/React.createElement(HashIcon, null), /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, /*#__PURE__*/React.createElement("span", {
className: "lc-term"
}, "Linking:"), " Mention a task in a branch, pull request, or commit and they will be connected automatically.")), /*#__PURE__*/React.createElement("div", {
className: "lc-lead"
}, /*#__PURE__*/React.createElement(CommandLineIcon, null), /*#__PURE__*/React.createElement(Body2, {
color: "secondary"
}, /*#__PURE__*/React.createElement("span", {
className: "lc-term"
}, "Commands:"), " Add short instructions in a commit or pull request description to act on the task: log time, close it, or leave a comment."))), /*#__PURE__*/React.createElement(ChooseV2, {
className: "lc-choosectl",
options: TAB_OPTIONS,
selected: [tab],
onChange: ids => setTab(ids[0])
}), tab === "linking" ? /*#__PURE__*/React.createElement(LinkingTab, {
branchLoading: branchLoading,
branch: suggestedBranch
}) : /*#__PURE__*/React.createElement(CommandsTab, null))))));
};
LinkingCommandsButton.displayName = "LinkingCommandsButton";
//# sourceMappingURL=LinkingCommandsButton.js.map