From a12a95eccc13c2e41102630bf526eccbf519d9f1 Mon Sep 17 00:00:00 2001 From: Riccardo Graziosi <31478034+riggraz@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:06:48 +0100 Subject: [PATCH] Add webhooks (#447) --- Gemfile | 5 +- Gemfile.lock | 6 +- app/assets/stylesheets/application.sass.scss | 1 + app/assets/stylesheets/common/_index.scss | 1 + .../stylesheets/components/Comments.scss | 2 + .../SiteSettings/Invitations/index.scss | 2 +- .../SiteSettings/Webhooks/index.scss | 126 ++++++ app/controllers/site_settings_controller.rb | 3 + app/controllers/webhooks_controller.rb | 97 ++++ app/javascript/actions/OAuth/updateOAuth.ts | 1 + .../actions/Webhook/deleteWebhook.ts | 69 +++ .../actions/Webhook/requestWebhooks.ts | 59 +++ .../actions/Webhook/submitWebhook.ts | 78 ++++ .../actions/Webhook/updateWebhook.ts | 96 ++++ .../components/Board/NewPostForm.tsx | 4 + .../components/Comments/CommentEditForm.tsx | 22 +- .../components/Comments/NewComment.tsx | 22 +- .../Authentication/AuthenticationFormPage.tsx | 1 - .../AuthenticationIndexPage.tsx | 2 +- .../SiteSettings/Authentication/OAuthForm.tsx | 2 + .../Authentication/OAuthProvidersList.tsx | 2 +- .../SiteSettings/Boards/BoardForm.tsx | 17 +- .../SiteSettings/Roadmap/RoadmapEmbedding.tsx | 1 + .../Webhooks/TemplateVariablesSelector.tsx | 241 ++++++++++ .../SiteSettings/Webhooks/WebhookForm.tsx | 415 ++++++++++++++++++ .../SiteSettings/Webhooks/WebhookFormPage.tsx | 48 ++ .../SiteSettings/Webhooks/WebhookListItem.tsx | 88 ++++ .../SiteSettings/Webhooks/WebhookTestPage.tsx | 84 ++++ .../Webhooks/WebhooksIndexPage.tsx | 77 ++++ .../SiteSettings/Webhooks/WebhooksList.tsx | 67 +++ .../Webhooks/WebhooksSiteSettingsP.tsx | 124 ++++++ .../SiteSettings/Webhooks/index.tsx | 33 ++ app/javascript/components/common/Badge.tsx | 4 +- app/javascript/components/common/Icons.tsx | 58 ++- app/javascript/constants/regex.ts | 3 +- .../containers/WebhooksSiteSettings.tsx | 44 ++ app/javascript/interfaces/IWebhook.ts | 77 ++++ .../reducers/SiteSettings/webhooksReducer.ts | 71 +++ app/javascript/reducers/rootReducer.ts | 2 + .../reducers/siteSettingsReducer.ts | 43 +- app/javascript/reducers/webhooksReducer.ts | 93 ++++ app/jobs/run_webhook.rb | 105 +++++ app/lib/custom_liquid_filters.rb | 7 + app/models/comment.rb | 22 + app/models/like.rb | 21 + app/models/post.rb | 55 +++ app/models/post_status_change.rb | 21 + app/models/user.rb | 17 + app/models/webhook.rb | 76 ++++ app/policies/webhook_policy.rb | 34 ++ app/views/site_settings/_menu.html.erb | 1 + app/views/site_settings/webhooks.html.erb | 13 + ...create_liquid_template_context_workflow.rb | 159 +++++++ config/initializers/liquid.rb | 4 + config/locales/backend/backend.en.yml | 12 + config/locales/en.yml | 30 ++ config/routes.rb | 4 + db/migrate/20241130112415_create_webhooks.rb | 19 + db/schema.rb | 19 +- package.json | 9 +- spec/factories/webhooks.rb | 11 + spec/models/webhook_spec.rb | 70 +++ yarn.lock | 78 +++- 63 files changed, 2914 insertions(+), 64 deletions(-) create mode 100644 app/assets/stylesheets/components/SiteSettings/Webhooks/index.scss create mode 100644 app/controllers/webhooks_controller.rb create mode 100644 app/javascript/actions/Webhook/deleteWebhook.ts create mode 100644 app/javascript/actions/Webhook/requestWebhooks.ts create mode 100644 app/javascript/actions/Webhook/submitWebhook.ts create mode 100644 app/javascript/actions/Webhook/updateWebhook.ts create mode 100644 app/javascript/components/SiteSettings/Webhooks/TemplateVariablesSelector.tsx create mode 100644 app/javascript/components/SiteSettings/Webhooks/WebhookForm.tsx create mode 100644 app/javascript/components/SiteSettings/Webhooks/WebhookFormPage.tsx create mode 100644 app/javascript/components/SiteSettings/Webhooks/WebhookListItem.tsx create mode 100644 app/javascript/components/SiteSettings/Webhooks/WebhookTestPage.tsx create mode 100644 app/javascript/components/SiteSettings/Webhooks/WebhooksIndexPage.tsx create mode 100644 app/javascript/components/SiteSettings/Webhooks/WebhooksList.tsx create mode 100644 app/javascript/components/SiteSettings/Webhooks/WebhooksSiteSettingsP.tsx create mode 100644 app/javascript/components/SiteSettings/Webhooks/index.tsx create mode 100644 app/javascript/containers/WebhooksSiteSettings.tsx create mode 100644 app/javascript/interfaces/IWebhook.ts create mode 100644 app/javascript/reducers/SiteSettings/webhooksReducer.ts create mode 100644 app/javascript/reducers/webhooksReducer.ts create mode 100644 app/jobs/run_webhook.rb create mode 100644 app/lib/custom_liquid_filters.rb create mode 100644 app/models/webhook.rb create mode 100644 app/policies/webhook_policy.rb create mode 100644 app/views/site_settings/webhooks.html.erb create mode 100644 app/workflows/create_liquid_template_context_workflow.rb create mode 100644 config/initializers/liquid.rb create mode 100644 db/migrate/20241130112415_create_webhooks.rb create mode 100644 spec/factories/webhooks.rb create mode 100644 spec/models/webhook_spec.rb diff --git a/Gemfile b/Gemfile index cf3a4084..0a33f3dd 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.0.6' -gem 'rake', '12.3.3' +gem 'rake', '13.2.1' gem 'rails', '6.1.7.9' @@ -67,6 +67,9 @@ gem 'sidekiq', '7.3.5' # Cron jobs with sidekiq gem 'sidekiq-cron', '2.0.1' +# Template language +gem 'liquid', '5.5.1' + group :development, :test do gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] diff --git a/Gemfile.lock b/Gemfile.lock index 6996e9c5..c425ba80 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -141,6 +141,7 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) + liquid (5.5.1) listen (3.5.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -218,7 +219,7 @@ GEM method_source rake (>= 12.2) thor (~> 1.0) - rake (12.3.3) + rake (13.2.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) @@ -330,6 +331,7 @@ DEPENDENCIES jbuilder (= 2.11.5) jsbundling-rails (= 1.1.1) kaminari (= 1.2.2) + liquid (= 5.5.1) listen (= 3.5.1) pg (= 1.3.5) puma (= 5.6.9) @@ -337,7 +339,7 @@ DEPENDENCIES rack-attack (= 6.7.0) rack-cors (= 2.0.2) rails (= 6.1.7.9) - rake (= 12.3.3) + rake (= 13.2.1) react-rails (= 2.6.2) rspec-rails (= 4.0.2) rspec-retry (= 0.6.2) diff --git a/app/assets/stylesheets/application.sass.scss b/app/assets/stylesheets/application.sass.scss index 5a2f69a5..4ccbdc9b 100644 --- a/app/assets/stylesheets/application.sass.scss +++ b/app/assets/stylesheets/application.sass.scss @@ -31,6 +31,7 @@ @import 'components/SiteSettings/Authentication'; @import 'components/SiteSettings/Appearance/'; @import 'components/SiteSettings/Invitations'; +@import 'components/SiteSettings/Webhooks'; /* Moderation Components */ @import 'components/Moderation/Feedback'; diff --git a/app/assets/stylesheets/common/_index.scss b/app/assets/stylesheets/common/_index.scss index 50209904..f518cb03 100644 --- a/app/assets/stylesheets/common/_index.scss +++ b/app/assets/stylesheets/common/_index.scss @@ -173,6 +173,7 @@ body { } .badgeWarning { @extend .badge-warning; } .badgeDanger { @extend .badge-danger; } +.badgeSuccess { @extend .badge-success; } .container { max-width: 960px; diff --git a/app/assets/stylesheets/components/Comments.scss b/app/assets/stylesheets/components/Comments.scss index 65f13239..3a42db47 100644 --- a/app/assets/stylesheets/components/Comments.scss +++ b/app/assets/stylesheets/components/Comments.scss @@ -53,6 +53,8 @@ } .editCommentForm { + .commentFormContainer { @extend .d-block; } + textarea { @extend .my-2; } diff --git a/app/assets/stylesheets/components/SiteSettings/Invitations/index.scss b/app/assets/stylesheets/components/SiteSettings/Invitations/index.scss index bce9f29b..6c68d461 100644 --- a/app/assets/stylesheets/components/SiteSettings/Invitations/index.scss +++ b/app/assets/stylesheets/components/SiteSettings/Invitations/index.scss @@ -59,7 +59,7 @@ .pl-0; list-style: none; - height: 500px; + max-height: 500px; overflow-y: scroll; li.invitationListItem { diff --git a/app/assets/stylesheets/components/SiteSettings/Webhooks/index.scss b/app/assets/stylesheets/components/SiteSettings/Webhooks/index.scss new file mode 100644 index 00000000..2def5424 --- /dev/null +++ b/app/assets/stylesheets/components/SiteSettings/Webhooks/index.scss @@ -0,0 +1,126 @@ +.previewStyling { + @extend .mt-4, .p-3; + + max-height: 400px; + max-width: 600px; + + background-color: #f4f4f4; + + border-radius: 8px; +} + +a.backButton { + @extend .mb-2, .align-self-start; + + font-size: 18px; +} + +.webhooksIndexPage { + .webhooksTitle { + @extend + .d-flex, + .mb-2; + + button { + @extend .ml-2; + + height: min-content; + } + } + + .webhooksList { + @extend .pl-1; + + list-style: none; + + h4 { @extend .mb-0, .mt-2; } + ul { @extend .pl-0; } + + .webhookListItem { + @extend + .d-flex, + .justify-content-between, + .mb-2, + .p-2; + + .webhookInfo { + @extend .d-flex; + + column-gap: 32px; + + .webhookName { font-size: 18px; } + .webhookDescription { + @extend .mutedText, .mb-1; + font-size: smaller; + } + } + + .webhookActions { + @extend .d-flex; + + align-self: center; + } + } + } +} + +.webhookFormPage { + #httpBody { + min-height: 200px; + } + + .httpBodyActions { + @extend + .d-flex, + .justify-content-between, + .mt-1; + + div:first-child { + min-width: 250px; + } + } + + .previewHttpBody { + display: block; + text-align: right; + } + + .urlAndHttpBodyPreview { + @extend .mt-3; + + #preview { + @extend .previewStyling; + margin-top: 0 !important; + } + } + + .formGroupHttpHeaders { + @extend .mb-0; + + div.formRow { @extend .mb-0; } + + div.formRow:last-child { + div.formGroup { @extend .mb-1; } + } + } + + .deleteHeaderActionLinkContainer { + @extend + .d-flex, + .align-self-end; + } + + .submitWebhookFormButton { @extend .mt-4; } +} + +.webhookTestPage { + .webhookActions { + @extend .d-flex; + } + + .webhookTestResponse { + @extend .mt-4; + + #testHttpResponse { @extend .previewStyling; } + } +} \ No newline at end of file diff --git a/app/controllers/site_settings_controller.rb b/app/controllers/site_settings_controller.rb index 7c20e945..bd1eb877 100644 --- a/app/controllers/site_settings_controller.rb +++ b/app/controllers/site_settings_controller.rb @@ -19,6 +19,9 @@ class SiteSettingsController < ApplicationController def roadmap end + def webhooks + end + def invitations @invitations = Invitation.all.order(updated_at: :desc) end diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb new file mode 100644 index 00000000..5a4daf05 --- /dev/null +++ b/app/controllers/webhooks_controller.rb @@ -0,0 +1,97 @@ +class WebhooksController < ApplicationController + def index + authorize Webhook + + @webhooks = Webhook.order(trigger: :asc, created_at: :desc) + + render json: @webhooks + end + + def create + @webhook = Webhook.new + @webhook.assign_attributes(webhook_params) + authorize @webhook + + if @webhook.save + render json: @webhook, status: :created + else + render json: { + error: @webhook.errors.full_messages + }, status: :unprocessable_entity + end + end + + def update + @webhook = Webhook.find(params[:id]) + authorize @webhook + + if @webhook.update(webhook_params) + render json: @webhook + else + render json: { + error: @webhook.errors.full_messages + }, status: :unprocessable_entity + end + end + + def destroy + @webhook = Webhook.find(params[:id]) + authorize @webhook + + if @webhook.destroy + render json: { + id: params[:id] + }, status: :accepted + else + render json: { + error: @webhook.errors.full_messages + }, status: :unprocessable_entity + end + end + + def preview + context = CreateLiquidTemplateContextWorkflow.new( + webhook_trigger: params[:webhook][:trigger], + is_test: true, + ).run + + url_template = Liquid::Template.parse(params[:webhook][:url]) + url_preview = url_template.render(context) + + http_body_template = Liquid::Template.parse(params[:webhook][:http_body]) + http_body_preview = http_body_template.render(context) + + render json: { + url_preview: url_preview, + http_body_preview: http_body_preview, + }, status: :ok + rescue => e + render json: { + error: e.message + }, status: :unprocessable_entity + end + + def test + response = RunWebhook.perform_now( + webhook_id: params[:id], + current_tenant_id: Current.tenant.id, + is_test: true + ) + + render json: { + response: response, + }, status: :ok + rescue => e + render json: { + error: e.message + }, status: :unprocessable_entity + end + + private + + def webhook_params + params + .require(:webhook) + .permit(policy(@webhook).permitted_attributes) + end +end \ No newline at end of file diff --git a/app/javascript/actions/OAuth/updateOAuth.ts b/app/javascript/actions/OAuth/updateOAuth.ts index b9cbef15..ff0f47a7 100644 --- a/app/javascript/actions/OAuth/updateOAuth.ts +++ b/app/javascript/actions/OAuth/updateOAuth.ts @@ -1,5 +1,6 @@ import { Action } from "redux"; import { ThunkAction } from "redux-thunk"; + import { ISiteSettingsOAuthForm } from "../../components/SiteSettings/Authentication/OAuthForm"; import HttpStatus from "../../constants/http_status"; import buildRequestHeaders from "../../helpers/buildRequestHeaders"; diff --git a/app/javascript/actions/Webhook/deleteWebhook.ts b/app/javascript/actions/Webhook/deleteWebhook.ts new file mode 100644 index 00000000..9f188d66 --- /dev/null +++ b/app/javascript/actions/Webhook/deleteWebhook.ts @@ -0,0 +1,69 @@ +import { Action } from "redux"; +import { ThunkAction } from "redux-thunk"; +import HttpStatus from "../../constants/http_status"; + +import buildRequestHeaders from "../../helpers/buildRequestHeaders"; +import { State } from "../../reducers/rootReducer"; + +export const WEBHOOK_DELETE_START = 'WEBHOOK_DELETE_START'; +interface WebhookDeleteStartAction { + type: typeof WEBHOOK_DELETE_START; +} + +export const WEBHOOK_DELETE_SUCCESS = 'WEBHOOK_DELETE_SUCCESS'; +interface WebhookDeleteSuccessAction { + type: typeof WEBHOOK_DELETE_SUCCESS; + id: number; +} + +export const WEBHOOK_DELETE_FAILURE = 'WEBHOOK_DELETE_FAILURE'; +interface WebhookDeleteFailureAction { + type: typeof WEBHOOK_DELETE_FAILURE; + error: string; +} + +export type WebhookDeleteActionTypes = + WebhookDeleteStartAction | + WebhookDeleteSuccessAction | + WebhookDeleteFailureAction; + +const webhookDeleteStart = (): WebhookDeleteStartAction => ({ + type: WEBHOOK_DELETE_START, +}); + +const webhookDeleteSuccess = ( + id: number, +): WebhookDeleteSuccessAction => ({ + type: WEBHOOK_DELETE_SUCCESS, + id, +}); + +const webhookDeleteFailure = (error: string): WebhookDeleteFailureAction => ({ + type: WEBHOOK_DELETE_FAILURE, + error, +}); + +export const deleteWebhook = ( + id: number, + authenticityToken: string, +): ThunkAction> => ( + async (dispatch) => { + dispatch(webhookDeleteStart()); + + try { + const res = await fetch(`/webhooks/${id}`, { + method: 'DELETE', + headers: buildRequestHeaders(authenticityToken), + }); + const json = await res.json(); + + if (res.status === HttpStatus.Accepted) { + dispatch(webhookDeleteSuccess(id)); + } else { + dispatch(webhookDeleteFailure(json.error)); + } + } catch (error) { + dispatch(webhookDeleteFailure(error.message)); + } + } +); \ No newline at end of file diff --git a/app/javascript/actions/Webhook/requestWebhooks.ts b/app/javascript/actions/Webhook/requestWebhooks.ts new file mode 100644 index 00000000..fcf53e69 --- /dev/null +++ b/app/javascript/actions/Webhook/requestWebhooks.ts @@ -0,0 +1,59 @@ +import { Action } from 'redux'; +import { ThunkAction } from 'redux-thunk'; + +import { IWebhookJSON } from '../../interfaces/IWebhook'; +import { State } from '../../reducers/rootReducer'; + +export const WEBHOOKS_REQUEST_START = 'WEBHOOKS_REQUEST_START'; +interface WebhooksRequestStartAction { + type: typeof WEBHOOKS_REQUEST_START; +} + +export const WEBHOOKS_REQUEST_SUCCESS = 'WEBHOOKS_REQUEST_SUCCESS'; +interface WebhooksRequestSuccessAction { + type: typeof WEBHOOKS_REQUEST_SUCCESS; + webhooks: Array; +} + +export const WEBHOOKS_REQUEST_FAILURE = 'WEBHOOKS_REQUEST_FAILURE'; +interface WebhooksRequestFailureAction { + type: typeof WEBHOOKS_REQUEST_FAILURE; + error: string; +} + +export type WebhooksRequestActionTypes = + WebhooksRequestStartAction | + WebhooksRequestSuccessAction | + WebhooksRequestFailureAction; + + +const webhooksRequestStart = (): WebhooksRequestActionTypes => ({ + type: WEBHOOKS_REQUEST_START, +}); + +const webhooksRequestSuccess = ( + webhooks: Array +): WebhooksRequestActionTypes => ({ + type: WEBHOOKS_REQUEST_SUCCESS, + webhooks, +}); + +const webhooksRequestFailure = (error: string): WebhooksRequestActionTypes => ({ + type: WEBHOOKS_REQUEST_FAILURE, + error, +}); + +export const requestWebhooks = (): ThunkAction> => ( + async (dispatch) => { + dispatch(webhooksRequestStart()); + + try { + const response = await fetch('/webhooks'); + const json = await response.json(); + + dispatch(webhooksRequestSuccess(json)); + } catch (e) { + dispatch(webhooksRequestFailure(e)); + } + } +) \ No newline at end of file diff --git a/app/javascript/actions/Webhook/submitWebhook.ts b/app/javascript/actions/Webhook/submitWebhook.ts new file mode 100644 index 00000000..8dabc040 --- /dev/null +++ b/app/javascript/actions/Webhook/submitWebhook.ts @@ -0,0 +1,78 @@ +import { Action } from "redux"; +import { ThunkAction } from "redux-thunk"; + +import { IWebhook, IWebhookJSON, webhookJS2JSON } from "../../interfaces/IWebhook"; +import { State } from "react-joyride"; +import buildRequestHeaders from "../../helpers/buildRequestHeaders"; +import HttpStatus from "../../constants/http_status"; + +export const WEBHOOK_SUBMIT_START = 'WEBHOOK_SUBMIT_START'; +interface WebhookSubmitStartAction { + type: typeof WEBHOOK_SUBMIT_START; +} + +export const WEBHOOK_SUBMIT_SUCCESS = 'WEBHOOK_SUBMIT_SUCCESS'; +interface WebhookSubmitSuccessAction { + type: typeof WEBHOOK_SUBMIT_SUCCESS; + webhook: IWebhookJSON; +} + +export const WEBHOOK_SUBMIT_FAILURE = 'WEBHOOK_SUBMIT_FAILURE'; +interface WebhookSubmitFailureAction { + type: typeof WEBHOOK_SUBMIT_FAILURE; + error: string; +} + +export type WebhookSubmitActionTypes = + WebhookSubmitStartAction | + WebhookSubmitSuccessAction | + WebhookSubmitFailureAction; + +const webhookSubmitStart = (): WebhookSubmitStartAction => ({ + type: WEBHOOK_SUBMIT_START, +}); + +const webhookSubmitSuccess = ( + webhookJSON: IWebhookJSON, +): WebhookSubmitSuccessAction => ({ + type: WEBHOOK_SUBMIT_SUCCESS, + webhook: webhookJSON, +}); + +const webhookSubmitFailure = (error: string): WebhookSubmitFailureAction => ({ + type: WEBHOOK_SUBMIT_FAILURE, + error, +}); + +export const submitWebhook = ( + webhook: IWebhook, + authenticityToken: string, +): ThunkAction> => async (dispatch) => { + dispatch(webhookSubmitStart()); + + try { + const res = await fetch(`/webhooks`, { + method: 'POST', + headers: buildRequestHeaders(authenticityToken), + body: JSON.stringify({ + webhook: { + ...webhookJS2JSON(webhook), + is_enabled: false, + }, + }), + }); + const json = await res.json(); + + if (res.status === HttpStatus.Created) { + dispatch(webhookSubmitSuccess(json)); + } else { + dispatch(webhookSubmitFailure(json.error)); + } + + return Promise.resolve(res); + } catch (e) { + dispatch(webhookSubmitFailure(e)); + + return Promise.resolve(null); + } +}; \ No newline at end of file diff --git a/app/javascript/actions/Webhook/updateWebhook.ts b/app/javascript/actions/Webhook/updateWebhook.ts new file mode 100644 index 00000000..35bbfd09 --- /dev/null +++ b/app/javascript/actions/Webhook/updateWebhook.ts @@ -0,0 +1,96 @@ +import { Action } from "redux"; +import { ThunkAction } from "redux-thunk"; + +import HttpStatus from "../../constants/http_status"; +import buildRequestHeaders from "../../helpers/buildRequestHeaders"; +import { State } from "../../reducers/rootReducer"; +import { IWebhookJSON } from "../../interfaces/IWebhook"; +import { ISiteSettingsWebhookFormUpdate } from "../../components/SiteSettings/Webhooks/WebhookForm"; + +export const WEBHOOK_UPDATE_START = 'WEBHOOK_UPDATE_START'; +interface WebhookUpdateStartAction { + type: typeof WEBHOOK_UPDATE_START; +} + +export const WEBHOOK_UPDATE_SUCCESS = 'WEBHOOK_UPDATE_SUCCESS'; +interface WebhookUpdateSuccessAction { + type: typeof WEBHOOK_UPDATE_SUCCESS; + webhook: IWebhookJSON; +} + +export const WEBHOOK_UPDATE_FAILURE = 'WEBHOOK_UPDATE_FAILURE'; +interface WebhookUpdateFailureAction { + type: typeof WEBHOOK_UPDATE_FAILURE; + error: string; +} + +export type WebhookUpdateActionTypes = + WebhookUpdateStartAction | + WebhookUpdateSuccessAction | + WebhookUpdateFailureAction; + +const webhookUpdateStart = (): WebhookUpdateStartAction => ({ + type: WEBHOOK_UPDATE_START, +}); + +const webhookUpdateSuccess = ( + webhookJSON: IWebhookJSON, +): WebhookUpdateSuccessAction => ({ + type: WEBHOOK_UPDATE_SUCCESS, + webhook: webhookJSON, +}); + +const webhookUpdateFailure = (error: string): WebhookUpdateFailureAction => ({ + type: WEBHOOK_UPDATE_FAILURE, + error, +}); + +interface UpdateWebhookParams { + id: number; + form?: ISiteSettingsWebhookFormUpdate; + isEnabled?: boolean; + authenticityToken: string; +} + +export const updateWebhook = ({ + id, + form = null, + isEnabled = null, + authenticityToken, +}: UpdateWebhookParams): ThunkAction> => async (dispatch) => { + dispatch(webhookUpdateStart()); + + const webhook = Object.assign({}, + form !== null ? { + name: form.name, + description: form.description, + trigger: form.trigger, + http_method: form.httpMethod, + url: form.url, + http_body: form.httpBody, + http_headers: form.httpHeaders, + } : null, + isEnabled !== null ? { is_enabled: isEnabled } : null, + ); + + try { + const res = await fetch(`/webhooks/${id}`, { + method: 'PATCH', + headers: buildRequestHeaders(authenticityToken), + body: JSON.stringify({ webhook }), + }); + const json = await res.json(); + + if (res.status === HttpStatus.OK) { + dispatch(webhookUpdateSuccess(json)); + } else { + dispatch(webhookUpdateFailure(json.error)); + } + + return Promise.resolve(res); + } catch (e) { + dispatch(webhookUpdateFailure(e)); + + return Promise.resolve(null); + } +}; \ No newline at end of file diff --git a/app/javascript/components/Board/NewPostForm.tsx b/app/javascript/components/Board/NewPostForm.tsx index 7e63a65c..b81a8eb5 100644 --- a/app/javascript/components/Board/NewPostForm.tsx +++ b/app/javascript/components/Board/NewPostForm.tsx @@ -3,6 +3,7 @@ import I18n from 'i18n-js'; import Button from '../common/Button'; import { SmallMutedText } from '../common/CustomTexts'; +import { MarkdownIcon } from '../common/Icons'; interface Props { title: string; @@ -93,6 +94,9 @@ const NewPostForm = ({ className="form-control" id="postDescription" > +
+ +