mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-02 19:11:39 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95ac3e310a | |||
| 9b524e3dc7 | |||
| bdf78254b5 | |||
| c5545c8781 | |||
| 5004c395de | |||
| ea3587f28d | |||
| 24e578db5f | |||
| 12562aa076 | |||
| 5a74a989b5 | |||
| a6558329e2 | |||
| 54a931d94d | |||
| 5229534d8f | |||
| 78b3996f47 | |||
| d9f7bf7fdd | |||
| 45c410dd4c | |||
| 80e56281b2 | |||
| 125eae052b | |||
| 6f52bb7533 | |||
| 8724260eb4 | |||
| f32e9f7704 | |||
| 83c8371e94 | |||
| 5f14d90ccd | |||
| ff0d8c03b0 |
@@ -19,10 +19,12 @@ Generate formatted markdown release notes for a Yaak tag.
|
|||||||
- `gh pr view <PR_NUMBER> --json number,title,body,author,url`
|
- `gh pr view <PR_NUMBER> --json number,title,body,author,url`
|
||||||
5. Extract useful details:
|
5. Extract useful details:
|
||||||
- Feedback URLs (`feedback.yaak.app`)
|
- Feedback URLs (`feedback.yaak.app`)
|
||||||
|
- Contributor GitHub handles from `author.login`
|
||||||
- Plugin install links or other notable context
|
- Plugin install links or other notable context
|
||||||
6. Format notes using Yaak style:
|
6. Format notes using Yaak style:
|
||||||
- Changelog badge at top
|
- Changelog badge at top
|
||||||
- Bulleted items with PR links where available
|
- Bulleted items with PR links where available
|
||||||
|
- Contributor handles for external PRs
|
||||||
- Feedback links where available
|
- Feedback links where available
|
||||||
- Full changelog compare link at bottom
|
- Full changelog compare link at bottom
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ Generate formatted markdown release notes for a Yaak tag.
|
|||||||
- Wrap final notes in a markdown code fence.
|
- Wrap final notes in a markdown code fence.
|
||||||
- Keep a blank line before and after the code fence.
|
- Keep a blank line before and after the code fence.
|
||||||
- Output the markdown code block last.
|
- Output the markdown code block last.
|
||||||
|
- Append contributor attribution to PR-backed bullets for non-`@gschier` authors, using `by [@handle](https://github.com/handle)`.
|
||||||
- Do not append `by @gschier` for PRs authored by `@gschier`.
|
- Do not append `by @gschier` for PRs authored by `@gschier`.
|
||||||
- These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
|
- These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
|
|
||||||
## Submission
|
## Submission
|
||||||
|
|
||||||
- [ ] This PR is a bug fix or small-scope improvement.
|
- [ ] This PR is a bug fix.
|
||||||
- [ ] If this PR is not a bug fix or small-scope improvement, I linked an approved feedback item below.
|
- [ ] If this PR is not a bug fix, I linked the feedback item where @gschier explicitly gave me permission to work on it.
|
||||||
- [ ] I have read and followed [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
- [ ] I have read and followed [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||||
- [ ] I tested this change locally.
|
- [ ] I tested this change locally.
|
||||||
- [ ] I added or updated tests when reasonable.
|
- [ ] I added or updated tests when reasonable.
|
||||||
|
- [ ] I added screenshots or recordings for UI changes when reasonable.
|
||||||
|
|
||||||
Approved feedback item (required if not a bug fix or small-scope improvement):
|
Explicit permission feedback item (required if not a bug fix):
|
||||||
|
|
||||||
<!-- https://yaak.app/feedback/... -->
|
<!-- https://yaak.app/feedback/... -->
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,69 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
|
|
||||||
const COMMENT_MARKER = "<!-- yaak-contribution-policy -->";
|
const COMMENT_MARKER = "<!-- yaak-contribution-policy -->";
|
||||||
|
|
||||||
const MAINTAINER_LOGINS = new Set(["gschier"]);
|
const MAINTAINER_LOGINS = new Set(["gschier"]);
|
||||||
const MAINTAINER_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
const MAINTAINER_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
||||||
const MAINTAINER_PERMISSIONS = new Set(["admin", "maintain", "write"]);
|
const MAINTAINER_PERMISSIONS = new Set(["admin", "maintain", "write"]);
|
||||||
|
const REVIEWER_LOGIN = "gschier";
|
||||||
|
|
||||||
const LARGE_DIFF_CHANGED_FILES = 20;
|
const LARGE_DIFF_CHANGED_FILES = 20;
|
||||||
const LARGE_DIFF_CHANGED_LINES = 800;
|
const LARGE_DIFF_CHANGED_LINES = 800;
|
||||||
|
const SUMMARY_TITLE_MAX_LENGTH = 80;
|
||||||
|
const AUTOMATIC_PR_CREATED_AFTER = "2026-06-30T07:00:00.000Z";
|
||||||
|
const AUTOMATIC_PR_CREATED_AFTER_LABEL = "June 30, 2026";
|
||||||
|
|
||||||
const LABELS = {
|
const LABELS = {
|
||||||
accepted: {
|
inScope: {
|
||||||
name: "contribution: accepted",
|
name: "contribution: in scope",
|
||||||
color: "0E8A16",
|
color: "0E8A16",
|
||||||
description: "Community PR appears to match Yaak's contribution policy.",
|
description: "Community PR appears to be in scope for maintainer review.",
|
||||||
},
|
},
|
||||||
approvedFeedback: {
|
outOfScope: {
|
||||||
name: "contribution: approved feedback",
|
name: "contribution: out of scope",
|
||||||
color: "5319E7",
|
|
||||||
description: "Community PR links an approved feedback item.",
|
|
||||||
},
|
|
||||||
needsTemplate: {
|
|
||||||
name: "contribution: needs template",
|
|
||||||
color: "D93F0B",
|
|
||||||
description: "Community PR needs a completed pull request template.",
|
|
||||||
},
|
|
||||||
needsApproval: {
|
|
||||||
name: "contribution: needs approval",
|
|
||||||
color: "B60205",
|
color: "B60205",
|
||||||
description: "Community PR needs an approved feedback item before review.",
|
description: "Community PR does not match Yaak's contribution policy.",
|
||||||
},
|
},
|
||||||
largeDiff: {
|
explicitPermission: {
|
||||||
name: "contribution: large diff",
|
name: "contribution: explicit permission",
|
||||||
|
color: "5319E7",
|
||||||
|
description:
|
||||||
|
"Community PR links feedback where @gschier explicitly allowed the work.",
|
||||||
|
},
|
||||||
|
missingTemplate: {
|
||||||
|
name: "contribution: missing template",
|
||||||
|
color: "D93F0B",
|
||||||
|
description:
|
||||||
|
"Community PR is missing enough of the pull request template to review.",
|
||||||
|
},
|
||||||
|
policyUnmet: {
|
||||||
|
name: "contribution: policy unmet",
|
||||||
|
color: "B60205",
|
||||||
|
description:
|
||||||
|
"Community PR does not currently satisfy the contribution policy.",
|
||||||
|
},
|
||||||
|
needsScopeReview: {
|
||||||
|
name: "contribution: needs scope review",
|
||||||
color: "FBCA04",
|
color: "FBCA04",
|
||||||
description:
|
description:
|
||||||
"Community PR has a larger-than-usual diff for a small-scope contribution.",
|
"Community PR may be broader than Yaak's bug-fix contribution policy.",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MANAGED_LABEL_NAMES = Object.values(LABELS).map((label) => label.name);
|
const MANAGED_LABEL_NAMES = [
|
||||||
|
...new Set(Object.values(LABELS).map((label) => label.name)),
|
||||||
|
];
|
||||||
|
|
||||||
const CHECKBOXES = {
|
const CHECKBOXES = {
|
||||||
smallScope: "This PR is a bug fix or small-scope improvement.",
|
bugFix: "This PR is a bug fix.",
|
||||||
approvedFeedback:
|
explicitPermission:
|
||||||
"If this PR is not a bug fix or small-scope improvement, I linked an approved feedback item below.",
|
"If this PR is not a bug fix, I linked the feedback item where @gschier explicitly gave me permission to work on it.",
|
||||||
readContributing:
|
readContributing:
|
||||||
"I have read and followed [`CONTRIBUTING.md`](CONTRIBUTING.md).",
|
"I have read and followed [`CONTRIBUTING.md`](CONTRIBUTING.md).",
|
||||||
testedLocally: "I tested this change locally.",
|
testedLocally: "I tested this change locally.",
|
||||||
testsUpdated: "I added or updated tests when reasonable.",
|
testsUpdated: "I added or updated tests when reasonable.",
|
||||||
|
screenshotsAdded:
|
||||||
|
"I added screenshots or recordings for UI changes when reasonable.",
|
||||||
};
|
};
|
||||||
|
|
||||||
function escapeRegExp(value) {
|
function escapeRegExp(value) {
|
||||||
@@ -77,19 +95,30 @@ function hasMeaningfulText(value) {
|
|||||||
return stripComments(value || "").length > 0;
|
return stripComments(value || "").length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkboxState(body, label) {
|
function normalizeCheckboxLabel(label) {
|
||||||
const flexibleLabel = escapeRegExp(label).replace(/\\ /g, "\\s+");
|
return label
|
||||||
const pattern = new RegExp(
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||||
`^\\s*[-*]\\s*\\[([ xX])\\]\\s*${flexibleLabel}\\s*$`,
|
.replace(/`/g, "")
|
||||||
"im",
|
.replace(/\s+/g, " ")
|
||||||
);
|
.trim();
|
||||||
const match = body.match(pattern);
|
}
|
||||||
|
|
||||||
if (match == null) {
|
function checkboxState(body, label) {
|
||||||
return null;
|
const expectedLabel = normalizeCheckboxLabel(label);
|
||||||
|
|
||||||
|
for (const line of body.split("\n")) {
|
||||||
|
const match = line.match(/^\s*[-*]\s*\[([ xX])\]\s*(.*?)\s*$/i);
|
||||||
|
|
||||||
|
if (match == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizeCheckboxLabel(match[2]) === expectedLabel) {
|
||||||
|
return match[1].toLowerCase() === "x";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return match[1].toLowerCase() === "x";
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findFeedbackUrl(body) {
|
function findFeedbackUrl(body) {
|
||||||
@@ -100,8 +129,13 @@ function findFeedbackUrl(body) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLabelNames(pr) {
|
||||||
|
return new Set((pr.labels || []).map((label) => label.name));
|
||||||
|
}
|
||||||
|
|
||||||
function analyzePullRequest(pr) {
|
function analyzePullRequest(pr) {
|
||||||
const body = normalizeBody(pr.body);
|
const body = normalizeBody(pr.body);
|
||||||
|
const labelNames = getLabelNames(pr);
|
||||||
const states = Object.fromEntries(
|
const states = Object.fromEntries(
|
||||||
Object.entries(CHECKBOXES).map(([key, label]) => [
|
Object.entries(CHECKBOXES).map(([key, label]) => [
|
||||||
key,
|
key,
|
||||||
@@ -123,9 +157,38 @@ function analyzePullRequest(pr) {
|
|||||||
changedFiles > LARGE_DIFF_CHANGED_FILES ||
|
changedFiles > LARGE_DIFF_CHANGED_FILES ||
|
||||||
totalChangedLines > LARGE_DIFF_CHANGED_LINES;
|
totalChangedLines > LARGE_DIFF_CHANGED_LINES;
|
||||||
|
|
||||||
|
if (labelNames.has(LABELS.outOfScope.name)) {
|
||||||
|
return {
|
||||||
|
blockers: [
|
||||||
|
{
|
||||||
|
label: LABELS.outOfScope.name,
|
||||||
|
message: "Marked out of scope by maintainer label.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
changedFiles,
|
||||||
|
desiredLabels: [LABELS.outOfScope.name],
|
||||||
|
largeDiff,
|
||||||
|
status: "out_of_scope",
|
||||||
|
templateUsed,
|
||||||
|
totalChangedLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelNames.has(LABELS.inScope.name)) {
|
||||||
|
return {
|
||||||
|
blockers: [],
|
||||||
|
changedFiles,
|
||||||
|
desiredLabels: [LABELS.inScope.name],
|
||||||
|
largeDiff,
|
||||||
|
status: "in_scope",
|
||||||
|
templateUsed,
|
||||||
|
totalChangedLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!templateUsed) {
|
if (!templateUsed) {
|
||||||
blockers.push({
|
blockers.push({
|
||||||
label: LABELS.needsTemplate.name,
|
label: LABELS.missingTemplate.name,
|
||||||
message:
|
message:
|
||||||
"Update the PR description with the repository pull request template.",
|
"Update the PR description with the repository pull request template.",
|
||||||
});
|
});
|
||||||
@@ -133,70 +196,83 @@ function analyzePullRequest(pr) {
|
|||||||
const summary = getSection(body, "Summary");
|
const summary = getSection(body, "Summary");
|
||||||
const hasSummary = hasMeaningfulText(summary);
|
const hasSummary = hasMeaningfulText(summary);
|
||||||
const feedbackUrl = findFeedbackUrl(body);
|
const feedbackUrl = findFeedbackUrl(body);
|
||||||
const smallScope = states.smallScope === true;
|
const bugFix = states.bugFix === true;
|
||||||
const approvedFeedback = states.approvedFeedback === true;
|
const explicitPermission = states.explicitPermission === true;
|
||||||
|
|
||||||
if (!hasSummary) {
|
if (!hasSummary) {
|
||||||
blockers.push({
|
blockers.push({
|
||||||
label: LABELS.needsTemplate.name,
|
label: LABELS.policyUnmet.name,
|
||||||
message: "Add a short summary describing the bug fix or improvement.",
|
message:
|
||||||
|
"Add a short summary describing the bug fix or permitted change.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smallScope && approvedFeedback) {
|
if (bugFix && explicitPermission) {
|
||||||
blockers.push({
|
blockers.push({
|
||||||
label: LABELS.needsTemplate.name,
|
label: LABELS.policyUnmet.name,
|
||||||
message:
|
message:
|
||||||
"Choose either the small-scope checkbox or the approved-feedback checkbox, not both.",
|
"Choose either the bug-fix checkbox or the explicit-permission checkbox, not both.",
|
||||||
});
|
});
|
||||||
} else if (!smallScope && !approvedFeedback) {
|
} else if (!bugFix && !explicitPermission) {
|
||||||
blockers.push({
|
blockers.push({
|
||||||
label: LABELS.needsTemplate.name,
|
label: LABELS.policyUnmet.name,
|
||||||
message:
|
message:
|
||||||
"Check whether this is a bug fix or small-scope improvement, or confirm that an approved feedback item is linked.",
|
"Check whether this is a bug fix, or confirm that explicit permission from @gschier is linked.",
|
||||||
});
|
});
|
||||||
} else if (approvedFeedback && feedbackUrl == null) {
|
} else if (explicitPermission && feedbackUrl == null) {
|
||||||
blockers.push({
|
blockers.push({
|
||||||
label: LABELS.needsApproval.name,
|
label: LABELS.policyUnmet.name,
|
||||||
message:
|
message:
|
||||||
"Link the approved feedback item where contribution approval was explicitly stated.",
|
"Link the feedback item where @gschier explicitly gave you permission to work on this.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (states.readContributing !== true) {
|
if (states.readContributing !== true) {
|
||||||
blockers.push({
|
blockers.push({
|
||||||
label: LABELS.needsTemplate.name,
|
label: LABELS.policyUnmet.name,
|
||||||
message: "Confirm that `CONTRIBUTING.md` was read and followed.",
|
message: "Confirm that `CONTRIBUTING.md` was read and followed.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (states.testedLocally !== true) {
|
if (states.testedLocally !== true) {
|
||||||
blockers.push({
|
blockers.push({
|
||||||
label: LABELS.needsTemplate.name,
|
label: LABELS.policyUnmet.name,
|
||||||
message: "Confirm that the change was tested locally.",
|
message: "Confirm that the change was tested locally.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (states.testsUpdated !== true) {
|
if (states.testsUpdated !== true) {
|
||||||
blockers.push({
|
blockers.push({
|
||||||
label: LABELS.needsTemplate.name,
|
label: LABELS.policyUnmet.name,
|
||||||
message: "Confirm that tests were added or updated when reasonable.",
|
message: "Confirm that tests were added or updated when reasonable.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (states.screenshotsAdded !== true) {
|
||||||
|
blockers.push({
|
||||||
|
label: LABELS.policyUnmet.name,
|
||||||
|
message:
|
||||||
|
"Confirm that screenshots or recordings were added for UI changes when reasonable.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const desiredLabels = new Set(blockers.map((blocker) => blocker.label));
|
const desiredLabels = new Set();
|
||||||
|
|
||||||
if (blockers.length === 0) {
|
if (blockers.length === 0) {
|
||||||
desiredLabels.add(
|
desiredLabels.add(
|
||||||
states.approvedFeedback
|
largeDiff
|
||||||
? LABELS.approvedFeedback.name
|
? LABELS.needsScopeReview.name
|
||||||
: LABELS.accepted.name,
|
: states.explicitPermission
|
||||||
|
? LABELS.explicitPermission.name
|
||||||
|
: LABELS.inScope.name,
|
||||||
);
|
);
|
||||||
}
|
} else if (
|
||||||
|
blockers.some((blocker) => blocker.label === LABELS.missingTemplate.name)
|
||||||
if (largeDiff) {
|
) {
|
||||||
desiredLabels.add(LABELS.largeDiff.name);
|
desiredLabels.add(LABELS.missingTemplate.name);
|
||||||
|
} else {
|
||||||
|
desiredLabels.add(LABELS.policyUnmet.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -204,6 +280,7 @@ function analyzePullRequest(pr) {
|
|||||||
changedFiles,
|
changedFiles,
|
||||||
desiredLabels: [...desiredLabels],
|
desiredLabels: [...desiredLabels],
|
||||||
largeDiff,
|
largeDiff,
|
||||||
|
status: blockers.length === 0 ? "in_scope" : "blocked",
|
||||||
templateUsed,
|
templateUsed,
|
||||||
totalChangedLines,
|
totalChangedLines,
|
||||||
};
|
};
|
||||||
@@ -212,43 +289,147 @@ function analyzePullRequest(pr) {
|
|||||||
function buildBlockingComment(analysis) {
|
function buildBlockingComment(analysis) {
|
||||||
const lines = [
|
const lines = [
|
||||||
COMMENT_MARKER,
|
COMMENT_MARKER,
|
||||||
"Thanks for the PR. Yaak currently accepts community PRs for bug fixes and small-scope improvements, plus larger changes that link an approved feedback item from https://yaak.app/feedback.",
|
"Thanks for the PR. Yaak currently accepts community PRs for bug fixes, plus larger changes that link a feedback item where @gschier explicitly gave permission to work on it.",
|
||||||
"",
|
"",
|
||||||
"This PR cannot be accepted yet. Please update the PR description to address:",
|
"This PR cannot be accepted yet because the following contribution policy requirements were unmet:",
|
||||||
"",
|
"",
|
||||||
...analysis.blockers.map((blocker) => `- ${blocker.message}`),
|
...analysis.blockers.map((blocker) => `- ${blocker.message}`),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (analysis.largeDiff) {
|
if (!analysis.templateUsed) {
|
||||||
lines.push(
|
lines.push(
|
||||||
"",
|
"",
|
||||||
`This PR also changes ${analysis.changedFiles} files and ${analysis.totalChangedLines} lines, so it has been labeled as a large diff. That label is advisory, but maintainers may ask for the scope to be reduced.`,
|
"You can copy this template into the PR description and keep any existing context that is still useful.",
|
||||||
|
"",
|
||||||
|
"<details>",
|
||||||
|
"<summary>PR description template</summary>",
|
||||||
|
"",
|
||||||
|
"```md",
|
||||||
|
getPullRequestTemplate(),
|
||||||
|
"```",
|
||||||
|
"",
|
||||||
|
"</details>",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(
|
if (analysis.largeDiff) {
|
||||||
"",
|
lines.push(
|
||||||
"I did not overwrite the PR body, since that can remove useful context. Editing the description directly is the safest way to keep your notes while completing the template.",
|
"",
|
||||||
);
|
`This PR also changes ${analysis.changedFiles} files and ${analysis.totalChangedLines} lines, so it has been labeled as needing scope review. That label is advisory, but maintainers may ask for the scope to be reduced.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function summarizeResult({ pr, analysis, skipped, skipReason }) {
|
function getPullRequestTemplate() {
|
||||||
if (skipped) {
|
return fs.readFileSync(".github/pull_request_template.md", "utf8").trim();
|
||||||
return `#${pr.number} ${pr.title} - skipped (${skipReason})`;
|
}
|
||||||
|
|
||||||
|
function buildInScopeComment() {
|
||||||
|
return [
|
||||||
|
COMMENT_MARKER,
|
||||||
|
"Thanks for the PR. This appears to match Yaak's contribution policy and is awaiting review by @gschier.",
|
||||||
|
"",
|
||||||
|
"This only means the PR is in scope for review. It does not mean the change has been reviewed or accepted for merge.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOutOfScopeComment() {
|
||||||
|
return [
|
||||||
|
COMMENT_MARKER,
|
||||||
|
"Thanks for the PR. This does not appear to match Yaak's current contribution policy.",
|
||||||
|
"",
|
||||||
|
"Yaak currently accepts community PRs for bug fixes, or changes tied to a feedback item where @gschier explicitly gave permission to work on it.",
|
||||||
|
"",
|
||||||
|
"If this PR is tied to a feedback item where @gschier explicitly gave permission, please link it in the PR description.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPolicyComment(analysis) {
|
||||||
|
if (analysis.status === "out_of_scope") {
|
||||||
|
return buildOutOfScopeComment();
|
||||||
}
|
}
|
||||||
|
|
||||||
const status =
|
if (analysis.blockers.length > 0) {
|
||||||
analysis.blockers.length > 0
|
return buildBlockingComment(analysis);
|
||||||
? `blocked: ${analysis.blockers.map((blocker) => blocker.message).join("; ")}`
|
}
|
||||||
: "accepted";
|
|
||||||
const labels =
|
|
||||||
analysis.desiredLabels.length > 0
|
|
||||||
? analysis.desiredLabels.join(", ")
|
|
||||||
: "none";
|
|
||||||
|
|
||||||
return `#${pr.number} ${pr.title} - ${status}; labels: ${labels}`;
|
return buildInScopeComment();
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateTitle(title) {
|
||||||
|
if (title.length <= SUMMARY_TITLE_MAX_LENGTH) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${title.slice(0, SUMMARY_TITLE_MAX_LENGTH - 3).trimEnd()}...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeTableText(value) {
|
||||||
|
return escapeHtml(value).replace(/\n/g, "<br>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeResult({ pr, analysis, skipped, skipReason }) {
|
||||||
|
const comment =
|
||||||
|
analysis == null
|
||||||
|
? "None"
|
||||||
|
: buildPolicyComment(analysis).replace(COMMENT_MARKER, "").trim();
|
||||||
|
const summary = {
|
||||||
|
blocked: analysis?.blockers.length > 0,
|
||||||
|
comment,
|
||||||
|
details: "None",
|
||||||
|
labels:
|
||||||
|
analysis?.desiredLabels.length > 0
|
||||||
|
? analysis.desiredLabels.join(", ")
|
||||||
|
: "None",
|
||||||
|
number: pr.number,
|
||||||
|
prLink: `<a href="${escapeHtml(pr.html_url)}">#${pr.number}</a>`,
|
||||||
|
status: "In scope",
|
||||||
|
title: escapeHtml(truncateTitle(pr.title)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (skipped) {
|
||||||
|
return {
|
||||||
|
...summary,
|
||||||
|
blocked: false,
|
||||||
|
comment: "None",
|
||||||
|
details: escapeHtml(skipReason),
|
||||||
|
labels: "None",
|
||||||
|
status: "Skipped",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summary.blocked) {
|
||||||
|
return {
|
||||||
|
...summary,
|
||||||
|
comment: escapeTableText(summary.comment),
|
||||||
|
details: escapeHtml(
|
||||||
|
analysis.blockers.map((blocker) => blocker.message).join("; "),
|
||||||
|
),
|
||||||
|
labels: escapeHtml(summary.labels),
|
||||||
|
status: analysis.status === "out_of_scope" ? "Out of scope" : "Blocked",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...summary,
|
||||||
|
comment: escapeTableText(summary.comment),
|
||||||
|
labels: escapeHtml(summary.labels),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function wasCreatedBefore(value, cutoff) {
|
||||||
|
return Date.parse(value) < Date.parse(cutoff);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isOfficialMaintainer({ github, owner, repo, pr }) {
|
async function isOfficialMaintainer({ github, owner, repo, pr }) {
|
||||||
@@ -394,6 +575,27 @@ async function deletePolicyComment({ github, owner, repo, issueNumber }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requestMaintainerReview({ github, owner, repo, pr }) {
|
||||||
|
if (pr.user.login === REVIEWER_LOGIN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await github.rest.pulls.requestReviewers({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pr.number,
|
||||||
|
reviewers: [REVIEWER_LOGIN],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 422) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkPullRequest({
|
async function checkPullRequest({
|
||||||
github,
|
github,
|
||||||
core,
|
core,
|
||||||
@@ -401,6 +603,7 @@ async function checkPullRequest({
|
|||||||
repo,
|
repo,
|
||||||
pullNumber,
|
pullNumber,
|
||||||
dryRun,
|
dryRun,
|
||||||
|
skipCreatedBefore,
|
||||||
}) {
|
}) {
|
||||||
const response = await github.rest.pulls.get({
|
const response = await github.rest.pulls.get({
|
||||||
owner,
|
owner,
|
||||||
@@ -410,6 +613,25 @@ async function checkPullRequest({
|
|||||||
const pr = response.data;
|
const pr = response.data;
|
||||||
const issueNumber = pr.number;
|
const issueNumber = pr.number;
|
||||||
|
|
||||||
|
if (
|
||||||
|
skipCreatedBefore != null &&
|
||||||
|
wasCreatedBefore(pr.created_at, skipCreatedBefore)
|
||||||
|
) {
|
||||||
|
core.notice(
|
||||||
|
`Skipping contribution policy for PR #${pr.number} because it was created before ${AUTOMATIC_PR_CREATED_AFTER_LABEL}.`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
blocked: false,
|
||||||
|
number: pr.number,
|
||||||
|
summary: summarizeResult({
|
||||||
|
pr,
|
||||||
|
skipped: true,
|
||||||
|
skipReason: `created before ${AUTOMATIC_PR_CREATED_AFTER_LABEL}`,
|
||||||
|
}),
|
||||||
|
skipped: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (pr.draft) {
|
if (pr.draft) {
|
||||||
core.notice(`Skipping contribution policy for draft PR #${pr.number}.`);
|
core.notice(`Skipping contribution policy for draft PR #${pr.number}.`);
|
||||||
return {
|
return {
|
||||||
@@ -448,7 +670,9 @@ async function checkPullRequest({
|
|||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
const summary = summarizeResult({ pr, analysis });
|
const summary = summarizeResult({ pr, analysis });
|
||||||
core.notice(`[dry-run] ${summary}`);
|
core.notice(
|
||||||
|
`[dry-run] PR #${summary.number}: ${summary.status}; labels: ${summary.labels}; details: ${summary.details}`,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
blocked: analysis.blockers.length > 0,
|
blocked: analysis.blockers.length > 0,
|
||||||
number: pr.number,
|
number: pr.number,
|
||||||
@@ -471,7 +695,7 @@ async function checkPullRequest({
|
|||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issueNumber,
|
issueNumber,
|
||||||
body: buildBlockingComment(analysis),
|
body: buildPolicyComment(analysis),
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
blocked: true,
|
blocked: true,
|
||||||
@@ -481,7 +705,14 @@ async function checkPullRequest({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await deletePolicyComment({ github, owner, repo, issueNumber });
|
await upsertPolicyComment({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
body: buildPolicyComment(analysis),
|
||||||
|
});
|
||||||
|
await requestMaintainerReview({ github, owner, repo, pr });
|
||||||
core.notice(`Contribution policy check passed for PR #${pr.number}.`);
|
core.notice(`Contribution policy check passed for PR #${pr.number}.`);
|
||||||
return {
|
return {
|
||||||
blocked: false,
|
blocked: false,
|
||||||
@@ -500,20 +731,59 @@ async function listOpenPullRequests({ github, owner, repo }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getManualPullRequestNumbers({ context, core }) {
|
||||||
|
const value = String(context.payload.inputs?.pr || "all").trim();
|
||||||
|
|
||||||
|
if (value.toLowerCase() === "all") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pullNumber = Number(value);
|
||||||
|
|
||||||
|
if (!Number.isInteger(pullNumber) || pullNumber <= 0) {
|
||||||
|
core.setFailed('The "pr" input must be "all" or a positive PR number.');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pullNumber];
|
||||||
|
}
|
||||||
|
|
||||||
async function run({ github, context, core }) {
|
async function run({ github, context, core }) {
|
||||||
const { owner, repo } = context.repo;
|
const { owner, repo } = context.repo;
|
||||||
const payloadPr = context.payload.pull_request;
|
const payloadPr = context.payload.pull_request;
|
||||||
|
const dryRunInput = context.payload.inputs?.dry_run;
|
||||||
const dryRun =
|
const dryRun =
|
||||||
context.eventName === "workflow_dispatch" &&
|
context.eventName === "workflow_dispatch" &&
|
||||||
context.payload.inputs?.dry_run !== "false";
|
dryRunInput !== false &&
|
||||||
|
dryRunInput !== "false";
|
||||||
|
const skipCreatedBefore =
|
||||||
|
payloadPr == null ? null : AUTOMATIC_PR_CREATED_AFTER;
|
||||||
|
let pullNumbers;
|
||||||
|
|
||||||
|
if (payloadPr != null) {
|
||||||
|
pullNumbers = [payloadPr.number];
|
||||||
|
} else {
|
||||||
|
pullNumbers = getManualPullRequestNumbers({ context, core });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pullNumbers?.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pullRequests =
|
const pullRequests =
|
||||||
payloadPr == null
|
pullNumbers == null
|
||||||
? await listOpenPullRequests({ github, owner, repo })
|
? await listOpenPullRequests({ github, owner, repo })
|
||||||
: [payloadPr];
|
: pullNumbers.map((number) => ({ number }));
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
core.notice("Running contribution policy in dry-run mode.");
|
core.notice(
|
||||||
|
`Running contribution policy in dry-run mode for ${
|
||||||
|
pullNumbers == null
|
||||||
|
? "all open PRs"
|
||||||
|
: pullNumbers.map((number) => `#${number}`).join(", ")
|
||||||
|
}.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pr of pullRequests) {
|
for (const pr of pullRequests) {
|
||||||
@@ -525,6 +795,7 @@ async function run({ github, context, core }) {
|
|||||||
repo,
|
repo,
|
||||||
pullNumber: pr.number,
|
pullNumber: pr.number,
|
||||||
dryRun,
|
dryRun,
|
||||||
|
skipCreatedBefore,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -534,9 +805,20 @@ async function run({ github, context, core }) {
|
|||||||
.addTable([
|
.addTable([
|
||||||
[
|
[
|
||||||
{ data: "PR", header: true },
|
{ data: "PR", header: true },
|
||||||
{ data: "Result", header: true },
|
{ data: "Title", header: true },
|
||||||
|
{ data: "Status", header: true },
|
||||||
|
{ data: "Labels", header: true },
|
||||||
|
{ data: "Details", header: true },
|
||||||
|
{ data: "Comment", header: true },
|
||||||
],
|
],
|
||||||
...results.map((result) => [`#${result.number}`, result.summary]),
|
...results.map((result) => [
|
||||||
|
result.summary.prLink,
|
||||||
|
result.summary.title,
|
||||||
|
result.summary.status,
|
||||||
|
result.summary.labels,
|
||||||
|
result.summary.details,
|
||||||
|
result.summary.comment,
|
||||||
|
]),
|
||||||
])
|
])
|
||||||
.write();
|
.write();
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,30 @@ name: Contribution Policy
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
pr:
|
||||||
|
description: PR number or all
|
||||||
|
required: true
|
||||||
|
default: all
|
||||||
|
type: string
|
||||||
dry_run:
|
dry_run:
|
||||||
description: Preview labels and comments without changing PRs
|
description: Dry run
|
||||||
required: true
|
required: true
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
- labeled
|
||||||
|
- unlabeled
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: read
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
@@ -22,6 +36,7 @@ jobs:
|
|||||||
- name: Checkout policy script
|
- name: Checkout policy script
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.base.sha || github.ref }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check contribution policy
|
- name: Check contribution policy
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
name: Update Flathub
|
name: Update Flathub
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
workflow_dispatch:
|
||||||
types: [published]
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: Release tag to publish to Flathub
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -10,8 +15,6 @@ jobs:
|
|||||||
update-flathub:
|
update-flathub:
|
||||||
name: Update Flathub manifest
|
name: Update Flathub manifest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Only run for stable releases (skip betas/pre-releases)
|
|
||||||
if: ${{ !github.event.release.prerelease }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout app repo
|
- name: Checkout app repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -39,7 +42,7 @@ jobs:
|
|||||||
git clone --depth 1 https://github.com/flatpak/flatpak-builder-tools flatpak/flatpak-builder-tools
|
git clone --depth 1 https://github.com/flatpak/flatpak-builder-tools flatpak/flatpak-builder-tools
|
||||||
|
|
||||||
- name: Run update-manifest.sh
|
- name: Run update-manifest.sh
|
||||||
run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" flathub-repo
|
run: bash flatpak/update-manifest.sh "${{ inputs.tag }}" flathub-repo
|
||||||
|
|
||||||
- name: Commit and push to Flathub
|
- name: Commit and push to Flathub
|
||||||
working-directory: flathub-repo
|
working-directory: flathub-repo
|
||||||
@@ -48,5 +51,5 @@ jobs:
|
|||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git add -A
|
git add -A
|
||||||
git diff --cached --quiet && echo "No changes to commit" && exit 0
|
git diff --cached --quiet && echo "No changes to commit" && exit 0
|
||||||
git commit -m "Update to ${{ github.event.release.tag_name }}"
|
git commit -m "Update to ${{ inputs.tag }}"
|
||||||
git push
|
git push
|
||||||
|
|||||||
+1
-2
@@ -3,13 +3,12 @@
|
|||||||
Yaak accepts community pull requests for:
|
Yaak accepts community pull requests for:
|
||||||
|
|
||||||
- Bug fixes
|
- Bug fixes
|
||||||
- Small-scope improvements directly tied to existing behavior
|
|
||||||
|
|
||||||
Pull requests that introduce broad new features, major redesigns, or large refactors are out of scope unless explicitly approved first.
|
Pull requests that introduce broad new features, major redesigns, or large refactors are out of scope unless explicitly approved first.
|
||||||
|
|
||||||
## Approval for Non-Bugfix Changes
|
## Approval for Non-Bugfix Changes
|
||||||
|
|
||||||
If your PR is not a bug fix or small-scope improvement, include a link to the approved [feedback item](https://yaak.app/feedback) where contribution approval was explicitly stated.
|
If your PR is not a bug fix, include a link to the [feedback item](https://yaak.app/feedback) where @gschier explicitly gave you permission to work on it.
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ Built with [Tauri](https://tauri.app), Rust, and React, it’s fast, lightweight
|
|||||||
## Contribution Policy
|
## Contribution Policy
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Community PRs are currently limited to bug fixes and small-scope improvements.
|
> Community PRs are currently limited to bug fixes.
|
||||||
> If your PR is out of scope, link an approved feedback item from [yaak.app/feedback](https://yaak.app/feedback).
|
> If your PR is not a bug fix, link the [feedback item](https://yaak.app/feedback) where @gschier explicitly gave you permission to work on it.
|
||||||
> See [`CONTRIBUTING.md`](CONTRIBUTING.md) for policy details and [`DEVELOPMENT.md`](DEVELOPMENT.md) for local setup.
|
> See [`CONTRIBUTING.md`](CONTRIBUTING.md) for policy details and [`DEVELOPMENT.md`](DEVELOPMENT.md) for local setup.
|
||||||
|
|
||||||
## Useful Resources
|
## Useful Resources
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
|
|||||||
id: "folder-settings",
|
id: "folder-settings",
|
||||||
title: null,
|
title: null,
|
||||||
size: "lg",
|
size: "lg",
|
||||||
className: "h-[50rem]",
|
className: "h-200",
|
||||||
noPadding: true,
|
noPadding: true,
|
||||||
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
|
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function BinaryFileEditor({
|
|||||||
<VStack space={2}>
|
<VStack space={2}>
|
||||||
<SelectFile onChange={handleChange} filePath={filePath} />
|
<SelectFile onChange={handleChange} filePath={filePath} />
|
||||||
{filePath != null && mimeType !== contentType && !ignoreContentType.value && (
|
{filePath != null && mimeType !== contentType && !ignoreContentType.value && (
|
||||||
<Banner className="mt-3 !py-5">
|
<Banner className="mt-3 py-5!">
|
||||||
<div className="mb-4 text-center">
|
<div className="mb-4 text-center">
|
||||||
<div>Set Content-Type header</div>
|
<div>Set Content-Type header</div>
|
||||||
<InlineCode>{mimeType}</InlineCode> for current request?
|
<InlineCode>{mimeType}</InlineCode> for current request?
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
|
|||||||
rightSlot={
|
rightSlot={
|
||||||
<IconButton
|
<IconButton
|
||||||
size="xs"
|
size="xs"
|
||||||
className="mr-0.5 !h-auto my-0.5"
|
className="mr-0.5 h-auto! my-0.5"
|
||||||
icon="folder"
|
icon="folder"
|
||||||
title="Browse"
|
title="Browse"
|
||||||
onClick={handleSelectDirectory}
|
onClick={handleSelectDirectory}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function ColorIndicator({ color, onClick, className }: Props) {
|
|||||||
const style: CSSProperties = { backgroundColor: color ?? undefined };
|
const style: CSSProperties = { backgroundColor: color ?? undefined };
|
||||||
const finalClassName = classNames(
|
const finalClassName = classNames(
|
||||||
className,
|
className,
|
||||||
"inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent flex-shrink-0",
|
"inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent shrink-0",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
name="command"
|
name="command"
|
||||||
label="Command"
|
label="Command"
|
||||||
placeholder="Search or type a command"
|
placeholder="Search or type a command"
|
||||||
className="font-sans !text-base"
|
className="font-sans text-base!"
|
||||||
defaultValue={command}
|
defaultValue={command}
|
||||||
onChange={handleSetCommand}
|
onChange={handleSetCommand}
|
||||||
onKeyDownCapture={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
@@ -448,7 +448,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
|
|||||||
<div className="h-full px-1.5 overflow-y-auto pt-2 pb-1">
|
<div className="h-full px-1.5 overflow-y-auto pt-2 pb-1">
|
||||||
{filteredGroups.map((g) => (
|
{filteredGroups.map((g) => (
|
||||||
<div key={g.key} className="mb-1.5 w-full">
|
<div key={g.key} className="mb-1.5 w-full">
|
||||||
<Heading level={2} className="!text-xs uppercase px-1.5 h-sm flex items-center">
|
<Heading level={2} className="text-xs! uppercase px-1.5 h-sm flex items-center">
|
||||||
{g.label}
|
{g.label}
|
||||||
</Heading>
|
</Heading>
|
||||||
{g.items.map((v) => (
|
{g.items.map((v) => (
|
||||||
@@ -491,7 +491,7 @@ function CommandPaletteItem({
|
|||||||
color="custom"
|
color="custom"
|
||||||
justify="start"
|
justify="start"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full h-sm flex items-center rounded px-1.5",
|
"w-full h-sm flex items-center rounded-sm px-1.5",
|
||||||
"hover:text-text",
|
"hover:text-text",
|
||||||
active && "bg-surface-highlight",
|
active && "bg-surface-highlight",
|
||||||
!active && "text-text-subtle",
|
!active && "text-text-subtle",
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function CommercialUseBanner({
|
|||||||
setSnoozedAt(JSON.stringify({ source, at: new Date().toISOString() })).catch(console.error);
|
setSnoozedAt(JSON.stringify({ source, at: new Date().toISOString() })).catch(console.error);
|
||||||
}, [setSnoozedAt, snoozed, source]);
|
}, [setSnoozedAt, snoozed, source]);
|
||||||
|
|
||||||
if (!visible || isSnoozeLoading || snoozed) {
|
if (!visible || isSnoozeLoading || (snoozed && !snoozeStartedRef.current)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,10 +96,10 @@ async function shouldShowCommercialUsePrompt(): Promise<boolean> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const license = await invoke<LicenseCheckStatus>("plugin:yaak-license|check");
|
const license = await invoke<LicenseCheckStatus>("plugin:yaak-license|check");
|
||||||
return license.status !== "active" && license.status !== "trialing";
|
return license.status === "personal_use";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Failed to check license before commercial-use prompt", err);
|
console.log("Failed to check license before commercial-use prompt", err);
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
|
|||||||
rightSlot={
|
rightSlot={
|
||||||
filter.length > 0 && (
|
filter.length > 0 && (
|
||||||
<IconButton
|
<IconButton
|
||||||
className="!bg-transparent !h-auto min-h-full opacity-50 hover:opacity-100 -mr-1"
|
className="bg-transparent! h-auto! min-h-full opacity-50 hover:opacity-100 -mr-1"
|
||||||
icon="x"
|
icon="x"
|
||||||
title="Clear filter"
|
title="Clear filter"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -239,7 +239,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
|
|||||||
<TableCell className={classNames("pl-2", isSelected && "rounded-l")}>
|
<TableCell className={classNames("pl-2", isSelected && "rounded-l")}>
|
||||||
{c.name}
|
{c.name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TruncatedWideTableCell className="min-w-[10rem]">
|
<TruncatedWideTableCell className="min-w-40">
|
||||||
{c.value}
|
{c.value}
|
||||||
</TruncatedWideTableCell>
|
</TruncatedWideTableCell>
|
||||||
<TableCell>{cookieDomain(c)}</TableCell>
|
<TableCell>{cookieDomain(c)}</TableCell>
|
||||||
@@ -547,7 +547,7 @@ function CookieEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CookieKeyValueRow({ labelClassName, ...props }: ComponentProps<typeof KeyValueRow>) {
|
function CookieKeyValueRow({ labelClassName, ...props }: ComponentProps<typeof KeyValueRow>) {
|
||||||
return <KeyValueRow labelClassName={classNames("w-[7rem]", labelClassName)} {...props} />;
|
return <KeyValueRow labelClassName={classNames("w-28", labelClassName)} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CookieTextInput({
|
function CookieTextInput({
|
||||||
@@ -589,7 +589,7 @@ function CookieTextarea({ onChange, value }: { onChange: (value: string) => void
|
|||||||
<textarea
|
<textarea
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
className={classNames(cookieInputClassName, "min-h-[5rem] resize-y")}
|
className={classNames(cookieInputClassName, "min-h-20 resize-y")}
|
||||||
onChange={(event) => onChange(event.target.value)}
|
onChange={(event) => onChange(event.target.value)}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
@@ -600,7 +600,7 @@ const NEW_COOKIE_KEY = "__new-cookie__";
|
|||||||
const NON_EMPTY_INPUT_PATTERN = ".*\\S.*";
|
const NON_EMPTY_INPUT_PATTERN = ".*\\S.*";
|
||||||
const cookieInputClassName = classNames(
|
const cookieInputClassName = classNames(
|
||||||
"x-theme-input w-full min-w-0 min-h-sm rounded-md bg-transparent",
|
"x-theme-input w-full min-w-0 min-h-sm rounded-md bg-transparent",
|
||||||
"border border-border-subtle outline-none",
|
"border border-border-subtle outline-hidden",
|
||||||
"px-2 text-xs font-mono cursor-text placeholder:text-placeholder",
|
"px-2 text-xs font-mono cursor-text placeholder:text-placeholder",
|
||||||
"focus:border-border-focus invalid:border-danger",
|
"focus:border-border-focus invalid:border-danger",
|
||||||
"disabled:opacity-disabled disabled:border-dotted",
|
"disabled:opacity-disabled disabled:border-dotted",
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function DnsOverridesEditor({ workspace }: Props) {
|
|||||||
<VStack space={3} className="pb-3">
|
<VStack space={3} className="pb-3">
|
||||||
<div className="text-text-subtle text-sm">
|
<div className="text-text-subtle text-sm">
|
||||||
Override DNS resolution for specific hostnames. This works like{" "}
|
Override DNS resolution for specific hostnames. This works like{" "}
|
||||||
<code className="text-text-subtlest bg-surface-highlight px-1 rounded">/etc/hosts</code> but
|
<code className="text-text-subtlest bg-surface-highlight px-1 rounded-sm">/etc/hosts</code> but
|
||||||
only for requests made from this workspace.
|
only for requests made from this workspace.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export const DropMarker = memo(
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"absolute bg-primary rounded-full",
|
"absolute bg-primary rounded-full",
|
||||||
orientation === "horizontal" && "left-2 right-2 -bottom-[0.1rem] h-[0.2rem]",
|
orientation === "horizontal" && "left-2 right-2 bottom-[-0.1rem] h-[0.2rem]",
|
||||||
orientation === "vertical" && "-left-[0.1rem] top-0 bottom-0 w-[0.2rem]",
|
orientation === "vertical" && "left-[-0.1rem] top-0 bottom-0 w-[0.2rem]",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
|
|||||||
<div key={i + stateKey}>
|
<div key={i + stateKey}>
|
||||||
<DetailsBanner
|
<DetailsBanner
|
||||||
summary={input.label}
|
summary={input.label}
|
||||||
className={classNames("!mb-auto", disabled && "opacity-disabled")}
|
className={classNames("mb-auto!", disabled && "opacity-disabled")}
|
||||||
>
|
>
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<FormInputsStack
|
<FormInputsStack
|
||||||
@@ -300,7 +300,7 @@ function TextArg({
|
|||||||
onChange,
|
onChange,
|
||||||
name: arg.name,
|
name: arg.name,
|
||||||
multiLine: arg.multiLine,
|
multiLine: arg.multiLine,
|
||||||
className: arg.multiLine ? "min-h-[4rem]" : undefined,
|
className: arg.multiLine ? "min-h-16" : undefined,
|
||||||
defaultValue: value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value,
|
defaultValue: value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value,
|
||||||
required: !arg.optional,
|
required: !arg.optional,
|
||||||
disabled: arg.disabled,
|
disabled: arg.disabled,
|
||||||
@@ -359,7 +359,7 @@ function EditorArg({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
"border border-border rounded-md overflow-hidden px-2 py-1",
|
"border border-border rounded-md overflow-hidden px-2 py-1",
|
||||||
"focus-within:border-border-focus",
|
"focus-within:border-border-focus",
|
||||||
!arg.rows && "max-h-[10rem]", // So it doesn't take up too much space
|
!arg.rows && "max-h-40", // So it doesn't take up too much space
|
||||||
)}
|
)}
|
||||||
style={arg.rows ? { height: `${arg.rows * 1.4 + 0.75}rem` } : undefined}
|
style={arg.rows ? { height: `${arg.rows * 1.4 + 0.75}rem` } : undefined}
|
||||||
>
|
>
|
||||||
@@ -372,7 +372,7 @@ function EditorArg({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
hideGutter
|
hideGutter
|
||||||
heightMode="auto"
|
heightMode="auto"
|
||||||
className="min-h-[3rem]"
|
className="min-h-12"
|
||||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
||||||
placeholder={arg.placeholder ?? undefined}
|
placeholder={arg.placeholder ?? undefined}
|
||||||
autocompleteFunctions={autocompleteFunctions}
|
autocompleteFunctions={autocompleteFunctions}
|
||||||
@@ -392,7 +392,7 @@ function EditorArg({
|
|||||||
id: "id",
|
id: "id",
|
||||||
size: "full",
|
size: "full",
|
||||||
title: arg.readOnly ? "View Value" : "Edit Value",
|
title: arg.readOnly ? "View Value" : "Edit Value",
|
||||||
className: "!max-w-[50rem] !max-h-[60rem]",
|
className: "max-w-200! max-h-240!",
|
||||||
description: arg.label && (
|
description: arg.label && (
|
||||||
<Label
|
<Label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
|||||||
size="sm"
|
size="sm"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"text !px-2 truncate",
|
"text px-2! truncate",
|
||||||
!activeEnvironment && !hasBaseVars && "text-text-subtlest italic",
|
!activeEnvironment && !hasBaseVars && "text-text-subtlest italic",
|
||||||
)}
|
)}
|
||||||
// If no environments, the button simply opens the dialog.
|
// If no environments, the button simply opens the dialog.
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function EnvironmentEditDialog({ initialEnvironmentId, setRef }: Props) {
|
|||||||
defaultRatio={0.75}
|
defaultRatio={0.75}
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
className="gap-0"
|
className="gap-0"
|
||||||
resizeHandleClassName="-translate-x-[1px]"
|
resizeHandleClassName="-translate-x-px"
|
||||||
firstSlot={() => (
|
firstSlot={() => (
|
||||||
<EnvironmentEditDialogSidebar
|
<EnvironmentEditDialogSidebar
|
||||||
selectedEnvironmentId={selectedEnvironment?.id ?? null}
|
selectedEnvironmentId={selectedEnvironment?.id ?? null}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ function HttpRequestCard({ request }: { request: HttpRequest }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-rows-2 grid-cols-[minmax(0,1fr)] gap-2 overflow-hidden">
|
<div className="grid grid-rows-2 grid-cols-[minmax(0,1fr)] gap-2 overflow-hidden">
|
||||||
<code className="font-mono text-editor text-info border border-info rounded px-2.5 py-0.5 truncate w-full min-w-0">
|
<code className="font-mono text-editor text-info border border-info rounded-sm px-2.5 py-0.5 truncate w-full min-w-0">
|
||||||
{request.method} {request.url}
|
{request.method} {request.url}
|
||||||
</code>
|
</code>
|
||||||
{latestResponse ? (
|
{latestResponse ? (
|
||||||
@@ -190,7 +190,7 @@ function HttpRequestCard({ request }: { request: HttpRequest }) {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
"cursor-default select-none",
|
"cursor-default select-none",
|
||||||
"whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
|
"whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
|
||||||
"font-mono text-editor border rounded px-1.5 py-0.5 truncate w-full",
|
"font-mono text-editor border rounded-sm px-1.5 py-0.5 truncate w-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{latestResponse.state !== "closed" && <LoadingIcon size="sm" />}
|
{latestResponse.state !== "closed" && <LoadingIcon size="sm" />}
|
||||||
|
|||||||
@@ -84,12 +84,12 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex items-center gap-3 px-6 pr-10 mt-4 mb-2 min-w-0 text-xl">
|
<div className="flex items-center gap-3 px-6 pr-10 mt-4 mb-2 min-w-0 text-xl">
|
||||||
<Icon icon="folder_cog" size="lg" color="secondary" className="flex-shrink-0" />
|
<Icon icon="folder_cog" size="lg" color="secondary" className="shrink-0" />
|
||||||
<div className="flex items-center gap-1.5 font-semibold text-text min-w-0 overflow-hidden flex-1">
|
<div className="flex items-center gap-1.5 font-semibold text-text min-w-0 overflow-hidden flex-1">
|
||||||
{breadcrumbs.map((item, index) => (
|
{breadcrumbs.map((item, index) => (
|
||||||
<Fragment key={item.id}>
|
<Fragment key={item.id}>
|
||||||
{index > 0 && (
|
{index > 0 && (
|
||||||
<Icon icon="chevron_right" size="lg" className="opacity-50 flex-shrink-0" />
|
<Icon icon="chevron_right" size="lg" className="opacity-50 shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="text-text-subtle truncate min-w-0" title={item.name}>
|
<span className="text-text-subtle truncate min-w-0" title={item.name}>
|
||||||
{item.name}
|
{item.name}
|
||||||
@@ -97,7 +97,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
{breadcrumbs.length > 0 && (
|
{breadcrumbs.length > 0 && (
|
||||||
<Icon icon="chevron_right" size="lg" className="opacity-50 flex-shrink-0" />
|
<Icon icon="chevron_right" size="lg" className="opacity-50 shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="whitespace-nowrap" title={folder.name}>
|
<span className="whitespace-nowrap" title={folder.name}>
|
||||||
{folder.name}
|
{folder.name}
|
||||||
@@ -149,7 +149,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
<InlineCode className="flex gap-1 items-center text-primary pl-2.5">
|
<InlineCode className="flex gap-1 items-center text-primary pl-2.5">
|
||||||
{folder.id}
|
{folder.id}
|
||||||
<CopyIconButton
|
<CopyIconButton
|
||||||
className="opacity-70 !text-primary"
|
className="opacity-70 text-primary!"
|
||||||
size="2xs"
|
size="2xs"
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
title="Copy folder ID"
|
title="Copy folder ID"
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function GrpcEditor({
|
|||||||
|
|
||||||
const actions = useMemo(
|
const actions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
<div key="reflection" className={classNames(services == null && "!opacity-100")}>
|
<div key="reflection" className={classNames(services == null && "opacity-100!")}>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
color={
|
color={
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export function GrpcRequestPane({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
"grid grid-cols-[minmax(0,1fr)_auto] gap-1.5",
|
"grid grid-cols-[minmax(0,1fr)_auto] gap-1.5",
|
||||||
paneWidth === 0 && "opacity-0",
|
paneWidth === 0 && "opacity-0",
|
||||||
paneWidth > 0 && paneWidth < 400 && "!grid-cols-1",
|
paneWidth > 0 && paneWidth < 400 && "grid-cols-1!",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UrlBar
|
<UrlBar
|
||||||
@@ -201,7 +201,7 @@ export function GrpcRequestPane({
|
|||||||
rightSlot={<Icon size="sm" icon="chevron_down" />}
|
rightSlot={<Icon size="sm" icon="chevron_down" />}
|
||||||
disabled={isStreaming || services == null}
|
disabled={isStreaming || services == null}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"font-mono text-editor min-w-[5rem] !ring-0",
|
"font-mono text-editor min-w-20 ring-0!",
|
||||||
paneWidth < 400 && "flex-1",
|
paneWidth < 400 && "flex-1",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -259,7 +259,7 @@ export function GrpcRequestPane({
|
|||||||
<Tabs
|
<Tabs
|
||||||
label="Request"
|
label="Request"
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
tabListClassName="mt-1 !mb-1.5"
|
tabListClassName="mt-1 mb-1.5!"
|
||||||
storageKey="grpc_request_tabs"
|
storageKey="grpc_request_tabs"
|
||||||
activeTabKey={activeRequest.id}
|
activeTabKey={activeRequest.id}
|
||||||
>
|
>
|
||||||
@@ -296,7 +296,7 @@ export function GrpcRequestPane({
|
|||||||
hideLabel
|
hideLabel
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
defaultValue={activeRequest.name}
|
defaultValue={activeRequest.name}
|
||||||
className="font-sans !text-xl !px-0"
|
className="font-sans text-xl! px-0!"
|
||||||
containerClassName="border-0"
|
containerClassName="border-0"
|
||||||
placeholder={resolvedModelName(activeRequest)}
|
placeholder={resolvedModelName(activeRequest)}
|
||||||
onChange={(name) => patchModel(activeRequest, { name })}
|
onChange={(name) => patchModel(activeRequest, { name })}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
title="Authentication Actions"
|
title="Authentication Actions"
|
||||||
icon="settings"
|
icon="settings"
|
||||||
size="xs"
|
size="xs"
|
||||||
className="!text-secondary"
|
className="text-secondary!"
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
|
|||||||
<GraphQLDocsExplorer
|
<GraphQLDocsExplorer
|
||||||
requestId={activeRequest.id}
|
requestId={activeRequest.id}
|
||||||
schema={graphQLSchema}
|
schema={graphQLSchema}
|
||||||
className={classNames(orientation === "horizontal" && "!ml-0")}
|
className={classNames(orientation === "horizontal" && "ml-0!")}
|
||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { useSendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
|
|||||||
import { deepEqualAtom } from "../lib/atoms";
|
import { deepEqualAtom } from "../lib/atoms";
|
||||||
import { languageFromContentType } from "../lib/contentType";
|
import { languageFromContentType } from "../lib/contentType";
|
||||||
import { generateId } from "../lib/generateId";
|
import { generateId } from "../lib/generateId";
|
||||||
|
import { extractPathPlaceholders } from "../lib/pathPlaceholders";
|
||||||
import {
|
import {
|
||||||
BODY_TYPE_BINARY,
|
BODY_TYPE_BINARY,
|
||||||
BODY_TYPE_FORM_MULTIPART,
|
BODY_TYPE_FORM_MULTIPART,
|
||||||
@@ -131,9 +132,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
||||||
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
|
const placeholderNames = extractPathPlaceholders(activeRequest.url);
|
||||||
(m) => m[1] ?? "",
|
|
||||||
);
|
|
||||||
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
|
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
|
||||||
const items: Pair[] = [...nonEmptyParameters];
|
const items: Pair[] = [...nonEmptyParameters];
|
||||||
for (const name of placeholderNames) {
|
for (const name of placeholderNames) {
|
||||||
@@ -347,7 +346,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
onUrlChange={handleUrlChange}
|
onUrlChange={handleUrlChange}
|
||||||
leftSlot={
|
leftSlot={
|
||||||
<div className="py-0.5">
|
<div className="py-0.5">
|
||||||
<RequestMethodDropdown request={activeRequest} className="ml-0.5 !h-full" />
|
<RequestMethodDropdown request={activeRequest} className="ml-0.5 h-full!" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
forceUpdateKey={updateKey}
|
forceUpdateKey={updateKey}
|
||||||
@@ -457,7 +456,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
hideLabel
|
hideLabel
|
||||||
forceUpdateKey={updateKey}
|
forceUpdateKey={updateKey}
|
||||||
defaultValue={activeRequest.name}
|
defaultValue={activeRequest.name}
|
||||||
className="font-sans !text-xl !px-0"
|
className="font-sans text-xl! px-0!"
|
||||||
containerClassName="border-0"
|
containerClassName="border-0"
|
||||||
placeholder={resolvedModelName(activeRequest)}
|
placeholder={resolvedModelName(activeRequest)}
|
||||||
onChange={(name) => patchModel(activeRequest, { name })}
|
onChange={(name) => patchModel(activeRequest, { name })}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import classNames from "classnames";
|
|||||||
import type { ComponentType, CSSProperties } from "react";
|
import type { ComponentType, CSSProperties } from "react";
|
||||||
import { lazy, Suspense, useMemo } from "react";
|
import { lazy, Suspense, useMemo } from "react";
|
||||||
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
|
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
|
||||||
|
import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse";
|
||||||
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
|
||||||
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
|
||||||
import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText";
|
import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText";
|
||||||
import { useResponseViewMode } from "../hooks/useResponseViewMode";
|
import { useResponseViewMode } from "../hooks/useResponseViewMode";
|
||||||
|
import { useSaveResponse } from "../hooks/useSaveResponse";
|
||||||
import { useTimelineViewMode } from "../hooks/useTimelineViewMode";
|
import { useTimelineViewMode } from "../hooks/useTimelineViewMode";
|
||||||
import { getMimeTypeFromContentType } from "../lib/contentType";
|
import { getMimeTypeFromContentType } from "../lib/contentType";
|
||||||
import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util";
|
import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util";
|
||||||
@@ -78,6 +80,8 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
activeResponse?.state === "closed" && redirectDropWarning != null;
|
activeResponse?.state === "closed" && redirectDropWarning != null;
|
||||||
|
|
||||||
const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]);
|
const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]);
|
||||||
|
const saveResponse = useSaveResponse(activeResponse ?? null);
|
||||||
|
const copyResponse = useCopyHttpResponse(activeResponse ?? null);
|
||||||
|
|
||||||
const tabs = useMemo<TabItem[]>(
|
const tabs = useMemo<TabItem[]>(
|
||||||
() => [
|
() => [
|
||||||
@@ -93,6 +97,22 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
? []
|
? []
|
||||||
: [{ label: "Response (Raw)", shortLabel: "Raw", value: "raw" }]),
|
: [{ label: "Response (Raw)", shortLabel: "Raw", value: "raw" }]),
|
||||||
],
|
],
|
||||||
|
itemsAfter: [
|
||||||
|
{
|
||||||
|
label: "Save to File",
|
||||||
|
onSelect: saveResponse.mutate,
|
||||||
|
leftSlot: <Icon icon="save" />,
|
||||||
|
hidden: activeResponse == null || !!activeResponse.error,
|
||||||
|
disabled: activeResponse?.state !== "closed" && (activeResponse?.status ?? 0) >= 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Copy Body",
|
||||||
|
onSelect: copyResponse.mutate,
|
||||||
|
leftSlot: <Icon icon="copy" />,
|
||||||
|
hidden: activeResponse == null || !!activeResponse.error,
|
||||||
|
disabled: activeResponse?.state !== "closed" && (activeResponse?.status ?? 0) >= 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -135,12 +155,18 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
activeResponse?.headers,
|
activeResponse?.headers,
|
||||||
|
activeResponse,
|
||||||
|
activeResponse?.error,
|
||||||
activeResponse?.requestContentLength,
|
activeResponse?.requestContentLength,
|
||||||
activeResponse?.requestHeaders.length,
|
activeResponse?.requestHeaders.length,
|
||||||
|
activeResponse?.state,
|
||||||
|
activeResponse?.status,
|
||||||
cookieCounts.sent,
|
cookieCounts.sent,
|
||||||
cookieCounts.received,
|
cookieCounts.received,
|
||||||
|
copyResponse.mutate,
|
||||||
mimeType,
|
mimeType,
|
||||||
responseEvents.data?.length,
|
responseEvents.data?.length,
|
||||||
|
saveResponse.mutate,
|
||||||
setViewMode,
|
setViewMode,
|
||||||
viewMode,
|
viewMode,
|
||||||
timelineViewMode,
|
timelineViewMode,
|
||||||
@@ -167,7 +193,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
<div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1">
|
<div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1">
|
||||||
<HStack
|
<HStack
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-text-subtle w-full flex-shrink-0",
|
"text-text-subtle w-full shrink-0",
|
||||||
// Remove a bit of space because the tabs have lots too
|
// Remove a bit of space because the tabs have lots too
|
||||||
"-mb-1.5",
|
"-mb-1.5",
|
||||||
)}
|
)}
|
||||||
@@ -180,7 +206,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
"whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
|
"whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<HStack space={2} className="w-full flex-shrink-0">
|
<HStack space={2} className="w-full shrink-0">
|
||||||
{activeResponse.state !== "closed" && <LoadingIcon size="sm" />}
|
{activeResponse.state !== "closed" && <LoadingIcon size="sm" />}
|
||||||
<HttpStatusTag showReason response={activeResponse} />
|
<HttpStatusTag showReason response={activeResponse} />
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
@@ -194,7 +220,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
{shouldShowRedirectDropWarning ? (
|
{shouldShowRedirectDropWarning ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="my-auto pl-3 flex-shrink-0 max-w-full justify-self-end overflow-hidden"
|
className="my-auto pl-3 shrink-0 max-w-full justify-self-end overflow-hidden"
|
||||||
content={
|
content={
|
||||||
<VStack alignItems="start" space={1} className="text-xs">
|
<VStack alignItems="start" space={1} className="text-xs">
|
||||||
<span className="font-medium text-warning">
|
<span className="font-medium text-warning">
|
||||||
@@ -223,7 +249,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
<span className="inline-flex min-w-0">
|
<span className="inline-flex min-w-0">
|
||||||
<PillButton
|
<PillButton
|
||||||
color="warning"
|
color="warning"
|
||||||
className="font-sans text-sm !flex-shrink max-w-full"
|
className="font-sans text-sm shrink! max-w-full"
|
||||||
innerClassName="flex items-center"
|
innerClassName="flex items-center"
|
||||||
leftSlot={<Icon icon="alert_triangle" size="xs" color="warning" />}
|
leftSlot={<Icon icon="alert_triangle" size="xs" color="warning" />}
|
||||||
>
|
>
|
||||||
@@ -236,7 +262,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<span />
|
<span />
|
||||||
)}
|
)}
|
||||||
<div className="justify-self-end flex-shrink-0">
|
<div className="justify-self-end shrink-0">
|
||||||
<RecentHttpResponsesDropdown
|
<RecentHttpResponsesDropdown
|
||||||
responses={responses}
|
responses={responses}
|
||||||
activeResponse={activeResponse}
|
activeResponse={activeResponse}
|
||||||
@@ -249,7 +275,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
|
|
||||||
<div className="overflow-hidden flex flex-col min-h-0">
|
<div className="overflow-hidden flex flex-col min-h-0">
|
||||||
{activeResponse?.error && (
|
{activeResponse?.error && (
|
||||||
<Banner color="danger" className="mx-3 mt-1 flex-shrink-0">
|
<Banner color="danger" className="mx-3 mt-1 shrink-0">
|
||||||
{activeResponse.error}
|
{activeResponse.error}
|
||||||
</Banner>
|
</Banner>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
|
|||||||
const actions = useMemo<EditorProps["actions"]>(
|
const actions = useMemo<EditorProps["actions"]>(
|
||||||
() => [
|
() => [
|
||||||
showBanner && (
|
showBanner && (
|
||||||
<Banner color="notice" className="!opacity-100 h-sm !py-0 !px-2 flex items-center text-xs">
|
<Banner color="notice" className="opacity-100! h-sm py-0! px-2! flex items-center text-xs">
|
||||||
<p className="inline-flex items-center gap-1 min-w-0">
|
<p className="inline-flex items-center gap-1 min-w-0">
|
||||||
<span className="truncate">Auto-fix enabled</span>
|
<span className="truncate">Auto-fix enabled</span>
|
||||||
<Icon icon="arrow_right" size="sm" className="opacity-disabled" />
|
<Icon icon="arrow_right" size="sm" className="opacity-disabled" />
|
||||||
</p>
|
</p>
|
||||||
</Banner>
|
</Banner>
|
||||||
),
|
),
|
||||||
<div key="settings" className="!opacity-100 !shadow">
|
<div key="settings" className="opacity-100! shadow!">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
onOpen={handleDropdownOpen}
|
onOpen={handleDropdownOpen}
|
||||||
items={
|
items={
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function getDetail(
|
|||||||
label: `License expired ${formatDate(data.data.periodEnd, "MMM dd, yyyy")}`,
|
label: `License expired ${formatDate(data.data.periodEnd, "MMM dd, yyyy")}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: <div className="min-w-[12rem]">Renew License</div>,
|
label: <div className="min-w-48">Renew License</div>,
|
||||||
leftSlot: <Icon icon="refresh" />,
|
leftSlot: <Icon icon="refresh" />,
|
||||||
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
|
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
|
||||||
hidden: data.data.changesUrl == null,
|
hidden: data.data.changesUrl == null,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function MarkdownEditor({
|
|||||||
<Editor
|
<Editor
|
||||||
hideGutter
|
hideGutter
|
||||||
wrapLines
|
wrapLines
|
||||||
className={classNames(editorClassName, "[&_.cm-line]:!max-w-lg max-h-full")}
|
className={classNames(editorClassName, "[&_.cm-line]:max-w-lg! max-h-full")}
|
||||||
language="markdown"
|
language="markdown"
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -46,7 +46,7 @@ export function MarkdownEditor({
|
|||||||
defaultValue.length === 0 ? (
|
defaultValue.length === 0 ? (
|
||||||
<p className="text-text-subtlest">No description</p>
|
<p className="text-text-subtlest">No description</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="pr-1.5 overflow-y-auto max-h-full [&_*]:cursor-auto [&_*]:select-auto">
|
<div className="pr-1.5 overflow-y-auto max-h-full **:cursor-auto **:select-auto">
|
||||||
<Markdown className="max-w-lg select-auto cursor-auto">{defaultValue}</Markdown>
|
<Markdown className="max-w-lg select-auto cursor-auto">{defaultValue}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ function NumberUnitInput({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||||
containerClassName="!w-48"
|
containerClassName="w-48!"
|
||||||
validate={validate}
|
validate={validate}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<span className="flex self-stretch items-center border-l border-border-subtle px-2 text-xs font-medium text-text-subtle">
|
<span className="flex self-stretch items-center border-l border-border-subtle px-2 text-xs font-medium text-text-subtle">
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function MoveToWorkspaceDialog({ onDone, requests, activeWorkspaceId }: P
|
|||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="mr-auto min-w-[5rem]"
|
className="mr-auto min-w-20"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await router.navigate({
|
await router.navigate({
|
||||||
to: "/workspaces/$workspaceId",
|
to: "/workspaces/$workspaceId",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@reference "../main.css";
|
||||||
|
|
||||||
.prose {
|
.prose {
|
||||||
@apply text-text;
|
@apply text-text;
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@
|
|||||||
@apply text-notice hover:underline;
|
@apply text-notice hover:underline;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@apply text-notice !important;
|
@apply text-notice!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,12 +115,12 @@
|
|||||||
ol code,
|
ol code,
|
||||||
ul code {
|
ul code {
|
||||||
@apply text-xs bg-surface-active text-info font-normal whitespace-nowrap;
|
@apply text-xs bg-surface-active text-info font-normal whitespace-nowrap;
|
||||||
@apply px-1.5 py-0.5 rounded not-italic;
|
@apply px-1.5 py-0.5 rounded-sm not-italic;
|
||||||
@apply select-text;
|
@apply select-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
@apply bg-surface-highlight text-text !important;
|
@apply bg-surface-highlight! text-text!;
|
||||||
@apply px-4 py-3 rounded-md;
|
@apply px-4 py-3 rounded-md;
|
||||||
@apply overflow-auto whitespace-pre;
|
@apply overflow-auto whitespace-pre;
|
||||||
@apply text-editor font-mono;
|
@apply text-editor font-mono;
|
||||||
@@ -130,7 +132,7 @@
|
|||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
@apply border border-dashed;
|
@apply border border-dashed;
|
||||||
@apply border-border bg-surface-highlight text-text px-4 py-3 rounded text-base;
|
@apply border-border bg-surface-highlight text-text px-4 py-3 rounded-sm text-base;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@apply block font-bold mb-1;
|
@apply block font-bold mb-1;
|
||||||
@@ -161,7 +163,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
@apply italic py-3 pl-5 pr-3 border-l-8 border-surface-active text-lg text-text bg-surface-highlight rounded shadow-lg;
|
@apply italic py-3 pl-5 pr-3 border-l-8 border-surface-active text-lg text-text bg-surface-highlight rounded-sm shadow-lg;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@apply m-0;
|
@apply m-0;
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import type { GrpcConnection } from "@yaakapp-internal/models";
|
import type { GrpcConnection } from "@yaakapp-internal/models";
|
||||||
import { deleteModel } from "@yaakapp-internal/models";
|
import { deleteModel } from "@yaakapp-internal/models";
|
||||||
import { HStack, Icon } from "@yaakapp-internal/ui";
|
import { HStack, Icon } from "@yaakapp-internal/ui";
|
||||||
import { formatDistanceToNowStrict } from "date-fns";
|
import {
|
||||||
|
differenceInHours,
|
||||||
|
differenceInMinutes,
|
||||||
|
format,
|
||||||
|
isToday,
|
||||||
|
isYesterday,
|
||||||
|
} from "date-fns";
|
||||||
import { useDeleteGrpcConnections } from "../hooks/useDeleteGrpcConnections";
|
import { useDeleteGrpcConnections } from "../hooks/useDeleteGrpcConnections";
|
||||||
import { pluralizeCount } from "../lib/pluralize";
|
import { pluralizeCount } from "../lib/pluralize";
|
||||||
import { Dropdown } from "./core/Dropdown";
|
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
||||||
|
import { formatMillis } from "./core/HttpResponseDurationTag";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -20,6 +27,63 @@ export function RecentGrpcConnectionsDropdown({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId);
|
const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId);
|
||||||
const latestConnectionId = connections[0]?.id ?? "n/a";
|
const latestConnectionId = connections[0]?.id ?? "n/a";
|
||||||
|
const connectionHistoryItems: DropdownItem[] = [];
|
||||||
|
let lastHistoryGroup: string | null = null;
|
||||||
|
let hasRecentConnections = false;
|
||||||
|
let hasShownRecentEmptyState = false;
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (const c of connections) {
|
||||||
|
const createdAt = `${c.createdAt}Z`;
|
||||||
|
const createdAtDate = new Date(createdAt);
|
||||||
|
const minutesAgo = differenceInMinutes(now, createdAtDate);
|
||||||
|
const hoursAgo = differenceInHours(now, createdAtDate);
|
||||||
|
let historyGroup = format(createdAtDate, "MMM d, yyyy");
|
||||||
|
if (minutesAgo < 5) historyGroup = "Just now";
|
||||||
|
else if (minutesAgo < 15) historyGroup = "5 minutes ago";
|
||||||
|
else if (minutesAgo < 60) historyGroup = "15 minutes ago";
|
||||||
|
else if (hoursAgo < 3) historyGroup = "1 hour ago";
|
||||||
|
else if (hoursAgo < 6) historyGroup = "3 hours ago";
|
||||||
|
else if (isToday(createdAtDate)) historyGroup = "Today";
|
||||||
|
else if (isYesterday(createdAtDate)) historyGroup = "Yesterday";
|
||||||
|
else if (createdAtDate.getFullYear() === now.getFullYear()) historyGroup = format(createdAtDate, "MMM d");
|
||||||
|
const absoluteTime = format(createdAt, "MMM d, yyyy, h:mm:ss a O");
|
||||||
|
|
||||||
|
if (historyGroup === "Just now") {
|
||||||
|
hasRecentConnections = true;
|
||||||
|
} else if (!hasRecentConnections && !hasShownRecentEmptyState) {
|
||||||
|
connectionHistoryItems.push({
|
||||||
|
type: "content",
|
||||||
|
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent connections</span>,
|
||||||
|
});
|
||||||
|
hasShownRecentEmptyState = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyGroup !== "Just now" && historyGroup !== lastHistoryGroup) {
|
||||||
|
connectionHistoryItems.push({
|
||||||
|
type: "separator",
|
||||||
|
label: <span title={absoluteTime}>{historyGroup}</span>,
|
||||||
|
});
|
||||||
|
lastHistoryGroup = historyGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionHistoryItems.push({
|
||||||
|
label: (
|
||||||
|
<HStack space={2} className="text-sm" title={absoluteTime}>
|
||||||
|
<span className="font-mono">{formatMillis(c.elapsed)}</span>
|
||||||
|
</HStack>
|
||||||
|
),
|
||||||
|
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
||||||
|
onSelect: () => onPinnedConnectionId(c.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRecentConnections && !hasShownRecentEmptyState) {
|
||||||
|
connectionHistoryItems.push({
|
||||||
|
type: "content",
|
||||||
|
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent connections</span>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -36,16 +100,7 @@ export function RecentGrpcConnectionsDropdown({
|
|||||||
disabled: connections.length === 0,
|
disabled: connections.length === 0,
|
||||||
},
|
},
|
||||||
{ type: "separator", label: "History" },
|
{ type: "separator", label: "History" },
|
||||||
...connections.map((c) => ({
|
...connectionHistoryItems,
|
||||||
label: (
|
|
||||||
<HStack space={2}>
|
|
||||||
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago •{" "}
|
|
||||||
<span className="font-mono text-sm">{c.elapsed}ms</span>
|
|
||||||
</HStack>
|
|
||||||
),
|
|
||||||
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
|
||||||
onSelect: () => onPinnedConnectionId(c.id),
|
|
||||||
})),
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { deleteModel } from "@yaakapp-internal/models";
|
import { deleteModel } from "@yaakapp-internal/models";
|
||||||
import { HStack, Icon } from "@yaakapp-internal/ui";
|
import { HStack, Icon } from "@yaakapp-internal/ui";
|
||||||
import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse";
|
import {
|
||||||
|
differenceInHours,
|
||||||
|
differenceInMinutes,
|
||||||
|
format,
|
||||||
|
isToday,
|
||||||
|
isYesterday,
|
||||||
|
} from "date-fns";
|
||||||
import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses";
|
import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses";
|
||||||
import { useSaveResponse } from "../hooks/useSaveResponse";
|
import { useKeyValue } from "../hooks/useKeyValue";
|
||||||
import { pluralize } from "../lib/pluralize";
|
import { DismissibleBanner } from "./core/DismissibleBanner";
|
||||||
import { Dropdown } from "./core/Dropdown";
|
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
||||||
|
import { formatMillis } from "./core/HttpResponseDurationTag";
|
||||||
import { HttpStatusTag } from "./core/HttpStatusTag";
|
import { HttpStatusTag } from "./core/HttpStatusTag";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
|
import { SizeTag } from "./core/SizeTag";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
responses: HttpResponse[];
|
responses: HttpResponse[];
|
||||||
@@ -22,32 +30,93 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
|
|||||||
onPinnedResponseId,
|
onPinnedResponseId,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
|
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
|
||||||
|
const movedActionsBannerId = "response-actions-moved-to-response-menu-2026-07-02-v2";
|
||||||
|
const { value: dismissedMovedActions } = useKeyValue<boolean>({
|
||||||
|
namespace: "global",
|
||||||
|
key: ["dismiss-banner", movedActionsBannerId],
|
||||||
|
fallback: false,
|
||||||
|
});
|
||||||
const latestResponseId = responses[0]?.id ?? "n/a";
|
const latestResponseId = responses[0]?.id ?? "n/a";
|
||||||
const saveResponse = useSaveResponse(activeResponse);
|
const responseHistoryItems: DropdownItem[] = [];
|
||||||
const copyResponse = useCopyHttpResponse(activeResponse);
|
let lastHistoryGroup: string | null = null;
|
||||||
|
let hasRecentResponses = false;
|
||||||
|
let hasShownRecentEmptyState = false;
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (const r of responses) {
|
||||||
|
const createdAt = `${r.createdAt}Z`;
|
||||||
|
const createdAtDate = new Date(createdAt);
|
||||||
|
const minutesAgo = differenceInMinutes(now, createdAtDate);
|
||||||
|
const hoursAgo = differenceInHours(now, createdAtDate);
|
||||||
|
let historyGroup = format(createdAtDate, "MMM d, yyyy");
|
||||||
|
if (minutesAgo < 5) historyGroup = "Just now";
|
||||||
|
else if (minutesAgo < 15) historyGroup = "5 minutes ago";
|
||||||
|
else if (minutesAgo < 60) historyGroup = "15 minutes ago";
|
||||||
|
else if (hoursAgo < 3) historyGroup = "1 hour ago";
|
||||||
|
else if (hoursAgo < 6) historyGroup = "3 hours ago";
|
||||||
|
else if (isToday(createdAtDate)) historyGroup = "Today";
|
||||||
|
else if (isYesterday(createdAtDate)) historyGroup = "Yesterday";
|
||||||
|
else if (createdAtDate.getFullYear() === now.getFullYear()) historyGroup = format(createdAtDate, "MMM d");
|
||||||
|
const absoluteTime = format(createdAt, "MMM d, yyyy, h:mm:ss a O");
|
||||||
|
|
||||||
|
if (historyGroup === "Just now") {
|
||||||
|
hasRecentResponses = true;
|
||||||
|
} else if (!hasRecentResponses && !hasShownRecentEmptyState) {
|
||||||
|
responseHistoryItems.push({
|
||||||
|
type: "content",
|
||||||
|
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent requests</span>,
|
||||||
|
});
|
||||||
|
hasShownRecentEmptyState = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyGroup !== "Just now" && historyGroup !== lastHistoryGroup) {
|
||||||
|
responseHistoryItems.push({
|
||||||
|
type: "separator",
|
||||||
|
label: <span title={absoluteTime}>{historyGroup}</span>,
|
||||||
|
});
|
||||||
|
lastHistoryGroup = historyGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseHistoryItems.push({
|
||||||
|
label: (
|
||||||
|
<HStack space={2} className="text-sm" title={absoluteTime}>
|
||||||
|
<HttpStatusTag short className="text-xs" response={r} />
|
||||||
|
<span className="text-text-subtlest">•</span>
|
||||||
|
<span className="font-mono">{r.elapsed >= 0 ? formatMillis(r.elapsed) : "n/a"}</span>
|
||||||
|
<span className="text-text-subtlest">•</span>
|
||||||
|
<SizeTag
|
||||||
|
className="text-xs"
|
||||||
|
contentLength={r.contentLength ?? 0}
|
||||||
|
contentLengthCompressed={r.contentLengthCompressed}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
),
|
||||||
|
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
||||||
|
onSelect: () => onPinnedResponseId(r.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRecentResponses && !hasShownRecentEmptyState) {
|
||||||
|
responseHistoryItems.push({
|
||||||
|
type: "content",
|
||||||
|
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent requests</span>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={[
|
items={[
|
||||||
{
|
|
||||||
label: "Save to File",
|
|
||||||
onSelect: saveResponse.mutate,
|
|
||||||
leftSlot: <Icon icon="save" />,
|
|
||||||
hidden: responses.length === 0 || !!activeResponse.error,
|
|
||||||
disabled: activeResponse.state !== "closed" && activeResponse.status >= 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Copy Body",
|
|
||||||
onSelect: copyResponse.mutate,
|
|
||||||
leftSlot: <Icon icon="copy" />,
|
|
||||||
hidden: responses.length === 0 || !!activeResponse.error,
|
|
||||||
disabled: activeResponse.state !== "closed" && activeResponse.status >= 100,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "Delete",
|
label: "Delete",
|
||||||
leftSlot: <Icon icon="trash" />,
|
leftSlot: <Icon icon="trash" />,
|
||||||
onSelect: () => deleteModel(activeResponse),
|
onSelect: () => deleteModel(activeResponse),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Delete all",
|
||||||
|
leftSlot: <Icon icon="trash" />,
|
||||||
|
onSelect: deleteAllResponses.mutate,
|
||||||
|
disabled: responses.length === 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Unpin Response",
|
label: "Unpin Response",
|
||||||
onSelect: () => onPinnedResponseId(activeResponse.id),
|
onSelect: () => onPinnedResponseId(activeResponse.id),
|
||||||
@@ -55,25 +124,25 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
|
|||||||
hidden: latestResponseId === activeResponse.id,
|
hidden: latestResponseId === activeResponse.id,
|
||||||
disabled: responses.length === 0,
|
disabled: responses.length === 0,
|
||||||
},
|
},
|
||||||
{ type: "separator", label: "History" },
|
|
||||||
{
|
{
|
||||||
label: `Delete ${responses.length} ${pluralize("Response", responses.length)}`,
|
type: "content",
|
||||||
onSelect: deleteAllResponses.mutate,
|
hidden: dismissedMovedActions === true,
|
||||||
hidden: responses.length === 0,
|
|
||||||
disabled: responses.length === 0,
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
...responses.map((r: HttpResponse) => ({
|
|
||||||
label: (
|
label: (
|
||||||
<HStack space={2}>
|
<DismissibleBanner
|
||||||
<HttpStatusTag short className="text-xs" response={r} />
|
id={movedActionsBannerId}
|
||||||
<span className="text-text-subtle">→</span>{" "}
|
color="info"
|
||||||
<span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : "n/a"}</span>
|
size="xs"
|
||||||
</HStack>
|
className="max-w-72"
|
||||||
|
>
|
||||||
|
<p>Copy and save actions moved to the Response tab menu.</p>
|
||||||
|
</DismissibleBanner>
|
||||||
),
|
),
|
||||||
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
},
|
||||||
onSelect: () => onPinnedResponseId(r.id),
|
{
|
||||||
})),
|
type: "separator",
|
||||||
|
label: "Recent",
|
||||||
|
},
|
||||||
|
...responseHistoryItems,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import type { WebsocketConnection } from "@yaakapp-internal/models";
|
import type { WebsocketConnection } from "@yaakapp-internal/models";
|
||||||
import { deleteModel, getModel } from "@yaakapp-internal/models";
|
import { deleteModel, getModel } from "@yaakapp-internal/models";
|
||||||
import { HStack, Icon } from "@yaakapp-internal/ui";
|
import { HStack, Icon } from "@yaakapp-internal/ui";
|
||||||
import { formatDistanceToNowStrict } from "date-fns";
|
import {
|
||||||
|
differenceInHours,
|
||||||
|
differenceInMinutes,
|
||||||
|
format,
|
||||||
|
isToday,
|
||||||
|
isYesterday,
|
||||||
|
} from "date-fns";
|
||||||
import { deleteWebsocketConnections } from "../commands/deleteWebsocketConnections";
|
import { deleteWebsocketConnections } from "../commands/deleteWebsocketConnections";
|
||||||
import { pluralizeCount } from "../lib/pluralize";
|
import { pluralizeCount } from "../lib/pluralize";
|
||||||
import { Dropdown } from "./core/Dropdown";
|
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
||||||
|
import { formatMillis } from "./core/HttpResponseDurationTag";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -19,6 +26,63 @@ export function RecentWebsocketConnectionsDropdown({
|
|||||||
onPinnedConnectionId,
|
onPinnedConnectionId,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const latestConnectionId = connections[0]?.id ?? "n/a";
|
const latestConnectionId = connections[0]?.id ?? "n/a";
|
||||||
|
const connectionHistoryItems: DropdownItem[] = [];
|
||||||
|
let lastHistoryGroup: string | null = null;
|
||||||
|
let hasRecentConnections = false;
|
||||||
|
let hasShownRecentEmptyState = false;
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (const c of connections) {
|
||||||
|
const createdAt = `${c.createdAt}Z`;
|
||||||
|
const createdAtDate = new Date(createdAt);
|
||||||
|
const minutesAgo = differenceInMinutes(now, createdAtDate);
|
||||||
|
const hoursAgo = differenceInHours(now, createdAtDate);
|
||||||
|
let historyGroup = format(createdAtDate, "MMM d, yyyy");
|
||||||
|
if (minutesAgo < 5) historyGroup = "Just now";
|
||||||
|
else if (minutesAgo < 15) historyGroup = "5 minutes ago";
|
||||||
|
else if (minutesAgo < 60) historyGroup = "15 minutes ago";
|
||||||
|
else if (hoursAgo < 3) historyGroup = "1 hour ago";
|
||||||
|
else if (hoursAgo < 6) historyGroup = "3 hours ago";
|
||||||
|
else if (isToday(createdAtDate)) historyGroup = "Today";
|
||||||
|
else if (isYesterday(createdAtDate)) historyGroup = "Yesterday";
|
||||||
|
else if (createdAtDate.getFullYear() === now.getFullYear()) historyGroup = format(createdAtDate, "MMM d");
|
||||||
|
const absoluteTime = format(createdAt, "MMM d, yyyy, h:mm:ss a O");
|
||||||
|
|
||||||
|
if (historyGroup === "Just now") {
|
||||||
|
hasRecentConnections = true;
|
||||||
|
} else if (!hasRecentConnections && !hasShownRecentEmptyState) {
|
||||||
|
connectionHistoryItems.push({
|
||||||
|
type: "content",
|
||||||
|
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent connections</span>,
|
||||||
|
});
|
||||||
|
hasShownRecentEmptyState = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyGroup !== "Just now" && historyGroup !== lastHistoryGroup) {
|
||||||
|
connectionHistoryItems.push({
|
||||||
|
type: "separator",
|
||||||
|
label: <span title={absoluteTime}>{historyGroup}</span>,
|
||||||
|
});
|
||||||
|
lastHistoryGroup = historyGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionHistoryItems.push({
|
||||||
|
label: (
|
||||||
|
<HStack space={2} className="text-sm" title={absoluteTime}>
|
||||||
|
<span className="font-mono">{formatMillis(c.elapsed)}</span>
|
||||||
|
</HStack>
|
||||||
|
),
|
||||||
|
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
||||||
|
onSelect: () => onPinnedConnectionId(c.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRecentConnections && !hasShownRecentEmptyState) {
|
||||||
|
connectionHistoryItems.push({
|
||||||
|
type: "content",
|
||||||
|
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent connections</span>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -40,16 +104,7 @@ export function RecentWebsocketConnectionsDropdown({
|
|||||||
disabled: connections.length === 0,
|
disabled: connections.length === 0,
|
||||||
},
|
},
|
||||||
{ type: "separator", label: "History" },
|
{ type: "separator", label: "History" },
|
||||||
...connections.map((c) => ({
|
...connectionHistoryItems,
|
||||||
label: (
|
|
||||||
<HStack space={2}>
|
|
||||||
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago •{" "}
|
|
||||||
<span className="font-mono text-sm">{c.elapsed}ms</span>
|
|
||||||
</HStack>
|
|
||||||
),
|
|
||||||
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
|
||||||
onSelect: () => onPinnedConnectionId(c.id),
|
|
||||||
})),
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function ResponseCookies({ response }: Props) {
|
|||||||
{cookie.value}
|
{cookie.value}
|
||||||
</span>
|
</span>
|
||||||
{cookie.isDeleted && (
|
{cookie.isDeleted && (
|
||||||
<span className="text-xs font-sans text-danger bg-danger/10 px-1.5 py-0.5 rounded">
|
<span className="text-xs font-sans text-danger bg-danger/10 px-1.5 py-0.5 rounded-sm">
|
||||||
Deleted
|
Deleted
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
|
import { format, formatDistanceToNowStrict } from "date-fns";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { CountBadge } from "./core/CountBadge";
|
import { CountBadge } from "./core/CountBadge";
|
||||||
import { DetailsBanner } from "./core/DetailsBanner";
|
import { DetailsBanner } from "./core/DetailsBanner";
|
||||||
@@ -29,12 +30,20 @@ export function ResponseHeaders({ response }: Props) {
|
|||||||
<div className="overflow-auto h-full pb-4 gap-y-3 flex flex-col pr-0.5">
|
<div className="overflow-auto h-full pb-4 gap-y-3 flex flex-col pr-0.5">
|
||||||
<DetailsBanner storageKey={`${response.requestId}.general`} summary={<h2>Info</h2>}>
|
<DetailsBanner storageKey={`${response.requestId}.general`} summary={<h2>Info</h2>}>
|
||||||
<KeyValueRows>
|
<KeyValueRows>
|
||||||
|
<KeyValueRow labelColor="secondary" label="Sent">
|
||||||
|
<time
|
||||||
|
dateTime={new Date(`${response.createdAt}Z`).toISOString()}
|
||||||
|
title={formatDistanceToNowStrict(`${response.createdAt}Z`, { addSuffix: true })}
|
||||||
|
>
|
||||||
|
{format(`${response.createdAt}Z`, "MMM d, yyyy, h:mm:ss a O")}
|
||||||
|
</time>
|
||||||
|
</KeyValueRow>
|
||||||
<KeyValueRow labelColor="secondary" label="Request URL">
|
<KeyValueRow labelColor="secondary" label="Request URL">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="select-text cursor-text">{response.url}</span>
|
<span className="select-text cursor-text">{response.url}</span>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
className="inline-block w-auto !h-auto opacity-50 hover:opacity-100"
|
className="inline-block w-auto h-auto! opacity-50 hover:opacity-100"
|
||||||
icon="external_link"
|
icon="external_link"
|
||||||
onClick={() => openUrl(response.url)}
|
onClick={() => openUrl(response.url)}
|
||||||
title="Open in browser"
|
title="Open in browser"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function ResponseInfo({ response }: Props) {
|
|||||||
URL
|
URL
|
||||||
<IconButton
|
<IconButton
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
className="inline-block w-auto ml-1 !h-auto opacity-50 hover:opacity-100"
|
className="inline-block w-auto ml-1 h-auto! opacity-50 hover:opacity-100"
|
||||||
icon="external_link"
|
icon="external_link"
|
||||||
onClick={() => openUrl(response.url)}
|
onClick={() => openUrl(response.url)}
|
||||||
title="Open in browser"
|
title="Open in browser"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function RouteError({ error }: { error: unknown }) {
|
|||||||
typeof error === "object" && error != null && "stack" in error ? String(error.stack) : null;
|
typeof error === "object" && error != null && "stack" in error ? String(error.stack) : null;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<VStack space={5} className="w-[50rem] !h-auto">
|
<VStack space={5} className="w-200 h-auto!">
|
||||||
<Heading>Route Error 🔥</Heading>
|
<Heading>Route Error 🔥</Heading>
|
||||||
<FormattedError>
|
<FormattedError>
|
||||||
{message}
|
{message}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function SelectFile({
|
|||||||
"rtl mr-1.5",
|
"rtl mr-1.5",
|
||||||
inline && "w-full",
|
inline && "w-full",
|
||||||
filePath && inline && "font-mono text-xs",
|
filePath && inline && "font-mono text-xs",
|
||||||
isHovering && "!border-notice",
|
isHovering && "border-notice!",
|
||||||
)}
|
)}
|
||||||
color={isHovering ? "primary" : "secondary"}
|
color={isHovering ? "primary" : "secondary"}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default function Settings({ hide }: Props) {
|
|||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
defaultValue={mainTab || tabFromQuery}
|
defaultValue={mainTab || tabFromQuery}
|
||||||
addBorders
|
addBorders
|
||||||
tabListClassName="min-w-[10rem] bg-surface x-theme-sidebar border-r border-border pl-3"
|
tabListClassName="min-w-40 bg-surface x-theme-sidebar border-r border-border pl-3"
|
||||||
label="Settings"
|
label="Settings"
|
||||||
tabs={tabs.map(
|
tabs={tabs.map(
|
||||||
(value): TabItem => ({
|
(value): TabItem => ({
|
||||||
@@ -131,28 +131,28 @@ export default function Settings({ hide }: Props) {
|
|||||||
}),
|
}),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-6 !py-4">
|
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-6 py-4!">
|
||||||
<SettingsGeneral />
|
<SettingsGeneral />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full px-6 !py-4">
|
<TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full px-6 py-4!">
|
||||||
<SettingsInterface />
|
<SettingsInterface />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_THEME} className="overflow-y-auto h-full px-6 !py-4">
|
<TabContent value={TAB_THEME} className="overflow-y-auto h-full px-6 py-4!">
|
||||||
<SettingsTheme />
|
<SettingsTheme />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_SHORTCUTS} className="overflow-y-auto h-full px-6 !py-4">
|
<TabContent value={TAB_SHORTCUTS} className="overflow-y-auto h-full px-6 py-4!">
|
||||||
<SettingsHotkeys />
|
<SettingsHotkeys />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1">
|
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1">
|
||||||
<SettingsPlugins defaultSubtab={mainTab === TAB_PLUGINS ? subtab : undefined} />
|
<SettingsPlugins defaultSubtab={mainTab === TAB_PLUGINS ? subtab : undefined} />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 !py-4">
|
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 py-4!">
|
||||||
<SettingsProxy />
|
<SettingsProxy />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_CERTIFICATES} className="overflow-y-auto h-full px-6 !py-4">
|
<TabContent value={TAB_CERTIFICATES} className="overflow-y-auto h-full px-6 py-4!">
|
||||||
<SettingsCertificates />
|
<SettingsCertificates />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-6 !py-4">
|
<TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-6 py-4!">
|
||||||
<SettingsLicense />
|
<SettingsLicense />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function SettingsGeneral() {
|
|||||||
model={settings}
|
model={settings}
|
||||||
modelKey="updateChannel"
|
modelKey="updateChannel"
|
||||||
label="Update Channel"
|
label="Update Channel"
|
||||||
selectClassName="!w-full"
|
selectClassName="w-full!"
|
||||||
options={[
|
options={[
|
||||||
{ label: "Stable", value: "stable" },
|
{ label: "Stable", value: "stable" },
|
||||||
{ label: "Beta", value: "beta" },
|
{ label: "Beta", value: "beta" },
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ function RecordHotkeyDialog({ label, onSave, onCancel }: RecordHotkeyDialogProps
|
|||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center justify-center",
|
"flex items-center justify-center",
|
||||||
"px-4 py-2 rounded-lg bg-surface-highlight border outline-none cursor-default w-full",
|
"px-4 py-2 rounded-lg bg-surface-highlight border outline-hidden cursor-default w-full",
|
||||||
"border-border-subtle focus:border-border-focus",
|
"border-border-subtle focus:border-border-focus",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function SettingsInterface() {
|
|||||||
<SettingSelectControl
|
<SettingSelectControl
|
||||||
name="uiFont"
|
name="uiFont"
|
||||||
label="Interface font"
|
label="Interface font"
|
||||||
selectClassName="!w-72"
|
selectClassName="w-72!"
|
||||||
value={settings.interfaceFont ?? NULL_FONT_VALUE}
|
value={settings.interfaceFont ?? NULL_FONT_VALUE}
|
||||||
defaultValue={NULL_FONT_VALUE}
|
defaultValue={NULL_FONT_VALUE}
|
||||||
options={[
|
options={[
|
||||||
@@ -106,7 +106,7 @@ export function SettingsInterface() {
|
|||||||
<SettingSelectControl
|
<SettingSelectControl
|
||||||
name="interfaceFontSize"
|
name="interfaceFontSize"
|
||||||
label="Interface Font Size"
|
label="Interface Font Size"
|
||||||
selectClassName="!w-20"
|
selectClassName="w-20!"
|
||||||
value={`${settings.interfaceFontSize}`}
|
value={`${settings.interfaceFontSize}`}
|
||||||
defaultValue="14"
|
defaultValue="14"
|
||||||
options={fontSizeOptions}
|
options={fontSizeOptions}
|
||||||
@@ -123,7 +123,7 @@ export function SettingsInterface() {
|
|||||||
<SettingSelectControl
|
<SettingSelectControl
|
||||||
name="editorFont"
|
name="editorFont"
|
||||||
label="Editor font"
|
label="Editor font"
|
||||||
selectClassName="!w-72"
|
selectClassName="w-72!"
|
||||||
value={settings.editorFont ?? NULL_FONT_VALUE}
|
value={settings.editorFont ?? NULL_FONT_VALUE}
|
||||||
defaultValue={NULL_FONT_VALUE}
|
defaultValue={NULL_FONT_VALUE}
|
||||||
options={[
|
options={[
|
||||||
@@ -139,7 +139,7 @@ export function SettingsInterface() {
|
|||||||
<SettingSelectControl
|
<SettingSelectControl
|
||||||
name="editorFontSize"
|
name="editorFontSize"
|
||||||
label="Editor Font Size"
|
label="Editor Font Size"
|
||||||
selectClassName="!w-20"
|
selectClassName="w-20!"
|
||||||
value={`${settings.editorFontSize}`}
|
value={`${settings.editorFontSize}`}
|
||||||
defaultValue="12"
|
defaultValue="12"
|
||||||
options={fontSizeOptions}
|
options={fontSizeOptions}
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ function PluginTableRow({
|
|||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
<TableCell className="!py-0">
|
<TableCell className="py-0!">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
hideLabel
|
hideLabel
|
||||||
title={plugin?.enabled ? "Disable plugin" : "Enable plugin"}
|
title={plugin?.enabled ? "Disable plugin" : "Enable plugin"}
|
||||||
@@ -249,7 +249,7 @@ function PluginTableRow({
|
|||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="!py-0">
|
<TableCell className="py-0!">
|
||||||
<HStack justifyContent="end" space={1.5}>
|
<HStack justifyContent="end" space={1.5}>
|
||||||
{plugin != null && latestVersion != null ? (
|
{plugin != null && latestVersion != null ? (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function SettingsProxy() {
|
|||||||
{ label: "Custom proxy configuration", value: "enabled" },
|
{ label: "Custom proxy configuration", value: "enabled" },
|
||||||
{ label: "No proxy", value: "disabled" },
|
{ label: "No proxy", value: "disabled" },
|
||||||
]}
|
]}
|
||||||
selectClassName="!w-64"
|
selectClassName="w-64!"
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export function SettingsProxy() {
|
|||||||
description="Comma-separated list of hosts that should bypass the proxy."
|
description="Comma-separated list of hosts that should bypass the proxy."
|
||||||
value={settings.proxy.bypass}
|
value={settings.proxy.bypass}
|
||||||
placeholder="127.0.0.1, *.example.com, localhost:3000"
|
placeholder="127.0.0.1, *.example.com, localhost:3000"
|
||||||
inputWidthClassName="!w-96"
|
inputWidthClassName="w-96!"
|
||||||
onChange={(bypass) => patchProxy({ bypass })}
|
onChange={(bypass) => patchProxy({ bypass })}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export function SettingsTheme() {
|
|||||||
<SettingsSection title="Preview">
|
<SettingsSection title="Preview">
|
||||||
<VStack
|
<VStack
|
||||||
space={3}
|
space={3}
|
||||||
className="mt-4 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto"
|
className="mt-4 w-full bg-surface p-3 border border-dashed border-border-subtle rounded-sm overflow-x-auto"
|
||||||
>
|
>
|
||||||
<HStack className="text" space={1.5}>
|
<HStack className="text" space={1.5}>
|
||||||
<Icon icon={appearance === "dark" ? "moon" : "sun"} />
|
<Icon icon={appearance === "dark" ? "moon" : "sun"} />
|
||||||
|
|||||||
@@ -588,7 +588,7 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
rightSlot={
|
rightSlot={
|
||||||
filterText.text && (
|
filterText.text && (
|
||||||
<IconButton
|
<IconButton
|
||||||
className="!bg-transparent !h-auto min-h-full opacity-50 hover:opacity-100 -mr-1"
|
className="bg-transparent! h-auto! min-h-full opacity-50 hover:opacity-100 -mr-1"
|
||||||
icon="x"
|
icon="x"
|
||||||
title="Clear filter"
|
title="Clear filter"
|
||||||
onClick={clearFilterText}
|
onClick={clearFilterText}
|
||||||
@@ -667,8 +667,8 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
<div className="p-3 text-sm text-center">
|
<div className="p-3 text-sm text-center">
|
||||||
{(emptyFilterSuggestions?.length ?? 0) > 0 ? (
|
{(emptyFilterSuggestions?.length ?? 0) > 0 ? (
|
||||||
<EmptyStateText
|
<EmptyStateText
|
||||||
wrapperClassName="!h-auto mb-auto"
|
wrapperClassName="h-auto! mb-auto"
|
||||||
className="!h-auto py-3 px-3 !text-text-subtle text-sm leading-relaxed text-center"
|
className="h-auto! py-3 px-3 text-text-subtle! text-sm leading-relaxed text-center"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
No results, but found matches for{" "}
|
No results, but found matches for{" "}
|
||||||
@@ -677,7 +677,7 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
{i > 0 && " or "}
|
{i > 0 && " or "}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="max-w-full rounded align-middle focus-visible:outline focus-visible:outline-2 focus-visible:outline-info"
|
className="max-w-full rounded-sm align-middle focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-info"
|
||||||
onClick={() => applyFilterExample(suggestion.filterText)}
|
onClick={() => applyFilterExample(suggestion.filterText)}
|
||||||
>
|
>
|
||||||
<InlineCode className="inline-block max-w-36 truncate align-middle whitespace-nowrap transition-colors hover:border-border hover:bg-surface-active hover:text-text">
|
<InlineCode className="inline-block max-w-36 truncate align-middle whitespace-nowrap transition-colors hover:border-border hover:bg-surface-active hover:text-text">
|
||||||
@@ -690,8 +690,8 @@ function Sidebar({ className }: { className?: string }) {
|
|||||||
</EmptyStateText>
|
</EmptyStateText>
|
||||||
) : (
|
) : (
|
||||||
<EmptyStateText
|
<EmptyStateText
|
||||||
wrapperClassName="!h-auto mb-auto"
|
wrapperClassName="h-auto! mb-auto"
|
||||||
className="!h-auto py-3 px-3 !text-text-subtle text-sm leading-relaxed text-center"
|
className="h-auto! py-3 px-3 text-text-subtle! text-sm leading-relaxed text-center"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
No results for{" "}
|
No results for{" "}
|
||||||
|
|||||||
@@ -208,10 +208,10 @@ function InitializedTemplateFunctionDialog({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<div className="relative w-full max-h-[10rem]">
|
<div className="relative w-full max-h-40">
|
||||||
<InlineCode
|
<InlineCode
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"block whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-auto hide-scrollbars !border-text-subtlest",
|
"block whitespace-pre-wrap select-text! cursor-text max-h-40 overflow-auto hide-scrollbars border-text-subtlest!",
|
||||||
tooLarge && "italic text-danger",
|
tooLarge && "italic text-danger",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -246,7 +246,7 @@ function InitializedTemplateFunctionDialog({
|
|||||||
) : (
|
) : (
|
||||||
<span />
|
<span />
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-stretch w-full flex-grow gap-2 [&>*]:flex-1">
|
<div className="flex justify-stretch w-full grow gap-2 *:flex-1">
|
||||||
{templateFunction.data.name === "secure" && (
|
{templateFunction.data.name === "secure" && (
|
||||||
<Button variant="border" color="secondary" onClick={setupOrConfigureEncryption}>
|
<Button variant="border" color="secondary" onClick={setupOrConfigureEncryption}>
|
||||||
Reveal Encryption Key
|
Reveal Encryption Key
|
||||||
@@ -271,7 +271,7 @@ TemplateFunctionDialog.show = (
|
|||||||
showDialog({
|
showDialog({
|
||||||
id: `template-function-${Math.random()}`, // Allow multiple at once
|
id: `template-function-${Math.random()}`, // Allow multiple at once
|
||||||
size: "md",
|
size: "md",
|
||||||
className: "h-[60rem]",
|
className: "h-240",
|
||||||
noPadding: true,
|
noPadding: true,
|
||||||
title: <InlineCode>{fn.name}(…)</InlineCode>,
|
title: <InlineCode>{fn.name}(…)</InlineCode>,
|
||||||
description: fn.description,
|
description: fn.description,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const UrlBar = memo(function UrlBar({
|
|||||||
iconSize="md"
|
iconSize="md"
|
||||||
title="Send Request"
|
title="Send Request"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-8 mr-0.5 !h-full"
|
className="w-8 mr-0.5 h-full!"
|
||||||
iconColor="secondary"
|
iconColor="secondary"
|
||||||
icon={isLoading ? "x" : submitIcon}
|
icon={isLoading ? "x" : submitIcon}
|
||||||
hotkeyAction="request.send"
|
hotkeyAction="request.send"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { useRequestUpdateKey } from "../hooks/useRequestUpdateKey";
|
|||||||
import { deepEqualAtom } from "../lib/atoms";
|
import { deepEqualAtom } from "../lib/atoms";
|
||||||
import { languageFromContentType } from "../lib/contentType";
|
import { languageFromContentType } from "../lib/contentType";
|
||||||
import { generateId } from "../lib/generateId";
|
import { generateId } from "../lib/generateId";
|
||||||
|
import { extractPathPlaceholders } from "../lib/pathPlaceholders";
|
||||||
import { prepareImportQuerystring } from "../lib/prepareImportQuerystring";
|
import { prepareImportQuerystring } from "../lib/prepareImportQuerystring";
|
||||||
import { resolvedModelName } from "../lib/resolvedModelName";
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
import { CountBadge } from "./core/CountBadge";
|
import { CountBadge } from "./core/CountBadge";
|
||||||
@@ -83,9 +84,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
||||||
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
|
const placeholderNames = extractPathPlaceholders(activeRequest.url);
|
||||||
(m) => m[1] ?? "",
|
|
||||||
);
|
|
||||||
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
|
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
|
||||||
const items: Pair[] = [...nonEmptyParameters];
|
const items: Pair[] = [...nonEmptyParameters];
|
||||||
for (const name of placeholderNames) {
|
for (const name of placeholderNames) {
|
||||||
@@ -218,7 +217,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
title="Close connection"
|
title="Close connection"
|
||||||
icon="x"
|
icon="x"
|
||||||
iconColor="secondary"
|
iconColor="secondary"
|
||||||
className="w-8 mr-0.5 !h-full"
|
className="w-8 mr-0.5 h-full!"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -237,7 +236,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
ref={tabsRef}
|
ref={tabsRef}
|
||||||
label="Request"
|
label="Request"
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
tabListClassName="mt-1 !mb-1.5"
|
tabListClassName="mt-1 mb-1.5!"
|
||||||
storageKey={TABS_STORAGE_KEY}
|
storageKey={TABS_STORAGE_KEY}
|
||||||
activeTabKey={activeRequestId}
|
activeTabKey={activeRequestId}
|
||||||
>
|
>
|
||||||
@@ -284,7 +283,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
hideLabel
|
hideLabel
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
defaultValue={activeRequest.name}
|
defaultValue={activeRequest.name}
|
||||||
className="font-sans !text-xl !px-0"
|
className="font-sans text-xl! px-0!"
|
||||||
containerClassName="border-0"
|
containerClassName="border-0"
|
||||||
placeholder={resolvedModelName(activeRequest)}
|
placeholder={resolvedModelName(activeRequest)}
|
||||||
onChange={(name) => patchModel(activeRequest, { name })}
|
onChange={(name) => patchModel(activeRequest, { name })}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function Workspace() {
|
|||||||
<div style={environmentBgStyle} className="absolute inset-0 opacity-[0.07]" />
|
<div style={environmentBgStyle} className="absolute inset-0 opacity-[0.07]" />
|
||||||
<div
|
<div
|
||||||
style={environmentBgStyle}
|
style={environmentBgStyle}
|
||||||
className="absolute left-0 right-0 -bottom-[1px] h-[1px] opacity-20"
|
className="absolute left-0 right-0 -bottom-px h-px opacity-20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<WorkspaceHeader className="pointer-events-none" floatingSidebar={floating} />
|
<WorkspaceHeader className="pointer-events-none" floatingSidebar={floating} />
|
||||||
@@ -162,7 +162,7 @@ function WorkspaceBody() {
|
|||||||
// Delay the entering because the workspaces might load after a slight delay
|
// Delay the entering because the workspaces might load after a slight delay
|
||||||
transition={{ delay: 0.5 }}
|
transition={{ delay: 0.5 }}
|
||||||
>
|
>
|
||||||
<Banner color="warning" className="max-w-[30rem]">
|
<Banner color="warning" className="max-w-120">
|
||||||
The active workspace was not found. Select a workspace from the header menu or report this
|
The active workspace was not found. Select a workspace from the header menu or report this
|
||||||
bug to <FeedbackLink />
|
bug to <FeedbackLink />
|
||||||
</Banner>
|
</Banner>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
size="sm"
|
size="sm"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"text !px-2 truncate",
|
"text px-2! truncate",
|
||||||
workspace === null && "italic opacity-disabled",
|
workspace === null && "italic opacity-disabled",
|
||||||
)}
|
)}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ function KeyRevealer({
|
|||||||
|
|
||||||
function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) {
|
function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs font-mono [&_*]:cursor-auto [&_*]:select-text">
|
<span className="text-xs font-mono **:cursor-auto **:select-text">
|
||||||
{show ? (
|
{show ? (
|
||||||
keyText.split("").map((c, i) => {
|
keyText.split("").map((c, i) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
placeholder="Workspace Name"
|
placeholder="Workspace Name"
|
||||||
label="Name"
|
label="Name"
|
||||||
defaultValue={workspace.name}
|
defaultValue={workspace.name}
|
||||||
className="!text-base font-sans"
|
className="text-base! font-sans"
|
||||||
onChange={(name) => patchModel(workspace, { name })}
|
onChange={(name) => patchModel(workspace, { name })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
<InlineCode className="flex gap-1 items-center text-primary pl-2.5">
|
<InlineCode className="flex gap-1 items-center text-primary pl-2.5">
|
||||||
{workspaceId}
|
{workspaceId}
|
||||||
<CopyIconButton
|
<CopyIconButton
|
||||||
className="opacity-70 !text-primary"
|
className="opacity-70 text-primary!"
|
||||||
size="2xs"
|
size="2xs"
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
title="Copy workspace ID"
|
title="Copy workspace ID"
|
||||||
@@ -182,7 +182,7 @@ WorkspaceSettingsDialog.show = (workspaceId: string, tab?: WorkspaceSettingsTab)
|
|||||||
showDialog({
|
showDialog({
|
||||||
id: "workspace-settings",
|
id: "workspace-settings",
|
||||||
size: "lg",
|
size: "lg",
|
||||||
className: "h-[calc(100vh-5rem)] !max-h-[50rem]",
|
className: "h-[calc(100vh-5rem)] max-h-200!",
|
||||||
noPadding: true,
|
noPadding: true,
|
||||||
render: ({ hide }) => (
|
render: ({ hide }) => (
|
||||||
<WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />
|
<WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function AutoScroller<T>({
|
|||||||
size="sm"
|
size="sm"
|
||||||
iconSize="md"
|
iconSize="md"
|
||||||
variant="border"
|
variant="border"
|
||||||
className="!bg-surface z-10"
|
className="bg-surface! z-10"
|
||||||
onClick={() => setAutoScroll((v) => !v)}
|
onClick={() => setAutoScroll((v) => !v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@ export function AutoScroller<T>({
|
|||||||
{header ?? <span aria-hidden />}
|
{header ?? <span aria-hidden />}
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="h-full w-full overflow-y-auto focus:outline-none"
|
className="h-full w-full overflow-y-auto focus:outline-hidden"
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
tabIndex={focusable ? 0 : undefined}
|
tabIndex={focusable ? 0 : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ export function Checkbox({
|
|||||||
<input
|
<input
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"appearance-none flex-shrink-0 border border-border",
|
"appearance-none shrink-0 border border-border",
|
||||||
size === "sm" && "w-4 h-4",
|
size === "sm" && "w-4 h-4",
|
||||||
size === "md" && "w-5 h-5",
|
size === "md" && "w-5 h-5",
|
||||||
"rounded outline-none ring-0",
|
"rounded-sm outline-hidden ring-0",
|
||||||
!disabled && "hocus:border-border-focus hocus:bg-focus/[5%]",
|
!disabled && "hocus:border-border-focus hocus:bg-focus/[5%]",
|
||||||
disabled && "border-dotted",
|
disabled && "border-dotted",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function ColorPicker({ onChange, color, className }: Props) {
|
|||||||
<div className={className}>
|
<div className={className}>
|
||||||
<HexColorPicker
|
<HexColorPicker
|
||||||
color={color ?? undefined}
|
color={color ?? undefined}
|
||||||
className="!w-full"
|
className="w-full!"
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
onChange(color);
|
onChange(color);
|
||||||
regenerateKey(); // To force input to change
|
regenerateKey(); // To force input to change
|
||||||
@@ -96,7 +96,7 @@ export function ColorPickerWithThemeColors({ onChange, color, className }: Props
|
|||||||
<>
|
<>
|
||||||
<HexColorPicker
|
<HexColorPicker
|
||||||
color={color ?? undefined}
|
color={color ?? undefined}
|
||||||
className="!w-full"
|
className="w-full!"
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
onChange(color);
|
onChange(color);
|
||||||
regenerateKey(); // To force input to change
|
regenerateKey(); // To force input to change
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function CountBadge({ count, count2, className, color, showZero }: Props)
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"flex items-center",
|
"flex items-center",
|
||||||
"opacity-70 border text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono",
|
"opacity-70 border text-4xs rounded-sm mb-0.5 px-1 ml-1 h-4 font-mono",
|
||||||
color == null && "border-border-subtle",
|
color == null && "border-border-subtle",
|
||||||
color === "primary" && "text-primary",
|
color === "primary" && "text-primary",
|
||||||
color === "secondary" && "text-secondary",
|
color === "secondary" && "text-secondary",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function DetailsBanner({
|
|||||||
return (
|
return (
|
||||||
<Banner color={color} className={className}>
|
<Banner color={color} className={className}>
|
||||||
<details className="group list-none" open={isOpen} onToggle={handleToggle} {...extraProps}>
|
<details className="group list-none" open={isOpen} onToggle={handleToggle} {...extraProps}>
|
||||||
<summary className="!cursor-default !select-none list-none flex items-center gap-3 focus:outline-none opacity-70">
|
<summary className="cursor-default! select-none! list-none flex items-center gap-3 focus:outline-hidden opacity-70">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"transition-transform",
|
"transition-transform",
|
||||||
|
|||||||
@@ -74,13 +74,13 @@ export function Dialog({
|
|||||||
"relative bg-surface pointer-events-auto",
|
"relative bg-surface pointer-events-auto",
|
||||||
"rounded-lg",
|
"rounded-lg",
|
||||||
"border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]",
|
"border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]",
|
||||||
"min-h-[10rem]",
|
"min-h-40",
|
||||||
"max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]",
|
"max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]",
|
||||||
size === "sm" && "w-[30rem]",
|
size === "sm" && "w-120",
|
||||||
size === "md" && "w-[50rem]",
|
size === "md" && "w-200",
|
||||||
size === "lg" && "w-[70rem]",
|
size === "lg" && "w-280",
|
||||||
size === "full" && "w-[100vw] h-[100vh]",
|
size === "full" && "w-screen h-screen",
|
||||||
size === "dynamic" && "min-w-[20rem] max-w-[100vw]",
|
size === "dynamic" && "min-w-80 max-w-[100vw]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{title ? (
|
{title ? (
|
||||||
|
|||||||
@@ -2,21 +2,26 @@ import type { Color } from "@yaakapp-internal/plugins";
|
|||||||
import type { BannerProps } from "@yaakapp-internal/ui";
|
import type { BannerProps } from "@yaakapp-internal/ui";
|
||||||
import { Banner } from "@yaakapp-internal/ui";
|
import { Banner } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useKeyValue } from "../../hooks/useKeyValue";
|
import { useKeyValue } from "../../hooks/useKeyValue";
|
||||||
import type { ButtonProps } from "./Button";
|
import type { ButtonProps } from "./Button";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
|
|
||||||
|
type DismissibleBannerSize = "sm" | "xs";
|
||||||
|
|
||||||
export function DismissibleBanner({
|
export function DismissibleBanner({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
id,
|
id,
|
||||||
|
size = "sm",
|
||||||
onDismiss,
|
onDismiss,
|
||||||
onShow,
|
onShow,
|
||||||
actions,
|
actions,
|
||||||
...props
|
...props
|
||||||
}: BannerProps & {
|
}: BannerProps & {
|
||||||
id: string;
|
id: string;
|
||||||
|
size?: DismissibleBannerSize;
|
||||||
onDismiss?: () => void | Promise<void>;
|
onDismiss?: () => void | Promise<void>;
|
||||||
onShow?: () => void | Promise<void>;
|
onShow?: () => void | Promise<void>;
|
||||||
actions?: {
|
actions?: {
|
||||||
@@ -46,17 +51,36 @@ export function DismissibleBanner({
|
|||||||
|
|
||||||
if (!shouldShow) return null;
|
if (!shouldShow) return null;
|
||||||
|
|
||||||
|
const actionSize: ButtonProps["size"] = size === "xs" ? "2xs" : "xs";
|
||||||
|
const stopParentClick = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Banner className={classNames(className, "relative")} {...props}>
|
<Banner
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
"relative",
|
||||||
|
size === "xs" && "!px-2 !py-2 text-xs",
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<div className="@container">
|
<div className="@container">
|
||||||
<div className="grid gap-2 @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center @[34rem]:gap-3">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"grid @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center",
|
||||||
|
size === "xs" ? "gap-1.5 @[34rem]:gap-2" : "gap-2 @[34rem]:gap-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<div className="flex flex-wrap gap-1.5 @[34rem]:justify-end">
|
<div className="flex flex-wrap gap-1.5 @[34rem]:justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="border"
|
variant="border"
|
||||||
color={props.color}
|
color={props.color}
|
||||||
size="xs"
|
size={actionSize}
|
||||||
onClick={() => {
|
onClick={(event) => {
|
||||||
|
stopParentClick(event);
|
||||||
setDismissed(true).catch(console.error);
|
setDismissed(true).catch(console.error);
|
||||||
Promise.resolve(onDismiss?.()).catch(console.error);
|
Promise.resolve(onDismiss?.()).catch(console.error);
|
||||||
}}
|
}}
|
||||||
@@ -69,8 +93,11 @@ export function DismissibleBanner({
|
|||||||
key={a.label}
|
key={a.label}
|
||||||
variant={a.variant ?? "border"}
|
variant={a.variant ?? "border"}
|
||||||
color={a.color ?? props.color}
|
color={a.color ?? props.color}
|
||||||
size="xs"
|
size={actionSize}
|
||||||
onClick={a.onClick}
|
onClick={(event) => {
|
||||||
|
stopParentClick(event);
|
||||||
|
a.onClick();
|
||||||
|
}}
|
||||||
title={a.label}
|
title={a.label}
|
||||||
>
|
>
|
||||||
{a.label}
|
{a.label}
|
||||||
|
|||||||
@@ -712,7 +712,7 @@ const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items"
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"x-theme-menu",
|
"x-theme-menu",
|
||||||
"outline-none my-1 pointer-events-auto z-40",
|
"outline-hidden my-1 pointer-events-auto z-40",
|
||||||
"fixed",
|
"fixed",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -734,7 +734,7 @@ const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items"
|
|||||||
{filter && (
|
{filter && (
|
||||||
<HStack
|
<HStack
|
||||||
space={2}
|
space={2}
|
||||||
className="pb-0.5 px-1.5 mb-2 text-sm border border-border-subtle mx-2 rounded font-mono h-xs"
|
className="pb-0.5 px-1.5 mb-2 text-sm border border-border-subtle mx-2 rounded-sm font-mono h-xs"
|
||||||
>
|
>
|
||||||
<Icon icon="search" size="xs" />
|
<Icon icon="search" size="xs" />
|
||||||
<div className="text">{filter}</div>
|
<div className="text">{filter}</div>
|
||||||
@@ -916,24 +916,24 @@ function MenuItem({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
|
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
|
||||||
innerClassName="!text-left"
|
innerClassName="text-left!"
|
||||||
color="custom"
|
color="custom"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"h-xs", // More compact
|
"h-xs", // More compact
|
||||||
"min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap",
|
"min-w-32 outline-hidden px-2 mx-1.5 flex whitespace-nowrap",
|
||||||
"focus:bg-surface-highlight focus:text rounded focus:outline-none focus-visible:outline-1",
|
"focus:bg-surface-highlight focus:text rounded-sm focus:outline-hidden focus-visible:outline-1",
|
||||||
isParentOfActiveSubmenu && "bg-surface-highlight text rounded",
|
isParentOfActiveSubmenu && "bg-surface-highlight text rounded-sm",
|
||||||
item.color === "danger" && "!text-danger",
|
item.color === "danger" && "text-danger!",
|
||||||
item.color === "primary" && "!text-primary",
|
item.color === "primary" && "text-primary!",
|
||||||
item.color === "success" && "!text-success",
|
item.color === "success" && "text-success!",
|
||||||
item.color === "warning" && "!text-warning",
|
item.color === "warning" && "text-warning!",
|
||||||
item.color === "notice" && "!text-notice",
|
item.color === "notice" && "text-notice!",
|
||||||
item.color === "info" && "!text-info",
|
item.color === "info" && "text-info!",
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={classNames("truncate min-w-[5rem]")}>{item.label}</div>
|
<div className={classNames("truncate min-w-20")}>{item.label}</div>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@reference "../../../main.css";
|
||||||
|
|
||||||
.cm-wrapper.cm-multiline .cm-mergeView {
|
.cm-wrapper.cm-multiline .cm-mergeView {
|
||||||
@apply h-full w-full overflow-auto pr-0.5;
|
@apply h-full w-full overflow-auto pr-0.5;
|
||||||
|
|
||||||
@@ -9,7 +11,7 @@
|
|||||||
@apply w-full min-h-full relative;
|
@apply w-full min-h-full relative;
|
||||||
|
|
||||||
.cm-collapsedLines {
|
.cm-collapsedLines {
|
||||||
@apply bg-none bg-surface border border-border py-1 mx-0.5 text-text opacity-80 hover:opacity-100 rounded cursor-default;
|
@apply bg-none bg-surface border border-border py-1 mx-0.5 text-text opacity-80 hover:opacity-100 rounded-sm cursor-default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,21 +21,21 @@
|
|||||||
.cm-changedLine {
|
.cm-changedLine {
|
||||||
/* Round top corners only if previous line is not a changed line */
|
/* Round top corners only if previous line is not a changed line */
|
||||||
&:not(.cm-changedLine + &) {
|
&:not(.cm-changedLine + &) {
|
||||||
@apply rounded-t;
|
@apply rounded-t-sm;
|
||||||
}
|
}
|
||||||
/* Round bottom corners only if next line is not a changed line */
|
/* Round bottom corners only if next line is not a changed line */
|
||||||
&:not(:has(+ .cm-changedLine)) {
|
&:not(:has(+ .cm-changedLine)) {
|
||||||
@apply rounded-b;
|
@apply rounded-b-sm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Let content grow and disable individual scrolling for sync */
|
/* Let content grow and disable individual scrolling for sync */
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
@apply h-auto relative !important;
|
@apply h-auto! relative!;
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-scroller {
|
.cm-scroller {
|
||||||
@apply overflow-visible !important;
|
@apply overflow-visible!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@reference "../../../main.css";
|
||||||
|
|
||||||
.cm-wrapper {
|
.cm-wrapper {
|
||||||
@apply h-full overflow-hidden;
|
@apply h-full overflow-hidden;
|
||||||
|
|
||||||
@@ -7,7 +9,7 @@
|
|||||||
/* Regular cursor */
|
/* Regular cursor */
|
||||||
|
|
||||||
.cm-cursor {
|
.cm-cursor {
|
||||||
@apply border-text !important;
|
@apply border-text!;
|
||||||
/* Widen the cursor a bit */
|
/* Widen the cursor a bit */
|
||||||
@apply border-l-[2px];
|
@apply border-l-[2px];
|
||||||
}
|
}
|
||||||
@@ -15,8 +17,8 @@
|
|||||||
/* Vim-mode cursor */
|
/* Vim-mode cursor */
|
||||||
|
|
||||||
.cm-fat-cursor {
|
.cm-fat-cursor {
|
||||||
@apply outline-0 bg-text !important;
|
@apply outline-0! bg-text!;
|
||||||
@apply text-surface !important;
|
@apply text-surface!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Matching bracket */
|
/* Matching bracket */
|
||||||
@@ -59,12 +61,12 @@
|
|||||||
|
|
||||||
* {
|
* {
|
||||||
@apply cursor-text;
|
@apply cursor-text;
|
||||||
@apply caret-transparent !important;
|
@apply caret-transparent!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-selectionBackground {
|
.cm-selectionBackground {
|
||||||
@apply bg-selection !important;
|
@apply bg-selection!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix WebKit/WKWebView rendering bug where selection layer leaves a ghost
|
/* Fix WebKit/WKWebView rendering bug where selection layer leaves a ghost
|
||||||
@@ -88,7 +90,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-gutter-lint {
|
.cm-gutter-lint {
|
||||||
@apply w-auto !important;
|
@apply w-auto!;
|
||||||
|
|
||||||
.cm-gutterElement {
|
.cm-gutterElement {
|
||||||
@apply px-0;
|
@apply px-0;
|
||||||
@@ -111,7 +113,7 @@
|
|||||||
@apply bg-surface text-text-subtle border-border-subtle whitespace-nowrap cursor-default;
|
@apply bg-surface text-text-subtle border-border-subtle whitespace-nowrap cursor-default;
|
||||||
@apply hover:border-border hover:text-text hover:bg-surface-highlight;
|
@apply hover:border-border hover:text-text hover:bg-surface-highlight;
|
||||||
|
|
||||||
@apply inline border px-1 mx-[0.5px] rounded dark:shadow;
|
@apply inline border px-1 mx-[0.5px] rounded-sm dark:shadow;
|
||||||
|
|
||||||
-webkit-text-security: none;
|
-webkit-text-security: none;
|
||||||
|
|
||||||
@@ -162,7 +164,7 @@
|
|||||||
|
|
||||||
&::-webkit-scrollbar-corner,
|
&::-webkit-scrollbar-corner,
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
@apply hidden !important;
|
@apply hidden!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,16 +191,16 @@
|
|||||||
|
|
||||||
/* Style search matches */
|
/* Style search matches */
|
||||||
.cm-searchMatch {
|
.cm-searchMatch {
|
||||||
@apply bg-transparent !important;
|
@apply bg-transparent!;
|
||||||
@apply rounded-[2px] outline outline-1;
|
@apply rounded-[2px] outline outline-1;
|
||||||
|
|
||||||
&.cm-searchMatch-selected {
|
&.cm-searchMatch-selected {
|
||||||
@apply outline-text;
|
@apply outline-text;
|
||||||
@apply bg-text !important;
|
@apply bg-text!;
|
||||||
|
|
||||||
&,
|
&,
|
||||||
* {
|
* {
|
||||||
@apply text-surface font-semibold !important;
|
@apply text-surface! font-semibold!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,8 +225,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor .fold-gutter-icon {
|
.cm-editor .fold-gutter-icon {
|
||||||
@apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 rounded;
|
@apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 rounded-sm;
|
||||||
@apply cursor-default !important;
|
@apply cursor-default!;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor .fold-gutter-icon::after {
|
.cm-editor .fold-gutter-icon::after {
|
||||||
@@ -248,7 +250,7 @@
|
|||||||
.cm-editor .cm-foldPlaceholder {
|
.cm-editor .cm-foldPlaceholder {
|
||||||
@apply px-2 border border-border-subtle bg-surface-highlight;
|
@apply px-2 border border-border-subtle bg-surface-highlight;
|
||||||
@apply hover:text-text hover:border-border-subtle text-text;
|
@apply hover:text-text hover:border-border-subtle text-text;
|
||||||
@apply cursor-default !important;
|
@apply cursor-default!;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor .cm-activeLineGutter {
|
.cm-editor .cm-activeLineGutter {
|
||||||
@@ -277,7 +279,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-tooltip-lint {
|
.cm-tooltip-lint {
|
||||||
@apply font-mono text-editor rounded overflow-hidden bg-surface-highlight border border-border shadow !important;
|
@apply font-mono! text-editor! rounded-sm! overflow-hidden! bg-surface-highlight! border! border-border! shadow!;
|
||||||
|
|
||||||
.cm-diagnostic-error {
|
.cm-diagnostic-error {
|
||||||
@apply border-l-danger px-4 py-2;
|
@apply border-l-danger px-4 py-2;
|
||||||
@@ -293,18 +295,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-tooltip.cm-tooltip-hover {
|
.cm-tooltip.cm-tooltip-hover {
|
||||||
@apply shadow-lg bg-surface rounded text-text-subtle border border-border-subtle z-50 pointer-events-auto text-sm;
|
@apply shadow-lg bg-surface rounded-sm text-text-subtle border border-border-subtle z-50 pointer-events-auto text-sm;
|
||||||
@apply p-1.5;
|
@apply p-1.5;
|
||||||
|
|
||||||
/* Style the tooltip for popping up "open in browser" and other stuff */
|
/* Style the tooltip for popping up "open in browser" and other stuff */
|
||||||
|
|
||||||
a,
|
a,
|
||||||
button {
|
button {
|
||||||
@apply text-text hover:bg-surface-highlight w-full h-sm flex items-center px-2 rounded;
|
@apply text-text hover:bg-surface-highlight w-full h-sm flex items-center px-2 rounded-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@apply cursor-default !important;
|
@apply cursor-default!;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
@apply text-text bg-text h-3 w-3 ml-1;
|
@apply text-text bg-text h-3 w-3 ml-1;
|
||||||
@@ -319,10 +321,10 @@
|
|||||||
/* NOTE: Extra selector required to override default styles */
|
/* NOTE: Extra selector required to override default styles */
|
||||||
.cm-tooltip.cm-tooltip-autocomplete,
|
.cm-tooltip.cm-tooltip-autocomplete,
|
||||||
.cm-tooltip.cm-completionInfo {
|
.cm-tooltip.cm-completionInfo {
|
||||||
@apply shadow-lg bg-surface rounded text-text-subtle border border-border-subtle z-50 pointer-events-auto;
|
@apply shadow-lg bg-surface rounded-sm text-text-subtle border border-border-subtle z-50 pointer-events-auto;
|
||||||
|
|
||||||
& * {
|
& * {
|
||||||
@apply font-mono text-editor !important;
|
@apply font-mono! text-editor!;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-completionIcon {
|
.cm-completionIcon {
|
||||||
@@ -409,7 +411,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-completionIcon {
|
.cm-completionIcon {
|
||||||
@apply text-sm flex items-center pb-0.5 flex-shrink-0;
|
@apply text-sm flex items-center pb-0.5 shrink-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-completionLabel {
|
.cm-completionLabel {
|
||||||
@@ -427,7 +429,7 @@
|
|||||||
|
|
||||||
input,
|
input,
|
||||||
button {
|
button {
|
||||||
@apply rounded-sm outline-none;
|
@apply rounded-sm outline-hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -436,12 +438,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button[name="close"] {
|
button[name="close"] {
|
||||||
@apply text-text-subtle hocus:text-text px-2 -mr-1.5 !important;
|
@apply text-text-subtle! hocus:text-text! px-2! -mr-1.5!;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
@apply bg-surface border-border-subtle focus:border-border-focus;
|
@apply bg-surface border-border-subtle focus:border-border-focus;
|
||||||
@apply border outline-none;
|
@apply border outline-hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.cm-textfield {
|
input.cm-textfield {
|
||||||
|
|||||||
@@ -282,6 +282,22 @@ function EditorInner({
|
|||||||
[disableTabIndent],
|
[disableTabIndent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update read-only
|
||||||
|
const readOnlyCompartment = useRef(new Compartment());
|
||||||
|
useEffect(
|
||||||
|
function configureReadOnly() {
|
||||||
|
if (cm.current === null) return;
|
||||||
|
const current = readOnlyCompartment.current.get(cm.current.view.state) ?? emptyExtension;
|
||||||
|
const next = readOnly ? readonlyExtensions : emptyExtension;
|
||||||
|
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
|
||||||
|
if (current === next) return;
|
||||||
|
|
||||||
|
const effects = readOnlyCompartment.current.reconfigure(next);
|
||||||
|
cm.current?.view.dispatch({ effects });
|
||||||
|
},
|
||||||
|
[readOnly],
|
||||||
|
);
|
||||||
|
|
||||||
const onClickFunction = useCallback(
|
const onClickFunction = useCallback(
|
||||||
async (fn: TemplateFunction, tagValue: string, startPos: number) => {
|
async (fn: TemplateFunction, tagValue: string, startPos: number) => {
|
||||||
const show = () => {
|
const show = () => {
|
||||||
@@ -394,9 +410,9 @@ function EditorInner({
|
|||||||
keymapCompartment.current.of(
|
keymapCompartment.current.of(
|
||||||
keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default,
|
keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default,
|
||||||
),
|
),
|
||||||
|
readOnlyCompartment.current.of(readOnly ? readonlyExtensions : emptyExtension),
|
||||||
...getExtensions({
|
...getExtensions({
|
||||||
container,
|
container,
|
||||||
readOnly,
|
|
||||||
singleLine,
|
singleLine,
|
||||||
hideGutter,
|
hideGutter,
|
||||||
stateKey,
|
stateKey,
|
||||||
@@ -470,7 +486,7 @@ function EditorInner({
|
|||||||
const decoratedActions = useMemo(() => {
|
const decoratedActions = useMemo(() => {
|
||||||
const results = [];
|
const results = [];
|
||||||
const actionClassName = classNames(
|
const actionClassName = classNames(
|
||||||
"bg-surface transition-opacity transform-gpu opacity-0 group-hover:opacity-100 hover:!opacity-100 shadow",
|
"bg-surface transition-opacity transform-gpu opacity-0 group-hover:opacity-100 hover:opacity-100! shadow",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (format) {
|
if (format) {
|
||||||
@@ -553,7 +569,6 @@ function EditorInner({
|
|||||||
function getExtensions({
|
function getExtensions({
|
||||||
stateKey,
|
stateKey,
|
||||||
container,
|
container,
|
||||||
readOnly,
|
|
||||||
singleLine,
|
singleLine,
|
||||||
hideGutter,
|
hideGutter,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -562,7 +577,7 @@ function getExtensions({
|
|||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}: Pick<EditorProps, "singleLine" | "readOnly" | "hideGutter"> & {
|
}: Pick<EditorProps, "singleLine" | "hideGutter"> & {
|
||||||
stateKey: EditorProps["stateKey"];
|
stateKey: EditorProps["stateKey"];
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
onChange: RefObject<EditorProps["onChange"]>;
|
onChange: RefObject<EditorProps["onChange"]>;
|
||||||
@@ -612,7 +627,6 @@ function getExtensions({
|
|||||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== "Enter") : defaultKeymap),
|
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== "Enter") : defaultKeymap),
|
||||||
...(singleLine ? [singleLineExtensions()] : []),
|
...(singleLine ? [singleLineExtensions()] : []),
|
||||||
...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
|
...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
|
||||||
...(readOnly ? readonlyExtensions : []),
|
|
||||||
|
|
||||||
// ------------------------ //
|
// ------------------------ //
|
||||||
// Things that must be last //
|
// Things that must be last //
|
||||||
|
|||||||
@@ -53,19 +53,17 @@ function pathParameters(
|
|||||||
if (node.name === "Text") {
|
if (node.name === "Text") {
|
||||||
// Find the `url` node and then jump into it to find the placeholders
|
// Find the `url` node and then jump into it to find the placeholders
|
||||||
for (let i = node.from; i < node.to; i++) {
|
for (let i = node.from; i < node.to; i++) {
|
||||||
const innerTree = syntaxTree(view.state).resolveInner(i);
|
const innerTree = tree.resolveInner(i);
|
||||||
if (innerTree.node.name === "url") {
|
if (innerTree.node.name === "url") {
|
||||||
innerTree.toTree().iterate({
|
innerTree.node.cursor().iterate((node) => {
|
||||||
enter(node) {
|
if (node.name !== "Placeholder") return;
|
||||||
if (node.name !== "Placeholder") return;
|
const globalFrom = node.from;
|
||||||
const globalFrom = innerTree.node.from + node.from;
|
const globalTo = node.to;
|
||||||
const globalTo = innerTree.node.from + node.to;
|
const rawText = view.state.doc.sliceString(globalFrom, globalTo);
|
||||||
const rawText = view.state.doc.sliceString(globalFrom, globalTo);
|
const onClick = () => onClickPathParameter(rawText);
|
||||||
const onClick = () => onClickPathParameter(rawText);
|
const widget = new PathPlaceholderWidget(rawText, globalFrom, onClick);
|
||||||
const widget = new PathPlaceholderWidget(rawText, globalFrom, onClick);
|
const deco = Decoration.replace({ widget, inclusive: false });
|
||||||
const deco = Decoration.replace({ widget, inclusive: false });
|
widgets.push(deco.range(globalFrom, globalTo));
|
||||||
widgets.push(deco.range(globalFrom, globalTo));
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
@top url { Protocol? Host Path? Query? }
|
// Host is optional so URLs starting with `/` go straight to Path. Without this,
|
||||||
|
// the parser error-recovers past the leading `/` and consumes the first segment as
|
||||||
|
// Host (since Host's char class includes `:` for `host:port`), eating an initial
|
||||||
|
// `:name` placeholder like `/:foo/:bar`.
|
||||||
|
@top url { Protocol? Host? Path? Query? }
|
||||||
|
|
||||||
Path { ("/" (Placeholder | PathSegment))+ }
|
Path { ("/" PathSegment)+ }
|
||||||
|
|
||||||
|
Placeholder { ":" pathChars }
|
||||||
|
PathSegment { Placeholder (":" pathChars)* | pathChars (":" pathChars)* }
|
||||||
|
|
||||||
Query { "?" queryPair ("&" queryPair)* }
|
Query { "?" queryPair ("&" queryPair)* }
|
||||||
|
|
||||||
@@ -9,9 +16,7 @@ Query { "?" queryPair ("&" queryPair)* }
|
|||||||
Host { $[a-zA-Z0-9-_.:\[\]]+ }
|
Host { $[a-zA-Z0-9-_.:\[\]]+ }
|
||||||
@precedence { Protocol, Host }
|
@precedence { Protocol, Host }
|
||||||
|
|
||||||
Placeholder { ":" ![/?#]+ }
|
pathChars { ![/?#:]+ }
|
||||||
PathSegment { ![?#/]+ }
|
|
||||||
@precedence { Placeholder, PathSegment }
|
|
||||||
|
|
||||||
queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) }
|
queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
export const url = 1,
|
export const
|
||||||
|
url = 1,
|
||||||
Protocol = 2,
|
Protocol = 2,
|
||||||
Host = 3,
|
Host = 3,
|
||||||
Port = 4,
|
Path = 4,
|
||||||
Path = 5,
|
PathSegment = 5,
|
||||||
Placeholder = 6,
|
Placeholder = 6,
|
||||||
PathSegment = 7,
|
Query = 7
|
||||||
Query = 8;
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, test } from "vite-plus/test";
|
||||||
|
import { parser } from "./url";
|
||||||
|
|
||||||
|
function expectValidParse(input: string) {
|
||||||
|
expect(parser.parse(input).toString()).not.toContain("⚠");
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeholderValues(input: string): string[] {
|
||||||
|
const values: string[] = [];
|
||||||
|
parser
|
||||||
|
.parse(input)
|
||||||
|
.cursor()
|
||||||
|
.iterate((node) => {
|
||||||
|
if (node.name === "Placeholder") values.push(input.slice(node.from, node.to));
|
||||||
|
});
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("URL grammar Placeholder", () => {
|
||||||
|
test("recognizes path placeholders", () => {
|
||||||
|
expectValidParse("https://x.com/users/:id");
|
||||||
|
expect(placeholderValues("https://x.com/users/:id")).toEqual([":id"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("treats a colon suffix as literal path text", () => {
|
||||||
|
expectValidParse("https://yaak.app/x/echo/:foo:bar/baz");
|
||||||
|
expect(placeholderValues("https://yaak.app/x/echo/:foo:bar/baz")).toEqual([":foo"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("treats repeated colon suffixes as literal path text", () => {
|
||||||
|
expectValidParse("https://yaak.app/x/echo/:foo:bar:baz");
|
||||||
|
expect(placeholderValues("https://yaak.app/x/echo/:foo:bar:baz")).toEqual([":foo"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not recognize a colon in the middle of a plain path segment", () => {
|
||||||
|
expectValidParse("https://yaak.app/x/echo/foo:bar/baz");
|
||||||
|
expect(placeholderValues("https://yaak.app/x/echo/foo:bar/baz")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not recognize query parameters as path placeholders", () => {
|
||||||
|
expect(placeholderValues("https://yaak.app/x/echo/:foo?bar=ss&:bar=baz")).toEqual([":foo"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("recognizes placeholders in a path fragment after a templated base URL", () => {
|
||||||
|
// Mixed Twig parsing can feed the URL parser only the text after a template tag,
|
||||||
|
// as in `${[ URL ]}/x/:foo/:hello`.
|
||||||
|
expect(placeholderValues("/x/hi:echo/:foo/:hello?bar=ss&:bar=baz")).toEqual([
|
||||||
|
":foo",
|
||||||
|
":hello",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
import { LRParser } from "@lezer/lr";
|
import {LRParser} from "@lezer/lr"
|
||||||
import { highlight } from "./highlight";
|
import {highlight} from "./highlight"
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states:
|
states: "#xQQOPOOO`OQO'#CdOhOPO'#C`OsOSO'#CcQOOOOOQZOPOOQWOPOOQTOPOOOxOQO'#CbO}OQO'#CaOOOO,59O,59OOOOO-E6b-E6bO!]OPO,58}OOOO,58|,58|O!eOQO'#CeO!jOQO,58{O!xOSO'#CfO!}OPO1G.iOOOO,59P,59POOOO-E6c-E6cOOOO,59Q,59QOOOO-E6d-E6d",
|
||||||
"!|OQOPOOQYOPOOOTOPOOObOQO'#CdOjOPO'#C`OuOSO'#CcQOOOOOQ]OPOOOOOO,59O,59OOOOO-E6b-E6bOzOPO,58}O!SOSO'#CeO!XOPO1G.iOOOO,59P,59POOOO-E6c-E6c",
|
stateData: "#Y~OQVORUO[PO_RO~O]WO^XO~O[POZSX_SX~O`[O~O^]O~O]^OZTX[TX_TX~Oa`OZVa~O^bO~O]^OZTa[Ta_Ta~O`dO~Oa`OZVi~OQR~",
|
||||||
stateData: "!g~OQQORPO~OZRO[TO~OTWOUWO~OZROYSX[SX~O]YO~O^ZOYVa~O]]O~O^ZOYVi~OQRTUT~",
|
goto: "!RZPPPP[adgmu{VTOUVRYPRXPXSOTUVUQOUVRZQQ_XRc_Qa[Rea",
|
||||||
goto: "nYPPPPZPP^bhRVPTUPVQSPRXSQ[YR^[",
|
nodeNames: "⚠ url Protocol Host Path PathSegment Placeholder Query",
|
||||||
nodeNames: "⚠ url Protocol Host Path Placeholder PathSegment Query",
|
maxTerm: 17,
|
||||||
maxTerm: 14,
|
|
||||||
propSources: [highlight],
|
propSources: [highlight],
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 2,
|
repeatNodeCount: 3,
|
||||||
tokenData:
|
tokenData: "+z~RgOs!jtv!jvw#[w}!j}!O#x!O!P#x!P!Q%|!QZ!b!c!j!c!})`!}#O#x#O#P!j#P#Q#x#Q#R!j#R#S#x#S#T!j#T#o)`#o;'S!j;'S;=`#U<%lO!jQ!oV^QOs!jt!P!j!Q![!j!]!a!j!b;'S!j;'S;=`#U<%lO!jQ#XP;=`<%l!jR#cVaP^QOs!jt!P!j!Q![!j!]!a!j!b;'S!j;'S;=`#U<%lO!jR$Pc^QRPOs!jt}!j}!O#x!O!P#x!Q![#x![!]%[!]!a!j!b!c!j!c!}#x!}#O#x#O#P!j#P#Q#x#Q#R!j#R#S#x#S#T!j#T#o#x#o;'S!j;'S;=`#U<%lO!jP%aXRP}!O%[!O!P%[!Q![%[![!]%[!c!}%[!}#O%[#P#Q%[#R#S%[#T#o%[~&RO[~V&[e^Q`SRPOs!jt}!j}!O#x!O!P#x!Q![&R![!]%[!]!_!j!_!`'m!`!a!j!b!c!j!c!}&R!}#O#x#O#P!j#P#Q#x#Q#R!j#R#S#x#S#T!j#T#o&R#o;'S!j;'S;=`#U<%lO!jU'tZ^Q`SOs!jt!P!j!Q!['m!]!a!j!b!c!j!c!}'m!}#T!j#T#o'm#o;'S!j;'S;=`#U<%lO!jR(nX]QRP}!O%[!O!P%[!Q![%[![!]%[!c!}%[!}#O%[#P#Q%[#R#S%[#T#o%[~)`O_~V)ie^Q`SRPOs!jt}!j}!O#x!O!P#x!Q![&R![!]*z!]!_!j!_!`'m!`!a!j!b!c!j!c!})`!}#O#x#O#P!j#P#Q#x#Q#R!j#R#S#x#S#T!j#T#o)`#o;'S!j;'S;=`#U<%lO!jP+PYRP}!O%[!O!P%[!P!Q+o!Q![%[![!]%[!c!}%[!}#O%[#P#Q%[#R#S%[#T#o%[P+rP!P!Q+uP+zOQP",
|
||||||
".i~RgOs!jtv!jvw#Xw}!j}!O#r!O!P#r!P!Q%U!Q![%Z![!]'o!]!a!j!a!b+W!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jQ!oUUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jQ#UP;=`<%l!jR#`U^PUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jR#ycRPUQOs!jt}!j}!O#r!O!P#r!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!j~%ZOZ~V%de]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!]#r!]!_!j!_!`&u!`!a!j!b!c!j!c!}%Z!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o%Z#o;'S!j;'S;=`#R<%lO!jU&|Z]SUQOs!jt!P!j!Q![&u![!a!j!b!c!j!c!}&u!}#T!j#T#o&u#o;'S!j;'S;=`#R<%lO!jR'vcRPUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)RQ)YUTQUQOs)Rt!P)R!Q!a)R!b;'S)R;'S;=`)l<%lO)RQ)oP;=`<%l)RR){cRPTQUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)R~+]O[~V+fe]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!],w!]!_!j!_!`&u!`!a!j!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jR-OdRPUQOs!jt}!j}!O#r!O!P#r!P!Q.^!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!jP.aP!P!Q.dP.iOQP",
|
|
||||||
tokenizers: [0, 1, 2],
|
tokenizers: [0, 1, 2],
|
||||||
topRules: { url: [0, 1] },
|
topRules: {"url":[0,1]},
|
||||||
tokenPrec: 63,
|
tokenPrec: 99
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { CopyIconButton } from "../CopyIconButton";
|
|||||||
import { AutoScroller } from "./AutoScroller";
|
import { AutoScroller } from "./AutoScroller";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import { IconButton } from "./IconButton";
|
import { IconButton } from "./IconButton";
|
||||||
|
import type { SelectProps } from "./Select";
|
||||||
|
import { Select } from "./Select";
|
||||||
import { Separator } from "./Separator";
|
import { Separator } from "./Separator";
|
||||||
|
|
||||||
interface EventViewerProps<T> {
|
interface EventViewerProps<T> {
|
||||||
@@ -151,7 +153,7 @@ export function EventViewer<T>({
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
storageKey={splitLayoutStorageKey}
|
storageKey={splitLayoutStorageKey}
|
||||||
defaultRatio={defaultRatio}
|
defaultRatio={defaultRatio}
|
||||||
minHeightPx={10}
|
minHeightPx={72}
|
||||||
firstSlot={({ style }) => (
|
firstSlot={({ style }) => (
|
||||||
<div style={style} className="w-full h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
<div style={style} className="w-full h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
{header ?? <span aria-hidden />}
|
{header ?? <span aria-hidden />}
|
||||||
@@ -202,23 +204,38 @@ export function EventViewer<T>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventDetailAction {
|
export type EventDetailAction =
|
||||||
/** Unique key for React */
|
| {
|
||||||
key: string;
|
type?: "button";
|
||||||
/** Button label */
|
/** Unique key for React */
|
||||||
label: string;
|
key: string;
|
||||||
/** Optional icon */
|
/** Button label */
|
||||||
icon?: ReactNode;
|
label: string;
|
||||||
/** Click handler */
|
/** Optional icon */
|
||||||
onClick: () => void;
|
icon?: ReactNode;
|
||||||
}
|
/** Click handler */
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "select";
|
||||||
|
/** Unique key for React */
|
||||||
|
key: string;
|
||||||
|
/** Select label */
|
||||||
|
label: string;
|
||||||
|
/** Selected value */
|
||||||
|
value: string;
|
||||||
|
/** Select options */
|
||||||
|
options: SelectProps<string>["options"];
|
||||||
|
/** Change handler */
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
interface EventDetailHeaderProps {
|
interface EventDetailHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
prefix?: ReactNode;
|
prefix?: ReactNode;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
actions?: EventDetailAction[];
|
actions?: EventDetailAction[];
|
||||||
copyText?: string;
|
copyText?: string | (() => Promise<string | null>);
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,40 +256,56 @@ export function EventDetailHeader({
|
|||||||
<h3 className="font-semibold select-auto cursor-auto truncate">{title}</h3>
|
<h3 className="font-semibold select-auto cursor-auto truncate">{title}</h3>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack space={2} className="items-center">
|
<HStack space={2} className="items-center">
|
||||||
{actions?.map((action) => (
|
{actions?.map((action) =>
|
||||||
<Button
|
action.type === "select" ? (
|
||||||
key={action.key}
|
<div key={action.key} className="w-32">
|
||||||
type="button"
|
<Select
|
||||||
variant="border"
|
name={action.key}
|
||||||
size="xs"
|
label={action.label}
|
||||||
onClick={action.onClick}
|
hideLabel
|
||||||
>
|
size="xs"
|
||||||
{action.icon}
|
value={action.value}
|
||||||
{action.label}
|
options={action.options}
|
||||||
</Button>
|
onChange={action.onChange}
|
||||||
))}
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
key={action.key}
|
||||||
|
type="button"
|
||||||
|
variant="border"
|
||||||
|
size="xs"
|
||||||
|
onClick={action.onClick}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
)}
|
||||||
{copyText != null && (
|
{copyText != null && (
|
||||||
<CopyIconButton text={copyText} size="xs" title="Copy" variant="border" iconSize="sm" />
|
<CopyIconButton text={copyText} size="xs" title="Copy" variant="border" iconSize="sm" />
|
||||||
)}
|
)}
|
||||||
{formattedTime && (
|
{formattedTime && (
|
||||||
<span className="text-text-subtlest font-mono text-editor ml-2">{formattedTime}</span>
|
<span className="text-text-subtlest font-mono text-editor ml-2">{formattedTime}</span>
|
||||||
)}
|
)}
|
||||||
<div
|
{onClose != null && (
|
||||||
className={classNames(
|
<div
|
||||||
copyText != null ||
|
className={classNames(
|
||||||
formattedTime ||
|
copyText != null ||
|
||||||
((actions ?? []).length > 0 && "border-l border-l-surface-highlight ml-2 pl-3"),
|
formattedTime ||
|
||||||
)}
|
((actions ?? []).length > 0 && "border-l border-l-surface-highlight ml-2 pl-3"),
|
||||||
>
|
)}
|
||||||
<IconButton
|
>
|
||||||
color="custom"
|
<IconButton
|
||||||
className="text-text-subtle -mr-3"
|
color="custom"
|
||||||
size="xs"
|
className="text-text-subtle -mr-3"
|
||||||
icon="x"
|
size="xs"
|
||||||
title="Close event panel"
|
icon="x"
|
||||||
onClick={onClose}
|
title="Close event panel"
|
||||||
/>
|
onClick={onClose}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export function EventViewerRow({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full grid grid-cols-[auto_minmax(0,1fr)_auto] gap-2 items-center text-left",
|
"w-full grid grid-cols-[auto_minmax(0,1fr)_auto] gap-2 items-center text-left",
|
||||||
"px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-none focus:text-text rounded",
|
"px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-hidden focus:text-text rounded-sm",
|
||||||
isActive && "bg-surface-active !text-text",
|
isActive && "bg-surface-active text-text!",
|
||||||
"text-text-subtle hover:text",
|
"text-text-subtle hover:text",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function HotkeyRaw({ labelParts, className, variant }: HotkeyRawProps) {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
variant === "with-bg" &&
|
variant === "with-bg" &&
|
||||||
"rounded bg-surface-highlight px-1 border border-border text-text-subtle",
|
"rounded-sm bg-surface-highlight px-1 border border-border text-text-subtle",
|
||||||
variant === "text" && "text-text-subtlest",
|
variant === "text" && "text-text-subtlest",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function HttpMethodTagRaw({
|
|||||||
colored && m === "PATCH" && "text-notice",
|
colored && m === "PATCH" && "text-notice",
|
||||||
colored && m === "POST" && "text-success",
|
colored && m === "POST" && "text-success",
|
||||||
colored && m === "DELETE" && "text-danger",
|
colored && m === "DELETE" && "text-danger",
|
||||||
"font-mono flex-shrink-0 whitespace-pre",
|
"font-mono shrink-0 whitespace-pre",
|
||||||
"pt-[0.15em]", // Fix for monospace font not vertically centering
|
"pt-[0.15em]", // Fix for monospace font not vertically centering
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function HttpResponseDurationTag({ response }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMillis(ms: number) {
|
export function formatMillis(ms: number) {
|
||||||
if (ms < 1000) {
|
if (ms < 1000) {
|
||||||
return `${ms} ms`;
|
return `${ms} ms`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ function BaseInput({
|
|||||||
const id = useRef(`input-${generateId()}`);
|
const id = useRef(`input-${generateId()}`);
|
||||||
const editorClassName = classNames(
|
const editorClassName = classNames(
|
||||||
className,
|
className,
|
||||||
"!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder",
|
"bg-transparent! min-w-0 h-auto w-full focus:outline-hidden placeholder:text-placeholder",
|
||||||
);
|
);
|
||||||
|
|
||||||
const isValid = useMemo(() => {
|
const isValid = useMemo(() => {
|
||||||
@@ -264,7 +264,7 @@ function BaseInput({
|
|||||||
"border",
|
"border",
|
||||||
focused && !disabled ? "border-border-focus" : "border-border",
|
focused && !disabled ? "border-border-focus" : "border-border",
|
||||||
disabled && "border-dotted",
|
disabled && "border-dotted",
|
||||||
!isValid && hasChanged && "!border-danger",
|
!isValid && hasChanged && "border-danger!",
|
||||||
size === "md" && "min-h-md",
|
size === "md" && "min-h-md",
|
||||||
size === "sm" && "min-h-sm",
|
size === "sm" && "min-h-sm",
|
||||||
size === "xs" && "min-h-xs",
|
size === "xs" && "min-h-xs",
|
||||||
@@ -333,7 +333,7 @@ function BaseInput({
|
|||||||
: `Obscure ${typeof label === "string" ? label : "field"}`
|
: `Obscure ${typeof label === "string" ? label : "field"}`
|
||||||
}
|
}
|
||||||
size="xs"
|
size="xs"
|
||||||
className={classNames("mr-0.5 !h-auto my-0.5", disabled && "opacity-disabled")}
|
className={classNames("mr-0.5 h-auto! my-0.5", disabled && "opacity-disabled")}
|
||||||
color={tint}
|
color={tint}
|
||||||
// iconClassName={classNames(
|
// iconClassName={classNames(
|
||||||
// tint === 'primary' && 'text-primary',
|
// tint === 'primary' && 'text-primary',
|
||||||
@@ -548,9 +548,9 @@ function EncryptionInput({
|
|||||||
color={tint}
|
color={tint}
|
||||||
aria-label="Configure encryption"
|
aria-label="Configure encryption"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center justify-center !h-full !px-1",
|
"flex items-center justify-center h-full! px-1!",
|
||||||
"opacity-70", // Makes it a bit subtler
|
"opacity-70", // Makes it a bit subtler
|
||||||
props.disabled && "!opacity-disabled",
|
props.disabled && "opacity-disabled!",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<HStack space={0.5}>
|
<HStack space={0.5}>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function KeyValueRow({
|
|||||||
<>
|
<>
|
||||||
<td
|
<td
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"select-none py-0.5 pr-2 h-full max-w-[10rem]",
|
"select-none py-0.5 pr-2 h-full max-w-40",
|
||||||
align === "top" && "align-top",
|
align === "top" && "align-top",
|
||||||
align === "middle" && "align-middle",
|
align === "middle" && "align-middle",
|
||||||
labelClassName,
|
labelClassName,
|
||||||
@@ -86,12 +86,12 @@ export function KeyValueRow({
|
|||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"select-none py-0.5 break-all max-w-[15rem]",
|
"select-none py-0.5 break-all max-w-60",
|
||||||
align === "top" && "align-top",
|
align === "top" && "align-top",
|
||||||
align === "middle" && "align-middle",
|
align === "middle" && "align-middle",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="select-text cursor-text max-h-[12rem] overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]">
|
<div className="select-text cursor-text max-h-48 overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]">
|
||||||
{leftSlot ?? <span aria-hidden />}
|
{leftSlot ?? <span aria-hidden />}
|
||||||
{children}
|
{children}
|
||||||
{resolvedRightSlot ? (
|
{resolvedRightSlot ? (
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function Label({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
visuallyHidden && "sr-only",
|
visuallyHidden && "sr-only",
|
||||||
"flex-shrink-0 text-sm",
|
"shrink-0 text-sm",
|
||||||
"text-text-subtle whitespace-nowrap flex items-center gap-1 mb-0.5",
|
"text-text-subtle whitespace-nowrap flex items-center gap-1 mb-0.5",
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ export function PairEditorRow({
|
|||||||
title={pair.enabled ? "Disable item" : "Enable item"}
|
title={pair.enabled ? "Disable item" : "Enable item"}
|
||||||
disabled={isLast || disabled}
|
disabled={isLast || disabled}
|
||||||
checked={isLast ? false : !!pair.enabled}
|
checked={isLast ? false : !!pair.enabled}
|
||||||
className={classNames(isLast && "!opacity-disabled")}
|
className={classNames(isLast && "opacity-disabled!")}
|
||||||
onChange={handleChangeEnabled}
|
onChange={handleChangeEnabled}
|
||||||
/>
|
/>
|
||||||
{!isLast && !disableDrag ? (
|
{!isLast && !disableDrag ? (
|
||||||
@@ -586,7 +586,7 @@ export function PairEditorRow({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"grid items-center",
|
"grid items-center",
|
||||||
"@xs:gap-2 @xs:!grid-rows-1 @xs:!grid-cols-[minmax(0,1fr)_minmax(0,1fr)]",
|
"@xs:gap-2 @xs:grid-rows-1! @xs:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]!",
|
||||||
"gap-0.5 grid-cols-1 grid-rows-2",
|
"gap-0.5 grid-cols-1 grid-rows-2",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -830,7 +830,7 @@ function MultilineEditDialog({
|
|||||||
const [value, setValue] = useState<string>(defaultValue);
|
const [value, setValue] = useState<string>(defaultValue);
|
||||||
const language = languageFromContentType(contentType, value);
|
const language = languageFromContentType(contentType, value);
|
||||||
return (
|
return (
|
||||||
<div className="w-[100vw] max-w-[40rem] h-[50vh] max-h-full grid grid-rows-[minmax(0,1fr)_auto]">
|
<div className="w-screen max-w-160 h-[50vh] max-h-full grid grid-rows-[minmax(0,1fr)_auto]">
|
||||||
<Editor
|
<Editor
|
||||||
heightMode="auto"
|
heightMode="auto"
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
|
|||||||
variant="border"
|
variant="border"
|
||||||
title={useBulk ? "Enable form edit" : "Enable bulk edit"}
|
title={useBulk ? "Enable form edit" : "Enable bulk edit"}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"transition-opacity opacity-0 group-hover:opacity-80 hover:!opacity-100 shadow",
|
"transition-opacity opacity-0 group-hover:opacity-80 hover:opacity-100! shadow",
|
||||||
"bg-surface hover:text group-hover/wrapper:opacity-100",
|
"bg-surface hover:text group-hover/wrapper:opacity-100",
|
||||||
)}
|
)}
|
||||||
onClick={() => setUseBulk((b) => !b)}
|
onClick={() => setUseBulk((b) => !b)}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function PillButton({ className, ...props }: ButtonProps) {
|
|||||||
<Button
|
<Button
|
||||||
size="2xs"
|
size="2xs"
|
||||||
variant="border"
|
variant="border"
|
||||||
className={classNames(className, "!rounded-full mx-1 !px-3")}
|
className={classNames(className, "rounded-full! mx-1 px-3!")}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
|||||||
const id = useRef(`input-${generateId()}`);
|
const id = useRef(`input-${generateId()}`);
|
||||||
const commonClassName = classNames(
|
const commonClassName = classNames(
|
||||||
className,
|
className,
|
||||||
"!bg-transparent min-w-0 w-full focus:outline-none placeholder:text-placeholder",
|
"bg-transparent! min-w-0 w-full focus:outline-hidden placeholder:text-placeholder",
|
||||||
"px-2 text-xs font-mono cursor-text",
|
"px-2 text-xs font-mono cursor-text",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
|||||||
"overflow-hidden",
|
"overflow-hidden",
|
||||||
focused && !disabled ? "border-border-focus" : "border-border-subtle",
|
focused && !disabled ? "border-border-focus" : "border-border-subtle",
|
||||||
disabled && "border-dotted",
|
disabled && "border-dotted",
|
||||||
hasChanged && "has-[:invalid]:border-danger", // For built-in HTML validation
|
hasChanged && "has-invalid:border-danger", // For built-in HTML validation
|
||||||
size === "md" && "min-h-md",
|
size === "md" && "min-h-md",
|
||||||
size === "sm" && "min-h-sm",
|
size === "sm" && "min-h-sm",
|
||||||
size === "xs" && "min-h-xs",
|
size === "xs" && "min-h-xs",
|
||||||
@@ -225,7 +225,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
|||||||
: `Obscure ${typeof label === "string" ? label : "field"}`
|
: `Obscure ${typeof label === "string" ? label : "field"}`
|
||||||
}
|
}
|
||||||
size="xs"
|
size="xs"
|
||||||
className="mr-0.5 group/obscure !h-auto my-0.5"
|
className="mr-0.5 group/obscure h-auto! my-0.5"
|
||||||
iconClassName="group-hover/obscure:text"
|
iconClassName="group-hover/obscure:text"
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
icon={obscured ? "eye" : "eye_closed"}
|
icon={obscured ? "eye" : "eye_closed"}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function RadioCards<T extends string>({
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-1 w-4 h-4 flex-shrink-0 rounded-full border",
|
"mt-1 w-4 h-4 shrink-0 rounded-full border",
|
||||||
"flex items-center justify-center",
|
"flex items-center justify-center",
|
||||||
selected ? "border-focus" : "border-border",
|
selected ? "border-focus" : "border-border",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export function SegmentedControl<T extends string>({
|
|||||||
role="radio"
|
role="radio"
|
||||||
tabIndex={isSelected ? 0 : -1}
|
tabIndex={isSelected ? 0 : -1}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isActive && "!text-text",
|
isActive && "text-text!",
|
||||||
"focus:ring-1 focus:ring-border-focus",
|
"focus:ring-1 focus:ring-border-focus",
|
||||||
)}
|
)}
|
||||||
onClick={() => onChange(o.value)}
|
onClick={() => onChange(o.value)}
|
||||||
@@ -111,8 +111,8 @@ export function SegmentedControl<T extends string>({
|
|||||||
role="radio"
|
role="radio"
|
||||||
tabIndex={isSelected ? 0 : -1}
|
tabIndex={isSelected ? 0 : -1}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isActive && "!text-text",
|
isActive && "text-text!",
|
||||||
"!px-1.5 !w-auto",
|
"px-1.5! w-auto!",
|
||||||
"focus:ring-border-focus",
|
"focus:ring-border-focus",
|
||||||
)}
|
)}
|
||||||
title={o.label}
|
title={o.label}
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ export function Select<T extends string>({
|
|||||||
onBlur={() => setFocused(false)}
|
onBlur={() => setFocused(false)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"pr-7 w-full outline-none bg-transparent disabled:opacity-disabled",
|
"pr-7 w-full outline-hidden bg-transparent disabled:opacity-disabled",
|
||||||
"leading-[1] rounded-none", // Center the text better vertically
|
"leading-none rounded-none", // Center the text better vertically
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isInvalidSelection && <option value={"__NONE__"}>-- Select an Option --</option>}
|
{isInvalidSelection && <option value={"__NONE__"}>-- Select an Option --</option>}
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export function ModelSettingRowBoolean<M extends AnyModel, K extends ModelKeyOfV
|
|||||||
|
|
||||||
export function SettingRowNumber({
|
export function SettingRowNumber({
|
||||||
inputClassName,
|
inputClassName,
|
||||||
inputWidthClassName = "!w-48",
|
inputWidthClassName = "w-48!",
|
||||||
name,
|
name,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -251,7 +251,7 @@ export function ModelSettingRowNumber<M extends AnyModel, K extends ModelKeyOfVa
|
|||||||
|
|
||||||
export function SettingRowText({
|
export function SettingRowText({
|
||||||
inputClassName,
|
inputClassName,
|
||||||
inputWidthClassName = "!w-80",
|
inputWidthClassName = "w-80!",
|
||||||
name,
|
name,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -358,7 +358,7 @@ export function SettingRowSelect<T extends string>({
|
|||||||
name,
|
name,
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
selectClassName = "!w-48",
|
selectClassName = "w-48!",
|
||||||
title,
|
title,
|
||||||
value,
|
value,
|
||||||
...props
|
...props
|
||||||
@@ -393,7 +393,7 @@ export function SettingSelectControl<T extends string>({
|
|||||||
name,
|
name,
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
selectClassName = "!w-48",
|
selectClassName = "w-48!",
|
||||||
value,
|
value,
|
||||||
}: {
|
}: {
|
||||||
defaultValue?: T;
|
defaultValue?: T;
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { formatSize } from "@yaakapp-internal/lib/formatSize";
|
import { formatSize } from "@yaakapp-internal/lib/formatSize";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
className?: string;
|
||||||
contentLength: number;
|
contentLength: number;
|
||||||
contentLengthCompressed?: number | null;
|
contentLengthCompressed?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SizeTag({ contentLength, contentLengthCompressed }: Props) {
|
export function SizeTag({ className, contentLength, contentLengthCompressed }: Props) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="font-mono"
|
className={classNames("font-mono", className)}
|
||||||
title={
|
title={
|
||||||
`${contentLength} bytes` +
|
`${contentLength} bytes` +
|
||||||
(contentLengthCompressed ? `\n${contentLengthCompressed} bytes compressed` : "")
|
(contentLengthCompressed ? `\n${contentLengthCompressed} bytes compressed` : "")
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
layout === "horizontal" && "flex flex-col w-full pb-3 mb-auto",
|
layout === "horizontal" && "flex flex-col w-full pb-3 mb-auto",
|
||||||
layout === "vertical" && "flex flex-row flex-shrink-0 w-full",
|
layout === "vertical" && "flex flex-row shrink-0 w-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tabButtons}
|
{tabButtons}
|
||||||
@@ -456,9 +456,9 @@ function TabButton({
|
|||||||
onChangeValue?.(tab.value);
|
onChangeValue?.(tab.value);
|
||||||
},
|
},
|
||||||
className: classNames(
|
className: classNames(
|
||||||
"flex items-center rounded whitespace-nowrap",
|
"flex items-center rounded-sm whitespace-nowrap",
|
||||||
"!px-2 ml-[1px]",
|
"px-2! ml-px",
|
||||||
"outline-none",
|
"outline-hidden",
|
||||||
"ring-none",
|
"ring-none",
|
||||||
"focus-visible-or-class:outline-2",
|
"focus-visible-or-class:outline-2",
|
||||||
addBorders && "border focus-visible:bg-surface-highlight",
|
addBorders && "border focus-visible:bg-surface-highlight",
|
||||||
@@ -468,7 +468,7 @@ function TabButton({
|
|||||||
: layout === "vertical"
|
: layout === "vertical"
|
||||||
? "border-border-subtle"
|
? "border-border-subtle"
|
||||||
: "border-transparent",
|
: "border-transparent",
|
||||||
layout === "horizontal" && "min-w-[10rem]",
|
layout === "horizontal" && "min-w-40",
|
||||||
isDragging && "opacity-50",
|
isDragging && "opacity-50",
|
||||||
overlay && "opacity-80",
|
overlay && "opacity-80",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -54,11 +54,11 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
|||||||
`x-theme-toast x-theme-toast--${color}`,
|
`x-theme-toast x-theme-toast--${color}`,
|
||||||
"pointer-events-auto overflow-hidden",
|
"pointer-events-auto overflow-hidden",
|
||||||
"relative pointer-events-auto bg-surface text-text rounded-lg",
|
"relative pointer-events-auto bg-surface text-text rounded-lg",
|
||||||
"border border-border shadow-lg w-[25rem]",
|
"border border-border shadow-lg w-100",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="pl-3 py-3 pr-10 flex items-start gap-2 w-full max-h-[11rem] overflow-auto">
|
<div className="pl-3 py-3 pr-10 flex items-start gap-2 w-full max-h-44 overflow-auto">
|
||||||
{toastIcon && <Icon icon={toastIcon} color={color} className="mt-1 flex-shrink-0" />}
|
{toastIcon && <Icon icon={toastIcon} color={color} className="mt-1 shrink-0" />}
|
||||||
<VStack space={2} className="w-full min-w-0">
|
<VStack space={2} className="w-full min-w-0">
|
||||||
<div className="select-auto">{children}</div>
|
<div className="select-auto">{children}</div>
|
||||||
{action?.({ hide: onClose })}
|
{action?.({ hide: onClose })}
|
||||||
@@ -68,7 +68,7 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
|||||||
<IconButton
|
<IconButton
|
||||||
color={color}
|
color={color}
|
||||||
variant="border"
|
variant="border"
|
||||||
className="opacity-60 border-0 !absolute top-2 right-2"
|
className="opacity-60 border-0 absolute! top-2 right-2"
|
||||||
title="Dismiss"
|
title="Dismiss"
|
||||||
icon="x"
|
icon="x"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export function Tooltip({ children, className, content, tabIndex, size = "md" }:
|
|||||||
role="button"
|
role="button"
|
||||||
aria-describedby={openState ? id.current : undefined}
|
aria-describedby={openState ? id.current : undefined}
|
||||||
tabIndex={tabIndex ?? -1}
|
tabIndex={tabIndex ?? -1}
|
||||||
className={classNames(className, "flex-grow-0 flex items-center")}
|
className={classNames(className, "grow-0 flex items-center")}
|
||||||
onClick={handleToggleImmediate}
|
onClick={handleToggleImmediate}
|
||||||
onMouseEnter={handleOpen}
|
onMouseEnter={handleOpen}
|
||||||
onMouseLeave={handleClose}
|
onMouseLeave={handleClose}
|
||||||
@@ -141,10 +141,10 @@ function Triangle({ className, position }: { className?: string; position: "top"
|
|||||||
shapeRendering="crispEdges"
|
shapeRendering="crispEdges"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"absolute z-50 left-[calc(50%-0.4rem)] h-[0.5rem] w-[0.8rem]",
|
"absolute z-50 left-[calc(50%-0.4rem)] h-2 w-[0.8rem]",
|
||||||
isBottom
|
isBottom
|
||||||
? "border-t-[2px] border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2"
|
? "border-t-2 border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2"
|
||||||
: "border-b-[2px] border-surface-highlight -top-[calc(0.5rem-3px)] mt-2",
|
: "border-b-2 border-surface-highlight -top-[calc(0.5rem-3px)] mt-2",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<title>Triangle</title>
|
<title>Triangle</title>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ function CommitListItem({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"w-full min-w-0 text-left rounded px-2 py-1.5",
|
"w-full min-w-0 text-left rounded-sm px-2 py-1.5",
|
||||||
selected && "bg-surface-active",
|
selected && "bg-surface-active",
|
||||||
)}
|
)}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
|
|||||||
secondSlot={({ style: innerStyle }) => (
|
secondSlot={({ style: innerStyle }) => (
|
||||||
<div style={innerStyle} className="grid grid-rows-[minmax(0,1fr)_auto] gap-3 pb-2">
|
<div style={innerStyle} className="grid grid-rows-[minmax(0,1fr)_auto] gap-3 pb-2">
|
||||||
<Input
|
<Input
|
||||||
className="!text-base font-sans rounded-md"
|
className="text-base! font-sans rounded-md"
|
||||||
placeholder="Commit message..."
|
placeholder="Commit message..."
|
||||||
onChange={setMessage}
|
onChange={setMessage}
|
||||||
stateKey={null}
|
stateKey={null}
|
||||||
@@ -325,7 +325,7 @@ function TreeNodeChildren({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="absolute -left-[100vw] right-0 top-0 bottom-0 bg-surface-active opacity-30 -z-10" />
|
<div className="absolute left-[-100vw] right-0 top-0 bottom-0 bg-surface-active opacity-30 -z-10" />
|
||||||
)}
|
)}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={checked}
|
checked={checked}
|
||||||
@@ -358,7 +358,7 @@ function TreeNodeChildren({
|
|||||||
{node.status.status !== "current" && (
|
{node.status.status !== "current" && (
|
||||||
<InlineCode
|
<InlineCode
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"py-0 bg-transparent w-[6rem] text-center shrink-0",
|
"py-0 bg-transparent w-24 text-center shrink-0",
|
||||||
node.status.status === "modified" && "text-info",
|
node.status.status === "modified" && "text-info",
|
||||||
node.status.status === "untracked" && "text-success",
|
node.status.status === "untracked" && "text-success",
|
||||||
node.status.status === "removed" && "text-danger",
|
node.status.status === "removed" && "text-danger",
|
||||||
@@ -400,7 +400,7 @@ function ExternalTreeNode({
|
|||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
fullWidth
|
fullWidth
|
||||||
className="h-xs w-full hover:bg-surface-highlight rounded px-1 group"
|
className="h-xs w-full hover:bg-surface-highlight rounded-sm px-1 group"
|
||||||
checked={entry.staged}
|
checked={entry.staged}
|
||||||
onChange={() => onCheck(entry)}
|
onChange={() => onCheck(entry)}
|
||||||
title={
|
title={
|
||||||
@@ -409,7 +409,7 @@ function ExternalTreeNode({
|
|||||||
<div className="truncate">{entry.relaPath}</div>
|
<div className="truncate">{entry.relaPath}</div>
|
||||||
<InlineCode
|
<InlineCode
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"py-0 ml-auto bg-transparent w-[6rem] text-center",
|
"py-0 ml-auto bg-transparent w-24 text-center",
|
||||||
entry.status === "modified" && "text-info",
|
entry.status === "modified" && "text-info",
|
||||||
entry.status === "untracked" && "text-success",
|
entry.status === "untracked" && "text-success",
|
||||||
entry.status === "removed" && "text-danger",
|
entry.status === "removed" && "text-danger",
|
||||||
|
|||||||
@@ -559,7 +559,7 @@ const GitMenuButton = forwardRef<HTMLButtonElement, HTMLAttributes<HTMLButtonEle
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"px-3 h-md border-t border-border flex items-center justify-between text-text-subtle outline-none focus-visible:bg-surface-highlight",
|
"px-3 h-md border-t border-border flex items-center justify-between text-text-subtle outline-hidden focus-visible:bg-surface-highlight",
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -113,19 +113,19 @@ export const GraphQLDocsExplorer = memo(function GraphQLDocsExplorer({
|
|||||||
name={{ value: "query", color: "primary" }}
|
name={{ value: "query", color: "primary" }}
|
||||||
item={qryItem}
|
item={qryItem}
|
||||||
setItem={setActiveItem}
|
setItem={setActiveItem}
|
||||||
className="!my-0"
|
className="my-0!"
|
||||||
/>
|
/>
|
||||||
<GqlTypeRow
|
<GqlTypeRow
|
||||||
name={{ value: "mutation", color: "primary" }}
|
name={{ value: "mutation", color: "primary" }}
|
||||||
item={mutItem}
|
item={mutItem}
|
||||||
setItem={setActiveItem}
|
setItem={setActiveItem}
|
||||||
className="!my-0"
|
className="my-0!"
|
||||||
/>
|
/>
|
||||||
<GqlTypeRow
|
<GqlTypeRow
|
||||||
name={{ value: "subscription", color: "primary" }}
|
name={{ value: "subscription", color: "primary" }}
|
||||||
item={subItem}
|
item={subItem}
|
||||||
setItem={setActiveItem}
|
setItem={setActiveItem}
|
||||||
className="!my-0"
|
className="my-0!"
|
||||||
/>
|
/>
|
||||||
<Subheading count={Object.keys(allTypes).length}>All Schema Types</Subheading>
|
<Subheading count={Object.keys(allTypes).length}>All Schema Types</Subheading>
|
||||||
<DocMarkdown>{schema.description ?? null}</DocMarkdown>
|
<DocMarkdown>{schema.description ?? null}</DocMarkdown>
|
||||||
@@ -192,7 +192,7 @@ function GraphQLExplorerHeader({
|
|||||||
noTruncate
|
noTruncate
|
||||||
item={crumb}
|
item={crumb}
|
||||||
setItem={setItem}
|
setItem={setItem}
|
||||||
className="!font-sans !text-sm flex-shrink-0"
|
className="font-sans! text-sm! shrink-0"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -208,7 +208,7 @@ function GraphQLExplorerHeader({
|
|||||||
className="hidden @[10rem]:block"
|
className="hidden @[10rem]:block"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex gap-1 [&>*]:text-text-subtle">
|
<div className="ml-auto flex gap-1 *:text-text-subtle">
|
||||||
<IconButton icon="x" size="sm" title="Close documentation explorer" onClick={onClose} />
|
<IconButton icon="x" size="sm" title="Close documentation explorer" onClick={onClose} />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -528,7 +528,7 @@ function GqlTypeRow({
|
|||||||
<span className="text-text-subtle">:</span>{" "}
|
<span className="text-text-subtle">:</span>{" "}
|
||||||
<GqlTypeLink color="notice" item={returnItem} setItem={setItem} />
|
<GqlTypeLink color="notice" item={returnItem} setItem={setItem} />
|
||||||
</div>
|
</div>
|
||||||
<DocMarkdown className="!text-text-subtle mt-0.5">
|
<DocMarkdown className="text-text-subtle! mt-0.5">
|
||||||
{item.type.description ?? null}
|
{item.type.description ?? null}
|
||||||
</DocMarkdown>
|
</DocMarkdown>
|
||||||
</div>
|
</div>
|
||||||
@@ -786,8 +786,8 @@ function GqlSchemaSearch({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"relative flex items-center bg-surface z-20 min-w-0",
|
"relative flex items-center bg-surface z-20 min-w-0",
|
||||||
!focused && "max-w-[6rem] ml-auto",
|
!focused && "max-w-24 ml-auto",
|
||||||
focused && "!absolute top-0 left-1.5 right-1.5 bottom-0 pt-1.5",
|
focused && "absolute! top-0 left-1.5 right-1.5 bottom-0 pt-1.5",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PlainInput
|
<PlainInput
|
||||||
@@ -879,7 +879,7 @@ function SearchResult({
|
|||||||
ref={initRef}
|
ref={initRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
"px-3 truncate w-full text-left h-sm rounded text-editor font-mono",
|
"px-3 truncate w-full text-left h-sm rounded-sm text-editor font-mono",
|
||||||
isActive && "bg-surface-highlight",
|
isActive && "bg-surface-highlight",
|
||||||
)}
|
)}
|
||||||
{...extraProps}
|
{...extraProps}
|
||||||
@@ -893,7 +893,7 @@ function Heading({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
function DocMarkdown({ children, className }: { children: string | null; className?: string }) {
|
function DocMarkdown({ children, className }: { children: string | null; className?: string }) {
|
||||||
return (
|
return (
|
||||||
<Markdown className={classNames(className, "!text-text-subtle italic")}>{children}</Markdown>
|
<Markdown className={classNames(className, "text-text-subtle! italic")}>{children}</Markdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
|
|||||||
|
|
||||||
const actions = useMemo<EditorProps["actions"]>(
|
const actions = useMemo<EditorProps["actions"]>(
|
||||||
() => [
|
() => [
|
||||||
<div key="actions" className="flex flex-row !opacity-100 !shadow">
|
<div key="actions" className="flex flex-row opacity-100! shadow!">
|
||||||
<div key="introspection" className="!opacity-100">
|
<div key="introspection" className="opacity-100!">
|
||||||
{schema === undefined ? null /* Initializing */ : (
|
{schema === undefined ? null /* Initializing */ : (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={[
|
items={[
|
||||||
@@ -217,7 +217,7 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
|
|||||||
stateKey={`graphql_body.${request.id}`}
|
stateKey={`graphql_body.${request.id}`}
|
||||||
{...extraEditorProps}
|
{...extraEditorProps}
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 min-h-[5rem]">
|
<div className="grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 min-h-20">
|
||||||
<Separator dashed className="pb-1">
|
<Separator dashed className="pb-1">
|
||||||
Variables
|
Variables
|
||||||
</Separator>
|
</Separator>
|
||||||
|
|||||||
@@ -1,21 +1,38 @@
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import type { ServerSentEvent } from "@yaakapp-internal/sse";
|
import { extractSseValueAtPath, type ServerSentEvent } from "@yaakapp-internal/sse";
|
||||||
import { HStack, Icon, InlineCode, VStack } from "@yaakapp-internal/ui";
|
import { HStack, Icon, InlineCode, SplitLayout, VStack } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
import { Fragment, useMemo, useState } from "react";
|
import { Fragment, useMemo, useState } from "react";
|
||||||
|
import { useKeyValue } from "../../hooks/useKeyValue";
|
||||||
import { useFormatText } from "../../hooks/useFormatText";
|
import { useFormatText } from "../../hooks/useFormatText";
|
||||||
import { useResponseBodyEventSource } from "../../hooks/useResponseBodyEventSource";
|
import { useResponseBodyEventSource } from "../../hooks/useResponseBodyEventSource";
|
||||||
|
import { useResponseBodySseSummary } from "../../hooks/useResponseBodySseSummary";
|
||||||
|
import {
|
||||||
|
sseSummaryResultKeyPathAutocomplete,
|
||||||
|
useSseSummaryResultKeyPath,
|
||||||
|
} from "../../hooks/useSseSummaryResultKeyPath";
|
||||||
import { isJSON } from "../../lib/contentType";
|
import { isJSON } from "../../lib/contentType";
|
||||||
|
import { EmptyStateText } from "../EmptyStateText";
|
||||||
|
import { Markdown } from "../Markdown";
|
||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
|
import type { DropdownItem } from "../core/Dropdown";
|
||||||
|
import { Dropdown } from "../core/Dropdown";
|
||||||
import type { EditorProps } from "../core/Editor/Editor";
|
import type { EditorProps } from "../core/Editor/Editor";
|
||||||
import { Editor } from "../core/Editor/LazyEditor";
|
import { Editor } from "../core/Editor/LazyEditor";
|
||||||
import { EventDetailHeader, EventViewer } from "../core/EventViewer";
|
import { EventDetailHeader, EventViewer } from "../core/EventViewer";
|
||||||
import { EventViewerRow } from "../core/EventViewerRow";
|
import { EventViewerRow } from "../core/EventViewerRow";
|
||||||
|
import { IconButton } from "../core/IconButton";
|
||||||
|
import { IconTooltip } from "../core/IconTooltip";
|
||||||
|
import { Input } from "../core/Input";
|
||||||
|
import { Select } from "../core/Select";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
response: HttpResponse;
|
response: HttpResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_EXTRACTED_TEXT_RATIO = 0.28;
|
||||||
|
|
||||||
export function EventStreamViewer({ response }: Props) {
|
export function EventStreamViewer({ response }: Props) {
|
||||||
return (
|
return (
|
||||||
<Fragment
|
<Fragment
|
||||||
@@ -29,64 +46,316 @@ export function EventStreamViewer({ response }: Props) {
|
|||||||
function ActualEventStreamViewer({ response }: Props) {
|
function ActualEventStreamViewer({ response }: Props) {
|
||||||
const [showLarge, setShowLarge] = useState<boolean>(false);
|
const [showLarge, setShowLarge] = useState<boolean>(false);
|
||||||
const [showingLarge, setShowingLarge] = useState<boolean>(false);
|
const [showingLarge, setShowingLarge] = useState<boolean>(false);
|
||||||
|
const filterEventPreviewsSetting = useKeyValue<boolean>({
|
||||||
|
namespace: "no_sync",
|
||||||
|
key: ["sse_filter_event_previews", response.requestId],
|
||||||
|
fallback: false,
|
||||||
|
});
|
||||||
|
const applyToDetailsSetting = useKeyValue<boolean>({
|
||||||
|
namespace: "no_sync",
|
||||||
|
key: ["sse_apply_to_details", response.requestId],
|
||||||
|
fallback: false,
|
||||||
|
});
|
||||||
|
const renderMarkdownSetting = useKeyValue<boolean>({
|
||||||
|
namespace: "no_sync",
|
||||||
|
key: ["sse_render_markdown", response.requestId],
|
||||||
|
fallback: false,
|
||||||
|
});
|
||||||
|
const summarySettings = useSseSummaryResultKeyPath({ response });
|
||||||
const events = useResponseBodyEventSource(response);
|
const events = useResponseBodyEventSource(response);
|
||||||
|
const summary = useResponseBodySseSummary(response, summarySettings.resultKeyPath);
|
||||||
|
const showExtractedText = summarySettings.resultKeyPath != null;
|
||||||
|
const showResultKeyPathWarning =
|
||||||
|
showExtractedText &&
|
||||||
|
summary.data != null &&
|
||||||
|
summary.data.fragmentCount === 0 &&
|
||||||
|
!summary.isFetching &&
|
||||||
|
summary.error == null;
|
||||||
|
const filterEventPreviews = showExtractedText && filterEventPreviewsSetting.value === true;
|
||||||
|
const applyToDetails = showExtractedText && applyToDetailsSetting.value === true;
|
||||||
|
const renderMarkdown = showExtractedText && renderMarkdownSetting.value === true;
|
||||||
|
const settingsItems = useMemo<DropdownItem[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: "Apply to Previews",
|
||||||
|
keepOpenOnSelect: true,
|
||||||
|
onSelect: () => filterEventPreviewsSetting.set(filterEventPreviewsSetting.value !== true),
|
||||||
|
leftSlot: (
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
filterEventPreviewsSetting.value === true
|
||||||
|
? "check_square_checked"
|
||||||
|
: "check_square_unchecked"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Apply to Details",
|
||||||
|
keepOpenOnSelect: true,
|
||||||
|
onSelect: () => applyToDetailsSetting.set(applyToDetailsSetting.value !== true),
|
||||||
|
leftSlot: (
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
applyToDetailsSetting.value === true
|
||||||
|
? "check_square_checked"
|
||||||
|
: "check_square_unchecked"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
applyToDetailsSetting,
|
||||||
|
filterEventPreviewsSetting,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventViewer
|
<div className="h-full min-h-0 grid grid-rows-[auto_minmax(0,1fr)]">
|
||||||
events={events.data ?? []}
|
<HStack space={2} alignItems="center" className="pt-1 pb-1 border-b border-border-subtle">
|
||||||
getEventKey={(_, index) => String(index)}
|
<div className={classNames(summarySettings.enabled ? "w-44 shrink-0" : "min-w-40 flex-1")}>
|
||||||
error={events.error ? String(events.error) : null}
|
<Select
|
||||||
splitLayoutStorageKey="sse_events"
|
name={`sse-summary-result-key-path-enabled::${response.requestId}`}
|
||||||
defaultRatio={0.4}
|
label="Extracted text"
|
||||||
renderRow={({ event, index, isActive, onClick }) => (
|
hideLabel
|
||||||
<EventViewerRow
|
size="xs"
|
||||||
isActive={isActive}
|
value={summarySettings.enabled ? "jsonpath" : "off"}
|
||||||
onClick={onClick}
|
options={[
|
||||||
icon={<Icon color="info" title="Server Message" icon="arrow_big_down_dash" />}
|
{ label: "Full events", value: "off" },
|
||||||
content={
|
{ label: "JSONPath", value: "jsonpath" },
|
||||||
<HStack space={2} className="items-center">
|
]}
|
||||||
<EventLabels event={event} index={index} isActive={isActive} />
|
onChange={(value) => summarySettings.setEnabled(value === "jsonpath")}
|
||||||
<span className="truncate text-xs">{event.data.slice(0, 1000)}</span>
|
/>
|
||||||
</HStack>
|
</div>
|
||||||
}
|
{summarySettings.enabled && (
|
||||||
/>
|
<>
|
||||||
)}
|
<div className="min-w-40 flex-1">
|
||||||
renderDetail={({ event, index, onClose }) => (
|
<Input
|
||||||
<EventDetail
|
label="Result JSON path"
|
||||||
event={event}
|
hideLabel
|
||||||
index={index}
|
size="xs"
|
||||||
showLarge={showLarge}
|
autocomplete={sseSummaryResultKeyPathAutocomplete}
|
||||||
showingLarge={showingLarge}
|
defaultValue={summarySettings.resultKeyPathInputValue}
|
||||||
setShowLarge={setShowLarge}
|
forceUpdateKey={`${response.requestId}:${summarySettings.inferredResultKeyPath ?? ""}`}
|
||||||
setShowingLarge={setShowingLarge}
|
placeholder="$.choices[0].delta.content"
|
||||||
onClose={onClose}
|
rightSlot={
|
||||||
/>
|
showResultKeyPathWarning ? (
|
||||||
)}
|
<div className="flex items-center px-2">
|
||||||
/>
|
<IconTooltip
|
||||||
|
tabIndex={-1}
|
||||||
|
icon="alert_triangle"
|
||||||
|
iconColor="notice"
|
||||||
|
content="No text fragments matched this JSONPath."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
stateKey={`sse-summary-result-key-path::${response.requestId}`}
|
||||||
|
tint={showResultKeyPathWarning ? "notice" : undefined}
|
||||||
|
onChange={summarySettings.setResultKeyPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Dropdown items={settingsItems}>
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
variant="border"
|
||||||
|
icon="settings"
|
||||||
|
title="Extracted text settings"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
<SplitLayout
|
||||||
|
layout="vertical"
|
||||||
|
storageKey={`sse_extracted_text::${response.requestId}`}
|
||||||
|
defaultRatio={DEFAULT_EXTRACTED_TEXT_RATIO}
|
||||||
|
minHeightPx={72}
|
||||||
|
resizeHandleClassName="hover:bg-surface-highlight active:bg-surface-highlight"
|
||||||
|
firstSlot={({ style }) => (
|
||||||
|
<div style={style} className="min-h-0">
|
||||||
|
<EventViewer
|
||||||
|
events={events.data ?? []}
|
||||||
|
getEventKey={(_, index) => String(index)}
|
||||||
|
error={events.error ? String(events.error) : null}
|
||||||
|
splitLayoutStorageKey="sse_events"
|
||||||
|
defaultRatio={0.4}
|
||||||
|
renderRow={({ event, index, isActive, onClick }) => (
|
||||||
|
<EventViewerRow
|
||||||
|
isActive={isActive}
|
||||||
|
onClick={onClick}
|
||||||
|
icon={<Icon color="info" title="Server Message" icon="arrow_big_down_dash" />}
|
||||||
|
content={
|
||||||
|
<HStack space={2} className="items-center">
|
||||||
|
<EventLabels event={event} index={index} isActive={isActive} />
|
||||||
|
<span className="truncate text-xs">
|
||||||
|
{getEventPreview(event, summarySettings.resultKeyPath, filterEventPreviews)}
|
||||||
|
</span>
|
||||||
|
</HStack>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderDetail={({ event, index, onClose }) => (
|
||||||
|
<EventDetail
|
||||||
|
event={event}
|
||||||
|
index={index}
|
||||||
|
applyJsonPath={applyToDetails}
|
||||||
|
resultKeyPath={summarySettings.resultKeyPath}
|
||||||
|
showLarge={showLarge}
|
||||||
|
showingLarge={showingLarge}
|
||||||
|
setShowLarge={setShowLarge}
|
||||||
|
setShowingLarge={setShowingLarge}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
secondSlot={
|
||||||
|
showExtractedText
|
||||||
|
? ({ style }) => (
|
||||||
|
<SseSummaryFooter
|
||||||
|
style={style}
|
||||||
|
error={summary.error ? String(summary.error) : null}
|
||||||
|
isLoading={summary.isLoading}
|
||||||
|
onRenderMarkdownChange={renderMarkdownSetting.set}
|
||||||
|
renderMarkdown={renderMarkdown}
|
||||||
|
resultKeyPath={summarySettings.resultKeyPath ?? ""}
|
||||||
|
summary={summary.data?.summary ?? ""}
|
||||||
|
fragmentCount={summary.data?.fragmentCount ?? 0}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SseSummaryFooter({
|
||||||
|
error,
|
||||||
|
fragmentCount,
|
||||||
|
isLoading,
|
||||||
|
onRenderMarkdownChange,
|
||||||
|
renderMarkdown,
|
||||||
|
resultKeyPath,
|
||||||
|
style,
|
||||||
|
summary,
|
||||||
|
}: {
|
||||||
|
error: string | null;
|
||||||
|
fragmentCount: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
onRenderMarkdownChange: (renderMarkdown: boolean) => void;
|
||||||
|
renderMarkdown: boolean;
|
||||||
|
resultKeyPath: string;
|
||||||
|
style: CSSProperties;
|
||||||
|
summary: string;
|
||||||
|
}) {
|
||||||
|
const hasSummary = fragmentCount > 0;
|
||||||
|
const actions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: "sse-summary-format",
|
||||||
|
label: "Extracted text format",
|
||||||
|
type: "select" as const,
|
||||||
|
value: renderMarkdown ? "markdown" : "text",
|
||||||
|
options: [
|
||||||
|
{ label: "Text", value: "text" },
|
||||||
|
{ label: "Markdown", value: "markdown" },
|
||||||
|
],
|
||||||
|
onChange: (value: string) => onRenderMarkdownChange(value === "markdown"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[onRenderMarkdownChange, renderMarkdown],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className="min-h-0 overflow-hidden border-t border-border-subtle bg-surface grid grid-rows-[auto_minmax(0,1fr)]"
|
||||||
|
>
|
||||||
|
<div className="pt-2">
|
||||||
|
<EventDetailHeader
|
||||||
|
actions={actions}
|
||||||
|
title="Extracted Text"
|
||||||
|
copyText={hasSummary ? summary : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"min-h-0 py-2 overflow-auto",
|
||||||
|
(error != null || isLoading || (hasSummary && !renderMarkdown)) && "text-xs",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{error != null ? (
|
||||||
|
<span className="text-danger">{error}</span>
|
||||||
|
) : isLoading ? (
|
||||||
|
<span className="italic text-text-subtlest">Loading extracted text...</span>
|
||||||
|
) : hasSummary ? (
|
||||||
|
renderMarkdown ? (
|
||||||
|
<div className="min-h-0">
|
||||||
|
<Markdown className="select-auto cursor-auto">{summary}</Markdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre className="font-mono whitespace-pre-wrap wrap-break-word select-auto cursor-auto">
|
||||||
|
{summary}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<EmptyStateText className="gap-1.5">
|
||||||
|
No fragments for <InlineCode className="py-0">{resultKeyPath}</InlineCode>
|
||||||
|
</EmptyStateText>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventPreview(
|
||||||
|
event: ServerSentEvent,
|
||||||
|
resultKeyPath: string | null,
|
||||||
|
filterEventPreview: boolean,
|
||||||
|
): string {
|
||||||
|
if (filterEventPreview && resultKeyPath != null) {
|
||||||
|
return (extractSseValueAtPath(event.data, resultKeyPath) ?? event.data).slice(0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.data.slice(0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
function EventDetail({
|
function EventDetail({
|
||||||
|
applyJsonPath,
|
||||||
event,
|
event,
|
||||||
index,
|
index,
|
||||||
|
resultKeyPath,
|
||||||
showLarge,
|
showLarge,
|
||||||
showingLarge,
|
showingLarge,
|
||||||
setShowLarge,
|
setShowLarge,
|
||||||
setShowingLarge,
|
setShowingLarge,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
|
applyJsonPath: boolean;
|
||||||
event: ServerSentEvent;
|
event: ServerSentEvent;
|
||||||
index: number;
|
index: number;
|
||||||
|
resultKeyPath: string | null;
|
||||||
showLarge: boolean;
|
showLarge: boolean;
|
||||||
showingLarge: boolean;
|
showingLarge: boolean;
|
||||||
setShowLarge: (v: boolean) => void;
|
setShowLarge: (v: boolean) => void;
|
||||||
setShowingLarge: (v: boolean) => void;
|
setShowingLarge: (v: boolean) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const detailText = useMemo(
|
||||||
|
() =>
|
||||||
|
applyJsonPath && resultKeyPath != null
|
||||||
|
? (extractSseValueAtPath(event.data, resultKeyPath) ?? event.data)
|
||||||
|
: event.data,
|
||||||
|
[applyJsonPath, event.data, resultKeyPath],
|
||||||
|
);
|
||||||
const language = useMemo<"text" | "json">(() => {
|
const language = useMemo<"text" | "json">(() => {
|
||||||
if (!event?.data) return "text";
|
if (!detailText) return "text";
|
||||||
return isJSON(event?.data) ? "json" : "text";
|
return isJSON(detailText) ? "json" : "text";
|
||||||
}, [event?.data]);
|
}, [detailText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
@@ -95,7 +364,7 @@ function EventDetail({
|
|||||||
prefix={<EventLabels event={event} index={index} />}
|
prefix={<EventLabels event={event} index={index} />}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
{!showLarge && event.data.length > 1000 * 1000 ? (
|
{!showLarge && detailText.length > 1000 * 1000 ? (
|
||||||
<VStack space={2} className="italic text-text-subtlest">
|
<VStack space={2} className="italic text-text-subtlest">
|
||||||
Message previews larger than 1MB are hidden
|
Message previews larger than 1MB are hidden
|
||||||
<div>
|
<div>
|
||||||
@@ -117,7 +386,7 @@ function EventDetail({
|
|||||||
</div>
|
</div>
|
||||||
</VStack>
|
</VStack>
|
||||||
) : (
|
) : (
|
||||||
<FormattedEditor language={language} text={event.data} />
|
<FormattedEditor language={language} text={detailText} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -142,14 +411,17 @@ function EventLabels({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<HStack space={1.5} alignItems="center" className={className}>
|
<HStack space={1.5} alignItems="center" className={className}>
|
||||||
<InlineCode className={classNames("py-0", isActive && "bg-text-subtlest text-text")}>
|
<EventLabel isActive={isActive}>{event.id ?? index}</EventLabel>
|
||||||
{event.id ?? index}
|
{event.eventType && <EventLabel isActive={isActive}>{event.eventType}</EventLabel>}
|
||||||
</InlineCode>
|
|
||||||
{event.eventType && (
|
|
||||||
<InlineCode className={classNames("py-0", isActive && "bg-text-subtlest text-text")}>
|
|
||||||
{event.eventType}
|
|
||||||
</InlineCode>
|
|
||||||
)}
|
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EventLabel({ children, isActive }: { children: ReactNode; isActive?: boolean }) {
|
||||||
|
return (
|
||||||
|
<InlineCode className={classNames("py-0", isActive && "relative overflow-hidden")}>
|
||||||
|
{isActive && <span className="absolute inset-0 bg-text opacity-5 pointer-events-none" />}
|
||||||
|
<span className="relative">{children}</span>
|
||||||
|
</InlineCode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { useCopyHttpResponse } from "../../hooks/useCopyHttpResponse";
|
||||||
import { useResponseBodyText } from "../../hooks/useResponseBodyText";
|
import { useResponseBodyText } from "../../hooks/useResponseBodyText";
|
||||||
|
import { useSaveResponse } from "../../hooks/useSaveResponse";
|
||||||
import { languageFromContentType } from "../../lib/contentType";
|
import { languageFromContentType } from "../../lib/contentType";
|
||||||
import { getContentTypeFromHeaders } from "../../lib/model_util";
|
import { getContentTypeFromHeaders } from "../../lib/model_util";
|
||||||
import type { EditorProps } from "../core/Editor/Editor";
|
import type { EditorProps } from "../core/Editor/Editor";
|
||||||
|
import { IconButton } from "../core/IconButton";
|
||||||
import { EmptyStateText } from "../EmptyStateText";
|
import { EmptyStateText } from "../EmptyStateText";
|
||||||
import { TextViewer } from "./TextViewer";
|
import { TextViewer } from "./TextViewer";
|
||||||
import { WebPageViewer } from "./WebPageViewer";
|
import { WebPageViewer } from "./WebPageViewer";
|
||||||
@@ -51,6 +54,9 @@ interface HttpTextViewerProps {
|
|||||||
function HttpTextViewer({ response, text, language, pretty, className }: HttpTextViewerProps) {
|
function HttpTextViewer({ response, text, language, pretty, className }: HttpTextViewerProps) {
|
||||||
const [currentFilter, setCurrentFilter] = useState<string | null>(null);
|
const [currentFilter, setCurrentFilter] = useState<string | null>(null);
|
||||||
const filteredBody = useResponseBodyText({ response, filter: currentFilter });
|
const filteredBody = useResponseBodyText({ response, filter: currentFilter });
|
||||||
|
const saveResponse = useSaveResponse(response);
|
||||||
|
const copyResponse = useCopyHttpResponse(response);
|
||||||
|
const actionsDisabled = response.state !== "closed" && response.status >= 100;
|
||||||
|
|
||||||
const filterCallback = useMemo(
|
const filterCallback = useMemo(
|
||||||
() => (filter: string) => {
|
() => (filter: string) => {
|
||||||
@@ -72,6 +78,26 @@ function HttpTextViewer({ response, text, language, pretty, className }: HttpTex
|
|||||||
filterStateKey={`response.body.${response.requestId}`}
|
filterStateKey={`response.body.${response.requestId}`}
|
||||||
pretty={pretty}
|
pretty={pretty}
|
||||||
className={className}
|
className={className}
|
||||||
|
footerActions={[
|
||||||
|
<IconButton
|
||||||
|
key="save"
|
||||||
|
size="sm"
|
||||||
|
icon="save"
|
||||||
|
title="Save response to file"
|
||||||
|
disabled={actionsDisabled}
|
||||||
|
onClick={() => saveResponse.mutate()}
|
||||||
|
className="border !border-border-subtle"
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
key="copy"
|
||||||
|
size="sm"
|
||||||
|
icon="copy"
|
||||||
|
title="Copy response body"
|
||||||
|
disabled={actionsDisabled}
|
||||||
|
onClick={() => copyResponse.mutate()}
|
||||||
|
className="border !border-border-subtle"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
onFilter={filterCallback}
|
onFilter={filterCallback}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function MultipartViewer({ data, boundary, idPrefix = "multipart" }: Prop
|
|||||||
<div className="h-5 w-5 overflow-auto flex items-center justify-end">
|
<div className="h-5 w-5 overflow-auto flex items-center justify-end">
|
||||||
<ImageViewer
|
<ImageViewer
|
||||||
data={part.arrayBuffer}
|
data={part.arrayBuffer}
|
||||||
className="ml-auto w-auto rounded overflow-hidden"
|
className="ml-auto w-auto rounded-sm overflow-hidden"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : part.filename ? (
|
) : part.filename ? (
|
||||||
@@ -76,7 +76,7 @@ export function MultipartViewer({ data, boundary, idPrefix = "multipart" }: Prop
|
|||||||
// oxlint-disable-next-line react/no-array-index-key -- Nothing else to key on
|
// oxlint-disable-next-line react/no-array-index-key -- Nothing else to key on
|
||||||
key={idPrefix + part.name + i}
|
key={idPrefix + part.name + i}
|
||||||
value={tabValue(part, i)}
|
value={tabValue(part, i)}
|
||||||
className="pl-3 !pt-0"
|
className="pl-3 pt-0!"
|
||||||
>
|
>
|
||||||
<Part part={part} />
|
<Part part={part} />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user