Skip to content

Commit

Permalink
feat: add entry list location
Browse files Browse the repository at this point in the history
  • Loading branch information
bgutsol committed Apr 1, 2022
1 parent 69233dd commit bf72726
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 0 deletions.
10 changes: 10 additions & 0 deletions lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import createEditor from './editor'
import createNavigator from './navigator'
import createApp from './app'
import locations from './locations'
import createEntryList from './entry-list'
import {
BaseExtensionSDK,
EntryFieldInfo,
Expand Down Expand Up @@ -36,6 +37,7 @@ const LOCATION_TO_API_PRODUCERS: { [location: string]: ProducerFunc[] } = {
[locations.LOCATION_DIALOG]: [makeSharedAPI, makeDialogAPI, makeWindowAPI],
[locations.LOCATION_PAGE]: [makeSharedAPI],
[locations.LOCATION_APP_CONFIG]: [makeSharedAPI, makeAppAPI],
[locations.LOCATION_ENTRY_LIST]: [makeSharedAPI, makeEntryListAPI],
}

export default function createAPI(
Expand Down Expand Up @@ -133,3 +135,11 @@ function makeAppAPI(channel: Channel) {
app,
}
}

function makeEntryListAPI(channel: Channel) {
const entryList = createEntryList(channel)

return {
entryList,
}
}
97 changes: 97 additions & 0 deletions lib/entry-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Channel } from './channel'
import {
EntryListAPI,
OnEntryListUpdatedHandler,
OnEntryListUpdatedHandlerProps,
OnEntryListUpdatedHandlerReturn,
} from './types'

interface Message {
msgId: string
props: OnEntryListUpdatedHandlerProps
}

export default function createEntryList(channel: Channel): EntryListAPI {
let _handler: OnEntryListUpdatedHandler | undefined
let cachedMessage: Message | undefined

const entryListUpdatedHandler = async (message: Message) => {
cachedMessage = message

if (!_handler) {
return
}

const result = await runHandler(_handler, message.props)
return channel.send('entryListResult', { msgId: message.msgId, result })
}

channel.addHandler('entryListUpdated', entryListUpdatedHandler)

return {
onEntryListUpdated(handler: OnEntryListUpdatedHandler) {
if (typeof handler !== 'function') {
throw new Error('OnEntryListUpdated handler must be a function')
}

_handler = handler
if (cachedMessage) {
entryListUpdatedHandler(cachedMessage)
}
},
}
}

const runHandler = async (
handler: OnEntryListUpdatedHandler,
handlerArg: OnEntryListUpdatedHandlerProps
) => {
try {
// await will accept both async and sync functions, no need for the isPromise check
const result = await handler(handlerArg)
validateResult(result)

return result
} catch (error) {
console.error(error)
return false
}
}

const validateResult = (result: OnEntryListUpdatedHandlerReturn) => {
if (result === false) {
return
}

if (typeof result === 'object') {
validateData(result)
return
}

throw new Error(`EntryListResult is invalid.`)
}

const schema: Record<string, (value: unknown) => boolean> = {
values: (value) =>
typeof value === 'object' &&
Object.keys(value as Record<string, unknown>).length > 0 &&
Object.values(value as Record<string, unknown>).every((item) => typeof item === 'string'),
}

const validateData = (data: Record<string, unknown>) => {
const dataKeys = Object.keys(data)

if (dataKeys.length === 0) {
throw new Error(`EntryListResult data is invalid.`)
}

dataKeys.forEach((key: string) => {
if (!(key in schema)) {
throw new Error(`EntryListResult data is invalid. Key "${key}" is not allowed.`)
}

if (!schema[key](data[key])) {
throw new Error(`EntryListResult data is invalid. Invalid value of key "${key}."`)
}
})
}
1 change: 1 addition & 0 deletions lib/locations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const locations: Locations = {
LOCATION_ENTRY_EDITOR: 'entry-editor',
LOCATION_PAGE: 'page',
LOCATION_APP_CONFIG: 'app-config',
LOCATION_ENTRY_LIST: 'entry-list',
}

export default locations
9 changes: 9 additions & 0 deletions lib/types/api.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { AppConfigAPI } from './app.types'
import { NavigatorAPI } from './navigator.types'
import { EntryFieldInfo, FieldInfo } from './field.types'
import { Adapter } from 'contentful-management/types'
import { EntryListAPI } from './entry-list.types'

/* User API */

Expand Down Expand Up @@ -234,13 +235,20 @@ export type AppExtensionSDK = Omit<BaseExtensionSDK, 'ids'> & {
app: AppConfigAPI
}

export type EntryListExtensionSDK = BaseExtensionSDK & {
/** A set of IDs actual for the app */
ids: Omit<IdsAPI, EntryScopedIds>
entryList: EntryListAPI
}

