mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
rename some items
This commit is contained in:
parent
afc04abfd4
commit
865de09ad9
12 changed files with 119 additions and 89 deletions
|
@ -1,5 +1,8 @@
|
|||
import { TelegramUser } from '@v9v/ts-react-telegram-login';
|
||||
|
||||
import { API } from '~/constants/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
import {
|
||||
ApiAttachSocialRequest,
|
||||
ApiAttachSocialResult,
|
||||
|
@ -22,24 +25,20 @@ import {
|
|||
ApiUpdateUserResult,
|
||||
ApiUserLoginRequest,
|
||||
ApiUserLoginResult,
|
||||
} from '~/api/auth/types';
|
||||
import { API } from '~/constants/api';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
} from './types';
|
||||
|
||||
export const apiUserLogin = ({ username, password }: ApiUserLoginRequest) =>
|
||||
api
|
||||
.post<ApiUserLoginResult>(API.USER.LOGIN, { username, password })
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiAuthGetUser = () =>
|
||||
api.get<ApiAuthGetUserResult>(API.USER.ME).then(cleanResult);
|
||||
api.get<ApiAuthGetUserResult>(API.USER.ME).then(unwrap);
|
||||
|
||||
export const apiAuthGetUserProfile = ({
|
||||
username,
|
||||
}: ApiAuthGetUserProfileRequest) =>
|
||||
api
|
||||
.get<ApiAuthGetUserProfileResult>(API.USER.PROFILE(username))
|
||||
.then(cleanResult);
|
||||
api.get<ApiAuthGetUserProfileResult>(API.USER.PROFILE(username)).then(unwrap);
|
||||
|
||||
export const apiAuthGetUpdates = ({
|
||||
exclude_dialogs,
|
||||
|
@ -49,44 +48,40 @@ export const apiAuthGetUpdates = ({
|
|||
.get<ApiAuthGetUpdatesResult>(API.USER.GET_UPDATES, {
|
||||
params: { exclude_dialogs, last },
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiUpdateUser = ({ user }: ApiUpdateUserRequest) =>
|
||||
api.patch<ApiUpdateUserResult>(API.USER.ME, user).then(cleanResult);
|
||||
api.patch<ApiUpdateUserResult>(API.USER.ME, user).then(unwrap);
|
||||
|
||||
export const apiUpdatePhoto = ({ file }: ApiUpdatePhotoRequest) =>
|
||||
api.post<ApiUpdateUserResult>(API.USER.UPDATE_PHOTO, file).then(cleanResult);
|
||||
api.post<ApiUpdateUserResult>(API.USER.UPDATE_PHOTO, file).then(unwrap);
|
||||
|
||||
export const apiUpdateCover = ({ file }: ApiUpdatePhotoRequest) =>
|
||||
api.post<ApiUpdateUserResult>(API.USER.UPDATE_COVER, file).then(cleanResult);
|
||||
api.post<ApiUpdateUserResult>(API.USER.UPDATE_COVER, file).then(unwrap);
|
||||
|
||||
export const apiRequestRestoreCode = (field: string) =>
|
||||
api
|
||||
.post<{ field: string }>(API.USER.REQUEST_CODE(), { field })
|
||||
.then(cleanResult);
|
||||
api.post<{ field: string }>(API.USER.REQUEST_CODE(), { field }).then(unwrap);
|
||||
|
||||
export const apiCheckRestoreCode = ({ code }: ApiCheckRestoreCodeRequest) =>
|
||||
api
|
||||
.get<ApiCheckRestoreCodeResult>(API.USER.REQUEST_CODE(code))
|
||||
.then(cleanResult);
|
||||
api.get<ApiCheckRestoreCodeResult>(API.USER.REQUEST_CODE(code)).then(unwrap);
|
||||
|
||||
export const apiRestoreCode = ({ code, password }: ApiRestoreCodeRequest) =>
|
||||
api
|
||||
.put<ApiRestoreCodeResult>(API.USER.REQUEST_CODE(code), { password })
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiGetSocials = () =>
|
||||
api.get<ApiGetSocialsResult>(API.USER.GET_SOCIALS).then(cleanResult);
|
||||
api.get<ApiGetSocialsResult>(API.USER.GET_SOCIALS).then(unwrap);
|
||||
|
||||
export const apiDropSocial = ({ id, provider }: ApiDropSocialRequest) =>
|
||||
api
|
||||
.delete<ApiDropSocialResult>(API.USER.DROP_SOCIAL(provider, id))
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiAttachSocial = ({ token }: ApiAttachSocialRequest) =>
|
||||
api
|
||||
.post<ApiAttachSocialResult>(API.USER.ATTACH_SOCIAL, { token })
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiLoginWithSocial = ({
|
||||
token,
|
||||
|
@ -99,7 +94,7 @@ export const apiLoginWithSocial = ({
|
|||
username,
|
||||
password,
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiAttachTelegram = (data: TelegramUser) =>
|
||||
api.post(API.USER.ATTACH_TELEGRAM, data);
|
||||
|
|
|
@ -2,16 +2,16 @@ import axios from 'axios';
|
|||
|
||||
import { API } from '~/constants/api';
|
||||
import { IGetGithubIssuesResult, StatBackend } from '~/types/boris';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
export const getBorisBackendStats = () =>
|
||||
api.get<StatBackend>(API.BORIS.GET_BACKEND_STATS).then(cleanResult);
|
||||
api.get<StatBackend>(API.BORIS.GET_BACKEND_STATS).then(unwrap);
|
||||
|
||||
export const getGithubIssues = () => {
|
||||
return axios
|
||||
.get<IGetGithubIssuesResult>(API.BORIS.GITHUB_ISSUES, {
|
||||
params: { state: 'all', sort: 'created' },
|
||||
})
|
||||
.then(result => result.data)
|
||||
.then(unwrap)
|
||||
.catch(() => []);
|
||||
};
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { API } from '~/constants/api';
|
||||
import { GetSearchResultsRequest, GetSearchResultsResult } from '~/types/flow';
|
||||
import { PostCellViewRequest, PostCellViewResult } from '~/types/node';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
export const postCellView = ({ id, flow }: PostCellViewRequest) =>
|
||||
api
|
||||
.post<PostCellViewResult>(API.NODES.SET_CELL_VIEW(id), { flow })
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const getSearchResults = ({
|
||||
text,
|
||||
|
@ -17,4 +17,4 @@ export const getSearchResults = ({
|
|||
.get<GetSearchResultsResult>(API.SEARCH.NODES, {
|
||||
params: { text, skip, take },
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
|
|
@ -5,12 +5,21 @@ import {
|
|||
GetLabStatsResult,
|
||||
GetLabUpdatesResult,
|
||||
} from '~/types/lab';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
export const getLabNodes = ({ offset, limit, sort, search }: GetLabNodesRequest) =>
|
||||
export const getLabNodes = ({
|
||||
offset,
|
||||
limit,
|
||||
sort,
|
||||
search,
|
||||
}: GetLabNodesRequest) =>
|
||||
api
|
||||
.get<GetLabNodesResult>(API.LAB.NODES, { params: { offset, limit, sort, search } })
|
||||
.then(cleanResult);
|
||||
.get<GetLabNodesResult>(API.LAB.NODES, {
|
||||
params: { offset, limit, sort, search },
|
||||
})
|
||||
.then(unwrap);
|
||||
|
||||
export const getLabStats = () => api.get<GetLabStatsResult>(API.LAB.STATS).then(cleanResult);
|
||||
export const getLabUpdates = () => api.get<GetLabUpdatesResult>(API.LAB.UPDATES).then(cleanResult);
|
||||
export const getLabStats = () =>
|
||||
api.get<GetLabStatsResult>(API.LAB.STATS).then(unwrap);
|
||||
export const getLabUpdates = () =>
|
||||
api.get<GetLabUpdatesResult>(API.LAB.UPDATES).then(unwrap);
|
||||
|
|
|
@ -7,23 +7,31 @@ import {
|
|||
ApiSendMessageResult,
|
||||
} from '~/api/messages/types';
|
||||
import { API } from '~/constants/api';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
export const apiGetUserMessages = ({ username, after, before }: ApiGetUserMessagesRequest) =>
|
||||
export const apiGetUserMessages = ({
|
||||
username,
|
||||
after,
|
||||
before,
|
||||
}: ApiGetUserMessagesRequest) =>
|
||||
api
|
||||
.get<ApiGetUserMessagesResponse>(API.USER.MESSAGES(username), {
|
||||
params: { after, before },
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiSendMessage = ({ username, message }: ApiSendMessageRequest) =>
|
||||
api
|
||||
.post<ApiSendMessageResult>(API.USER.MESSAGE_SEND(username), { message })
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiDeleteMessage = ({ username, id, is_locked }: ApiDeleteMessageRequest) =>
|
||||
export const apiDeleteMessage = ({
|
||||
username,
|
||||
id,
|
||||
is_locked,
|
||||
}: ApiDeleteMessageRequest) =>
|
||||
api
|
||||
.delete<ApiDeleteMessageResult>(API.USER.MESSAGE_DELETE(username, id), {
|
||||
params: { is_locked },
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { ApiGetEmbedYoutubeResult } from '~/api/metadata/types';
|
||||
import { API } from '~/constants/api';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
export const apiGetEmbedYoutube = (ids: string[]) =>
|
||||
api
|
||||
.get<ApiGetEmbedYoutubeResult>(API.EMBED.YOUTUBE, { params: { ids: ids.join(',') } })
|
||||
.then(cleanResult);
|
||||
.get<ApiGetEmbedYoutubeResult>(API.EMBED.YOUTUBE, {
|
||||
params: { ids: ids.join(',') },
|
||||
})
|
||||
.then(unwrap);
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
GetNodeDiffRequest,
|
||||
GetNodeDiffResult,
|
||||
} from '~/types/node';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
export type ApiPostNodeRequest = { node: INode };
|
||||
export type ApiPostNodeResult = {
|
||||
|
@ -45,7 +45,7 @@ export type ApiGetNodeCommentsResponse = {
|
|||
};
|
||||
|
||||
export const apiPostNode = ({ node }: ApiPostNodeRequest) =>
|
||||
api.post<ApiPostNodeResult>(API.NODES.SAVE, node).then(cleanResult);
|
||||
api.post<ApiPostNodeResult>(API.NODES.SAVE, node).then(unwrap);
|
||||
|
||||
export const getNodeDiff = ({
|
||||
start,
|
||||
|
@ -68,7 +68,7 @@ export const getNodeDiff = ({
|
|||
with_valid,
|
||||
},
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiGetNode = (
|
||||
{ id }: ApiGetNodeRequest,
|
||||
|
@ -76,7 +76,7 @@ export const apiGetNode = (
|
|||
) =>
|
||||
api
|
||||
.get<ApiGetNodeResponse>(API.NODES.GET(id), config)
|
||||
.then(cleanResult)
|
||||
.then(unwrap)
|
||||
.then((data) => ({
|
||||
node: data.node,
|
||||
last_seen: data.last_seen,
|
||||
|
@ -90,13 +90,13 @@ export const apiGetNodeWithCancel = ({ id }: ApiGetNodeRequest) => {
|
|||
.get<ApiGetNodeResponse>(API.NODES.GET(id), {
|
||||
cancelToken: cancelToken.token,
|
||||
})
|
||||
.then(cleanResult),
|
||||
.then(unwrap),
|
||||
cancel: cancelToken.cancel,
|
||||
};
|
||||
};
|
||||
|
||||
export const apiPostComment = ({ id, data }: ApiPostCommentRequest) =>
|
||||
api.post<ApiPostCommentResult>(API.NODES.COMMENT(id), data).then(cleanResult);
|
||||
api.post<ApiPostCommentResult>(API.NODES.COMMENT(id), data).then(unwrap);
|
||||
|
||||
export const apiLikeComment = (
|
||||
nodeId: number,
|
||||
|
@ -110,7 +110,7 @@ export const apiLikeComment = (
|
|||
data,
|
||||
{ cancelToken: options?.cancelToken },
|
||||
)
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiGetNodeComments = ({
|
||||
id,
|
||||
|
@ -121,31 +121,31 @@ export const apiGetNodeComments = ({
|
|||
.get<ApiGetNodeCommentsResponse>(API.NODES.COMMENT(id), {
|
||||
params: { take, skip },
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiGetNodeRelated = ({ id }: ApiGetNodeRelatedRequest) =>
|
||||
api.get<ApiGetNodeRelatedResult>(API.NODES.RELATED(id)).then(cleanResult);
|
||||
api.get<ApiGetNodeRelatedResult>(API.NODES.RELATED(id)).then(unwrap);
|
||||
|
||||
export const apiPostNodeTags = ({ id, tags }: ApiPostNodeTagsRequest) =>
|
||||
api
|
||||
.post<ApiPostNodeTagsResult>(API.NODES.UPDATE_TAGS(id), { tags })
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiDeleteNodeTag = ({ id, tagId }: ApiDeleteNodeTagsRequest) =>
|
||||
api
|
||||
.delete<ApiDeleteNodeTagsResult>(API.NODES.DELETE_TAG(id, tagId))
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiPostNodeLike = ({ id }: ApiPostNodeLikeRequest) =>
|
||||
api.post<ApiPostNodeLikeResult>(API.NODES.LIKE(id)).then(cleanResult);
|
||||
api.post<ApiPostNodeLikeResult>(API.NODES.LIKE(id)).then(unwrap);
|
||||
|
||||
export const apiPostNodeHeroic = ({ id }: ApiPostNodeHeroicRequest) =>
|
||||
api.post<ApiPostNodeHeroicResponse>(API.NODES.HEROIC(id)).then(cleanResult);
|
||||
api.post<ApiPostNodeHeroicResponse>(API.NODES.HEROIC(id)).then(unwrap);
|
||||
|
||||
export const apiLockNode = ({ id, is_locked }: ApiLockNodeRequest) =>
|
||||
api
|
||||
.delete<ApiLockNodeResult>(API.NODES.DELETE(id), { params: { is_locked } })
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiLockComment = ({
|
||||
id,
|
||||
|
@ -158,4 +158,4 @@ export const apiLockComment = ({
|
|||
is_locked: isLocked,
|
||||
},
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
|
|
@ -6,26 +6,26 @@ import {
|
|||
ApiUpdateNoteRequest,
|
||||
} from '~/api/notes/types';
|
||||
import { URLS } from '~/constants/urls';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
export const apiListNotes = ({ limit, offset, search }: ApiListNotesRequest) =>
|
||||
api
|
||||
.get<ApiGetNotesResponse>(URLS.NOTES, { params: { limit, offset, search } })
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiCreateNote = ({ text }: ApiCreateNoteRequest) =>
|
||||
api
|
||||
.post<ApiUpdateNoteResponse>(URLS.NOTES, {
|
||||
text,
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
||||
export const apiDeleteNote = (id: number) =>
|
||||
api.delete(URLS.NOTE(id)).then(cleanResult);
|
||||
api.delete(URLS.NOTE(id)).then(unwrap);
|
||||
|
||||
export const apiUpdateNote = ({ id, text }: ApiUpdateNoteRequest) =>
|
||||
api
|
||||
.put<ApiUpdateNoteResponse>(URLS.NOTE(id), {
|
||||
content: text,
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { API } from '~/constants/api';
|
||||
import { NotificationSettings } from '~/types/notifications';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
import {
|
||||
notificationSettingsFromRequest,
|
||||
notificationSettingsToRequest,
|
||||
|
@ -15,13 +15,11 @@ import {
|
|||
export const apiGetNotificationSettings = (): Promise<NotificationSettings> =>
|
||||
api
|
||||
.get<ApiGetNotificationSettingsResponse>(API.NOTIFICATIONS.SETTINGS)
|
||||
.then(cleanResult)
|
||||
.then(unwrap)
|
||||
.then(notificationSettingsFromRequest);
|
||||
|
||||
export const apiGetNotifications = () =>
|
||||
api
|
||||
.get<ApiGetNotificationsResponse>(API.NOTIFICATIONS.LIST)
|
||||
.then(cleanResult);
|
||||
api.get<ApiGetNotificationsResponse>(API.NOTIFICATIONS.LIST).then(unwrap);
|
||||
|
||||
export const apiUpdateNotificationSettings = (
|
||||
settings: Partial<NotificationSettings>,
|
||||
|
@ -31,5 +29,5 @@ export const apiUpdateNotificationSettings = (
|
|||
API.NOTIFICATIONS.SETTINGS,
|
||||
notificationSettingsToRequest(settings),
|
||||
)
|
||||
.then(cleanResult)
|
||||
.then(unwrap)
|
||||
.then(notificationSettingsFromRequest);
|
||||
|
|
|
@ -5,14 +5,25 @@ import {
|
|||
ApiGetTagSuggestionsRequest,
|
||||
ApiGetTagSuggestionsResult,
|
||||
} from '~/types/tags';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
export const apiGetNodesOfTag = ({ tag, offset, limit }: ApiGetNodesOfTagRequest) =>
|
||||
export const apiGetNodesOfTag = ({
|
||||
tag,
|
||||
offset,
|
||||
limit,
|
||||
}: ApiGetNodesOfTagRequest) =>
|
||||
api
|
||||
.get<ApiGetNodesOfTagResult>(API.TAG.NODES, { params: { name: tag, offset, limit } })
|
||||
.then(cleanResult);
|
||||
.get<ApiGetNodesOfTagResult>(API.TAG.NODES, {
|
||||
params: { name: tag, offset, limit },
|
||||
})
|
||||
.then(unwrap);
|
||||
|
||||
export const apiGetTagSuggestions = ({ search, exclude }: ApiGetTagSuggestionsRequest) =>
|
||||
export const apiGetTagSuggestions = ({
|
||||
search,
|
||||
exclude,
|
||||
}: ApiGetTagSuggestionsRequest) =>
|
||||
api
|
||||
.get<ApiGetTagSuggestionsResult>(API.TAG.AUTOCOMPLETE, { params: { search, exclude } })
|
||||
.then(cleanResult);
|
||||
.get<ApiGetTagSuggestionsResult>(API.TAG.AUTOCOMPLETE, {
|
||||
params: { search, exclude },
|
||||
})
|
||||
.then(unwrap);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ApiUploadFileRequest, ApiUploadFIleResult } from '~/api/uploads/types';
|
||||
import { API } from '~/constants/api';
|
||||
import { UploadTarget, UploadType } from '~/constants/uploads';
|
||||
import { api, cleanResult } from '~/utils/api';
|
||||
import { api, unwrap } from '~/utils/api';
|
||||
|
||||
export const apiUploadFile = ({
|
||||
file,
|
||||
|
@ -16,5 +16,5 @@ export const apiUploadFile = ({
|
|||
.post<ApiUploadFIleResult>(API.USER.UPLOAD(target, type), data, {
|
||||
onUploadProgress: onProgress,
|
||||
})
|
||||
.then(cleanResult);
|
||||
.then(unwrap);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ export const api = axios.create({
|
|||
});
|
||||
|
||||
// Pass token to axios
|
||||
api.interceptors.request.use(options => {
|
||||
api.interceptors.request.use((options) => {
|
||||
const token = getMOBXStore().auth.token;
|
||||
|
||||
if (!token) {
|
||||
|
@ -20,15 +20,21 @@ api.interceptors.request.use(options => {
|
|||
});
|
||||
|
||||
// Logout on 401
|
||||
api.interceptors.response.use(undefined, (error: AxiosError<{ error: string }>) => {
|
||||
if (error.response?.status === HTTP_RESPONSES.UNAUTHORIZED) {
|
||||
getMOBXStore().auth.logout();
|
||||
}
|
||||
api.interceptors.response.use(
|
||||
undefined,
|
||||
(error: AxiosError<{ error: string }>) => {
|
||||
if (error.response?.status === HTTP_RESPONSES.UNAUTHORIZED) {
|
||||
getMOBXStore().auth.logout();
|
||||
}
|
||||
|
||||
error.message = error?.response?.data?.error || error?.response?.statusText || error.message;
|
||||
error.message =
|
||||
error?.response?.data?.error ||
|
||||
error?.response?.statusText ||
|
||||
error.message;
|
||||
|
||||
throw error;
|
||||
});
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
|
||||
export const HTTP_RESPONSES = {
|
||||
SUCCESS: 200,
|
||||
|
@ -38,6 +44,7 @@ export const HTTP_RESPONSES = {
|
|||
UNAUTHORIZED: 401,
|
||||
NOT_FOUND: 404,
|
||||
TOO_MANY_REQUESTS: 429,
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const cleanResult = <T extends any>(response: AxiosResponse<T>): T => response?.data;
|
||||
export const unwrap = <T extends any>(response: AxiosResponse<T>): T =>
|
||||
response?.data;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue