diff --git a/addon/components/date-time-input.hbs b/addon/components/date-time-input.hbs
index 79a04341..d2cf98c4 100644
--- a/addon/components/date-time-input.hbs
+++ b/addon/components/date-time-input.hbs
@@ -1,4 +1,4 @@
-
diff --git a/addon/components/date-time-input.js b/addon/components/date-time-input.js
index 3d99f7fd..b304ccc6 100644
--- a/addon/components/date-time-input.js
+++ b/addon/components/date-time-input.js
@@ -1,7 +1,7 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
-import { parse, format, isValid } from 'date-fns';
+import { parse, parseISO, format, isValid } from 'date-fns';
export default class DateTimeInputComponent extends Component {
@tracked timeFormat = 'HH:mm';
@@ -13,10 +13,14 @@ export default class DateTimeInputComponent extends Component {
constructor() {
super(...arguments);
- const value = this.parseValue(this.args.value);
+ this.syncValue(this.args.value);
+ }
+
+ syncValue(value) {
+ const parsedValue = this.parseValue(value);
- this.date = value ? format(value, this.dateFormat) : null;
- this.time = value ? format(value, this.timeFormat) : null;
+ this.date = parsedValue ? format(parsedValue, this.dateFormat) : null;
+ this.time = parsedValue ? format(parsedValue, this.timeFormat) : null;
}
parseValue(value) {
@@ -25,49 +29,81 @@ export default class DateTimeInputComponent extends Component {
}
if (typeof value === 'string') {
- const parsedValue = parse(value, this.dateTimeFormat, new Date());
+ const dateTimeValue = parse(value, this.dateTimeFormat, new Date());
+
+ if (isValid(dateTimeValue)) {
+ return dateTimeValue;
+ }
+
+ const isoLocalValue = value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/);
- if (isValid(parsedValue)) {
- return parsedValue;
+ if (isoLocalValue) {
+ const localDateTimeValue = parse(`${isoLocalValue[1]} ${isoLocalValue[2]}`, this.dateTimeFormat, new Date());
+
+ if (isValid(localDateTimeValue)) {
+ return localDateTimeValue;
+ }
+ }
+
+ const isoValue = parseISO(value);
+
+ if (isValid(isoValue)) {
+ return isoValue;
}
}
return null;
}
+ @action updateValue(value) {
+ this.syncValue(value);
+ }
+
/**
- * Update component value
+ * Update component value.
*
* @param {*} prop
* @param {*} { target }
* @memberof DateTimeInputComponent
*/
@action update(prop, { target }) {
- const { onUpdate } = this.args;
+ const { onUpdate, onChange } = this.args;
let { dateTimeFormat, date, time } = this;
let { value } = target;
- let dateTime, dateTimeInstance;
+ let dateTimeInstance;
+
+ this[prop] = value;
if (prop === 'time') {
- if (date) {
- dateTimeInstance = parse(`${date} ${value}`, dateTimeFormat, new Date());
- } else {
- dateTimeInstance = parse(`${value}`, this.timeFormat, new Date());
- }
+ time = value;
+ dateTimeInstance = date ? parse(`${date} ${time}`, dateTimeFormat, new Date()) : parse(`${time}`, this.timeFormat, new Date());
}
if (prop === 'date') {
- if (time) {
- dateTimeInstance = parse(`${value} ${time}`, dateTimeFormat, new Date());
- } else {
- dateTimeInstance = parse(`${value}`, this.dateFormat, new Date());
+ date = value;
+ dateTimeInstance = time ? parse(`${date} ${time}`, dateTimeFormat, new Date()) : parse(`${date}`, this.dateFormat, new Date());
+ }
+
+ if (!dateTimeInstance || !isValid(dateTimeInstance)) {
+ if (typeof onUpdate === 'function') {
+ onUpdate(null, null);
}
+
+ if (typeof onChange === 'function') {
+ onChange(null, null);
+ }
+
+ return;
}
- dateTime = format(dateTimeInstance, dateTimeFormat);
+ const dateTime = format(dateTimeInstance, dateTimeFormat);
if (typeof onUpdate === 'function') {
onUpdate(dateTimeInstance, dateTime);
}
+
+ if (typeof onChange === 'function') {
+ onChange(dateTimeInstance, dateTime);
+ }
}
}
diff --git a/addon/components/layout/header.hbs b/addon/components/layout/header.hbs
index a373a3ed..0f85afe5 100644
--- a/addon/components/layout/header.hbs
+++ b/addon/components/layout/header.hbs
@@ -18,11 +18,7 @@
Pass `@mutateMenuItems={{fn}}` to mutate items before rendering
(same contract as the previous static implementation).
--}}
-
+
{{/unless}}
{{yield}}
@@ -32,7 +28,10 @@
-
+
\ No newline at end of file
diff --git a/addon/components/layout/resource/card.js b/addon/components/layout/resource/card.js
index bc5a63d5..20b2d1e5 100644
--- a/addon/components/layout/resource/card.js
+++ b/addon/components/layout/resource/card.js
@@ -10,7 +10,7 @@ export default class LayoutResourceCardComponent extends Component {
}
get cardClass() {
- const baseClasses = 'bg-white dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow duration-200 overflow-hidden';
+ const baseClasses = 'bg-gray-50 dark:bg-gray-700/50 rounded-md border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow duration-200 overflow-hidden';
return this.args.class ? `${baseClasses} ${this.args.class}` : baseClasses;
}
diff --git a/addon/components/lazy-engine-component.hbs b/addon/components/lazy-engine-component.hbs
index ce062274..003d8f3e 100644
--- a/addon/components/lazy-engine-component.hbs
+++ b/addon/components/lazy-engine-component.hbs
@@ -5,4 +5,4 @@
{{component this.resolvedComponent params=this.params}}
{{/if}}
{{/if}}
-
\ No newline at end of file
+
diff --git a/addon/components/locale-selector-tray.hbs b/addon/components/locale-selector-tray.hbs
index d3753d0d..6ddea283 100644
--- a/addon/components/locale-selector-tray.hbs
+++ b/addon/components/locale-selector-tray.hbs
@@ -6,7 +6,7 @@
@calculatePosition={{this.calculatePosition}}
@verticalPosition={{@verticalPosition}}
@horizontalPosition={{@horizontalPosition}}
- @renderInPlace={{or @renderInPlace (not (media "isMobile"))}}
+ @renderInPlace={{this.renderInPlace}}
as |dd|
>
diff --git a/addon/components/locale-selector-tray.js b/addon/components/locale-selector-tray.js
index cda8f488..1fa192c5 100644
--- a/addon/components/locale-selector-tray.js
+++ b/addon/components/locale-selector-tray.js
@@ -13,6 +13,15 @@ export default class LocaleSelectorTrayComponent extends Component {
@tracked locales = [];
@tracked currentLocale;
+ get renderInPlace() {
+ if (this.media.isMobile) return false;
+ if (typeof this.args.renderInPlace === 'boolean') {
+ return this.args.renderInPlace;
+ }
+
+ return false;
+ }
+
/**
* Creates an instance of LocaleSelectorComponent.
* @memberof LocaleSelectorComponent
diff --git a/addon/components/metadata-viewer.hbs b/addon/components/metadata-viewer.hbs
index 7f579623..3fcb3ea5 100644
--- a/addon/components/metadata-viewer.hbs
+++ b/addon/components/metadata-viewer.hbs
@@ -26,7 +26,13 @@
{{#each-in @metadata as |key value|}}
| {{key}} |
- {{value}} |
+
+ {{#if (or (is-array @value) (is-object @value))}}
+ {{format-json @value}}
+ {{else}}
+ {{value}}
+ {{/if}}
+ |
{{/each-in}}
diff --git a/addon/components/notification-tray.hbs b/addon/components/notification-tray.hbs
index 5c5f045a..22f1d91e 100644
--- a/addon/components/notification-tray.hbs
+++ b/addon/components/notification-tray.hbs
@@ -7,7 +7,7 @@
@calculatePosition={{this.calculatePosition}}
@verticalPosition={{@verticalPosition}}
@horizontalPosition={{@horizontalPosition}}
- @renderInPlace={{or @renderInPlace (not (media "isMobile"))}}
+ @renderInPlace={{this.renderInPlace}}
as |dd|
>
diff --git a/addon/components/notification-tray.js b/addon/components/notification-tray.js
index f2c3007a..9ddb2ee0 100644
--- a/addon/components/notification-tray.js
+++ b/addon/components/notification-tray.js
@@ -45,6 +45,15 @@ export default class NotificationTrayComponent extends Component {
*/
@tracked notificationSound = new Audio('/sounds/notification-sound.mp3');
+ get renderInPlace() {
+ if (this.media.isMobile) return false;
+ if (typeof this.args.renderInPlace === 'boolean') {
+ return this.args.renderInPlace;
+ }
+
+ return false;
+ }
+
/**
* Creates an instance of the NotificationTrayComponent
*/
diff --git a/addon/components/table/cell/dropdown.hbs b/addon/components/table/cell/dropdown.hbs
index 7c940469..e6a088a8 100644
--- a/addon/components/table/cell/dropdown.hbs
+++ b/addon/components/table/cell/dropdown.hbs
@@ -6,7 +6,7 @@
@size="xs"
@horizontalPosition="left"
@calculatePosition={{this.calculatePosition}}
- @renderInPlace={{true}}
+ @renderInPlace={{this.renderInPlace}}
@onOpen={{this.onOpen}}
@onClose={{this.onClose}}
as |dd|
@@ -25,4 +25,4 @@
{{/each}}
-
\ No newline at end of file
+
diff --git a/addon/components/table/cell/dropdown.js b/addon/components/table/cell/dropdown.js
index c3bbc792..1456a620 100644
--- a/addon/components/table/cell/dropdown.js
+++ b/addon/components/table/cell/dropdown.js
@@ -7,7 +7,7 @@ export default class TableCellDropdownComponent extends Component {
defaultButtonText = 'Actions';
@computed('args.column.ddButtonText', 'defaultButtonText') get buttonText() {
- const { ddButtonText } = this.args.column;
+ const { ddButtonText } = this.args.column ?? {};
if (ddButtonText === undefined) {
return this.defaultButtonText;
@@ -20,25 +20,43 @@ export default class TableCellDropdownComponent extends Component {
return ddButtonText;
}
+ get renderInPlace() {
+ return this.args.column?.renderInPlace ?? true;
+ }
+
@action setupComponent(dropdownWrapperNode) {
const tableCellNode = this.getOwnerTableCell(dropdownWrapperNode);
- tableCellNode.style.overflow = 'visible';
+
+ if (tableCellNode) {
+ tableCellNode.style.overflow = 'visible';
+ }
+
this.tableCellNode = tableCellNode;
}
@action onOpen() {
- this.tableCellNode.style.zIndex = parseInt(this.tableCellNode.style.zIndex) + 1;
+ if (!this.tableCellNode) {
+ return;
+ }
+
+ const currentZIndex = Number.parseInt(this.tableCellNode.style.zIndex, 10) || 0;
+ this.tableCellNode.style.zIndex = currentZIndex + 1;
}
@action onClose() {
- this.tableCellNode.style.zIndex = parseInt(this.tableCellNode.style.zIndex) - 1;
+ if (!this.tableCellNode) {
+ return;
+ }
+
+ const currentZIndex = Number.parseInt(this.tableCellNode.style.zIndex, 10) || 1;
+ this.tableCellNode.style.zIndex = currentZIndex - 1;
}
@action getOwnerTableCell(dropdownWrapperNode) {
while (dropdownWrapperNode) {
dropdownWrapperNode = dropdownWrapperNode.parentNode;
- if (dropdownWrapperNode.tagName.toLowerCase() === 'td') {
+ if (dropdownWrapperNode?.tagName?.toLowerCase() === 'td') {
return dropdownWrapperNode;
}
}
@@ -57,15 +75,19 @@ export default class TableCellDropdownComponent extends Component {
}
@action calculatePosition(trigger, content) {
+ if (typeof this.args.column?.calculatePosition === 'function') {
+ return this.args.column.calculatePosition(trigger, content);
+ }
+
const triggerRect = trigger.getBoundingClientRect();
const contentRect = content?.getBoundingClientRect?.();
const contentWidth = contentRect?.width || 224;
- let style = {
+ const style = {
position: 'fixed',
marginTop: '0px',
- left: `${triggerRect.left - contentWidth - 3}px`,
- top: `${triggerRect.top}px`,
+ left: triggerRect.left - contentWidth - 3,
+ top: triggerRect.top,
};
return { style };
diff --git a/addon/components/table/cell/dropdown/action-item.hbs b/addon/components/table/cell/dropdown/action-item.hbs
index 50c58907..78ec7a91 100644
--- a/addon/components/table/cell/dropdown/action-item.hbs
+++ b/addon/components/table/cell/dropdown/action-item.hbs
@@ -1,8 +1,8 @@
{{#if this.visible}}
- {{#if @columnAction.separator}}
-
- {{else}}
- {{#if this.isVisible}}
+ {{#if this.isVisible}}
+ {{#if @columnAction.separator}}
+
+ {{else}}
{{/if}}
{{/if}}
-{{/if}}
\ No newline at end of file
+{{/if}}
diff --git a/addon/helpers/format-json.js b/addon/helpers/format-json.js
new file mode 100644
index 00000000..3bef03b8
--- /dev/null
+++ b/addon/helpers/format-json.js
@@ -0,0 +1,5 @@
+import { helper } from '@ember/component/helper';
+
+export default helper(function formatJson([jsonable]) {
+ return JSON.stringify(jsonable, null, 2);
+});
diff --git a/addon/styles/components/dashboard.css b/addon/styles/components/dashboard.css
index 3b30dcb3..2d9304d9 100644
--- a/addon/styles/components/dashboard.css
+++ b/addon/styles/components/dashboard.css
@@ -103,13 +103,13 @@
transform 180ms ease-out,
box-shadow 180ms ease-out,
border-color 180ms ease-out;
- background-color: theme('colors.white');
- border-color: theme('colors.gray.200');
+ background-color: #ffffff;
+ border-color: #e5e7eb;
}
body[data-theme='dark'] .fleet-ops-kpi-tile-pop {
- background-color: theme('colors.gray.800');
- border-color: theme('colors.gray.700');
+ background-color: #1f2937;
+ border-color: #374151;
}
.fleet-ops-kpi-tile-pop:hover {
@@ -193,16 +193,16 @@ body[data-theme='dark'] .fleet-ops-kpi-tile-pop.kpi-accent-neutral {
* - A neon-hued value number that pops against the dark theme.
* ============================================================ */
.fleet-ops-pulse-tile {
- background-color: theme('colors.gray.50');
- border-color: theme('colors.gray.200');
+ background-color: #f9fafb;
+ border-color: #e5e7eb;
transition:
transform 160ms ease-out,
box-shadow 160ms ease-out;
}
body[data-theme='dark'] .fleet-ops-pulse-tile {
- background-color: theme('colors.gray.800');
- border-color: theme('colors.gray.700');
+ background-color: #1f2937;
+ border-color: #374151;
}
.fleet-ops-pulse-tile:hover {
diff --git a/addon/styles/components/tab-navigation.css b/addon/styles/components/tab-navigation.css
index 4aed0096..34d5b906 100644
--- a/addon/styles/components/tab-navigation.css
+++ b/addon/styles/components/tab-navigation.css
@@ -1,27 +1,27 @@
:root {
- --tab-bg-primary: theme('colors.white');
- --tab-bg-secondary: theme('colors.gray.50');
- --tab-border-color: theme('colors.gray.200');
- --tab-text-primary: theme('colors.gray.900');
- --tab-text-secondary: theme('colors.gray.600');
- --tab-text-hover: theme('colors.gray.900');
- --tab-hover-bg: theme('colors.gray.100');
- --tab-active-indicator: theme('colors.blue.500');
- --tab-focus-ring: theme('colors.blue.500');
- --tab-close-hover: theme('colors.gray.200');
+ --tab-bg-primary: #ffffff;
+ --tab-bg-secondary: #f9fafb;
+ --tab-border-color: #e5e7eb;
+ --tab-text-primary: #111827;
+ --tab-text-secondary: #4b5563;
+ --tab-text-hover: #111827;
+ --tab-hover-bg: #f3f4f6;
+ --tab-active-indicator: #3b82f6;
+ --tab-focus-ring: #3b82f6;
+ --tab-close-hover: #e5e7eb;
}
body[data-theme='dark'] {
- --tab-bg-primary: theme('colors.gray.900');
- --tab-bg-secondary: theme('colors.gray.800');
- --tab-border-color: theme('colors.gray.700');
- --tab-text-primary: theme('colors.white');
- --tab-text-secondary: theme('colors.gray.400');
- --tab-text-hover: theme('colors.white');
- --tab-hover-bg: theme('colors.gray.700');
- --tab-active-indicator: theme('colors.blue.500');
- --tab-focus-ring: theme('colors.blue.500');
- --tab-close-hover: theme('colors.gray.600');
+ --tab-bg-primary: #111827;
+ --tab-bg-secondary: #1f2937;
+ --tab-border-color: #374151;
+ --tab-text-primary: #ffffff;
+ --tab-text-secondary: #9ca3af;
+ --tab-text-hover: #ffffff;
+ --tab-hover-bg: #374151;
+ --tab-active-indicator: #3b82f6;
+ --tab-focus-ring: #3b82f6;
+ --tab-close-hover: #4b5563;
}
.tab-navigation {
@@ -269,7 +269,7 @@ body[data-theme='dark'] .tab-badge {
@media (prefers-contrast: high) {
.tab-navigation[data-style='github'] .tab-item--active {
- --tab-active-indicator: theme('colors.blue.600');
+ --tab-active-indicator: #2563eb;
}
.tab-navigation .tab-item {
@@ -340,7 +340,7 @@ body[data-theme='dark'] .tab-badge {
}
body[data-theme='dark'] .tab-overflow-menu .tab-overflow-menu-item {
- color: theme('colors.gray.200');
+ color: #e5e7eb;
}
.tab-overflow-menu-icon {
diff --git a/addon/styles/layout/next.css b/addon/styles/layout/next.css
index bbb9b482..8146e527 100644
--- a/addon/styles/layout/next.css
+++ b/addon/styles/layout/next.css
@@ -1742,6 +1742,49 @@ body[data-theme='dark']
@apply bg-blue-600;
}
+#view-header-tray-items > .next-user-button {
+ display: flex;
+ align-items: center;
+ margin: 0;
+}
+
+#view-header-tray-items > .next-user-button > .ember-basic-dropdown {
+ display: flex;
+ align-items: center;
+}
+
+#view-header-tray-items > .next-user-button > .ember-basic-dropdown > .ember-basic-dropdown-trigger,
+#view-header-tray-items > .next-user-button > .ember-basic-dropdown > .ember-basic-dropdown-trigger > .local-selector-tray-trigger,
+#view-header-tray-items > .next-user-button > button {
+ display: flex;
+ width: 40px;
+ height: 31px;
+ box-sizing: border-box;
+ align-items: center;
+ justify-content: center;
+ margin: 0;
+ border: 0;
+ background: transparent;
+ padding: 0;
+ font: inherit;
+ line-height: 1;
+}
+
+#view-header-tray-items > .next-user-button > button {
+ appearance: none;
+}
+
+#view-header-tray-items > .next-user-button .next-org-button-trigger {
+ display: flex;
+ width: 40px;
+ height: 31px;
+ max-height: 31px;
+ box-sizing: border-box;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+}
+
.chat-tray-panel-container.is-mobile > .next-dd-menu,
.notification-tray-panel-container.is-mobile > .next-dd-menu,
.notification-tray-panel-container.is-mobile > .notification-tray-panel,
@@ -1939,6 +1982,11 @@ section.next-view-section {
overflow: hidden;
}
+.next-view-section-body.scrollable,
+.next-view-section-body.overflow-y-scroll {
+ overflow-y: scroll !important;
+}
+
.next-view-section-body > .next-table-wrapper {
flex: 1 1 auto;
min-width: 0;
diff --git a/app/helpers/format-json.js b/app/helpers/format-json.js
new file mode 100644
index 00000000..d1926219
--- /dev/null
+++ b/app/helpers/format-json.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-ui/helpers/format-json';
diff --git a/package.json b/package.json
index c7e3e8b6..76f95177 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@fleetbase/ember-ui",
- "version": "0.3.38",
+ "version": "0.3.39",
"description": "Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.",
"keywords": [
"fleetbase-ui",
@@ -19,7 +19,9 @@
"scripts": {
"build": "ember build --environment=production",
"lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
- "lint:css": "stylelint \"**/*.css\"",
+ "check:css-alpha": "node scripts/normalize-rgba-alpha.js --check",
+ "fix:css-alpha": "node scripts/normalize-rgba-alpha.js",
+ "lint:css": "npm run check:css-alpha && stylelint \"**/*.css\"",
"lint:css:fix": "concurrently \"npm:lint:css -- --fix\"",
"lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
"lint:hbs": "ember-template-lint .",
diff --git a/scripts/normalize-rgba-alpha.js b/scripts/normalize-rgba-alpha.js
new file mode 100644
index 00000000..b8fcd858
--- /dev/null
+++ b/scripts/normalize-rgba-alpha.js
@@ -0,0 +1,89 @@
+#!/usr/bin/env node
+/* eslint-env node */
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+const root = process.cwd();
+const checkOnly = process.argv.includes('--check');
+const cssRoots = ['addon', 'app', 'tests'].map((dir) => path.join(root, dir)).filter((dir) => fs.existsSync(dir));
+const rgbaPercentAlphaPattern = /rgba\(\s*([+-]?(?:\d*\.)?\d+)\s*,\s*([+-]?(?:\d*\.)?\d+)\s*,\s*([+-]?(?:\d*\.)?\d+)\s*,\s*([+-]?(?:\d*\.)?\d+)%\s*\)/g;
+
+function collectCssFiles(dir, files = []) {
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const fullPath = path.join(dir, entry.name);
+
+ if (entry.isDirectory()) {
+ collectCssFiles(fullPath, files);
+ continue;
+ }
+
+ if (entry.isFile() && entry.name.endsWith('.css')) {
+ files.push(fullPath);
+ }
+ }
+
+ return files;
+}
+
+function formatAlpha(percent) {
+ const alpha = Number(percent) / 100;
+ return Number.isInteger(alpha) ? String(alpha) : String(Number(alpha.toFixed(4)));
+}
+
+function lineForIndex(source, index) {
+ return source.slice(0, index).split('\n').length;
+}
+
+const changes = [];
+
+for (const cssRoot of cssRoots) {
+ for (const filePath of collectCssFiles(cssRoot)) {
+ const source = fs.readFileSync(filePath, 'utf8');
+ const matches = [];
+
+ const next = source.replace(rgbaPercentAlphaPattern, (match, red, green, blue, alphaPercent, offset) => {
+ const replacement = `rgba(${red}, ${green}, ${blue}, ${formatAlpha(alphaPercent)})`;
+ matches.push({
+ line: lineForIndex(source, offset),
+ before: match,
+ after: replacement,
+ });
+ return replacement;
+ });
+
+ if (matches.length === 0) {
+ continue;
+ }
+
+ changes.push({
+ filePath,
+ matches,
+ });
+
+ if (!checkOnly) {
+ fs.writeFileSync(filePath, next);
+ }
+ }
+}
+
+if (changes.length === 0) {
+ console.log('No comma-form rgba() percent-alpha values found.');
+ process.exit(0);
+}
+
+for (const change of changes) {
+ const relativePath = path.relative(root, change.filePath);
+
+ for (const match of change.matches) {
+ console.log(`${relativePath}:${match.line} ${match.before} -> ${match.after}`);
+ }
+}
+
+if (checkOnly) {
+ console.error('\nRun `npm run fix:css-alpha` to normalize these values before production builds.');
+ process.exit(1);
+}
+
+console.log(`\nNormalized ${changes.reduce((total, change) => total + change.matches.length, 0)} rgba() alpha value(s).`);
diff --git a/tests/integration/components/date-time-input-test.js b/tests/integration/components/date-time-input-test.js
index c93a0013..d0bf0ffb 100644
--- a/tests/integration/components/date-time-input-test.js
+++ b/tests/integration/components/date-time-input-test.js
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
-import { fillIn, render } from '@ember/test-helpers';
+import { fillIn, render, settled } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | date-time-input', function (hooks) {
@@ -30,6 +30,15 @@ module('Integration | Component | date-time-input', function (hooks) {
assert.dom('[aria-label="Time Input"]').hasValue('18:47');
});
+ test('it renders an iso date-time string value', async function (assert) {
+ this.set('value', '2026-07-03T15:00:00+08:00');
+
+ await render(hbs``);
+
+ assert.dom('[aria-label="Date Input"]').hasValue('2026-07-03');
+ assert.dom('[aria-label="Time Input"]').hasValue('15:00');
+ });
+
test('it renders empty inputs for an invalid date-time string value', async function (assert) {
this.set('value', 'not a date');
@@ -68,4 +77,46 @@ module('Integration | Component | date-time-input', function (hooks) {
await render(hbs``);
await fillIn('[aria-label="Time Input"]', '19:12');
});
+
+ test('it keeps the latest date when changing time after date', async function (assert) {
+ assert.expect(2);
+
+ const changes = [];
+ this.set('value', '2026-06-18 18:47');
+ this.set('onUpdate', (_dateTimeInstance, dateTime) => {
+ changes.push(dateTime);
+ });
+
+ await render(hbs``);
+ await fillIn('[aria-label="Date Input"]', '2026-06-19');
+ await fillIn('[aria-label="Time Input"]', '19:12');
+
+ assert.strictEqual(changes[0], '2026-06-19 18:47');
+ assert.strictEqual(changes[1], '2026-06-19 19:12');
+ });
+
+ test('it calls onChange for legacy callers', async function (assert) {
+ assert.expect(2);
+
+ this.set('value', '2026-06-18 18:47');
+ this.set('onChange', (dateTimeInstance, dateTime) => {
+ assert.true(dateTimeInstance instanceof Date);
+ assert.strictEqual(dateTime, '2026-06-18 19:12');
+ });
+
+ await render(hbs``);
+ await fillIn('[aria-label="Time Input"]', '19:12');
+ });
+
+ test('it syncs when the external value changes', async function (assert) {
+ this.set('value', '2026-06-18 18:47');
+
+ await render(hbs``);
+
+ this.set('value', '2026-07-03T15:00:00+08:00');
+ await settled();
+
+ assert.dom('[aria-label="Date Input"]').hasValue('2026-07-03');
+ assert.dom('[aria-label="Time Input"]').hasValue('15:00');
+ });
});
diff --git a/tests/integration/components/table/cell/dropdown-test.js b/tests/integration/components/table/cell/dropdown-test.js
index 23548b65..f6957eb1 100644
--- a/tests/integration/components/table/cell/dropdown-test.js
+++ b/tests/integration/components/table/cell/dropdown-test.js
@@ -23,4 +23,49 @@ module('Integration | Component | table/cell/dropdown', function (hooks) {
assert.dom(this.element).hasText('template block text');
});
+
+ test('it supports custom and default dropdown positioning', function (assert) {
+ const ComponentClass = this.owner.factoryFor('component:table/cell/dropdown').class;
+ const trigger = {
+ getBoundingClientRect() {
+ return { left: 500, top: 120 };
+ },
+ };
+ const content = {
+ getBoundingClientRect() {
+ return { width: 220 };
+ },
+ };
+ const defaultComponent = new ComponentClass(this.owner, { column: {} });
+ const customPosition = { style: { left: 10, top: 20 } };
+ const customComponent = new ComponentClass(this.owner, {
+ column: {
+ calculatePosition(receivedTrigger, receivedContent) {
+ assert.strictEqual(receivedTrigger, trigger, 'custom calculator receives trigger');
+ assert.strictEqual(receivedContent, content, 'custom calculator receives content');
+
+ return customPosition;
+ },
+ },
+ });
+
+ assert.deepEqual(defaultComponent.calculatePosition(trigger, content), {
+ style: {
+ position: 'fixed',
+ marginTop: '0px',
+ left: 277,
+ top: 120,
+ },
+ });
+ assert.strictEqual(customComponent.calculatePosition(trigger, content), customPosition, 'custom calculator result is used');
+ });
+
+ test('it defaults renderInPlace to true and preserves explicit false', function (assert) {
+ const ComponentClass = this.owner.factoryFor('component:table/cell/dropdown').class;
+ const defaultComponent = new ComponentClass(this.owner, { column: {} });
+ const disabledInPlaceComponent = new ComponentClass(this.owner, { column: { renderInPlace: false } });
+
+ assert.true(defaultComponent.renderInPlace, 'renderInPlace defaults to true');
+ assert.false(disabledInPlaceComponent.renderInPlace, 'explicit renderInPlace false is preserved');
+ });
});
diff --git a/tests/integration/components/table/cell/dropdown/action-item-test.js b/tests/integration/components/table/cell/dropdown/action-item-test.js
index 19598a86..1ec08eb4 100644
--- a/tests/integration/components/table/cell/dropdown/action-item-test.js
+++ b/tests/integration/components/table/cell/dropdown/action-item-test.js
@@ -23,4 +23,26 @@ module('Integration | Component | table/cell/dropdown/action-item', function (ho
assert.dom().hasText('template block text');
});
+
+ test('separator obeys isVisible', async function (assert) {
+ this.set('row', { id: 'row_1', shouldShowSeparator: true });
+ this.set('visibleSeparator', {
+ separator: true,
+ isVisible(row) {
+ return row.shouldShowSeparator;
+ },
+ });
+ this.set('hiddenSeparator', {
+ separator: true,
+ isVisible() {
+ return false;
+ },
+ });
+
+ await render(hbs``);
+ assert.dom('.next-dd-menu-seperator').exists('visible separator renders');
+
+ await render(hbs``);
+ assert.dom('.next-dd-menu-seperator').doesNotExist('hidden separator does not render');
+ });
});
diff --git a/tests/integration/helpers/format-json-test.js b/tests/integration/helpers/format-json-test.js
new file mode 100644
index 00000000..5c2c087a
--- /dev/null
+++ b/tests/integration/helpers/format-json-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Helper | format-json', function (hooks) {
+ setupRenderingTest(hooks);
+
+ // TODO: Replace this with your real tests.
+ test('it renders', async function (assert) {
+ this.set('inputValue', '1234');
+
+ await render(hbs`{{format-json this.inputValue}}`);
+
+ assert.dom().hasText('1234');
+ });
+});