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:
parent
b1666f596f
commit
5e057dee11
@ -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"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>({
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
});
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/test'
|
||||
'plugin:ghost/ts-test'
|
||||
]
|
||||
};
|
||||
|
@ -1,8 +0,0 @@
|
||||
import assert from 'assert/strict';
|
||||
|
||||
describe('Hello world', function () {
|
||||
it('Runs a test', function () {
|
||||
// TODO: Write me!
|
||||
assert.ok(require('../'));
|
||||
});
|
||||
});
|
75
apps/admin-x-framework/test/unit/hooks/useForm.test.ts
Normal file
75
apps/admin-x-framework/test/unit/hooks/useForm.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
58
apps/admin-x-framework/test/unit/utils/api/fetchApi.test.tsx
Normal file
58
apps/admin-x-framework/test/unit/utils/api/fetchApi.test.tsx
Normal 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)
|
||||
}]);
|
||||
});
|
||||
});
|
||||
});
|
459
apps/admin-x-framework/test/unit/utils/api/hooks.test.tsx
Normal file
459
apps/admin-x-framework/test/unit/utils/api/hooks.test.tsx
Normal 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});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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: []}]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
23
apps/admin-x-framework/test/utils/mockFetch.ts
Normal file
23
apps/admin-x-framework/test/utils/mockFetch.ts
Normal 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;
|
||||
};
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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> = {
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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> {
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
@ -1,4 +1,4 @@
|
||||
import assert from 'assert';
|
||||
import assert from 'assert/strict';
|
||||
import {InMemoryRepository} from '../src/index';
|
||||
|
||||
type SimpleEntity = {
|
||||
|
@ -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 = (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import assert from 'assert';
|
||||
import assert from 'assert/strict';
|
||||
import sinon from 'sinon';
|
||||
import {PostRevisions} from '../src';
|
||||
|
||||
|
13
package.json
13
package.json
@ -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
146
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user