2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Added tests to AdminX framework package (#19022)

refs https://github.com/TryGhost/Product/issues/4159

---

<!-- Leave the line below if you'd like GitHub Copilot to generate a
summary from your commit -->
<!--
copilot:summary
-->
### <samp>🤖[[deprecated]](https://githubnext.com/copilot-for-prs-sunset)
Generated by Copilot at 9e68f4d</samp>

This pull request refactors several components in the `admin-x-settings`
app to use common hooks from the `@tryghost/admin-x-framework` package,
which reduces code duplication and improves consistency. It also updates
the `package.json` file and adds unit tests for the `admin-x-framework`
package, which improves the formatting, testing, and dependency
management. Additionally, it makes some minor changes to the `hooks.ts`,
`FrameworkProvider.tsx`, and `.eslintrc.cjs` files in the
`admin-x-framework` package, which enhance the public API and the
linting configuration.
This commit is contained in:
Jono M 2023-11-20 11:00:51 +00:00 committed by GitHub
parent b1666f596f
commit 5e057dee11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 874 additions and 157 deletions

View file

@ -7,38 +7,39 @@
"private": true,
"exports": {
".": {
"import": "./es/index.js",
"types": "./types/index.d.ts"
"import": "./es/index.js",
"types": "./types/index.d.ts"
},
"./errors": {
"import": "./es/errors.js",
"types": "./types/errors.d.ts"
"import": "./es/errors.js",
"types": "./types/errors.d.ts"
},
"./helpers": {
"import": "./es/helpers.js",
"types": "./types/helpers.d.ts"
"import": "./es/helpers.js",
"types": "./types/helpers.d.ts"
},
"./hooks": {
"import": "./es/hooks.js",
"types": "./types/hooks.d.ts"
"import": "./es/hooks.js",
"types": "./types/hooks.d.ts"
},
"./routing": {
"import": "./es/routing.js",
"types": "./types/routing.d.ts"
"import": "./es/routing.js",
"types": "./types/routing.d.ts"
},
"./api/*": {
"import": "./es/api/*.js",
"types": "./types/api/*.d.ts"
"import": "./es/api/*.js",
"types": "./types/api/*.d.ts"
}
},
"sideEffects": false,
"scripts": {
"build": "vite build && tsc -p tsconfig.declaration.json",
"prepare": "yarn build",
"test": "yarn test:types",
"test": "yarn test:types && yarn test:unit",
"test:types": "tsc --noEmit",
"test:unit": "vitest run --coverage",
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache",
"lint": "yarn lint:code && (yarn lint:test || echo \"TODO ADD TESTS TO LINT\")",
"lint": "yarn lint:code && yarn lint:test",
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache"
},
"files": [
@ -46,15 +47,17 @@
"types"
],
"devDependencies": {
"@testing-library/react": "14.1.0",
"@types/mocha": "10.0.1",
"@vitejs/plugin-react": "4.2.0",
"c8": "8.0.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"mocha": "10.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"sinon": "17.0.0",
"ts-node": "10.9.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "5.2.2",
"vite": "4.5.0"
},
@ -74,6 +77,12 @@
"build",
{"projects": ["@tryghost/admin-x-design-system"], "target": "build"}
]
},
"test:unit": {
"dependsOn": [
"test:unit",
{"projects": ["@tryghost/admin-x-design-system"], "target": "build"}
]
}
}
}

View file

