@fayz-ai/plugin-conversations 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/{ConversationsContext.d.ts → context.d.ts} +2 -2
- package/dist/context.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/{channel.d.ts → lib/channel.d.ts} +1 -1
- package/dist/lib/channel.d.ts.map +1 -0
- package/dist/{ConversationsPage.d.ts → views/ConversationsPage.d.ts} +2 -2
- package/dist/views/ConversationsPage.d.ts.map +1 -0
- package/package.json +5 -7
- package/dist/ConversationsContext.d.ts.map +0 -1
- package/dist/ConversationsPage.d.ts.map +0 -1
- package/dist/channel.d.ts.map +0 -1
- package/src/ConversationsContext.tsx +0 -51
- package/src/ConversationsPage.tsx +0 -21
- package/src/channel.ts +0 -34
- package/src/data/accents.ts +0 -12
- package/src/data/mock.test.ts +0 -90
- package/src/data/mock.ts +0 -261
- package/src/data/supabase.ts +0 -264
- package/src/data/tables.ts +0 -7
- package/src/data/types.ts +0 -17
- package/src/index.ts +0 -190
- package/src/locales/en.ts +0 -68
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -68
- package/src/migrations/001_conversations.sql +0 -74
- package/src/migrations/002_contact_person.sql +0 -22
- package/src/migrations/index.ts +0 -108
- package/src/store.ts +0 -136
- package/src/types.ts +0 -77
- package/src/views/ContactPanel.tsx +0 -97
- package/src/views/ConversationList.tsx +0 -135
- package/src/views/InboxView.tsx +0 -64
- package/src/views/MessageThread.tsx +0 -167
- package/src/views/NewConversationModal.tsx +0 -204
- package/src/views/shared.tsx +0 -97
package/src/data/supabase.ts
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { getSupabaseClientOptional } from '@fayz-ai/core'
|
|
2
|
-
import type { ConversationsProvider } from './types'
|
|
3
|
-
import { T } from './tables'
|
|
4
|
-
import { CHANNEL_ACCENT_HEX } from './accents'
|
|
5
|
-
import type {
|
|
6
|
-
Conversation,
|
|
7
|
-
Message,
|
|
8
|
-
ListConversationsQuery,
|
|
9
|
-
SendMessageInput,
|
|
10
|
-
CreateConversationInput,
|
|
11
|
-
ConversationStatus,
|
|
12
|
-
} from '../types'
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Supabase-backed conversations provider — reads/writes the project's
|
|
16
|
-
// `conversations` + `conversation_messages` tables (tenant-scoped via RLS +
|
|
17
|
-
// an explicit tenant filter from the active-org context). Mirrors the inline
|
|
18
|
-
// client-casting style of @fayz-ai/core's createSupabaseProvider; the global
|
|
19
|
-
// client is `unknown`-typed so each call narrows the shape it needs.
|
|
20
|
-
//
|
|
21
|
-
// Real channel connectors (Twilio, WhatsApp Cloud, Meta, IMAP) deliver inbound
|
|
22
|
-
// rows into these tables out-of-band; this provider is the read/compose surface.
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
export interface SupabaseConversationsConfig {
|
|
26
|
-
/** Supabase client; defaults to the global one registered by createFayzApp. */
|
|
27
|
-
supabaseClient?: unknown
|
|
28
|
-
/** Active tenant id (value or getter) used to scope reads and stamp writes. */
|
|
29
|
-
tenantId?: string | (() => string | undefined)
|
|
30
|
-
/** Display name stamped as the author of outbound messages. */
|
|
31
|
-
selfAuthor?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type Row = Record<string, unknown>
|
|
35
|
-
|
|
36
|
-
function mapConversation(r: Row): Conversation {
|
|
37
|
-
return {
|
|
38
|
-
id: String(r.id),
|
|
39
|
-
contactName: (r.contact_name as string) ?? '',
|
|
40
|
-
contactPersonId: (r.contact_person_id as string | null) ?? undefined,
|
|
41
|
-
contactHandle: (r.contact_handle as string) ?? '',
|
|
42
|
-
channel: (r.channel as Conversation['channel']) ?? 'sms',
|
|
43
|
-
lastMessagePreview: (r.last_message_preview as string) ?? '',
|
|
44
|
-
lastMessageAt: (r.last_message_at as string) ?? '',
|
|
45
|
-
unreadCount: Number(r.unread_count ?? 0),
|
|
46
|
-
status: (r.status as ConversationStatus) ?? 'open',
|
|
47
|
-
assignedTo: (r.assigned_to as string | null) ?? undefined,
|
|
48
|
-
accent: (r.accent as string) ?? '#6366f1',
|
|
49
|
-
tags: (r.tags as string[] | null) ?? [],
|
|
50
|
-
location: (r.location as string | null) ?? undefined,
|
|
51
|
-
note: (r.note as string | null) ?? undefined,
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function mapMessage(r: Row): Message {
|
|
56
|
-
return {
|
|
57
|
-
id: String(r.id),
|
|
58
|
-
conversationId: String(r.conversation_id),
|
|
59
|
-
channel: (r.channel as Message['channel']) ?? 'sms',
|
|
60
|
-
direction: (r.direction as Message['direction']) ?? 'inbound',
|
|
61
|
-
body: (r.body as string) ?? '',
|
|
62
|
-
at: (r.at as string) ?? '',
|
|
63
|
-
author: (r.author as string) ?? '',
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function createSupabaseConversationsProvider(
|
|
68
|
-
config?: SupabaseConversationsConfig,
|
|
69
|
-
): ConversationsProvider {
|
|
70
|
-
const selfAuthor = config?.selfAuthor ?? 'You'
|
|
71
|
-
|
|
72
|
-
function resolveTenantId(): string | undefined {
|
|
73
|
-
if (!config?.tenantId) return undefined
|
|
74
|
-
return typeof config.tenantId === 'function' ? config.tenantId() : config.tenantId
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function client(): { from: (t: string) => Row } {
|
|
78
|
-
const supabase = (config?.supabaseClient ?? getSupabaseClientOptional()) as
|
|
79
|
-
| { from: (t: string) => Row }
|
|
80
|
-
| null
|
|
81
|
-
if (!supabase) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
'[plugin-conversations] Supabase client not available. Pass supabaseClient or register the global client via createFayzApp.',
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
return supabase
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
async listConversations(query?: ListConversationsQuery): Promise<Conversation[]> {
|
|
91
|
-
let q = (client().from(T.conversations) as { select: (s: string) => Row }).select('*')
|
|
92
|
-
|
|
93
|
-
const tenantId = resolveTenantId()
|
|
94
|
-
if (tenantId) q = (q as { eq: (c: string, v: string) => Row }).eq('tenant_id', tenantId)
|
|
95
|
-
if (query?.channel && query.channel !== 'all') {
|
|
96
|
-
q = (q as { eq: (c: string, v: string) => Row }).eq('channel', query.channel)
|
|
97
|
-
}
|
|
98
|
-
if (query?.status && query.status !== 'all') {
|
|
99
|
-
q = (q as { eq: (c: string, v: string) => Row }).eq('status', query.status)
|
|
100
|
-
}
|
|
101
|
-
if (query?.search) {
|
|
102
|
-
const term = `%${query.search}%`
|
|
103
|
-
q = (q as { or: (c: string) => Row }).or(
|
|
104
|
-
`contact_name.ilike.${term},last_message_preview.ilike.${term}`,
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
q = (q as { order: (c: string, o: unknown) => Row }).order('last_message_at', {
|
|
108
|
-
ascending: false,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
const { data, error } = (await q) as { data: Row[] | null; error: unknown }
|
|
112
|
-
if (error) throw error
|
|
113
|
-
return (data ?? []).map(mapConversation)
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
async getMessages(conversationId: string): Promise<Message[]> {
|
|
117
|
-
const selected = (
|
|
118
|
-
client().from(T.messages) as { select: (s: string) => Row }
|
|
119
|
-
).select('*')
|
|
120
|
-
const filtered = (selected as { eq: (c: string, v: string) => Row }).eq(
|
|
121
|
-
'conversation_id',
|
|
122
|
-
conversationId,
|
|
123
|
-
)
|
|
124
|
-
const ordered = (filtered as { order: (c: string, o: unknown) => Row }).order('at', {
|
|
125
|
-
ascending: true,
|
|
126
|
-
})
|
|
127
|
-
const { data, error } = (await ordered) as { data: Row[] | null; error: unknown }
|
|
128
|
-
if (error) throw error
|
|
129
|
-
return (data ?? []).map(mapMessage)
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
async createConversation(input: CreateConversationInput): Promise<Conversation> {
|
|
133
|
-
const tenantId = resolveTenantId()
|
|
134
|
-
// Insert with a null tenant_id fails the plg_conversations RLS WITH CHECK
|
|
135
|
-
// (tenant_id ∈ user_tenant_ids()) with an opaque Postgres error. When the
|
|
136
|
-
// active tenant hasn't resolved yet (bootstrap race), fail fast with an
|
|
137
|
-
// actionable message the compose modal can surface via toast.
|
|
138
|
-
if (!tenantId) {
|
|
139
|
-
throw new Error('[plugin-conversations] Active tenant not resolved — cannot create conversation. Try again in a moment.')
|
|
140
|
-
}
|
|
141
|
-
const now = new Date().toISOString()
|
|
142
|
-
const firstMessage = input.firstMessage?.trim()
|
|
143
|
-
|
|
144
|
-
const convRow: Row = {
|
|
145
|
-
contact_name: input.contactName.trim(),
|
|
146
|
-
// Only written when the picker resolved a real person — an app that
|
|
147
|
-
// hasn't run migration 002 yet would reject an unknown column, and
|
|
148
|
-
// omitting it keeps the free-text path working there.
|
|
149
|
-
...(input.contactPersonId ? { contact_person_id: input.contactPersonId } : {}),
|
|
150
|
-
contact_handle: input.contactHandle?.trim() || null,
|
|
151
|
-
channel: input.channel,
|
|
152
|
-
last_message_preview: firstMessage || null,
|
|
153
|
-
last_message_at: now,
|
|
154
|
-
unread_count: 0,
|
|
155
|
-
status: 'open',
|
|
156
|
-
assigned_to: selfAuthor,
|
|
157
|
-
accent: CHANNEL_ACCENT_HEX[input.channel],
|
|
158
|
-
tags: [],
|
|
159
|
-
note: input.note?.trim() || null,
|
|
160
|
-
}
|
|
161
|
-
if (tenantId) convRow.tenant_id = tenantId
|
|
162
|
-
|
|
163
|
-
const { data: created, error } = (await (
|
|
164
|
-
(client().from(T.conversations) as { insert: (r: Row) => Row }).insert(convRow) as {
|
|
165
|
-
select: () => { single: () => Promise<{ data: Row | null; error: unknown }> }
|
|
166
|
-
}
|
|
167
|
-
)
|
|
168
|
-
.select()
|
|
169
|
-
.single()) as { data: Row | null; error: unknown }
|
|
170
|
-
if (error) throw error
|
|
171
|
-
if (!created) throw new Error('Conversation not created')
|
|
172
|
-
|
|
173
|
-
// Seed the thread with the first outbound message when provided.
|
|
174
|
-
if (firstMessage) {
|
|
175
|
-
const msgRow: Row = {
|
|
176
|
-
conversation_id: String(created.id),
|
|
177
|
-
channel: input.channel,
|
|
178
|
-
direction: 'outbound',
|
|
179
|
-
body: firstMessage,
|
|
180
|
-
author: selfAuthor,
|
|
181
|
-
at: now,
|
|
182
|
-
}
|
|
183
|
-
if (tenantId) msgRow.tenant_id = tenantId
|
|
184
|
-
await (client().from(T.messages) as { insert: (r: Row) => Promise<unknown> }).insert(msgRow)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return mapConversation(created)
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
async sendMessage(input: SendMessageInput): Promise<Message> {
|
|
191
|
-
const tenantId = resolveTenantId()
|
|
192
|
-
|
|
193
|
-
// Resolve the channel from the parent conversation so the message matches.
|
|
194
|
-
const convSelected = (client().from(T.conversations) as { select: (s: string) => Row }).select(
|
|
195
|
-
'channel',
|
|
196
|
-
)
|
|
197
|
-
const convFiltered = (convSelected as { eq: (c: string, v: string) => Row }).eq(
|
|
198
|
-
'id',
|
|
199
|
-
input.conversationId,
|
|
200
|
-
)
|
|
201
|
-
const { data: conv } = (await (
|
|
202
|
-
convFiltered as { maybeSingle: () => Promise<{ data: Row | null }> }
|
|
203
|
-
).maybeSingle()) as { data: Row | null }
|
|
204
|
-
const channel = (conv?.channel as Message['channel']) ?? 'sms'
|
|
205
|
-
|
|
206
|
-
const at = new Date().toISOString()
|
|
207
|
-
const row: Row = {
|
|
208
|
-
conversation_id: input.conversationId,
|
|
209
|
-
channel,
|
|
210
|
-
direction: 'outbound',
|
|
211
|
-
body: input.body,
|
|
212
|
-
author: selfAuthor,
|
|
213
|
-
at,
|
|
214
|
-
}
|
|
215
|
-
if (tenantId) row.tenant_id = tenantId
|
|
216
|
-
|
|
217
|
-
const { data: created, error } = (await (
|
|
218
|
-
(client().from(T.messages) as { insert: (r: Row) => Row }).insert(row) as {
|
|
219
|
-
select: () => { single: () => Promise<{ data: Row | null; error: unknown }> }
|
|
220
|
-
}
|
|
221
|
-
)
|
|
222
|
-
.select()
|
|
223
|
-
.single()) as { data: Row | null; error: unknown }
|
|
224
|
-
if (error) throw error
|
|
225
|
-
|
|
226
|
-
// Roll the parent conversation forward (preview / timestamp / unread / reopen).
|
|
227
|
-
await (
|
|
228
|
-
(client().from(T.conversations) as {
|
|
229
|
-
update: (r: Row) => Row
|
|
230
|
-
}).update({
|
|
231
|
-
last_message_preview: input.body,
|
|
232
|
-
last_message_at: at,
|
|
233
|
-
unread_count: 0,
|
|
234
|
-
status: 'open',
|
|
235
|
-
}) as { eq: (c: string, v: string) => Promise<unknown> }
|
|
236
|
-
).eq('id', input.conversationId)
|
|
237
|
-
|
|
238
|
-
return mapMessage(created ?? row)
|
|
239
|
-
},
|
|
240
|
-
|
|
241
|
-
async markRead(conversationId: string): Promise<void> {
|
|
242
|
-
const { error } = (await (
|
|
243
|
-
(client().from(T.conversations) as { update: (r: Row) => Row }).update({
|
|
244
|
-
unread_count: 0,
|
|
245
|
-
}) as { eq: (c: string, v: string) => Promise<{ error: unknown }> }
|
|
246
|
-
).eq('id', conversationId)) as { error: unknown }
|
|
247
|
-
if (error) throw error
|
|
248
|
-
},
|
|
249
|
-
|
|
250
|
-
async setStatus(conversationId: string, status: ConversationStatus): Promise<Conversation> {
|
|
251
|
-
const updated = (client().from(T.conversations) as { update: (r: Row) => Row }).update({
|
|
252
|
-
status,
|
|
253
|
-
})
|
|
254
|
-
const filtered = (updated as { eq: (c: string, v: string) => Row }).eq('id', conversationId)
|
|
255
|
-
const selected = (filtered as { select: () => Row }).select()
|
|
256
|
-
const { data, error } = (await (
|
|
257
|
-
selected as { single: () => Promise<{ data: Row | null; error: unknown }> }
|
|
258
|
-
).single()) as { data: Row | null; error: unknown }
|
|
259
|
-
if (error) throw error
|
|
260
|
-
if (!data) throw new Error('Conversation not found')
|
|
261
|
-
return mapConversation(data)
|
|
262
|
-
},
|
|
263
|
-
}
|
|
264
|
-
}
|
package/src/data/tables.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
// Physical table names for the conversations plugin (plg_conversations* prefix).
|
|
2
|
-
// Import T and reference T.<key> in the data provider so a rename lands in
|
|
3
|
-
// exactly one place.
|
|
4
|
-
export const T = {
|
|
5
|
-
conversations: 'plg_conversations',
|
|
6
|
-
messages: 'plg_conversation_messages',
|
|
7
|
-
} as const
|
package/src/data/types.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Conversation,
|
|
3
|
-
Message,
|
|
4
|
-
ListConversationsQuery,
|
|
5
|
-
SendMessageInput,
|
|
6
|
-
CreateConversationInput,
|
|
7
|
-
ConversationStatus,
|
|
8
|
-
} from '../types'
|
|
9
|
-
|
|
10
|
-
export interface ConversationsProvider {
|
|
11
|
-
listConversations(query?: ListConversationsQuery): Promise<Conversation[]>
|
|
12
|
-
getMessages(conversationId: string): Promise<Message[]>
|
|
13
|
-
createConversation(input: CreateConversationInput): Promise<Conversation>
|
|
14
|
-
sendMessage(input: SendMessageInput): Promise<Message>
|
|
15
|
-
markRead(conversationId: string): Promise<void>
|
|
16
|
-
setStatus(conversationId: string, status: ConversationStatus): Promise<Conversation>
|
|
17
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
|
|
3
|
-
import { getActiveTenantId, getSupabaseClientOptional, registerTranslations } from '@fayz-ai/core'
|
|
4
|
-
import type { EntityLookup } from '@fayz-ai/saas'
|
|
5
|
-
import { ConversationsPage } from './ConversationsPage'
|
|
6
|
-
import type { ResolvedConversationsConfig } from './ConversationsContext'
|
|
7
|
-
import type { ConversationsProvider } from './data/types'
|
|
8
|
-
import { createMockConversationsProvider } from './data/mock'
|
|
9
|
-
import { createSupabaseConversationsProvider } from './data/supabase'
|
|
10
|
-
import { createConversationsStore } from './store'
|
|
11
|
-
import { conversationsLocales } from './locales'
|
|
12
|
-
import { MIGRATION_001_CONVERSATIONS, MIGRATION_002_CONTACT_PERSON } from './migrations'
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// @fayz-ai/plugin-conversations — the GoHighLevel "Conversations" equivalent:
|
|
16
|
-
// one omni-channel inbox (SMS / WhatsApp / Instagram / Email / Web chat).
|
|
17
|
-
// Universal plugin — reusable by any vertical (beauty, resto, agency…).
|
|
18
|
-
//
|
|
19
|
-
// Supabase-backed (plg_conversations / plg_conversation_messages, tenant-scoped
|
|
20
|
-
// via RLS) when a client is registered; a persisted mock inbox otherwise. Real
|
|
21
|
-
// channel connectors (Twilio, WhatsApp Cloud, Meta, IMAP) deliver inbound rows
|
|
22
|
-
// out-of-band; this plugin is the read/compose surface.
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
export interface ConversationsPluginOptions {
|
|
26
|
-
navPosition?: number
|
|
27
|
-
navSection?: 'main' | 'secondary' | 'settings'
|
|
28
|
-
navLabel?: string
|
|
29
|
-
scope?: PluginScope
|
|
30
|
-
verticalId?: VerticalId
|
|
31
|
-
dataProvider?: ConversationsProvider
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* `people.kind` for contacts created from the compose modal's picker. Each
|
|
35
|
-
* vertical names its people differently (beauty 'client', agency 'lead'),
|
|
36
|
-
* hence a knob rather than a hardcoded value. Default: 'contact'.
|
|
37
|
-
*/
|
|
38
|
-
contactKind?: string
|
|
39
|
-
/**
|
|
40
|
-
* Per-vertical extension table linked by `person_id` (e.g. 'clients'). Pools
|
|
41
|
-
* without it live off `public.people` alone — absent is not an error.
|
|
42
|
-
*/
|
|
43
|
-
contactExtensionTable?: string
|
|
44
|
-
/** Custom search source for the picker. Defaults to the person archetype lookup. */
|
|
45
|
-
contactLookup?: EntityLookup
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function createSafeProvider(): ConversationsProvider {
|
|
49
|
-
// Real data when a Supabase client is registered; mock inbox otherwise.
|
|
50
|
-
// Resolution is LAZY (per call): createConversationsPlugin runs at module
|
|
51
|
-
// scope in the app config, BEFORE the Supabase client is registered — an
|
|
52
|
-
// eager check here would capture the mock forever even on live backends.
|
|
53
|
-
let real: ConversationsProvider | null = null
|
|
54
|
-
let mock: ConversationsProvider | null = null
|
|
55
|
-
const resolve = (): ConversationsProvider => {
|
|
56
|
-
if (getSupabaseClientOptional()) {
|
|
57
|
-
real ??= createSupabaseConversationsProvider({ tenantId: () => getActiveTenantId() })
|
|
58
|
-
return real
|
|
59
|
-
}
|
|
60
|
-
mock ??= createMockConversationsProvider({ tenantId: () => getActiveTenantId() })
|
|
61
|
-
return mock
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
listConversations: (...a) => resolve().listConversations(...a),
|
|
65
|
-
getMessages: (...a) => resolve().getMessages(...a),
|
|
66
|
-
sendMessage: (...a) => resolve().sendMessage(...a),
|
|
67
|
-
markRead: (...a) => resolve().markRead(...a),
|
|
68
|
-
setStatus: (...a) => resolve().setStatus(...a),
|
|
69
|
-
createConversation: (...a) => resolve().createConversation(...a),
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function createConversationsPlugin(options?: ConversationsPluginOptions): PluginManifest {
|
|
74
|
-
registerTranslations(conversationsLocales)
|
|
75
|
-
const provider = options?.dataProvider ?? createSafeProvider()
|
|
76
|
-
const store = createConversationsStore(provider)
|
|
77
|
-
|
|
78
|
-
const config: ResolvedConversationsConfig = {
|
|
79
|
-
contactKind: options?.contactKind ?? 'contact',
|
|
80
|
-
contactExtensionTable: options?.contactExtensionTable,
|
|
81
|
-
contactLookup: options?.contactLookup,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const PageComponent: React.ComponentType<unknown> = () =>
|
|
85
|
-
React.createElement(ConversationsPage, { store, config })
|
|
86
|
-
PageComponent.displayName = 'ConversationsPage'
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
id: 'conversations',
|
|
90
|
-
name: options?.navLabel ?? 'Conversations',
|
|
91
|
-
icon: 'MessageCircle',
|
|
92
|
-
version: '1.0.0',
|
|
93
|
-
scope: options?.scope ?? 'universal',
|
|
94
|
-
verticalId: options?.verticalId,
|
|
95
|
-
defaultEnabled: true,
|
|
96
|
-
dependencies: [],
|
|
97
|
-
declaredFeatures: [{ id: 'conversations', label: 'Conversations', group: 'Engage' }],
|
|
98
|
-
// Recurring monthly quota — counts conversation threads created this month.
|
|
99
|
-
declaredLimits: [
|
|
100
|
-
{ key: 'conversations_month', label: 'Conversations this month', table: 'plg_conversations', period: 'month' },
|
|
101
|
-
],
|
|
102
|
-
navigation: [
|
|
103
|
-
{
|
|
104
|
-
section: options?.navSection ?? 'main',
|
|
105
|
-
position: options?.navPosition ?? 1,
|
|
106
|
-
label: options?.navLabel ?? 'Conversations',
|
|
107
|
-
route: '/conversations',
|
|
108
|
-
icon: 'MessageCircle',
|
|
109
|
-
permission: { feature: 'conversations', action: 'read' as const },
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
routes: [
|
|
113
|
-
{
|
|
114
|
-
path: '/conversations',
|
|
115
|
-
component: PageComponent,
|
|
116
|
-
fullBleed: true,
|
|
117
|
-
permission: { feature: 'conversations', action: 'read' as const },
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
widgets: [],
|
|
121
|
-
events: [
|
|
122
|
-
{ name: 'conversations.message.received', description: 'An inbound message arrived on any channel' },
|
|
123
|
-
{ name: 'conversations.message.sent', description: 'An outbound message was sent' },
|
|
124
|
-
],
|
|
125
|
-
aiTools: [
|
|
126
|
-
{
|
|
127
|
-
id: 'conversations.list-threads',
|
|
128
|
-
name: 'listConversations',
|
|
129
|
-
description: 'Lists open conversations across all channels, optionally filtered by channel.',
|
|
130
|
-
icon: 'MessageCircle',
|
|
131
|
-
mode: 'read' as const,
|
|
132
|
-
category: 'Conversations',
|
|
133
|
-
parameters: {
|
|
134
|
-
type: 'object' as const,
|
|
135
|
-
properties: {
|
|
136
|
-
channel: {
|
|
137
|
-
type: 'string' as const,
|
|
138
|
-
enum: ['all', 'sms', 'whatsapp', 'instagram', 'email', 'webchat'],
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
suggestions: [
|
|
143
|
-
{ label: 'Show unread conversations' },
|
|
144
|
-
{ label: 'Any new WhatsApp messages?' },
|
|
145
|
-
],
|
|
146
|
-
permission: { feature: 'conversations', action: 'read' as const },
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
id: 'conversations.send-message',
|
|
150
|
-
name: 'sendMessage',
|
|
151
|
-
description: 'Sends a reply in a conversation thread.',
|
|
152
|
-
icon: 'Send',
|
|
153
|
-
mode: 'persist' as const,
|
|
154
|
-
category: 'Conversations',
|
|
155
|
-
parameters: {
|
|
156
|
-
type: 'object' as const,
|
|
157
|
-
properties: {
|
|
158
|
-
conversationId: { type: 'string' as const, description: 'Conversation id' },
|
|
159
|
-
body: { type: 'string' as const, description: 'Message body' },
|
|
160
|
-
},
|
|
161
|
-
required: ['conversationId', 'body'],
|
|
162
|
-
},
|
|
163
|
-
permission: { feature: 'conversations', action: 'create' as const },
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
migrations: [
|
|
167
|
-
{
|
|
168
|
-
id: 'conversations-001-base-tables',
|
|
169
|
-
version: '1.0.0',
|
|
170
|
-
sql: MIGRATION_001_CONVERSATIONS,
|
|
171
|
-
description: 'Create plg_conversations and plg_conversation_messages (tenant-scoped RLS)',
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
id: 'conversations-002-contact-person',
|
|
175
|
-
version: '1.1.0',
|
|
176
|
-
sql: MIGRATION_002_CONTACT_PERSON,
|
|
177
|
-
description: 'Link threads to public.people via contact_person_id (nullable, ON DELETE SET NULL)',
|
|
178
|
-
},
|
|
179
|
-
],
|
|
180
|
-
locales: conversationsLocales,
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export type { ConversationsProvider } from './data/types'
|
|
185
|
-
export type { Conversation, Message, Channel, CreateConversationInput } from './types'
|
|
186
|
-
export { createMockConversationsProvider, type MockConversationsConfig } from './data/mock'
|
|
187
|
-
export {
|
|
188
|
-
createSupabaseConversationsProvider,
|
|
189
|
-
type SupabaseConversationsConfig,
|
|
190
|
-
} from './data/supabase'
|
package/src/locales/en.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
export const en: Record<string, string> = {
|
|
2
|
-
'conversations.title': 'Conversations',
|
|
3
|
-
'conversations.subtitle': 'Unified inbox across every channel',
|
|
4
|
-
|
|
5
|
-
// Conversation list
|
|
6
|
-
'conversations.list.search': 'Search conversations',
|
|
7
|
-
'conversations.list.loading': 'Loading…',
|
|
8
|
-
'conversations.list.empty': 'No conversations',
|
|
9
|
-
'conversations.list.new': 'New conversation',
|
|
10
|
-
|
|
11
|
-
// Channel filters
|
|
12
|
-
'conversations.filter.all': 'All',
|
|
13
|
-
'conversations.filter.whatsapp': 'WhatsApp',
|
|
14
|
-
'conversations.filter.sms': 'SMS',
|
|
15
|
-
'conversations.filter.instagram': 'Instagram',
|
|
16
|
-
'conversations.filter.email': 'Email',
|
|
17
|
-
'conversations.filter.webchat': 'Web',
|
|
18
|
-
|
|
19
|
-
// Status labels
|
|
20
|
-
'conversations.status.open': 'Open',
|
|
21
|
-
'conversations.status.snoozed': 'Snoozed',
|
|
22
|
-
'conversations.status.closed': 'Closed',
|
|
23
|
-
|
|
24
|
-
// Empty state
|
|
25
|
-
'conversations.empty.select': 'Select a conversation to start chatting',
|
|
26
|
-
|
|
27
|
-
// Thread
|
|
28
|
-
'conversations.thread.back': 'Back to conversations',
|
|
29
|
-
'conversations.thread.snooze': 'Snooze',
|
|
30
|
-
'conversations.thread.close': 'Close',
|
|
31
|
-
'conversations.thread.details': 'Toggle contact details',
|
|
32
|
-
'conversations.thread.empty': 'No messages yet',
|
|
33
|
-
'conversations.thread.reply': 'Reply via {{channel}}…',
|
|
34
|
-
'conversations.thread.send': 'Send',
|
|
35
|
-
|
|
36
|
-
// Contact panel
|
|
37
|
-
'conversations.contact.details': 'Details',
|
|
38
|
-
'conversations.contact.closeDetails': 'Close details',
|
|
39
|
-
'conversations.contact.channel': 'Channel',
|
|
40
|
-
'conversations.contact.status': 'Status',
|
|
41
|
-
'conversations.contact.assignedTo': 'Assigned to',
|
|
42
|
-
'conversations.contact.location': 'Location',
|
|
43
|
-
'conversations.contact.tags': 'Tags',
|
|
44
|
-
'conversations.contact.note': 'Note',
|
|
45
|
-
'conversations.contact.linkedRecords': 'Linked records',
|
|
46
|
-
'conversations.contact.noLinkedRecords': 'No linked records yet.',
|
|
47
|
-
|
|
48
|
-
// New-conversation modal
|
|
49
|
-
'conversations.new.title': 'New conversation',
|
|
50
|
-
'conversations.new.channel': 'Channel',
|
|
51
|
-
'conversations.new.contactName': 'Contact name',
|
|
52
|
-
'conversations.new.contactNamePlaceholder': 'e.g. Jane Doe',
|
|
53
|
-
'conversations.new.handle': 'Phone / handle / email',
|
|
54
|
-
'conversations.new.handlePlaceholder': '+1 555 000 0000',
|
|
55
|
-
// The handle field only surfaces when the picked contact has nothing usable
|
|
56
|
-
// for the active channel — otherwise it is derived and shown on the chip.
|
|
57
|
-
'conversations.new.addHandle': 'Add {label}',
|
|
58
|
-
'conversations.new.handleLabel.phone': 'phone',
|
|
59
|
-
'conversations.new.handleLabel.email': 'email',
|
|
60
|
-
'conversations.new.handleLabel.instagram': 'Instagram handle',
|
|
61
|
-
'conversations.new.handleLabel.webchat': 'web chat id',
|
|
62
|
-
'conversations.new.firstMessage': 'First message',
|
|
63
|
-
'conversations.new.firstMessagePlaceholder': 'Write the first message (optional)…',
|
|
64
|
-
'conversations.new.cancel': 'Cancel',
|
|
65
|
-
'conversations.new.create': 'Start conversation',
|
|
66
|
-
'conversations.new.creating': 'Starting…',
|
|
67
|
-
'conversations.new.createFailed': 'Could not create the conversation',
|
|
68
|
-
}
|
package/src/locales/index.ts
DELETED
package/src/locales/pt-BR.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
export const ptBR: Record<string, string> = {
|
|
2
|
-
'conversations.title': 'Conversas',
|
|
3
|
-
'conversations.subtitle': 'Caixa de entrada unificada de todos os canais',
|
|
4
|
-
|
|
5
|
-
// Lista de conversas
|
|
6
|
-
'conversations.list.search': 'Buscar conversas',
|
|
7
|
-
'conversations.list.loading': 'Carregando…',
|
|
8
|
-
'conversations.list.empty': 'Nenhuma conversa',
|
|
9
|
-
'conversations.list.new': 'Nova conversa',
|
|
10
|
-
|
|
11
|
-
// Filtros de canal
|
|
12
|
-
'conversations.filter.all': 'Todas',
|
|
13
|
-
'conversations.filter.whatsapp': 'WhatsApp',
|
|
14
|
-
'conversations.filter.sms': 'SMS',
|
|
15
|
-
'conversations.filter.instagram': 'Instagram',
|
|
16
|
-
'conversations.filter.email': 'E-mail',
|
|
17
|
-
'conversations.filter.webchat': 'Web',
|
|
18
|
-
|
|
19
|
-
// Rótulos de status
|
|
20
|
-
'conversations.status.open': 'Aberta',
|
|
21
|
-
'conversations.status.snoozed': 'Adiada',
|
|
22
|
-
'conversations.status.closed': 'Encerrada',
|
|
23
|
-
|
|
24
|
-
// Estado vazio
|
|
25
|
-
'conversations.empty.select': 'Selecione uma conversa para começar a conversar',
|
|
26
|
-
|
|
27
|
-
// Thread
|
|
28
|
-
'conversations.thread.back': 'Voltar às conversas',
|
|
29
|
-
'conversations.thread.snooze': 'Adiar',
|
|
30
|
-
'conversations.thread.close': 'Encerrar',
|
|
31
|
-
'conversations.thread.details': 'Alternar detalhes do contato',
|
|
32
|
-
'conversations.thread.empty': 'Nenhuma mensagem ainda',
|
|
33
|
-
'conversations.thread.reply': 'Responder via {{channel}}…',
|
|
34
|
-
'conversations.thread.send': 'Enviar',
|
|
35
|
-
|
|
36
|
-
// Painel do contato
|
|
37
|
-
'conversations.contact.details': 'Detalhes',
|
|
38
|
-
'conversations.contact.closeDetails': 'Fechar detalhes',
|
|
39
|
-
'conversations.contact.channel': 'Canal',
|
|
40
|
-
'conversations.contact.status': 'Status',
|
|
41
|
-
'conversations.contact.assignedTo': 'Responsável',
|
|
42
|
-
'conversations.contact.location': 'Localização',
|
|
43
|
-
'conversations.contact.tags': 'Etiquetas',
|
|
44
|
-
'conversations.contact.note': 'Nota',
|
|
45
|
-
'conversations.contact.linkedRecords': 'Registros vinculados',
|
|
46
|
-
'conversations.contact.noLinkedRecords': 'Nenhum registro vinculado ainda.',
|
|
47
|
-
|
|
48
|
-
// Modal de nova conversa
|
|
49
|
-
'conversations.new.title': 'Nova conversa',
|
|
50
|
-
'conversations.new.channel': 'Canal',
|
|
51
|
-
'conversations.new.contactName': 'Nome do contato',
|
|
52
|
-
'conversations.new.contactNamePlaceholder': 'ex.: Maria Silva',
|
|
53
|
-
'conversations.new.handle': 'Telefone / usuário / e-mail',
|
|
54
|
-
'conversations.new.handlePlaceholder': '+55 11 99999-0000',
|
|
55
|
-
// O campo de handle só aparece quando o contato escolhido não tem o dado do
|
|
56
|
-
// canal ativo — caso contrário ele é derivado e mostrado no chip.
|
|
57
|
-
'conversations.new.addHandle': 'Adicionar {label}',
|
|
58
|
-
'conversations.new.handleLabel.phone': 'telefone',
|
|
59
|
-
'conversations.new.handleLabel.email': 'e-mail',
|
|
60
|
-
'conversations.new.handleLabel.instagram': '@ do Instagram',
|
|
61
|
-
'conversations.new.handleLabel.webchat': 'id do chat',
|
|
62
|
-
'conversations.new.firstMessage': 'Primeira mensagem',
|
|
63
|
-
'conversations.new.firstMessagePlaceholder': 'Escreva a primeira mensagem (opcional)…',
|
|
64
|
-
'conversations.new.cancel': 'Cancelar',
|
|
65
|
-
'conversations.new.create': 'Iniciar conversa',
|
|
66
|
-
'conversations.new.creating': 'Iniciando…',
|
|
67
|
-
'conversations.new.createFailed': 'Não foi possível criar a conversa',
|
|
68
|
-
}
|