Merge branch 'entity-request' into 'develop'
EntityStore: refactor all hooks to use callbacks See merge request soapbox-pub/soapbox!2379
This commit is contained in:
commit
7c1182bfb3
|
@ -1,4 +1,5 @@
|
||||||
import type { Entity } from '../types';
|
import type { Entity } from '../types';
|
||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
import type z from 'zod';
|
import type z from 'zod';
|
||||||
|
|
||||||
type EntitySchema<TEntity extends Entity = Entity> = z.ZodType<TEntity, z.ZodTypeDef, any>;
|
type EntitySchema<TEntity extends Entity = Entity> = z.ZodType<TEntity, z.ZodTypeDef, any>;
|
||||||
|
@ -24,9 +25,23 @@ type EntitiesPath = [entityType: string, listKey: string]
|
||||||
/** Used to look up a single entity by its ID. */
|
/** Used to look up a single entity by its ID. */
|
||||||
type EntityPath = [entityType: string, entityId: string]
|
type EntityPath = [entityType: string, entityId: string]
|
||||||
|
|
||||||
|
/** Callback functions for entity actions. */
|
||||||
|
interface EntityCallbacks<Value, Error = unknown> {
|
||||||
|
onSuccess?(value: Value): void
|
||||||
|
onError?(error: Error): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passed into hooks to make requests.
|
||||||
|
* Must return an Axios response.
|
||||||
|
*/
|
||||||
|
type EntityFn<T> = (value: T) => Promise<AxiosResponse>
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
EntitySchema,
|
EntitySchema,
|
||||||
ExpandedEntitiesPath,
|
ExpandedEntitiesPath,
|
||||||
EntitiesPath,
|
EntitiesPath,
|
||||||
EntityPath,
|
EntityPath,
|
||||||
|
EntityCallbacks,
|
||||||
|
EntityFn,
|
||||||
};
|
};
|
|
@ -1,52 +1,33 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch, useLoading } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { importEntities } from '../actions';
|
import { importEntities } from '../actions';
|
||||||
|
|
||||||
import { parseEntitiesPath } from './utils';
|
import { parseEntitiesPath } from './utils';
|
||||||
|
|
||||||
import type { Entity } from '../types';
|
import type { Entity } from '../types';
|
||||||
import type { EntitySchema, ExpandedEntitiesPath } from './types';
|
import type { EntityCallbacks, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
|
||||||
|
|
||||||
type CreateFn<Params, Result> = (params: Params) => Promise<Result> | Result;
|
|
||||||
|
|
||||||
interface UseCreateEntityOpts<TEntity extends Entity = Entity> {
|
interface UseCreateEntityOpts<TEntity extends Entity = Entity> {
|
||||||
schema?: EntitySchema<TEntity>
|
schema?: EntitySchema<TEntity>
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateEntityResult<TEntity extends Entity = Entity, Result = unknown, Error = unknown> =
|
function useCreateEntity<TEntity extends Entity = Entity, Data = unknown>(
|
||||||
{
|
|
||||||
success: true
|
|
||||||
result: Result
|
|
||||||
entity: TEntity
|
|
||||||
} | {
|
|
||||||
success: false
|
|
||||||
error: Error
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EntityCallbacks<TEntity extends Entity = Entity, Error = unknown> {
|
|
||||||
onSuccess?(entity: TEntity): void
|
|
||||||
onError?(error: Error): void
|
|
||||||
}
|
|
||||||
|
|
||||||
function useCreateEntity<TEntity extends Entity = Entity, Params = any, Result = unknown>(
|
|
||||||
expandedPath: ExpandedEntitiesPath,
|
expandedPath: ExpandedEntitiesPath,
|
||||||
createFn: CreateFn<Params, Result>,
|
entityFn: EntityFn<Data>,
|
||||||
opts: UseCreateEntityOpts<TEntity> = {},
|
opts: UseCreateEntityOpts<TEntity> = {},
|
||||||
) {
|
) {
|
||||||
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
return async function createEntity(
|
const [isLoading, setPromise] = useLoading();
|
||||||
params: Params,
|
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
||||||
callbacks: EntityCallbacks = {},
|
|
||||||
): Promise<CreateEntityResult<TEntity>> {
|
async function createEntity(data: Data, callbacks: EntityCallbacks<TEntity> = {}): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await createFn(params);
|
const result = await setPromise(entityFn(data));
|
||||||
const schema = opts.schema || z.custom<TEntity>();
|
const schema = opts.schema || z.custom<TEntity>();
|
||||||
const entity = schema.parse(result);
|
const entity = schema.parse(result.data);
|
||||||
|
|
||||||
// TODO: optimistic updating
|
// TODO: optimistic updating
|
||||||
dispatch(importEntities([entity], entityType, listKey));
|
dispatch(importEntities([entity], entityType, listKey));
|
||||||
|
@ -54,22 +35,16 @@ function useCreateEntity<TEntity extends Entity = Entity, Params = any, Result =
|
||||||
if (callbacks.onSuccess) {
|
if (callbacks.onSuccess) {
|
||||||
callbacks.onSuccess(entity);
|
callbacks.onSuccess(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
result,
|
|
||||||
entity,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (callbacks.onError) {
|
if (callbacks.onError) {
|
||||||
callbacks.onError(error);
|
callbacks.onError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createEntity,
|
||||||
|
isLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,23 @@
|
||||||
import { useAppDispatch, useGetState } from 'soapbox/hooks';
|
import { useAppDispatch, useGetState, useLoading } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { deleteEntities, importEntities } from '../actions';
|
import { deleteEntities, importEntities } from '../actions';
|
||||||
|
|
||||||
type DeleteFn<T> = (entityId: string) => Promise<T> | T;
|
import type { EntityCallbacks, EntityFn } from './types';
|
||||||
|
|
||||||
interface EntityCallbacks {
|
|
||||||
onSuccess?(): void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optimistically deletes an entity from the store.
|
* Optimistically deletes an entity from the store.
|
||||||
* This hook should be used to globally delete an entity from all lists.
|
* This hook should be used to globally delete an entity from all lists.
|
||||||
* To remove an entity from a single list, see `useDismissEntity`.
|
* To remove an entity from a single list, see `useDismissEntity`.
|
||||||
*/
|
*/
|
||||||
function useDeleteEntity<T = unknown>(
|
function useDeleteEntity(
|
||||||
entityType: string,
|
entityType: string,
|
||||||
deleteFn: DeleteFn<T>,
|
entityFn: EntityFn<string>,
|
||||||
) {
|
) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const getState = useGetState();
|
const getState = useGetState();
|
||||||
|
const [isLoading, setPromise] = useLoading();
|
||||||
|
|
||||||
return async function deleteEntity(entityId: string, callbacks: EntityCallbacks = {}): Promise<T> {
|
async function deleteEntity(entityId: string, callbacks: EntityCallbacks<string> = {}): Promise<void> {
|
||||||
// Get the entity before deleting, so we can reverse the action if the API request fails.
|
// Get the entity before deleting, so we can reverse the action if the API request fails.
|
||||||
const entity = getState().entities[entityType]?.store[entityId];
|
const entity = getState().entities[entityType]?.store[entityId];
|
||||||
|
|
||||||
|
@ -28,22 +25,29 @@ function useDeleteEntity<T = unknown>(
|
||||||
dispatch(deleteEntities([entityId], entityType, { preserveLists: true }));
|
dispatch(deleteEntities([entityId], entityType, { preserveLists: true }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await deleteFn(entityId);
|
await setPromise(entityFn(entityId));
|
||||||
|
|
||||||
// Success - finish deleting entity from the state.
|
// Success - finish deleting entity from the state.
|
||||||
dispatch(deleteEntities([entityId], entityType));
|
dispatch(deleteEntities([entityId], entityType));
|
||||||
|
|
||||||
if (callbacks.onSuccess) {
|
if (callbacks.onSuccess) {
|
||||||
callbacks.onSuccess();
|
callbacks.onSuccess(entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (entity) {
|
if (entity) {
|
||||||
// If the API failed, reimport the entity.
|
// If the API failed, reimport the entity.
|
||||||
dispatch(importEntities([entity], entityType));
|
dispatch(importEntities([entity], entityType));
|
||||||
}
|
}
|
||||||
throw e;
|
|
||||||
|
if (callbacks.onError) {
|
||||||
|
callbacks.onError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteEntity,
|
||||||
|
isLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch, useLoading } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { dismissEntities } from '../actions';
|
import { dismissEntities } from '../actions';
|
||||||
|
|
||||||
import { parseEntitiesPath } from './utils';
|
import { parseEntitiesPath } from './utils';
|
||||||
|
|
||||||
import type { ExpandedEntitiesPath } from './types';
|
import type { EntityFn, ExpandedEntitiesPath } from './types';
|
||||||
|
|
||||||
type DismissFn<T> = (entityId: string) => Promise<T> | T;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes an entity from a specific list.
|
* Removes an entity from a specific list.
|
||||||
* To remove an entity globally from all lists, see `useDeleteEntity`.
|
* To remove an entity globally from all lists, see `useDeleteEntity`.
|
||||||
*/
|
*/
|
||||||
function useDismissEntity<T = unknown>(expandedPath: ExpandedEntitiesPath, dismissFn: DismissFn<T>) {
|
function useDismissEntity(expandedPath: ExpandedEntitiesPath, entityFn: EntityFn<string>) {
|
||||||
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const [isLoading, setPromise] = useLoading();
|
||||||
|
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
||||||
|
|
||||||
// TODO: optimistic dismissing
|
// TODO: optimistic dismissing
|
||||||
return async function dismissEntity(entityId: string): Promise<T> {
|
async function dismissEntity(entityId: string) {
|
||||||
const result = await dismissFn(entityId);
|
const result = await setPromise(entityFn(entityId));
|
||||||
dispatch(dismissEntities([entityId], entityType, listKey));
|
dispatch(dismissEntities([entityId], entityType, listKey));
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dismissEntity,
|
||||||
|
isLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess, invalida
|
||||||
import { parseEntitiesPath } from './utils';
|
import { parseEntitiesPath } from './utils';
|
||||||
|
|
||||||
import type { Entity, EntityListState } from '../types';
|
import type { Entity, EntityListState } from '../types';
|
||||||
import type { EntitiesPath, EntitySchema, ExpandedEntitiesPath } from './types';
|
import type { EntitiesPath, EntityFn, EntitySchema, ExpandedEntitiesPath } from './types';
|
||||||
import type { RootState } from 'soapbox/store';
|
import type { RootState } from 'soapbox/store';
|
||||||
|
|
||||||
/** Additional options for the hook. */
|
/** Additional options for the hook. */
|
||||||
|
@ -32,7 +32,7 @@ function useEntities<TEntity extends Entity>(
|
||||||
/** Tells us where to find/store the entity in the cache. */
|
/** Tells us where to find/store the entity in the cache. */
|
||||||
expandedPath: ExpandedEntitiesPath,
|
expandedPath: ExpandedEntitiesPath,
|
||||||
/** API route to GET, eg `'/api/v1/notifications'`. If undefined, nothing will be fetched. */
|
/** API route to GET, eg `'/api/v1/notifications'`. If undefined, nothing will be fetched. */
|
||||||
endpoint: string | undefined,
|
entityFn: EntityFn<void>,
|
||||||
/** Additional options for the hook. */
|
/** Additional options for the hook. */
|
||||||
opts: UseEntitiesOpts<TEntity> = {},
|
opts: UseEntitiesOpts<TEntity> = {},
|
||||||
) {
|
) {
|
||||||
|
@ -54,14 +54,14 @@ function useEntities<TEntity extends Entity>(
|
||||||
const next = useListState(path, 'next');
|
const next = useListState(path, 'next');
|
||||||
const prev = useListState(path, 'prev');
|
const prev = useListState(path, 'prev');
|
||||||
|
|
||||||
const fetchPage = async(url: string, overwrite = false): Promise<void> => {
|
const fetchPage = async(req: EntityFn<void>, overwrite = false): Promise<void> => {
|
||||||
// Get `isFetching` state from the store again to prevent race conditions.
|
// Get `isFetching` state from the store again to prevent race conditions.
|
||||||
const isFetching = selectListState(getState(), path, 'fetching');
|
const isFetching = selectListState(getState(), path, 'fetching');
|
||||||
if (isFetching) return;
|
if (isFetching) return;
|
||||||
|
|
||||||
dispatch(entitiesFetchRequest(entityType, listKey));
|
dispatch(entitiesFetchRequest(entityType, listKey));
|
||||||
try {
|
try {
|
||||||
const response = await api.get(url);
|
const response = await req();
|
||||||
const schema = opts.schema || z.custom<TEntity>();
|
const schema = opts.schema || z.custom<TEntity>();
|
||||||
const entities = filteredArray(schema).parse(response.data);
|
const entities = filteredArray(schema).parse(response.data);
|
||||||
const parsedCount = realNumberSchema.safeParse(response.headers['x-total-count']);
|
const parsedCount = realNumberSchema.safeParse(response.headers['x-total-count']);
|
||||||
|
@ -82,20 +82,18 @@ function useEntities<TEntity extends Entity>(
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchEntities = async(): Promise<void> => {
|
const fetchEntities = async(): Promise<void> => {
|
||||||
if (endpoint) {
|
await fetchPage(entityFn, true);
|
||||||
await fetchPage(endpoint, true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchNextPage = async(): Promise<void> => {
|
const fetchNextPage = async(): Promise<void> => {
|
||||||
if (next) {
|
if (next) {
|
||||||
await fetchPage(next);
|
await fetchPage(() => api.get(next));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPreviousPage = async(): Promise<void> => {
|
const fetchPreviousPage = async(): Promise<void> => {
|
||||||
if (prev) {
|
if (prev) {
|
||||||
await fetchPage(prev);
|
await fetchPage(() => api.get(prev));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -114,7 +112,7 @@ function useEntities<TEntity extends Entity>(
|
||||||
if (isInvalid || isUnset || isStale) {
|
if (isInvalid || isUnset || isStale) {
|
||||||
fetchEntities();
|
fetchEntities();
|
||||||
}
|
}
|
||||||
}, [endpoint, isEnabled]);
|
}, [isEnabled]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entities,
|
entities,
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
|
|
||||||
import { useApi, useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { importEntities } from '../actions';
|
import { importEntities } from '../actions';
|
||||||
|
|
||||||
import type { Entity } from '../types';
|
import type { Entity } from '../types';
|
||||||
import type { EntitySchema, EntityPath } from './types';
|
import type { EntitySchema, EntityPath, EntityFn } from './types';
|
||||||
|
|
||||||
/** Additional options for the hook. */
|
/** Additional options for the hook. */
|
||||||
interface UseEntityOpts<TEntity extends Entity> {
|
interface UseEntityOpts<TEntity extends Entity> {
|
||||||
|
@ -18,10 +18,10 @@ interface UseEntityOpts<TEntity extends Entity> {
|
||||||
|
|
||||||
function useEntity<TEntity extends Entity>(
|
function useEntity<TEntity extends Entity>(
|
||||||
path: EntityPath,
|
path: EntityPath,
|
||||||
endpoint: string,
|
entityFn: EntityFn<void>,
|
||||||
opts: UseEntityOpts<TEntity> = {},
|
opts: UseEntityOpts<TEntity> = {},
|
||||||
) {
|
) {
|
||||||
const api = useApi();
|
const [isFetching, setPromise] = useLoading();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [entityType, entityId] = path;
|
const [entityType, entityId] = path;
|
||||||
|
@ -31,18 +31,16 @@ function useEntity<TEntity extends Entity>(
|
||||||
|
|
||||||
const entity = useAppSelector(state => state.entities[entityType]?.store[entityId] as TEntity | undefined);
|
const entity = useAppSelector(state => state.entities[entityType]?.store[entityId] as TEntity | undefined);
|
||||||
|
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
|
||||||
const isLoading = isFetching && !entity;
|
const isLoading = isFetching && !entity;
|
||||||
|
|
||||||
const fetchEntity = () => {
|
const fetchEntity = async () => {
|
||||||
setIsFetching(true);
|
try {
|
||||||
api.get(endpoint).then(({ data }) => {
|
const response = await setPromise(entityFn());
|
||||||
const entity = schema.parse(data);
|
const entity = schema.parse(response.data);
|
||||||
dispatch(importEntities([entity], entityType));
|
dispatch(importEntities([entity], entityType));
|
||||||
setIsFetching(false);
|
} catch (e) {
|
||||||
}).catch(() => {
|
// do nothing
|
||||||
setIsFetching(false);
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { useApi } from 'soapbox/hooks';
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { useCreateEntity } from './useCreateEntity';
|
import { useCreateEntity } from './useCreateEntity';
|
||||||
|
@ -18,7 +16,7 @@ interface EntityActionEndpoints {
|
||||||
delete?: string
|
delete?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEntityActions<TEntity extends Entity = Entity, Params = any>(
|
function useEntityActions<TEntity extends Entity = Entity, Data = any>(
|
||||||
expandedPath: ExpandedEntitiesPath,
|
expandedPath: ExpandedEntitiesPath,
|
||||||
endpoints: EntityActionEndpoints,
|
endpoints: EntityActionEndpoints,
|
||||||
opts: UseEntityActionsOpts<TEntity> = {},
|
opts: UseEntityActionsOpts<TEntity> = {},
|
||||||
|
@ -26,24 +24,16 @@ function useEntityActions<TEntity extends Entity = Entity, Params = any>(
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
const { entityType, path } = parseEntitiesPath(expandedPath);
|
const { entityType, path } = parseEntitiesPath(expandedPath);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const { deleteEntity, isLoading: deleteLoading } =
|
||||||
|
useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replaceAll(':id', entityId)));
|
||||||
|
|
||||||
const deleteEntity = useDeleteEntity(entityType, (entityId) => {
|
const { createEntity, isLoading: createLoading } =
|
||||||
if (!endpoints.delete) return Promise.reject(endpoints);
|
useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts);
|
||||||
return api.delete(endpoints.delete.replace(':id', entityId))
|
|
||||||
.finally(() => setIsLoading(false));
|
|
||||||
});
|
|
||||||
|
|
||||||
const createEntity = useCreateEntity(path, (params: Params) => {
|
|
||||||
if (!endpoints.post) return Promise.reject(endpoints);
|
|
||||||
return api.post(endpoints.post, params)
|
|
||||||
.finally(() => setIsLoading(false));
|
|
||||||
}, opts);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createEntity,
|
createEntity,
|
||||||
deleteEntity,
|
deleteEntity,
|
||||||
isLoading,
|
isLoading: createLoading || deleteLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,32 +1,36 @@
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch, useLoading } from 'soapbox/hooks';
|
||||||
|
|
||||||
import { incrementEntities } from '../actions';
|
import { incrementEntities } from '../actions';
|
||||||
|
|
||||||
import { parseEntitiesPath } from './utils';
|
import { parseEntitiesPath } from './utils';
|
||||||
|
|
||||||
import type { ExpandedEntitiesPath } from './types';
|
import type { EntityFn, ExpandedEntitiesPath } from './types';
|
||||||
|
|
||||||
type IncrementFn<T> = (entityId: string) => Promise<T> | T;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increases (or decreases) the `totalCount` in the entity list by the specified amount.
|
* Increases (or decreases) the `totalCount` in the entity list by the specified amount.
|
||||||
* This only works if the API returns an `X-Total-Count` header and your components read it.
|
* This only works if the API returns an `X-Total-Count` header and your components read it.
|
||||||
*/
|
*/
|
||||||
function useIncrementEntity<T = unknown>(
|
function useIncrementEntity(
|
||||||
expandedPath: ExpandedEntitiesPath,
|
expandedPath: ExpandedEntitiesPath,
|
||||||
diff: number,
|
diff: number,
|
||||||
incrementFn: IncrementFn<T>,
|
entityFn: EntityFn<string>,
|
||||||
) {
|
) {
|
||||||
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const [isLoading, setPromise] = useLoading();
|
||||||
|
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
||||||
|
|
||||||
return async function incrementEntity(entityId: string): Promise<void> {
|
async function incrementEntity(entityId: string): Promise<void> {
|
||||||
dispatch(incrementEntities(entityType, listKey, diff));
|
dispatch(incrementEntities(entityType, listKey, diff));
|
||||||
try {
|
try {
|
||||||
await incrementFn(entityId);
|
await setPromise(entityFn(entityId));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(incrementEntities(entityType, listKey, diff * -1));
|
dispatch(incrementEntities(entityType, listKey, diff * -1));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
incrementEntity,
|
||||||
|
isLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,4 +12,5 @@ function parseEntitiesPath(expandedPath: ExpandedEntitiesPath) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export { parseEntitiesPath };
|
export { parseEntitiesPath };
|
|
@ -11,20 +11,20 @@ function useGroupMembershipRequests(groupId: string) {
|
||||||
|
|
||||||
const { entities, invalidate, ...rest } = useEntities(
|
const { entities, invalidate, ...rest } = useEntities(
|
||||||
path,
|
path,
|
||||||
`/api/v1/groups/${groupId}/membership_requests`,
|
() => api.get(`/api/v1/groups/${groupId}/membership_requests`),
|
||||||
{ schema: accountSchema },
|
{ schema: accountSchema },
|
||||||
);
|
);
|
||||||
|
|
||||||
const authorize = useIncrementEntity(path, -1, (accountId: string) => {
|
const { incrementEntity: authorize } = useIncrementEntity(path, -1, async (accountId: string) => {
|
||||||
return api
|
const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`);
|
||||||
.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`)
|
invalidate();
|
||||||
.then(invalidate);
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
const reject = useIncrementEntity(path, -1, (accountId: string) => {
|
const { incrementEntity: reject } = useIncrementEntity(path, -1, async (accountId: string) => {
|
||||||
return api
|
const response = await api.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`);
|
||||||
.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`)
|
invalidate();
|
||||||
.then(invalidate);
|
return response;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,10 +2,14 @@ import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
import { GroupMember, groupMemberSchema } from 'soapbox/schemas';
|
import { GroupMember, groupMemberSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
|
||||||
function useGroupMembers(groupId: string, role: string) {
|
function useGroupMembers(groupId: string, role: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
const { entities, ...result } = useEntities<GroupMember>(
|
const { entities, ...result } = useEntities<GroupMember>(
|
||||||
[Entities.GROUP_MEMBERSHIPS, groupId, role],
|
[Entities.GROUP_MEMBERSHIPS, groupId, role],
|
||||||
`/api/v1/groups/${groupId}/memberships?role=${role}`,
|
() => api.get(`/api/v1/groups/${groupId}/memberships?role=${role}`),
|
||||||
{ schema: groupMemberSchema },
|
{ schema: groupMemberSchema },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -2,15 +2,17 @@ import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
import { Group, groupSchema } from 'soapbox/schemas';
|
import { Group, groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useApi } from '../useApi';
|
||||||
import { useFeatures } from '../useFeatures';
|
import { useFeatures } from '../useFeatures';
|
||||||
import { useGroupRelationships } from '../useGroups';
|
import { useGroupRelationships } from '../useGroups';
|
||||||
|
|
||||||
function usePopularGroups() {
|
function usePopularGroups() {
|
||||||
|
const api = useApi();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
|
||||||
const { entities, ...result } = useEntities<Group>(
|
const { entities, ...result } = useEntities<Group>(
|
||||||
[Entities.GROUPS, 'popular'],
|
[Entities.GROUPS, 'popular'],
|
||||||
'/api/mock/groups', // '/api/v1/truth/trends/groups'
|
() => api.get('/api/mock/groups'), // '/api/v1/truth/trends/groups'
|
||||||
{
|
{
|
||||||
schema: groupSchema,
|
schema: groupSchema,
|
||||||
enabled: features.groupsDiscovery,
|
enabled: features.groupsDiscovery,
|
||||||
|
|
|
@ -2,15 +2,17 @@ import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
import { Group, groupSchema } from 'soapbox/schemas';
|
import { Group, groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useApi } from '../useApi';
|
||||||
import { useFeatures } from '../useFeatures';
|
import { useFeatures } from '../useFeatures';
|
||||||
import { useGroupRelationships } from '../useGroups';
|
import { useGroupRelationships } from '../useGroups';
|
||||||
|
|
||||||
function useSuggestedGroups() {
|
function useSuggestedGroups() {
|
||||||
|
const api = useApi();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
|
||||||
const { entities, ...result } = useEntities<Group>(
|
const { entities, ...result } = useEntities<Group>(
|
||||||
[Entities.GROUPS, 'suggested'],
|
[Entities.GROUPS, 'suggested'],
|
||||||
'/api/mock/groups', // '/api/v1/truth/suggestions/groups'
|
() => api.get('/api/mock/groups'), // '/api/v1/truth/suggestions/groups'
|
||||||
{
|
{
|
||||||
schema: groupSchema,
|
schema: groupSchema,
|
||||||
enabled: features.groupsDiscovery,
|
enabled: features.groupsDiscovery,
|
||||||
|
|
|
@ -11,6 +11,7 @@ export { useGroupsPath } from './useGroupsPath';
|
||||||
export { useDimensions } from './useDimensions';
|
export { useDimensions } from './useDimensions';
|
||||||
export { useFeatures } from './useFeatures';
|
export { useFeatures } from './useFeatures';
|
||||||
export { useInstance } from './useInstance';
|
export { useInstance } from './useInstance';
|
||||||
|
export { useLoading } from './useLoading';
|
||||||
export { useLocale } from './useLocale';
|
export { useLocale } from './useLocale';
|
||||||
export { useOnScreen } from './useOnScreen';
|
export { useOnScreen } from './useOnScreen';
|
||||||
export { useOwnAccount } from './useOwnAccount';
|
export { useOwnAccount } from './useOwnAccount';
|
||||||
|
|
|
@ -2,17 +2,19 @@ import { z } from 'zod';
|
||||||
|
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntities, useEntity } from 'soapbox/entity-store/hooks';
|
import { useEntities, useEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
import { groupSchema, Group } from 'soapbox/schemas/group';
|
import { groupSchema, Group } from 'soapbox/schemas/group';
|
||||||
import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship';
|
import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship';
|
||||||
|
|
||||||
import { useFeatures } from './useFeatures';
|
import { useFeatures } from './useFeatures';
|
||||||
|
|
||||||
function useGroups() {
|
function useGroups() {
|
||||||
|
const api = useApi();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
|
||||||
const { entities, ...result } = useEntities<Group>(
|
const { entities, ...result } = useEntities<Group>(
|
||||||
[Entities.GROUPS],
|
[Entities.GROUPS],
|
||||||
'/api/v1/groups',
|
() => api.get('/api/v1/groups'),
|
||||||
{ enabled: features.groups, schema: groupSchema },
|
{ enabled: features.groups, schema: groupSchema },
|
||||||
);
|
);
|
||||||
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||||
|
@ -29,9 +31,11 @@ function useGroups() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGroup(groupId: string, refetch = true) {
|
function useGroup(groupId: string, refetch = true) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
const { entity: group, ...result } = useEntity<Group>(
|
const { entity: group, ...result } = useEntity<Group>(
|
||||||
[Entities.GROUPS, groupId],
|
[Entities.GROUPS, groupId],
|
||||||
`/api/v1/groups/${groupId}`,
|
() => api.get(`/api/v1/groups/${groupId}`),
|
||||||
{ schema: groupSchema, refetch },
|
{ schema: groupSchema, refetch },
|
||||||
);
|
);
|
||||||
const { entity: relationship } = useGroupRelationship(groupId);
|
const { entity: relationship } = useGroupRelationship(groupId);
|
||||||
|
@ -43,20 +47,22 @@ function useGroup(groupId: string, refetch = true) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGroupRelationship(groupId: string) {
|
function useGroupRelationship(groupId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
return useEntity<GroupRelationship>(
|
return useEntity<GroupRelationship>(
|
||||||
[Entities.GROUP_RELATIONSHIPS, groupId],
|
[Entities.GROUP_RELATIONSHIPS, groupId],
|
||||||
`/api/v1/groups/relationships?id[]=${groupId}`,
|
() => api.get(`/api/v1/groups/relationships?id[]=${groupId}`),
|
||||||
{ schema: z.array(groupRelationshipSchema).transform(arr => arr[0]) },
|
{ schema: z.array(groupRelationshipSchema).transform(arr => arr[0]) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGroupRelationships(groupIds: string[]) {
|
function useGroupRelationships(groupIds: string[]) {
|
||||||
|
const api = useApi();
|
||||||
const q = groupIds.map(id => `id[]=${id}`).join('&');
|
const q = groupIds.map(id => `id[]=${id}`).join('&');
|
||||||
const endpoint = groupIds.length ? `/api/v1/groups/relationships?${q}` : undefined;
|
|
||||||
const { entities, ...result } = useEntities<GroupRelationship>(
|
const { entities, ...result } = useEntities<GroupRelationship>(
|
||||||
[Entities.GROUP_RELATIONSHIPS, ...groupIds],
|
[Entities.GROUP_RELATIONSHIPS, ...groupIds],
|
||||||
endpoint,
|
() => api.get(`/api/v1/groups/relationships?${q}`),
|
||||||
{ schema: groupRelationshipSchema },
|
{ schema: groupRelationshipSchema, enabled: groupIds.length > 0 },
|
||||||
);
|
);
|
||||||
|
|
||||||
const relationships = entities.reduce<Record<string, GroupRelationship>>((map, relationship) => {
|
const relationships = entities.reduce<Record<string, GroupRelationship>>((map, relationship) => {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
function useLoading() {
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
function setPromise<T>(promise: Promise<T>) {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then(() => setIsLoading(false))
|
||||||
|
.catch(() => setIsLoading(false));
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [isLoading, setPromise] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useLoading };
|
Loading…
Reference in New Issue