@fleetbase/ember-core 0.3.18 → 0.3.20

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.
@@ -61,8 +61,8 @@ export default class FleetbaseAuthenticator extends Base {
61
61
  * @param {boolean} remember
62
62
  * @param {string} path
63
63
  */
64
- authenticate(credentials = {}, remember = false, path = 'auth/login') {
65
- return this.fetch.post(path, { ...credentials, remember }).then((response) => {
64
+ authenticate(credentials = {}, remember = false, path = 'auth/login', options = {}) {
65
+ return this.fetch.post(path, { ...credentials, remember }, options).then((response) => {
66
66
  if (response.errors) {
67
67
  const errorMessage = getWithDefault(response.errors, '0', 'Authentication failed!');
68
68
  const errorCode = getWithDefault(response, 'code');
@@ -64,7 +64,7 @@ export default class SubjectCustomFields {
64
64
  this.setFieldValue(value, customField);
65
65
 
66
66
  const fieldId = typeof customField === 'string' ? customField : customField?.id;
67
- const valueType = typeof customField === 'string' ? null : customField?.valueType ?? customField?.value_type ?? null;
67
+ const valueType = typeof customField === 'string' ? null : (customField?.valueType ?? customField?.value_type ?? null);
68
68
  if (!fieldId || !resource) return;
69
69
 
70
70
  let rec = this.#getLocalValueRecord(resource, fieldId);
@@ -91,7 +91,7 @@ export default class SubjectCustomFields {
91
91
  let { value_type } = entry;
92
92
  if (!value_type) {
93
93
  const cf = this.store.peekRecord?.('custom-field', fieldId);
94
- value_type = cf ? cf.valueType ?? cf.value_type ?? null : null;
94
+ value_type = cf ? (cf.valueType ?? cf.value_type ?? null) : null;
95
95
  }
96
96
  next[fieldId] = { value, value_type };
97
97
  }
@@ -253,7 +253,7 @@ export default class SubjectCustomFields {
253
253
  }
254
254
 
255
255
  // Normalize to array
256
- const existing = isArray(existingMany) ? existingMany : existingMany?.toArray?.() ?? [];
256
+ const existing = isArray(existingMany) ? existingMany : (existingMany?.toArray?.() ?? []);
257
257
 
258
258
  const byFieldId = new Map();
259
259
  for (let i = 0; i < existing.length; i++) {
@@ -9,6 +9,7 @@ export default class ChatService extends Service.extend(Evented) {
9
9
  @service store;
10
10
  @service currentUser;
11
11
  @service appCache;
12
+ @service fetch;
12
13
  @tracked channels = [];
13
14
  @tracked openChannels = [];
14
15
 
@@ -72,7 +73,28 @@ export default class ChatService extends Service.extend(Evented) {
72
73
  return this.openChannels;
73
74
  }
74
75
 
75
- createChatChannel(name) {
76
+ createChatChannel(name, participants = []) {
77
+ return this.fetch
78
+ .post(
79
+ 'chat-channels',
80
+ {
81
+ chatChannel: {
82
+ name,
83
+ participants,
84
+ },
85
+ },
86
+ {
87
+ normalizeToEmberData: true,
88
+ normalizeModelType: 'chatChannel',
89
+ }
90
+ )
91
+ .then((chatChannelRecord) => {
92
+ this.trigger('chat.created', chatChannelRecord);
93
+ return chatChannelRecord;
94
+ });
95
+ }
96
+
97
+ createEmptyChatChannel(name) {
76
98
  const chatChannelRecord = this.store.createRecord('chat-channel', { name });
77
99
  return chatChannelRecord.save().finally(() => {
78
100
  this.trigger('chat.created', chatChannelRecord);
@@ -3,12 +3,12 @@ import Evented from '@ember/object/evented';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { tracked } from '@glimmer/tracking';
5
5
  import { dasherize } from '@ember/string';
6
- import { computed, get } from '@ember/object';
6
+ import { get } from '@ember/object';
7
7
  import { isBlank } from '@ember/utils';
8
8
  import { alias } from '@ember/object/computed';
9
9
  import { storageFor } from 'ember-local-storage';
10
10
  import { debug } from '@ember/debug';
11
- import lookupUserIp from '../utils/lookup-user-ip';
11
+ import lookupUserIp, { getBrowserTimezone } from '../utils/lookup-user-ip';
12
12
 
13
13
  /**
14
14
  * CurrentUserService
@@ -60,8 +60,29 @@ export default class CurrentUserService extends Service.extend(Evented) {
60
60
  @alias('userSnapshot.role_name') roleName;
61
61
  @alias('userSnapshot.role') role;
62
62
 
63
- @computed('id') get optionsPrefix() {
64
- return `${this.id}:`;
63
+ get authenticatedOptionOwnerId() {
64
+ const authenticatedUserId = this.session?.data?.authenticated?.user;
65
+
66
+ if (this.session?.isAuthenticated && authenticatedUserId) {
67
+ return authenticatedUserId;
68
+ }
69
+
70
+ try {
71
+ const localStorageSession = JSON.parse(window.localStorage.getItem('ember_simple_auth-session'));
72
+ const authenticatedSession = localStorageSession?.authenticated;
73
+
74
+ if (authenticatedSession?.token && authenticatedSession?.user) {
75
+ return authenticatedSession.user;
76
+ }
77
+ } catch (error) {
78
+ // Ignore malformed session storage and fall back to the current snapshot.
79
+ }
80
+
81
+ return null;
82
+ }
83
+
84
+ get optionsPrefix() {
85
+ return `${this.authenticatedOptionOwnerId || this.id || 'anon'}:`;
65
86
  }
66
87
 
67
88
  get latitude() {
@@ -84,12 +105,16 @@ export default class CurrentUserService extends Service.extend(Evented) {
84
105
  return this.whois('country_code');
85
106
  }
86
107
 
108
+ get timezone() {
109
+ return this.whois('timezone') || getBrowserTimezone();
110
+ }
111
+
87
112
  async load() {
88
113
  if (this.session.isAuthenticated) {
89
114
  const user = await this.store.findRecord('user', 'me');
90
115
 
91
116
  // set user
92
- this.setUser(user);
117
+ await this.setUser(user);
93
118
 
94
119
  // Load preferences
95
120
  await this.loadPreferences();
@@ -110,7 +135,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
110
135
  const user = await this.store.queryRecord('user', { me: true });
111
136
 
112
137
  // set user
113
- this.setUser(user);
138
+ await this.setUser(user);
114
139
 
115
140
  // Load user whois data
116
141
  await this.loadWhois();
@@ -180,7 +205,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
180
205
  const fallback = {
181
206
  city: null,
182
207
  country_code: null,
183
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
208
+ timezone: getBrowserTimezone(),
184
209
  _source: 'fallback',
185
210
  };
186
211
 
@@ -344,6 +369,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
344
369
  // Set current user
345
370
  this.set('user', user);
346
371
  this.set('userSnapshot', snapshot);
372
+ this.theme.syncThemeFromCurrentUser();
347
373
 
348
374
  // Resolve the organization for event payload
349
375
  const organization = this.store.peekRecord('company', user.get('company_uuid'));
@@ -3,7 +3,7 @@ import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { get, set } from '@ember/object';
5
5
  import { isBlank } from '@ember/utils';
6
- import { dasherize } from '@ember/string';
6
+ import { dasherize, underscore } from '@ember/string';
7
7
  import { isArray } from '@ember/array';
8
8
  import { singularize, pluralize } from 'ember-inflector';
9
9
  import { task } from 'ember-concurrency';
@@ -186,11 +186,16 @@ export default class FetchService extends Service {
186
186
  }
187
187
 
188
188
  const type = dasherize(singularize(modelType));
189
+ const pluralizedModelType = pluralize(underscore(modelType));
189
190
 
190
191
  if (isArray(payload)) {
191
192
  return payload.map((instance) => this.store.push(this.store.normalize(type, instance)));
192
193
  }
193
194
 
195
+ if (isArray(payload[pluralizedModelType])) {
196
+ return payload[pluralizedModelType].map((instance) => this.store.push(this.store.normalize(type, instance)));
197
+ }
198
+
194
199
  if (isArray(payload[modelType])) {
195
200
  return payload[modelType].map((instance) => this.store.push(this.store.normalize(type, instance)));
196
201
  }
@@ -2,7 +2,6 @@ import Service from '@ember/service';
2
2
  import Evented from '@ember/object/evented';
3
3
  import { tracked } from '@glimmer/tracking';
4
4
  import { inject as service } from '@ember/service';
5
- import { computed } from '@ember/object';
6
5
  import { dasherize } from '@ember/string';
7
6
  import { isArray } from '@ember/array';
8
7
  import { getOwner } from '@ember/application';
@@ -35,8 +34,8 @@ export default class ThemeService extends Service.extend(Evented) {
35
34
  *
36
35
  * @var {String}
37
36
  */
38
- @computed('currentTheme', 'initialTheme') get activeTheme() {
39
- const userSetTheme = this.currentUser.getOption(`theme`);
37
+ get activeTheme() {
38
+ const userSetTheme = this.currentUser.getOption('theme');
40
39
 
41
40
  if (userSetTheme) {
42
41
  return userSetTheme;
@@ -67,7 +66,7 @@ export default class ThemeService extends Service.extend(Evented) {
67
66
  *
68
67
  * @var {String}
69
68
  */
70
- @tracked currentTheme = 'dark';
69
+ @tracked currentTheme = this.currentUser.getOption('theme', 'dark');
71
70
 
72
71
  /**
73
72
  * The initially set theme
@@ -131,7 +130,7 @@ export default class ThemeService extends Service.extend(Evented) {
131
130
  */
132
131
  initialize(options = {}) {
133
132
  this.initialTheme = options?.theme;
134
- this.setTheme(this.activeTheme);
133
+ this.applyTheme(this.activeTheme, { persist: false });
135
134
  this.setEnvironment();
136
135
  this.resetScroll();
137
136
  this.setRoutebodyClassNames(options.bodyClassNames && isArray(options.bodyClassNames) ? options.bodyClassNames : []);
@@ -203,10 +202,32 @@ export default class ThemeService extends Service.extend(Evented) {
203
202
  * @void
204
203
  */
205
204
  setTheme(theme = 'light') {
205
+ this.applyTheme(theme);
206
+ }
207
+
208
+ /**
209
+ * Synchronize the active theme from the current user-scoped preference.
210
+ *
211
+ * @void
212
+ */
213
+ syncThemeFromCurrentUser() {
214
+ this.applyTheme(this.activeTheme, { persist: false });
215
+ }
216
+
217
+ /**
218
+ * Apply a theme consistently to service state and document body.
219
+ *
220
+ * @void
221
+ */
222
+ applyTheme(theme = 'light', options = {}) {
223
+ const persist = options.persist !== false;
224
+
206
225
  debug(`Theme was changed to: ${theme}`);
207
- document.body.classList.remove(`${this.currentTheme}-theme`);
226
+ document.body.classList.remove('light-theme', 'dark-theme');
208
227
  document.body.classList.add(`${theme}-theme`);
209
- this.currentUser.setOption('theme', theme);
228
+ if (persist) {
229
+ this.currentUser.setOption('theme', theme);
230
+ }
210
231
  this.activeTheme = theme;
211
232
  this.trigger('theme.changed', theme);
212
233
  }
@@ -11,6 +11,8 @@ export default async function loadInstalledExtensions(additionalCoreEngines = []
11
11
  '@fleetbase/iam-engine',
12
12
  '@fleetbase/ledger-engine',
13
13
  '@fleetbase/pallet-engine',
14
+ '@fleetbase/vroom-engine',
15
+ '@fleetbase/valhalla-engine',
14
16
  ...additionalCoreEngines,
15
17
  ];
16
18
  const INDEXED_ENGINES = await loadExtensions();
@@ -59,13 +59,14 @@ export default async function lookupUserIp(options = {}) {
59
59
 
60
60
  const data = await response.json();
61
61
  const normalized = api.normalize(data);
62
+ const normalizedWithTimezone = ensureWhoisTimezone(normalized);
62
63
 
63
64
  // Cache the result if enabled
64
65
  if (cache) {
65
- cacheWhois(normalized);
66
+ cacheWhois(normalizedWithTimezone);
66
67
  }
67
68
 
68
- return normalized;
69
+ return normalizedWithTimezone;
69
70
  } catch (error) {
70
71
  console.warn(`[lookupUserIp] ${api.url} failed:`, error.message);
71
72
  // Continue to next API
@@ -92,7 +93,7 @@ function normalizeGeoIPLookup(data) {
92
93
  latitude: data.latitude,
93
94
  longitude: data.longitude,
94
95
  postal_code: data.postal_code,
95
- timezone: data.timezone_name,
96
+ timezone: data.timezone_name || getBrowserTimezone(),
96
97
  currency: {
97
98
  code: data.currency_code,
98
99
  name: data.currency_name,
@@ -128,7 +129,7 @@ function normalizeIPApi(data) {
128
129
  latitude: data.latitude,
129
130
  longitude: data.longitude,
130
131
  postal_code: data.postal,
131
- timezone: data.timezone,
132
+ timezone: data.timezone || getBrowserTimezone(),
132
133
  currency: {
133
134
  code: data.currency,
134
135
  name: data.currency_name,
@@ -163,7 +164,12 @@ function getCachedWhois() {
163
164
  return null;
164
165
  }
165
166
 
166
- return data;
167
+ const normalized = ensureWhoisTimezone(data);
168
+ if (normalized.timezone !== data.timezone) {
169
+ cacheWhois(normalized);
170
+ }
171
+
172
+ return normalized;
167
173
  } catch (error) {
168
174
  console.error('[getCachedWhois] Error reading cache:', error);
169
175
  return null;
@@ -187,7 +193,7 @@ function cacheWhois(data) {
187
193
  function getFallbackWhois() {
188
194
  // Try to get browser language and timezone as fallback
189
195
  const browserLang = navigator.language || 'en-US';
190
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
196
+ const timezone = getBrowserTimezone();
191
197
 
192
198
  return {
193
199
  ip: null,
@@ -219,3 +225,20 @@ function getFallbackWhois() {
219
225
  _timestamp: Date.now(),
220
226
  };
221
227
  }
228
+
229
+ export function getBrowserTimezone() {
230
+ try {
231
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
232
+ } catch (error) {
233
+ return null;
234
+ }
235
+ }
236
+
237
+ export function ensureWhoisTimezone(whois = {}) {
238
+ whois = whois || {};
239
+
240
+ return {
241
+ ...whois,
242
+ timezone: whois.timezone || getBrowserTimezone(),
243
+ };
244
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/ember-core",
3
- "version": "0.3.18",
3
+ "version": "0.3.20",
4
4
  "description": "Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.",
5
5
  "keywords": [
6
6
  "fleetbase-core",
@@ -0,0 +1,2 @@
1
+ allowBuilds:
2
+ core-js: false