From fa9903f0cd84c3a2a4ef72810d038273f0d89010 Mon Sep 17 00:00:00 2001 From: runarvestmann Date: Mon, 23 Sep 2024 16:09:12 +0000 Subject: [PATCH 1/4] Change folder structure --- .../{lists => editors}/ContentfulField.tsx | 8 +- .../TeamMemberEditor/TeamMemberEditor.tsx | 86 ++++++++ .../TeamMemberFilterTagsField.tsx | 189 ++++++++++++++++++ .../GenericListEditor/GenericListEditor.tsx | 6 +- .../GenericListItemEditor.tsx | 6 +- .../components/{lists => editors}/utils.ts | 0 .../pages/editors/generic-list-editor.ts | 2 +- .../pages/editors/generic-list-item-editor.ts | 2 +- .../pages/editors/team-member-editor.ts | 3 + 9 files changed, 293 insertions(+), 9 deletions(-) rename apps/contentful-apps/components/{lists => editors}/ContentfulField.tsx (94%) create mode 100644 apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx create mode 100644 apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberFilterTagsField.tsx rename apps/contentful-apps/components/{ => editors}/lists/GenericListEditor/GenericListEditor.tsx (97%) rename apps/contentful-apps/components/{ => editors}/lists/GenericListItemEditor/GenericListItemEditor.tsx (93%) rename apps/contentful-apps/components/{lists => editors}/utils.ts (100%) create mode 100644 apps/contentful-apps/pages/editors/team-member-editor.ts diff --git a/apps/contentful-apps/components/lists/ContentfulField.tsx b/apps/contentful-apps/components/editors/ContentfulField.tsx similarity index 94% rename from apps/contentful-apps/components/lists/ContentfulField.tsx rename to apps/contentful-apps/components/editors/ContentfulField.tsx index 723147b9cfad..031da947f28f 100644 --- a/apps/contentful-apps/components/lists/ContentfulField.tsx +++ b/apps/contentful-apps/components/editors/ContentfulField.tsx @@ -5,14 +5,16 @@ import { Box, FormControl, Text } from '@contentful/f36-components' import { mapLocalesToFieldApis } from './utils' -export const ContentfulField = (props: { +export interface ContentfulFieldProps { sdk: EditorExtensionSDK localeToFieldMapping: Record> - fieldID: keyof typeof props.localeToFieldMapping + fieldID: keyof ContentfulFieldProps['localeToFieldMapping'] displayName: string widgetId?: string helpText?: string -}) => { +} + +export const ContentfulField = (props: ContentfulFieldProps) => { const availableLocales = useMemo(() => { const validLocales = props.sdk.locales.available.filter( (locale) => props.localeToFieldMapping[props.fieldID]?.[locale], diff --git a/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx new file mode 100644 index 000000000000..d41920d8d300 --- /dev/null +++ b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx @@ -0,0 +1,86 @@ +import { useMemo } from 'react' +import dynamic from 'next/dynamic' +import type { EditorExtensionSDK } from '@contentful/app-sdk' +import { Box } from '@contentful/f36-components' +import { useSDK } from '@contentful/react-apps-toolkit' + +import { mapLocalesToFieldApis } from '../utils' +import { TeamMemberFilterTagsField } from './TeamMemberFilterTagsField' + +const ContentfulField = dynamic( + () => + // Dynamically import via client side rendering since the @contentful/default-field-editors package accesses the window and navigator global objects + import('../ContentfulField').then(({ ContentfulField }) => ContentfulField), + { + ssr: false, + }, +) + +const createField = (name: string, sdk: EditorExtensionSDK) => { + return mapLocalesToFieldApis(sdk.entry.fields[name].locales, sdk, name) +} + +const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { + return { + name: createField('name', sdk), + title: createField('title', sdk), + image: createField('mynd', sdk), + imageOnSelect: createField('imageOnSelect', sdk), + filterTags: createField('filterTags', sdk), + email: createField('email', sdk), + phone: createField('phone', sdk), + intro: createField('intro', sdk), + } +} + +export const TeamMemberEditor = () => { + const sdk = useSDK() + + const localeToFieldMapping = useMemo(() => { + return createLocaleToFieldMapping(sdk) + }, [sdk]) + + return ( + + + + + + + + ) +} diff --git a/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberFilterTagsField.tsx b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberFilterTagsField.tsx new file mode 100644 index 000000000000..2a3994a7fffa --- /dev/null +++ b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberFilterTagsField.tsx @@ -0,0 +1,189 @@ +import { useEffect, useState } from 'react' +import { useDebounce } from 'react-use' +import { + CollectionProp, + EntryProps, + KeyValueMap, + QueryOptions, + SysLink, +} from 'contentful-management' +import type { CMAClient, EditorExtensionSDK } from '@contentful/app-sdk' +import { Checkbox, Spinner, Stack, Text } from '@contentful/f36-components' +import { useCMA } from '@contentful/react-apps-toolkit' + +import { sortAlpha } from '@island.is/shared/utils' + +const DEBOUNCE_TIME = 500 + +const fetchAll = async (cma: CMAClient, query: QueryOptions) => { + let response: CollectionProp> | null = null + const items: EntryProps[] = [] + let limit = 100 + + while ((response === null || items.length < response.total) && limit > 0) { + try { + response = await cma.entry.getMany({ + query: { + ...query, + limit, + skip: items.length, + }, + }) + items.push(...response.items) + } catch (error) { + const isResponseTooBig = (error?.message as string) + ?.toLowerCase() + ?.includes('response size too big') + + if (isResponseTooBig) limit = Math.floor(limit / 2) + else throw error + } + } + + return items +} + +interface TeamMemberFilterTagsField { + sdk: EditorExtensionSDK +} + +export const TeamMemberFilterTagsField = ({ + sdk, +}: TeamMemberFilterTagsField) => { + const cma = useCMA() + const [isLoading, setIsLoading] = useState(true) + + const [filterTagSysLinks, setFilterTagSysLinks] = useState( + sdk.entry.fields['filterTags']?.getValue() ?? [], + ) + + const [tagGroups, setTagGroups] = useState< + { + tagGroup: EntryProps + tags: EntryProps[] + }[] + >([]) + + useEffect(() => { + const fetchTeamList = async () => { + try { + const teamListResponse = await cma.entry.getMany({ + query: { + links_to_entry: sdk.entry.getSys().id, + content_type: 'teamList', + }, + }) + + if (teamListResponse.items.length === 0) { + setIsLoading(false) + return + } + + const tagGroupSysLinks: SysLink[] = + teamListResponse.items[0].fields.filterGroups?.[ + sdk.locales.default + ] ?? [] + + const promises = tagGroupSysLinks.map(async (tagGroupSysLink) => { + const [tagGroup, tags] = await Promise.all([ + cma.entry.get({ + entryId: tagGroupSysLink.sys.id, + }), + fetchAll(cma, { + links_to_entry: tagGroupSysLink.sys.id, + content_type: 'genericTag', + }), + ]) + + tags.sort((a, b) => { + return sortAlpha(sdk.locales.default)( + a.fields.title, + b.fields.title, + ) + }) + + return { tagGroup, tags } + }) + + setTagGroups(await Promise.all(promises)) + } finally { + setIsLoading(false) + } + } + + fetchTeamList() + }, [cma, sdk.entry, sdk.locales.default, setTagGroups]) + + useDebounce( + () => { + sdk.entry.fields['filterTags']?.setValue(filterTagSysLinks) + }, + DEBOUNCE_TIME, + [filterTagSysLinks], + ) + + return ( + <> + {isLoading && } + + {!isLoading && ( + + {tagGroups.map(({ tagGroup, tags }) => { + return ( + + + {tagGroup.fields.title[sdk.locales.default]} + + + {tags.map((tag) => { + const isChecked = filterTagSysLinks.some( + (filterTagSysLink) => + filterTagSysLink.sys.id === tag.sys.id, + ) + return ( + { + setFilterTagSysLinks((prev) => { + const alreadyExists = prev.some( + (filterTagSysLink) => + filterTagSysLink.sys.id === tag.sys.id, + ) + if (alreadyExists) { + return prev.filter( + (filterTagSysLink) => + filterTagSysLink.sys.id !== tag.sys.id, + ) + } + return prev.concat({ + sys: { + id: tag.sys.id, + type: 'Link', + linkType: 'Entry', + }, + }) + }) + }} + > + {tag.fields.title[sdk.locales.default]} + + ) + })} + + + ) + })} + + )} + + ) +} diff --git a/apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx b/apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx similarity index 97% rename from apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx rename to apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx index ec933a66f49d..0740aff1d59d 100644 --- a/apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx +++ b/apps/contentful-apps/components/editors/lists/GenericListEditor/GenericListEditor.tsx @@ -17,7 +17,7 @@ import { import { PlusIcon } from '@contentful/f36-icons' import { useCMA, useSDK } from '@contentful/react-apps-toolkit' -import { mapLocalesToFieldApis } from '../utils' +import { mapLocalesToFieldApis } from '../../utils' const SEARCH_DEBOUNCE_TIME_IN_MS = 300 const LIST_ITEM_CONTENT_TYPE_ID = 'genericListItem' @@ -46,7 +46,9 @@ const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { const ContentfulField = dynamic( () => // Dynamically import via client side rendering since the @contentful/default-field-editors package accesses the window and navigator global objects - import('../ContentfulField').then(({ ContentfulField }) => ContentfulField), + import('../../ContentfulField').then( + ({ ContentfulField }) => ContentfulField, + ), { ssr: false, }, diff --git a/apps/contentful-apps/components/lists/GenericListItemEditor/GenericListItemEditor.tsx b/apps/contentful-apps/components/editors/lists/GenericListItemEditor/GenericListItemEditor.tsx similarity index 93% rename from apps/contentful-apps/components/lists/GenericListItemEditor/GenericListItemEditor.tsx rename to apps/contentful-apps/components/editors/lists/GenericListItemEditor/GenericListItemEditor.tsx index c5bd0b45d6d9..270660b6d17e 100644 --- a/apps/contentful-apps/components/lists/GenericListItemEditor/GenericListItemEditor.tsx +++ b/apps/contentful-apps/components/editors/lists/GenericListItemEditor/GenericListItemEditor.tsx @@ -4,7 +4,7 @@ import { EditorExtensionSDK } from '@contentful/app-sdk' import { Box } from '@contentful/f36-components' import { useSDK } from '@contentful/react-apps-toolkit' -import { mapLocalesToFieldApis } from '../utils' +import { mapLocalesToFieldApis } from '../../utils' const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { return { @@ -26,7 +26,9 @@ const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { const ContentfulField = dynamic( () => // Dynamically import via client side rendering since the @contentful/default-field-editors package accesses the window and navigator global objects - import('../ContentfulField').then(({ ContentfulField }) => ContentfulField), + import('../../ContentfulField').then( + ({ ContentfulField }) => ContentfulField, + ), { ssr: false, }, diff --git a/apps/contentful-apps/components/lists/utils.ts b/apps/contentful-apps/components/editors/utils.ts similarity index 100% rename from apps/contentful-apps/components/lists/utils.ts rename to apps/contentful-apps/components/editors/utils.ts diff --git a/apps/contentful-apps/pages/editors/generic-list-editor.ts b/apps/contentful-apps/pages/editors/generic-list-editor.ts index a701c779c83e..5be02cb3ca50 100644 --- a/apps/contentful-apps/pages/editors/generic-list-editor.ts +++ b/apps/contentful-apps/pages/editors/generic-list-editor.ts @@ -1,3 +1,3 @@ -import { GenericListEditor } from '../../components/lists/GenericListEditor/GenericListEditor' +import { GenericListEditor } from '../../components/editors/lists/GenericListEditor/GenericListEditor' export default GenericListEditor diff --git a/apps/contentful-apps/pages/editors/generic-list-item-editor.ts b/apps/contentful-apps/pages/editors/generic-list-item-editor.ts index f59cc2153543..e499a338b821 100644 --- a/apps/contentful-apps/pages/editors/generic-list-item-editor.ts +++ b/apps/contentful-apps/pages/editors/generic-list-item-editor.ts @@ -1,3 +1,3 @@ -import { GenericListItemEditor } from '../../components/lists/GenericListItemEditor/GenericListItemEditor' +import { GenericListItemEditor } from '../../components/editors/lists/GenericListItemEditor/GenericListItemEditor' export default GenericListItemEditor diff --git a/apps/contentful-apps/pages/editors/team-member-editor.ts b/apps/contentful-apps/pages/editors/team-member-editor.ts new file mode 100644 index 000000000000..457e78592a66 --- /dev/null +++ b/apps/contentful-apps/pages/editors/team-member-editor.ts @@ -0,0 +1,3 @@ +import { TeamMemberEditor } from '../../components/editors/TeamMemberEditor/TeamMemberEditor' + +export default TeamMemberEditor From 6bc1c8acbd02c951b359cd67e019c36bae204aa3 Mon Sep 17 00:00:00 2001 From: runarvestmann Date: Tue, 1 Oct 2024 15:13:46 +0000 Subject: [PATCH 2/4] Hide fields when not accordion variant --- .../TeamMemberEditor/TeamMemberEditor.tsx | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx index d41920d8d300..fedcf53f29ce 100644 --- a/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx +++ b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx @@ -1,8 +1,9 @@ -import { useMemo } from 'react' +import { useEffect, useMemo, useState } from 'react' +import type { EntryProps, KeyValueMap } from 'contentful-management' import dynamic from 'next/dynamic' import type { EditorExtensionSDK } from '@contentful/app-sdk' import { Box } from '@contentful/f36-components' -import { useSDK } from '@contentful/react-apps-toolkit' +import { useCMA, useSDK } from '@contentful/react-apps-toolkit' import { mapLocalesToFieldApis } from '../utils' import { TeamMemberFilterTagsField } from './TeamMemberFilterTagsField' @@ -35,11 +36,28 @@ const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { export const TeamMemberEditor = () => { const sdk = useSDK() + const cma = useCMA() const localeToFieldMapping = useMemo(() => { return createLocaleToFieldMapping(sdk) }, [sdk]) + const [teamList, setTeamList] = useState>() + + useEffect(() => { + const fetchTeamList = async () => { + const teamLists = await cma.entry.getMany({ + query: { + content_type: 'teamList', + links_to_entry: sdk.entry.getSys().id, + }, + }) + if (teamLists.items.length > 0) setTeamList(teamLists.items[0]) + } + + fetchTeamList() + }, [cma.entry, sdk.entry]) + return ( { /> { sdk={sdk} widgetId="assetLinkEditor" /> + {teamList?.fields?.variant?.[sdk.locales.default] === 'accordion' && ( + + + + + + )} ) From c9d222b728f5589d41eb0beb203420eab5a36d33 Mon Sep 17 00:00:00 2001 From: runarvestmann Date: Wed, 2 Oct 2024 08:44:57 +0000 Subject: [PATCH 3/4] Stop displaying column if there is no image --- libs/cms/src/lib/models/teamMember.model.ts | 6 ++--- .../contentful/src/lib/TeamList/TeamList.tsx | 26 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/libs/cms/src/lib/models/teamMember.model.ts b/libs/cms/src/lib/models/teamMember.model.ts index c6aa3916d28e..d34b47845c11 100644 --- a/libs/cms/src/lib/models/teamMember.model.ts +++ b/libs/cms/src/lib/models/teamMember.model.ts @@ -34,8 +34,8 @@ export class TeamMember { @Field({ nullable: true }) phone?: string - @CacheField(() => Image) - image!: Image + @CacheField(() => Image, { nullable: true }) + image!: Image | null @CacheField(() => Image, { nullable: true }) imageOnSelect?: Image | null @@ -89,7 +89,7 @@ export const mapTeamMember = ({ fields, sys }: ITeamMember): TeamMember => { id: sys.id, name: fields.name ?? '', title: fields.title ?? '', - image: mapImage(fields.mynd), + image: fields.mynd ? mapImage(fields.mynd) : null, imageOnSelect: fields.imageOnSelect ? mapImage(fields.imageOnSelect) : null, filterTags, intro: fields.intro ? mapDocument(fields.intro, `${sys.id}:intro`) : [], diff --git a/libs/island-ui/contentful/src/lib/TeamList/TeamList.tsx b/libs/island-ui/contentful/src/lib/TeamList/TeamList.tsx index 56459921c009..79a3fa813483 100644 --- a/libs/island-ui/contentful/src/lib/TeamList/TeamList.tsx +++ b/libs/island-ui/contentful/src/lib/TeamList/TeamList.tsx @@ -159,18 +159,20 @@ const TeamMemberAccordionList = ({ labelUse="div" > - - ( - - )} - /> - + {member.image?.url && ( + + ( + + )} + /> + + )} {member.email && ( From d5ec47fed8a7085e7defb86c90a1bd5731268d38 Mon Sep 17 00:00:00 2001 From: runarvestmann Date: Wed, 2 Oct 2024 09:11:01 +0000 Subject: [PATCH 4/4] Add help text --- .../editors/TeamMemberEditor/TeamMemberEditor.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx index fedcf53f29ce..3faf439d0d62 100644 --- a/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx +++ b/apps/contentful-apps/components/editors/TeamMemberEditor/TeamMemberEditor.tsx @@ -25,7 +25,7 @@ const createLocaleToFieldMapping = (sdk: EditorExtensionSDK) => { return { name: createField('name', sdk), title: createField('title', sdk), - image: createField('mynd', sdk), + mynd: createField('mynd', sdk), imageOnSelect: createField('imageOnSelect', sdk), filterTags: createField('filterTags', sdk), email: createField('email', sdk), @@ -58,6 +58,9 @@ export const TeamMemberEditor = () => { fetchTeamList() }, [cma.entry, sdk.entry]) + const teamListIsAccordionVariant = + teamList?.fields?.variant?.[sdk.locales.default] === 'accordion' + return ( { localeToFieldMapping={localeToFieldMapping} sdk={sdk} /> + + - {teamList?.fields?.variant?.[sdk.locales.default] === 'accordion' && ( + {teamListIsAccordionVariant && (