Skip to content

Commit

Permalink
Merge pull request #43 from fleetbase/dev-v0.2.10
Browse files Browse the repository at this point in the history
v0.2.10
  • Loading branch information
roncodes authored Feb 1, 2024
2 parents c669420 + 0a83e5e commit e532986
Show file tree
Hide file tree
Showing 26 changed files with 674 additions and 37 deletions.
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

0 comments on commit e532986

Please sign in to comment.