export type KnownSDK =
| FieldExtensionSDK
| SidebarExtensionSDK
| DialogExtensionSDK
| EditorExtensionSDK
| PageExtensionSDK
| AppExtensionSDK
| EntryListExtensionSDK

export interface Locations {
LOCATION_ENTRY_FIELD: 'entry-field'
Expand All @@ -250,6 +258,7 @@ export interface Locations {
LOCATION_ENTRY_EDITOR: 'entry-editor'
LOCATION_PAGE: 'page'
LOCATION_APP_CONFIG: 'app-config'
LOCATION_ENTRY_LIST: 'entry-list'
}

export interface ConnectMessage {
Expand Down
22 changes: 22 additions & 0 deletions lib/types/entry-list.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { EntryProps } from 'contentful-management/types'

export type EntryListEntriesType = EntryProps[]

export type EntryListExtraDataType = {
values: { [entryId: string]: string }
}

export type OnEntryListUpdatedHandlerReturn = EntryListExtraDataType | false

export type OnEntryListUpdatedHandlerProps = {
entries: EntryListEntriesType
}

export type OnEntryListUpdatedHandler = (
props: OnEntryListUpdatedHandlerProps
) => OnEntryListUpdatedHandlerReturn | Promise<OnEntryListUpdatedHandlerReturn>

export interface EntryListAPI {
/** Registers a handler to be called every time entries on EntryList changes */
onEntryListUpdated: (handler: OnEntryListUpdatedHandler) => void
}
10 changes: 10 additions & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type {
SidebarExtensionSDK,
UserAPI,
JSONPatchItem,
EntryListExtensionSDK,
} from './api.types'

export type {
Expand Down Expand Up @@ -106,3 +107,12 @@ export type {
} from './validation-error'

export type { WindowAPI } from './window.types'

export type {
EntryListExtraDataType,
OnEntryListUpdatedHandlerReturn,
OnEntryListUpdatedHandlerProps,
OnEntryListUpdatedHandler,
EntryListAPI,
EntryListEntriesType,
} from './entry-list.types'
110 changes: 110 additions & 0 deletions test/unit/entry-list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { sinon, expect } from '../helpers'

import createEntryList from '../../lib/entry-list'
import {
EntryListAPI,
OnEntryListUpdatedHandler,
OnEntryListUpdatedHandlerReturn,
} from '../../lib/types'

const DATA_STUB: OnEntryListUpdatedHandlerReturn = {
values: {
entryId: 'value',
},
}

describe('createEntryList()', () => {
describe('returned "entryList" object', () => {
let channelStub: any
let entryList: EntryListAPI

beforeEach(() => {
channelStub = { addHandler: sinon.spy(), send: sinon.spy() }
entryList = createEntryList(channelStub)
})

describe('.onEntryListUpdated()', () => {
const test = async (
handler: OnEntryListUpdatedHandler | undefined,
result: OnEntryListUpdatedHandlerReturn
) => {
expect(channelStub.addHandler).to.have.been.calledOnce // eslint-disable-line no-unused-expressions
const [channelMethod, sendMessage] = channelStub.addHandler.args[0]
expect(channelMethod).to.eql('entryListUpdated')

if (handler) {
entryList.onEntryListUpdated(handler)
}
const msgId = 'testId'
const msg = { msgId, props: {} }
const expected = { msgId, result }

await sendMessage(msg)

expect(channelStub.send).to.have.been.calledWithExactly('entryListResult', expected)
}

it('requires the handler to be a function', () => {
expect(() => {
entryList.onEntryListUpdated('wrong handler' as any)
}).to.throw(/OnEntryListUpdated handler must be a function/)
})

it('will only call the last added handler', () => {
const first = sinon.spy()
const second = sinon.spy()

entryList.onEntryListUpdated(first)
entryList.onEntryListUpdated(second)

const [, sendMessage] = channelStub.addHandler.args[0]
sendMessage({ msgId: 'testId', props: {} })

sinon.assert.notCalled(first)
sinon.assert.calledOnce(second)
})

it('returns result when handler is sync function', () => test(() => DATA_STUB, DATA_STUB))

it('returns result when handler is async function', () =>
test(async () => DATA_STUB, DATA_STUB))

it('returns false when an error is thrown', () =>
test(async () => {
throw new Error()
}, false))

it('returns false when a promise rejects', () =>
test(() => Promise.reject(new Error()), false))

it('returns false if the result data has invalid key', () =>
test(
() =>
({
wrongKey: {
entryId: 'value',
},
} as any),
false
))

it('returns false if the result data has an invalid value type', () =>
test(
() => ({
values: {
entryId: {} as any,
},
}),
false
))

it('returns false if the result data has empty values', () =>
test(
() => ({
values: {},
}),
false
))
})
})
})

0 comments on commit bf72726

Please sign in to comment.