@fleetbase/ember-ui 0.3.14 → 0.3.16

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,6 +1,6 @@
1
- <div class="fleetbase-power-select {{@wrapperClass}}">
1
+ <div class="fleetbase-power-select {{@wrapperClass}}" {{did-update this.handleChange @value}}>
2
2
  <PowerSelect
3
- @renderInPlace={{true}}
3
+ @renderInPlace={{this.renderInPlace}}
4
4
  @searchEnabled={{true}}
5
5
  @selected={{this.selected}}
6
6
  @searchField="name"
@@ -17,5 +17,4 @@
17
17
  <span class="mr-1">{{country.emoji}}</span>
18
18
  <span>{{country.name}}</span>
19
19
  </PowerSelect>
20
- <Input @type="hidden" class="hidden" @value={{@value}} aria-label="Country Value" data-county={{@value}} {{did-insert this.listenForInputChanges}} />
21
20
  </div>
@@ -4,7 +4,6 @@ import { tracked } from '@glimmer/tracking';
4
4
  import { action } from '@ember/object';
5
5
  import { guidFor } from '@ember/object/internals';
6
6
  import { task } from 'ember-concurrency';
7
- import { later } from '@ember/runloop';
8
7
 
9
8
  export default class CountrySelectComponent extends Component {
10
9
  @service fetch;
@@ -14,6 +13,10 @@ export default class CountrySelectComponent extends Component {
14
13
  @tracked value;
15
14
  @tracked id = guidFor(this);
16
15
 
16
+ get renderInPlace() {
17
+ return this.args.renderInPlace ?? true;
18
+ }
19
+
17
20
  constructor(owner, { value = null, disabled = false }) {
18
21
  super(...arguments);
19
22
  this.disabled = disabled;
@@ -23,10 +26,12 @@ export default class CountrySelectComponent extends Component {
23
26
 
24
27
  @task *fetchCountries(value = null) {
25
28
  try {
26
- this.countries = yield this.fetch.get('lookup/countries', { columns: ['name', 'cca2', 'flag', 'emoji'] });
27
- if (value) {
28
- this.selected = this.findCountry(value);
29
- }
29
+ this.countries = yield this.fetch.get(
30
+ 'lookup/countries',
31
+ { columns: ['name', 'cca2', 'flag', 'emoji'] },
32
+ { fromCache: true, expirationInterval: 1, expirationIntervalUnit: 'week' }
33
+ );
34
+ this.selected = this.findCountry(value);
30
35
  } catch (error) {
31
36
  this.countries = [];
32
37
  }
@@ -40,15 +45,8 @@ export default class CountrySelectComponent extends Component {
40
45
  }
41
46
  }
42
47
 
43
- @action listenForInputChanges(element) {
44
- later(() => {
45
- const { value } = element;
46
-
47
- if (this.value !== value) {
48
- this.value = value;
49
- this.changed(value);
50
- }
51
- }, 100);
48
+ @action handleChange(el, [value]) {
49
+ this.selected = this.findCountry(value);
52
50
  }
53
51
 
54
52
  @action selectCountry(country) {
@@ -19,7 +19,7 @@
19
19
  <div class="{{@item.class}}">
20
20
  {{#if line.component}}
21
21
  {{component
22
- line.component
22
+ (lazy-engine-component line.component)
23
23
  item=line
24
24
  text=line.text
25
25
  label=(or line.label line.text)
@@ -108,5 +108,5 @@
108
108
  {{/if}}
109
109
 
110
110
  {{#if this.isComponent}}
111
- {{component @item.component @item}}
111
+ {{component (lazy-engine-component @item.component) @item}}
112
112
  {{/if}}
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
3
3
  import { computed, action } from '@ember/object';
4
4
  import { isBlank } from '@ember/utils';
5
5
  import { bool } from '@ember/object/computed';
6
+ import { ExtensionComponent } from '@fleetbase/ember-core/contracts';
6
7
  import isMenuItemActive from '../../../../utils/is-menu-item-active';
7
8
  import isEmptyObject from '../../../../utils/is-empty-object';
8
9
 
@@ -20,6 +21,8 @@ export default class LayoutHeaderDropdownItemComponent extends Component {
20
21
  }
21
22
 
22
23
  @computed('args.item.{component,onClick}') get isComponent() {
24
+ if (this.args.item.component instanceof ExtensionComponent) return true;
25
+
23
26
  return this.args.item && typeof this.args.item.component === 'string' && typeof this.args.item.onClick !== 'function';
24
27
  }
25
28
 
@@ -17,7 +17,7 @@
17
17
  >
18
18
  <div class="w-6">
19
19
  {{#if menuItem.iconComponent}}
20
- {{component menuItem.iconComponent options=menuItem.iconComponentOptions}}
20
+ {{component (lazy-engine-component menuItem.iconComponent) options=menuItem.iconComponentOptions}}
21
21
  {{else}}
22
22
  <FaIcon @icon={{menuItem.icon}} @prefix={{menuItem.iconPrefix}} @size="sm" />
23
23
  {{/if}}
@@ -8,6 +8,7 @@
8
8
  @fullHeight={{true}}
9
9
  @isResizable={{this.isResizable}}
10
10
  @width={{this.width}}
11
+ ...attributes
11
12
  >
12
13
  {{#if @headerComponent}}
13
14
  {{component
@@ -16,10 +16,10 @@
16
16
  </dd.Trigger>
17
17
  <dd.Content class="{{@contentClass}} locale-selector-tray-content {{if (media 'isMobile') 'is-mobile'}}">
18
18
  <div class="next-dd-menu {{@dropdownMenuClass}} {{if dd.isOpen 'is-open'}}">
19
- {{#if this.loadAvailableCountries.isRunning}}
19
+ {{#if this.language.loadAvailableCountries.isRunning}}
20
20
  <Spinner />
21
21
  {{else}}
22
- {{#each-in this.availableLocales as |key country|}}
22
+ {{#each-in this.language.availableLocales as |key country|}}
23
23
  <div class="px-1">
24
24
  <a href="javascript:;" class="next-dd-item" {{on "click" (fn this.changeLocale key)}}>
25
25
  <div class="flex flex-row items-center justify-between w-full">
@@ -2,7 +2,6 @@ import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { action } from '@ember/object';
5
- import { debug } from '@ember/debug';
6
5
  import { task } from 'ember-concurrency';
7
6
  import calculatePosition from 'ember-basic-dropdown/utils/calculate-position';
8
7
 
@@ -10,26 +9,8 @@ export default class LocaleSelectorTrayComponent extends Component {
10
9
  @service intl;
11
10
  @service fetch;
12
11
  @service media;
13
-
14
- /**
15
- * Tracks all the available locales.
16
- *
17
- * @memberof LocaleSelectorComponent
18
- */
12
+ @service language;
19
13
  @tracked locales = [];
20
-
21
- /**
22
- * All available countries data.
23
- *
24
- * @memberof LocaleSelectorComponent
25
- */
26
- @tracked countries = [];
27
-
28
- /**
29
- * The current locale in use.
30
- *
31
- * @memberof LocaleSelectorComponent
32
- */
33
14
  @tracked currentLocale;
34
15
 
35
16
  /**
@@ -41,7 +22,6 @@ export default class LocaleSelectorTrayComponent extends Component {
41
22
 
42
23
  this.locales = this.intl.locales;
43
24
  this.currentLocale = this.intl.primaryLocale;
44
- this.loadAvailableCountries.perform();
45
25
 
46
26
  // Check for locale change
47
27
  this.intl.onLocaleChanged(() => {
@@ -85,28 +65,6 @@ export default class LocaleSelectorTrayComponent extends Component {
85
65
  this.saveUserLocale.perform(selectedLocale);
86
66
  }
87
67
 
88
- /**
89
- * Loads available countries asynchronously.
90
- * @returns {void}
91
- * @memberof LocaleSelectorComponent
92
- * @method loadAvailableCountries
93
- * @instance
94
- * @task
95
- * @generator
96
- */
97
- @task *loadAvailableCountries() {
98
- try {
99
- this.countries = yield this.fetch.get(
100
- 'lookup/countries',
101
- { columns: ['name', 'cca2', 'flag', 'emoji', 'languages'] },
102
- { fromCache: true, expirationInterval: 1, expirationIntervalUnit: 'week' }
103
- );
104
- this.availableLocales = this._createAvailableLocaleMap();
105
- } catch (error) {
106
- debug(`Locale Error: ${error.message}`);
107
- }
108
- }
109
-
110
68
  /**
111
69
  * Saves the user's selected locale to the server.
112
70
  * @param {string} locale - The user's selected locale.
@@ -120,45 +78,4 @@ export default class LocaleSelectorTrayComponent extends Component {
120
78
  @task *saveUserLocale(locale) {
121
79
  yield this.fetch.post('users/locale', { locale });
122
80
  }
123
-
124
- /**
125
- * Creates a map of available locales.
126
- * @private
127
- * @returns {Object} - The map of available locales.
128
- * @memberof LocaleSelectorComponent
129
- * @method _createAvailableLocaleMap
130
- * @instance
131
- */
132
- _createAvailableLocaleMap() {
133
- const localeMap = {};
134
-
135
- for (let i = 0; i < this.locales.length; i++) {
136
- const locale = this.locales.objectAt(i);
137
-
138
- localeMap[locale] = this._findCountryDataForLocale(locale);
139
- }
140
-
141
- return localeMap;
142
- }
143
-
144
- /**
145
- * Finds country data for a given locale.
146
- * @private
147
- * @param {string} locale - The locale to find country data for.
148
- * @returns {Object|null} - The country data or null if not found.
149
- * @memberof LocaleSelectorComponent
150
- * @method _findCountryDataForLocale
151
- * @instance
152
- */
153
- _findCountryDataForLocale(locale) {
154
- const localeCountry = locale.split('-')[1];
155
- const country = this.countries.find((country) => country.cca2.toLowerCase() === localeCountry);
156
-
157
- if (country) {
158
- // get the language
159
- country.language = Object.values(country.languages)[0];
160
- }
161
-
162
- return country;
163
- }
164
81
  }
@@ -28,6 +28,7 @@ export default class OverlayComponent extends Component {
28
28
  isOpen: this.isOpen,
29
29
  isMinimized: this.isMinimized,
30
30
  isMaximized: this.isMaximized,
31
+ overlayNode: this.overlayNode,
31
32
  };
32
33
 
33
34
  @action setupComponent(element) {
@@ -26,23 +26,25 @@
26
26
  {{#if (has-block)}}
27
27
  {{yield @resource}}
28
28
  {{else}}
29
- <div class="text-sm {{@titleClass}}">{{n-a @title this.resourceName}}</div>
29
+ <div class="text-sm {{@titleClass}}">{{or @title this.resourceName @titleFallback "-"}}</div>
30
30
  {{#if @subtitle}}
31
31
  <div class="text-xs text-gray-400 dark:text-gray-500 {{@subtitleClass}}">{{n-a @subtitle}}</div>
32
32
  {{/if}}
33
33
  {{yield @resource}}
34
34
  {{/if}}
35
35
  </div>
36
- {{#if (has-block "tooltip")}}
37
- <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
38
- <InputInfo>
39
- {{yield @resource to="tooltip"}}
40
- </InputInfo>
41
- </Attach::Tooltip>
42
- {{else if @tooltipComponent}}
43
- <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
44
- {{component @tooltipComponent}}
45
- </Attach::Tooltip>
46
- {{/if}}
36
+ {{#unless @noTooltip}}
37
+ {{#if (has-block "tooltip")}}
38
+ <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
39
+ <InputInfo>
40
+ {{yield @resource to="tooltip"}}
41
+ </InputInfo>
42
+ </Attach::Tooltip>
43
+ {{else if @tooltipComponent}}
44
+ <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
45
+ {{component @tooltipComponent}}
46
+ </Attach::Tooltip>
47
+ {{/if}}
48
+ {{/unless}}
47
49
  </a>
48
50
  </div>
@@ -6,7 +6,7 @@ export default class PillComponent extends Component {
6
6
  /* eslint-disable ember/no-get */
7
7
  get resourceName() {
8
8
  const record = this.args.resource;
9
- if (!record) return 'resource';
9
+ if (!record) return null;
10
10
 
11
11
  return (
12
12
  get(record, this.args.namePath ?? 'name') ??
@@ -2,6 +2,7 @@ import { getOwner } from '@ember/application';
2
2
  import { DEBUG } from '@glimmer/env';
3
3
  import { warn } from '@ember/debug';
4
4
  import { schedule } from '@ember/runloop';
5
+ import { isArray } from '@ember/array';
5
6
  import { all } from 'rsvp';
6
7
  import requirejs from 'require';
7
8
 
@@ -166,3 +167,92 @@ export function waitForInsertedAndSized(getElOrEl, { timeoutMs = 4000 } = {}) {
166
167
  }
167
168
  });
168
169
  }
170
+
171
+ /**
172
+ * Create a DOM element with declarative options.
173
+ *
174
+ * @param {string} tag
175
+ * @param {Object} [options]
176
+ * @param {string|string[]|Node|Node[]} [children]
177
+ * @returns {HTMLElement}
178
+ */
179
+ export function createElement(tag, options = {}, children = null) {
180
+ const el = document.createElement(tag);
181
+
182
+ // ---------- Classes ----------
183
+ if (options.classNames) {
184
+ const classes = isArray(options.classNames) ? options.classNames : options.classNames.split(' ');
185
+ el.classList.add(...classes.filter(Boolean));
186
+ }
187
+
188
+ // ---------- Styles ----------
189
+ if (options.styles && typeof options.styles === 'object') {
190
+ Object.assign(el.style, options.styles);
191
+ }
192
+
193
+ // ---------- Attributes ----------
194
+ if (options.attrs && typeof options.attrs === 'object') {
195
+ for (const [key, value] of Object.entries(options.attrs)) {
196
+ if (value !== false && value != null) {
197
+ el.setAttribute(key, value === true ? '' : value);
198
+ }
199
+ }
200
+ }
201
+
202
+ // ---------- Dataset ----------
203
+ if (options.dataset && typeof options.dataset === 'object') {
204
+ for (const [key, value] of Object.entries(options.dataset)) {
205
+ el.dataset[key] = value;
206
+ }
207
+ }
208
+
209
+ // ---------- Event listeners ----------
210
+ if (options.on && typeof options.on === 'object') {
211
+ for (const [event, handler] of Object.entries(options.on)) {
212
+ if (typeof handler === 'function') {
213
+ el.addEventListener(event, handler);
214
+ }
215
+ }
216
+ }
217
+
218
+ // ---------- Text / HTML (exclusive) ----------
219
+ const hasText = options.text != null || options.innerText != null;
220
+ const hasHtml = options.html != null || options.innerHTML != null;
221
+
222
+ if (hasText && hasHtml) {
223
+ throw new Error('createElement: use either text OR html, not both.');
224
+ }
225
+
226
+ if (hasText) {
227
+ el.textContent = options.text ?? options.innerText;
228
+ } else if (hasHtml) {
229
+ el.innerHTML = options.html ?? options.innerHTML;
230
+ } else {
231
+ // ---------- Children ----------
232
+ const append = (child) => {
233
+ if (child == null) return;
234
+ if (Array.isArray(child)) return child.forEach(append);
235
+ if (child instanceof Node) el.appendChild(child);
236
+ else el.appendChild(document.createTextNode(String(child)));
237
+ };
238
+
239
+ append(children);
240
+ }
241
+
242
+ // ---------- Mount ----------
243
+ if (options.mount) {
244
+ let mountTarget = options.mount;
245
+
246
+ if (typeof mountTarget === 'string') {
247
+ mountTarget = document.querySelector(mountTarget);
248
+ }
249
+
250
+ if (mountTarget instanceof Element) {
251
+ mountTarget.appendChild(el);
252
+ } else {
253
+ console.warn('createElement: mount target not found', options.mount);
254
+ }
255
+ }
256
+
257
+ return el;
258
+ }
@@ -0,0 +1,81 @@
1
+ import { computePosition, offset, flip, shift } from '@floating-ui/dom';
2
+ import { isArray } from '@ember/array';
3
+ import { createElement } from './dom';
4
+
5
+ export class Tooltip {
6
+ mountEl;
7
+ tooltipEl;
8
+ options;
9
+ cleanupFns = [];
10
+
11
+ constructor(mountEl, options = {}) {
12
+ this.mountEl = mountEl;
13
+ this.options = options;
14
+ this.#setup();
15
+ }
16
+
17
+ #setup() {
18
+ const { text, classNames = [], placement = 'top', offset: offsetValue = 5 } = this.options;
19
+
20
+ const classes = isArray(classNames) ? classNames : String(classNames).split(' ');
21
+
22
+ this.tooltipEl = createElement('div', {
23
+ classNames: ['ui-input-info', 'text-xs', ...classes],
24
+ text,
25
+ attrs: {
26
+ role: 'tooltip',
27
+ },
28
+ styles: {
29
+ position: 'absolute',
30
+ width: 'max-content',
31
+ zIndex: 777,
32
+ opacity: 0,
33
+ pointerEvents: 'none',
34
+ transition: 'opacity 0.15s ease',
35
+ },
36
+ mount: document.body,
37
+ });
38
+
39
+ const show = async () => {
40
+ const { x, y } = await computePosition(this.mountEl, this.tooltipEl, {
41
+ placement,
42
+ middleware: [offset(offsetValue), flip(), shift({ padding: 8 })],
43
+ });
44
+
45
+ Object.assign(this.tooltipEl.style, {
46
+ left: `${x}px`,
47
+ top: `${y}px`,
48
+ opacity: 1,
49
+ });
50
+ };
51
+
52
+ const hide = () => {
53
+ this.tooltipEl.style.opacity = 0;
54
+ };
55
+
56
+ this.mountEl.addEventListener('mouseenter', show);
57
+ this.mountEl.addEventListener('mouseleave', hide);
58
+ this.mountEl.addEventListener('focus', show);
59
+ this.mountEl.addEventListener('blur', hide);
60
+
61
+ // cleanup tracking
62
+ this.cleanupFns.push(() => {
63
+ this.mountEl.removeEventListener('mouseenter', show);
64
+ this.mountEl.removeEventListener('mouseleave', hide);
65
+ this.mountEl.removeEventListener('focus', show);
66
+ this.mountEl.removeEventListener('blur', hide);
67
+ this.tooltipEl.remove();
68
+ });
69
+ }
70
+
71
+ destroy() {
72
+ this.cleanupFns.forEach((fn) => fn());
73
+ this.cleanupFns = [];
74
+ }
75
+ }
76
+
77
+ export default {
78
+ createTooltip() {
79
+ return new Tooltip(...arguments);
80
+ },
81
+ };
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/ember-ui/utils/floating';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/ember-ui",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.",
5
5
  "keywords": [
6
6
  "fleetbase-ui",