From 336f4c8960ec94b4a16d18d9e28c6bebd240f2f3 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Thu, 25 Jan 2024 17:42:13 +0800
Subject: [PATCH 1/5] bumped version preparing for next release with
 improvements

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 5765337..e1d10ad 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@fleetbase/ember-ui",
-    "version": "0.2.9",
+    "version": "0.2.10",
     "description": "Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.",
     "keywords": [
         "fleetbase-ui",

From 8ed728464836b42447e91923fc8b3ece84808a65 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Thu, 25 Jan 2024 19:01:29 +0800
Subject: [PATCH 2/5] few minor patches to item

---
 .../components/layout/header/dropdown/item.hbs | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/addon/components/layout/header/dropdown/item.hbs b/addon/components/layout/header/dropdown/item.hbs
index fe737b9..2481d44 100644
--- a/addon/components/layout/header/dropdown/item.hbs
+++ b/addon/components/layout/header/dropdown/item.hbs
@@ -1,9 +1,9 @@
 {{#if this.isTextOnly}}
-    <div class="px-1 flex flex-row {{@item.wrapperClass}}">
+    <div class={{if @item.overwriteWrapperClass @item.wrapperClass (concat @item.wrapperClass " px-1 flex flex-row")}}>
         {{#if @item.icon}}
-            <FaIcon @icon={{@item.icon}} @size={{or @item.iconSize "sm"}} class="mr-2" />
+            <FaIcon @icon={{@item.icon}} @size={{or @item.iconSize "sm"}} class={{or @item.iconClass "mr-2"}} />
         {{/if}}
-        <div class="inline-flex flex-col">
+        <div class="inline-flex flex-col {{@item.inlineClass}}">
             {{#if (is-array @item.text)}}
                 {{#each @item.text as |line|}}
                     <div class="{{@item.class}}">{{line}}</div>
@@ -16,10 +16,10 @@
 {{/if}}
 
 {{#if this.isLink}}
-    <div class="px-1 {{@item.wrapperClass}}">
+    <div class={{if @item.overwriteWrapperClass @item.wrapperClass (concat @item.wrapperClass " px-1")}}>
         <LinkTo @route={{@item.route}} disabled={{@item.disabled}} class="next-dd-item {{@item.class}}">
             {{#if @item.icon}}
-                <FaIcon @icon={{@item.icon}} @size={{or @item.iconSize "sm"}} class="mr-2" />
+                <FaIcon @icon={{@item.icon}} @size={{or @item.iconSize "sm"}} class={{or @item.iconClass "mr-2"}} />
             {{/if}}
             <span>{{@item.text}}</span>
         </LinkTo>
@@ -27,10 +27,10 @@
 {{/if}}
 
 {{#if this.isAnchor}}
-    <div class="px-1 {{@item.wrapperClass}}">
+    <div class={{if @item.overwriteWrapperClass @item.wrapperClass (concat @item.wrapperClass " px-1")}}>
         <a href={{@item.href}} class="next-dd-item {{if @item.disabled 'disabled'}} {{@item.class}}" target={{@item.target}} disabled={{@item.disabled}} {{on "click" (fn @onAction @item.action @item.params)}}>
             {{#if @item.icon}}
-                <FaIcon @icon={{@item.icon}} @size={{or @item.iconSize "sm"}} class="mr-2" />
+                <FaIcon @icon={{@item.icon}} @size={{or @item.iconSize "sm"}} class={{or @item.iconClass "mr-2"}} />
             {{/if}}
             <span>{{@item.text}}</span>
         </a>
@@ -38,10 +38,10 @@
 {{/if}}
 
 {{#if this.isInteractive}}
-    <div class="px-1 {{@item.wrapperClass}}">
+    <div class={{if @item.overwriteWrapperClass @item.wrapperClass (concat @item.wrapperClass " px-1")}}>
         <a href="javascript:;" class="next-dd-item {{if this.active 'active'}}" disabled={{@item.disabled}} {{on "click" @item.onClick}} ...attributes>
             {{#if @item.icon}}
-                <FaIcon @icon={{@item.icon}} @size={{or @item.iconSize "sm"}} class="mr-2" />
+                <FaIcon @icon={{@item.icon}} @size={{or @item.iconSize "sm"}} class={{or @item.iconClass "mr-2"}} />
             {{/if}}
             <span>{{@item.text}}</span>
         </a>

From 1c29773be451241d3d67fe6a6ceb63ca58d46691 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Thu, 1 Feb 2024 15:02:27 +0800
Subject: [PATCH 3/5] added comment thread component for subjects

---
 addon/components/comment-thread.hbs           | 15 +++++
 addon/components/comment-thread.js            | 60 ++++++++++++++++++
 addon/components/comment-thread/comment.hbs   | 46 ++++++++++++++
 addon/components/comment-thread/comment.js    | 62 +++++++++++++++++++
 addon/styles/layout/next.css                  | 10 +++
 app/components/comment-thread.js              |  1 +
 app/components/comment-thread/comment.js      |  1 +
 .../components/comment-thread-test.js         | 26 ++++++++
 .../components/comment-thread/comment-test.js | 26 ++++++++
 9 files changed, 247 insertions(+)
 create mode 100644 addon/components/comment-thread.hbs
 create mode 100644 addon/components/comment-thread.js
 create mode 100644 addon/components/comment-thread/comment.hbs
 create mode 100644 addon/components/comment-thread/comment.js
 create mode 100644 app/components/comment-thread.js
 create mode 100644 app/components/comment-thread/comment.js
 create mode 100644 tests/integration/components/comment-thread-test.js
 create mode 100644 tests/integration/components/comment-thread/comment-test.js

diff --git a/addon/components/comment-thread.hbs b/addon/components/comment-thread.hbs
new file mode 100644
index 0000000..991b7e8
--- /dev/null
+++ b/addon/components/comment-thread.hbs
@@ -0,0 +1,15 @@
+<div class="flex flex-col mb-4" ...attributes>
+    <Textarea @value={{this.input}} class="form-input w-full" placeholder={{t "component.comment-thread.comment-input-placeholder"}} rows={{3}} disabled={{not this.publishComment.isIdle}} />
+    <div class="flex flex-row items-center justify-end mt-2">
+        <Button @type="primary" @buttonType="button" @icon="paper-plane" @text={{t "component.comment-thread.publish-comment-button-text"}} @onClick={{perform this.publishComment}} @disabled={{or (not this.publishComment.isIdle) (not this.input)}} />
+    </div>
+</div>
+<div>
+    {{#each this.comments as |comment|}}
+        {{#if (has-block)}}
+            {{yield (component "comment-thread/comment" comment=comment) comment}}
+        {{else}}
+            <CommentThread::Comment @comment={{comment}} />
+        {{/if}}
+    {{/each}}
+</div>
\ No newline at end of file
diff --git a/addon/components/comment-thread.js b/addon/components/comment-thread.js
new file mode 100644
index 0000000..54fa438
--- /dev/null
+++ b/addon/components/comment-thread.js
@@ -0,0 +1,60 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import { isArray } from '@ember/array';
+import { task } from 'ember-concurrency-decorators';
+import getWithDefault from '@fleetbase/ember-core/utils/get-with-default';
+import getModelName from '@fleetbase/ember-core/utils/get-model-name';
+
+export default class CommentThreadComponent extends Component {
+    @service store;
+    @service notifications;
+    @tracked subject;
+    @tracked comments = [];
+    @tracked input = '';
+
+    constructor(owner, { subject, subjectType }) {
+        super(...arguments);
+
+        this.subject = subject;
+        this.comments = subject.comments;
+        this.subjectType = subjectType ? subjectType : getModelName(subject);
+    }
+
+    @task *publishComment() {
+        if (this.isCommentInvalid(this.input)) {
+            return;
+        }
+
+        let comment = this.store.createRecord('comment', {
+            content: this.input,
+            subject_uuid: this.subject.id,
+            subject_type: this.subjectType,
+        });
+
+        yield comment.save();
+        yield this.reloadComments.perform();
+
+        this.input = '';
+    }
+
+    @task *reloadComments() {
+        this.comments = yield this.store.query('comment', { subject_uuid: this.subject.id, sort: '-created_at' });
+    }
+
+    isCommentInvalid(comment) {
+        if (!comment) {
+            this.notification.warning(this.intl.t('fleet-ops.operations.orders.index.view.comment-input-empty-notification'));
+            return true;
+        }
+
+        // make sure comment is atleast 12 characters
+        if (typeof comment === 'string' && comment.length < 2) {
+            this.notification.warning(this.intl.t('fleet-ops.operations.orders.index.view.comment-min-length-notification'));
+            return true;
+        }
+
+        return false;
+    }
+}
diff --git a/addon/components/comment-thread/comment.hbs b/addon/components/comment-thread/comment.hbs
new file mode 100644
index 0000000..09f07d3
--- /dev/null
+++ b/addon/components/comment-thread/comment.hbs
@@ -0,0 +1,46 @@
+<div class="thread-comment flex flex-row p-1 space-x-3" ...attributes>
+    <div class="thread-comment-avatar-wrapper w-18 flex flex-col items-center">
+        <Image src={{this.comment.author.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.comment.author.name}} class="w-8 h-8 rounded-full" />
+    </div>
+    <div class="thread-comment-content-wrapper flex-1">
+        <div class="thread-comment-author flex flex-row items-center">
+            <div class="thread-comment-author-name text-sm dark:text-white text-black font-bold mr-1.5">{{this.comment.author.name}}</div>
+            <div class="thread-comment-created-at dark:text-gray-300 text-gray-600 text-xs">{{t "component.comment-thread.comment-published-ago" createdAgo=this.comment.createdAgo}}</div>
+        </div>
+        <div class="thread-comment-conent-paragraph-wrapper mt-2">
+            {{#if this.editing}}
+                <Textarea @value={{this.comment.content}} class="form-input w-full" placeholder={{t "component.comment-thread.comment-reply-placeholder"}} rows={{2}} disabled={{not this.updateComment.isIdle}} />
+                <div class="mt-2 flex flex-row items-center justify-end">
+                    <Button @type="primary" @buttonType="button" @icon="save" @size="xs" @iconSize="xs" @iconClass="text-xs" @text={{t "common.save"}} @onClick={{perform this.updateComment}} @disabled={{or (not this.updateComment.isIdle) (not this.comment.content)}} />
+                </div>
+            {{else}}
+                <p class="thread-comment-conent-paragraph text-xs text-gray-900 dark:text-gray-100">{{this.comment.content}}</p>
+            {{/if}}
+        </div>
+        <div class="thread-comment-conent-actions-wrapper flex flex-row items-center mt-2 space-x-4">
+            <Button @wrapperClass="thread-comment-conent-actions-reply" @type="link" @buttonType="button" @size="xs" @iconSize="xs" @textClass="text-xs" @icon="reply" @text={{t "component.comment-thread.reply-comment-button-text"}} @onClick={{this.reply}} />
+            {{#if this.comment.editable}}
+                <Button @wrapperClass="thread-comment-conent-actions-edit" @type="link" @buttonType="button" @size="xs" @iconSize="xs" @textClass="text-xs" @icon="edit" @text={{t "component.comment-thread.edit-comment-button-text"}} @onClick={{this.edit}} />
+                <Button @wrapperClass="thread-comment-conent-actions-delete" @type="link" @buttonType="button" @size="xs" @iconSize="xs" @iconClass="text-xs text-danger" @textClass="text-xs text-danger" @icon="trash" @text={{t "component.comment-thread.delete-comment-button-text"}} @onClick={{this.delete}} />
+            {{/if}}
+        </div>
+        {{#if this.replying}}
+            <div class="flex flex-col mt-3">
+                <Textarea @value={{this.input}} class="form-input w-full" placeholder={{t "component.comment-thread.comment-reply-placeholder"}} rows={{2}} disabled={{not this.publishReply.isIdle}} />
+                <div class="flex flex-row items-center justify-end mt-2 space-x-4">
+                    <Button @type="link" @buttonType="button" @size="xs" @text={{t "common.cancel"}} @onClick={{this.cancelReply}} @disabled={{not this.publishReply.isIdle}} />
+                    <Button @type="primary" @buttonType="button" @icon="reply" @size="xs" @iconSize="xs" @iconClass="text-xs" @text={{t "component.comment-thread.publish-reply-button-text"}} @onClick={{perform this.publishReply}} @disabled={{or (not this.publishReply.isIdle) (not this.input)}} />
+                </div>
+            </div>
+        {{/if}}
+        <div class="thread-comment-replies mt-3">
+            {{#each this.comment.replies as |reply|}}
+                {{#if (has-block)}}
+                    {{yield (component "comment-thread/comment" comment=reply) reply}}
+                {{else}}
+                    <CommentThread::Comment @comment={{reply}} />
+                {{/if}}
+            {{/each}}
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/addon/components/comment-thread/comment.js b/addon/components/comment-thread/comment.js
new file mode 100644
index 0000000..026c954
--- /dev/null
+++ b/addon/components/comment-thread/comment.js
@@ -0,0 +1,62 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import { task } from 'ember-concurrency-decorators';
+
+export default class CommentThreadCommentComponent extends Component {
+    @service store;
+    @tracked input = '';
+    @tracked replying = false;
+    @tracked editing = false;
+
+    constructor(owner, { comment }) {
+        super(...arguments);
+        
+        this.comment = comment;
+    }
+
+    @action reply() {
+        this.replying = true;
+    }
+
+    @action cancelReply() {
+        this.replying = false;
+    }
+
+    @action edit() {
+        this.editing = true;
+    }
+
+    @action cancelEdit() {
+        this.editing = false;
+    }
+
+    @action delete() {
+        this.comment.destroyRecord();
+    }
+
+    @task *updateComment() {
+        yield this.comment.save();
+        this.editing = false;
+    }
+
+    @task *publishReply() {
+        let comment = this.store.createRecord('comment', {
+            content: this.input,
+            parent_comment_uuid: this.comment.id,
+            subject_uuid: this.comment.subject_uuid,
+            subject_type: this.comment.subject_type,
+        });
+
+        yield comment.save();
+        yield this.reloadReplies.perform();
+
+        this.replying = false;
+        this.input = '';
+    }
+
+    @task *reloadReplies() {
+        this.comment = yield this.comment.reload();
+    }
+}
diff --git a/addon/styles/layout/next.css b/addon/styles/layout/next.css
index 8feb518..95ebf76 100644
--- a/addon/styles/layout/next.css
+++ b/addon/styles/layout/next.css
@@ -1724,10 +1724,16 @@ body[data-theme='light'] .next-dd-menu-seperator {
     height: 57px;
 }
 
+.text-danger,
+span.text-danger,
 a.text-danger {
     @apply text-red-500;
 }
 
+.text-danger:active,
+.text-danger:hover,
+span.text-danger:active,
+span.text-danger:hover,
 a.text-danger:active,
 a.text-danger:hover {
     @apply text-red-400;
@@ -1855,4 +1861,8 @@ input.order-list-overlay-search:focus {
     padding-top: 0.5rem;
     padding-bottom: 0.5rem;
     max-width: 1200px;
+}
+
+.thread-comment-conent-paragraph-wrapper {
+    min-height: 1.75rem;
 }
\ No newline at end of file
diff --git a/app/components/comment-thread.js b/app/components/comment-thread.js
new file mode 100644
index 0000000..8b188bd
--- /dev/null
+++ b/app/components/comment-thread.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-ui/components/comment-thread';
diff --git a/app/components/comment-thread/comment.js b/app/components/comment-thread/comment.js
new file mode 100644
index 0000000..88c68cd
--- /dev/null
+++ b/app/components/comment-thread/comment.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-ui/components/comment-thread/comment';
diff --git a/tests/integration/components/comment-thread-test.js b/tests/integration/components/comment-thread-test.js
new file mode 100644
index 0000000..19f657c
--- /dev/null
+++ b/tests/integration/components/comment-thread-test.js
@@ -0,0 +1,26 @@
+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 | Component | comment-thread', function (hooks) {
+    setupRenderingTest(hooks);
+
+    test('it renders', async function (assert) {
+        // Set any properties with this.set('myProperty', 'value');
+        // Handle any actions with this.set('myAction', function(val) { ... });
+
+        await render(hbs`<CommentThread />`);
+
+        assert.dom().hasText('');
+
+        // Template block usage:
+        await render(hbs`
+      <CommentThread>
+        template block text
+      </CommentThread>
+    `);
+
+        assert.dom().hasText('template block text');
+    });
+});
diff --git a/tests/integration/components/comment-thread/comment-test.js b/tests/integration/components/comment-thread/comment-test.js
new file mode 100644
index 0000000..b71e953
--- /dev/null
+++ b/tests/integration/components/comment-thread/comment-test.js
@@ -0,0 +1,26 @@
+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 | Component | comment-thread/comment', function (hooks) {
+    setupRenderingTest(hooks);
+
+    test('it renders', async function (assert) {
+        // Set any properties with this.set('myProperty', 'value');
+        // Handle any actions with this.set('myAction', function(val) { ... });
+
+        await render(hbs`<CommentThread::Comment />`);
+
+        assert.dom().hasText('');
+
+        // Template block usage:
+        await render(hbs`
+      <CommentThread::Comment>
+        template block text
+      </CommentThread::Comment>
+    `);
+
+        assert.dom().hasText('template block text');
+    });
+});

From 4e735b761cb541682eac33d8d963a19fcbbe0f7b Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Thu, 1 Feb 2024 15:48:56 +0800
Subject: [PATCH 4/5] added spacer component and patched and completed comment
 thread component

---
 addon/components/comment-thread.hbs         |  4 +-
 addon/components/comment-thread.js          | 76 ++++++++++++++++++---
 addon/components/comment-thread/comment.hbs |  5 +-
 addon/components/comment-thread/comment.js  | 72 ++++++++++++++++++-
 addon/components/spacer.hbs                 |  1 +
 addon/components/spacer.js                  | 17 +++++
 addon/styles/layout/next.css                |  8 +++
 app/components/spacer.js                    |  1 +
 tests/integration/components/spacer-test.js | 26 +++++++
 9 files changed, 196 insertions(+), 14 deletions(-)
 create mode 100644 addon/components/spacer.hbs
 create mode 100644 addon/components/spacer.js
 create mode 100644 app/components/spacer.js
 create mode 100644 tests/integration/components/spacer-test.js

diff --git a/addon/components/comment-thread.hbs b/addon/components/comment-thread.hbs
index 991b7e8..39c8e60 100644
--- a/addon/components/comment-thread.hbs
+++ b/addon/components/comment-thread.hbs
@@ -7,9 +7,9 @@
 <div>
     {{#each this.comments as |comment|}}
         {{#if (has-block)}}
-            {{yield (component "comment-thread/comment" comment=comment) comment}}
+            {{yield (component "comment-thread/comment" comment=comment contextApi=this.context) comment}}
         {{else}}
-            <CommentThread::Comment @comment={{comment}} />
+            <CommentThread::Comment @comment={{comment}} @contextApi={{this.context}} />
         {{/if}}
     {{/each}}
 </div>
\ No newline at end of file
diff --git a/addon/components/comment-thread.js b/addon/components/comment-thread.js
index 54fa438..b8259bb 100644
--- a/addon/components/comment-thread.js
+++ b/addon/components/comment-thread.js
@@ -1,27 +1,78 @@
 import Component from '@glimmer/component';
 import { tracked } from '@glimmer/tracking';
-import { action } from '@ember/object';
 import { inject as service } from '@ember/service';
-import { isArray } from '@ember/array';
 import { task } from 'ember-concurrency-decorators';
 import getWithDefault from '@fleetbase/ember-core/utils/get-with-default';
 import getModelName from '@fleetbase/ember-core/utils/get-model-name';
 
+/**
+ * Component to handle a thread of comments.
+ */
 export default class CommentThreadComponent extends Component {
+    /**
+     * Service to handle data store operations.
+     * @service
+     */
     @service store;
+
+    /**
+     * Service for handling notifications.
+     * @service
+     */
     @service notifications;
+
+    /**
+     * Service for internationalization.
+     * @service
+     */
+    @service intl;
+
+    /**
+     * The subject related to the comments.
+     * @tracked
+     */
     @tracked subject;
+
+    /**
+     * Array of comments related to the subject.
+     * @tracked
+     */
     @tracked comments = [];
+
+    /**
+     * The text input for publishing a new comment.
+     * @tracked
+     */
     @tracked input = '';
 
+    /**
+     * Context object containing utility functions.
+     */
+    context = {
+        isCommentInvalid: this.isCommentInvalid.bind(this),
+        reloadComments: () => {
+            return this.reloadComments.perform();
+        },
+    };
+
+    /**
+     * Constructor for the comment thread component.
+     * @param owner - The owner of the component.
+     * @param subject - The subject of the comment thread.
+     * @param subjectType - The type of the subject.
+     */
     constructor(owner, { subject, subjectType }) {
         super(...arguments);
 
         this.subject = subject;
-        this.comments = subject.comments;
+        this.comments = getWithDefault(subject, 'comments', []);
         this.subjectType = subjectType ? subjectType : getModelName(subject);
     }
 
+    /**
+     * Asynchronous task to publish a new comment.
+     * @task
+     */
     @task *publishComment() {
         if (this.isCommentInvalid(this.input)) {
             return;
@@ -39,19 +90,28 @@ export default class CommentThreadComponent extends Component {
         this.input = '';
     }
 
+    /**
+     * Asynchronous task to reload the comments related to the subject.
+     * @task
+     */
     @task *reloadComments() {
-        this.comments = yield this.store.query('comment', { subject_uuid: this.subject.id, sort: '-created_at' });
+        this.comments = yield this.store.query('comment', { subject_uuid: this.subject.id, withoutParent: 1, sort: '-created_at' });
     }
 
+    /**
+     * Checks if a comment is invalid.
+     * @param {string} comment - The comment to validate.
+     * @returns {boolean} True if the comment is invalid, false otherwise.
+     */
     isCommentInvalid(comment) {
         if (!comment) {
-            this.notification.warning(this.intl.t('fleet-ops.operations.orders.index.view.comment-input-empty-notification'));
+            this.notifications.warning(this.intl.t('component.comment-thread.comment-input-empty-notification'));
             return true;
         }
 
-        // make sure comment is atleast 12 characters
-        if (typeof comment === 'string' && comment.length < 2) {
-            this.notification.warning(this.intl.t('fleet-ops.operations.orders.index.view.comment-min-length-notification'));
+        // make sure comment is at least 2 characters
+        if (typeof comment === 'string' && comment.length <= 1) {
+            this.notifications.warning(this.intl.t('component.comment-thread.comment-min-length-notification'));
             return true;
         }
 
diff --git a/addon/components/comment-thread/comment.hbs b/addon/components/comment-thread/comment.hbs
index 09f07d3..92b76da 100644
--- a/addon/components/comment-thread/comment.hbs
+++ b/addon/components/comment-thread/comment.hbs
@@ -10,7 +10,8 @@
         <div class="thread-comment-conent-paragraph-wrapper mt-2">
             {{#if this.editing}}
                 <Textarea @value={{this.comment.content}} class="form-input w-full" placeholder={{t "component.comment-thread.comment-reply-placeholder"}} rows={{2}} disabled={{not this.updateComment.isIdle}} />
-                <div class="mt-2 flex flex-row items-center justify-end">
+                <div class="flex flex-row items-center justify-end space-x-2 mt-2">
+                    <Button @type="link" @buttonType="button" @size="xs" @text={{t "common.cancel"}} @onClick={{this.cancelEdit}} @disabled={{not this.updateComment.isIdle}} />
                     <Button @type="primary" @buttonType="button" @icon="save" @size="xs" @iconSize="xs" @iconClass="text-xs" @text={{t "common.save"}} @onClick={{perform this.updateComment}} @disabled={{or (not this.updateComment.isIdle) (not this.comment.content)}} />
                 </div>
             {{else}}
@@ -27,7 +28,7 @@
         {{#if this.replying}}
             <div class="flex flex-col mt-3">
                 <Textarea @value={{this.input}} class="form-input w-full" placeholder={{t "component.comment-thread.comment-reply-placeholder"}} rows={{2}} disabled={{not this.publishReply.isIdle}} />
-                <div class="flex flex-row items-center justify-end mt-2 space-x-4">
+                <div class="flex flex-row items-center justify-end space-x-2 mt-2">
                     <Button @type="link" @buttonType="button" @size="xs" @text={{t "common.cancel"}} @onClick={{this.cancelReply}} @disabled={{not this.publishReply.isIdle}} />
                     <Button @type="primary" @buttonType="button" @icon="reply" @size="xs" @iconSize="xs" @iconClass="text-xs" @text={{t "component.comment-thread.publish-reply-button-text"}} @onClick={{perform this.publishReply}} @disabled={{or (not this.publishReply.isIdle) (not this.input)}} />
                 </div>
diff --git a/addon/components/comment-thread/comment.js b/addon/components/comment-thread/comment.js
index 026c954..e6fa4fd 100644
--- a/addon/components/comment-thread/comment.js
+++ b/addon/components/comment-thread/comment.js
@@ -4,44 +4,108 @@ import { action } from '@ember/object';
 import { inject as service } from '@ember/service';
 import { task } from 'ember-concurrency-decorators';
 
+/**
+ * Component to handle individual comments in a comment thread.
+ */
 export default class CommentThreadCommentComponent extends Component {
+    /**
+     * Service to handle data store operations.
+     * @service
+     */
     @service store;
+
+    /**
+     * The text input for replying or editing comments.
+     * @tracked
+     */
     @tracked input = '';
+
+    /**
+     * Flag to indicate if the reply interface is active.
+     * @tracked
+     */
     @tracked replying = false;
+
+    /**
+     * Flag to indicate if the edit interface is active.
+     * @tracked
+     */
     @tracked editing = false;
 
-    constructor(owner, { comment }) {
+    /**
+     * The constructor for the comment component.
+     * @param owner - The owner of the component.
+     * @param comment - The comment data for the component.
+     */
+    constructor(owner, { comment, contextApi }) {
         super(...arguments);
-        
+
         this.comment = comment;
+        this.contextApi = contextApi;
     }
 
+    /**
+     * Activates the reply interface.
+     * @action
+     */
     @action reply() {
         this.replying = true;
     }
 
+    /**
+     * Deactivates the reply interface.
+     * @action
+     */
     @action cancelReply() {
         this.replying = false;
     }
 
+    /**
+     * Activates the edit interface.
+     * @action
+     */
     @action edit() {
         this.editing = true;
     }
 
+    /**
+     * Deactivates the edit interface.
+     * @action
+     */
     @action cancelEdit() {
         this.editing = false;
     }
 
+    /**
+     * Deletes the current comment.
+     * @action
+     */
     @action delete() {
         this.comment.destroyRecord();
     }
 
+    /**
+     * Asynchronous task to update the current comment.
+     * @task
+     */
     @task *updateComment() {
+        if (this.contextApi && this.contextApi.isCommentInvalid(this.comment.content)) {
+            return;
+        }
+
         yield this.comment.save();
         this.editing = false;
     }
 
+    /**
+     * Asynchronous task to publish a reply to the current comment.
+     * @task
+     */
     @task *publishReply() {
+        if (this.contextApi && this.contextApi.isCommentInvalid(this.input)) {
+            return;
+        }
+
         let comment = this.store.createRecord('comment', {
             content: this.input,
             parent_comment_uuid: this.comment.id,
@@ -56,6 +120,10 @@ export default class CommentThreadCommentComponent extends Component {
         this.input = '';
     }
 
+    /**
+     * Asynchronous task to reload replies to the current comment.
+     * @task
+     */
     @task *reloadReplies() {
         this.comment = yield this.comment.reload();
     }
diff --git a/addon/components/spacer.hbs b/addon/components/spacer.hbs
new file mode 100644
index 0000000..a0616c2
--- /dev/null
+++ b/addon/components/spacer.hbs
@@ -0,0 +1 @@
+<div class="x-fleetbase-spacer" {{did-insert this.createSpacer}}>{{yield}}</div>
\ No newline at end of file
diff --git a/addon/components/spacer.js b/addon/components/spacer.js
new file mode 100644
index 0000000..61fdd9d
--- /dev/null
+++ b/addon/components/spacer.js
@@ -0,0 +1,17 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { camelize } from '@ember/string';
+
+export default class SpacerComponent extends Component {
+    @action createSpacer(spacerElement) {
+        const properties = Object.keys(this.args);
+
+        for (let index = 0; index < properties.length; index++) {
+            const prop = properties[index];
+
+            if (typeof this.args[prop] === 'string' || !isNaN(this.args[prop])) {
+                spacerElement.style[camelize(prop)] = this.args[prop];
+            }
+        }
+    }
+}
diff --git a/addon/styles/layout/next.css b/addon/styles/layout/next.css
index 95ebf76..f7ff45c 100644
--- a/addon/styles/layout/next.css
+++ b/addon/styles/layout/next.css
@@ -211,10 +211,15 @@ body[data-theme='dark'] .fleetbase-next-container {
 
 .next-content-overlay.full-height > .next-content-overlay-panel-container > .next-content-overlay-panel .next-content-overlay-panel-body {
     height: 100%;
+    min-height: 100%;
+    max-height: 100%;
+    padding-bottom: 300px;
 }
 
 .next-content-overlay.full-height > .next-content-overlay-panel-container > .next-content-overlay-panel .next-content-overlay-panel-body .next-content-overlay-panel-body-inner-wrapper {
     height: 100%;
+    min-height: 100%;
+    max-height: 100%;
     box-sizing: border-box;
 }
 
@@ -253,6 +258,9 @@ body[data-theme='dark'] .fleetbase-next-container {
 
 .next-content-overlay > .next-content-overlay-panel-container {
     @apply absolute transform transition ease-in-out duration-500 pointer-events-auto;
+    height: 100%;
+    min-height: 100%;
+    max-height: 100%;
 }
 
 .next-content-overlay > .next-content-overlay-panel-container > .next-content-overlay-panel {
diff --git a/app/components/spacer.js b/app/components/spacer.js
new file mode 100644
index 0000000..f8dcba7
--- /dev/null
+++ b/app/components/spacer.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-ui/components/spacer';
diff --git a/tests/integration/components/spacer-test.js b/tests/integration/components/spacer-test.js
new file mode 100644
index 0000000..bc39a63
--- /dev/null
+++ b/tests/integration/components/spacer-test.js
@@ -0,0 +1,26 @@
+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 | Component | spacer', function (hooks) {
+    setupRenderingTest(hooks);
+
+    test('it renders', async function (assert) {
+        // Set any properties with this.set('myProperty', 'value');
+        // Handle any actions with this.set('myAction', function(val) { ... });
+
+        await render(hbs`<Spacer />`);
+
+        assert.dom().hasText('');
+
+        // Template block usage:
+        await render(hbs`
+      <Spacer>
+        template block text
+      </Spacer>
+    `);
+
+        assert.dom().hasText('template block text');
+    });
+});

From 0a83e5effc4c60792daa0665a85ec0f7efc93825 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Thu, 1 Feb 2024 18:47:36 +0800
Subject: [PATCH 5/5] completed styling updates and added new component and
 helpers

---
 addon/components/dropdown-button.hbs          |  2 +-
 addon/components/file-icon.hbs                | 10 ++-
 addon/components/file-icon.js                 | 26 ++-----
 addon/components/file.hbs                     | 41 +++++++++++
 addon/components/file.js                      | 43 +++++++++++
 addon/helpers/truncate-filename.js            | 20 +++++
 addon/styles/layout/legacy.css                |  2 +-
 addon/styles/layout/next.css                  | 73 ++++++++++++++++++-
 app/components/file.js                        |  1 +
 app/helpers/truncate-filename.js              |  1 +
 tests/integration/components/file-test.js     | 26 +++++++
 .../helpers/truncate-filename-test.js         | 17 +++++
 12 files changed, 235 insertions(+), 27 deletions(-)
 create mode 100644 addon/components/file.hbs
 create mode 100644 addon/components/file.js
 create mode 100644 addon/helpers/truncate-filename.js
 create mode 100644 app/components/file.js
 create mode 100644 app/helpers/truncate-filename.js
 create mode 100644 tests/integration/components/file-test.js
 create mode 100644 tests/integration/helpers/truncate-filename-test.js

diff --git a/addon/components/dropdown-button.hbs b/addon/components/dropdown-button.hbs
index 4d5cfcc..11d486e 100644
--- a/addon/components/dropdown-button.hbs
+++ b/addon/components/dropdown-button.hbs
@@ -1,4 +1,4 @@
-<BasicDropdown class={{@wrapperClass}} @renderInPlace={{@renderInPlace}} @registerAPI={{@registerAPI}} @horizontalPosition={{@horizontalPosition}} @verticalPosition={{@verticalPosition}} @calculatePosition={{@calculatePosition}} @defaultClass={{@defaultClass}} @matchTriggerWidth={{@matchTriggerWidth}} @onOpen={{@onOpen}} @onClose={{@onClose}} {{did-insert this.onInsert}} as |dd|>
+<BasicDropdown id={{@dropdownId}} class={{@wrapperClass}} @renderInPlace={{@renderInPlace}} @registerAPI={{@registerAPI}} @horizontalPosition={{@horizontalPosition}} @verticalPosition={{@verticalPosition}} @calculatePosition={{@calculatePosition}} @defaultClass={{@defaultClass}} @matchTriggerWidth={{@matchTriggerWidth}} @onOpen={{@onOpen}} @onClose={{@onClose}} {{did-insert this.onInsert}} as |dd|>
     <dd.Trigger class={{@triggerClass}}>
         {{#if @buttonComponent}}
             {{component @buttonComponent buttonComponentArgs=this.buttonComponentArgs text=@text class=(concat @buttonClass (if dd.isOpen ' dd-is-open')) wrapperClass=@buttonWrapperClass type=this.type active=@active size=this.buttonSize isLoading=@isLoading disabled=@disabled textClass=@textClass helpText=@helpText tooltipPlacement=@tooltipPlacement img=@img imgClass=@imgClass alt=@alt}}
diff --git a/addon/components/file-icon.hbs b/addon/components/file-icon.hbs
index f5ecbdd..5c65059 100644
--- a/addon/components/file-icon.hbs
+++ b/addon/components/file-icon.hbs
@@ -1,8 +1,10 @@
-<div class="file-icon file-icon-{{this.extension}}">
+<div class="file-icon file-icon-{{this.extension}}" ...attributes>
     <FaIcon @icon={{this.icon}} class={{@iconClass}} @size={{@iconSize}} />
-    <span class="file-extension truncate">
-        {{this.extension}}
-    </span>
+    {{#unless @hideExtension}}
+        <span class="file-extension truncate {{@fileExtensionClass}}">
+            {{this.extension}}
+        </span>
+    {{/unless}}
     <div>
         {{yield}}
     </div>
diff --git a/addon/components/file-icon.js b/addon/components/file-icon.js
index 8e76b48..1195f47 100644
--- a/addon/components/file-icon.js
+++ b/addon/components/file-icon.js
@@ -16,24 +16,14 @@ export default class FileIconComponent extends Component {
     }
 
     getExtension(file) {
-        return getWithDefault(
-            {
-                'application/vnd.ms-excel': 'xls',
-                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xls',
-                'vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xls',
-                'vnd.ms-excel': 'xls',
-                'text/csv': 'csv',
-                'text/tsv': 'tsv',
-                xlsx: 'xls',
-                xls: 'xls',
-                xlsb: 'xls',
-                xlsm: 'xls',
-                docx: 'doc',
-                docm: 'doc',
-            },
-            getWithDefault(file, 'extension', 'xls'),
-            'xls'
-        );
+        if (!file || (!file.original_filename && !file.url && !file.path)) {
+            return null;
+        }
+
+        // Prefer to use the original filename if available, then URL, then path
+        const filename = file.original_filename || file.url || file.path;
+        const extensionMatch = filename.match(/\.(.+)$/);
+        return extensionMatch ? extensionMatch[1] : null;
     }
 
     getIcon(file) {
diff --git a/addon/components/file.hbs b/addon/components/file.hbs
new file mode 100644
index 0000000..c5488a9
--- /dev/null
+++ b/addon/components/file.hbs
@@ -0,0 +1,41 @@
+<div class="x-fleetbase-file" ...attributes>
+    <div class="x-fleetbase-file-wrapper">
+        <div class="x-fleetbase-file-actions">
+            <DropdownButton @dropdownId="x-fleetbase-file-actions-dropdown" @icon="ellipsis" @iconSize="xs" @iconPrefix={{@dropdownButtonIconPrefix}} @text={{@dropdownButtonText}} @size="xs" @horizontalPosition="left" @calculatePosition={{@dropdownButtonCalculatePosition}} @renderInPlace={{or @dropdownButtonRenderInPlace true}} @wrapperClass={{concat @dropdownButtonWrapperClass " " "next-nav-item-dropdown-button"}} @triggerClass={{@dropdownButtonTriggerClass}} @registerAPI={{@registerAPI}} @onInsert={{this.onDropdownButtonInsert}} as |dd|>
+                <div class="next-dd-menu mt-0i" role="menu" aria-orientation="vertical" aria-labelledby="user-menu">
+                    <div class="px-1">
+                        <div class="text-sm flex flex-row items-center px-3 py-1 rounded-md my-1 text-gray-800 dark:text-gray-300">
+                            {{t "component.file.dropdown-label"}}
+                        </div>
+                    </div>
+                    <div class="next-dd-menu-seperator"></div>
+                    <div role="group" class="px-1">
+                        {{!-- template-lint-disable no-nested-interactive --}}
+                        <a href="javascript:;" role="menuitem" class="next-dd-item text-danger" {{on "click" (fn this.onDropdownItemClick "onDelete" dd)}}>
+                            <span class="mr-1">
+                                <FaIcon @icon="trash" @prefix={{@dropdownButtonIconPrefix}} />
+                            </span>
+                            {{t "common.delete"}}
+                        </a>
+                    </div>
+                </div>
+            </DropdownButton>
+        </div>
+        <div class="flex flex-1 flex-col justify-between items-center">
+            <div class="flex-1">
+                {{#if this.isImage}}
+                    <Image src={{@file.url}} alt={{@file.original_filename}} class="x-fleetbase-file-preview rounded-md shadow-sm" />
+                {{else}}
+                    <div class="x-fleetbase-file-preview">
+                        <FileIcon @file={{@file}} @hideExtension={{true}} @iconSize="2xl" />
+                    </div>
+                {{/if}}
+            </div>
+            <div class="flex-1 overflow-hidden flex flex-col items-center justify-end">
+                <div class="x-fleetbase-file-name">
+                    {{truncate-filename @file.original_filename}}
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/addon/components/file.js b/addon/components/file.js
new file mode 100644
index 0000000..509ef96
--- /dev/null
+++ b/addon/components/file.js
@@ -0,0 +1,43 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+export default class FileComponent extends Component {
+    @tracked file;
+    @tracked isImage = false;
+
+    constructor(owner, { file }) {
+        super(...arguments);
+
+        this.file = file;
+        this.isImage = this.isImageFile(file);
+    }
+
+    @action onDropdownItemClick(action, dd) {
+        if (typeof dd.actions === 'object' && typeof dd.actions.close === 'function') {
+            dd.actions.close();
+        }
+
+        if (typeof this.args[action] === 'function') {
+            this.args[action](this.file);
+        }
+    }
+
+    isImageFile(file) {
+        if (!file || (!file.original_filename && !file.url && !file.path)) {
+            return false;
+        }
+
+        const filename = file.original_filename || file.url || file.path;
+        const extensionMatch = filename.match(/\.(.+)$/);
+
+        if (!extensionMatch) {
+            return false;
+        }
+
+        const extension = extensionMatch[1].toLowerCase();
+        const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'];
+
+        return imageExtensions.includes(extension);
+    }
+}
diff --git a/addon/helpers/truncate-filename.js b/addon/helpers/truncate-filename.js
new file mode 100644
index 0000000..4e630b5
--- /dev/null
+++ b/addon/helpers/truncate-filename.js
@@ -0,0 +1,20 @@
+import { helper } from '@ember/component/helper';
+
+export default helper(function truncateFilename([filename, maxLength = 20]) {
+    if (!filename || typeof filename !== 'string' || filename.length <= maxLength) {
+        return filename;
+    }
+
+    const extensionMatch = filename.match(/\.(.+)$/);
+    const extension = extensionMatch ? extensionMatch[0] : '';
+    const baseName = filename.slice(0, -extension.length);
+
+    if (maxLength <= extension.length) {
+        // If the maximum length is less than or equal to the extension's length, return only the extension
+        return `...${extension}`;
+    }
+
+    const truncated = baseName.slice(0, maxLength - extension.length - 3) + '...';
+
+    return truncated + extension;
+});
diff --git a/addon/styles/layout/legacy.css b/addon/styles/layout/legacy.css
index 18b528e..7458fbb 100644
--- a/addon/styles/layout/legacy.css
+++ b/addon/styles/layout/legacy.css
@@ -737,7 +737,7 @@ body[data-theme='dark'] .ui-combo-box .options-list a.combo-box-option:hover {
 
 .file-dropzone {
     @apply w-full rounded-lg px-4 py-8 bg-gray-50 text-gray-800 text-center flex flex-col items-center justify-center border-2 border-dashed border-gray-200;
-    min-height: 14rem;
+    min-height: 10rem;
 }
 
 body[data-theme='dark'] .file-dropzone {
diff --git a/addon/styles/layout/next.css b/addon/styles/layout/next.css
index f7ff45c..77fd08a 100644
--- a/addon/styles/layout/next.css
+++ b/addon/styles/layout/next.css
@@ -1,3 +1,9 @@
+body {
+    height: 100%; 
+    min-height: 100%; 
+    max-height: 100%;
+}
+
 body, html, button, a, * {
     cursor: default;
 }
@@ -70,8 +76,9 @@ body[data-theme='dark'] .next-mobile-navbar .next-mobile-navbar-tabs > .next-mob
     @apply text-gray-800 flex flex-col bg-white border-t-0;
     overflow: hidden;
     width: 100%;
-    height: 100%;
-    min-height: 100%;
+    height: 100%; 
+    min-height: 100%; 
+    max-height: 100%;
     align-items: stretch;
     border-top: none;
 }
@@ -997,7 +1004,7 @@ body[data-theme='dark'] .next-content-panel-wrapper .next-content-panel-containe
     height: 57px;
     flex-shrink: 0;
     min-width: 100vw;
-    max-width: 100vw;
+    max-width: 100%;
     -moz-user-select: none;
     user-select: none;
     -webkit-user-select: none;
@@ -1873,4 +1880,64 @@ input.order-list-overlay-search:focus {
 
 .thread-comment-conent-paragraph-wrapper {
     min-height: 1.75rem;
+}
+
+.x-fleetbase-file > .x-fleetbase-file-wrapper {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: .65rem;
+    max-width: 96px;
+    height: 135px;
+    max-height: 160px;
+    border: 1px #d1d5db solid;
+    border-radius: .5rem;
+}
+
+body[data-theme="dark"] .x-fleetbase-file > .x-fleetbase-file-wrapper {
+    border: 1px #374151 solid;
+}
+
+.x-fleetbase-file > .x-fleetbase-file-wrapper:hover {
+    border: 1px #3b82f6 solid;
+}
+
+.x-fleetbase-file > .x-fleetbase-file-wrapper .x-fleetbase-file-name {
+    background-color: #3b82f6;
+    padding: .15rem;
+    border-radius: .15rem;
+    text-align: center;
+    font-size: 0.75rem;
+    line-height: 1rem;
+    color: #fff;
+}
+
+.x-fleetbase-file > .x-fleetbase-file-wrapper .x-fleetbase-file-preview {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 4rem;
+    height: 4rem;
+    margin-bottom: .5rem;
+}
+
+.x-fleetbase-file > .x-fleetbase-file-wrapper .x-fleetbase-file-actions {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: end;
+}
+
+.x-fleetbase-file > .x-fleetbase-file-wrapper .x-fleetbase-file-actions #x-fleetbase-file-actions-dropdown {
+    position: absolute;
+    right: 0;
+    top: 0;
+    margin: 0.25rem;
+}
+
+.x-fleetbase-file > .x-fleetbase-file-wrapper .x-fleetbase-file-actions #x-fleetbase-file-actions-dropdown .ember-basic-dropdown-trigger button.btn {
+    padding: 0.15rem 0.65rem;
 }
\ No newline at end of file
diff --git a/app/components/file.js b/app/components/file.js
new file mode 100644
index 0000000..dfdb575
--- /dev/null
+++ b/app/components/file.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-ui/components/file';
diff --git a/app/helpers/truncate-filename.js b/app/helpers/truncate-filename.js
new file mode 100644
index 0000000..b3d9880
--- /dev/null
+++ b/app/helpers/truncate-filename.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-ui/helpers/truncate-filename';
diff --git a/tests/integration/components/file-test.js b/tests/integration/components/file-test.js
new file mode 100644
index 0000000..8ddc8eb
--- /dev/null
+++ b/tests/integration/components/file-test.js
@@ -0,0 +1,26 @@
+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 | Component | file', function (hooks) {
+    setupRenderingTest(hooks);
+
+    test('it renders', async function (assert) {
+        // Set any properties with this.set('myProperty', 'value');
+        // Handle any actions with this.set('myAction', function(val) { ... });
+
+        await render(hbs`<File />`);
+
+        assert.dom().hasText('');
+
+        // Template block usage:
+        await render(hbs`
+      <File>
+        template block text
+      </File>
+    `);
+
+        assert.dom().hasText('template block text');
+    });
+});
diff --git a/tests/integration/helpers/truncate-filename-test.js b/tests/integration/helpers/truncate-filename-test.js
new file mode 100644
index 0000000..4c278b3
--- /dev/null
+++ b/tests/integration/helpers/truncate-filename-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 | truncate-filename', function (hooks) {
+    setupRenderingTest(hooks);
+
+    // TODO: Replace this with your real tests.
+    test('it renders', async function (assert) {
+        this.set('inputValue', '1234');
+
+        await render(hbs`{{truncate-filename this.inputValue}}`);
+
+        assert.dom().hasText('1234');
+    });
+});