diff --git a/github-gql/get-recent-design-reviews.graphql b/github-gql/get-recent-design-reviews.graphql
index f892e42..2ba37cd 100644
--- a/github-gql/get-recent-design-reviews.graphql
+++ b/github-gql/get-recent-design-reviews.graphql
@@ -30,6 +30,11 @@ query RecentDesignReviews(
name
}
}
+ milestone {
+ id
+ title
+ dueOn
+ }
}
}
}
diff --git a/package.json b/package.json
index 06297d1..0a9d22e 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,9 @@
},
"dependencies": {
"@astrojs/node": "^8.3.4",
+ "@github/relative-time-element": "^4.4.3",
"@graphql-typed-document-node/core": "^3.2.0",
+ "@js-temporal/polyfill": "^0.4.4",
"@observablehq/plot": "^0.6.16",
"@octokit/webhooks": "^13.3.0",
"@prisma/client": "^5.22.0",
@@ -21,6 +23,7 @@
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-gfm": "^3.0.0",
"mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
"micromark-extension-gfm": "^3.0.0",
"octokit": "^4.0.2",
"prisma": "^5.22.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d4edebe..66d6497 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,9 +11,15 @@ importers:
'@astrojs/node':
specifier: ^8.3.4
version: 8.3.4(astro@4.16.9(@types/node@22.9.0)(rollup@4.24.4)(typescript@5.6.3))
+ '@github/relative-time-element':
+ specifier: ^4.4.3
+ version: 4.4.3
'@graphql-typed-document-node/core':
specifier: ^3.2.0
version: 3.2.0(graphql@16.9.0)
+ '@js-temporal/polyfill':
+ specifier: ^0.4.4
+ version: 0.4.4
'@observablehq/plot':
specifier: ^0.6.16
version: 0.6.16
@@ -35,6 +41,9 @@ importers:
mdast-util-to-string:
specifier: ^4.0.0
version: 4.0.0
+ micromark:
+ specifier: ^4.0.0
+ version: 4.0.0
micromark-extension-gfm:
specifier: ^3.0.0
version: 3.0.0
@@ -623,6 +632,9 @@ packages:
resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@github/relative-time-element@4.4.3':
+ resolution: {integrity: sha512-EVKokqx9/DdUAZ2l9WVyY51EtRCO2gQWWMvsRIn7r4glJ91q9CXcnILVHZVCpfD52ucXUhUvtYsAjNJ4qP4uIg==}
+
'@graphql-codegen/add@5.0.3':
resolution: {integrity: sha512-SxXPmramkth8XtBlAHu4H4jYcYXM/o3p01+psU+0NADQowA8jtYkK6MW5rV6T+CxkEaNZItfSmZRPgIuypcqnA==}
peerDependencies:
@@ -989,6 +1001,10 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+ '@js-temporal/polyfill@0.4.4':
+ resolution: {integrity: sha512-2X6bvghJ/JAoZO52lbgyAPFj8uCflhTo2g7nkFzEQdXd/D8rEeD4HtmTEpmtGCva260fcd66YNXBOYdnmHqSOg==}
+ engines: {node: '>=12'}
+
'@kamilkisiela/fast-url-parser@1.1.4':
resolution: {integrity: sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==}
@@ -2531,6 +2547,9 @@ packages:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
+ jsbi@4.3.0:
+ resolution: {integrity: sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==}
+
jsesc@3.0.2:
resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
engines: {node: '>=6'}
@@ -4502,6 +4521,8 @@ snapshots:
dependencies:
levn: 0.4.1
+ '@github/relative-time-element@4.4.3': {}
+
'@graphql-codegen/add@5.0.3(graphql@16.9.0)':
dependencies:
'@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.9.0)
@@ -5045,6 +5066,11 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
+ '@js-temporal/polyfill@0.4.4':
+ dependencies:
+ jsbi: 4.3.0
+ tslib: 2.8.1
+
'@kamilkisiela/fast-url-parser@1.1.4': {}
'@nodelib/fs.scandir@2.1.5':
@@ -5843,7 +5869,7 @@ snapshots:
camel-case@4.1.2:
dependencies:
pascal-case: 3.1.2
- tslib: 2.6.3
+ tslib: 2.8.1
camelcase@5.3.1: {}
@@ -5854,7 +5880,7 @@ snapshots:
capital-case@1.0.4:
dependencies:
no-case: 3.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
upper-case-first: 2.0.2
ccount@2.0.1: {}
@@ -5900,7 +5926,7 @@ snapshots:
path-case: 3.0.4
sentence-case: 3.0.4
snake-case: 3.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
character-entities-html4@2.1.0: {}
@@ -5988,7 +6014,7 @@ snapshots:
constant-case@3.0.4:
dependencies:
no-case: 3.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
upper-case: 2.0.2
convert-source-map@2.0.0: {}
@@ -6238,7 +6264,7 @@ snapshots:
dot-case@3.0.4:
dependencies:
no-case: 3.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
dotenv@16.4.5: {}
@@ -6670,7 +6696,7 @@ snapshots:
header-case@2.0.4:
dependencies:
capital-case: 1.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
html-escaper@3.0.3: {}
@@ -6794,7 +6820,7 @@ snapshots:
is-lower-case@2.0.2:
dependencies:
- tslib: 2.6.3
+ tslib: 2.8.1
is-number@7.0.0: {}
@@ -6816,7 +6842,7 @@ snapshots:
is-upper-case@2.0.2:
dependencies:
- tslib: 2.6.3
+ tslib: 2.8.1
is-windows@1.0.2: {}
@@ -6849,6 +6875,8 @@ snapshots:
dependencies:
argparse: 2.0.1
+ jsbi@4.3.0: {}
+
jsesc@3.0.2: {}
json-buffer@3.0.1: {}
@@ -6948,11 +6976,11 @@ snapshots:
lower-case-first@2.0.2:
dependencies:
- tslib: 2.6.3
+ tslib: 2.8.1
lower-case@2.0.2:
dependencies:
- tslib: 2.6.3
+ tslib: 2.8.1
lru-cache@10.4.3: {}
@@ -7344,7 +7372,7 @@ snapshots:
no-case@3.0.4:
dependencies:
lower-case: 2.0.2
- tslib: 2.6.3
+ tslib: 2.8.1
node-fetch@2.7.0:
dependencies:
@@ -7466,7 +7494,7 @@ snapshots:
param-case@3.0.4:
dependencies:
dot-case: 3.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
parent-module@1.0.1:
dependencies:
@@ -7501,14 +7529,14 @@ snapshots:
pascal-case@3.1.2:
dependencies:
no-case: 3.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
path-browserify@1.0.1: {}
path-case@3.0.4:
dependencies:
dot-case: 3.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
path-exists@4.0.0: {}
@@ -7815,7 +7843,7 @@ snapshots:
sentence-case@3.0.4:
dependencies:
no-case: 3.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
upper-case-first: 2.0.2
server-destroy@1.0.1: {}
@@ -7902,7 +7930,7 @@ snapshots:
snake-case@3.0.4:
dependencies:
dot-case: 3.0.4
- tslib: 2.6.3
+ tslib: 2.8.1
source-map-js@1.2.1: {}
@@ -7910,7 +7938,7 @@ snapshots:
sponge-case@1.0.1:
dependencies:
- tslib: 2.6.3
+ tslib: 2.8.1
sprintf-js@1.0.3: {}
@@ -7971,7 +7999,7 @@ snapshots:
swap-case@2.0.2:
dependencies:
- tslib: 2.6.3
+ tslib: 2.8.1
text-table@0.2.0: {}
@@ -7989,7 +8017,7 @@ snapshots:
title-case@3.0.3:
dependencies:
- tslib: 2.6.3
+ tslib: 2.8.1
tmp@0.0.33:
dependencies:
@@ -8122,11 +8150,11 @@ snapshots:
upper-case-first@2.0.2:
dependencies:
- tslib: 2.6.3
+ tslib: 2.8.1
upper-case@2.0.2:
dependencies:
- tslib: 2.6.3
+ tslib: 2.8.1
uri-js@4.4.1:
dependencies:
diff --git a/prisma/migrations/20241111001803_milestones/migration.sql b/prisma/migrations/20241111001803_milestones/migration.sql
new file mode 100644
index 0000000..80741c1
--- /dev/null
+++ b/prisma/migrations/20241111001803_milestones/migration.sql
@@ -0,0 +1,27 @@
+-- CreateTable
+CREATE TABLE "Milestone" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "title" TEXT NOT NULL,
+ "dueOn" DATETIME
+);
+
+-- RedefineTables
+PRAGMA defer_foreign_keys=ON;
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_DesignReview" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "number" INTEGER NOT NULL,
+ "title" TEXT NOT NULL,
+ "body" TEXT NOT NULL,
+ "milestoneId" TEXT,
+ "created" DATETIME NOT NULL,
+ "updated" DATETIME NOT NULL,
+ "closed" DATETIME,
+ CONSTRAINT "DesignReview_milestoneId_fkey" FOREIGN KEY ("milestoneId") REFERENCES "Milestone" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);
+INSERT INTO "new_DesignReview" ("body", "closed", "created", "id", "number", "title", "updated") SELECT "body", "closed", "created", "id", "number", "title", "updated" FROM "DesignReview";
+DROP TABLE "DesignReview";
+ALTER TABLE "new_DesignReview" RENAME TO "DesignReview";
+CREATE UNIQUE INDEX "DesignReview_number_key" ON "DesignReview"("number");
+PRAGMA foreign_keys=ON;
+PRAGMA defer_foreign_keys=OFF;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 9aa4807..40f5237 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -18,6 +18,8 @@ model DesignReview {
/// The initial comment on the issue, in Markdown format.
body String
labels ReviewLabel[]
+ milestoneId String?
+ milestone Milestone? @relation(fields: [milestoneId], references: [id], onDelete: SetNull)
created DateTime
updated DateTime
closed DateTime?
@@ -35,6 +37,13 @@ model ReviewLabel {
@@unique([reviewId, labelId])
}
+model Milestone {
+ id String @id
+ title String
+ dueOn DateTime?
+ designReviews DesignReview[]
+}
+
/// Either an in-person multi-day meeting or a week of telecons.
model Meeting {
year Int
diff --git a/src/components/Markdown.astro b/src/components/Markdown.astro
new file mode 100644
index 0000000..abe2b65
--- /dev/null
+++ b/src/components/Markdown.astro
@@ -0,0 +1,17 @@
+---
+import { micromark } from "micromark";
+import { gfm, gfmHtml } from "micromark-extension-gfm";
+
+interface Props {
+ source: string;
+}
+
+const { source } = Astro.props;
+
+const html = micromark(source, {
+ extensions: [gfm()],
+ htmlExtensions: [gfmHtml()],
+});
+---
+
+
diff --git a/src/components/RelativeTime.astro b/src/components/RelativeTime.astro
new file mode 100644
index 0000000..4128e44
--- /dev/null
+++ b/src/components/RelativeTime.astro
@@ -0,0 +1,18 @@
+---
+interface Props {
+ datetime: Date;
+}
+const { datetime } = Astro.props;
+
+const defaultFormatter = Intl.DateTimeFormat("en-us", {
+ timeZone: "UTC",
+ dateStyle: "medium",
+});
+---
+
+
+{defaultFormatter.format(datetime)}
diff --git a/src/components/ReviewList.astro b/src/components/ReviewList.astro
new file mode 100644
index 0000000..024f68d
--- /dev/null
+++ b/src/components/ReviewList.astro
@@ -0,0 +1,21 @@
+---
+import type { DesignReview } from "@prisma/client";
+
+interface Props {
+ reviews: DesignReview[];
+}
+
+const { reviews } = Astro.props;
+---
+
+{
+ reviews
+ .toSorted((a, b) => a.number - b.number)
+ .map((review) => (
+
+
+ #{review.number}: {review.title}
+
+
+ ))
+}
diff --git a/src/gql/gql.ts b/src/gql/gql.ts
index 4afdef6..da0bef1 100644
--- a/src/gql/gql.ts
+++ b/src/gql/gql.ts
@@ -12,10 +12,11 @@ import * as types from './graphql';
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
+ * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
const documents = {
"query BlobContents($ids: [ID!]!) {\n nodes(ids: $ids) {\n __typename\n ... on Blob {\n id\n isTruncated\n isBinary\n text\n }\n }\n}": types.BlobContentsDocument,
- "query RecentDesignReviews($owner: String!, $repo: String!, $since: DateTime, $cursor: String) {\n repository(owner: $owner, name: $repo) {\n issues(\n states: [OPEN, CLOSED]\n first: 100\n filterBy: {since: $since}\n after: $cursor\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n number\n title\n body\n createdAt\n updatedAt\n closedAt\n labels(first: 100) {\n totalCount\n nodes {\n id\n name\n }\n }\n }\n }\n }\n}": types.RecentDesignReviewsDocument,
+ "query RecentDesignReviews($owner: String!, $repo: String!, $since: DateTime, $cursor: String) {\n repository(owner: $owner, name: $repo) {\n issues(\n states: [OPEN, CLOSED]\n first: 100\n filterBy: {since: $since}\n after: $cursor\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n number\n title\n body\n createdAt\n updatedAt\n closedAt\n labels(first: 100) {\n totalCount\n nodes {\n id\n name\n }\n }\n milestone {\n id\n title\n dueOn\n }\n }\n }\n }\n}": types.RecentDesignReviewsDocument,
"query ListMinutes($owner: String!, $repo: String!) {\n repository(owner: $owner, name: $repo) {\n defaultBranchRef {\n name\n }\n object(expression: \"HEAD:\") {\n __typename\n ... on Tree {\n entries {\n name\n object {\n __typename\n ... on Tree {\n entries {\n name\n object {\n __typename\n ... on Tree {\n entries {\n name\n path\n object {\n __typename\n ... on Blob {\n id\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n}": types.ListMinutesDocument,
};
@@ -26,7 +27,7 @@ export function graphql(source: "query BlobContents($ids: [ID!]!) {\n nodes(ids
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
-export function graphql(source: "query RecentDesignReviews($owner: String!, $repo: String!, $since: DateTime, $cursor: String) {\n repository(owner: $owner, name: $repo) {\n issues(\n states: [OPEN, CLOSED]\n first: 100\n filterBy: {since: $since}\n after: $cursor\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n number\n title\n body\n createdAt\n updatedAt\n closedAt\n labels(first: 100) {\n totalCount\n nodes {\n id\n name\n }\n }\n }\n }\n }\n}"): typeof import('./graphql').RecentDesignReviewsDocument;
+export function graphql(source: "query RecentDesignReviews($owner: String!, $repo: String!, $since: DateTime, $cursor: String) {\n repository(owner: $owner, name: $repo) {\n issues(\n states: [OPEN, CLOSED]\n first: 100\n filterBy: {since: $since}\n after: $cursor\n ) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n number\n title\n body\n createdAt\n updatedAt\n closedAt\n labels(first: 100) {\n totalCount\n nodes {\n id\n name\n }\n }\n milestone {\n id\n title\n dueOn\n }\n }\n }\n }\n}"): typeof import('./graphql').RecentDesignReviewsDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
diff --git a/src/gql/graphql.ts b/src/gql/graphql.ts
index 03089e4..35af7b3 100644
--- a/src/gql/graphql.ts
+++ b/src/gql/graphql.ts
@@ -31540,7 +31540,7 @@ export type RecentDesignReviewsQueryVariables = Exact<{
}>;
-export type RecentDesignReviewsQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', issues: { __typename?: 'IssueConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, nodes?: Array<{ __typename?: 'Issue', id: string, number: number, title: string, body: string, createdAt: any, updatedAt: any, closedAt?: any | null, labels?: { __typename?: 'LabelConnection', totalCount: number, nodes?: Array<{ __typename?: 'Label', id: string, name: string } | null> | null } | null } | null> | null } } | null };
+export type RecentDesignReviewsQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', issues: { __typename?: 'IssueConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null }, nodes?: Array<{ __typename?: 'Issue', id: string, number: number, title: string, body: string, createdAt: any, updatedAt: any, closedAt?: any | null, labels?: { __typename?: 'LabelConnection', totalCount: number, nodes?: Array<{ __typename?: 'Label', id: string, name: string } | null> | null } | null, milestone?: { __typename?: 'Milestone', id: string, title: string, dueOn?: any | null } | null } | null> | null } } | null };
export type ListMinutesQueryVariables = Exact<{
owner: Scalars['String']['input'];
@@ -31556,7 +31556,7 @@ export class TypedDocumentString
{
__apiType?: DocumentTypeDecoration['__apiType'];
- constructor(private value: string, public __meta__?: Record) {
+ constructor(private value: string, public __meta__?: Record | undefined) {
super(value);
}
@@ -31606,6 +31606,11 @@ export const RecentDesignReviewsDocument = new TypedDocumentString(`
name
}
}
+ milestone {
+ id
+ title
+ dueOn
+ }
}
}
}
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index 1efec6c..50ef698 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -1,6 +1,6 @@
---
interface Props {
- title: string;
+ title: string;
}
const { title } = Astro.props;
@@ -8,15 +8,28 @@ const { title } = Astro.props;
-
-
-
-
-
-
- {title}
-
-
-
-
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
diff --git a/src/lib/date.ts b/src/lib/date.ts
new file mode 100644
index 0000000..20ef26f
--- /dev/null
+++ b/src/lib/date.ts
@@ -0,0 +1,15 @@
+import { Temporal } from "@js-temporal/polyfill";
+
+/** Maps Saturday and Sunday to Monday of the next week, and the other days to the Monday that
+ * starts their week. */
+export function closestMonday(from: Temporal.PlainDate): Temporal.PlainDate {
+ return from.subtract({ days: ((from.dayOfWeek + 1) % 7) - 2 });
+}
+
+export function toPlainDateUTC(from: Date): Temporal.PlainDate {
+ return new Temporal.PlainDate(
+ from.getUTCFullYear(),
+ from.getUTCMonth() + 1,
+ from.getUTCDate(),
+ );
+}
diff --git a/src/lib/github/update.ts b/src/lib/github/update.ts
index c0f74e2..aa0373e 100644
--- a/src/lib/github/update.ts
+++ b/src/lib/github/update.ts
@@ -17,6 +17,9 @@ export async function updateDesignReviews(): Promise {
orderBy: { updated: "desc" },
select: { updated: true },
});
+ console.log(
+ `Querying design reviews after ${latestKnownReview?.updated.toISOString()}`,
+ );
const result = await pagedQuery(RecentDesignReviewsDocument, {
since: latestKnownReview?.updated,
owner: TAG_ORG,
@@ -29,6 +32,8 @@ export async function updateDesignReviews(): Promise {
if (!issues || issues.length === 0) {
return 0;
}
+ console.log(`Inserting ${issues.length} reviews.`);
+
await prisma.$transaction(
issues.map((issue) =>
prisma.designReview.upsert({
@@ -46,6 +51,18 @@ export async function updateDesignReviews(): Promise {
?.filter(notNull)
.map((label) => ({ label: label.name, labelId: label.id })),
},
+ milestone: issue.milestone
+ ? {
+ connectOrCreate: {
+ where: { id: issue.milestone.id },
+ create: {
+ id: issue.milestone.id,
+ dueOn: issue.milestone.dueOn as string | null,
+ title: issue.milestone.title,
+ },
+ },
+ }
+ : undefined,
},
update: {
title: issue.title,
@@ -69,6 +86,18 @@ export async function updateDesignReviews(): Promise {
update: {},
})),
},
+ milestone: issue.milestone
+ ? {
+ connectOrCreate: {
+ where: { id: issue.milestone.id },
+ create: {
+ id: issue.milestone.id,
+ dueOn: issue.milestone.dueOn as string | null,
+ title: issue.milestone.title,
+ },
+ },
+ }
+ : undefined,
},
select: null,
}),
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 640f1fa..c6abb31 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,11 +1,15 @@
---
-import { REVIEWS_REPO, TAG_ORG } from "astro:env/client";
+import { Temporal } from "@js-temporal/polyfill";
+import type { DesignReview } from "@prisma/client";
+import ReviewList from "../components/ReviewList.astro";
import Layout from "../layouts/Layout.astro";
+import { closestMonday, toPlainDateUTC } from "../lib/date";
import { prisma } from "../lib/prisma";
+import { setMapDefault } from "../lib/util";
const openReviews = await prisma.designReview.findMany({
where: { closed: null },
- orderBy: { created: "desc" },
+ include: { milestone: { select: { dueOn: true } } },
});
let serverEmpty = false;
@@ -13,6 +17,20 @@ let serverEmpty = false;
if (openReviews.length === 0) {
serverEmpty = (await prisma.designReview.count()) === 0;
}
+
+const thisMonday = closestMonday(Temporal.Now.plainDateISO("UTC"));
+const withMilestone = new Map();
+const withoutMilestone: DesignReview[] = [];
+for (const review of openReviews) {
+ if (review.milestone?.dueOn) {
+ const due = toPlainDateUTC(review.milestone.dueOn);
+ if (Temporal.PlainDate.compare(due, thisMonday) >= 0) {
+ setMapDefault(withMilestone, due.toString(), []).push(review);
+ continue;
+ }
+ }
+ withoutMilestone.push(review);
+}
---
@@ -27,19 +45,29 @@ if (openReviews.length === 0) {
No design reviews have been loaded into this server.
Try to load some from Github?
- ) : (
-
- {openReviews.map((review) => (
- -
-
- {review.title}
-
-
- ))}
-
- )
+ ) : null
+ }
+ {
+ Array.from(withMilestone.entries())
+ .sort(([m1], [m2]) => Temporal.PlainDate.compare(m1, m2))
+ .map(([milestone, reviews]) => (
+ <>
+ Scheduled for {milestone}
+
+
+
+ >
+ ))
+ }
+ {
+ withoutMilestone.length > 0 ? (
+ <>
+ Backlog
+
+
+
+ >
+ ) : null
}
diff --git a/src/pages/review/[number].astro b/src/pages/review/[number].astro
new file mode 100644
index 0000000..04fbba7
--- /dev/null
+++ b/src/pages/review/[number].astro
@@ -0,0 +1,125 @@
+---
+import { REVIEWS_REPO, TAG_ORG } from "astro:env/client";
+import Markdown from "../../components/Markdown.astro";
+import RelativeTime from "../../components/RelativeTime.astro";
+import ReviewList from "../../components/ReviewList.astro";
+import Layout from "../../layouts/Layout.astro";
+import { prisma } from "../../lib/prisma";
+
+const { number } = Astro.params;
+
+const parsedNumber = parseInt(number!);
+if (isNaN(parsedNumber)) {
+ return new Response(undefined, { status: 404 });
+}
+
+const review = await prisma.designReview.findUnique({
+ where: { number: parsedNumber },
+ include: {
+ labels: true,
+ milestone: true,
+ discussions: {
+ include: { meeting: true, proposedComments: true },
+ orderBy: [{ meetingYear: "asc" }, { meetingName: "asc" }],
+ },
+ },
+});
+
+if (review == null) {
+ return new Response(undefined, { status: 404 });
+}
+
+const sameWeek = await prisma.designReview.findMany({
+ where: {
+ milestoneId: review.milestoneId,
+ },
+ orderBy: { created: "desc" },
+});
+---
+
+
+
+
+
+ #{review.number}: {review.title}
+
+ Visit on Github.
+
+
+
+ Discussions
+ {
+ review.discussions.map((discussion) => (
+
+
+ {discussion.meetingYear}-{discussion.meetingName}
+
+ Minutes
+
+
+ ))
+ }
+
+
+
+
diff --git a/src/pages/webhook.ts b/src/pages/webhook.ts
index f33a97d..4b2830b 100644
--- a/src/pages/webhook.ts
+++ b/src/pages/webhook.ts
@@ -79,6 +79,31 @@ webhooks.on("issues.unlabeled", async ({ payload }) => {
});
});
+webhooks.on("issues.milestoned", async ({ payload }) => {
+ await prisma.designReview.update({
+ where: { id: payload.issue.node_id },
+ data: {
+ milestone: {
+ connectOrCreate: {
+ where: { id: payload.milestone.node_id },
+ create: {
+ id: payload.milestone.node_id,
+ dueOn: payload.milestone.due_on,
+ title: payload.milestone.title,
+ },
+ },
+ },
+ },
+ });
+});
+
+webhooks.on("issues.demilestoned", async ({ payload }) => {
+ await prisma.designReview.update({
+ where: { id: payload.issue.node_id },
+ data: { milestoneId: null },
+ });
+});
+
export async function handleWebHook(request: Request): Promise {
try {
await webhooks.verifyAndReceive({
diff --git a/test/date.test.ts b/test/date.test.ts
new file mode 100644
index 0000000..22d7525
--- /dev/null
+++ b/test/date.test.ts
@@ -0,0 +1,23 @@
+import { Temporal } from "@js-temporal/polyfill";
+import { expect, test } from "vitest";
+import { closestMonday, toPlainDateUTC } from "../src/lib/date";
+
+test("closestMonday", () => {
+ expect(
+ closestMonday(Temporal.PlainDate.from("2024-11-08")).toString(),
+ ).toEqual("2024-11-04");
+ for (let day = 9; day <= 15; day++) {
+ expect(
+ closestMonday(new Temporal.PlainDate(2024, 11, day)).toString(),
+ ).toEqual("2024-11-11");
+ }
+ expect(
+ closestMonday(Temporal.PlainDate.from("2024-11-16")).toString(),
+ ).toEqual("2024-11-18");
+});
+
+test("toPlainDateUTC", () => {
+ expect(
+ toPlainDateUTC(new Date("2024-05-11T00:00:00.000Z")).toString(),
+ ).toEqual("2024-05-11");
+});
diff --git a/test/webhook.test.ts b/test/webhook.test.ts
index e167723..c517d97 100644
--- a/test/webhook.test.ts
+++ b/test/webhook.test.ts
@@ -247,6 +247,7 @@ describe("issues", () => {
updated: new Date("2024-09-20T19:38:15"),
closed: null,
labels: [{ label: "Label 1" }, { label: "Label 2" }],
+ milestoneId: null,
} satisfies typeof result);
});
});
Opened
+