Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.2.10 #43

Merged
merged 5 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions addon/components/comment-thread.hbs
Original file line number Diff line number Diff line change
@@ -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 contextApi=this.context) comment}}
{{else}}
<CommentThread::Comment @comment={{comment}} @contextApi={{this.context}} />
{{/if}}
{{/each}}
</div>
120 changes: 120 additions & 0 deletions addon/components/comment-thread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
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 = 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;
}

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 = '';
}

/**
* 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, 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.notifications.warning(this.intl.t('component.comment-thread.comment-input-empty-notification'));
return true;
}

// 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;
}

return false;
}
}
47 changes: 47 additions & 0 deletions addon/components/comment-thread/comment.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<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="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}}
<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 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>
</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>
130 changes: 130 additions & 0 deletions addon/components/comment-thread/comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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';

/**
* 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;

/**
* 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,
subject_uuid: this.comment.subject_uuid,
subject_type: this.comment.subject_type,
});

yield comment.save();
yield this.reloadReplies.perform();

this.replying = false;
this.input = '';
}

/**
* Asynchronous task to reload replies to the current comment.
* @task
*/
@task *reloadReplies() {
this.comment = yield this.comment.reload();
}
}
2 changes: 1 addition & 1 deletion addon/components/dropdown-button.hbs
Original file line number Diff line number Diff line change
@@ -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}}
Expand Down
10 changes: 6 additions & 4 deletions addon/components/file-icon.hbs
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
26 changes: 8 additions & 18 deletions addon/components/file-icon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading