From 3e9642b95f095d444ac15b2c3300f1de8f0fa28a Mon Sep 17 00:00:00 2001 From: Renato Costa Date: Thu, 21 May 2026 16:00:32 +0300 Subject: [PATCH 1/5] Added Klipy as a GIF provider alongside Tenor ref https://linear.app/ghost/project/replace-tenor-with-klipy-for-gif-cards-c48db5d2a3d5/overview - Google is shutting down the Tenor API on 2026-06-30, so the GIF card needs a replacement provider - makes the GIF client provider-aware: host and API key now come from card config via getGifProviderConfig(), resolving Klipy when configured and falling back to Tenor - Klipy exposes a Tenor-compatible search API, so the success path is unchanged; extractErrorMessage() handles Klipy's different error shape - fixes media_filter to request tinygif,gif explicitly - updates the selector placeholder and error copy to be provider-neutral - adds unit tests and e2e coverage for the Klipy path --- packages/koenig-lexical/README.md | 6 +- packages/koenig-lexical/demo/DemoApp.jsx | 3 +- .../koenig-lexical/demo/HtmlOutputDemo.jsx | 5 +- .../demo/RestrictedContentDemo.jsx | 5 +- .../koenig-lexical/demo/utils/tenorConfig.js | 21 ++++ .../src/components/ui/TenorPlugin.jsx | 6 +- .../src/components/ui/TenorSelector.jsx | 8 +- .../components/ui/TenorSelector.stories.jsx | 4 +- .../ui/file-selectors/Tenor/Error.jsx | 6 +- .../koenig-lexical/src/nodes/ImageNode.jsx | 4 +- .../src/utils/services/tenor.js | 65 +++++++--- .../test/e2e/cards/image-card.test.js | 115 ++++++++++++++++-- .../test/unit/utils/gif-provider.test.js | 65 ++++++++++ 13 files changed, 272 insertions(+), 41 deletions(-) create mode 100644 packages/koenig-lexical/test/unit/utils/gif-provider.test.js diff --git a/packages/koenig-lexical/README.md b/packages/koenig-lexical/README.md index 8cd5523a74..ea9ae8c98d 100644 --- a/packages/koenig-lexical/README.md +++ b/packages/koenig-lexical/README.md @@ -32,12 +32,14 @@ Now, if you navigate to Ghost Admin at http://localhost:2368/ghost and open a po #### Gif card -To see this card locally, you need to create `.env.local` file in `koenig-lexical` root package with the next data: +To see this card locally, create a `.env.local` file in the `koenig-lexical` root package with a GIF provider key: ``` +VITE_KLIPY_API_KEY=xxx +# or, for the legacy Tenor provider: VITE_TENOR_API_KEY=xxx ``` -How to get the tenor key is described here https://ghost.org/docs/config/#tenor +The card resolves to Klipy when `VITE_KLIPY_API_KEY` is set, otherwise Tenor. Get a Klipy key at https://partner.klipy.com; the Tenor key is described at https://ghost.org/docs/config/#tenor #### Bookmark & Embed cards diff --git a/packages/koenig-lexical/demo/DemoApp.jsx b/packages/koenig-lexical/demo/DemoApp.jsx index 97376bcf74..f60740e15a 100644 --- a/packages/koenig-lexical/demo/DemoApp.jsx +++ b/packages/koenig-lexical/demo/DemoApp.jsx @@ -22,7 +22,7 @@ import { import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig'; import {fetchEmbed} from './utils/fetchEmbed'; import {fileTypes, useFileUpload} from './utils/useFileUpload'; -import {tenorConfig} from './utils/tenorConfig'; +import {klipyConfig, tenorConfig} from './utils/tenorConfig'; import {useLocation, useSearchParams} from 'react-router-dom'; import {useSnippets} from './utils/useSnippets'; @@ -47,6 +47,7 @@ const defaultCardConfig = { unsplash: defaultUnsplashHeaders, fetchEmbed: fetchEmbed, tenor: tenorConfig, + klipy: klipyConfig, fetchAutocompleteLinks: () => Promise.resolve([ {label: 'Homepage', value: window.location.origin + '/'}, {label: 'Free signup', value: window.location.origin + '/#/portal/signup/free'} diff --git a/packages/koenig-lexical/demo/HtmlOutputDemo.jsx b/packages/koenig-lexical/demo/HtmlOutputDemo.jsx index 547f9998e3..28e008056d 100644 --- a/packages/koenig-lexical/demo/HtmlOutputDemo.jsx +++ b/packages/koenig-lexical/demo/HtmlOutputDemo.jsx @@ -6,13 +6,14 @@ import {$getRoot, $isDecoratorNode} from 'lexical'; import {HtmlOutputPlugin, KoenigComposableEditor, KoenigComposer} from '../src'; import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig'; import {fileTypes, useFileUpload} from './utils/useFileUpload'; -import {tenorConfig} from './utils/tenorConfig'; +import {klipyConfig, tenorConfig} from './utils/tenorConfig'; import {useSnippets} from './utils/useSnippets'; import {useState} from 'react'; const cardConfig = { unsplash: {defaultHeaders: defaultUnsplashHeaders}, - tenor: tenorConfig + tenor: tenorConfig, + klipy: klipyConfig }; function HtmlOutputDemo() { diff --git a/packages/koenig-lexical/demo/RestrictedContentDemo.jsx b/packages/koenig-lexical/demo/RestrictedContentDemo.jsx index 6002645a4c..930073934c 100644 --- a/packages/koenig-lexical/demo/RestrictedContentDemo.jsx +++ b/packages/koenig-lexical/demo/RestrictedContentDemo.jsx @@ -6,14 +6,15 @@ import {$getRoot, $isDecoratorNode} from 'lexical'; import {KoenigComposableEditor, KoenigComposer, RestrictContentPlugin} from '../src'; import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig'; import {fileTypes, useFileUpload} from './utils/useFileUpload'; -import {tenorConfig} from './utils/tenorConfig'; +import {klipyConfig, tenorConfig} from './utils/tenorConfig'; import {useLocation} from 'react-router-dom'; import {useSnippets} from './utils/useSnippets'; import {useState} from 'react'; const cardConfig = { unsplash: {defaultHeaders: defaultUnsplashHeaders}, - tenor: tenorConfig + tenor: tenorConfig, + klipy: klipyConfig }; function useQuery() { diff --git a/packages/koenig-lexical/demo/utils/tenorConfig.js b/packages/koenig-lexical/demo/utils/tenorConfig.js index f5b653b8a8..9991063a1c 100644 --- a/packages/koenig-lexical/demo/utils/tenorConfig.js +++ b/packages/koenig-lexical/demo/utils/tenorConfig.js @@ -2,6 +2,10 @@ import {isTestEnv} from '../../test/utils/isTestEnv'; export const tenorConfig = isTestEnv ? {googleApiKey: 'xxx'} : getTenorConfig(); +// In tests the GIF provider defaults to Tenor; the ?gifProvider=klipy query +// param opts a specific test into the Klipy path. +export const klipyConfig = isTestEnv ? getTestKlipyConfig() : getKlipyConfig(); + function getTenorConfig() { let config = null; @@ -13,3 +17,20 @@ function getTenorConfig() { return config; } + +function getKlipyConfig() { + let config = null; + + if (import.meta.env.VITE_KLIPY_API_KEY) { + config = { + apiKey: import.meta.env.VITE_KLIPY_API_KEY + }; + } + + return config; +} + +function getTestKlipyConfig() { + const provider = new URLSearchParams(window.location.search).get('gifProvider'); + return provider === 'klipy' ? {apiKey: 'xxx'} : null; +} diff --git a/packages/koenig-lexical/src/components/ui/TenorPlugin.jsx b/packages/koenig-lexical/src/components/ui/TenorPlugin.jsx index d99033451b..78b018ddff 100644 --- a/packages/koenig-lexical/src/components/ui/TenorPlugin.jsx +++ b/packages/koenig-lexical/src/components/ui/TenorPlugin.jsx @@ -3,12 +3,13 @@ import React from 'react'; import TenorSelector from './TenorSelector'; import {DELETE_CARD_COMMAND} from '../../plugins/KoenigBehaviourPlugin.jsx'; import {INSERT_FROM_TENOR_COMMAND} from '../../plugins/KoenigSelectorPlugin.jsx'; +import {getGifProviderConfig, useTenor} from '../../utils/services/tenor.js'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -import {useTenor} from '../../utils/services/tenor.js'; const TenorPlugin = ({nodeKey}) => { const {cardConfig} = React.useContext(KoenigComposerContext); - const tenorHook = useTenor({config: cardConfig.tenor}); + const providerConfig = getGifProviderConfig(cardConfig); + const tenorHook = useTenor({config: providerConfig}); const [editor] = useLexicalComposerContext(); React.useEffect(() => { @@ -38,6 +39,7 @@ const TenorPlugin = ({nodeKey}) => { return ( { +const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLoading, isLazyLoading, error, changeColumnCount, loadNextPage, gifs, provider}) => { const selectorRef = useRef(null); const searchRef = useRef(null); const [highlightedGif, setHighlightedGif] = useState(undefined); @@ -294,7 +294,7 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo
e.stopPropagation()} > @@ -304,7 +304,7 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo @@ -330,7 +330,7 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo {!!isLoading && !error && } - {!!error &&
} + {!!error &&
}
diff --git a/packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx b/packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx index b9b35d67d4..7c9c4437f6 100644 --- a/packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx @@ -1,6 +1,6 @@ import TenorSelector from './TenorSelector'; +import {getGifProviderConfig, useTenor} from '../../utils/services/tenor.js'; import {tenorConfig} from '../../../demo/utils/tenorConfig'; -import {useTenor} from '../../utils/services/tenor.js'; const story = { title: 'File Selectors/Tenor', @@ -14,7 +14,7 @@ const story = { export default story; const Template = (args) => { - const tenorHook = useTenor({config: tenorConfig}); + const tenorHook = useTenor({config: getGifProviderConfig({tenor: tenorConfig})}); return ( diff --git a/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Error.jsx b/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Error.jsx index a7ed7161fa..0a27b12e41 100644 --- a/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Error.jsx +++ b/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Error.jsx @@ -4,7 +4,7 @@ export function Error({error}) { if (error === ERROR_TYPE.COMMON) { return (

- Uh-oh! Trouble reaching the Tenor API, please check your connection + Uh-oh! Trouble reaching the GIF service, please check your connection

); } @@ -12,8 +12,8 @@ export function Error({error}) { if (error === ERROR_TYPE.INVALID_API_KEY) { return (

- This version of the Tenor API is no longer supported. Please update your API key by following our - documentation here. + The GIF API key is not valid. Please check your configuration by following our + documentation here.

); } diff --git a/packages/koenig-lexical/src/nodes/ImageNode.jsx b/packages/koenig-lexical/src/nodes/ImageNode.jsx index 1e24261405..afeadb447c 100644 --- a/packages/koenig-lexical/src/nodes/ImageNode.jsx +++ b/packages/koenig-lexical/src/nodes/ImageNode.jsx @@ -55,10 +55,10 @@ export class ImageNode extends BaseImageNode { insertParams: { triggerFileDialog: false }, - matches: ['gif', 'giphy', 'tenor'], + matches: ['gif', 'giphy', 'tenor', 'klipy'], priority: 17, queryParams: ['src'], - isHidden: ({config}) => !config?.tenor, + isHidden: ({config}) => !config?.tenor && !config?.klipy, shortcut: '/gif' }]; diff --git a/packages/koenig-lexical/src/utils/services/tenor.js b/packages/koenig-lexical/src/utils/services/tenor.js index f13856cec0..48d09d7142 100644 --- a/packages/koenig-lexical/src/utils/services/tenor.js +++ b/packages/koenig-lexical/src/utils/services/tenor.js @@ -1,15 +1,52 @@ import debounce from 'lodash/debounce'; import {useRef, useState} from 'react'; -const API_URL = 'https://tenor.googleapis.com'; const API_VERSION = 'v2'; const DEBOUNCE_MS = 600; +// Both Tenor and Klipy expose a Tenor-compatible v2 API; only the base URL and +// API key differ, so one client serves either provider. +const PROVIDER_API_URLS = { + klipy: 'https://api.klipy.com', + tenor: 'https://tenor.googleapis.com' +}; + export const ERROR_TYPE = { COMMON: 'common', INVALID_API_KEY: 'invalid_key' }; +// Resolve which GIF provider to use from the editor's cardConfig. Klipy takes +// precedence when both are configured; returns null when neither is set. +export function getGifProviderConfig(cardConfig) { + if (cardConfig?.klipy?.apiKey) { + return { + provider: 'klipy', + apiUrl: PROVIDER_API_URLS.klipy, + apiKey: cardConfig.klipy.apiKey, + contentFilter: cardConfig.klipy.contentFilter || 'off' + }; + } + if (cardConfig?.tenor?.googleApiKey) { + return { + provider: 'tenor', + apiUrl: PROVIDER_API_URLS.tenor, + apiKey: cardConfig.tenor.googleApiKey, + contentFilter: cardConfig.tenor.contentFilter || 'off' + }; + } + return null; +} + +// Tenor returns {error: {message}}; Klipy returns {result: false, errors: {message: [...]}}. +export function extractErrorMessage(json) { + const klipyMessage = json?.errors?.message; + return json?.error?.message + || json?.error + || (Array.isArray(klipyMessage) ? klipyMessage[0] : klipyMessage) + || 'Unknown error'; +} + export function useTenor({config}) { const [columns, setColumns] = useState([]); const [error, setError] = useState(null); @@ -47,7 +84,7 @@ export function useTenor({config}) { await makeRequest(loadedType.current, {params: { q: term, - media_filter: 'minimal' + media_filter: 'tinygif,gif' }}); } @@ -56,7 +93,7 @@ export function useTenor({config}) { await makeRequest(loadedType.current, {params: { q: 'excited', - media_filter: 'minimal' + media_filter: 'tinygif,gif' }}); } @@ -120,10 +157,10 @@ export function useTenor({config}) { async function makeRequest(path, options) { const versionedPath = `${API_VERSION}/${path}`.replace(/\/+/, '/'); - const url = new URL(versionedPath, API_URL); + const url = new URL(versionedPath, config.apiUrl); const params = new URLSearchParams(options.params); - params.set('key', config.googleApiKey); + params.set('key', config.apiKey); params.set('client_key', 'ghost-editor'); params.set('contentfilter', getContentFilter()); @@ -145,13 +182,13 @@ export function useTenor({config}) { setGifs(internalStateGifs.current); }) .catch((e) => { - // if the error text isn't already set then we've get a connection error from `fetch` - if (!options.ignoreErrors && !error) { - setError(ERROR_TYPE.COMMON); - } - - if (error && error.startsWith('API key not valid')) { - setError(ERROR_TYPE.INVALID_API_KEY); + // e.message is the API error text (from checkStatus) or a fetch + // connection error. Tenor and Klipy word invalid-key errors + // differently, so match either phrasing. + if (!options.ignoreErrors) { + const message = e?.message || ''; + const isInvalidKey = /api key/i.test(message) && /(invalid|not valid)/i.test(message); + setError(isInvalidKey ? ERROR_TYPE.INVALID_API_KEY : ERROR_TYPE.COMMON); } console.error(e); }) @@ -170,7 +207,7 @@ export function useTenor({config}) { let responseText; if (response.headers.map['content-type'].startsWith('application/json')) { - responseText = await response.json().then(json => json.error.message || json.error); + responseText = await response.json().then(json => extractErrorMessage(json)); } else if (response.headers.map['content-type'] === 'text/xml') { responseText = await response.text(); } @@ -207,7 +244,7 @@ export function useTenor({config}) { if (nextPos.current !== null) { const params = { pos: nextPos.current, - media_filter: 'minimal' + media_filter: 'tinygif,gif' }; if (loadedType.current === 'search') { diff --git a/packages/koenig-lexical/test/e2e/cards/image-card.test.js b/packages/koenig-lexical/test/e2e/cards/image-card.test.js index 8a080d21d6..f8bc38251a 100644 --- a/packages/koenig-lexical/test/e2e/cards/image-card.test.js +++ b/packages/koenig-lexical/test/e2e/cards/image-card.test.js @@ -815,7 +815,7 @@ test.describe('Image card', async () => { `, {ignoreCardToolbarContents: true}); }); - test('can insert tenor image with key Tab', async () => { + test('can insert a gif with the keyboard', async () => { await mockTenorApi(page); await focusEditor(page); await page.click('[data-kg-plus-button]'); @@ -861,26 +861,26 @@ test.describe('Image card', async () => { `, {ignoreCardToolbarContents: true}); }); - test('can close tenor selector on Esc', async () => { + test('can close the gif selector on Esc', async () => { await mockTenorApi(page); await focusEditor(page); await page.click('[data-kg-plus-button]'); await page.click('button[data-kg-card-menu-item="GIF"]'); - await expect(await page.getByTestId('tenor-selector')).toBeVisible(); + await expect(await page.getByTestId('gif-selector')).toBeVisible(); await page.keyboard.press('Escape'); - await expect(await page.getByTestId('tenor-selector')).toBeHidden(); + await expect(await page.getByTestId('gif-selector')).toBeHidden(); }); - test('can show tenor error', async () => { + test('can show a gif selector error', async () => { await mockTenorApi(page, {status: 400}); await focusEditor(page); await page.click('[data-kg-plus-button]'); await page.click('button[data-kg-card-menu-item="GIF"]'); - await expect(await page.getByTestId('tenor-selector-error')).toBeVisible(); + await expect(await page.getByTestId('gif-selector-error')).toBeVisible(); }); test('can add snippet', async function () { @@ -1447,10 +1447,111 @@ function tenorTestData() { ); } -const tenorUrl = 'https://tenor.googleapis.com/v2/featured?q=excited&media_filter=minimal&key=xxx&client_key=ghost-editor&contentfilter=off'; +const tenorUrl = /https:\/\/tenor\.googleapis\.com\/v2\//; async function mockTenorApi(page, {status} = {status: 200}) { await page.route(tenorUrl, route => route.fulfill({ status, body: JSON.stringify(tenorTestData()) })); } + +function klipyTestData() { + return { + results: [ + { + id: '2484942301552561', + title: 'Klipy Cat', + media_formats: { + tinygif: { + url: 'https://static.klipy.com/gif/klipy-cat-tiny.gif', + duration: 0, + preview: '', + dims: [220, 220], + size: 271080 + }, + gif: { + url: 'https://static.klipy.com/gif/klipy-cat.gif', + duration: 0, + preview: '', + dims: [498, 498], + size: 273268 + } + }, + created: 1765483200, + content_description: 'Klipy Cat GIF', + itemurl: 'https://klipy.com/gifs/klipy-cat', + url: 'https://static.klipy.com/gif/klipy-cat.gif', + tags: ['cat'], + flags: [], + hasaudio: false + } + ], + next: '' + }; +} + +const klipyUrl = /https:\/\/api\.klipy\.com\/v2\//; +async function mockKlipyApi(page, {status} = {status: 200}) { + await page.route(klipyUrl, route => route.fulfill({ + status, + body: JSON.stringify(klipyTestData()) + })); +} + +test.describe('Image card - Klipy GIF provider', async () => { + let page; + + test.beforeAll(async ({browser}) => { + page = await browser.newPage(); + }); + + test.afterAll(async () => { + await page.close(); + }); + + test('can insert klipy image', async () => { + await mockKlipyApi(page); + // ?gifProvider=klipy makes the demo resolve the GIF card to Klipy + await initialize({page, uri: '/?gifProvider=klipy#/?content=false'}); + await focusEditor(page); + await page.click('[data-kg-plus-button]'); + + await page.click('button[data-kg-card-menu-item="GIF"]'); + + await expect(await page.locator('[data-tenor-index="0"]')).toBeVisible(); + await page.click('[data-tenor-index="0"]'); + + // toBeAttached rather than toBeVisible: the fixture GIF URL is not + // network-loaded in tests, so visibility would depend on an external fetch + await expect(await page.getByTestId('image-card-populated')).toBeAttached(); + + await assertHTML(page, html` +
+
+
+
+ +
+
+
+
+
+
+


+
+
+
Type caption for image (optional)
+
+
+ +
+
+
+
+
+


+ `, {ignoreCardToolbarContents: true}); + }); +}); diff --git a/packages/koenig-lexical/test/unit/utils/gif-provider.test.js b/packages/koenig-lexical/test/unit/utils/gif-provider.test.js new file mode 100644 index 0000000000..19f5f775f8 --- /dev/null +++ b/packages/koenig-lexical/test/unit/utils/gif-provider.test.js @@ -0,0 +1,65 @@ +import {describe, expect, test} from 'vitest'; +import {extractErrorMessage, getGifProviderConfig} from '../../../src/utils/services/tenor.js'; + +describe('Utils: getGifProviderConfig', () => { + test('returns null when neither provider is configured', () => { + expect(getGifProviderConfig(undefined)).toBeNull(); + expect(getGifProviderConfig({})).toBeNull(); + expect(getGifProviderConfig({tenor: null, klipy: null})).toBeNull(); + }); + + test('resolves Tenor when only Tenor is configured', () => { + const config = getGifProviderConfig({tenor: {googleApiKey: 'tenor-key'}}); + + expect(config).toEqual({ + provider: 'tenor', + apiUrl: 'https://tenor.googleapis.com', + apiKey: 'tenor-key', + contentFilter: 'off' + }); + }); + + test('resolves Klipy when only Klipy is configured', () => { + const config = getGifProviderConfig({klipy: {apiKey: 'klipy-key'}}); + + expect(config).toEqual({ + provider: 'klipy', + apiUrl: 'https://api.klipy.com', + apiKey: 'klipy-key', + contentFilter: 'off' + }); + }); + + test('prefers Klipy when both providers are configured', () => { + const config = getGifProviderConfig({ + tenor: {googleApiKey: 'tenor-key'}, + klipy: {apiKey: 'klipy-key'} + }); + + expect(config.provider).toEqual('klipy'); + expect(config.apiKey).toEqual('klipy-key'); + }); + + test('passes through a configured content filter', () => { + expect(getGifProviderConfig({tenor: {googleApiKey: 'k', contentFilter: 'high'}}).contentFilter).toEqual('high'); + expect(getGifProviderConfig({klipy: {apiKey: 'k', contentFilter: 'low'}}).contentFilter).toEqual('low'); + }); +}); + +describe('Utils: extractErrorMessage', () => { + test('reads the Tenor error shape', () => { + expect(extractErrorMessage({error: {message: 'API key not valid'}})).toEqual('API key not valid'); + }); + + test('reads a Tenor string error', () => { + expect(extractErrorMessage({error: 'Something went wrong'})).toEqual('Something went wrong'); + }); + + test('reads the Klipy error shape', () => { + expect(extractErrorMessage({result: false, errors: {message: ['The provided API key is invalid']}})).toEqual('The provided API key is invalid'); + }); + + test('falls back to a generic message when the shape is unknown', () => { + expect(extractErrorMessage({})).toEqual('Unknown error'); + }); +}); From 905cd97db5125dbea73569fe383a217d59c9e75e Mon Sep 17 00:00:00 2001 From: Renato Costa Date: Thu, 21 May 2026 16:11:24 +0300 Subject: [PATCH 2/5] Cleaned up GIF card naming to be provider-agnostic ref https://linear.app/ghost/project/replace-tenor-with-klipy-for-gif-cards-c48db5d2a3d5/overview - the editor GIF card was named after Tenor throughout, but it now supports multiple providers (Tenor and Klipy) - renamed the files, components, commands and DOM attribute from Tenor* to Gif*: tenor.js -> gif.js, TenorPlugin -> GifPlugin, TenorSelector -> GifSelector, useTenor -> useGif, the OPEN/INSERT_FROM_TENOR commands -> _GIF, data-tenor-index -> data-gif-index - pure rename: Tenor-the-provider references (config.tenor, provider: 'tenor', the Tenor-specific tests) are unchanged --- packages/koenig-lexical/demo/DemoApp.jsx | 2 +- .../koenig-lexical/demo/HtmlOutputDemo.jsx | 2 +- .../demo/RestrictedContentDemo.jsx | 2 +- .../utils/{tenorConfig.js => gifConfig.js} | 0 .../ui/{TenorPlugin.jsx => GifPlugin.jsx} | 18 +++++++++--------- .../ui/{TenorSelector.jsx => GifSelector.jsx} | 18 +++++++++--------- ...tor.stories.jsx => GifSelector.stories.jsx} | 14 +++++++------- .../ui/file-selectors/{Tenor => Gif}/Error.jsx | 2 +- .../ui/file-selectors/{Tenor => Gif}/Gif.jsx | 2 +- .../file-selectors/{Tenor => Gif}/Loader.jsx | 0 .../koenig-lexical/src/nodes/ImageNode.jsx | 4 ++-- .../src/plugins/AllDefaultPlugins.jsx | 2 +- .../src/plugins/KoenigSelectorPlugin.jsx | 12 ++++++------ .../koenig-lexical/src/utils/buildCardMenu.js | 2 +- .../src/utils/services/{tenor.js => gif.js} | 2 +- .../test/e2e/cards/image-card.test.js | 10 +++++----- .../test/unit/utils/gif-provider.test.js | 2 +- 17 files changed, 47 insertions(+), 47 deletions(-) rename packages/koenig-lexical/demo/utils/{tenorConfig.js => gifConfig.js} (100%) rename packages/koenig-lexical/src/components/ui/{TenorPlugin.jsx => GifPlugin.jsx} (74%) rename packages/koenig-lexical/src/components/ui/{TenorSelector.jsx => GifSelector.jsx} (94%) rename packages/koenig-lexical/src/components/ui/{TenorSelector.stories.jsx => GifSelector.stories.jsx} (70%) rename packages/koenig-lexical/src/components/ui/file-selectors/{Tenor => Gif}/Error.jsx (90%) rename packages/koenig-lexical/src/components/ui/file-selectors/{Tenor => Gif}/Gif.jsx (95%) rename packages/koenig-lexical/src/components/ui/file-selectors/{Tenor => Gif}/Loader.jsx (100%) rename packages/koenig-lexical/src/utils/services/{tenor.js => gif.js} (99%) diff --git a/packages/koenig-lexical/demo/DemoApp.jsx b/packages/koenig-lexical/demo/DemoApp.jsx index f60740e15a..eb86e4502b 100644 --- a/packages/koenig-lexical/demo/DemoApp.jsx +++ b/packages/koenig-lexical/demo/DemoApp.jsx @@ -22,7 +22,7 @@ import { import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig'; import {fetchEmbed} from './utils/fetchEmbed'; import {fileTypes, useFileUpload} from './utils/useFileUpload'; -import {klipyConfig, tenorConfig} from './utils/tenorConfig'; +import {klipyConfig, tenorConfig} from './utils/gifConfig'; import {useLocation, useSearchParams} from 'react-router-dom'; import {useSnippets} from './utils/useSnippets'; diff --git a/packages/koenig-lexical/demo/HtmlOutputDemo.jsx b/packages/koenig-lexical/demo/HtmlOutputDemo.jsx index 28e008056d..bb65f9e9da 100644 --- a/packages/koenig-lexical/demo/HtmlOutputDemo.jsx +++ b/packages/koenig-lexical/demo/HtmlOutputDemo.jsx @@ -6,7 +6,7 @@ import {$getRoot, $isDecoratorNode} from 'lexical'; import {HtmlOutputPlugin, KoenigComposableEditor, KoenigComposer} from '../src'; import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig'; import {fileTypes, useFileUpload} from './utils/useFileUpload'; -import {klipyConfig, tenorConfig} from './utils/tenorConfig'; +import {klipyConfig, tenorConfig} from './utils/gifConfig'; import {useSnippets} from './utils/useSnippets'; import {useState} from 'react'; diff --git a/packages/koenig-lexical/demo/RestrictedContentDemo.jsx b/packages/koenig-lexical/demo/RestrictedContentDemo.jsx index 930073934c..1c4b25cd9b 100644 --- a/packages/koenig-lexical/demo/RestrictedContentDemo.jsx +++ b/packages/koenig-lexical/demo/RestrictedContentDemo.jsx @@ -6,7 +6,7 @@ import {$getRoot, $isDecoratorNode} from 'lexical'; import {KoenigComposableEditor, KoenigComposer, RestrictContentPlugin} from '../src'; import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig'; import {fileTypes, useFileUpload} from './utils/useFileUpload'; -import {klipyConfig, tenorConfig} from './utils/tenorConfig'; +import {klipyConfig, tenorConfig} from './utils/gifConfig'; import {useLocation} from 'react-router-dom'; import {useSnippets} from './utils/useSnippets'; import {useState} from 'react'; diff --git a/packages/koenig-lexical/demo/utils/tenorConfig.js b/packages/koenig-lexical/demo/utils/gifConfig.js similarity index 100% rename from packages/koenig-lexical/demo/utils/tenorConfig.js rename to packages/koenig-lexical/demo/utils/gifConfig.js diff --git a/packages/koenig-lexical/src/components/ui/TenorPlugin.jsx b/packages/koenig-lexical/src/components/ui/GifPlugin.jsx similarity index 74% rename from packages/koenig-lexical/src/components/ui/TenorPlugin.jsx rename to packages/koenig-lexical/src/components/ui/GifPlugin.jsx index 78b018ddff..0763ff3f27 100644 --- a/packages/koenig-lexical/src/components/ui/TenorPlugin.jsx +++ b/packages/koenig-lexical/src/components/ui/GifPlugin.jsx @@ -1,15 +1,15 @@ +import GifSelector from './GifSelector'; import KoenigComposerContext from '../../context/KoenigComposerContext.jsx'; import React from 'react'; -import TenorSelector from './TenorSelector'; import {DELETE_CARD_COMMAND} from '../../plugins/KoenigBehaviourPlugin.jsx'; -import {INSERT_FROM_TENOR_COMMAND} from '../../plugins/KoenigSelectorPlugin.jsx'; -import {getGifProviderConfig, useTenor} from '../../utils/services/tenor.js'; +import {INSERT_FROM_GIF_COMMAND} from '../../plugins/KoenigSelectorPlugin.jsx'; +import {getGifProviderConfig, useGif} from '../../utils/services/gif.js'; import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; -const TenorPlugin = ({nodeKey}) => { +const GifPlugin = ({nodeKey}) => { const {cardConfig} = React.useContext(KoenigComposerContext); const providerConfig = getGifProviderConfig(cardConfig); - const tenorHook = useTenor({config: providerConfig}); + const gifHook = useGif({config: providerConfig}); const [editor] = useLexicalComposerContext(); React.useEffect(() => { @@ -34,17 +34,17 @@ const TenorPlugin = ({nodeKey}) => { }; const insertImageToNode = async (image) => { - editor.dispatchCommand(INSERT_FROM_TENOR_COMMAND, image); + editor.dispatchCommand(INSERT_FROM_GIF_COMMAND, image); }; return ( - ); }; -export default TenorPlugin; +export default GifPlugin; diff --git a/packages/koenig-lexical/src/components/ui/TenorSelector.jsx b/packages/koenig-lexical/src/components/ui/GifSelector.jsx similarity index 94% rename from packages/koenig-lexical/src/components/ui/TenorSelector.jsx rename to packages/koenig-lexical/src/components/ui/GifSelector.jsx index 84ea10d4d3..ca24c08d11 100644 --- a/packages/koenig-lexical/src/components/ui/TenorSelector.jsx +++ b/packages/koenig-lexical/src/components/ui/GifSelector.jsx @@ -1,14 +1,14 @@ import React, {useEffect, useRef, useState} from 'react'; import SearchIcon from '../../assets/icons/kg-search.svg?react'; -import {Error} from './file-selectors/Tenor/Error'; -import {Gif} from './file-selectors/Tenor/Gif'; -import {Loader} from './file-selectors/Tenor/Loader'; +import {Error} from './file-selectors/Gif/Error'; +import {Gif} from './file-selectors/Gif/Gif'; +import {Loader} from './file-selectors/Gif/Loader'; // number of columns based on selector container width const TWO_COLUMN_WIDTH = 540; const THREE_COLUMN_WIDTH = 940; -const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLoading, isLazyLoading, error, changeColumnCount, loadNextPage, gifs, provider}) => { +const GifSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLoading, isLazyLoading, error, changeColumnCount, loadNextPage, gifs, provider}) => { const selectorRef = useRef(null); const searchRef = useRef(null); const [highlightedGif, setHighlightedGif] = useState(undefined); @@ -138,7 +138,7 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo } function moveToNextHorizontalGif(direction) { - const highlightedElem = document.querySelector(`[data-tenor-index="${highlightedGif.index}"]`); + const highlightedElem = document.querySelector(`[data-gif-index="${highlightedGif.index}"]`); const highlightedElemRect = highlightedElem.getBoundingClientRect(); let x; @@ -155,9 +155,9 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo // we might hit spacing between gifs, keep moving up 5 px until we get a match while (!foundGifElem) { - let possibleMatch = document.elementFromPoint(x, y)?.closest('[data-tenor-index]'); + let possibleMatch = document.elementFromPoint(x, y)?.closest('[data-gif-index]'); - if (possibleMatch?.dataset.tenorIndex !== undefined) { + if (possibleMatch?.dataset.gifIndex !== undefined) { foundGifElem = possibleMatch; break; } @@ -172,7 +172,7 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo } if (foundGifElem) { - setHighlightedGif(gifs[foundGifElem.dataset.tenorIndex]); + setHighlightedGif(gifs[foundGifElem.dataset.gifIndex]); } } @@ -337,4 +337,4 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo ); }; -export default TenorSelector; +export default GifSelector; diff --git a/packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx b/packages/koenig-lexical/src/components/ui/GifSelector.stories.jsx similarity index 70% rename from packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx rename to packages/koenig-lexical/src/components/ui/GifSelector.stories.jsx index 7c9c4437f6..d122d4a61b 100644 --- a/packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx +++ b/packages/koenig-lexical/src/components/ui/GifSelector.stories.jsx @@ -1,10 +1,10 @@ -import TenorSelector from './TenorSelector'; -import {getGifProviderConfig, useTenor} from '../../utils/services/tenor.js'; -import {tenorConfig} from '../../../demo/utils/tenorConfig'; +import GifSelector from './GifSelector'; +import {getGifProviderConfig, useGif} from '../../utils/services/gif.js'; +import {tenorConfig} from '../../../demo/utils/gifConfig'; const story = { - title: 'File Selectors/Tenor', - component: TenorSelector, + title: 'File Selectors/Gif', + component: GifSelector, parameters: { status: { type: 'Functional' @@ -14,10 +14,10 @@ const story = { export default story; const Template = (args) => { - const tenorHook = useTenor({config: getGifProviderConfig({tenor: tenorConfig})}); + const gifHook = useGif({config: getGifProviderConfig({tenor: tenorConfig})}); return ( - + ); }; diff --git a/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Error.jsx b/packages/koenig-lexical/src/components/ui/file-selectors/Gif/Error.jsx similarity index 90% rename from packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Error.jsx rename to packages/koenig-lexical/src/components/ui/file-selectors/Gif/Error.jsx index 0a27b12e41..b77144b271 100644 --- a/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Error.jsx +++ b/packages/koenig-lexical/src/components/ui/file-selectors/Gif/Error.jsx @@ -1,4 +1,4 @@ -import {ERROR_TYPE} from '../../../../utils/services/tenor.js'; +import {ERROR_TYPE} from '../../../../utils/services/gif.js'; export function Error({error}) { if (error === ERROR_TYPE.COMMON) { diff --git a/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Gif.jsx b/packages/koenig-lexical/src/components/ui/file-selectors/Gif/Gif.jsx similarity index 95% rename from packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Gif.jsx rename to packages/koenig-lexical/src/components/ui/file-selectors/Gif/Gif.jsx index 2acf6ab4f4..19a3c8a814 100644 --- a/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Gif.jsx +++ b/packages/koenig-lexical/src/components/ui/file-selectors/Gif/Gif.jsx @@ -20,7 +20,7 @@ export function Gif({gif, onClick, highlightedGif = {}}) {