UNPKG

@activecollab/components

Version:

ActiveCollab Components

354 lines (342 loc) 17.2 kB
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