Merge pull request #2497 from ClearlyClaire/glitch-soc/ports/account_notes-typescript
Port upstream's TypeScript refactor of account_notes
This commit is contained in:
commit
13902903d3
|
@ -1,37 +0,0 @@
|
|||
import api from '../api';
|
||||
|
||||
export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
|
||||
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
|
||||
export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
|
||||
|
||||
export function submitAccountNote(id, value) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(submitAccountNoteRequest());
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/note`, {
|
||||
comment: value,
|
||||
}).then(response => {
|
||||
dispatch(submitAccountNoteSuccess(response.data));
|
||||
}).catch(error => dispatch(submitAccountNoteFail(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function submitAccountNoteRequest() {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
export function submitAccountNoteSuccess(relationship) {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
||||
relationship,
|
||||
};
|
||||
}
|
||||
|
||||
export function submitAccountNoteFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_FAIL,
|
||||
error,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { createAppAsyncThunk } from 'flavours/glitch/store/typed_functions';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
export const submitAccountNote = createAppAsyncThunk(
|
||||
'account_note/submit',
|
||||
async (args: { id: string; value: string }, { getState }) => {
|
||||
// TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged
|
||||
const response = await api(getState).post<unknown>(
|
||||
`/api/v1/accounts/${args.id}/note`,
|
||||
{
|
||||
comment: args.value,
|
||||
},
|
||||
);
|
||||
|
||||
return { relationship: response.data };
|
||||
},
|
||||
);
|
|
@ -1,75 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
import ready from './ready';
|
||||
/**
|
||||
* @param {import('axios').AxiosResponse} response
|
||||
* @returns {LinkHeader}
|
||||
*/
|
||||
export const getLinks = response => {
|
||||
const value = response.headers.link;
|
||||
|
||||
if (!value) {
|
||||
return new LinkHeader();
|
||||
}
|
||||
|
||||
return LinkHeader.parse(value);
|
||||
};
|
||||
|
||||
/** @type {import('axios').RawAxiosRequestHeaders} */
|
||||
const csrfHeader = {};
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
const setCSRFHeader = () => {
|
||||
/** @type {HTMLMetaElement | null} */
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]');
|
||||
|
||||
if (csrfToken) {
|
||||
csrfHeader['X-CSRF-Token'] = csrfToken.content;
|
||||
}
|
||||
};
|
||||
|
||||
ready(setCSRFHeader);
|
||||
|
||||
/**
|
||||
* @param {() => import('immutable').Map<string,any>} getState
|
||||
* @returns {import('axios').RawAxiosRequestHeaders}
|
||||
*/
|
||||
const authorizationHeaderFromState = getState => {
|
||||
const accessToken = getState && getState().getIn(['meta', 'access_token'], '');
|
||||
|
||||
if (!accessToken) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {() => import('immutable').Map<string,any>} getState
|
||||
* @returns {import('axios').AxiosInstance}
|
||||
*/
|
||||
export default function api(getState) {
|
||||
return axios.create({
|
||||
headers: {
|
||||
...csrfHeader,
|
||||
...authorizationHeaderFromState(getState),
|
||||
},
|
||||
|
||||
transformResponse: [
|
||||
function (data) {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import type { AxiosResponse, RawAxiosRequestHeaders } from 'axios';
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
import ready from './ready';
|
||||
import type { GetState } from './store';
|
||||
|
||||
export const getLinks = (response: AxiosResponse) => {
|
||||
const value = response.headers.link as string | undefined;
|
||||
|
||||
if (!value) {
|
||||
return new LinkHeader();
|
||||
}
|
||||
|
||||
return LinkHeader.parse(value);
|
||||
};
|
||||
|
||||
const csrfHeader: RawAxiosRequestHeaders = {};
|
||||
|
||||
const setCSRFHeader = () => {
|
||||
const csrfToken = document.querySelector<HTMLMetaElement>(
|
||||
'meta[name=csrf-token]',
|
||||
);
|
||||
|
||||
if (csrfToken) {
|
||||
csrfHeader['X-CSRF-Token'] = csrfToken.content;
|
||||
}
|
||||
};
|
||||
|
||||
void ready(setCSRFHeader);
|
||||
|
||||
const authorizationHeaderFromState = (getState?: GetState) => {
|
||||
const accessToken =
|
||||
getState && (getState().meta.get('access_token', '') as string);
|
||||
|
||||
if (!accessToken) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
} as RawAxiosRequestHeaders;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function api(getState: GetState) {
|
||||
return axios.create({
|
||||
headers: {
|
||||
...csrfHeader,
|
||||
...authorizationHeaderFromState(getState),
|
||||
},
|
||||
|
||||
transformResponse: [
|
||||
function (data: unknown) {
|
||||
try {
|
||||
return JSON.parse(data as string) as unknown;
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
|
@ -11,7 +11,7 @@ const mapStateToProps = (state, { account }) => ({
|
|||
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||
|
||||
onSave (value) {
|
||||
dispatch(submitAccountNote(account.get('id'), value));
|
||||
dispatch(submitAccountNote({ id: account.get('id'), value}));
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
||||
submitAccountNote,
|
||||
} from '../actions/account_notes';
|
||||
import {
|
||||
ACCOUNT_FOLLOW_SUCCESS,
|
||||
|
@ -73,10 +73,11 @@ export default function relationships(state = initialState, action) {
|
|||
case ACCOUNT_UNMUTE_SUCCESS:
|
||||
case ACCOUNT_PIN_SUCCESS:
|
||||
case ACCOUNT_UNPIN_SUCCESS:
|
||||
case ACCOUNT_NOTE_SUBMIT_SUCCESS:
|
||||
return normalizeRelationship(state, action.relationship);
|
||||
case RELATIONSHIPS_FETCH_SUCCESS:
|
||||
return normalizeRelationships(state, action.relationships);
|
||||
case submitAccountNote.fulfilled:
|
||||
return normalizeRelationship(state, action.payload.relationship);
|
||||
case DOMAIN_BLOCK_SUCCESS:
|
||||
return setDomainBlocking(state, action.accounts, true);
|
||||
case DOMAIN_UNBLOCK_SUCCESS:
|
||||
|
|
|
@ -1,45 +1,8 @@
|
|||
import type { TypedUseSelectorHook } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
export { store } from './store';
|
||||
export type { GetState, AppDispatch, RootState } from './store';
|
||||
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
|
||||
import { rootReducer } from '../reducers';
|
||||
|
||||
import { errorsMiddleware } from './middlewares/errors';
|
||||
import { loadingBarMiddleware } from './middlewares/loading_bar';
|
||||
import { soundsMiddleware } from './middlewares/sounds';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: rootReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
// In development, Redux Toolkit enables 2 default middlewares to detect
|
||||
// common issues with states. Unfortunately, our use of ImmutableJS for state
|
||||
// triggers both, so lets disable them until our state is fully refactored
|
||||
|
||||
// https://redux-toolkit.js.org/api/serializabilityMiddleware
|
||||
// This checks recursively that every values in the state are serializable in JSON
|
||||
// Which is not the case, as we use ImmutableJS structures, but also File objects
|
||||
serializableCheck: false,
|
||||
|
||||
// https://redux-toolkit.js.org/api/immutabilityMiddleware
|
||||
// This checks recursively if every value in the state is immutable (ie, a JS primitive type)
|
||||
// But this is not the case, as our Root State is an ImmutableJS map, which is an object
|
||||
immutableCheck: false,
|
||||
})
|
||||
.concat(
|
||||
loadingBarMiddleware({
|
||||
promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
|
||||
}),
|
||||
)
|
||||
.concat(errorsMiddleware)
|
||||
.concat(soundsMiddleware()),
|
||||
});
|
||||
|
||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
export {
|
||||
createAppAsyncThunk,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from './typed_functions';
|
||||
|
|
|
@ -5,7 +5,7 @@ import { showAlertForError } from '../../actions/alerts';
|
|||
|
||||
const defaultFailSuffix = 'FAIL';
|
||||
|
||||
export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
|
||||
export const errorsMiddleware: Middleware<unknown, RootState> =
|
||||
({ dispatch }) =>
|
||||
(next) =>
|
||||
(action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => {
|
||||
|
|
|
@ -15,7 +15,7 @@ const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
|
|||
|
||||
export const loadingBarMiddleware = (
|
||||
config: Config = {},
|
||||
): Middleware<Record<string, never>, RootState> => {
|
||||
): Middleware<unknown, RootState> => {
|
||||
const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes;
|
||||
|
||||
return ({ dispatch }) =>
|
||||
|
|
|
@ -34,10 +34,7 @@ const play = (audio: HTMLAudioElement) => {
|
|||
void audio.play();
|
||||
};
|
||||
|
||||
export const soundsMiddleware = (): Middleware<
|
||||
Record<string, never>,
|
||||
RootState
|
||||
> => {
|
||||
export const soundsMiddleware = (): Middleware<unknown, RootState> => {
|
||||
const soundCache: Record<string, HTMLAudioElement> = {};
|
||||
|
||||
void ready(() => {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { configureStore } from '@reduxjs/toolkit';
|
||||
|
||||
import { rootReducer } from '../reducers';
|
||||
|
||||
import { errorsMiddleware } from './middlewares/errors';
|
||||
import { loadingBarMiddleware } from './middlewares/loading_bar';
|
||||
import { soundsMiddleware } from './middlewares/sounds';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: rootReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
// In development, Redux Toolkit enables 2 default middlewares to detect
|
||||
// common issues with states. Unfortunately, our use of ImmutableJS for state
|
||||
// triggers both, so lets disable them until our state is fully refactored
|
||||
|
||||
// https://redux-toolkit.js.org/api/serializabilityMiddleware
|
||||
// This checks recursively that every values in the state are serializable in JSON
|
||||
// Which is not the case, as we use ImmutableJS structures, but also File objects
|
||||
serializableCheck: false,
|
||||
|
||||
// https://redux-toolkit.js.org/api/immutabilityMiddleware
|
||||
// This checks recursively if every value in the state is immutable (ie, a JS primitive type)
|
||||
// But this is not the case, as our Root State is an ImmutableJS map, which is an object
|
||||
immutableCheck: false,
|
||||
})
|
||||
.concat(
|
||||
loadingBarMiddleware({
|
||||
promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
|
||||
}),
|
||||
)
|
||||
.concat(errorsMiddleware)
|
||||
.concat(soundsMiddleware()),
|
||||
});
|
||||
|
||||
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type GetState = typeof store.getState;
|
|
@ -0,0 +1,15 @@
|
|||
import type { TypedUseSelectorHook } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
|
||||
import type { AppDispatch, RootState } from './store';
|
||||
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
|
||||
state: RootState;
|
||||
dispatch: AppDispatch;
|
||||
rejectValue: string;
|
||||
}>();
|
Loading…
Reference in New Issue