-
Notifications
You must be signed in to change notification settings - Fork 2
Preview Ideas in CMS (pt. 2/2) #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
e0552b2
fix ideas resource label in cms registry
interim17 72037f1
update IdeaPostPreview component to TS, add data normalization, guard…
interim17 c2ba5d2
get relational data from Decap's metadata channel
interim17 84e7c92
render idea preview without resolving relational widget data
interim17 69a857e
update IdeaPostPreview component to TS, add data normalization, guard…
interim17 c4d6823
get relational data from Decap's metadata channel
interim17 fac1e1d
Merge branch 'idea-preview-post-leave' of https://github.com/AllenCel…
interim17 c2661db
add time zone utc to new date
interim17 70da6e3
Merge branch 'idea-preview-non-relation' of https://github.com/AllenC…
interim17 8f04724
fix typo
interim17 57910b8
Merge branch 'main' of https://github.com/AllenCell/idea-board into i…
interim17 40c5a8e
Merge branch 'idea-preview-non-relation' of https://github.com/AllenC…
interim17 db2140f
Merge branch 'main' of https://github.com/AllenCell/idea-board into i…
interim17 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import React from "react"; | ||
|
|
||
| import { | ||
| IdeaPostTemplate, | ||
| IdeaPostTemplateProps, | ||
| } from "../../templates/idea-post"; | ||
| import { PreliminaryFindings } from "../../types"; | ||
| import { ImmutableLike, fromImmutable } from "../utils/immutable"; | ||
| import { | ||
| FieldsMetaData, | ||
| GetAsset, | ||
| resolveAllenite, | ||
| resolveFigures, | ||
| resolveRelatedIdea, | ||
| resolveRelationList, | ||
| resolveResource, | ||
| } from "../utils/resolvers"; | ||
|
|
||
| interface PreviewProps { | ||
| entry?: ImmutableLike; | ||
| value?: unknown; | ||
| fieldsMetaData?: FieldsMetaData; | ||
| getAsset?: GetAsset; | ||
| } | ||
|
|
||
| /** | ||
| * Normalize CMS form data into the shape IdeaPostTemplate expects. | ||
| * Decap gives us raw widget values which differ from resolved Gatsby data: | ||
| * - relation widgets return value_field strings, not resolved objects | ||
| * - single select widgets return a string, not an array | ||
| */ | ||
| function normalizeCmsData( | ||
| raw: Record<string, unknown>, | ||
| fieldsMetaData?: FieldsMetaData, | ||
| getAsset?: GetAsset, | ||
| ): Partial<IdeaPostTemplateProps> { | ||
| const v = raw as Partial<IdeaPostTemplateProps>; | ||
|
|
||
| // program: single select string → array | ||
| const program = v.program; | ||
| const normalizedProgram = program | ||
| ? Array.isArray(program) | ||
| ? program | ||
| : [String(program)] | ||
| : undefined; | ||
|
|
||
| // authors: relation gives ["name1", "name2"]; resolve each to its | ||
| // { name, contactId } node via metadata (falls back to the bare name). | ||
| const authors = resolveRelationList(v.authors, (name) => | ||
| resolveAllenite(fieldsMetaData, "authors", name), | ||
| ); | ||
|
|
||
| // primaryContact: single relation → a name string; resolve the same way. | ||
| const rawPrimaryContact = v.primaryContact; | ||
| const primaryContact = | ||
| typeof rawPrimaryContact === "string" | ||
| ? resolveAllenite( | ||
| fieldsMetaData, | ||
| "primaryContact", | ||
| rawPrimaryContact, | ||
| ) | ||
| : rawPrimaryContact; | ||
|
|
||
| // resources: relation gives slugs; resolve each to its flattened ResourceNode | ||
| // so MaterialsAndMethodsComponent can render (unhydrated entries dropped). | ||
| const resources = resolveRelationList(v.resources, (slug) => | ||
| resolveResource(fieldsMetaData, slug), | ||
| ); | ||
|
|
||
| // related_ideas: relation gives slugs; resolve each to { title, slug }. | ||
| const relatedIdeas = resolveRelationList(raw.related_ideas, (slug) => | ||
| resolveRelatedIdea(fieldsMetaData, slug), | ||
| ); | ||
|
|
||
| // preliminaryFindings.figures: uploaded images arrive as raw paths (no | ||
| // childImageSharp yet); resolve them to URLs so FigureGallery can render. | ||
| const rawFindings = raw.preliminaryFindings as | ||
| | Record<string, unknown> | ||
| | undefined; | ||
| const preliminaryFindings = rawFindings | ||
| ? ({ | ||
| ...rawFindings, | ||
| figures: resolveFigures(rawFindings.figures, getAsset), | ||
| } as unknown as PreliminaryFindings) | ||
| : (rawFindings as PreliminaryFindings | undefined); | ||
|
|
||
| // date: datetime widget returns a Date/object, template expects a string | ||
| const rawDate = raw.date; | ||
| const date = rawDate | ||
| ? typeof rawDate === "string" | ||
| ? rawDate | ||
| : new Date(rawDate as string | number).toLocaleDateString("en-US", { | ||
| year: "numeric", | ||
| month: "long", | ||
| day: "2-digit", | ||
| }) | ||
| : undefined; | ||
|
|
||
| return { | ||
| ...v, | ||
| authors, | ||
| date, | ||
| isPreview: true, | ||
| preliminaryFindings, | ||
| primaryContact, | ||
| program: normalizedProgram, | ||
| relatedIdeas, | ||
| resources, | ||
| }; | ||
| } | ||
|
|
||
| const IdeaPostPreview: React.FC<PreviewProps> = ({ | ||
| entry, | ||
| fieldsMetaData, | ||
| getAsset, | ||
| value, | ||
| }) => { | ||
| const raw = value ?? (entry?.get("data") as ImmutableLike | undefined); | ||
| const v = fromImmutable<Record<string, unknown>>(raw) ?? {}; | ||
| return ( | ||
| <> | ||
| <div | ||
| style={{ | ||
| background: "#fffbe6", | ||
| border: "1px solid #ffe58f", | ||
| borderRadius: 4, | ||
| color: "#874d00", | ||
| fontSize: 12, | ||
| margin: 8, | ||
| padding: "6px 12px", | ||
| }} | ||
| > | ||
| Previews are approximate/under development — content and styling | ||
| may differ from production, and not all functionality will be | ||
| available. | ||
| </div> | ||
| <IdeaPostTemplate | ||
| {...(normalizeCmsData( | ||
| v, | ||
| fieldsMetaData, | ||
| getAsset, | ||
| ) as IdeaPostTemplateProps)} | ||
| /> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default IdeaPostPreview; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import { ResourceNode } from "../../types"; | ||
| import { fromImmutable } from "./immutable"; | ||
|
|
||
| /** Decap's Immutable metadata side-channel, keyed [field, collection, value]. */ | ||
| export interface FieldsMetaData { | ||
| getIn: (path: string[]) => unknown; | ||
| } | ||
|
|
||
| /** | ||
| * Relation widgets (referenced via dropdowns in the CMS) need their cotent resolved | ||
| * via Decap's metadata side-channel. | ||
| * | ||
| * Relation widgets store only the value_field (a name or slug) in entry data; | ||
| * Decap stashes the full referenced node in `fieldsMetaData` under | ||
| * [field, collection, value]. This is populated asynchronously — on opening an | ||
| * existing entry the relation control hydrates the selected values — so this | ||
| * returns null until the lookup resolves. Each caller supplies the shape its | ||
| * template needs and decides how to handle null (fall back, or filter out). | ||
| */ | ||
| export function resolveRelation( | ||
| fieldsMetaData: FieldsMetaData | undefined, | ||
| field: string, | ||
| collection: string, | ||
| value: string, | ||
| ): Record<string, unknown> | null { | ||
| return fromImmutable<Record<string, unknown>>( | ||
| fieldsMetaData?.getIn([field, collection, value]), | ||
| ); | ||
| } | ||
|
|
||
|
Comment on lines
+1
to
+30
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file is primarily agent written. We are taking string identifiers like "allenite" for known relation widgets and getting the data out of Decap's |
||
| /** | ||
| * Map an array of relation values (value_field strings) through a resolver, | ||
| * dropping entries that haven't hydrated yet (resolver returned null). Decap | ||
| * hands relation values as an array of value_field strings in the preview, so | ||
| * non-string entries are ignored. | ||
| */ | ||
| export function resolveRelationList<T>( | ||
| values: unknown, | ||
| resolve: (value: string) => T | null, | ||
| ): T[] | undefined { | ||
| if (!Array.isArray(values)) return undefined; | ||
| return values | ||
| .filter((v): v is string => typeof v === "string") | ||
| .map(resolve) | ||
| .filter((v): v is T => v != null); | ||
| } | ||
|
|
||
| /** The subset of an allenite node the idea post template actually consumes. */ | ||
| export type ResolvedAllenite = { name: string; contactId: string | null }; | ||
|
|
||
| /** allenite relation (name) → { name, contactId }; falls back to the bare name. */ | ||
| export function resolveAllenite( | ||
| fieldsMetaData: FieldsMetaData | undefined, | ||
| field: string, | ||
| name: string, | ||
| ): ResolvedAllenite { | ||
| const node = resolveRelation(fieldsMetaData, field, "allenite", name); | ||
| return { | ||
| name: typeof node?.name === "string" ? node.name : name, | ||
| contactId: typeof node?.contactId === "string" ? node.contactId : null, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * resource relation (slug) → flattened ResourceNode. The full frontmatter lives | ||
| * in metadata as `{ name, resourceDetails: {...} }`; Gatsby flattens | ||
| * resourceDetails up onto the node (see createNode in gatsby-node.js), so we | ||
| * mirror that. Returns null until hydrated; the caller filters those out. | ||
| */ | ||
| export function resolveResource( | ||
| fieldsMetaData: FieldsMetaData | undefined, | ||
| slug: string, | ||
| ): ResourceNode | null { | ||
| const node = resolveRelation( | ||
| fieldsMetaData, | ||
| "resources", | ||
| "resources", | ||
| slug, | ||
| ); | ||
| if (!node) return null; | ||
| const { resourceDetails, ...rest } = node; | ||
| const details = | ||
| resourceDetails && typeof resourceDetails === "object" | ||
| ? (resourceDetails as Record<string, unknown>) | ||
| : {}; | ||
| return { ...rest, ...details, slug } as unknown as ResourceNode; | ||
| } | ||
|
|
||
| /** | ||
| * Decap's preview asset resolver. Maps a stored media path to a browser-usable | ||
| * URL, transparently handling both committed public paths (e.g. "/img/x.png") | ||
| * and freshly-uploaded, not-yet-committed files (returned as blob URLs). Returns | ||
| * an AssetProxy whose toString() is the URL. | ||
| */ | ||
| export type GetAsset = (path: string) => { toString: () => string }; | ||
|
|
||
| /** | ||
| * Normalize a figure list's uploaded images for preview. | ||
| * | ||
| * In production, uploaded figures are processed by gatsby-transformer-sharp into | ||
| * `file.childImageSharp`; the preview only has the raw upload path in `file`. | ||
| * We resolve that path through getAsset and expose it as `url` — the shape | ||
| * FigureGallery renders with a plain <img>. Figures that already have an external | ||
| * `url` render as-is and pass through untouched. | ||
| */ | ||
| export function resolveFigures( | ||
| figures: unknown, | ||
| getAsset: GetAsset | undefined, | ||
| ): Record<string, unknown>[] | undefined { | ||
| if (!Array.isArray(figures)) return undefined; | ||
| return figures.map((figure) => { | ||
| const f = (figure ?? {}) as Record<string, unknown>; | ||
| if (!f.url && getAsset && typeof f.file === "string" && f.file) { | ||
| return { ...f, url: getAsset(f.file).toString() }; | ||
| } | ||
| return f; | ||
| }); | ||
| } | ||
|
|
||
| /** The subset of a related idea the template renders (title + routing slug). */ | ||
| type ResolvedRelatedIdea = { title: string; slug: string }; | ||
|
|
||
| /** | ||
| * related_ideas relation (slug) → { title, slug }. The stored value is the CMS | ||
| * slug (not the Gatsby routing path), so links can't navigate inside the preview | ||
| * iframe — the template renders these as plain text under isPreview. Falls back | ||
| * to the slug as the label until the title hydrates. | ||
| */ | ||
| export function resolveRelatedIdea( | ||
| fieldsMetaData: FieldsMetaData | undefined, | ||
| slug: string, | ||
| ): ResolvedRelatedIdea { | ||
| const node = resolveRelation( | ||
| fieldsMetaData, | ||
| "related_ideas", | ||
| "ideas", | ||
| slug, | ||
| ); | ||
| return { | ||
| title: typeof node?.title === "string" ? node.title : slug, | ||
| slug, | ||
| }; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tiniest: "cotent" --> "content"