diff --git a/app/components/form/element.hbs b/app/components/form/element.hbs
index f2e39d5..c1c75c1 100644
--- a/app/components/form/element.hbs
+++ b/app/components/form/element.hbs
@@ -3,7 +3,7 @@
diff --git a/app/components/icons/close.js b/app/components/icons/close.js
new file mode 100644
index 0000000..a956cb2
--- /dev/null
+++ b/app/components/icons/close.js
@@ -0,0 +1 @@
+export { Close as default } from 'ember-mdi';
diff --git a/app/components/icons/loading.js b/app/components/icons/loading.js
index 2ff4aa1..5642ac5 100644
--- a/app/components/icons/loading.js
+++ b/app/components/icons/loading.js
@@ -1 +1 @@
-export { Heart as default } from 'ember-mdi';
+export { Loading as default } from 'ember-mdi';
diff --git a/app/components/progress-bar.hbs b/app/components/progress-bar.hbs
new file mode 100644
index 0000000..ced0b62
--- /dev/null
+++ b/app/components/progress-bar.hbs
@@ -0,0 +1,9 @@
+
diff --git a/app/components/progress-bar.js b/app/components/progress-bar.js
new file mode 100644
index 0000000..ad40ade
--- /dev/null
+++ b/app/components/progress-bar.js
@@ -0,0 +1,3 @@
+import Component from '@glimmer/component';
+
+export default class ProgressBarComponent extends Component {}
diff --git a/app/components/success-toast.hbs b/app/components/success-toast.hbs
new file mode 100644
index 0000000..333cd87
--- /dev/null
+++ b/app/components/success-toast.hbs
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ {{@text}}
+
+
+
+
diff --git a/app/components/success-toast.js b/app/components/success-toast.js
new file mode 100644
index 0000000..13720d8
--- /dev/null
+++ b/app/components/success-toast.js
@@ -0,0 +1,3 @@
+import Component from '@glimmer/component';
+
+export default class SuccessToastComponent extends Component {}
diff --git a/app/components/task-toast.hbs b/app/components/task-toast.hbs
new file mode 100644
index 0000000..71c4812
--- /dev/null
+++ b/app/components/task-toast.hbs
@@ -0,0 +1,43 @@
+
+
+ {{#if this.icon}}
+
+
+
+ {{/if}}
+
+
+
Task {{this.taskType}} uid:{{this.taskUid}}
+
+ Status:
+ {{this.taskStatus}}
+
+ {{#if this.task.error}}
+ Error:
+
{{this.task.error.message}}
+ {{/if}}
+
+
+
+
+
diff --git a/app/components/task-toast.js b/app/components/task-toast.js
new file mode 100644
index 0000000..3a2c86b
--- /dev/null
+++ b/app/components/task-toast.js
@@ -0,0 +1,118 @@
+import Component from '@glimmer/component';
+import { registerDestructor } from '@ember/destroyable';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { Loading, Check, AlertOctagonOutline } from 'ember-mdi';
+
+export default class TaskToastComponent extends Component {
+ @service meilisearch;
+
+ @tracked task;
+ @tracked autoClose;
+
+ constructor(owner, args) {
+ super(owner, args);
+
+ this.interval = setInterval(this.reloadTask, 1000);
+
+ registerDestructor(this, this.clearInterval);
+ }
+
+ get icon() {
+ switch (this.taskStatus) {
+ case 'enqueued':
+ return Loading;
+ case 'succeeded':
+ return Check;
+ case 'failed':
+ return AlertOctagonOutline;
+ default:
+ return null;
+ }
+ }
+
+ get status() {
+ if (this.isEnqueued) {
+ return 'info';
+ } else if (this.isFailed) {
+ return 'danger';
+ } else if (this.isSucceeded) {
+ return 'success';
+ } else {
+ return null;
+ }
+ }
+
+ get color() {
+ // Current classes need for taiwind builder
+ if (this.isEnqueued) {
+ return 'text-info';
+ } else if (this.isFailed) {
+ return 'text-danger';
+ } else if (this.isSucceeded) {
+ return 'text-success';
+ } else {
+ return null;
+ }
+ }
+
+ get bg() {
+ // Current classes need for taiwind builder
+ if (this.isEnqueued) {
+ return 'bg-info';
+ } else if (this.isFailed) {
+ return 'bg-danger';
+ } else if (this.isSucceeded) {
+ return 'bg-success';
+ } else {
+ return null;
+ }
+ }
+
+ get isEnqueued() {
+ return this.taskStatus === 'enqueued';
+ }
+
+ get isFailed() {
+ return this.taskStatus === 'failed';
+ }
+
+ get isSucceeded() {
+ return this.taskStatus === 'succeeded';
+ }
+
+ get taskUid() {
+ return this.task?.taskUid ?? this.args.task.taskUid;
+ }
+
+ get taskType() {
+ return this.task?.type ?? this.args.task.type;
+ }
+
+ get taskStatus() {
+ return this.task?.status ?? this.args.task.status;
+ }
+
+ @action
+ reloadTask() {
+ this.meilisearch.getTask(this.taskUid).then((task) => {
+ this.task = task;
+
+ if (!this.isEnqueued) {
+ this.clearInterval();
+ }
+
+ if (this.isSucceeded) {
+ this.autoClose = true;
+ }
+ });
+ }
+
+ @action
+ clearInterval() {
+ if (this.interval) {
+ clearInterval(this.interval);
+ }
+ }
+}
diff --git a/app/components/toast.hbs b/app/components/toast.hbs
new file mode 100644
index 0000000..afa82a2
--- /dev/null
+++ b/app/components/toast.hbs
@@ -0,0 +1,20 @@
+{{#in-element this.toasts.destinationElement insertBefore=null}}
+
+
+ {{yield (hash triggerOnClose=this.triggerOnClose)}}
+
+ {{#if @autoClose}}
+
+ {{/if}}
+
+{{/in-element}}
diff --git a/app/components/toast.js b/app/components/toast.js
new file mode 100644
index 0000000..8ea3b3f
--- /dev/null
+++ b/app/components/toast.js
@@ -0,0 +1,75 @@
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import { registerDestructor } from '@ember/destroyable';
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+import { htmlSafe } from '@ember/template';
+
+const margin = 16;
+
+export default class ToastComponent extends Component {
+ @service toasts;
+
+ @tracked width;
+ @tracked height;
+
+ get autoCloseDuration() {
+ return this.args?.autoCloseDuration ?? 3000;
+ }
+
+ get top() {
+ const queue = this.toasts.queue;
+
+ for (let i = 0, height = 0; i < queue.length; i++) {
+ height += margin;
+
+ if (queue[i] === this) {
+ return `top: ${height}px`;
+ }
+
+ height += queue[i].height;
+ }
+
+ return `top: ${margin}px`;
+ }
+
+ get right() {
+ return `right: ${margin}px`;
+ }
+
+ get style() {
+ return htmlSafe([this.top, this.right].join('; '));
+ }
+
+ @action
+ triggerOnClose() {
+ this.args.onClose?.();
+ }
+
+ @action
+ didInsert(el) {
+ this.registerInQueue();
+ this.registerResizeObserver(el);
+ }
+
+ registerResizeObserver(el) {
+ const ro = new ResizeObserver((entries) => {
+ this.width = entries[0].borderBoxSize[0].inlineSize;
+ this.height = entries[0].borderBoxSize[0].blockSize;
+ });
+
+ ro.observe(el);
+
+ registerDestructor(this, () => {
+ ro.disconnect();
+ });
+ }
+
+ registerInQueue() {
+ this.toasts.add(this);
+
+ registerDestructor(this, () => {
+ this.toasts.remove(this);
+ });
+ }
+}
diff --git a/app/components/toasts-container.hbs b/app/components/toasts-container.hbs
new file mode 100644
index 0000000..0b8f949
--- /dev/null
+++ b/app/components/toasts-container.hbs
@@ -0,0 +1,5 @@
+
+ {{#each this.toasts.container as |Item|}}
+
+ {{/each}}
+
diff --git a/app/components/toasts-container.js b/app/components/toasts-container.js
new file mode 100644
index 0000000..8c53aaf
--- /dev/null
+++ b/app/components/toasts-container.js
@@ -0,0 +1,6 @@
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+
+export default class ToastsContainerComponent extends Component {
+ @service toasts;
+}
diff --git a/app/controllers/admin/indexes/item/edit.js b/app/controllers/admin/indexes/item/edit.js
index b19a38a..991e287 100644
--- a/app/controllers/admin/indexes/item/edit.js
+++ b/app/controllers/admin/indexes/item/edit.js
@@ -4,15 +4,18 @@ import { inject as service } from '@ember/service';
export default class AdminIndexesItemEditController extends Controller {
@service router;
+ @service toasts;
@action
- save(event) {
+ async save(event) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const data = Object.fromEntries(formData.entries());
- this.model.save(data);
+ const task = await this.model.save(data);
+
+ this.toasts.taskToast(task);
}
@action
diff --git a/app/modifiers/animate.js b/app/modifiers/animate.js
new file mode 100644
index 0000000..d033b91
--- /dev/null
+++ b/app/modifiers/animate.js
@@ -0,0 +1,15 @@
+import { modifier } from 'ember-modifier';
+
+export default modifier(
+ (element, [keyframes, timing], { onFinish, onCancel }) => {
+ const animation = element.animate(keyframes, timing);
+
+ animation.finished
+ .then(() => onFinish?.())
+ .catch((error) => onCancel?.(error));
+
+ return () => {
+ animation.cancel();
+ };
+ }
+);
diff --git a/app/services/toasts.js b/app/services/toasts.js
new file mode 100644
index 0000000..dade19a
--- /dev/null
+++ b/app/services/toasts.js
@@ -0,0 +1,46 @@
+import Service from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { hbs } from 'ember-template-imports';
+import SuccessToast from '../components/success-toast';
+import TaskToast from '../components/task-toast';
+
+export default class ToastsService extends Service {
+ @tracked container = [];
+ @tracked queue = [];
+
+ constructor() {
+ super();
+
+ this.destinationElement = document.createElement('div');
+
+ document.body.appendChild(this.destinationElement);
+ }
+
+ successToast({ text }) {
+ const onClose = () => {
+ this.container = this.container.filter((item) => item !== wrapper);
+ };
+
+ const wrapper = hbs``;
+
+ this.container = this.container.concat([wrapper]);
+ }
+
+ taskToast(task) {
+ const onClose = () => {
+ this.container = this.container.filter((item) => item !== wrapper);
+ };
+
+ const wrapper = hbs``;
+
+ this.container = this.container.concat([wrapper]);
+ }
+
+ add(toast) {
+ this.queue = this.queue.concat(toast);
+ }
+
+ remove(toast) {
+ this.queue = this.queue.filter((item) => item !== toast);
+ }
+}
diff --git a/app/templates/admin/indexes/item/edit.hbs b/app/templates/admin/indexes/item/edit.hbs
index 5c088c9..130ec11 100644
--- a/app/templates/admin/indexes/item/edit.hbs
+++ b/app/templates/admin/indexes/item/edit.hbs
@@ -1,4 +1,6 @@
-
+{{#if @model.isNew}}
+
+{{/if}}