@datlv-trustshop/shopify-inapp-components 0.1.9
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 +141 -0
- package/dist/components/AppList.d.ts +13 -0
- package/dist/components/AppList.js +64 -0
- package/dist/components/ArticleList.d.ts +20 -0
- package/dist/components/ArticleList.js +174 -0
- package/dist/components/ArticleSlide.d.ts +14 -0
- package/dist/components/ArticleSlide.js +151 -0
- package/dist/components/FooterBanner.d.ts +10 -0
- package/dist/components/FooterBanner.js +72 -0
- package/dist/components/GrowApps.d.ts +13 -0
- package/dist/components/GrowApps.js +213 -0
- package/dist/components/ImageLoading.d.ts +15 -0
- package/dist/components/ImageLoading.js +66 -0
- package/dist/components/PartnerList.d.ts +9 -0
- package/dist/components/PartnerList.js +102 -0
- package/dist/components/PopupBanner.d.ts +12 -0
- package/dist/components/PopupBanner.js +100 -0
- package/dist/components/TopBanner.d.ts +14 -0
- package/dist/components/TopBanner.js +31 -0
- package/dist/components/WhatsNew.d.ts +14 -0
- package/dist/components/WhatsNew.js +258 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.js +9 -0
- package/dist/components/inlineStyles.d.ts +110 -0
- package/dist/components/inlineStyles.js +114 -0
- package/dist/components/styles.d.ts +152 -0
- package/dist/components/styles.js +158 -0
- package/dist/core/adapter.d.ts +6 -0
- package/dist/core/adapter.js +301 -0
- package/dist/core/engine.d.ts +33 -0
- package/dist/core/engine.js +176 -0
- package/dist/core/fetcher.d.ts +4 -0
- package/dist/core/fetcher.js +72 -0
- package/dist/core/global-manager.d.ts +99 -0
- package/dist/core/global-manager.js +315 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/useApps.d.ts +3 -0
- package/dist/hooks/useApps.js +18 -0
- package/dist/hooks/useArticles.d.ts +11 -0
- package/dist/hooks/useArticles.js +49 -0
- package/dist/hooks/useBanner.d.ts +5 -0
- package/dist/hooks/useBanner.js +22 -0
- package/dist/hooks/useDashboard.d.ts +11 -0
- package/dist/hooks/useDashboard.js +13 -0
- package/dist/hooks/useGrowApps.d.ts +10 -0
- package/dist/hooks/useGrowApps.js +14 -0
- package/dist/hooks/useTranslations.d.ts +3 -0
- package/dist/hooks/useTranslations.js +9 -0
- package/dist/hooks/useWhatsNew.d.ts +11 -0
- package/dist/hooks/useWhatsNew.js +34 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/provider/DashboardProvider.d.ts +36 -0
- package/dist/provider/DashboardProvider.js +184 -0
- package/dist/translations/default.d.ts +2 -0
- package/dist/translations/default.js +27 -0
- package/dist/types/app.d.ts +14 -0
- package/dist/types/app.js +1 -0
- package/dist/types/article.d.ts +14 -0
- package/dist/types/article.js +1 -0
- package/dist/types/banner.d.ts +22 -0
- package/dist/types/banner.js +1 -0
- package/dist/types/dashboard.d.ts +42 -0
- package/dist/types/dashboard.js +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +6 -0
- package/dist/types/partner.d.ts +8 -0
- package/dist/types/partner.js +1 -0
- package/dist/types/product-update.d.ts +23 -0
- package/dist/types/product-update.js +1 -0
- package/dist/types/translations.d.ts +28 -0
- package/dist/types/translations.js +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { adaptDashboardData } from "./adapter";
|
|
2
|
+
import { fetchDashboard } from "./fetcher";
|
|
3
|
+
export class DashboardEngine {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.data = null;
|
|
6
|
+
this.initPromise = null;
|
|
7
|
+
this.lastFetchTime = 0;
|
|
8
|
+
this.listeners = new Set();
|
|
9
|
+
this.fetchInProgress = false;
|
|
10
|
+
this.config = {
|
|
11
|
+
cacheTime: 5 * 60 * 1000,
|
|
12
|
+
retryAttempts: 3,
|
|
13
|
+
retryDelay: 1000,
|
|
14
|
+
locale: "en",
|
|
15
|
+
...config,
|
|
16
|
+
};
|
|
17
|
+
this.currentLocale = this.config.locale || "en";
|
|
18
|
+
}
|
|
19
|
+
static getInstance(config) {
|
|
20
|
+
if (!DashboardEngine.instance) {
|
|
21
|
+
if (!config) {
|
|
22
|
+
throw new Error("DashboardEngine requires config on first initialization");
|
|
23
|
+
}
|
|
24
|
+
DashboardEngine.instance = new DashboardEngine(config);
|
|
25
|
+
}
|
|
26
|
+
return DashboardEngine.instance;
|
|
27
|
+
}
|
|
28
|
+
static resetInstance() {
|
|
29
|
+
DashboardEngine.instance = null;
|
|
30
|
+
}
|
|
31
|
+
async init(forceRefresh = false) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const cacheExpired = now - this.lastFetchTime > (this.config.cacheTime || 0);
|
|
34
|
+
if (!forceRefresh && !cacheExpired && this.data) {
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
}
|
|
37
|
+
if (this.fetchInProgress && this.initPromise) {
|
|
38
|
+
return this.initPromise;
|
|
39
|
+
}
|
|
40
|
+
if (!this.initPromise || forceRefresh || cacheExpired) {
|
|
41
|
+
this.fetchInProgress = true;
|
|
42
|
+
this.initPromise = this.loadData().finally(() => {
|
|
43
|
+
this.fetchInProgress = false;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return this.initPromise;
|
|
47
|
+
}
|
|
48
|
+
async loadData() {
|
|
49
|
+
try {
|
|
50
|
+
const rawData = await fetchDashboard(this.config);
|
|
51
|
+
this.data = adaptDashboardData(rawData);
|
|
52
|
+
this.lastFetchTime = Date.now();
|
|
53
|
+
this.notifyListeners();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error("Failed to load dashboard data:", error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
subscribe(listener) {
|
|
61
|
+
this.listeners.add(listener);
|
|
62
|
+
listener(this.data);
|
|
63
|
+
return () => {
|
|
64
|
+
this.listeners.delete(listener);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
notifyListeners() {
|
|
68
|
+
this.listeners.forEach((listener) => listener(this.data));
|
|
69
|
+
}
|
|
70
|
+
getData() {
|
|
71
|
+
return this.data;
|
|
72
|
+
}
|
|
73
|
+
getBanners(type) {
|
|
74
|
+
if (!this.data?.banners)
|
|
75
|
+
return [];
|
|
76
|
+
if (type) {
|
|
77
|
+
return this.data.banners.filter((banner) => banner.type === type);
|
|
78
|
+
}
|
|
79
|
+
return this.data.banners;
|
|
80
|
+
}
|
|
81
|
+
getActiveBanners(type) {
|
|
82
|
+
return this.getBanners(type).filter((banner) => banner.isActive !== false);
|
|
83
|
+
}
|
|
84
|
+
getApps(group) {
|
|
85
|
+
if (!this.data?.apps)
|
|
86
|
+
return [];
|
|
87
|
+
if (group) {
|
|
88
|
+
return this.data.apps[group] || [];
|
|
89
|
+
}
|
|
90
|
+
return Object.values(this.data.apps).flat();
|
|
91
|
+
}
|
|
92
|
+
getAppGroups() {
|
|
93
|
+
if (!this.data?.apps)
|
|
94
|
+
return [];
|
|
95
|
+
return Object.keys(this.data.apps);
|
|
96
|
+
}
|
|
97
|
+
getArticles(limit) {
|
|
98
|
+
if (!this.data?.articles)
|
|
99
|
+
return [];
|
|
100
|
+
if (limit && limit > 0) {
|
|
101
|
+
return this.data.articles.slice(0, limit);
|
|
102
|
+
}
|
|
103
|
+
return this.data.articles;
|
|
104
|
+
}
|
|
105
|
+
getWhatsNew(limit) {
|
|
106
|
+
if (!this.data?.whatsNew)
|
|
107
|
+
return [];
|
|
108
|
+
if (limit && limit > 0) {
|
|
109
|
+
return this.data.whatsNew.slice(0, limit);
|
|
110
|
+
}
|
|
111
|
+
return this.data.whatsNew;
|
|
112
|
+
}
|
|
113
|
+
async refresh() {
|
|
114
|
+
return this.init(true);
|
|
115
|
+
}
|
|
116
|
+
isInitialized() {
|
|
117
|
+
return this.data !== null;
|
|
118
|
+
}
|
|
119
|
+
getCacheAge() {
|
|
120
|
+
if (!this.lastFetchTime)
|
|
121
|
+
return Infinity;
|
|
122
|
+
return Date.now() - this.lastFetchTime;
|
|
123
|
+
}
|
|
124
|
+
isCacheValid() {
|
|
125
|
+
return this.getCacheAge() < (this.config.cacheTime || 0);
|
|
126
|
+
}
|
|
127
|
+
updateLocale(locale) {
|
|
128
|
+
if (this.currentLocale !== locale) {
|
|
129
|
+
this.currentLocale = locale;
|
|
130
|
+
this.config = { ...this.config, locale };
|
|
131
|
+
this.data = null;
|
|
132
|
+
this.lastFetchTime = 0;
|
|
133
|
+
this.initPromise = null;
|
|
134
|
+
return true; // Locale was changed
|
|
135
|
+
}
|
|
136
|
+
return false; // Locale was not changed
|
|
137
|
+
}
|
|
138
|
+
async setLocale(locale) {
|
|
139
|
+
if (this.currentLocale !== locale) {
|
|
140
|
+
const wasInitialized = this.isInitialized();
|
|
141
|
+
this.currentLocale = locale;
|
|
142
|
+
this.config = { ...this.config, locale };
|
|
143
|
+
// Clear data to force refresh with new locale
|
|
144
|
+
this.data = null;
|
|
145
|
+
this.lastFetchTime = 0;
|
|
146
|
+
this.initPromise = null;
|
|
147
|
+
// If engine was initialized, automatically refresh with new locale
|
|
148
|
+
if (wasInitialized) {
|
|
149
|
+
await this.init();
|
|
150
|
+
}
|
|
151
|
+
// Notify listeners about the change
|
|
152
|
+
this.notifyListeners();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
updateConfig(newConfig) {
|
|
156
|
+
const oldConfig = this.config;
|
|
157
|
+
this.config = { ...this.config, ...newConfig };
|
|
158
|
+
// If locale changed, update current locale and clear cache
|
|
159
|
+
if (newConfig.locale && newConfig.locale !== this.currentLocale) {
|
|
160
|
+
this.currentLocale = newConfig.locale;
|
|
161
|
+
this.data = null;
|
|
162
|
+
this.lastFetchTime = 0;
|
|
163
|
+
this.initPromise = null;
|
|
164
|
+
}
|
|
165
|
+
// If API URL changed, clear cache
|
|
166
|
+
if (newConfig.apiUrl && newConfig.apiUrl !== oldConfig.apiUrl) {
|
|
167
|
+
this.data = null;
|
|
168
|
+
this.lastFetchTime = 0;
|
|
169
|
+
this.initPromise = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
getCurrentLocale() {
|
|
173
|
+
return this.currentLocale;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
DashboardEngine.instance = null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const cache = new Map();
|
|
2
|
+
export async function fetchDashboard(config) {
|
|
3
|
+
// Build URL with locale query param
|
|
4
|
+
// Handle both absolute and relative URLs
|
|
5
|
+
let url;
|
|
6
|
+
try {
|
|
7
|
+
// Try as absolute URL first
|
|
8
|
+
url = new URL(config.apiUrl);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// If it fails, treat as relative URL
|
|
12
|
+
const baseUrl = typeof window !== 'undefined'
|
|
13
|
+
? window.location.origin
|
|
14
|
+
: 'http://localhost:3000';
|
|
15
|
+
url = new URL(config.apiUrl, baseUrl);
|
|
16
|
+
}
|
|
17
|
+
const locale = config.locale || 'en';
|
|
18
|
+
url.searchParams.set('locale', locale);
|
|
19
|
+
// Use URL with locale as cache key
|
|
20
|
+
const cacheKey = url.toString();
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
if (config.cacheTime && config.cacheTime > 0) {
|
|
23
|
+
const cached = cache.get(cacheKey);
|
|
24
|
+
if (cached && now - cached.timestamp < config.cacheTime) {
|
|
25
|
+
return Promise.resolve(cached.data);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const attemptFetch = async (attempt = 1) => {
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(url.toString(), {
|
|
31
|
+
method: "GET",
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
"Accept-Language": locale, // Also send locale in header
|
|
35
|
+
...config.headers,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
40
|
+
}
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
if (config.cacheTime && config.cacheTime > 0) {
|
|
43
|
+
cache.set(cacheKey, {
|
|
44
|
+
data,
|
|
45
|
+
timestamp: now,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const maxAttempts = config.retryAttempts || 3;
|
|
52
|
+
if (attempt < maxAttempts) {
|
|
53
|
+
const delay = (config.retryDelay || 1000) * attempt;
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
55
|
+
return attemptFetch(attempt + 1);
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return attemptFetch();
|
|
61
|
+
}
|
|
62
|
+
export function clearCache(url) {
|
|
63
|
+
if (url) {
|
|
64
|
+
cache.delete(url);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
cache.clear();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function getCacheSize() {
|
|
71
|
+
return cache.size;
|
|
72
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { DashboardEngine } from "./engine";
|
|
2
|
+
import { DashboardConfig, DashboardState } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* GlobalDashboardManager - Singleton manager for shared SDK state
|
|
5
|
+
*
|
|
6
|
+
* This manager ensures all DashboardProvider instances share:
|
|
7
|
+
* - The same engine instance
|
|
8
|
+
* - The same data/cache
|
|
9
|
+
* - The same locale state
|
|
10
|
+
* - Deduplicated API calls
|
|
11
|
+
*
|
|
12
|
+
* Multiple providers can mount/unmount independently while maintaining
|
|
13
|
+
* a stable, shared runtime.
|
|
14
|
+
*/
|
|
15
|
+
export declare class GlobalDashboardManager {
|
|
16
|
+
private static instance;
|
|
17
|
+
private engine;
|
|
18
|
+
private providers;
|
|
19
|
+
private stateListeners;
|
|
20
|
+
private globalState;
|
|
21
|
+
private initPromise;
|
|
22
|
+
private autoInitTimer;
|
|
23
|
+
private lastConfig;
|
|
24
|
+
private isInitializing;
|
|
25
|
+
private constructor();
|
|
26
|
+
static getInstance(): GlobalDashboardManager;
|
|
27
|
+
/**
|
|
28
|
+
* Register a provider instance
|
|
29
|
+
*/
|
|
30
|
+
registerProvider(id: string, config?: DashboardConfig): void;
|
|
31
|
+
/**
|
|
32
|
+
* Unregister a provider instance
|
|
33
|
+
*/
|
|
34
|
+
unregisterProvider(id: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Get or create the shared engine instance
|
|
37
|
+
*/
|
|
38
|
+
private getOrCreateEngine;
|
|
39
|
+
/**
|
|
40
|
+
* Update the global configuration
|
|
41
|
+
*/
|
|
42
|
+
private updateGlobalConfig;
|
|
43
|
+
/**
|
|
44
|
+
* Check if two configs are equal
|
|
45
|
+
*/
|
|
46
|
+
private configEquals;
|
|
47
|
+
/**
|
|
48
|
+
* Schedule auto-initialization
|
|
49
|
+
*/
|
|
50
|
+
private scheduleAutoInit;
|
|
51
|
+
/**
|
|
52
|
+
* Auto-initialize the engine
|
|
53
|
+
*/
|
|
54
|
+
private autoInit;
|
|
55
|
+
/**
|
|
56
|
+
* Initialize the shared engine
|
|
57
|
+
*/
|
|
58
|
+
init(config?: DashboardConfig): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Refresh the dashboard data
|
|
61
|
+
*/
|
|
62
|
+
refresh(): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Update locale globally
|
|
65
|
+
*/
|
|
66
|
+
setLocale(locale: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Get the current locale
|
|
69
|
+
*/
|
|
70
|
+
getCurrentLocale(): string;
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe to global state changes
|
|
73
|
+
*/
|
|
74
|
+
subscribe(id: string, callback: (state: DashboardState) => void): () => void;
|
|
75
|
+
/**
|
|
76
|
+
* Update and broadcast global state
|
|
77
|
+
*/
|
|
78
|
+
private updateGlobalState;
|
|
79
|
+
/**
|
|
80
|
+
* Get the current global state
|
|
81
|
+
*/
|
|
82
|
+
getState(): DashboardState;
|
|
83
|
+
/**
|
|
84
|
+
* Get the shared engine instance (if created)
|
|
85
|
+
*/
|
|
86
|
+
getEngine(): DashboardEngine | null;
|
|
87
|
+
/**
|
|
88
|
+
* Check if manager is initialized
|
|
89
|
+
*/
|
|
90
|
+
isInitialized(): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Get provider count
|
|
93
|
+
*/
|
|
94
|
+
getProviderCount(): number;
|
|
95
|
+
/**
|
|
96
|
+
* Reset the global manager (for testing)
|
|
97
|
+
*/
|
|
98
|
+
static reset(): void;
|
|
99
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { DashboardEngine } from "./engine";
|
|
2
|
+
/**
|
|
3
|
+
* GlobalDashboardManager - Singleton manager for shared SDK state
|
|
4
|
+
*
|
|
5
|
+
* This manager ensures all DashboardProvider instances share:
|
|
6
|
+
* - The same engine instance
|
|
7
|
+
* - The same data/cache
|
|
8
|
+
* - The same locale state
|
|
9
|
+
* - Deduplicated API calls
|
|
10
|
+
*
|
|
11
|
+
* Multiple providers can mount/unmount independently while maintaining
|
|
12
|
+
* a stable, shared runtime.
|
|
13
|
+
*/
|
|
14
|
+
export class GlobalDashboardManager {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.engine = null;
|
|
17
|
+
this.providers = new Map();
|
|
18
|
+
this.stateListeners = new Set();
|
|
19
|
+
this.globalState = {
|
|
20
|
+
data: null,
|
|
21
|
+
loading: false,
|
|
22
|
+
error: null,
|
|
23
|
+
lastFetch: null,
|
|
24
|
+
};
|
|
25
|
+
this.initPromise = null;
|
|
26
|
+
this.autoInitTimer = null;
|
|
27
|
+
this.lastConfig = null;
|
|
28
|
+
this.isInitializing = false;
|
|
29
|
+
// Private constructor for singleton
|
|
30
|
+
}
|
|
31
|
+
static getInstance() {
|
|
32
|
+
if (!GlobalDashboardManager.instance) {
|
|
33
|
+
GlobalDashboardManager.instance = new GlobalDashboardManager();
|
|
34
|
+
}
|
|
35
|
+
return GlobalDashboardManager.instance;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Register a provider instance
|
|
39
|
+
*/
|
|
40
|
+
registerProvider(id, config) {
|
|
41
|
+
this.providers.set(id, {
|
|
42
|
+
id,
|
|
43
|
+
mounted: true,
|
|
44
|
+
config
|
|
45
|
+
});
|
|
46
|
+
// Update config if provided and different
|
|
47
|
+
if (config && !this.configEquals(this.lastConfig, config)) {
|
|
48
|
+
this.updateGlobalConfig(config);
|
|
49
|
+
}
|
|
50
|
+
// Auto-initialize if this is the first provider
|
|
51
|
+
if (this.providers.size === 1 && config) {
|
|
52
|
+
this.scheduleAutoInit();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Unregister a provider instance
|
|
57
|
+
*/
|
|
58
|
+
unregisterProvider(id) {
|
|
59
|
+
this.providers.delete(id);
|
|
60
|
+
// Remove associated listeners
|
|
61
|
+
this.stateListeners = new Set(Array.from(this.stateListeners).filter(l => !l.id.startsWith(id)));
|
|
62
|
+
// Don't destroy engine even if no providers - maintain stable runtime
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get or create the shared engine instance
|
|
66
|
+
*/
|
|
67
|
+
getOrCreateEngine(config) {
|
|
68
|
+
if (!this.engine) {
|
|
69
|
+
if (!config && !this.lastConfig) {
|
|
70
|
+
throw new Error("GlobalDashboardManager requires config on first use");
|
|
71
|
+
}
|
|
72
|
+
const finalConfig = config || this.lastConfig;
|
|
73
|
+
this.engine = DashboardEngine.getInstance(finalConfig);
|
|
74
|
+
this.lastConfig = finalConfig;
|
|
75
|
+
// Subscribe to engine changes
|
|
76
|
+
this.engine.subscribe((data) => {
|
|
77
|
+
this.updateGlobalState({
|
|
78
|
+
data,
|
|
79
|
+
loading: false,
|
|
80
|
+
error: null,
|
|
81
|
+
lastFetch: data ? new Date() : this.globalState.lastFetch,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return this.engine;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Update the global configuration
|
|
89
|
+
*/
|
|
90
|
+
updateGlobalConfig(config) {
|
|
91
|
+
this.lastConfig = config;
|
|
92
|
+
if (this.engine) {
|
|
93
|
+
// Update existing engine config
|
|
94
|
+
this.engine.updateConfig(config);
|
|
95
|
+
// If locale changed and engine is initialized, refresh
|
|
96
|
+
if (config.locale && config.locale !== this.engine.getCurrentLocale()) {
|
|
97
|
+
const wasInitialized = this.engine.isInitialized();
|
|
98
|
+
if (wasInitialized) {
|
|
99
|
+
this.engine.setLocale(config.locale).catch(console.error);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.engine.updateLocale(config.locale);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if two configs are equal
|
|
109
|
+
*/
|
|
110
|
+
configEquals(a, b) {
|
|
111
|
+
if (!a || !b)
|
|
112
|
+
return a === b;
|
|
113
|
+
return (a.apiUrl === b.apiUrl &&
|
|
114
|
+
a.locale === b.locale &&
|
|
115
|
+
a.cacheTime === b.cacheTime &&
|
|
116
|
+
a.retryAttempts === b.retryAttempts &&
|
|
117
|
+
a.retryDelay === b.retryDelay);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Schedule auto-initialization
|
|
121
|
+
*/
|
|
122
|
+
scheduleAutoInit() {
|
|
123
|
+
if (this.autoInitTimer) {
|
|
124
|
+
clearTimeout(this.autoInitTimer);
|
|
125
|
+
}
|
|
126
|
+
// Delay slightly to allow all providers to mount
|
|
127
|
+
this.autoInitTimer = setTimeout(() => {
|
|
128
|
+
this.autoInit().catch(console.error);
|
|
129
|
+
}, 10);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Auto-initialize the engine
|
|
133
|
+
*/
|
|
134
|
+
async autoInit() {
|
|
135
|
+
if (!this.lastConfig || this.isInitializing)
|
|
136
|
+
return;
|
|
137
|
+
const engine = this.getOrCreateEngine();
|
|
138
|
+
// Only init if not already initialized
|
|
139
|
+
if (!engine.isInitialized() && !this.initPromise) {
|
|
140
|
+
await this.init();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Initialize the shared engine
|
|
145
|
+
*/
|
|
146
|
+
async init(config) {
|
|
147
|
+
if (this.initPromise && !config) {
|
|
148
|
+
return this.initPromise;
|
|
149
|
+
}
|
|
150
|
+
if (config) {
|
|
151
|
+
this.updateGlobalConfig(config);
|
|
152
|
+
}
|
|
153
|
+
this.isInitializing = true;
|
|
154
|
+
this.updateGlobalState({ ...this.globalState, loading: true, error: null });
|
|
155
|
+
this.initPromise = (async () => {
|
|
156
|
+
try {
|
|
157
|
+
const engine = this.getOrCreateEngine(config);
|
|
158
|
+
await engine.init();
|
|
159
|
+
const data = engine.getData();
|
|
160
|
+
this.updateGlobalState({
|
|
161
|
+
data,
|
|
162
|
+
loading: false,
|
|
163
|
+
error: null,
|
|
164
|
+
lastFetch: new Date(),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const err = error instanceof Error
|
|
169
|
+
? error
|
|
170
|
+
: new Error("Failed to initialize dashboard");
|
|
171
|
+
this.updateGlobalState({
|
|
172
|
+
...this.globalState,
|
|
173
|
+
loading: false,
|
|
174
|
+
error: err,
|
|
175
|
+
});
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
this.isInitializing = false;
|
|
180
|
+
}
|
|
181
|
+
})();
|
|
182
|
+
return this.initPromise;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Refresh the dashboard data
|
|
186
|
+
*/
|
|
187
|
+
async refresh() {
|
|
188
|
+
if (!this.engine) {
|
|
189
|
+
return this.init();
|
|
190
|
+
}
|
|
191
|
+
this.updateGlobalState({ ...this.globalState, loading: true, error: null });
|
|
192
|
+
try {
|
|
193
|
+
await this.engine.refresh();
|
|
194
|
+
const data = this.engine.getData();
|
|
195
|
+
this.updateGlobalState({
|
|
196
|
+
data,
|
|
197
|
+
loading: false,
|
|
198
|
+
error: null,
|
|
199
|
+
lastFetch: new Date(),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const err = error instanceof Error
|
|
204
|
+
? error
|
|
205
|
+
: new Error("Failed to refresh dashboard");
|
|
206
|
+
this.updateGlobalState({
|
|
207
|
+
...this.globalState,
|
|
208
|
+
loading: false,
|
|
209
|
+
error: err,
|
|
210
|
+
});
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Update locale globally
|
|
216
|
+
*/
|
|
217
|
+
async setLocale(locale) {
|
|
218
|
+
if (!this.engine) {
|
|
219
|
+
// Update config for future initialization
|
|
220
|
+
if (this.lastConfig) {
|
|
221
|
+
this.lastConfig = { ...this.lastConfig, locale };
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
await this.engine.setLocale(locale);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Get the current locale
|
|
229
|
+
*/
|
|
230
|
+
getCurrentLocale() {
|
|
231
|
+
if (this.engine) {
|
|
232
|
+
return this.engine.getCurrentLocale();
|
|
233
|
+
}
|
|
234
|
+
return this.lastConfig?.locale || "en";
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Subscribe to global state changes
|
|
238
|
+
*/
|
|
239
|
+
subscribe(id, callback) {
|
|
240
|
+
const listener = { id, callback };
|
|
241
|
+
this.stateListeners.add(listener);
|
|
242
|
+
// Immediately call with current state
|
|
243
|
+
callback(this.globalState);
|
|
244
|
+
// Return unsubscribe function
|
|
245
|
+
return () => {
|
|
246
|
+
this.stateListeners.delete(listener);
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Update and broadcast global state
|
|
251
|
+
*/
|
|
252
|
+
updateGlobalState(state) {
|
|
253
|
+
this.globalState = state;
|
|
254
|
+
// Notify all listeners
|
|
255
|
+
this.stateListeners.forEach(listener => {
|
|
256
|
+
try {
|
|
257
|
+
listener.callback(state);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
console.error(`Error in state listener ${listener.id}:`, error);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get the current global state
|
|
266
|
+
*/
|
|
267
|
+
getState() {
|
|
268
|
+
return this.globalState;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get the shared engine instance (if created)
|
|
272
|
+
*/
|
|
273
|
+
getEngine() {
|
|
274
|
+
return this.engine;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Check if manager is initialized
|
|
278
|
+
*/
|
|
279
|
+
isInitialized() {
|
|
280
|
+
return this.engine?.isInitialized() || false;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get provider count
|
|
284
|
+
*/
|
|
285
|
+
getProviderCount() {
|
|
286
|
+
return this.providers.size;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Reset the global manager (for testing)
|
|
290
|
+
*/
|
|
291
|
+
static reset() {
|
|
292
|
+
if (GlobalDashboardManager.instance) {
|
|
293
|
+
const manager = GlobalDashboardManager.instance;
|
|
294
|
+
// Clear timers
|
|
295
|
+
if (manager.autoInitTimer) {
|
|
296
|
+
clearTimeout(manager.autoInitTimer);
|
|
297
|
+
}
|
|
298
|
+
// Clear listeners
|
|
299
|
+
manager.stateListeners.clear();
|
|
300
|
+
manager.providers.clear();
|
|
301
|
+
// Reset state
|
|
302
|
+
manager.globalState = {
|
|
303
|
+
data: null,
|
|
304
|
+
loading: false,
|
|
305
|
+
error: null,
|
|
306
|
+
lastFetch: null,
|
|
307
|
+
};
|
|
308
|
+
manager.initPromise = null;
|
|
309
|
+
manager.lastConfig = null;
|
|
310
|
+
manager.engine = null;
|
|
311
|
+
}
|
|
312
|
+
GlobalDashboardManager.instance = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
GlobalDashboardManager.instance = null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useDashboardContext } from "../provider/DashboardProvider";
|
|
3
|
+
export function useApps(group) {
|
|
4
|
+
const { engine, state } = useDashboardContext();
|
|
5
|
+
return useMemo(() => {
|
|
6
|
+
if (!engine || !state.data)
|
|
7
|
+
return [];
|
|
8
|
+
return engine.getApps(group);
|
|
9
|
+
}, [engine, group, state.data]);
|
|
10
|
+
}
|
|
11
|
+
export function useAppGroups() {
|
|
12
|
+
const { engine, state } = useDashboardContext();
|
|
13
|
+
return useMemo(() => {
|
|
14
|
+
if (!engine || !state.data)
|
|
15
|
+
return [];
|
|
16
|
+
return engine.getAppGroups();
|
|
17
|
+
}, [engine, state.data]);
|
|
18
|
+
}
|