spine-framework-portal 0.2.21 → 0.2.22

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,5 +1,4 @@
1
1
  import { useState } from 'react'
2
- import { getTypeIdAsync } from '../hooks/useTypeRegistry'
3
2
  import { apiFetch } from '@core/lib/api'
4
3
  import { Card, CardContent, CardHeader } from '@core/components/ui/card'
5
4
  import { Button } from '@core/components/ui/button'
@@ -40,13 +39,12 @@ export function KBGenerator({ ticket, onGenerated, onCancel }: KBGeneratorProps)
40
39
  const handleSave = async () => {
41
40
  if (!generatedArticle) return
42
41
  try {
43
- const kbArticleTypeId = await getTypeIdAsync('kb_article')
44
42
  const res = await apiFetch('/.netlify/functions/admin-data', {
45
43
  method: 'POST',
46
44
  headers: { 'Content-Type': 'application/json' },
47
45
  body: JSON.stringify({
48
46
  entity: 'items',
49
- type_id: kbArticleTypeId,
47
+ type_slug: 'kb_article',
50
48
  title: generatedArticle.title,
51
49
  status: 'published',
52
50
  description: editedContent,
@@ -1,6 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from 'react'
2
2
  import { apiFetch } from '@core/lib/api'
3
- import { getTypeIdAsync } from './useTypeRegistry'
4
3
 
5
4
  export interface CommunityPost {
6
5
  id: string
@@ -59,12 +58,9 @@ export function useCreatePost() {
59
58
  setLoading(true)
60
59
  setError(null)
61
60
  try {
62
- const typeId = await getTypeIdAsync('community_post')
63
- if (!typeId) throw new Error('community_post type not found')
64
-
65
61
  return await fetchJSON('/.netlify/functions/admin-data', {
66
62
  method: 'POST',
67
- body: JSON.stringify({ entity: 'items', type_id: typeId, ...fields }),
63
+ body: JSON.stringify({ entity: 'items', type_slug: 'community_post', ...fields }),
68
64
  })
69
65
  } catch (e: any) {
70
66
  setError(e.message)
@@ -76,7 +76,7 @@ export function useUpsertProgress() {
76
76
  const upsert = useCallback(async (params: {
77
77
  personId: string
78
78
  itemId: string
79
- typeId: string
79
+ typeSlug: string
80
80
  accountId: string
81
81
  appId?: string
82
82
  status?: string
@@ -92,7 +92,7 @@ export function useUpsertProgress() {
92
92
  body: JSON.stringify({
93
93
  person_id: params.personId,
94
94
  item_id: params.itemId,
95
- type_id: params.typeId,
95
+ type_slug: params.typeSlug,
96
96
  account_id: params.accountId,
97
97
  app_id: params.appId,
98
98
  status: params.status,
@@ -1,6 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from 'react'
2
2
  import { apiFetch } from '@core/lib/api'
3
- import { getTypeIdAsync } from './useTypeRegistry'
4
3
 
5
4
  export interface PortalMessage {
6
5
  id: string
@@ -63,11 +62,8 @@ export function usePortalThread(targetType: string, targetId: string | null, dom
63
62
  setLoading(true)
64
63
  setError(null)
65
64
  try {
66
- const threadTypeId = await getTypeIdAsync(threadTypeSlug)
67
- if (!threadTypeId) throw new Error(`${threadTypeSlug} type not found`)
68
-
69
65
  const threads = await fetchJSON(
70
- `/.netlify/functions/admin-data?entity=threads&target_type=${targetType}&target_id=${targetId}&type_id=${threadTypeId}`
66
+ `/.netlify/functions/admin-data?entity=threads&target_type=${targetType}&target_id=${targetId}&type_slug=${threadTypeSlug}`
71
67
  )
72
68
  const found: PortalThread | null = Array.isArray(threads) ? (threads[0] ?? null) : null
73
69
  setThread(found)
@@ -92,14 +88,11 @@ export function usePortalThread(targetType: string, targetId: string | null, dom
92
88
  let activeThread = thread
93
89
 
94
90
  if (!activeThread?.id) {
95
- const threadTypeId = await getTypeIdAsync(threadTypeSlug)
96
- if (!threadTypeId) throw new Error(`${threadTypeSlug} type not found`)
97
-
98
91
  activeThread = await fetchJSON('/.netlify/functions/admin-data', {
99
92
  method: 'POST',
100
93
  body: JSON.stringify({
101
94
  entity: 'threads',
102
- type_id: threadTypeId,
95
+ type_slug: threadTypeSlug,
103
96
  target_type: targetType,
104
97
  target_id: targetId,
105
98
  status: 'open',
@@ -108,9 +101,6 @@ export function usePortalThread(targetType: string, targetId: string | null, dom
108
101
  setThread(activeThread)
109
102
  }
110
103
 
111
- const messageTypeId = await getTypeIdAsync(messageTypeSlug)
112
- if (!messageTypeId) throw new Error(`${messageTypeSlug} type not found`)
113
-
114
104
  // Calculate next sequence based on current messages to avoid stale state
115
105
  const nextSequence = messages.reduce((max, m) => Math.max(max, m.sequence || 0), 0) + 1
116
106
 
@@ -118,7 +108,7 @@ export function usePortalThread(targetType: string, targetId: string | null, dom
118
108
  method: 'POST',
119
109
  body: JSON.stringify({
120
110
  entity: 'messages',
121
- type_id: messageTypeId,
111
+ type_slug: messageTypeSlug,
122
112
  thread_id: activeThread!.id,
123
113
  content,
124
114
  direction: 'inbound',
@@ -1,6 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from 'react'
2
2
  import { apiFetch } from '@core/lib/api'
3
- import { getTypeIdAsync } from './useTypeRegistry'
4
3
 
5
4
  export interface Ticket {
6
5
  id: string
@@ -77,12 +76,9 @@ export function useCreateTicket() {
77
76
  setLoading(true)
78
77
  setError(null)
79
78
  try {
80
- const typeId = await getTypeIdAsync('support_ticket')
81
- if (!typeId) throw new Error('support_ticket type not found')
82
-
83
79
  return await fetchJSON('/.netlify/functions/admin-data', {
84
80
  method: 'POST',
85
- body: JSON.stringify({ entity: 'items', type_id: typeId, ...fields }),
81
+ body: JSON.stringify({ entity: 'items', type_slug: 'support_ticket', ...fields }),
86
82
  })
87
83
  } catch (e: any) {
88
84
  setError(e.message)
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "Portal",
3
3
  "slug": "portal",
4
4
  "description": "Self-service portal for customers to access tickets, knowledge base, courses, and community",
5
- "version": "0.2.21",
5
+ "version": "0.2.22",
6
6
  "app_type": "full",
7
7
  "route_prefix": "/portal",
8
8
  "required_roles": ["member", "member_admin"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework-portal",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "private": false,
5
5
  "description": "Customer Portal — self-service portal app for Spine Framework",
6
6
  "type": "module",
@@ -1,10 +1,9 @@
1
- import { useState, useEffect, useRef } from 'react'
1
+ import { useState, useEffect } from 'react'
2
2
  import { CheckCircle, Circle, PlayCircle, GraduationCap, Send } from 'lucide-react'
3
3
  import { useCourseLessons, type CourseItem } from '../hooks/useCourses'
4
4
  import { usePortalSignal } from '../hooks/usePortalSignal'
5
5
  import { useItemProgress, useUpsertProgress } from '../hooks/useItemProgress'
6
6
  import { usePortalThread } from '../hooks/usePortalThreads'
7
- import { getTypeIdAsync } from '../hooks/useTypeRegistry'
8
7
  import { useAuth } from '@core/contexts/AuthContext'
9
8
  import { SearchFilterBar } from '../components/SearchFilterBar'
10
9
  import { Button } from '@core/components/ui/button'
@@ -42,7 +41,6 @@ export function CoursesPage() {
42
41
  const [selectedId, setSelectedId] = useState<string | null>(() => localStorage.getItem(LAST_LESSON_KEY))
43
42
  const [replyText, setReplyText] = useState('')
44
43
  const [replying, setReplying] = useState(false)
45
- const progressTypeIdRef = useRef<string | null>(null)
46
44
 
47
45
  const { lessons, loading, error } = useCourseLessons()
48
46
  const lessonIds = lessons.map((l) => l.id)
@@ -60,10 +58,6 @@ export function CoursesPage() {
60
58
  }
61
59
  }, [lessons, selectedId])
62
60
 
63
- useEffect(() => {
64
- getTypeIdAsync(PROGRESS_TYPE_SLUG).then((id) => { progressTypeIdRef.current = id })
65
- }, [])
66
-
67
61
  const courseMap = groupByCourse(lessons)
68
62
  const courseNames = Array.from(courseMap.keys())
69
63
  const filteredCourses = courseNames.filter((c) => c.toLowerCase().includes(search.toLowerCase()))
@@ -75,11 +69,10 @@ export function CoursesPage() {
75
69
  const handleSelectLesson = async (lessonId: string) => {
76
70
  setSelectedId(lessonId)
77
71
  localStorage.setItem(LAST_LESSON_KEY, lessonId)
78
- const typeId = progressTypeIdRef.current
79
- if (!typeId || !user?.id || !user?.account_id) return
72
+ if (!user?.id || !user?.account_id) return
80
73
  const existing = progressMap.get(lessonId)
81
74
  if (!existing || existing.status === 'not_started') {
82
- await upsert({ personId: user.id, itemId: lessonId, typeId, accountId: user.account_id, status: 'in_progress' })
75
+ await upsert({ personId: user.id, itemId: lessonId, typeSlug: PROGRESS_TYPE_SLUG, accountId: user.account_id, status: 'in_progress' })
83
76
  refetchProgress()
84
77
  sendSignal('lesson_start', 'Started a course lesson')
85
78
  }
@@ -87,9 +80,7 @@ export function CoursesPage() {
87
80
 
88
81
  const handleComplete = async () => {
89
82
  if (!selectedId || !user?.id || !user?.account_id) return
90
- const typeId = progressTypeIdRef.current
91
- if (!typeId) return
92
- await upsert({ personId: user.id, itemId: selectedId, typeId, accountId: user.account_id, status: 'completed' })
83
+ await upsert({ personId: user.id, itemId: selectedId, typeSlug: PROGRESS_TYPE_SLUG, accountId: user.account_id, status: 'completed' })
93
84
  refetchProgress()
94
85
  sendSignal('lesson_complete', 'Completed a course lesson')
95
86
  }
@@ -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
- }