Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,35 @@ module.exports = {

overrides: [
{
files: ['*.ts'],
files: ['*.cjs'],
parserOptions: {
sourceType: 'script',
},
},
{
files: ['*.ts', '*.tsx', '*.mts'],
extends: ['@metamask/eslint-config-typescript'],
},

{
files: ['*.test.ts'],
files: ['*.test.ts', '*.test.tsx'],
extends: ['@metamask/eslint-config-jest'],
},

{
files: ['src/ui/**/*.tsx'],
extends: ['plugin:react/recommended', 'plugin:react/jsx-runtime'],
rules: {
// This rule isn't useful for us
'react/no-unescaped-entities': 'off',
},
settings: {
react: {
version: 'detect',
},
},
},
],

ignorePatterns: ['!.eslintrc.js', '!.prettierrc.js', 'dist/'],
ignorePatterns: ['dist/', 'node_modules/'],
};
6 changes: 3 additions & 3 deletions babel.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
env: {
test: {
presets: ['@babel/preset-env', '@babel/preset-typescript'],
plugins: ['@babel/plugin-transform-modules-commonjs']
}
}
plugins: ['@babel/plugin-transform-modules-commonjs'],
},
},
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"build:ui": "vite build",
"build:clean": "rimraf dist && yarn build",
"lint": "yarn lint:eslint && yarn lint:misc --check",
"lint:eslint": "eslint . --cache --ext js,ts",
"lint:eslint": "eslint . --cache --ext cjs,cts,js,mjs,mts,ts,tsx",
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write",
"lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore --no-error-on-unmatched-pattern",
"prepack": "./scripts/prepack.sh",
Expand Down Expand Up @@ -75,6 +75,7 @@
"eslint-plugin-jsdoc": "^39.6.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.5",
"jest": "^29.7.0",
"jest-it-up": "^3.0.0",
"jest-when": "^3.5.2",
Expand Down
56 changes: 45 additions & 11 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import './style.css';
import React, { useState, useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import { SemVer } from 'semver';
import { ErrorMessage } from './ErrorMessage.js';
import { PackageItem } from './PackageItem.js';
import { Package, RELEASE_TYPE_OPTIONS, ReleaseType } from './types.js';

// This file doesn't export anything, it is used to load Tailwind.
// eslint-disable-next-line import/no-unassigned-import
import './style.css';

// Helper function to compare sets
const setsAreEqual = (a: Set<string>, b: Set<string>) => {
if (a.size !== b.size) return false;
if (a.size !== b.size) {
return false;
}

return [...a].every((value) => b.has(value));
};

Expand All @@ -21,6 +27,16 @@ type SubmitButtonProps = {
onSubmit: () => Promise<void>;
};

/**
* Creates the release branch.
*
* @param props - The props.
* @param props.selections - The packages that have been added to the release.
* @param props.packageDependencyErrors - Errors that are discovered relating to
* dependencies or dependents of packages.
* @param props.onSubmit - The callback to call when the button is pressed.
* @returns The submit button.
*/
function SubmitButton({
selections,
packageDependencyErrors,
Expand All @@ -33,7 +49,7 @@ function SubmitButton({

return (
<button
onClick={() => void onSubmit()}
onClick={onSubmit}
disabled={isDisabled}
className={`mt-6 px-4 py-2 rounded ${
isDisabled
Expand All @@ -46,6 +62,11 @@ function SubmitButton({
);
}

/**
* The main component of the `create-release-branch` app.
*
* @returns The app component.
*/
function App() {
const [packages, setPackages] = useState<Package[]>([]);
const [selections, setSelections] = useState<Record<string, string>>({});
Expand Down Expand Up @@ -85,6 +106,7 @@ function App() {
if (!res.ok) {
throw new Error(`Received ${res.status}`);
}

return res.json();
})
.then((data: Package[]) => {
Expand Down Expand Up @@ -114,7 +136,9 @@ function App() {
}, [selections]);

const checkDependencies = async (selectionData: Record<string, string>) => {
if (Object.keys(selectionData).length === 0) return;
if (Object.keys(selectionData).length === 0) {
return false;
}

try {
const response = await fetch('/api/check-packages', {
Expand Down Expand Up @@ -144,7 +168,7 @@ function App() {

useEffect(() => {
const timeoutId = setTimeout(() => {
void checkDependencies(selections);
checkDependencies(selections);
}, 500);

return () => clearTimeout(timeoutId);
Expand Down Expand Up @@ -210,6 +234,7 @@ function App() {

const handleSubmit = async (): Promise<void> => {
setIsSubmitting(true);

try {
const response = await fetch('/api/release', {
method: 'POST',
Expand All @@ -234,11 +259,12 @@ function App() {

if (data.status === 'error' && data.errors) {
setSubmitErrors(
data.errors.flatMap((error) => {
if (Array.isArray(error.message)) {
return error.message;
data.errors.flatMap((submitError) => {
if (Array.isArray(submitError.message)) {
return submitError.message;
}
return error.message;

return submitError.message;
}),
);
}
Expand All @@ -251,6 +277,8 @@ function App() {
err instanceof Error ? err.message : 'An error occurred';
setError(errorMessage);
console.error('Error submitting selections:', err);
// TODO: Show an error message instead of an alert
// eslint-disable-next-line no-alert
alert('Failed to submit selections. Please try again.');
} finally {
setIsSubmitting(false);
Expand All @@ -259,11 +287,14 @@ function App() {

const fetchChangelog = async (packageName: string): Promise<void> => {
setLoadingChangelogs((prev) => ({ ...prev, [packageName]: true }));

try {
const response = await fetch(`/api/changelog?package=${packageName}`);

if (!response.ok) {
throw new Error('Failed to fetch changelog');
}

const changelog = await response.text();
setChangelogs((prev) => ({ ...prev, [packageName]: changelog }));
} catch (err) {
Expand All @@ -288,11 +319,13 @@ function App() {
const togglePackageSelection = (packageName: string) => {
setSelectedPackages((prev) => {
const newSet = new Set(prev);

if (newSet.has(packageName)) {
newSet.delete(packageName);
} else {
newSet.add(packageName);
}

return newSet;
});
};
Expand Down Expand Up @@ -422,12 +455,13 @@ function App() {
}

const container = document.getElementById('root');

if (container === null) {
throw new Error('Failed to find the root element');
}

const root = createRoot(container);
root.render(
const reactRoot = createRoot(container);
reactRoot.render(
<React.StrictMode>
<App />
</React.StrictMode>,
Expand Down
10 changes: 10 additions & 0 deletions src/ui/DependencyErrorSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ type DependencyErrorSectionProps = {
description: string;
};

/**
* Display details about missing dependents or dependencies.
*
* @param props - The props.
* @param props.title - The title of the section.
* @param props.items - The missing dependents or dependencies.
* @param props.setSelections - Updates data around packages selected for the release.
* @param props.description - Describes the error.
* @returns The section component.
*/
export function DependencyErrorSection({
title,
items,
Expand Down
11 changes: 10 additions & 1 deletion src/ui/ErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ type ErrorMessageProps = {
errors: string[];
};

/**
* A generic error message.
*
* @param props - The props.
* @param props.errors - The list of errors.
* @returns The error message component.
*/
export function ErrorMessage({ errors }: ErrorMessageProps) {
if (errors.length === 0) return null;
if (errors.length === 0) {
return null;
}

return (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
Expand Down
12 changes: 10 additions & 2 deletions src/ui/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import ReactMarkdown from 'react-markdown';

/**
* Renders Markdown, used to render the changelog for a package.
*
* @param props - The props.
* @param props.content - The text to render.
* @returns The rendered Markdown.
*/
export function Markdown({ content }: { content: string }) {
return (
<ReactMarkdown
children={content}
components={{
h1: ({ node, ...props }) => (
<h1 className="text-2xl font-bold my-4" {...props} />
Expand Down Expand Up @@ -35,6 +41,8 @@ export function Markdown({ content }: { content: string }) {
/>
),
}}
/>
>
{content}
</ReactMarkdown>
);
}
40 changes: 36 additions & 4 deletions src/ui/PackageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,38 @@ type PackageItemProps = {
onToggleSelect: () => void;
};

/**
* Displays a box for a candidate package within a release, which contains the
* name of the package, its current and new version, a dropdown for selecting a
* new version, and any errors that are generated when selecting a new version.
*
* @param props - The props.
* @param props.pkg - Data on the package.
* @param props.selections - The list of selected packages in this release.
* @param props.versionErrors - Validation errors specific to the new version
* chosen.
* @param props.packageDependencyErrors - Errors related to dependencies or
* dependents of the package.
* @param props.loadingChangelogs - Used to determine whether the changelog for
* this package is being loaded.
* @param props.changelogs - Used to display the changelog for this package.
* @param props.isSelected - Whether this package is selected (for bulk
* actions).
* @param props.showCheckbox - Whether a checkbox should be shown next to the
* package name (for bulk actions).
* @param props.onSelectionChange - Callback called when the version selector
* for the package is changed.
* @param props.onCustomVersionChange - Callback called when a custom version is
* set or changed.
* @param props.onFetchChangelog - Callback called when the changelog is fetched.
* @param props.setSelections - Used to update the list of packages selected for
* this release.
* @param props.setChangelogs - Used to update the list of changelogs loaded
* across all packages.
* @param props.onToggleSelect - Callback called when selection for this package
* is toggled.
* @returns The package item component.
*/
export function PackageItem({
pkg,
selections,
Expand Down Expand Up @@ -82,16 +114,16 @@ export function PackageItem({
!versionErrors[pkg.name] && (
<p className="text-yellow-700">
New version:{' '}
{!['patch', 'minor', 'major'].includes(selections[pkg.name])
? selections[pkg.name]
: new SemVer(pkg.version)
{['patch', 'minor', 'major'].includes(selections[pkg.name])
? new SemVer(pkg.version)
.inc(
selections[pkg.name] as Exclude<
ReleaseType,
'intentionally-skip' | 'custom' | string
>,
)
.toString()}
.toString()
: selections[pkg.name]}
</p>
)}
{versionErrors[pkg.name] && (
Expand Down
18 changes: 17 additions & 1 deletion src/ui/VersionSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ type VersionSelectorProps = {
isLoadingChangelog: boolean;
};

/**
* The dropdown used to select a version for a package.
*
* @param props - The props.
* @param props.packageName - The name of the package.
* @param props.selection - The selected value of the dropdown.
* @param props.onSelectionChange - Callback called when the value of the
* dropdown changes.
* @param props.onCustomVersionChange - Callback called when the value of the
* custom version text field is changed.
* @param props.onFetchChangelog - Callback called when the changelog for the
* package is fetched.
* @param props.isLoadingChangelog - Whether the changelog for the package is
* being loaded.
* @returns The version selector component.
*/
export function VersionSelector({
packageName,
selection,
Expand Down Expand Up @@ -54,7 +70,7 @@ export function VersionSelector({
/>
)}
<button
onClick={() => void onFetchChangelog(packageName)}
onClick={() => onFetchChangelog(packageName)}
disabled={isLoadingChangelog}
className="bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600 disabled:bg-gray-400"
>
Expand Down
File renamed without changes.
Loading
Loading