Merge pull request #2497 from ClearlyClaire/glitch-soc/ports/account_notes-typescript
Port upstream's TypeScript refactor of account_notesth-new
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 }) => ({
|
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||||
|
|
||||||
onSave (value) {
|
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 { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
submitAccountNote,
|
||||||
} from '../actions/account_notes';
|
} from '../actions/account_notes';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_FOLLOW_SUCCESS,
|
ACCOUNT_FOLLOW_SUCCESS,
|
||||||
|
@ -73,10 +73,11 @@ export default function relationships(state = initialState, action) {
|
||||||
case ACCOUNT_UNMUTE_SUCCESS:
|
case ACCOUNT_UNMUTE_SUCCESS:
|
||||||
case ACCOUNT_PIN_SUCCESS:
|
case ACCOUNT_PIN_SUCCESS:
|
||||||
case ACCOUNT_UNPIN_SUCCESS:
|
case ACCOUNT_UNPIN_SUCCESS:
|
||||||
case ACCOUNT_NOTE_SUBMIT_SUCCESS:
|
|
||||||
return normalizeRelationship(state, action.relationship);
|
return normalizeRelationship(state, action.relationship);
|
||||||
case RELATIONSHIPS_FETCH_SUCCESS:
|
case RELATIONSHIPS_FETCH_SUCCESS:
|
||||||
return normalizeRelationships(state, action.relationships);
|
return normalizeRelationships(state, action.relationships);
|
||||||
|
case submitAccountNote.fulfilled:
|
||||||
|
return normalizeRelationship(state, action.payload.relationship);
|
||||||
case DOMAIN_BLOCK_SUCCESS:
|
case DOMAIN_BLOCK_SUCCESS:
|
||||||
return setDomainBlocking(state, action.accounts, true);
|
return setDomainBlocking(state, action.accounts, true);
|
||||||
case DOMAIN_UNBLOCK_SUCCESS:
|
case DOMAIN_UNBLOCK_SUCCESS:
|
||||||
|
|
|
@ -1,45 +1,8 @@
|
||||||
import type { TypedUseSelectorHook } from 'react-redux';
|
export { store } from './store';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
export type { GetState, AppDispatch, RootState } from './store';
|
||||||
|
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
export {
|
||||||
|
createAppAsyncThunk,
|
||||||
import { rootReducer } from '../reducers';
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
import { errorsMiddleware } from './middlewares/errors';
|
} from './typed_functions';
|
||||||
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;
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { showAlertForError } from '../../actions/alerts';
|
||||||
|
|
||||||
const defaultFailSuffix = 'FAIL';
|
const defaultFailSuffix = 'FAIL';
|
||||||
|
|
||||||
export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
|
export const errorsMiddleware: Middleware<unknown, RootState> =
|
||||||
({ dispatch }) =>
|
({ dispatch }) =>
|
||||||
(next) =>
|
(next) =>
|
||||||
(action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => {
|
(action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => {
|
||||||
|
|
|
@ -15,7 +15,7 @@ const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
|
||||||
|
|
||||||
export const loadingBarMiddleware = (
|
export const loadingBarMiddleware = (
|
||||||
config: Config = {},
|
config: Config = {},
|
||||||
): Middleware<Record<string, never>, RootState> => {
|
): Middleware<unknown, RootState> => {
|
||||||
const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes;
|
const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes;
|
||||||
|
|
||||||
return ({ dispatch }) =>
|
return ({ dispatch }) =>
|
||||||
|
|
|
@ -34,10 +34,7 @@ const play = (audio: HTMLAudioElement) => {
|
||||||
void audio.play();
|
void audio.play();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const soundsMiddleware = (): Middleware<
|
export const soundsMiddleware = (): Middleware<unknown, RootState> => {
|
||||||
Record<string, never>,
|
|
||||||
RootState
|
|
||||||
> => {
|
|
||||||
const soundCache: Record<string, HTMLAudioElement> = {};
|
const soundCache: Record<string, HTMLAudioElement> = {};
|
||||||
|
|
||||||
void ready(() => {
|
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