@ -49,7 +49,7 @@ export const useBrowseOffers = createQuery<OffersResponseType>({
export const useBrowseOffersById = createQueryWithId<OffersResponseType>({
dataType,
path: `/offers/`
path: id => `/offers/${id}/`
});
export const useEditOffer = createMutation<OfferEditResponseType, Offer>({

View file

@ -1,4 +1,6 @@
export {default as useFilterableApi} from './hooks/useFilterableApi';
export {default as useForm} from './hooks/useForm';
export type {Dirtyable, ErrorMessages, FormHook, OkProps, SaveHandler, SaveState} from './hooks/useForm';
export {default as useHandleError} from './hooks/useHandleError';
export {usePermission} from './hooks/usePermissions';

View file

@ -8,7 +8,7 @@ export interface FrameworkProviderProps {
basePath: string;
ghostVersion: string;
externalNavigate: RoutingProviderProps['externalNavigate'];
modals: RoutingProviderProps['modals'];
modals?: RoutingProviderProps['modals'];
unsplashConfig: {
Authorization: string;
'Accept-Version': string;

View file

@ -19,7 +19,7 @@ export const useFetchApi = () => {
const {ghostVersion, sentryDSN} = useFramework();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return async <ResponseData = any>(endpoint: string | URL, options: RequestOptions = {}): Promise<ResponseData> => {
return async <ResponseData = any>(endpoint: string | URL, {headers = {}, retry, ...options}: RequestOptions = {}): Promise<ResponseData> => {
// By default, we set the Content-Type header to application/json
const defaultHeaders: Record<string, string> = {
'app-pragma': 'no-cache',
@ -28,7 +28,6 @@ export const useFetchApi = () => {
if (typeof options.body === 'string') {
defaultHeaders['content-type'] = 'application/json';
}
const headers = options?.headers || {};
const controller = new AbortController();
const {timeout} = options;
@ -41,7 +40,7 @@ export const useFetchApi = () => {
// 1. Server Unreachable error from the browser (code 0 or TypeError), typically from short internet blips
// 2. Maintenance error from Ghost, upgrade in progress so API is temporarily unavailable
let attempts = 0;
const shouldRetry = options.retry === true || options.retry === undefined;
const shouldRetry = retry !== false;
let retryingMs = 0;
const startTime = Date.now();
const maxRetryingMs = 15_000;

View file

@ -17,15 +17,6 @@ export interface Meta {
}
}
const parameterizedPath = (path: string, params: string | string[]) => {
const paramList = Array.isArray(params) ? params : [params];
return paramList.reduce(function (updatedPath, param) {
updatedPath = updatedPath + param + '/';
updatedPath.replace(/:[a-z0-9]+/, encodeURIComponent(param));
return updatedPath;
}, path);
};
interface QueryOptions<ResponseData> {
dataType: string
path: string
@ -147,8 +138,8 @@ export const createInfiniteQuery = <ResponseData>(options: InfiniteQueryOptions<
};
};
export const createQueryWithId = <ResponseData>(options: QueryOptions<ResponseData>) => (id: string, {searchParams, ...query}: QueryHookOptions<ResponseData> = {}) => {
const queryHook = createQuery<ResponseData>({...options, path: parameterizedPath(options.path, id)});
export const createQueryWithId = <ResponseData>(options: Omit<QueryOptions<ResponseData>, 'path'> & {path: (id: string) => string}) => (id: string, {searchParams, ...query}: QueryHookOptions<ResponseData> = {}) => {
const queryHook = createQuery<ResponseData>({...options, path: options.path(id)});
return queryHook({searchParams: searchParams || options.defaultSearchParams, ...query});
};
@ -165,7 +156,7 @@ const mutate = <ResponseData, Payload>({fetchApi, path, payload, searchParams, o
path: string;
payload?: Payload;
searchParams?: Record<string, string>;
options: MutationOptions<ResponseData, Payload>
options: Omit<MutationOptions<ResponseData, Payload>, 'path'>
}) => {
const {defaultSearchParams, body, ...requestOptions} = options;
const url = apiUrl(path, searchParams || defaultSearchParams);
@ -184,33 +175,33 @@ const mutate = <ResponseData, Payload>({fetchApi, path, payload, searchParams, o
});
};
export const createMutation = <ResponseData, Payload>(options: MutationOptions<ResponseData, Payload>) => () => {
export const createMutation = <ResponseData, Payload>({path, searchParams, defaultSearchParams, updateQueries, invalidateQueries, ...mutateOptions}: MutationOptions<ResponseData, Payload>) => () => {
const fetchApi = useFetchApi();
const queryClient = useQueryClient();
const {onUpdate, onInvalidate, onDelete} = useFramework();
const afterMutate = useCallback((newData: ResponseData, payload: Payload) => {
if (options.invalidateQueries) {
queryClient.invalidateQueries([options.invalidateQueries.dataType]);
onInvalidate(options.invalidateQueries.dataType);
if (invalidateQueries) {
queryClient.invalidateQueries([invalidateQueries.dataType]);
onInvalidate(invalidateQueries.dataType);
}
if (options.updateQueries) {
queryClient.setQueriesData([options.updateQueries.dataType], (data: unknown) => options.updateQueries!.update(newData, data, payload));
if (options.updateQueries.emberUpdateType === 'createOrUpdate') {
onUpdate(options.updateQueries.dataType, newData);
} else if (options.updateQueries.emberUpdateType === 'delete') {
if (updateQueries) {
queryClient.setQueriesData([updateQueries.dataType], (data: unknown) => updateQueries!.update(newData, data, payload));
if (updateQueries.emberUpdateType === 'createOrUpdate') {
onUpdate(updateQueries.dataType, newData);
} else if (updateQueries.emberUpdateType === 'delete') {
if (typeof payload !== 'string') {
throw new Error('Expected delete mutation to have a string (ID) payload. Either change the payload or update the createMutation hook');
}
onDelete(options.updateQueries.dataType, payload);
onDelete(updateQueries.dataType, payload);
}
}
}, [onInvalidate, onUpdate, onDelete, queryClient]);
return useMutation<ResponseData, unknown, Payload>({
mutationFn: payload => mutate({fetchApi, path: options.path(payload), payload, searchParams: options.searchParams?.(payload) || options.defaultSearchParams, options}),
mutationFn: payload => mutate({fetchApi, path: path(payload), payload, searchParams: searchParams?.(payload) || defaultSearchParams, options: mutateOptions}),
onSuccess: afterMutate
});
};

View file

@ -1,7 +1,6 @@
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['ghost'],
extends: [
'plugin:ghost/test'
'plugin:ghost/ts-test'
]
};

View file

@ -1,8 +0,0 @@
import assert from 'assert/strict';
describe('Hello world', function () {
it('Runs a test', function () {
// TODO: Write me!
assert.ok(require('../'));
});
});

View file

@ -0,0 +1,75 @@
import {act, renderHook} from '@testing-library/react';
import * as assert from 'assert/strict';
import useForm from '../../../src/hooks/useForm';
describe('useForm', function () {
describe('formState', function () {
it('returns the initial form state', function () {
const {result} = renderHook(() => useForm({
initialState: {a: 1},
onSave: () => {}
}));
assert.deepEqual(result.current.formState, {a: 1});
});
});
describe('updateForm', function () {
it('updates the form state', function () {
const {result} = renderHook(() => useForm({
initialState: {a: 1},
onSave: () => {}
}));
act(() => result.current.updateForm(state => ({...state, b: 2})));
assert.deepEqual(result.current.formState, {a: 1, b: 2});
});
it('sets the saveState to unsaved', function () {
const {result} = renderHook(() => useForm({
initialState: {a: 1},
onSave: () => {}
}));
act(() => result.current.updateForm(state => ({...state, a: 2})));
assert.deepEqual(result.current.saveState, 'unsaved');
});
});
describe('handleSave', function () {
it('does nothing when the state has not changed', async function () {
let onSaveCalled = false;
const {result} = renderHook(() => useForm({
initialState: {a: 1},
onSave: () => {
onSaveCalled = true;
}
}));
assert.equal(await act(() => result.current.handleSave()), true);
assert.equal(result.current.saveState, '');
assert.equal(onSaveCalled, false);
});
it('calls the onSave callback when the state has changed', async function () {
let onSaveCalled = false;
const {result} = renderHook(() => useForm({
initialState: {a: 1},
onSave: () => {
onSaveCalled = true;
}
}));
act(() => result.current.updateForm(state => ({...state, a: 2})));
assert.equal(await act(() => result.current.handleSave()), true);
assert.equal(result.current.saveState, 'saved');
assert.equal(onSaveCalled, true);
});
});
});

View file

@ -0,0 +1,58 @@
import {renderHook} from '@testing-library/react';
import React, {ReactNode} from 'react';
import FrameworkProvider from '../../../../src/providers/FrameworkProvider';
import {useFetchApi} from '../../../../src/utils/api/fetchApi';
import {withMockFetch} from '../../../utils/mockFetch';
const wrapper: React.FC<{ children: ReactNode }> = ({children}) => (
<FrameworkProvider
basePath=''
externalNavigate={() => {}}
ghostVersion='5.x'
sentryDSN=''
unsplashConfig={{
Authorization: '',
'Accept-Version': '',
'Content-Type': '',
'App-Pragma': '',
'X-Unsplash-Cache': true
}}
onDelete={() => {}}
onInvalidate={() => {}}
onUpdate={() => {}}
>
{children}
</FrameworkProvider>
);
describe('useFetchApi', function () {
it('makes an API request', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const {result} = renderHook(() => useFetchApi(), {wrapper});
const data = await result.current<{test: number}>('http://localhost:3000/ghost/api/admin/test/', {
method: 'POST',
body: 'test',
retry: false
});
expect(data).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/', {
body: 'test',
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'x-ghost-version': '5.x',
'content-type': 'application/json'
},
method: 'POST',
mode: 'cors',
signal: expect.any(AbortSignal)
}]);
});
});
});

View file

@ -0,0 +1,459 @@
import {InfiniteData, QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {act, renderHook, waitFor} from '@testing-library/react';
import React, {ReactNode} from 'react';
import FrameworkProvider from '../../../../src/providers/FrameworkProvider';
import {createInfiniteQuery, createMutation, createPaginatedQuery, createQuery, createQueryWithId} from '../../../../src/utils/api/hooks';
import {withMockFetch} from '../../../utils/mockFetch';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false
}
}
});
const wrapper: React.FC<{ children: ReactNode }> = ({children}) => (
<FrameworkProvider
basePath=''
externalNavigate={() => {}}
ghostVersion='5.x'
sentryDSN=''
unsplashConfig={{
Authorization: '',
'Accept-Version': '',
'Content-Type': '',
'App-Pragma': '',
'X-Unsplash-Cache': true
}}
onDelete={() => {}}
onInvalidate={() => {}}
onUpdate={() => {}}
>
{/* Being nested, this overrides the default QueryClientProvider from the framework */}
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</FrameworkProvider>
);
describe('API hooks', function () {
describe('createQuery', function () {
afterEach(function () {
queryClient.clear();
});
it('makes an API request', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestQuery = createQuery({
dataType: 'test',
path: '/test/'
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'x-ghost-version': '5.x'
},
method: 'GET',
mode: 'cors',
signal: expect.any(AbortSignal)
}]);
});
});
it('sends default query params', async function () {
await withMockFetch({}, async (mock) => {
const useTestQuery = createQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {a: '?'}
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?a=%3F');
});
});
it('can override default query params', async function () {
await withMockFetch({}, async (mock) => {
const useTestQuery = createQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {a: '?'}
});
const {result} = renderHook(() => useTestQuery({searchParams: {b: '1'}}), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?b=1');
});
});
it('can transform return data', async function () {
await withMockFetch({json: {test: 1}}, async () => {
const useTestQuery = createQuery({
dataType: 'test',
path: '/test/',
returnData: data => (data as {test: number}).test + 1
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual(2);
});
});
});
describe('createPaginatedQuery', function () {
afterEach(function () {
queryClient.clear();
});
it('makes a paginated API request', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/'
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/?page=1', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'x-ghost-version': '5.x'
},
method: 'GET',
mode: 'cors',
signal: expect.any(AbortSignal)
}]);
});
});
it('sends default query params', async function () {
await withMockFetch({}, async (mock) => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {a: '?'}
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?a=%3F&page=1');
});
});
it('can override default query params', async function () {
await withMockFetch({}, async (mock) => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {a: '?'}
});
const {result} = renderHook(() => useTestQuery({searchParams: {b: '1'}}), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?b=1&page=1');
});
});
it('can transform return data', async function () {
await withMockFetch({json: {test: 1}}, async () => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/',
returnData: data => ({test: (data as {test: number}).test + 1, meta: undefined})
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({test: 2});
});
});
it('exposes pagination metadata', async function () {
await withMockFetch({json: {meta: {pagination: {pages: 2, total: 100}}}}, async () => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/',
defaultSearchParams: {limit: '15'}
});
const {result} = renderHook(() => useTestQuery({}), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.pagination.limit).toEqual(15);
expect(result.current.pagination.page).toEqual(1);
expect(result.current.pagination.pages).toEqual(2);
expect(result.current.pagination.total).toEqual(100);
});
});
it('supports navigating pages', async function () {
await withMockFetch({json: {meta: {pagination: {pages: 2}}}}, async (mock) => {
const useTestQuery = createPaginatedQuery({
dataType: 'test',
path: '/test/'
});
const {result} = renderHook(() => useTestQuery({}), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=1');
act(() => result.current.pagination.nextPage());
await waitFor(() => expect(mock.calls.length).toBe(2));
expect(mock.calls[1][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=2');
act(() => result.current.pagination.prevPage());
await waitFor(() => expect(mock.calls.length).toBe(3));
expect(mock.calls[2][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=1');
act(() => result.current.pagination.setPage(5));
await waitFor(() => expect(mock.calls.length).toBe(4));
expect(mock.calls[3][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=5');
});
});
});
describe('createInfiniteQuery', function () {
afterEach(function () {
queryClient.clear();
});
it('makes a paginated API request', async function () {
await withMockFetch({
json: {test: 1, pagination: {next: 2}}
}, async (mock) => {
const useTestQuery = createInfiniteQuery({
dataType: 'test',
path: '/test/',
defaultNextPageParams: (lastPage, otherParams) => ({
...otherParams,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
page: ((lastPage as any).pagination.next || 1).toString()
}),
returnData: (originalData) => {
const {pages} = originalData as InfiniteData<{test: number}>;
return pages.map(page => page.test);
}
});
const {result} = renderHook(() => useTestQuery(), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual([1]);
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'x-ghost-version': '5.x'
},
method: 'GET',
mode: 'cors',
signal: expect.any(AbortSignal)
}]);
await act(() => result.current.fetchNextPage());
await waitFor(() => expect(mock.calls.length).toBe(2));
expect(mock.calls[1][0]).toEqual('http://localhost:3000/ghost/api/admin/test/?page=2');
await waitFor(() => expect(result.current.data).toEqual([1, 1]));
});
});
});
describe('createQueryWithId', function () {
afterEach(function () {
queryClient.clear();
});
it('fills in the ID in the request', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestQuery = createQueryWithId({
dataType: 'test',
path: id => `/test/${id}/`
});
const {result} = renderHook(() => useTestQuery('1'), {wrapper});
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0][0]).toEqual('http://localhost:3000/ghost/api/admin/test/1/');
});
});
});
describe('createMutation', function () {
afterEach(function () {
queryClient.clear();
});
it('makes a request', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestMutation = createMutation({
path: () => '/test/',
method: 'PUT'
});
const {result} = renderHook(() => useTestMutation(), {wrapper});
expect(await result.current.mutateAsync({})).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'x-ghost-version': '5.x'
},
method: 'PUT',
mode: 'cors',
body: undefined,
signal: expect.any(AbortSignal)
}]);
});
});
it('computes path, body, searchParams', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
const useTestMutation = createMutation({
path: payload => `/test/${payload}/`,
searchParams: payload => ({a: `${payload}`}),
body: payload => ({b: `${payload}`}),
method: 'POST'
});
const {result} = renderHook(() => useTestMutation(), {wrapper});
expect(await result.current.mutateAsync('hello')).toEqual({test: 1});
expect(mock.calls.length).toBe(1);
expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/hello/?a=hello', {
credentials: 'include',
headers: {
'app-pragma': 'no-cache',
'content-type': 'application/json',
'x-ghost-version': '5.x'
},
method: 'POST',
mode: 'cors',
body: '{"b":"hello"}',
signal: expect.any(AbortSignal)
}]);
});
});
it('can invalidate queries in the cache', async function () {
await withMockFetch({
json: {test: 1}
}, async (mock) => {
queryClient.setQueryData(['MyDataType', '1'], {test: 1});
queryClient.setQueryData(['MyDataType', '2'], {test: 2});
const useTestMutation = createMutation({
path: () => '/test/',
method: 'PUT',
invalidateQueries: {dataType: 'MyDataType'}
});
const {result} = renderHook(() => useTestMutation(), {wrapper});
await result.current.mutateAsync({});
expect(mock.calls.length).toBe(1);
expect(queryClient.getQueryState(['MyDataType', '1'])?.isInvalidated).toBe(true);
expect(queryClient.getQueryState(['MyDataType', '2'])?.isInvalidated).toBe(true);
});
});
it('can update queries in the cache', async function () {
await withMockFetch({
json: {test: 10}
}, async (mock) => {
queryClient.setQueryData(['MyDataType', '1'], {test: 1});
queryClient.setQueryData(['MyDataType', '2'], {test: 2});
const useTestMutation = createMutation({
path: () => '/test/',
method: 'PUT',
updateQueries: {
emberUpdateType: 'skip',
dataType: 'MyDataType',
update: (newData, currentData) => {
return {test: (newData as {test: number}).test + (currentData as {test: number}).test};
}
}
});
const {result} = renderHook(() => useTestMutation(), {wrapper});
await result.current.mutateAsync({});
expect(mock.calls.length).toBe(1);
expect(queryClient.getQueryData(['MyDataType', '1'])).toEqual({test: 11});
expect(queryClient.getQueryData(['MyDataType', '2'])).toEqual({test: 12});
});
});
});
});

