Skip to content

Commit 321b232

Browse files
authored
Move "Webhook" settings to table (outline#12119)
* Move 'Webhook' settings to table * Add tests
1 parent 69e8aac commit 321b232

File tree

17 files changed

+653
-152
lines changed

17 files changed

+653
-152
lines changed

app/models/WebhookSubscription.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { observable } from "mobx";
1+
import { computed, observable } from "mobx";
22
import Model from "./base/Model";
33
import Field from "./decorators/Field";
4+
import Relation from "./decorators/Relation";
5+
import type { Searchable } from "./interfaces/Searchable";
6+
import User from "./User";
47

5-
class WebhookSubscription extends Model {
8+
class WebhookSubscription extends Model implements Searchable {
69
static modelName = "WebhookSubscription";
710

811
@Field
@@ -24,6 +27,23 @@ class WebhookSubscription extends Model {
2427
@Field
2528
@observable
2629
events: string[];
30+
31+
/** The user who created this webhook subscription. */
32+
@Relation(() => User)
33+
createdBy?: User;
34+
35+
/** The user ID that created this webhook subscription. */
36+
createdById: string;
37+
38+
@computed
39+
get searchContent(): string[] {
40+
return [this.name, this.url, ...(this.events ?? [])].filter(Boolean);
41+
}
42+
43+
@computed
44+
get searchSuppressed(): boolean {
45+
return false;
46+
}
2747
}
2848

2949
export default WebhookSubscription;
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import Text from "@shared/components/Text";
22
import { s } from "@shared/styles";
3-
import styled from "styled-components";
3+
import styled, { css } from "styled-components";
4+
5+
type StatusColor = "accent" | "warning" | "danger" | "textTertiary" | "success";
46

57
export const Status = styled(Text).attrs({
68
type: "secondary",
79
size: "small",
810
as: "span",
9-
})`
11+
})<{ $color?: StatusColor }>`
1012
display: inline-flex;
1113
align-items: center;
1214
@@ -16,11 +18,13 @@ export const Status = styled(Text).attrs({
1618
width: 17px;
1719
height: 17px;
1820
19-
background: radial-gradient(
20-
circle at center,
21-
${s("accent")} 0 33%,
22-
transparent 33%
23-
);
21+
${(props) => css`
22+
background: radial-gradient(
23+
circle at center,
24+
${s(props.$color ?? "accent")} 0 33%,
25+
transparent 33%
26+
);
27+
`}
2428
border-radius: 50%;
2529
}
2630
`;

plugins/webhooks/client/Settings.tsx

Lines changed: 105 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,106 @@
1+
import type { ColumnSort } from "@tanstack/react-table";
12
import { observer } from "mobx-react";
23
import { PlusIcon, WebhooksIcon } from "outline-icons";
3-
import * as React from "react";
4+
import { useCallback, useEffect, useMemo, useState } from "react";
45
import { useTranslation, Trans } from "react-i18next";
5-
import type WebhookSubscription from "~/models/WebhookSubscription";
6+
import { useHistory, useLocation } from "react-router-dom";
7+
import { toast } from "sonner";
68
import { Action } from "~/components/Actions";
79
import Button from "~/components/Button";
10+
import { ConditionalFade } from "~/components/Fade";
811
import Heading from "~/components/Heading";
9-
import Modal from "~/components/Modal";
10-
import PaginatedList from "~/components/PaginatedList";
12+
import InputSearch from "~/components/InputSearch";
1113
import Scene from "~/components/Scene";
1214
import Text from "~/components/Text";
1315
import env from "~/env";
14-
import useBoolean from "~/hooks/useBoolean";
1516
import useCurrentTeam from "~/hooks/useCurrentTeam";
1617
import usePolicy from "~/hooks/usePolicy";
18+
import useQuery from "~/hooks/useQuery";
1719
import useStores from "~/hooks/useStores";
18-
import WebhookSubscriptionListItem from "./components/WebhookSubscriptionListItem";
19-
import WebhookSubscriptionNew from "./components/WebhookSubscriptionNew";
20+
import { useTableRequest } from "~/hooks/useTableRequest";
21+
import { StickyFilters } from "~/scenes/Settings/components/StickyFilters";
22+
import { createWebhookSubscription } from "./actions";
23+
import { WebhookSubscriptionsTable } from "./components/WebhookSubscriptionsTable";
2024

2125
function Webhooks() {
2226
const team = useCurrentTeam();
2327
const { t } = useTranslation();
2428
const { webhookSubscriptions } = useStores();
25-
const [newModalOpen, handleNewModalOpen, handleNewModalClose] = useBoolean();
2629
const can = usePolicy(team);
2730
const appName = env.APP_NAME;
31+
const params = useQuery();
32+
const history = useHistory();
33+
const location = useLocation();
34+
const [query, setQuery] = useState(params.get("query") || "");
35+
36+
const reqParams = useMemo(
37+
() => ({
38+
query: params.get("query") || undefined,
39+
sort: params.get("sort") || "createdAt",
40+
direction: (params.get("direction") || "desc").toUpperCase() as
41+
| "ASC"
42+
| "DESC",
43+
}),
44+
[params]
45+
);
46+
47+
const sort: ColumnSort = useMemo(
48+
() => ({
49+
id: reqParams.sort,
50+
desc: reqParams.direction === "DESC",
51+
}),
52+
[reqParams.sort, reqParams.direction]
53+
);
54+
55+
const orderedData = webhookSubscriptions.orderedData;
56+
const filteredWebhooks = useMemo(
57+
() =>
58+
reqParams.query
59+
? webhookSubscriptions.findByQuery(reqParams.query)
60+
: orderedData,
61+
[webhookSubscriptions, orderedData, reqParams.query]
62+
);
63+
64+
const { data, error, loading, next } = useTableRequest({
65+
data: filteredWebhooks,
66+
sort,
67+
reqFn: webhookSubscriptions.fetchPage,
68+
reqParams,
69+
});
70+
71+
const updateParams = useCallback(
72+
(name: string, value: string) => {
73+
if (value) {
74+
params.set(name, value);
75+
} else {
76+
params.delete(name);
77+
}
78+
79+
history.replace({
80+
pathname: location.pathname,
81+
search: params.toString(),
82+
});
83+
},
84+
[params, history, location.pathname]
85+
);
86+
87+
const handleSearch = useCallback(
88+
(event: React.ChangeEvent<HTMLInputElement>) => {
89+
setQuery(event.target.value);
90+
},
91+
[]
92+
);
93+
94+
useEffect(() => {
95+
if (error) {
96+
toast.error(t("Could not load webhooks"));
97+
}
98+
}, [t, error]);
99+
100+
useEffect(() => {
101+
const timeout = setTimeout(() => updateParams("query", query), 250);
102+
return () => clearTimeout(timeout);
103+
}, [query, updateParams]);
28104

29105
return (
30106
<Scene
@@ -36,7 +112,7 @@ function Webhooks() {
36112
<Action>
37113
<Button
38114
type="button"
39-
onClick={handleNewModalOpen}
115+
action={createWebhookSubscription}
40116
icon={<PlusIcon />}
41117
>
42118
{`${t("New webhook")}…`}
@@ -45,6 +121,7 @@ function Webhooks() {
45121
)}
46122
</>
47123
}
124+
wide
48125
>
49126
<Heading>{t("Webhooks")}</Heading>
50127
<Text as="p" type="secondary">
@@ -54,29 +131,25 @@ function Webhooks() {
54131
in near real-time.
55132
</Trans>
56133
</Text>
57-
<PaginatedList<WebhookSubscription>
58-
fetch={webhookSubscriptions.fetchPage}
59-
items={webhookSubscriptions.enabled}
60-
heading={<h2>{t("Active")}</h2>}
61-
renderItem={(webhook) => (
62-
<WebhookSubscriptionListItem key={webhook.id} webhook={webhook} />
63-
)}
64-
/>
65-
<PaginatedList<WebhookSubscription>
66-
items={webhookSubscriptions.disabled}
67-
heading={<h2>{t("Inactive")}</h2>}
68-
renderItem={(webhook) => (
69-
<WebhookSubscriptionListItem key={webhook.id} webhook={webhook} />
70-
)}
71-
/>
72-
<Modal
73-
title={t("New webhook")}
74-
onRequestClose={handleNewModalClose}
75-
isOpen={newModalOpen}
76-
width="480px"
77-
>
78-
<WebhookSubscriptionNew onSubmit={handleNewModalClose} />
79-
</Modal>
134+
<StickyFilters>
135+
<InputSearch
136+
short
137+
value={query}
138+
placeholder={`${t("Filter")}…`}
139+
onChange={handleSearch}
140+
/>
141+
</StickyFilters>
142+
<ConditionalFade animate={!data}>
143+
<WebhookSubscriptionsTable
144+
data={data ?? []}
145+
sort={sort}
146+
loading={loading}
147+
page={{
148+
hasNext: !!next,
149+
fetchNext: next,
150+
}}
151+
/>
152+
</ConditionalFade>
80153
</Scene>
81154
);
82155
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { EditIcon, PlusIcon, TrashIcon } from "outline-icons";
2+
import stores from "~/stores";
3+
import type WebhookSubscription from "~/models/WebhookSubscription";
4+
import { createAction } from "~/actions";
5+
import { SettingsSection } from "~/actions/sections";
6+
import WebhookSubscriptionDeleteDialog from "./components/WebhookSubscriptionDeleteDialog";
7+
import WebhookSubscriptionEdit from "./components/WebhookSubscriptionEdit";
8+
import WebhookSubscriptionNew from "./components/WebhookSubscriptionNew";
9+
10+
export const createWebhookSubscription = createAction({
11+
name: ({ t }) => t("New webhook"),
12+
analyticsName: "New webhook",
13+
section: SettingsSection,
14+
icon: <PlusIcon />,
15+
keywords: "create",
16+
visible: () =>
17+
stores.policies.abilities(stores.auth.team?.id || "")
18+
.createWebhookSubscription,
19+
perform: ({ t, event }) => {
20+
event?.preventDefault();
21+
event?.stopPropagation();
22+
23+
stores.dialogs.openModal({
24+
title: t("New webhook"),
25+
content: (
26+
<WebhookSubscriptionNew onSubmit={stores.dialogs.closeAllModals} />
27+
),
28+
});
29+
},
30+
});
31+
32+
export const editWebhookSubscriptionFactory = ({
33+
webhook,
34+
}: {
35+
webhook: WebhookSubscription;
36+
}) =>
37+
createAction({
38+
name: ({ t }) => `${t("Edit")}…`,
39+
analyticsName: "Edit webhook",
40+
section: SettingsSection,
41+
icon: <EditIcon />,
42+
perform: ({ t, event }) => {
43+
event?.preventDefault();
44+
event?.stopPropagation();
45+
46+
stores.dialogs.openModal({
47+
title: t("Edit webhook"),
48+
content: (
49+
<WebhookSubscriptionEdit
50+
onSubmit={stores.dialogs.closeAllModals}
51+
webhookSubscription={webhook}
52+
/>
53+
),
54+
});
55+
},
56+
});
57+
58+
export const deleteWebhookSubscriptionFactory = ({
59+
webhook,
60+
}: {
61+
webhook: WebhookSubscription;
62+
}) =>
63+
createAction({
64+
name: ({ t }) => `${t("Delete")}…`,
65+
analyticsName: "Delete webhook",
66+
section: SettingsSection,
67+
icon: <TrashIcon />,
68+
keywords: "delete remove",
69+
dangerous: true,
70+
perform: ({ t, event }) => {
71+
event?.preventDefault();
72+
event?.stopPropagation();
73+
74+
stores.dialogs.openModal({
75+
title: t("Delete webhook"),
76+
content: (
77+
<WebhookSubscriptionDeleteDialog
78+
onSubmit={stores.dialogs.closeAllModals}
79+
webhook={webhook}
80+
/>
81+
),
82+
});
83+
},
84+
});

0 commit comments

Comments
 (0)