@orsetra/shared-ui 1.10.10 → 1.10.12
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.
|
@@ -78,15 +78,18 @@ export function EnvironmentPickerDialog({
|
|
|
78
78
|
<p className="text-sm text-ibm-gray-60">
|
|
79
79
|
No environments found for this project yet. Create one to continue.
|
|
80
80
|
</p>
|
|
81
|
-
{CREATE_ENV_URL
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
className="
|
|
85
|
-
leftIcon={<Plus className="h-3.5 w-3.5" />}
|
|
86
|
-
onClick={() => { window.location.href = CREATE_ENV_URL }}
|
|
81
|
+
{CREATE_ENV_URL ? (
|
|
82
|
+
<a
|
|
83
|
+
href={CREATE_ENV_URL}
|
|
84
|
+
className="inline-flex items-center gap-1.5 h-9 px-4 text-sm border border-ibm-gray-30 bg-white hover:bg-ibm-gray-10 transition-colors"
|
|
87
85
|
>
|
|
86
|
+
<Plus className="h-3.5 w-3.5" />
|
|
88
87
|
Create an environment
|
|
89
|
-
</
|
|
88
|
+
</a>
|
|
89
|
+
) : (
|
|
90
|
+
<p className="text-xs text-ibm-gray-50">
|
|
91
|
+
Go to Environments in the platform settings to create one.
|
|
92
|
+
</p>
|
|
90
93
|
)}
|
|
91
94
|
</div>
|
|
92
95
|
) : (
|
package/context/user-context.tsx
CHANGED
|
@@ -4,10 +4,11 @@ import React, { createContext, useContext } from "react"
|
|
|
4
4
|
import type { OrgInfo, Project } from "../components/layout/sidebar/data"
|
|
5
5
|
|
|
6
6
|
export interface UserProfile {
|
|
7
|
-
|
|
7
|
+
sub?: string
|
|
8
|
+
email?: string
|
|
8
9
|
preferred_username?: string
|
|
9
|
-
name?:
|
|
10
|
-
avatar?:
|
|
10
|
+
name?: string
|
|
11
|
+
avatar?: string
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
interface UserContextValue {
|
|
@@ -19,7 +20,7 @@ interface UserContextValue {
|
|
|
19
20
|
openBusinessUnitSwitcher: () => void
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const UserContext = createContext<UserContextValue | null>(null)
|
|
23
|
+
export const UserContext = createContext<UserContextValue | null>(null)
|
|
23
24
|
|
|
24
25
|
export function useUserContext(): UserContextValue {
|
|
25
26
|
const ctx = useContext(UserContext)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from "react"
|
|
3
|
+
import { useState, useEffect, useCallback, useContext } from "react"
|
|
4
|
+
import { UserContext } from "../context/user-context"
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
const POOL_SIZE
|
|
7
|
-
const DISPLAY_SIZE = 4
|
|
8
|
-
const DECAY_DAYS
|
|
6
|
+
const BASE_KEY = "sidebar:quick-access"
|
|
7
|
+
const POOL_SIZE = 20 // internal storage
|
|
8
|
+
const DISPLAY_SIZE = 4 // shown in UI
|
|
9
|
+
const DECAY_DAYS = 14 // recency half-life
|
|
9
10
|
|
|
10
11
|
export interface QuickAccessItem {
|
|
11
12
|
id: string // origin + pathname (stable, no query params)
|
|
@@ -20,14 +21,14 @@ export interface QuickAccessItem {
|
|
|
20
21
|
function score(item: QuickAccessItem): number {
|
|
21
22
|
const ageMs = Date.now() - item.lastVisit
|
|
22
23
|
const ageDays = ageMs / (1000 * 60 * 60 * 24)
|
|
23
|
-
const recency = Math.exp(-ageDays / DECAY_DAYS)
|
|
24
|
+
const recency = Math.exp(-ageDays / DECAY_DAYS)
|
|
24
25
|
return item.visitCount * 0.6 + recency * 0.4
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function readStorage(): QuickAccessItem[] {
|
|
28
|
+
function readStorage(key: string): QuickAccessItem[] {
|
|
28
29
|
if (typeof window === "undefined") return []
|
|
29
30
|
try {
|
|
30
|
-
const raw = localStorage.getItem(
|
|
31
|
+
const raw = localStorage.getItem(key)
|
|
31
32
|
if (!raw) return []
|
|
32
33
|
const parsed = JSON.parse(raw)
|
|
33
34
|
return Array.isArray(parsed)
|
|
@@ -46,8 +47,8 @@ function readStorage(): QuickAccessItem[] {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function writeStorage(items: QuickAccessItem[]): void {
|
|
50
|
-
try { localStorage.setItem(
|
|
50
|
+
function writeStorage(key: string, items: QuickAccessItem[]): void {
|
|
51
|
+
try { localStorage.setItem(key, JSON.stringify(items)) } catch {}
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/** Top DISPLAY_SIZE items from pool, sorted by score descending. */
|
|
@@ -56,24 +57,38 @@ function topItems(pool: QuickAccessItem[]): QuickAccessItem[] {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export function useQuickAccess() {
|
|
59
|
-
|
|
60
|
-
const
|
|
60
|
+
// Read sub from UserContext without throwing when outside provider
|
|
61
|
+
const ctx = useContext(UserContext)
|
|
62
|
+
const sub = ctx?.profile?.sub
|
|
63
|
+
const storageKey = sub ? `${BASE_KEY}:${sub}` : null
|
|
64
|
+
|
|
65
|
+
const [pool, setPool] = useState<QuickAccessItem[]>(() => storageKey ? readStorage(storageKey) : [])
|
|
66
|
+
const [items, setItems] = useState<QuickAccessItem[]>(() => storageKey ? topItems(readStorage(storageKey)) : [])
|
|
67
|
+
|
|
68
|
+
// Re-read when storage key changes (user logs in / switches account)
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!storageKey) { setPool([]); setItems([]); return }
|
|
71
|
+
const p = readStorage(storageKey)
|
|
72
|
+
setPool(p)
|
|
73
|
+
setItems(topItems(p))
|
|
74
|
+
}, [storageKey])
|
|
61
75
|
|
|
62
76
|
// Sync across tabs
|
|
63
77
|
useEffect(() => {
|
|
78
|
+
if (!storageKey) return
|
|
64
79
|
const onStorage = (e: StorageEvent) => {
|
|
65
|
-
if (e.key ===
|
|
66
|
-
const p = readStorage()
|
|
80
|
+
if (e.key === storageKey) {
|
|
81
|
+
const p = readStorage(storageKey)
|
|
67
82
|
setPool(p)
|
|
68
83
|
setItems(topItems(p))
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
window.addEventListener("storage", onStorage)
|
|
72
87
|
return () => window.removeEventListener("storage", onStorage)
|
|
73
|
-
}, [])
|
|
88
|
+
}, [storageKey])
|
|
74
89
|
|
|
75
90
|
const addItem = useCallback((item: { label: string; href: string; icon?: string }) => {
|
|
76
|
-
|
|
91
|
+
if (!storageKey) return
|
|
77
92
|
let id: string
|
|
78
93
|
try {
|
|
79
94
|
const u = new URL(item.href)
|
|
@@ -81,38 +96,58 @@ export function useQuickAccess() {
|
|
|
81
96
|
} catch {
|
|
82
97
|
id = item.href
|
|
83
98
|
}
|
|
99
|
+
// Normalise: strip trailing slash for consistent comparison
|
|
100
|
+
const normId = id.replace(/\/$/, "")
|
|
84
101
|
|
|
85
102
|
setPool(prev => {
|
|
86
|
-
const
|
|
103
|
+
const prefix = (ancestor: string, descendant: string) =>
|
|
104
|
+
descendant !== ancestor && descendant.startsWith(ancestor.replace(/\/$/, "") + "/")
|
|
105
|
+
|
|
106
|
+
// A deeper item for this path already exists → bump its score, skip adding the shallower one
|
|
107
|
+
const deeper = prev.find(i => prefix(normId, i.id))
|
|
108
|
+
if (deeper) {
|
|
109
|
+
const updated = { ...deeper, visitCount: deeper.visitCount + 1, lastVisit: Date.now() }
|
|
110
|
+
const next = [updated, ...prev.filter(i => i.id !== deeper.id)]
|
|
111
|
+
.sort((a, b) => score(b) - score(a))
|
|
112
|
+
.slice(0, POOL_SIZE)
|
|
113
|
+
writeStorage(storageKey, next)
|
|
114
|
+
setItems(topItems(next))
|
|
115
|
+
return next
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Remove any shallower items that are ancestors of the new path
|
|
119
|
+
const withoutAncestors = prev.filter(i => !prefix(i.id, normId))
|
|
120
|
+
|
|
121
|
+
const existing = withoutAncestors.find(i => i.id === normId)
|
|
87
122
|
const updated: QuickAccessItem = existing
|
|
88
123
|
? { ...existing, label: item.label, icon: item.icon, visitCount: existing.visitCount + 1, lastVisit: Date.now() }
|
|
89
|
-
: { id, label: item.label, href:
|
|
124
|
+
: { id: normId, label: item.label, href: normId, icon: item.icon, visitCount: 1, lastVisit: Date.now() }
|
|
90
125
|
|
|
91
|
-
|
|
92
|
-
const next = [updated, ...prev.filter(i => i.id !== id)]
|
|
126
|
+
const next = [updated, ...withoutAncestors.filter(i => i.id !== normId)]
|
|
93
127
|
.sort((a, b) => score(b) - score(a))
|
|
94
128
|
.slice(0, POOL_SIZE)
|
|
95
129
|
|
|
96
|
-
writeStorage(next)
|
|
130
|
+
writeStorage(storageKey, next)
|
|
97
131
|
setItems(topItems(next))
|
|
98
132
|
return next
|
|
99
133
|
})
|
|
100
|
-
}, [])
|
|
134
|
+
}, [storageKey])
|
|
101
135
|
|
|
102
136
|
const removeItem = useCallback((id: string) => {
|
|
137
|
+
if (!storageKey) return
|
|
103
138
|
setPool(prev => {
|
|
104
139
|
const next = prev.filter(i => i.id !== id)
|
|
105
|
-
writeStorage(next)
|
|
140
|
+
writeStorage(storageKey, next)
|
|
106
141
|
setItems(topItems(next))
|
|
107
142
|
return next
|
|
108
143
|
})
|
|
109
|
-
}, [])
|
|
144
|
+
}, [storageKey])
|
|
110
145
|
|
|
111
146
|
const clearItems = useCallback(() => {
|
|
112
147
|
setPool([])
|
|
113
148
|
setItems([])
|
|
114
|
-
try { localStorage.removeItem(
|
|
115
|
-
}, [])
|
|
149
|
+
if (storageKey) try { localStorage.removeItem(storageKey) } catch {}
|
|
150
|
+
}, [storageKey])
|
|
116
151
|
|
|
117
152
|
return { items, pool, addItem, removeItem, clearItems }
|
|
118
153
|
}
|