View file

@ -0,0 +1,97 @@
import {deleteFromQueryCache, insertToQueryCache, updateQueryCache} from '../../../../src/utils/api/updateQueries';
describe('cache update functions', function () {
describe('insertToQueryCache', function () {
it('appends records from the new data', function () {
const newData = {
posts: [{id: '2'}]
};
const currentData = {
posts: [{id: '1'}]
};
const result = insertToQueryCache('posts')(newData, currentData);
expect(result).toEqual({
posts: [{id: '1'}, {id: '2'}]
});
});
it('appends to the last page for paginated queries', function () {
const newData = {
posts: [{id: '3'}]
};
const currentData = {
pages: [{posts: [{id: '1'}]}, {posts: [{id: '2'}]}]
};
const result = insertToQueryCache('posts')(newData, currentData);
expect(result).toEqual({
pages: [{posts: [{id: '1'}]}, {posts: [{id: '2'}, {id: '3'}]}]
});
});
});
describe('updateQueryCache', function () {
it('updates based on the ID', function () {
const newData = {
posts: [{id: '2', title: 'New Title'}]
};
const currentData = {
posts: [{id: '1'}, {id: '2', title: 'Old Title'}]
};
const result = updateQueryCache('posts')(newData, currentData);
expect(result).toEqual({
posts: [{id: '1'}, {id: '2', title: 'New Title'}]
});
});
it('updates nested records in paginated queries', function () {
const newData = {
posts: [{id: '2', title: 'New Title'}]
};
const currentData = {
pages: [{posts: [{id: '1'}]}, {posts: [{id: '2', title: 'Old Title'}]}]
};
const result = updateQueryCache('posts')(newData, currentData);
expect(result).toEqual({
pages: [{posts: [{id: '1'}]}, {posts: [{id: '2', title: 'New Title'}]}]
});
});
});
describe('deleteFromQueryCache', function () {
it('deletes based on the ID', function () {
const currentData = {
posts: [{id: '1'}, {id: '2'}]
};
const result = deleteFromQueryCache('posts')(null, currentData, '2');
expect(result).toEqual({
posts: [{id: '1'}]
});
});
it('deletes nested records in paginated queries', function () {
const currentData = {
pages: [{posts: [{id: '1'}]}, {posts: [{id: '2'}]}]
};
const result = deleteFromQueryCache('posts')(null, currentData, '2');
expect(result).toEqual({
pages: [{posts: [{id: '1'}]}, {posts: []}]
});
});
});
});

