Conversation
…uild for the author drop down
There was a problem hiding this comment.
Pull request overview
This pull request implements a global author management system by integrating with the SSW.People.Profiles repository. The changes replace the previous author storage format (storing full name and URL) with a slug-based system that references a centrally maintained people index.
Changes:
- Introduces a new Author type that stores only author slugs, replacing the previous object structure with title and url fields
- Adds a people index generation script that fetches profiles from SSW.People.Profiles and creates a local JSON index
- Implements custom TinaCMS field component (AuthorSelector) with searchable dropdown UI for selecting authors from the global people list
Reviewed changes
Copilot reviewed 16 out of 18 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| types/author.ts | New type definition for author references containing just the slug |
| types/rule.ts | Updated to use the new Author type |
| models/Rule.ts | Updated to use the new Author type for type consistency |
| scripts/generate-people-index.js | New script to fetch and generate people index from GitHub |
| tina/fields/AuthorSelector.tsx | New custom field component for selecting authors with search functionality |
| tina/collection/rule.tsx | Updated rule schema to use new author field structure |
| tina/queries/queries.gql | Updated GraphQL queries to reference author slugs instead of title/url |
| tina/tina-lock.json | Schema lock file update reflecting the structural changes |
| lib/people/index.ts | Server-side utilities for resolving author slugs to person data |
| lib/people/usePeople.ts | Client-side hooks for author resolution |
| components/AuthorsCard.tsx | Refactored to resolve author slugs to full person data |
| app/user/client-page.tsx | Updated to use author slugs instead of names |
| app/api/people/route.ts | New API endpoint to serve the people index |
| app/api/tina/rules-by-author/route.ts | Updated to query by authorSlug instead of authorTitle |
| app/api/rules/by-guid/route.ts | Updated author data mapping |
| package.json | Added new npm script for generating people index |
| .gitignore | Added people-index.json and people folder to ignore list |
| .github/workflows/build-artifacts.yml | Added step to generate people index during build |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export interface Author { | ||
| author?: string | null; |
There was a problem hiding this comment.
The field name author within the Author interface is confusing and redundant. Consider renaming the field to slug or authorSlug for better clarity, since it's storing the author's slug, not the full author object. This would make the code more self-documenting.
| export interface Author { | |
| author?: string | null; | |
| export interface Author { | |
| /** | |
| * @deprecated Use `authorSlug` instead. | |
| */ | |
| author?: string | null; | |
| authorSlug?: string | null; |
| function nameToSlug(name) { | ||
| return name | ||
| .toLowerCase() | ||
| .replace(/['']/g, "") | ||
| .replace(/[^a-z0-9]+/g, "-") | ||
| .replace(/^-+|-+$/g, ""); |
There was a problem hiding this comment.
The nameToSlug function may produce duplicate slugs for names that differ only by special characters or apostrophes. For example, "O'Brien" and "OBrien" would both become "obrien". Consider maintaining a set of generated slugs and appending a numeric suffix when duplicates are detected to ensure uniqueness, or logging warnings when duplicate slugs are generated so they can be manually reviewed.
| function nameToSlug(name) { | |
| return name | |
| .toLowerCase() | |
| .replace(/['']/g, "") | |
| .replace(/[^a-z0-9]+/g, "-") | |
| .replace(/^-+|-+$/g, ""); | |
| const generatedSlugs = new Set(); | |
| function nameToSlug(name) { | |
| const baseSlug = name | |
| .toLowerCase() | |
| .replace(/['']/g, "") | |
| .replace(/[^a-z0-9]+/g, "-") | |
| .replace(/^-+|-+$/g, ""); | |
| let slug = baseSlug; | |
| let counter = 2; | |
| if (generatedSlugs.has(slug)) { | |
| // Append a numeric suffix to ensure uniqueness | |
| while (generatedSlugs.has(slug)) { | |
| slug = `${baseSlug}-${counter}`; | |
| counter += 1; | |
| } | |
| console.warn( | |
| `Duplicate slug detected for name "${name}". Using unique slug "${slug}".` | |
| ); | |
| } | |
| generatedSlugs.add(slug); | |
| return slug; |
| <div className="relative z-1000"> | ||
| <input type="hidden" id={input.name} {...input} /> | ||
| <Popover> | ||
| {({ open }) => ( | ||
| <> | ||
| <PopoverButton className="text-sm h-11 px-4 justify-between w-full bg-white border border-gray-200 rounded-full hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors flex items-center"> | ||
| <span className="flex items-center gap-2 truncate"> | ||
| <BiUser className="w-4 h-4 text-gray-400 shrink-0" /> | ||
| <span className="truncate" title={selectedAuthorLabel || undefined}> | ||
| {selectedAuthorLabel || "Select an author"} | ||
| </span> | ||
| </span> | ||
| <BiChevronDown className={`w-4 h-4 transition-transform ${open ? "rotate-180" : ""}`} /> | ||
| </PopoverButton> | ||
| <div className="absolute inset-x-0 -bottom-2 translate-y-full z-1000"> |
There was a problem hiding this comment.
The z-1000 class is used twice (lines 154 and 168) which may cause z-index issues. The z-1000 is not a standard Tailwind class (Tailwind's z-index scale typically goes up to z-50). Consider using a proper z-index value that's defined in your Tailwind configuration or use standard values like z-50. Also, applying z-1000 to both the parent container and the dropdown may be redundant.
| return { | ||
| slug: profile.slug || profile.fullName?.toLowerCase().replace(/\s+/g, "-") || "", | ||
| fullName: profile.fullName || "", | ||
| }; |
There was a problem hiding this comment.
The resolveAuthor function has a fallback slug generation that may not match the slug format used in the people index. The fallback profile.fullName?.toLowerCase().replace(/\s+/g, "-") is a simple conversion, but the actual slug generation in scripts/generate-people-index.js uses the nameToSlug function which also removes apostrophes and handles special characters differently. This inconsistency could lead to slug mismatches. Consider using a shared utility function for slug generation or ensuring the API response includes the correct slug.
| import { type NextRequest, NextResponse } from "next/server"; | ||
| import path from "path"; | ||
|
|
||
| export const dynamic = "force-static"; |
There was a problem hiding this comment.
The dynamic = "force-static" export combined with revalidate = 3600 may not work as expected. When you use force-static, Next.js will try to statically generate this route at build time, but the route depends on people-index.json which is generated during the build process. If the file doesn't exist at build time, the route will return an empty object. Consider using dynamic = "force-dynamic" or removing the dynamic export to allow Next.js to handle caching appropriately with ISR (Incremental Static Regeneration) via the revalidate setting.
| export const dynamic = "force-static"; |
| - name: Generate people index | ||
| run: | | ||
| npm ci | ||
| npm run generate:people |
There was a problem hiding this comment.
Running npm ci in the workflow installs all dependencies, which can be time-consuming and unnecessary if only the people index generation script needs to run. Consider either:
- Adding a caching step for node_modules to speed up the workflow
- Installing only the required dependencies (gray-matter) if the script doesn't need the full dependency tree
Example caching approach:
- name: Cache node_modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}| useEffect(() => { | ||
| if (resolvedAuthors.length > 0) { | ||
| setImgSrcList(resolvedAuthors.map(getImgSource)); | ||
| } | ||
| }, [resolvedAuthors, getImgSource]); | ||
|
|
||
| useEffect(() => { | ||
| if (resolvedAuthors.length > 0) { | ||
| resolvedAuthors.forEach((author) => { | ||
| if (!author.title) { | ||
| console.warn(`Profile title is missing for author with URL: ${author.url}`); | ||
| } | ||
| }); | ||
| if (displayAuthors.length > 0) { | ||
| setImgSrcList(displayAuthors.map((a) => a.imageUrl || placeholderImg)); | ||
| } | ||
| }, [resolvedAuthors]); | ||
| }, [displayAuthors, placeholderImg]); |
There was a problem hiding this comment.
The imageUrl is already set to placeholderImg when building displayAuthors (line 33), so checking for it again in the effect (line 41) and when rendering (lines 83, 96) is redundant. You can simplify the logic since displayAuthors[i].imageUrl will never be falsy. This current implementation could cause confusion and unnecessary defensive checks.
| isArchived?: boolean; | ||
| archivedreason?: string; | ||
| authors?: ({ title?: string | null; url?: string | null } | null)[] | null; | ||
| authors?: (Author | null)[] | null; |
There was a problem hiding this comment.
The Author type is referenced but not imported. You need to add an import statement at the top of this file:
import { Author } from "@/types/author";Without this import, TypeScript will fail to compile as it cannot resolve the Author type.
| function nameToSlug(name) { | ||
| return name | ||
| .toLowerCase() | ||
| .replace(/['']/g, "") |
There was a problem hiding this comment.
Character ''' is repeated in the same character class.
| .replace(/['']/g, "") | |
| .replace(/'/g, "") |
Bump tina packages to latest
#2429) * Update github action to use the current branch name as the tina branch * Pass in tina branch from pr-open
* Initial plan * Change SEO Description field to textarea with 3 rows Co-authored-by: Aibono1225 <127192800+Aibono1225@users.noreply.github.com> * Complete SEO Description textarea implementation Co-authored-by: Aibono1225 <127192800+Aibono1225@users.noreply.github.com> * Remove test rule file * Revert changes to extensions.json * tina-lock * update tina lock * update lock * bypass Tina UIField typing for textarea props --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Aibono1225 <127192800+Aibono1225@users.noreply.github.com> Co-authored-by: Chloe Lin <chloelin@ssw.com.au> Co-authored-by: Jake Bayliss <bayliss.jw@gmail.com>
Description
Adding global author list using SSW.People.Profiles.
This PR generates a new people index json file which we use as the source of truth for the author drop down list
Screenshot (optional)