spine-framework-cortex 0.2.24 → 0.2.26

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.
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, useState } from 'react'
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
2
  import { useParams, useNavigate } from 'react-router-dom'
3
3
  import { useAppPath } from '@core/hooks/useAppPath'
4
4
  import { apiFetch } from '@core/lib/api'
@@ -78,14 +78,16 @@ const STATUS_BADGE: Record<string, string> = {
78
78
  }
79
79
 
80
80
  // Merged Thread Panel - combines internal and external messages
81
- function MergedThreadPanel({
82
- ticketId,
83
- externalThread,
84
- internalThread
85
- }: {
81
+ function MergedThreadPanel({
82
+ ticketId,
83
+ externalThread,
84
+ internalThread,
85
+ refreshThreads,
86
+ }: {
86
87
  ticketId: string
87
88
  externalThread: Thread | null
88
89
  internalThread: Thread | null
90
+ refreshThreads: () => Promise<void>
89
91
  }) {
90
92
  const [messages, setMessages] = useState<Message[]>([])
91
93
  const [loading, setLoading] = useState(true)
@@ -132,29 +134,19 @@ function MergedThreadPanel({
132
134
  try {
133
135
  // Create internal thread if it doesn't exist
134
136
  if (!targetThread && replyType === 'internal') {
135
- const threadTypesRes = await apiFetch('/api/types?kind=thread&limit=1').then(r => r.json())
136
- const threadTypes = Array.isArray(threadTypesRes?.data) ? threadTypesRes.data : Array.isArray(threadTypesRes) ? threadTypesRes : []
137
- if (!threadTypes.length) {
138
- console.error('No thread type found')
139
- return
140
- }
141
-
142
137
  const threadRes = await apiFetch('/api/admin-data?action=create&entity=threads', {
143
138
  method: 'POST', headers: { 'Content-Type': 'application/json' },
144
139
  body: JSON.stringify({
145
140
  target_type: 'item',
146
141
  target_id: ticketId,
147
142
  visibility: 'internal',
148
- type_id: threadTypes[0].id
143
+ type_slug: 'support_thread'
149
144
  }),
150
145
  })
151
146
  const newThread = await threadRes.json()
152
147
  if (newThread?.id) {
153
148
  targetThread = newThread
154
- // Refresh threads list
155
- const thr = await apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${ticketId}&limit=10`).then(r => r.json())
156
- const threadList = thr?.data ?? thr
157
- setThreads(Array.isArray(threadList) ? threadList : [])
149
+ await refreshThreads()
158
150
  }
159
151
  }
160
152
 
@@ -163,14 +155,6 @@ function MergedThreadPanel({
163
155
  return
164
156
  }
165
157
 
166
- // Resolve message type_id
167
- const typesRes = await apiFetch('/api/types?kind=message&limit=1').then(r => r.json())
168
- const types = Array.isArray(typesRes?.data) ? typesRes.data : Array.isArray(typesRes) ? typesRes : []
169
- if (!types.length) {
170
- console.error('No message type found')
171
- return
172
- }
173
-
174
158
  // Get current message count for sequencing
175
159
  const currentMsgs = await apiFetch(`/api/admin-data?action=list&entity=messages&thread_id=${targetThread.id}&limit=1000`).then(r => r.json())
176
160
  const msgCount = Array.isArray(currentMsgs?.data) ? currentMsgs.data.length : Array.isArray(currentMsgs) ? currentMsgs.length : 0
@@ -178,11 +162,11 @@ function MergedThreadPanel({
178
162
 
179
163
  const res = await apiFetch('/api/admin-data?action=create&entity=messages', {
180
164
  method: 'POST', headers: { 'Content-Type': 'application/json' },
181
- body: JSON.stringify({
182
- thread_id: targetThread.id,
183
- content: reply.trim(),
184
- direction: 'outbound',
185
- type_id: types[0].id,
165
+ body: JSON.stringify({
166
+ thread_id: targetThread.id,
167
+ content: reply.trim(),
168
+ direction: 'outbound',
169
+ type_slug: 'support_reply',
186
170
  sequence: nextSeq,
187
171
  visibility: replyType
188
172
  }),
@@ -740,18 +724,25 @@ export default function TicketDetailPage() {
740
724
  const [watchLoading, setWatchLoading] = useState(false)
741
725
  const [analysisLoading, setAnalysisLoading] = useState(false)
742
726
 
743
- useEffect(() => {
727
+ const loadTicketAndThreads = useCallback(async () => {
744
728
  if (!id) return
745
- Promise.all([
746
- apiFetch(`/api/admin-data?action=get&entity=items&id=${id}`).then(r => r.json()),
747
- apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${id}&limit=10`).then(r => r.json()),
748
- ]).then(([ir, thr]) => {
729
+ setLoading(true)
730
+ try {
731
+ const [ir, thr] = await Promise.all([
732
+ apiFetch(`/api/admin-data?action=get&entity=items&id=${id}`).then(r => r.json()),
733
+ apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${id}&type_slug=support_thread&limit=10`).then(r => r.json()),
734
+ ])
749
735
  setTicket(ir?.data ?? ir ?? null)
750
736
  const threadList = thr?.data ?? thr
751
737
  setThreads(Array.isArray(threadList) ? threadList : [])
752
- }).catch(() => {}).finally(() => setLoading(false))
738
+ } catch {
739
+ } finally {
740
+ setLoading(false)
741
+ }
753
742
  }, [id])
754
743
 
744
+ useEffect(() => { loadTicketAndThreads() }, [loadTicketAndThreads])
745
+
755
746
  // Check if current user is watching this ticket
756
747
  useEffect(() => {
757
748
  if (!id || !user?.id) return
@@ -776,16 +767,12 @@ export default function TicketDetailPage() {
776
767
  setIsWatching(false)
777
768
  setWatcherId(null)
778
769
  } else {
779
- // Resolve watcher type_id via types API
780
- const typeRes = await apiFetch('/api/types?kind=watcher&limit=1').then(r => r.json())
781
- const types = Array.isArray(typeRes?.data) ? typeRes.data : Array.isArray(typeRes) ? typeRes : []
782
- if (!types.length) { console.error('No watcher type found'); return }
783
770
  const res = await apiFetch('/api/admin-data?action=create&entity=watchers', {
784
771
  method: 'POST',
785
772
  headers: { 'Content-Type': 'application/json' },
786
773
  body: JSON.stringify({
787
774
  entity: 'watchers',
788
- type_id: types[0].id,
775
+ type_slug: 'watcher',
789
776
  target_type: 'item',
790
777
  target_id: id,
791
778
  person_id: user.id,
@@ -887,10 +874,11 @@ export default function TicketDetailPage() {
887
874
  <div className="flex flex-1 min-h-0">
888
875
  {/* Main Conversation Panel */}
889
876
  <div className="flex-1 min-w-0 border-r border-border">
890
- <MergedThreadPanel
891
- ticketId={id || ''}
892
- externalThread={externalThread}
893
- internalThread={internalThread}
877
+ <MergedThreadPanel
878
+ ticketId={id || ''}
879
+ externalThread={externalThread}
880
+ internalThread={internalThread}
881
+ refreshThreads={loadTicketAndThreads}
894
882
  />
895
883
  </div>
896
884
 
package/seed/types.json CHANGED
@@ -1868,7 +1868,7 @@
1868
1868
  "kind": "item",
1869
1869
  "slug": "course_lesson",
1870
1870
  "name": "Course Lesson",
1871
- "description": "Learning content unit part of a course sequence",
1871
+ "description": "Learning content unit \u2014 part of a course sequence",
1872
1872
  "icon": "graduation-cap",
1873
1873
  "color": "#F97316",
1874
1874
  "ownership": "tenant",
@@ -1986,4 +1986,4 @@
1986
1986
  },
1987
1987
  "validation_schema": {}
1988
1988
  }
1989
- ]
1989
+ ]
@@ -1,74 +0,0 @@
1
- import { useState, useEffect } from 'react'
2
- import { supabase } from '@core/lib/supabase'
3
-
4
- interface TypeRecord {
5
- id: string
6
- slug: string
7
- name: string
8
- description?: string
9
- }
10
-
11
- // Module-level cache - persists across renders, shared across hooks
12
- let typeCache: Map<string, TypeRecord> | null = null
13
- let loadPromise: Promise<Map<string, TypeRecord>> | null = null
14
-
15
- async function fetchTypes(): Promise<Map<string, TypeRecord>> {
16
- if (typeCache) return typeCache
17
- if (loadPromise) return loadPromise
18
-
19
- loadPromise = (async () => {
20
- try {
21
- // Query Supabase directly - types is a config table, not exposed via admin-data
22
- const { data: types, error } = await supabase
23
- .from('types')
24
- .select('id, slug, name, description')
25
- .eq('is_active', true)
26
- .limit(100)
27
-
28
- if (error) throw error
29
-
30
- typeCache = new Map((types || []).map(t => [t.slug, t]))
31
- return typeCache
32
- } catch (e) {
33
- console.error('Failed to load types:', e)
34
- typeCache = new Map() // Empty cache on error
35
- return typeCache
36
- } finally {
37
- loadPromise = null
38
- }
39
- })()
40
-
41
- return loadPromise
42
- }
43
-
44
- export function useTypeRegistry() {
45
- const [types, setTypes] = useState<Map<string, TypeRecord>>(typeCache || new Map())
46
- const [loading, setLoading] = useState(!typeCache)
47
- const [error, setError] = useState<string | null>(null)
48
-
49
- useEffect(() => {
50
- if (typeCache) return
51
-
52
- fetchTypes()
53
- .then(cache => {
54
- setTypes(cache)
55
- setLoading(false)
56
- })
57
- .catch(e => {
58
- setError(e.message)
59
- setLoading(false)
60
- })
61
- }, [])
62
-
63
- const getTypeId = (slug: string): string | null => {
64
- return types.get(slug)?.id || null
65
- }
66
-
67
- return { types, loading, error, getTypeId }
68
- }
69
-
70
- // Synchronous version for use inside async functions
71
- export async function getTypeIdAsync(slug: string): Promise<string | null> {
72
- const cache = await fetchTypes()
73
- return cache.get(slug)?.id || null
74
- }