Merge branch 'create-group-hook' into 'develop'
Add useEntityActions hooks See merge request soapbox-pub/soapbox!2351
This commit is contained in:
commit
822ab987a1
|
@ -1,5 +1,6 @@
|
||||||
import { entitiesFetchFail, entitiesFetchRequest, importEntities } from '../actions';
|
import { deleteEntities, entitiesFetchFail, entitiesFetchRequest, importEntities } from '../actions';
|
||||||
import reducer from '../reducer';
|
import reducer, { State } from '../reducer';
|
||||||
|
import { createListState } from '../utils';
|
||||||
|
|
||||||
import type { EntityCache } from '../types';
|
import type { EntityCache } from '../types';
|
||||||
|
|
||||||
|
@ -76,4 +77,24 @@ test('failure adds the error to the state', () => {
|
||||||
const result = reducer(undefined, action);
|
const result = reducer(undefined, action);
|
||||||
|
|
||||||
expect(result.TestEntity!.lists.thingies!.state.error).toBe(error);
|
expect(result.TestEntity!.lists.thingies!.state.error).toBe(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deleting items', () => {
|
||||||
|
const state: State = {
|
||||||
|
TestEntity: {
|
||||||
|
store: { '1': { id: '1' }, '2': { id: '2' }, '3': { id: '3' } },
|
||||||
|
lists: {
|
||||||
|
'': {
|
||||||
|
ids: new Set(['1', '2', '3']),
|
||||||
|
state: createListState(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = deleteEntities(['3', '1'], 'TestEntity');
|
||||||
|
const result = reducer(state, action);
|
||||||
|
|
||||||
|
expect(result.TestEntity!.store).toMatchObject({ '2': { id: '2' } });
|
||||||
|
expect([...result.TestEntity!.lists['']!.ids]).toEqual(['2']);
|
||||||
});
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Entity, EntityListState } from './types';
|
import type { Entity, EntityListState } from './types';
|
||||||
|
|
||||||
const ENTITIES_IMPORT = 'ENTITIES_IMPORT' as const;
|
const ENTITIES_IMPORT = 'ENTITIES_IMPORT' as const;
|
||||||
|
const ENTITIES_DELETE = 'ENTITIES_DELETE' as const;
|
||||||
const ENTITIES_FETCH_REQUEST = 'ENTITIES_FETCH_REQUEST' as const;
|
const ENTITIES_FETCH_REQUEST = 'ENTITIES_FETCH_REQUEST' as const;
|
||||||
const ENTITIES_FETCH_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const;
|
const ENTITIES_FETCH_SUCCESS = 'ENTITIES_FETCH_SUCCESS' as const;
|
||||||
const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const;
|
const ENTITIES_FETCH_FAIL = 'ENTITIES_FETCH_FAIL' as const;
|
||||||
|
@ -15,6 +16,14 @@ function importEntities(entities: Entity[], entityType: string, listKey?: string
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteEntities(ids: Iterable<string>, entityType: string) {
|
||||||
|
return {
|
||||||
|
type: ENTITIES_DELETE,
|
||||||
|
ids,
|
||||||
|
entityType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function entitiesFetchRequest(entityType: string, listKey?: string) {
|
function entitiesFetchRequest(entityType: string, listKey?: string) {
|
||||||
return {
|
return {
|
||||||
type: ENTITIES_FETCH_REQUEST,
|
type: ENTITIES_FETCH_REQUEST,
|
||||||
|
@ -45,16 +54,19 @@ function entitiesFetchFail(entityType: string, listKey: string | undefined, erro
|
||||||
/** Any action pertaining to entities. */
|
/** Any action pertaining to entities. */
|
||||||
type EntityAction =
|
type EntityAction =
|
||||||
ReturnType<typeof importEntities>
|
ReturnType<typeof importEntities>
|
||||||
|
| ReturnType<typeof deleteEntities>
|
||||||
| ReturnType<typeof entitiesFetchRequest>
|
| ReturnType<typeof entitiesFetchRequest>
|
||||||
| ReturnType<typeof entitiesFetchSuccess>
|
| ReturnType<typeof entitiesFetchSuccess>
|
||||||
| ReturnType<typeof entitiesFetchFail>;
|
| ReturnType<typeof entitiesFetchFail>;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ENTITIES_IMPORT,
|
ENTITIES_IMPORT,
|
||||||
|
ENTITIES_DELETE,
|
||||||
ENTITIES_FETCH_REQUEST,
|
ENTITIES_FETCH_REQUEST,
|
||||||
ENTITIES_FETCH_SUCCESS,
|
ENTITIES_FETCH_SUCCESS,
|
||||||
ENTITIES_FETCH_FAIL,
|
ENTITIES_FETCH_FAIL,
|
||||||
importEntities,
|
importEntities,
|
||||||
|
deleteEntities,
|
||||||
entitiesFetchRequest,
|
entitiesFetchRequest,
|
||||||
entitiesFetchSuccess,
|
entitiesFetchSuccess,
|
||||||
entitiesFetchFail,
|
entitiesFetchFail,
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export { useEntities } from './useEntities';
|
export { useEntities } from './useEntities';
|
||||||
export { useEntity } from './useEntity';
|
export { useEntity } from './useEntity';
|
||||||
|
export { useEntityActions } from './useEntityActions';
|
|
@ -0,0 +1,6 @@
|
||||||
|
import type { Entity } from '../types';
|
||||||
|
import type z from 'zod';
|
||||||
|
|
||||||
|
type EntitySchema<TEntity extends Entity = Entity> = z.ZodType<TEntity, z.ZodTypeDef, any>;
|
||||||
|
|
||||||
|
export type { EntitySchema };
|
|
@ -8,6 +8,7 @@ import { filteredArray } from 'soapbox/schemas/utils';
|
||||||
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions';
|
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess } from '../actions';
|
||||||
|
|
||||||
import type { Entity, EntityListState } from '../types';
|
import type { Entity, EntityListState } from '../types';
|
||||||
|
import type { EntitySchema } from './types';
|
||||||
import type { RootState } from 'soapbox/store';
|
import type { RootState } from 'soapbox/store';
|
||||||
|
|
||||||
/** Tells us where to find/store the entity in the cache. */
|
/** Tells us where to find/store the entity in the cache. */
|
||||||
|
@ -25,7 +26,7 @@ type EntityPath = [
|
||||||
/** Additional options for the hook. */
|
/** Additional options for the hook. */
|
||||||
interface UseEntitiesOpts<TEntity extends Entity> {
|
interface UseEntitiesOpts<TEntity extends Entity> {
|
||||||
/** A zod schema to parse the API entities. */
|
/** A zod schema to parse the API entities. */
|
||||||
schema?: z.ZodType<TEntity, z.ZodTypeDef, any>
|
schema?: EntitySchema<TEntity>
|
||||||
/**
|
/**
|
||||||
* Time (milliseconds) until this query becomes stale and should be refetched.
|
* Time (milliseconds) until this query becomes stale and should be refetched.
|
||||||
* It is 1 minute by default, and can be set to `Infinity` to opt-out of automatic fetching.
|
* It is 1 minute by default, and can be set to `Infinity` to opt-out of automatic fetching.
|
||||||
|
|
|
@ -6,13 +6,14 @@ import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
import { importEntities } from '../actions';
|
import { importEntities } from '../actions';
|
||||||
|
|
||||||
import type { Entity } from '../types';
|
import type { Entity } from '../types';
|
||||||
|
import type { EntitySchema } from './types';
|
||||||
|
|
||||||
type EntityPath = [entityType: string, entityId: string]
|
type EntityPath = [entityType: string, entityId: string]
|
||||||
|
|
||||||
/** Additional options for the hook. */
|
/** Additional options for the hook. */
|
||||||
interface UseEntityOpts<TEntity> {
|
interface UseEntityOpts<TEntity extends Entity> {
|
||||||
/** A zod schema to parse the API entity. */
|
/** A zod schema to parse the API entity. */
|
||||||
schema?: z.ZodType<TEntity, z.ZodTypeDef, any>
|
schema?: EntitySchema<TEntity>
|
||||||
/** Whether to refetch this entity every time the hook mounts, even if it's already in the store. */
|
/** Whether to refetch this entity every time the hook mounts, even if it's already in the store. */
|
||||||
refetch?: boolean
|
refetch?: boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { useApi, useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import { importEntities } from '../actions';
|
||||||
|
|
||||||
|
import type { Entity } from '../types';
|
||||||
|
import type { EntitySchema } from './types';
|
||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
type EntityPath = [entityType: string, listKey?: string]
|
||||||
|
|
||||||
|
interface UseEntityActionsOpts<TEntity extends Entity = Entity> {
|
||||||
|
schema?: EntitySchema<TEntity>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateEntityResult<TEntity extends Entity = Entity> {
|
||||||
|
response: AxiosResponse
|
||||||
|
entity: TEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteEntityResult {
|
||||||
|
response: AxiosResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntityActionEndpoints {
|
||||||
|
post?: string
|
||||||
|
delete?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function useEntityActions<TEntity extends Entity = Entity, P = any>(
|
||||||
|
path: EntityPath,
|
||||||
|
endpoints: EntityActionEndpoints,
|
||||||
|
opts: UseEntityActionsOpts<TEntity> = {},
|
||||||
|
) {
|
||||||
|
const api = useApi();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [entityType, listKey] = path;
|
||||||
|
|
||||||
|
function createEntity(params: P): Promise<CreateEntityResult<TEntity>> {
|
||||||
|
if (!endpoints.post) return Promise.reject(endpoints);
|
||||||
|
|
||||||
|
return api.post(endpoints.post, params).then((response) => {
|
||||||
|
const schema = opts.schema || z.custom<TEntity>();
|
||||||
|
const entity = schema.parse(response.data);
|
||||||
|
|
||||||
|
// TODO: optimistic updating
|
||||||
|
dispatch(importEntities([entity], entityType, listKey));
|
||||||
|
|
||||||
|
return {
|
||||||
|
response,
|
||||||
|
entity,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteEntity(entityId: string): Promise<DeleteEntityResult> {
|
||||||
|
if (!endpoints.delete) return Promise.reject(endpoints);
|
||||||
|
return api.delete(endpoints.delete.replaceAll(':id', entityId)).then((response) => {
|
||||||
|
|
||||||
|
return {
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createEntity: endpoints.post ? createEntity : undefined,
|
||||||
|
deleteEntity: endpoints.delete ? deleteEntity : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useEntityActions };
|
|
@ -2,6 +2,7 @@ import produce, { enableMapSet } from 'immer';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ENTITIES_IMPORT,
|
ENTITIES_IMPORT,
|
||||||
|
ENTITIES_DELETE,
|
||||||
ENTITIES_FETCH_REQUEST,
|
ENTITIES_FETCH_REQUEST,
|
||||||
ENTITIES_FETCH_SUCCESS,
|
ENTITIES_FETCH_SUCCESS,
|
||||||
ENTITIES_FETCH_FAIL,
|
ENTITIES_FETCH_FAIL,
|
||||||
|
@ -43,6 +44,26 @@ const importEntities = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteEntities = (
|
||||||
|
state: State,
|
||||||
|
entityType: string,
|
||||||
|
ids: Iterable<string>,
|
||||||
|
) => {
|
||||||
|
return produce(state, draft => {
|
||||||
|
const cache = draft[entityType] ?? createCache();
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
delete cache.store[id];
|
||||||
|
|
||||||
|
for (const list of Object.values(cache.lists)) {
|
||||||
|
list?.ids.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draft[entityType] = cache;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const setFetching = (
|
const setFetching = (
|
||||||
state: State,
|
state: State,
|
||||||
entityType: string,
|
entityType: string,
|
||||||
|
@ -69,6 +90,8 @@ function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ENTITIES_IMPORT:
|
case ENTITIES_IMPORT:
|
||||||
return importEntities(state, action.entityType, action.entities, action.listKey);
|
return importEntities(state, action.entityType, action.entities, action.listKey);
|
||||||
|
case ENTITIES_DELETE:
|
||||||
|
return deleteEntities(state, action.entityType, action.ids);
|
||||||
case ENTITIES_FETCH_SUCCESS:
|
case ENTITIES_FETCH_SUCCESS:
|
||||||
return importEntities(state, action.entityType, action.entities, action.listKey, action.newState);
|
return importEntities(state, action.entityType, action.entities, action.listKey, action.newState);
|
||||||
case ENTITIES_FETCH_REQUEST:
|
case ENTITIES_FETCH_REQUEST:
|
||||||
|
@ -80,4 +103,5 @@ function reducer(state: Readonly<State> = {}, action: EntityAction): State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default reducer;
|
export default reducer;
|
||||||
|
export type { State };
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Entity, EntityStore, EntityList, EntityCache } from './types';
|
import type { Entity, EntityStore, EntityList, EntityCache, EntityListState } from './types';
|
||||||
|
|
||||||
/** Insert the entities into the store. */
|
/** Insert the entities into the store. */
|
||||||
const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => {
|
const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => {
|
||||||
|
@ -26,14 +26,17 @@ const createCache = (): EntityCache => ({
|
||||||
/** Create an empty entity list. */
|
/** Create an empty entity list. */
|
||||||
const createList = (): EntityList => ({
|
const createList = (): EntityList => ({
|
||||||
ids: new Set(),
|
ids: new Set(),
|
||||||
state: {
|
state: createListState(),
|
||||||
next: undefined,
|
});
|
||||||
prev: undefined,
|
|
||||||
error: null,
|
/** Create an empty entity list state. */
|
||||||
fetched: false,
|
const createListState = (): EntityListState => ({
|
||||||
fetching: false,
|
next: undefined,
|
||||||
lastFetchedAt: undefined,
|
prev: undefined,
|
||||||
},
|
error: null,
|
||||||
|
fetched: false,
|
||||||
|
fetching: false,
|
||||||
|
lastFetchedAt: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -41,4 +44,5 @@ export {
|
||||||
updateList,
|
updateList,
|
||||||
createCache,
|
createCache,
|
||||||
createList,
|
createList,
|
||||||
|
createListState,
|
||||||
};
|
};
|
|
@ -6,7 +6,7 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"module": "es2022",
|
"module": "es2022",
|
||||||
"lib": ["es2019", "es6", "dom", "webworker"],
|
"lib": ["es2019", "es6", "dom", "webworker"],
|
||||||
"target": "es5",
|
"target": "es2015",
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
|
Loading…
Reference in New Issue