Merge branch 'develop' into feat/github-integration

This commit is contained in:
Muhsin Keloth
2025-08-12 14:48:17 +05:30
committed by GitHub
79 changed files with 417 additions and 113 deletions

View File

@@ -1,4 +1,11 @@
class Platform::Api::V1::AccountsController < PlatformController
def index
@resources = @platform_app.platform_app_permissibles
.where(permissible_type: 'Account')
.includes(:permissible)
.map(&:permissible)
end
def show; end
def create

View File

@@ -137,7 +137,6 @@ export default {
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
id="app"
class="flex flex-col w-full h-screen min-h-0"
:class="{ 'app-rtl--wrapper': isRTL }"
:dir="isRTL ? 'rtl' : 'ltr'"
>
<UpdateBanner :latest-chatwoot-version="latestChatwootVersion" />

View File

@@ -315,7 +315,9 @@ const handleAvatarDelete = () => {
class="[&>div>button:not(.focused)]:!outline-n-weak"
/>
</div>
<div class="flex items-start justify-between w-full gap-2">
<div
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
>
<label
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
>

View File

@@ -170,7 +170,7 @@ useKeyboardEvents(keyboardEvents);
/>
<EmojiInput
v-if="isEmojiPickerOpen"
class="left-0 top-full mt-1.5"
class="ltr:left-0 rtl:right-0 top-full mt-1.5"
:on-click="onClickInsertEmoji"
/>
</div>

View File

@@ -57,7 +57,7 @@ const removeAttachment = id => {
variant="ghost"
icon="i-lucide-trash"
color="slate"
class="absolute top-1 right-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100"
class="absolute top-1 ltr:right-1 rtl:left-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100"
@click="removeAttachment(attachment.resource.id)"
/>
</div>

View File

@@ -83,7 +83,7 @@ const targetInboxLabel = computed(() => {
<DropdownMenu
v-if="contactableInboxesList?.length > 0 && showInboxesDropdown"
:menu-items="contactableInboxesList"
class="left-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
@action="emit('handleInboxAction', $event)"
/>
</div>

View File

@@ -2,6 +2,7 @@
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import WhatsappTemplateParser from './WhatsappTemplateParser.vue';
@@ -84,10 +85,13 @@ const handleSendMessage = template => {
/>
<div
v-if="showTemplatesMenu"
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto left-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
>
<div class="relative w-full">
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
<Icon
icon="i-lucide-search"
class="absolute size-3.5 top-2 ltr:left-3 rtl:right-3"
/>
<input
v-model="searchQuery"
type="search"
@@ -96,7 +100,7 @@ const handleSendMessage = template => {
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.SEARCH_PLACEHOLDER'
)
"
class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
class="w-full h-8 py-2 ltr:pl-10 rtl:pr-10 ltr:pr-2 rtl:pl-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
/>
</div>
<div

View File

@@ -106,7 +106,7 @@ onMounted(() => {
<template>
<div
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto left-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
>
<span class="text-sm text-n-slate-12">
{{
@@ -138,7 +138,8 @@ onMounted(() => {
class="flex items-center w-full gap-2"
>
<span
class="flex items-center h-8 text-sm min-w-6 ltr:text-left rtl:text-right text-n-slate-10"
class="block h-8 text-sm min-w-6 text-start truncate text-n-slate-10 leading-8"
:title="key"
>
{{ key }}
</span>

View File

@@ -207,7 +207,7 @@ watch(
<!-- Delete Avatar Button -->
<div
v-if="src && allowUpload"
class="absolute z-20 flex items-center justify-center invisible w-6 h-6 transition-all duration-300 ease-in-out opacity-0 cursor-pointer outline outline-1 outline-n-container -top-2 -right-2 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
class="absolute z-20 flex items-center justify-center invisible w-6 h-6 transition-all duration-300 ease-in-out opacity-0 cursor-pointer outline outline-1 outline-n-container -top-2 ltr:-right-2 rtl:-left-2 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
@click="handleDismiss"
>
<Icon icon="i-lucide-x" class="text-n-slate-11 size-4" />

View File

@@ -303,26 +303,17 @@ watch(
<input
v-model="state.features.conversationFaqs"
type="checkbox"
class="form-checkbox"
/>
{{
t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS')
}}
</label>
<label class="flex items-center gap-2">
<input
v-model="state.features.memories"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.memories" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
</label>
<label class="flex items-center gap-2">
<input
v-model="state.features.citations"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.citations" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
</label>
</div>

View File

@@ -126,27 +126,15 @@ watch(
</label>
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2">
<input
v-model="state.features.conversationFaqs"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.conversationFaqs" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS') }}
</label>
<label class="flex items-center gap-2">
<input
v-model="state.features.memories"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.memories" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
</label>
<label class="flex items-center gap-2">
<input
v-model="state.features.citations"
type="checkbox"
class="form-checkbox"
/>
<input v-model="state.features.citations" type="checkbox" />
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CITATIONS') }}
</label>
</div>

View File

@@ -218,8 +218,8 @@ onMounted(async () => {
left: 0;
}
.app-rtl--wrapper .sidebar-group-children > .child-item:last-child::after,
.app-rtl--wrapper
#app[dir='rtl'] .sidebar-group-children > .child-item:last-child::after,
#app[dir='rtl']
.sidebar-group-children
> *:last-child
> *:last-child

View File

@@ -230,7 +230,7 @@ const handleBlur = e => emit('blur', e);
v-if="showDropdownMenu"
:menu-items="filteredMenuItems"
:is-searching="isLoading"
class="left-0 z-[100] top-8 overflow-y-auto max-h-60 w-[inherit] max-w-md dark:!outline-n-slate-5"
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-60 w-[inherit] max-w-md dark:!outline-n-slate-5"
@action="handleDropdownAction"
/>
</div>

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "رابط الصفحة الرئيسية للبوابة",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Enllaç a la pàgina d'inici del portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link til portalens hjemmeside",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Snegl",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link zur Startseite des Portals",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Σύνδεσμος αρχικής σελίδας πύλης",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Enlace de página de inicio del portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "پیوند صفحه اصلی پورتال",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Lien vers la page d'accueil du portail",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -2,13 +2,13 @@
"AGENT_BOTS": {
"HEADER": "בוטים",
"LOADING_EDITOR": "Loading editor...",
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
"DESCRIPTION": "בוטים של סוכנים הם כמו החברים הכי נפלאים בצוות שלכם. הם יכולים להתמודד עם הדברים הקטנים, כך שאתם יכולים להתמקד בדברים החשובים. נסו אותם. אתם יכולים לנהל את הבוטים שלכם מדף זה או ליצור חדשים באמצעות כפתור 'הוסף בוט'.",
"LEARN_MORE": "Learn about agent bots",
"GLOBAL_BOT": "System bot",
"GLOBAL_BOT": "בוט מערכת",
"GLOBAL_BOT_BADGE": "System",
"AVATAR": {
"SUCCESS_DELETE": "Bot avatar deleted successfully",
"ERROR_DELETE": "Error deleting bot avatar, please try again"
"SUCCESS_DELETE": "בוט אווטר נמחק בהצלחה",
"ERROR_DELETE": "תקלה במחיקת בוט אווטר, יש לנסות שוב"
},
"BOT_CONFIGURATION": {
"TITLE": "בחר סוכן בוט",
@@ -22,7 +22,7 @@
"SELECT_PLACEHOLDER": "Select bot"
},
"ADD": {
"TITLE": "Add Bot",
"TITLE": "הוסף בוט",
"CANCEL_BUTTON_TEXT": "ביטול",
"API": {
"SUCCESS_MESSAGE": "הבוט התווסף בהצלחה.",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "קישור לדף הבית של הפורטל",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "שבלול",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portál főoldal link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Tautan halaman utama portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link della pagina iniziale del portale",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "ホームページリンク",
"PLACEHOLDER": "ポータルホームページリンク",
"ERROR": "無効なURLです。ホームページリンクは「http://」または「https://」で始まる必要があります。"
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "スラッグ",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portalo nuoroda į pagrindinį puslapį",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Mājas lapas saite",
"PLACEHOLDER": "Portāla mājaslapas saite",
"ERROR": "Nederīgs URL. Sākumlapas saitei jāsākas ar “http://” vai “https://."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link do strony głównej portalu",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link da página inicial do portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Link da Página Inicial",
"PLACEHOLDER": "Link da página inicial do portal",
"ERROR": "URL inválida. O link da página inicial deve começar com 'http://' ou 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Link pagină portal",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Ссылка на домашнюю страницу",
"PLACEHOLDER": "Ссылка на главную страницу портала",
"ERROR": "Неправильный URL-адрес. Ссылка на главную страницу должна начинаться с 'http://' или 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Метка",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Veza do početne stranice portala",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal ana sayfa bağlantısı",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Посилання на головну сторінку порталу",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Мітка",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Liên kế trang chủ cổng thông tin",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "主页链接",
"PLACEHOLDER": "门户主页链接",
"ERROR": "无效的URL。主页链接必须以 'http://' 'https://' 开头。"
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -732,7 +732,7 @@
"HOME_PAGE_LINK": {
"LABEL": "Home page link",
"PLACEHOLDER": "Portal home page link",
"ERROR": "Invalid URL. The Home page link must start with 'http://' or 'https://'."
"ERROR": "Enter a valid URL. The Home page link must start with 'http://' or 'https://'."
},
"SLUG": {
"LABEL": "Slug",

View File

@@ -2,24 +2,26 @@
#
# Table name: account_users
#
# id :bigint not null, primary key
# active_at :datetime
# auto_offline :boolean default(TRUE), not null
# availability :integer default("online"), not null
# role :integer default("agent")
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint
# custom_role_id :bigint
# inviter_id :bigint
# user_id :bigint
# id :bigint not null, primary key
# active_at :datetime
# auto_offline :boolean default(TRUE), not null
# availability :integer default("online"), not null
# role :integer default("agent")
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint
# agent_capacity_policy_id :bigint
# custom_role_id :bigint
# inviter_id :bigint
# user_id :bigint
#
# Indexes
#
# index_account_users_on_account_id (account_id)
# index_account_users_on_custom_role_id (custom_role_id)
# index_account_users_on_user_id (user_id)
# uniq_user_id_per_account_id (account_id,user_id) UNIQUE
# index_account_users_on_account_id (account_id)
# index_account_users_on_agent_capacity_policy_id (agent_capacity_policy_id)
# index_account_users_on_custom_role_id (custom_role_id)
# index_account_users_on_user_id (user_id)
# uniq_user_id_per_account_id (account_id,user_id) UNIQUE
#
class AccountUser < ApplicationRecord

View File

@@ -92,6 +92,9 @@ class Whatsapp::IncomingMessageBaseService
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
# Update existing contact name if ProfileName is available and current name is just phone number
update_contact_with_profile_name(contact_params)
end
def set_conversation
@@ -171,4 +174,21 @@ class Whatsapp::IncomingMessageBaseService
)
end
end
def update_contact_with_profile_name(contact_params)
profile_name = contact_params.dig(:profile, :name)
return if profile_name.blank?
return if @contact.name == profile_name
# Only update if current name exactly matches the phone number or formatted phone number
return unless contact_name_matches_phone_number?
@contact.update!(name: profile_name)
end
def contact_name_matches_phone_number?
phone_number = "+#{@processed_params[:messages].first[:from]}"
formatted_phone_number = TelephoneNumber.parse(phone_number).international_number
@contact.name == phone_number || @contact.name == formatted_phone_number
end
end

View File

@@ -0,0 +1,3 @@
json.array! @resources do |account|
json.partial! 'platform/api/v1/models/account', formats: [:json], resource: account
end

View File

@@ -433,7 +433,7 @@ Rails.application.routes.draw do
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
delete :avatar, on: :member
end
resources :accounts, only: [:create, :show, :update, :destroy] do
resources :accounts, only: [:index, :create, :show, :update, :destroy] do
resources :account_users, only: [:index, :create] do
collection do
delete :destroy

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
class CreateAssignmentPolicies < ActiveRecord::Migration[7.1]
def change
create_table :assignment_policies do |t|
t.references :account, null: false, index: true
t.string :name, null: false, limit: 255
t.text :description
t.integer :assignment_order, null: false, default: 0 # 0: round_robin, 1: balanced
t.integer :conversation_priority, null: false, default: 0 # 0: earliest_created, 1: longest_waiting
t.integer :fair_distribution_limit, null: false, default: 100
t.integer :fair_distribution_window, null: false, default: 3600 # seconds
t.boolean :enabled, null: false, default: true
t.timestamps
end
add_index :assignment_policies, [:account_id, :name], unique: true
add_index :assignment_policies, :enabled
end
end

View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateInboxAssignmentPolicies < ActiveRecord::Migration[7.1]
def change
create_table :inbox_assignment_policies do |t|
t.references :inbox, null: false, index: { unique: true }
t.references :assignment_policy, null: false, index: true
t.timestamps
end
end
end

View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
class CreateAgentCapacityPolicies < ActiveRecord::Migration[7.1]
def change
create_table :agent_capacity_policies do |t|
t.references :account, null: false, index: true
t.string :name, null: false, limit: 255
t.text :description
t.jsonb :exclusion_rules, default: {}, null: false
t.timestamps
end
end
end

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateInboxCapacityLimits < ActiveRecord::Migration[7.1]
def change
create_table :inbox_capacity_limits do |t|
t.references :agent_capacity_policy, null: false, index: true
t.references :inbox, null: false, index: true
t.integer :conversation_limit, null: false
t.timestamps
end
add_index :inbox_capacity_limits, [:agent_capacity_policy_id, :inbox_id], unique: true
end
end

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddAgentCapacityPolicyToAccountUsers < ActiveRecord::Migration[7.1]
def change
add_reference :account_users, :agent_capacity_policy, null: true, index: true
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
class CreateLeaves < ActiveRecord::Migration[7.1]
def change
create_table :leaves do |t|
t.references :account, null: false
t.references :user, null: false
t.date :start_date, null: false
t.date :end_date, null: false
t.integer :leave_type, null: false, default: 0
t.integer :status, null: false, default: 0
t.text :reason
t.references :approved_by
t.datetime :approved_at
t.timestamps
end
add_index :leaves, [:account_id, :status]
end
end

View File

@@ -39,8 +39,10 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.integer "availability", default: 0, null: false
t.boolean "auto_offline", default: true, null: false
t.bigint "custom_role_id"
t.bigint "agent_capacity_policy_id"
t.index ["account_id", "user_id"], name: "uniq_user_id_per_account_id", unique: true
t.index ["account_id"], name: "index_account_users_on_account_id"
t.index ["agent_capacity_policy_id"], name: "index_account_users_on_agent_capacity_policy_id"
t.index ["custom_role_id"], name: "index_account_users_on_custom_role_id"
t.index ["user_id"], name: "index_account_users_on_user_id"
end
@@ -120,6 +122,16 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.index ["account_id"], name: "index_agent_bots_on_account_id"
end
create_table "agent_capacity_policies", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "name", limit: 255, null: false
t.text "description"
t.jsonb "exclusion_rules", default: {}, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_agent_capacity_policies_on_account_id"
end
create_table "applied_slas", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "sla_policy_id", null: false
@@ -169,6 +181,22 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.index ["views"], name: "index_articles_on_views"
end
create_table "assignment_policies", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "name", limit: 255, null: false
t.text "description"
t.integer "assignment_order", default: 0, null: false
t.integer "conversation_priority", default: 0, null: false
t.integer "fair_distribution_limit", default: 100, null: false
t.integer "fair_distribution_window", default: 3600, null: false
t.boolean "enabled", default: true, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "name"], name: "index_assignment_policies_on_account_id_and_name", unique: true
t.index ["account_id"], name: "index_assignment_policies_on_account_id"
t.index ["enabled"], name: "index_assignment_policies_on_enabled"
end
create_table "attachments", id: :serial, force: :cascade do |t|
t.integer "file_type", default: 0
t.string "external_url"
@@ -728,6 +756,26 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.datetime "updated_at", null: false
end
create_table "inbox_assignment_policies", force: :cascade do |t|
t.bigint "inbox_id", null: false
t.bigint "assignment_policy_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["assignment_policy_id"], name: "index_inbox_assignment_policies_on_assignment_policy_id"
t.index ["inbox_id"], name: "index_inbox_assignment_policies_on_inbox_id", unique: true
end
create_table "inbox_capacity_limits", force: :cascade do |t|
t.bigint "agent_capacity_policy_id", null: false
t.bigint "inbox_id", null: false
t.integer "conversation_limit", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["agent_capacity_policy_id", "inbox_id"], name: "idx_on_agent_capacity_policy_id_inbox_id_71c7ec4caf", unique: true
t.index ["agent_capacity_policy_id"], name: "index_inbox_capacity_limits_on_agent_capacity_policy_id"
t.index ["inbox_id"], name: "index_inbox_capacity_limits_on_inbox_id"
end
create_table "inbox_members", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "inbox_id", null: false
@@ -800,6 +848,24 @@ ActiveRecord::Schema[7.1].define(version: 2025_08_08_123008) do
t.index ["title", "account_id"], name: "index_labels_on_title_and_account_id", unique: true
end
create_table "leaves", force: :cascade do |t|
t.bigint "account_id", null: false
t.bigint "user_id", null: false
t.date "start_date", null: false
t.date "end_date", null: false
t.integer "leave_type", default: 0, null: false
t.integer "status", default: 0, null: false
t.text "reason"
t.bigint "approved_by_id"
t.datetime "approved_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "status"], name: "index_leaves_on_account_id_and_status"
t.index ["account_id"], name: "index_leaves_on_account_id"
t.index ["approved_by_id"], name: "index_leaves_on_approved_by_id"
t.index ["user_id"], name: "index_leaves_on_user_id"
end
create_table "macros", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "name", null: false

View File

@@ -78,6 +78,42 @@ RSpec.describe 'Platform Accounts API', type: :request do
end
end
describe 'GET /platform/api/v1/accounts' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
get '/platform/api/v1/accounts'
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
get '/platform/api/v1/accounts', headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
let!(:account1) { create(:account, name: 'Account A') }
let!(:account2) { create(:account, name: 'Account B') }
before do
create(:platform_app_permissible, platform_app: platform_app, permissible: account1)
create(:platform_app_permissible, platform_app: platform_app, permissible: account2)
end
it 'returns all permissible accounts' do
get '/platform/api/v1/accounts', headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response.size).to eq(2)
expect(json_response.map { |acc| acc['name'] }).to include('Account A', 'Account B')
end
end
end
describe 'GET /platform/api/v1/accounts/{account_id}' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do

View File

@@ -371,5 +371,100 @@ describe Whatsapp::IncomingMessageService do
Redis::Alfred.delete(key)
end
end
context 'when profile name is available for contact updates' do
let(:wa_id) { '1234567890' }
let(:phone_number) { "+#{wa_id}" }
it 'updates existing contact name when current name matches phone number' do
# Create contact with phone number as name
existing_contact = create(:contact,
account: whatsapp_channel.inbox.account,
name: phone_number,
phone_number: phone_number)
create(:contact_inbox,
contact: existing_contact,
inbox: whatsapp_channel.inbox,
source_id: wa_id)
params = {
'contacts' => [{ 'profile' => { 'name' => 'Jane Smith' }, 'wa_id' => wa_id }],
'messages' => [{ 'from' => wa_id, 'id' => 'message123', 'text' => { 'body' => 'Hello' },
'timestamp' => '1633034394', 'type' => 'text' }]
}.with_indifferent_access
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
existing_contact.reload
expect(existing_contact.name).to eq('Jane Smith')
end
it 'does not update contact name when current name is different from phone number' do
# Create contact with human name
existing_contact = create(:contact,
account: whatsapp_channel.inbox.account,
name: 'John Doe',
phone_number: phone_number)
create(:contact_inbox,
contact: existing_contact,
inbox: whatsapp_channel.inbox,
source_id: wa_id)
params = {
'contacts' => [{ 'profile' => { 'name' => 'Jane Smith' }, 'wa_id' => wa_id }],
'messages' => [{ 'from' => wa_id, 'id' => 'message123', 'text' => { 'body' => 'Hello' },
'timestamp' => '1633034394', 'type' => 'text' }]
}.with_indifferent_access
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
existing_contact.reload
expect(existing_contact.name).to eq('John Doe') # Should not change
end
it 'updates contact name when current name matches formatted phone number' do
formatted_number = TelephoneNumber.parse(phone_number).international_number
# Create contact with formatted phone number as name
existing_contact = create(:contact,
account: whatsapp_channel.inbox.account,
name: formatted_number,
phone_number: phone_number)
create(:contact_inbox,
contact: existing_contact,
inbox: whatsapp_channel.inbox,
source_id: wa_id)
params = {
'contacts' => [{ 'profile' => { 'name' => 'Alice Johnson' }, 'wa_id' => wa_id }],
'messages' => [{ 'from' => wa_id, 'id' => 'message123', 'text' => { 'body' => 'Hello' },
'timestamp' => '1633034394', 'type' => 'text' }]
}.with_indifferent_access
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
existing_contact.reload
expect(existing_contact.name).to eq('Alice Johnson')
end
it 'does not update when profile name is blank' do
# Create contact with phone number as name
existing_contact = create(:contact,
account: whatsapp_channel.inbox.account,
name: phone_number,
phone_number: phone_number)
create(:contact_inbox,
contact: existing_contact,
inbox: whatsapp_channel.inbox,
source_id: wa_id)
params = {
'contacts' => [{ 'profile' => { 'name' => '' }, 'wa_id' => wa_id }],
'messages' => [{ 'from' => wa_id, 'id' => 'message123', 'text' => { 'body' => 'Hello' },
'timestamp' => '1633034394', 'type' => 'text' }]
}.with_indifferent_access
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
existing_contact.reload
expect(existing_contact.name).to eq(phone_number) # Should not change
end
end
end
end