Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
320e7ea246 | ||
|
|
c1f6d9f76f | ||
|
|
ed562832a6 |
+1
-1
@@ -799,7 +799,7 @@ GEM
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.4.2)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (0.13.0)
|
||||
uri (1.0.3)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (5.2.6)
|
||||
activemodel (>= 3.2)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
button,
|
||||
input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"],
|
||||
.button {
|
||||
button:not(.reset-base),
|
||||
input[type='button']:not(.reset-base),
|
||||
input[type='reset']:not(.reset-base),
|
||||
input[type='submit']:not(.reset-base),
|
||||
.button:not(.reset-base) {
|
||||
appearance: none;
|
||||
background-color: $color-woot;
|
||||
border: 0;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
.icon-container {
|
||||
margin-right: 2px;
|
||||
|
||||
}
|
||||
|
||||
.value-container {
|
||||
|
||||
@@ -78,7 +78,11 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
COLLECTION_FILTERS = {
|
||||
active: ->(resources) { resources.where(status: :active) },
|
||||
suspended: ->(resources) { resources.where(status: :suspended) },
|
||||
recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) }
|
||||
}.freeze
|
||||
|
||||
# Overwrite this method to customize how accounts are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
|
||||
@@ -94,7 +94,12 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
# COLLECTION_FILTERS = {
|
||||
# open: ->(resources) { resources.where(open: true) }
|
||||
# }.freeze
|
||||
COLLECTION_FILTERS = {}.freeze
|
||||
COLLECTION_FILTERS = {
|
||||
super_admin: ->(resources) { resources.where(type: 'SuperAdmin') },
|
||||
confirmed: ->(resources) { resources.where.not(confirmed_at: nil) },
|
||||
unconfirmed: ->(resources) { resources.where(confirmed_at: nil) },
|
||||
recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) }
|
||||
}.freeze
|
||||
|
||||
# Overwrite this method to customize how users are displayed
|
||||
# across all pages of the admin dashboard.
|
||||
|
||||
@@ -4,7 +4,6 @@ import AddAccountModal from '../dashboard/components/layout/sidebarComponents/Ad
|
||||
import LoadingState from './components/widgets/LoadingState.vue';
|
||||
import NetworkNotification from './components/NetworkNotification.vue';
|
||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||
import UpgradeBanner from './components/app/UpgradeBanner.vue';
|
||||
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
|
||||
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
@@ -31,7 +30,6 @@ export default {
|
||||
UpdateBanner,
|
||||
PaymentPendingBanner,
|
||||
WootSnackbarBox,
|
||||
UpgradeBanner,
|
||||
PendingEmailVerificationBanner,
|
||||
},
|
||||
setup() {
|
||||
@@ -146,7 +144,6 @@ export default {
|
||||
<template v-if="currentAccountId">
|
||||
<PendingEmailVerificationBanner v-if="hideOnOnboardingView" />
|
||||
<PaymentPendingBanner v-if="hideOnOnboardingView" />
|
||||
<UpgradeBanner />
|
||||
</template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const ALWAYS_ON_ROUTES = [
|
||||
'agent_list',
|
||||
'settings_inbox_list',
|
||||
'billing_settings_index',
|
||||
];
|
||||
|
||||
const { accountId } = useAccount();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const routeName = computed(() => {
|
||||
return route.name || '';
|
||||
});
|
||||
const isOnChatwootCloud = computed(
|
||||
() => store.getters['globalConfig/isOnChatwootCloud']
|
||||
);
|
||||
const account = computed(() =>
|
||||
store.getters['accounts/getAccount'](accountId.value)
|
||||
);
|
||||
|
||||
const isTrialAccount = computed(() => {
|
||||
if (!account.value) return false;
|
||||
|
||||
const createdAt = new Date(account.value.created_at);
|
||||
const diffDays = differenceInDays(new Date(), createdAt);
|
||||
|
||||
return diffDays <= 15;
|
||||
});
|
||||
|
||||
const testLimit = ({ allowed, consumed }) => {
|
||||
return consumed > allowed;
|
||||
};
|
||||
|
||||
const isLimitExceeded = computed(() => {
|
||||
if (ALWAYS_ON_ROUTES.includes(routeName.value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!account.value) return false;
|
||||
|
||||
const { limits } = account.value;
|
||||
if (!limits) return false;
|
||||
|
||||
const { conversation, non_web_inboxes: nonWebInboxes } = limits;
|
||||
return testLimit(conversation) || testLimit(nonWebInboxes);
|
||||
});
|
||||
|
||||
const shouldShowBanner = computed(() => {
|
||||
if (!isOnChatwootCloud.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isTrialAccount.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isLimitExceeded.value;
|
||||
});
|
||||
|
||||
const fetchLimits = () => store.dispatch('accounts/limits');
|
||||
|
||||
onMounted(() => {
|
||||
if (isOnChatwootCloud.value) {
|
||||
fetchLimits();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="shouldShowBanner">Limits exceeded</div>
|
||||
|
||||
<slot v-else />
|
||||
</template>
|
||||
@@ -9,7 +9,7 @@ import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAc
|
||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
|
||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
|
||||
import PaymentPaywall from 'dashboard/components/app/PaymentPaywall.vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
CommandBar,
|
||||
WootKeyShortcutModal,
|
||||
AddAccountModal,
|
||||
PaymentPaywall,
|
||||
AccountSelector,
|
||||
AddLabelModal,
|
||||
NotificationPanel,
|
||||
@@ -194,7 +195,9 @@ export default {
|
||||
@show-add-label-popup="showAddLabelPopup"
|
||||
/>
|
||||
<main class="flex flex-1 h-full min-h-0 px-0 overflow-hidden">
|
||||
<router-view />
|
||||
<PaymentPaywall>
|
||||
<router-view />
|
||||
</PaymentPaywall>
|
||||
<CommandBar />
|
||||
<AccountSelector
|
||||
:show-account-modal="showAccountModal"
|
||||
|
||||
@@ -23,6 +23,7 @@ const state = {
|
||||
|
||||
export const getters = {
|
||||
getAccount: $state => id => {
|
||||
console.log('account', id);
|
||||
return findRecordById($state, id);
|
||||
},
|
||||
getUIFlags($state) {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
<% content_for(:title) do %>
|
||||
Configure Settings - <%= @config.titleize %>
|
||||
<% end %>
|
||||
|
||||
<header class="main-content__header" role="banner">
|
||||
<h1 class="main-content__page-title" id="page-title">
|
||||
<%= content_for(:title) %>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.eye-icon.eye-hide path {
|
||||
d: path('M3 3l18 18M10.5 10.677a2 2 0 002.823 2.823M7.362 7.561C5.68 8.74 4.279 10.42 3 12c1.889 2.991 5.282 6 9 6 1.55 0 3.043-.523 4.395-1.35M12 6c4.008 0 6.701 3.009 9 6a15.66 15.66 0 01-1.078 1.5');
|
||||
}
|
||||
</style>
|
||||
|
||||
<section class="main-content__body">
|
||||
<%= form_with url: super_admin_app_config_url(config: @config) , method: :post do |form| %>
|
||||
<% @allowed_configs.each do |key| %>
|
||||
@@ -15,18 +23,36 @@
|
||||
</div>
|
||||
<div class="-mt-2 field-unit__field ">
|
||||
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
|
||||
<%= form.select "app_config[#{key}]",
|
||||
[["True", true], ["False", false]],
|
||||
<%= form.select "app_config[#{key}]",
|
||||
[["True", true], ["False", false]],
|
||||
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
|
||||
class: "mt-2 border border-slate-100 p-1 rounded-md"
|
||||
class: "mt-2 border border-slate-100 p-1 rounded-md"
|
||||
%>
|
||||
<% elsif @installation_configs[key]&.dig('type') == 'code' %>
|
||||
<%= form.text_area "app_config[#{key}]",
|
||||
value: @app_config[key],
|
||||
<%= form.text_area "app_config[#{key}]",
|
||||
value: @app_config[key],
|
||||
rows: 12,
|
||||
wrap: 'off',
|
||||
class: "mt-2 border font-mono text-xs border-slate-100 p-1 rounded-md overflow-scroll"
|
||||
%>
|
||||
<% elsif @installation_configs[key]&.dig('type') == 'secret' %>
|
||||
<div class="relative">
|
||||
<%= form.password_field "app_config[#{key}]",
|
||||
id: "app_config_#{key}",
|
||||
value: @app_config[key],
|
||||
class: "mt-2 border border-slate-100 p-1.5 pr-8 rounded-md w-full"
|
||||
%>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute reset-base !bg-white top-1/2 !outline-0 !text-n-slate-11 -translate-y-1/2 right-2 p-1 hover:!bg-n-slate-5 rounded-sm toggle-password"
|
||||
data-target="app_config_<%= key %>"
|
||||
>
|
||||
<svg class="eye-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 5C5.63636 5 2 12 2 12C2 12 5.63636 19 12 19C18.3636 19 22 12 22 12C22 12 18.3636 5 12 5Z"/>
|
||||
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
|
||||
<% end %>
|
||||
@@ -43,3 +69,26 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<% content_for :javascript do %>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.toggle-password').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const targetId = button.dataset.target;
|
||||
const input = document.getElementById(targetId);
|
||||
const type = input.type === 'password' ? 'text' : 'password';
|
||||
input.type = type;
|
||||
|
||||
// Toggle icon
|
||||
const svg = button.querySelector('.eye-icon');
|
||||
if (type === 'password') {
|
||||
svg.classList.remove('eye-hide')
|
||||
} else {
|
||||
svg.classList.add('eye-hide')
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<%#
|
||||
# Filters
|
||||
|
||||
This partial is used on the `index` page to display available filters
|
||||
for a collection of resources.
|
||||
|
||||
## Local variables:
|
||||
|
||||
- `page`:
|
||||
An instance of [Administrate::Page::Collection][1].
|
||||
Contains helper methods to help display a table,
|
||||
and knows which attributes should be displayed in the resource's table.
|
||||
|
||||
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
|
||||
%>
|
||||
|
||||
<%
|
||||
# Get the dashboard class name from the resource name
|
||||
resource_name = page.resource_name.classify
|
||||
dashboard_class_name = "#{resource_name}Dashboard"
|
||||
dashboard_class = dashboard_class_name.constantize
|
||||
|
||||
# Get the current filter if any
|
||||
current_filter = nil
|
||||
if params[:search] && params[:search].include?(':')
|
||||
current_filter = params[:search].split(':').first
|
||||
end
|
||||
%>
|
||||
|
||||
<% if dashboard_class.const_defined?(:COLLECTION_FILTERS) && !dashboard_class::COLLECTION_FILTERS.empty? %>
|
||||
<div class="flex items-center bg-gray-100 border-0 rounded-md shadow-none relative w-[260px]">
|
||||
<div class="flex items-center h-10 px-2 w-full">
|
||||
<div class="flex items-center justify-center flex-shrink-0 mr-2 text-gray-500" title="Filter by">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 h-full min-w-0 relative">
|
||||
<select id="filter-select" class="appearance-none bg-gray-100 border-0 text-gray-700 cursor-pointer text-sm h-full overflow-hidden truncate whitespace-nowrap w-full pr-7 pl-0 py-2 focus:outline-none bg-[url('data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2712%27 height=%2712%27 viewBox=%270 0 12 12%27%3E%3Cpath fill=%27%23293f54%27 d=%27M6 9L1 4h10z%27/%3E%3C/svg%3E')] bg-[right_0.25rem_center] bg-no-repeat bg-[length:0.75rem]" onchange="applyFilter(this.value)">
|
||||
<option value="">All records</option>
|
||||
<% dashboard_class::COLLECTION_FILTERS.each do |filter_name, _| %>
|
||||
<option value="<%= filter_name %>" <%= 'selected' if filter_name.to_s == current_filter %>>
|
||||
<%= filter_name.to_s.titleize %>
|
||||
</option>
|
||||
<% end %>
|
||||
</select>
|
||||
<% if current_filter %>
|
||||
<a href="?" class="flex items-center justify-center rounded-full text-gray-500 text-xl font-bold h-[18px] w-[18px] leading-none absolute right-5 top-1/2 -translate-y-1/2 no-underline z-2 hover:text-gray-900" title="Clear filter">×</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyFilter(filterName) {
|
||||
if (filterName) {
|
||||
window.location.href = "?search=" + encodeURIComponent(filterName) + "%3A";
|
||||
} else {
|
||||
window.location.href = "?";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<% end %>
|
||||
@@ -0,0 +1,24 @@
|
||||
<form class="search" role="search">
|
||||
<label class="search__label" for="search">
|
||||
<svg class="search__eyeglass-icon" role="img">
|
||||
<title>
|
||||
<%= t("administrate.search.label", resource: resource_name) %>
|
||||
</title>
|
||||
<use xlink:href="#icon-eyeglass" />
|
||||
</svg>
|
||||
</label>
|
||||
|
||||
<input class="search__input"
|
||||
id="search"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="<%= t("administrate.search.label", resource: resource_name) %>"
|
||||
value="<%= search_term %>">
|
||||
|
||||
<%= link_to clear_search_params, class: "search__clear-link" do %>
|
||||
<svg class="search__clear-icon" role="img">
|
||||
<title><%= t("administrate.search.clear") %></title>
|
||||
<use xlink:href="#icon-cancel" />
|
||||
</svg>
|
||||
<% end %>
|
||||
</form>
|
||||
@@ -28,27 +28,36 @@ It renders the `_table` partial to display details about the resources.
|
||||
<% end %>
|
||||
|
||||
<header class="main-content__header" role="banner">
|
||||
<h1 class="main-content__page-title" id="page-title">
|
||||
<%= content_for(:title) %>
|
||||
</h1>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<h1 class="main-content__page-title m-0 mr-6" id="page-title">
|
||||
<%= content_for(:title) %>
|
||||
</h1>
|
||||
|
||||
<% if show_search_bar %>
|
||||
<%= render(
|
||||
"search",
|
||||
search_term: search_term,
|
||||
resource_name: display_resource_name(page.resource_name)
|
||||
) %>
|
||||
<% end %>
|
||||
<div class="flex items-center">
|
||||
<% if show_search_bar %>
|
||||
<div class="flex items-center">
|
||||
<%= render("filters", page: page) %>
|
||||
<div class="ml-3">
|
||||
<%= render(
|
||||
"search",
|
||||
search_term: search_term,
|
||||
resource_name: display_resource_name(page.resource_name)
|
||||
) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= link_to(
|
||||
t(
|
||||
"administrate.actions.new_resource",
|
||||
name: page.resource_name.titleize.downcase
|
||||
),
|
||||
[:new, namespace, page.resource_path.to_sym],
|
||||
class: "button",
|
||||
) if accessible_action?(new_resource, :new) %>
|
||||
<div class="whitespace-nowrap ml-4">
|
||||
<%= link_to(
|
||||
t(
|
||||
"administrate.actions.new_resource",
|
||||
name: page.resource_name.titleize.downcase
|
||||
),
|
||||
[:new, namespace, page.resource_path.to_sym],
|
||||
class: "button",
|
||||
) if accessible_action?(new_resource, :new) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -103,13 +103,16 @@
|
||||
display_title: 'Facebook Verify Token'
|
||||
description: 'The verify token used for Facebook Messenger Webhook'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: FB_APP_SECRET
|
||||
display_title: 'Facebook App Secret'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: IG_VERIFY_TOKEN
|
||||
display_title: 'Instagram Verify Token'
|
||||
description: 'The verify token used for Instagram Webhook'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: FACEBOOK_API_VERSION
|
||||
display_title: 'Facebook API Version'
|
||||
description: 'Configure this if you want to use a different Facebook API version. Make sure its prefixed with `v`'
|
||||
@@ -131,6 +134,7 @@
|
||||
- name: AZURE_APP_SECRET
|
||||
display_title: 'Azure App Secret'
|
||||
locked: false
|
||||
type: secret
|
||||
# End of Microsoft Email Channel Config
|
||||
|
||||
# MARK: Captain Config
|
||||
@@ -138,6 +142,7 @@
|
||||
display_title: 'OpenAI API Key'
|
||||
description: 'The API key used to authenticate requests to OpenAI services for Captain AI.'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: CAPTAIN_OPEN_AI_MODEL
|
||||
display_title: 'OpenAI Model'
|
||||
description: 'The OpenAI model configured for use in Captain AI. Default: gpt-4o-mini'
|
||||
@@ -146,6 +151,7 @@
|
||||
display_title: 'FireCrawl API Key (optional)'
|
||||
description: 'The FireCrawl API key for the Captain AI service'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: CAPTAIN_CLOUD_PLAN_LIMITS
|
||||
display_title: 'Captain Cloud Plan Limits'
|
||||
description: 'The limits for the Captain AI service for different plans'
|
||||
@@ -160,11 +166,13 @@
|
||||
display_title: 'Inbox Token'
|
||||
description: 'The Chatwoot Inbox Token for Contact Support in Cloud'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: CHATWOOT_INBOX_HMAC_KEY
|
||||
value:
|
||||
display_title: 'Inbox HMAC Key'
|
||||
description: 'The Chatwoot Inbox HMAC Key for Contact Support in Cloud'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: CHATWOOT_CLOUD_PLANS
|
||||
display_title: 'Cloud Plans'
|
||||
value:
|
||||
@@ -180,10 +188,12 @@
|
||||
value:
|
||||
display_title: 'Analytics Token'
|
||||
description: 'The June.so analytics token for Chatwoot cloud'
|
||||
type: secret
|
||||
- name: CLEARBIT_API_KEY
|
||||
value:
|
||||
display_title: 'Clearbit API Key'
|
||||
description: 'This API key is used for onboarding the users, to pre-fill account data.'
|
||||
type: secret
|
||||
- name: DASHBOARD_SCRIPTS
|
||||
value:
|
||||
display_title: 'Dashboard Scripts'
|
||||
@@ -206,12 +216,14 @@
|
||||
- name: CHATWOOT_SUPPORT_WEBSITE_TOKEN
|
||||
value:
|
||||
description: 'The Chatwoot website token, used to identify the Chatwoot inbox and display the "Contact Support" option on the billing page'
|
||||
type: secret
|
||||
- name: CHATWOOT_SUPPORT_SCRIPT_URL
|
||||
value:
|
||||
description: 'The Chatwoot script base URL, to display the "Contact Support" option on the billing page'
|
||||
- name: CHATWOOT_SUPPORT_IDENTIFIER_HASH
|
||||
value:
|
||||
description: 'The Chatwoot identifier hash, to validate the contact in the live chat window.'
|
||||
type: secret
|
||||
- name: ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL
|
||||
display_title: Webhook URL to post security analysis
|
||||
value:
|
||||
@@ -245,6 +257,7 @@
|
||||
display_title: 'Firebase Credentials'
|
||||
value:
|
||||
locked: false
|
||||
type: secret
|
||||
description: 'Contents on your firebase credentials json file'
|
||||
## ------ End of Configs added for FCM v1 notifications ------ ##
|
||||
|
||||
@@ -259,4 +272,5 @@
|
||||
value:
|
||||
locked: false
|
||||
description: 'Linear client secret'
|
||||
type: secret
|
||||
## ------ End of Configs added for Linear ------ ##
|
||||
|
||||
Reference in New Issue
Block a user