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) => ( -
    1. - - {review.title} - -
    2. - ))} -
    - ) + ) : 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. +

    +
    + Opened + +
    + +

    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); }); });