View file

@ -0,0 +1,23 @@
import {MockContext, vi} from 'vitest';
const originalFetch = global.fetch;
type FetchArgs = Parameters<typeof global.fetch>;
export const withMockFetch = async (
{json = {}, headers = {}, status = 200, ok = true}: {json?: unknown; headers?: Record<string, string>; status?: number; ok?: boolean},
callback: (mock: MockContext<FetchArgs, Promise<Response>>) => void | Promise<void>
) => {
const mockFetch = vi.fn<FetchArgs, Promise<Response>>(() => Promise.resolve({
json: () => Promise.resolve(json),
headers: new Headers(headers),
status,
ok
} as Response));
global.fetch = mockFetch as any; // eslint-disable-line @typescript-eslint/no-explicit-any
await callback(mockFetch.mock);
global.fetch = originalFetch;
};

View file

@ -2,7 +2,6 @@ import APIKeys from './APIKeys';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useState} from 'react';
import WebhooksTable from './WebhooksTable';
import useForm from '../../../../hooks/useForm';
import {APIKey, useRefreshAPIKey} from '@tryghost/admin-x-framework/api/apiKeys';
import {ConfirmationModal, Form, ImageUpload, Modal, TextField, showToast} from '@tryghost/admin-x-design-system';
import {Integration, useBrowseIntegrations, useEditIntegration} from '@tryghost/admin-x-framework/api/integrations';
@ -10,7 +9,7 @@ import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {toast} from 'react-hot-toast';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({integration}) => {
const modal = useModal();

View file

@ -1,12 +1,11 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import toast from 'react-hot-toast';
import useForm from '../../../../hooks/useForm';
import validator from 'validator';
import webhookEventOptions from './webhookEventOptions';
import {Form, Modal, Select, TextField, showToast} from '@tryghost/admin-x-design-system';
import {Webhook, useCreateWebhook, useEditWebhook} from '@tryghost/admin-x-framework/api/webhooks';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
interface WebhookModalProps {
webhook?: Webhook;

View file

@ -1,13 +1,12 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect} from 'react';
import useForm from '../../../../hooks/useForm';
import {Form, LimitModal, Modal, TextArea, TextField, Toggle, showToast} from '@tryghost/admin-x-design-system';
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {toast} from 'react-hot-toast';
import {useAddNewsletter} from '@tryghost/admin-x-framework/api/newsletters';
import {useBrowseMembers} from '@tryghost/admin-x-framework/api/members';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
const AddNewsletterModal: React.FC<RoutingModalProps> = () => {
const modal = useModal();

View file

@ -2,10 +2,10 @@ import NewsletterPreview from './NewsletterPreview';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useState} from 'react';
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import validator from 'validator';
import {Button, ButtonGroup, ColorPickerField, ConfirmationModal, Form, Heading, Hint, HtmlField, Icon, ImageUpload, LimitModal, PreviewModalContent, Select, SelectOption, Separator, Tab, TabView, TextArea, TextField, Toggle, ToggleGroup, showToast} from '@tryghost/admin-x-design-system';
import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '@tryghost/admin-x-framework/api/newsletters';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
@ -14,7 +14,6 @@ import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/image
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {textColorForBackgroundColor} from '@tryghost/color-utils';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
const Sidebar: React.FC<{
newsletter: Newsletter;

View file

@ -6,18 +6,17 @@ import ProfileDetails from './users/ProfileDetails';
import React, {useCallback, useEffect} from 'react';
import StaffToken from './users/StaffToken';
import clsx from 'clsx';
import useForm, {ErrorMessages} from '../../../hooks/useForm';
import usePinturaEditor from '../../../hooks/usePinturaEditor';
import useStaffUsers from '../../../hooks/useStaffUsers';
import validator from 'validator';
import {ConfirmationModal, Heading, Icon, ImageUpload, LimitModal, Menu, MenuItem, Modal, showToast} from '@tryghost/admin-x-design-system';
import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {User, canAccessSettings, hasAdminAccess, isAdminUser, isAuthorOrContributor, isEditorUser, isOwnerUser, useDeleteUser, useEditUser, useMakeOwner} from '@tryghost/admin-x-framework/api/users';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {toast} from 'react-hot-toast';
import {useGlobalData} from '../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {validateFacebookUrl, validateTwitterUrl} from '../../../utils/socialUrls';
const validators: Record<string, (u: Partial<User>) => string> = {

View file

@ -1,12 +1,12 @@
import PortalFrame from '../../membership/portal/PortalFrame';
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import useForm from '../../../../hooks/useForm';
import {Form, Icon, PreviewModalContent, Select, SelectOption, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system';
import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../utils/getOffersPortalPreviewUrl';
import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
import {getTiersCadences} from '../../../../utils/getTiersCadences';
import {useAddOffer} from '@tryghost/admin-x-framework/api/offers';
import {useEffect, useState} from 'react';
import {useForm} from '@tryghost/admin-x-framework/hooks';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useModal} from '@ebay/nice-modal-react';
import {useRouting} from '@tryghost/admin-x-framework/routing';

View file

@ -1,14 +1,13 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import PortalFrame from '../../membership/portal/PortalFrame';
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
import {Button, ConfirmationModal, Form, PreviewModalContent, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system';
import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {Offer, useBrowseOffersById, useEditOffer} from '@tryghost/admin-x-framework/api/offers';
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../utils/getOffersPortalPreviewUrl';
import {useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
function formatTimestamp(timestamp: string): string {

View file

@ -1,9 +1,9 @@
import AddRecommendationModalConfirm from './AddRecommendationModalConfirm';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useState} from 'react';
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
import {AlreadyExistsError} from '@tryghost/admin-x-framework/errors';
import {EditOrAddRecommendation, useCheckRecommendation} from '@tryghost/admin-x-framework/api/recommendations';
import {ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks';
import {Form, LoadingIndicator, Modal, TextField, dismissAllToasts, formatUrl, showToast} from '@tryghost/admin-x-design-system';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {trimSearchAndHash} from '../../../../utils/url';

View file

@ -3,10 +3,9 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import RecommendationDescriptionForm, {validateDescriptionForm} from './RecommendationDescriptionForm';
import trackEvent from '../../../../utils/plausible';
import useForm from '../../../../hooks/useForm';
import {EditOrAddRecommendation, useAddRecommendation} from '@tryghost/admin-x-framework/api/recommendations';
import {Modal, dismissAllToasts, showToast} from '@tryghost/admin-x-design-system';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
interface AddRecommendationModalProps {

View file

@ -1,11 +1,10 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import RecommendationDescriptionForm, {validateDescriptionForm} from './RecommendationDescriptionForm';
import useForm from '../../../../hooks/useForm';
import {ConfirmationModal, Modal, dismissAllToasts, showToast} from '@tryghost/admin-x-design-system';
import {Recommendation, useDeleteRecommendation, useEditRecommendation} from '@tryghost/admin-x-framework/api/recommendations';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
interface EditRecommendationModalProps {
recommendation: Recommendation,

View file

@ -1,7 +1,7 @@
import React from 'react';
import RecommendationIcon from './RecommendationIcon';
import {EditOrAddRecommendation, Recommendation} from '@tryghost/admin-x-framework/api/recommendations';
import {ErrorMessages} from '../../../../hooks/useForm';
import {ErrorMessages} from '@tryghost/admin-x-framework/hooks';
import {Form, Heading, Hint, TextArea, TextField, URLTextField} from '@tryghost/admin-x-design-system';
interface Props<T extends EditOrAddRecommendation> {

View file

@ -4,14 +4,13 @@ import NiceModal from '@ebay/nice-modal-react';
import PortalPreview from './PortalPreview';
import React, {useEffect, useState} from 'react';
import SignupOptions from './SignupOptions';
import useForm, {Dirtyable} from '../../../../hooks/useForm';
import useQueryParams from '../../../../hooks/useQueryParams';
import {ConfirmationModal, PreviewModalContent, Tab, TabView} from '@tryghost/admin-x-design-system';
import {Dirtyable, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {Setting, SettingValue, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {Tier, useBrowseTiers, useEditTier} from '@tryghost/admin-x-framework/api/tiers';
import {fullEmailAddress} from '@tryghost/admin-x-framework/api/site';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import {verifyEmailToken} from '@tryghost/admin-x-framework/api/emailVerification';

View file

@ -1,15 +1,14 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useRef} from 'react';
import TierDetailPreview from './TierDetailPreview';
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import {Button, ButtonProps, ConfirmationModal, CurrencyField, Form, Heading, Icon, Modal, Select, SortableList, TextField, Toggle, URLTextField, showToast, useSortableIndexedList} from '@tryghost/admin-x-design-system';
import {ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {Tier, useAddTier, useBrowseTiers, useEditTier} from '@tryghost/admin-x-framework/api/tiers';
import {currencies, currencySelectGroups, validateCurrencyAmount} from '../../../../utils/currency';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {toast} from 'react-hot-toast';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
export type TierFormState = Partial<Omit<Tier, 'trial_days'>> & {
trial_days: string;

View file

@ -2,15 +2,14 @@ import BrandSettings, {BrandSettingValues} from './designAndBranding/BrandSettin
import React, {useEffect, useState} from 'react';
import ThemePreview from './designAndBranding/ThemePreview';
import ThemeSettings from './designAndBranding/ThemeSettings';
import useForm from '../../../hooks/useForm';
import {CustomThemeSetting, useBrowseCustomThemeSettings, useEditCustomThemeSettings} from '@tryghost/admin-x-framework/api/customThemeSettings';
import {Icon, PreviewModalContent, StickyFooter, Tab, TabView} from '@tryghost/admin-x-design-system';
import {Setting, SettingValue, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
import {useBrowsePosts} from '@tryghost/admin-x-framework/api/posts';
import {useBrowseThemes} from '@tryghost/admin-x-framework/api/themes';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useGlobalData} from '../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const Sidebar: React.FC<{

View file

@ -1,11 +1,10 @@
import React, {useEffect, useRef, useState} from 'react';
import useForm, {ErrorMessages, OkProps, SaveHandler, SaveState} from './useForm';
import {ErrorMessages, OkProps, SaveHandler, SaveState, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {Setting, SettingValue, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {SiteData} from '@tryghost/admin-x-framework/api/site';
import {showToast, useGlobalDirtyState} from '@tryghost/admin-x-design-system';
import {toast} from 'react-hot-toast';
import {useGlobalData} from '../components/providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
interface LocalSetting extends Setting {
dirty?: boolean;

View file

@ -1,7 +1,7 @@
import assert from 'assert';
import {BookshelfRepository, ModelClass, ModelInstance} from '../src/index';
import {Knex} from 'knex';
import nql from '@tryghost/nql';
import assert from 'assert/strict';
import {Knex} from 'knex';
import {BookshelfRepository, ModelClass, ModelInstance} from '../src/index';
type SimpleEntity = {
id: string;

View file

@ -1,4 +1,4 @@
import assert from 'assert';
import assert from 'assert/strict';
import ObjectID from 'bson-objectid';
import {Collection} from '../src/index';

@ -1 +1 @@
Subproject commit 0798ee6e48273ab912d8e8e7ceee287bf481a789
Subproject commit 946e06311787f864cd347678fb9012f6d3abab22

View file

@ -1,4 +1,4 @@
import assert from 'assert';
import assert from 'assert/strict';
import {InMemoryRepository} from '../src/index';
type SimpleEntity = {

View file

@ -1,8 +1,8 @@
import assert from 'assert';
import assert from 'assert/strict';
import sinon from 'sinon';
import {MailEvent} from '../src/MailEvent';
import {InMemoryMailEventRepository as MailEventRepository} from '../src/InMemoryMailEventRepository';
import {MailEvent} from '../src/MailEvent';
import {MailEventService} from '../src/MailEventService';
const makePayloadEvent = (

View file

@ -1,4 +1,4 @@
import assert from 'assert';
import assert from 'assert/strict';
import sinon from 'sinon';
import {PostRevisions} from '../src';

View file

@ -6,10 +6,13 @@
"repository": "https://github.com/TryGhost/Ghost",
"author": "Ghost Foundation",
"license": "MIT",
"workspaces": [
"ghost/*",
"apps/*"
],
"workspaces": {
"packages": [
"ghost/*",
"apps/*"
],
"nohoist": ["**/@testing-library/react"]
},
"monorepo": {
"public": false,
"internalPackages": true,
@ -108,7 +111,7 @@
"chalk": "4.1.2",
"concurrently": "8.2.2",
"eslint": "8.44.0",
"eslint-plugin-ghost": "3.3.2",
"eslint-plugin-ghost": "3.4.0",
"eslint-plugin-react": "7.33.0",
"husky": "8.0.3",
"lint-staged": "14.0.1",

146
yarn.lock
View file

@ -7408,6 +7408,15 @@
"@testing-library/dom" "^8.0.0"
"@types/react-dom" "<18.0.0"
"@testing-library/react@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.1.0.tgz#01d64915111db99b50f8361d51d7217606805989"
integrity sha512-hcvfZEEyO0xQoZeHmUbuMs7APJCGELpilL7bY+BaJaMP57aWc6q1etFwScnoZDheYjk4ESdlzPdQ33IbsKAK/A==
dependencies:
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^9.0.0"
"@types/react-dom" "^18.0.0"
"@testing-library/user-event@14.4.3":
version "14.4.3"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591"
@ -8654,7 +8663,7 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-dom@18.2.15":
"@types/react-dom@18.2.15", "@types/react-dom@^18.0.0":
version "18.2.15"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.15.tgz#921af67f9ee023ac37ea84b1bc0cc40b898ea522"
integrity sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==
@ -8810,16 +8819,16 @@
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz#19ba09aa34fd504696445100262e5a9e1b1d7024"
integrity sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==
"@typescript-eslint/eslint-plugin@6.9.1":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz#d8ce497dc0ed42066e195c8ecc40d45c7b1254f4"
integrity sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==
dependencies:
"@eslint-community/regexpp" "^4.5.1"
"@typescript-eslint/scope-manager" "6.6.0"
"@typescript-eslint/type-utils" "6.6.0"
"@typescript-eslint/utils" "6.6.0"
"@typescript-eslint/visitor-keys" "6.6.0"
"@typescript-eslint/scope-manager" "6.9.1"
"@typescript-eslint/type-utils" "6.9.1"
"@typescript-eslint/utils" "6.9.1"
"@typescript-eslint/visitor-keys" "6.9.1"
debug "^4.3.4"
graphemer "^1.4.0"
ignore "^5.2.4"
@ -8827,72 +8836,72 @@
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/parser@6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.6.0.tgz#fe323a7b4eafb6d5ea82b96216561810394a739e"
integrity sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==
"@typescript-eslint/parser@6.9.1":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.9.1.tgz#4f685f672f8b9580beb38d5fb99d52fc3e34f7a3"
integrity sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==
dependencies:
"@typescript-eslint/scope-manager" "6.6.0"
"@typescript-eslint/types" "6.6.0"
"@typescript-eslint/typescript-estree" "6.6.0"
"@typescript-eslint/visitor-keys" "6.6.0"
"@typescript-eslint/scope-manager" "6.9.1"
"@typescript-eslint/types" "6.9.1"
"@typescript-eslint/typescript-estree" "6.9.1"
"@typescript-eslint/visitor-keys" "6.9.1"
debug "^4.3.4"
"@typescript-eslint/scope-manager@6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz#57105d4419d6de971f7d2c30a2ff4ac40003f61a"
integrity sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==
"@typescript-eslint/scope-manager@6.9.1":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz#e96afeb9a68ad1cd816dba233351f61e13956b75"
integrity sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==
dependencies:
"@typescript-eslint/types" "6.6.0"
"@typescript-eslint/visitor-keys" "6.6.0"
"@typescript-eslint/types" "6.9.1"
"@typescript-eslint/visitor-keys" "6.9.1"
"@typescript-eslint/type-utils@6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz#14f651d13b884915c4fca0d27adeb652a4499e86"
integrity sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==
"@typescript-eslint/type-utils@6.9.1":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz#efd5db20ed35a74d3c7d8fba51b830ecba09ce32"
integrity sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==
dependencies:
"@typescript-eslint/typescript-estree" "6.6.0"
"@typescript-eslint/utils" "6.6.0"
"@typescript-eslint/typescript-estree" "6.9.1"
"@typescript-eslint/utils" "6.9.1"
debug "^4.3.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/types@6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.6.0.tgz#95e7ea650a2b28bc5af5ea8907114a48f54618c2"
integrity sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==
"@typescript-eslint/types@6.9.1":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.9.1.tgz#a6cfc20db0fcedcb2f397ea728ef583e0ee72459"
integrity sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==
"@typescript-eslint/typescript-estree@6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz#373c420d2e12c28220f4a83352280a04823a91b7"
integrity sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==
"@typescript-eslint/typescript-estree@6.9.1":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz#8c77910a49a04f0607ba94d78772da07dab275ad"
integrity sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==
dependencies:
"@typescript-eslint/types" "6.6.0"
"@typescript-eslint/visitor-keys" "6.6.0"
"@typescript-eslint/types" "6.9.1"
"@typescript-eslint/visitor-keys" "6.9.1"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/utils@6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.6.0.tgz#2d686c0f0786da6362d909e27a9de1c13ba2e7dc"
integrity sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==
"@typescript-eslint/utils@6.9.1":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.9.1.tgz#763da41281ef0d16974517b5f0d02d85897a1c1e"
integrity sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "6.6.0"
"@typescript-eslint/types" "6.6.0"
"@typescript-eslint/typescript-estree" "6.6.0"
"@typescript-eslint/scope-manager" "6.9.1"
"@typescript-eslint/types" "6.9.1"
"@typescript-eslint/typescript-estree" "6.9.1"
semver "^7.5.4"
"@typescript-eslint/visitor-keys@6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz#1109088b4346c8b2446f3845db526374d9a3bafc"
integrity sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==
"@typescript-eslint/visitor-keys@6.9.1":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz#6753a9225a0ba00459b15d6456b9c2780b66707d"
integrity sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==
dependencies:
"@typescript-eslint/types" "6.6.0"
"@typescript-eslint/types" "6.9.1"
eslint-visitor-keys "^3.4.1"
"@tyriar/fibonacci-heap@^2.0.7":
@ -16785,18 +16794,18 @@ eslint-plugin-filenames@allouis/eslint-plugin-filenames#15dc354f4e3d155fc2d6ae08
lodash.snakecase "4.1.1"
lodash.upperfirst "4.3.1"
eslint-plugin-ghost@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-ghost/-/eslint-plugin-ghost-3.3.2.tgz#b789c20e645d285743bc8c852f92b9898474eb98"
integrity sha512-Ae3u4lTo2ApB8wBdaEShJVEuqNSYG1IiarAFvee8bSx8Ykwj0vRxpyy9MRgwaFc6Lre7dcaIJ60iWVBoO4MwwA==
eslint-plugin-ghost@3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-ghost/-/eslint-plugin-ghost-3.4.0.tgz#2a84e53e1bdc3ca722e2886e49d5de0dd2344d6f"
integrity sha512-uj/ClW5yyfm0tHikI7jyWJbMqLVnn3PTr5GwVc0NTciM9tgkfS7TxEi8jAEa1idLRGPou+gF+Tvt0QBI/29S4g==
dependencies:
"@kapouer/eslint-plugin-no-return-in-loop" "1.0.0"
"@typescript-eslint/eslint-plugin" "6.6.0"
"@typescript-eslint/parser" "6.6.0"
"@typescript-eslint/eslint-plugin" "6.9.1"
"@typescript-eslint/parser" "6.9.1"
eslint-plugin-ember "11.11.1"
eslint-plugin-filenames allouis/eslint-plugin-filenames#15dc354f4e3d155fc2d6ae082dbfc26377539a18
eslint-plugin-mocha "7.0.1"
eslint-plugin-n "16.0.2"
eslint-plugin-n "16.2.0"
eslint-plugin-sort-imports-es6-autofix "0.6.0"
eslint-plugin-unicorn "42.0.0"
typescript "5.2.2"
@ -16817,14 +16826,15 @@ eslint-plugin-mocha@7.0.1:
eslint-utils "^2.0.0"
ramda "^0.27.0"
eslint-plugin-n@16.0.2:
version "16.0.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-16.0.2.tgz#5b2c0ad8dd9b724244d30fad2cc49ff4308a2152"
integrity sha512-Y66uDfUNbBzypsr0kELWrIz+5skicECrLUqlWuXawNSLUq3ltGlCwu6phboYYOTSnoTdHgTLrc+5Ydo6KjzZog==
eslint-plugin-n@16.2.0:
version "16.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-16.2.0.tgz#3f98ca9fadd9f7bdaaf60068533118ecb685bfb5"
integrity sha512-AQER2jEyQOt1LG6JkGJCCIFotzmlcCZFur2wdKrp1JX2cNotC7Ae0BcD/4lLv3lUAArM9uNS8z/fsvXTd0L71g==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
builtins "^5.0.1"
eslint-plugin-es-x "^7.1.0"
get-tsconfig "^4.7.0"
ignore "^5.2.4"
is-core-module "^2.12.1"
minimatch "^3.1.2"
@ -18518,6 +18528,13 @@ get-symbol-description@^1.0.0:
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
get-tsconfig@^4.7.0:
version "4.7.2"
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.2.tgz#0dcd6fb330391d46332f4c6c1bf89a6514c2ddce"
integrity sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==
dependencies:
resolve-pkg-maps "^1.0.0"
get-value@^2.0.3, get-value@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@ -27727,6 +27744,11 @@ resolve-path@^1.4.0:
http-errors "~1.6.2"
path-is-absolute "1.0.1"
resolve-pkg-maps@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f"
integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==
resolve-url-loader@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795"