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}} + +{{/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}}
{{#if @model.isNew}} diff --git a/app/templates/application.hbs b/app/templates/application.hbs index c24cd68..7fa6fb9 100644 --- a/app/templates/application.hbs +++ b/app/templates/application.hbs @@ -1 +1,3 @@ {{outlet}} + + diff --git a/app/templates/login.hbs b/app/templates/login.hbs index f7f880c..cd112e3 100644 --- a/app/templates/login.hbs +++ b/app/templates/login.hbs @@ -26,7 +26,7 @@ /> {{#if this.errorMessage}} -

+

{{this.errorMessage}}

{{/if}} diff --git a/config/tailwind.config.js b/config/tailwind.config.js index 7b4dfae..73af661 100644 --- a/config/tailwind.config.js +++ b/config/tailwind.config.js @@ -23,21 +23,26 @@ module.exports = { DEFAULT: colors.pink['500'], dark: colors.pink['800'], }, - yellow: { - light: colors.yellow['100'], - DEFAULT: colors.yellow['400'], - dark: colors.yellow['700'], - }, - green: { + success: { light: colors.green['100'], DEFAULT: colors.green['400'], dark: colors.green['800'], }, - red: { + danger: { light: colors.red['100'], DEFAULT: colors.red['500'], dark: colors.red['800'], }, + warning: { + light: colors.yellow['100'], + DEFAULT: colors.yellow['400'], + dark: colors.yellow['700'], + }, + info: { + light: colors.blue['100'], + DEFAULT: colors.blue['400'], + dark: colors.blue['700'], + }, }, zIndex: { 'scroll-link': '10', diff --git a/package.json b/package.json index fd46b2b..5eacaa5 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "ember-headlessui": "^0.13.0", "ember-load-initializers": "^2.1.2", "ember-mdi": "2.0.0-beta.1", + "ember-modifier": "4.0.0-beta.1", "ember-qunit": "^5.1.5", "ember-resolver": "^8.0.3", "ember-resources": "^4.7.1", @@ -99,7 +100,7 @@ "qunit": "^2.19.1", "qunit-dom": "^2.0.0", "tailwindcss": "^3.1.8", - "tracked-built-ins": "^2.0.1", + "tracked-built-ins": "^3.1.0", "webpack": "^5.74.0" }, "engines": { diff --git a/tests/integration/components/progress-bar-test.js b/tests/integration/components/progress-bar-test.js new file mode 100644 index 0000000..d296fdc --- /dev/null +++ b/tests/integration/components/progress-bar-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'meiliadmin/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | progress-bar', 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``); + + assert.dom(this.element).hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom(this.element).hasText('template block text'); + }); +}); diff --git a/tests/integration/components/success-toast-test.js b/tests/integration/components/success-toast-test.js new file mode 100644 index 0000000..6c99445 --- /dev/null +++ b/tests/integration/components/success-toast-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'meiliadmin/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | success-toast', 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``); + + assert.dom(this.element).hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom(this.element).hasText('template block text'); + }); +}); diff --git a/tests/integration/components/task-toast-test.js b/tests/integration/components/task-toast-test.js new file mode 100644 index 0000000..9983d76 --- /dev/null +++ b/tests/integration/components/task-toast-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'meiliadmin/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | task-toast', 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``); + + assert.dom(this.element).hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom(this.element).hasText('template block text'); + }); +}); diff --git a/tests/integration/components/toast-test.js b/tests/integration/components/toast-test.js new file mode 100644 index 0000000..b13a5cd --- /dev/null +++ b/tests/integration/components/toast-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'meiliadmin/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | toast', 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``); + + assert.dom(this.element).hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom(this.element).hasText('template block text'); + }); +}); diff --git a/tests/integration/components/toasts-container-test.js b/tests/integration/components/toasts-container-test.js new file mode 100644 index 0000000..fbf3bb9 --- /dev/null +++ b/tests/integration/components/toasts-container-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'meiliadmin/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | toasts-container', 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``); + + assert.dom(this.element).hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom(this.element).hasText('template block text'); + }); +}); diff --git a/tests/unit/services/toasts-test.js b/tests/unit/services/toasts-test.js new file mode 100644 index 0000000..ef47b31 --- /dev/null +++ b/tests/unit/services/toasts-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'meiliadmin/tests/helpers'; + +module('Unit | Service | toasts', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let service = this.owner.lookup('service:toasts'); + assert.ok(service); + }); +}); diff --git a/yarn.lock b/yarn.lock index 44352b1..7f9c9ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12779,17 +12779,16 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -tracked-built-ins@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/tracked-built-ins/-/tracked-built-ins-2.0.1.tgz#9d6d0db29ad187e8533591f0e891e15b7a2259c5" - integrity sha512-v0DwPVNm3zA4cmJLpcBJxJfKefN6ldhkc8l5YA+cWHq7kI8WMUcXjIXgl/AIxmt1SoO1bwPdpikPRHHrIBDtFQ== +tracked-built-ins@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/tracked-built-ins/-/tracked-built-ins-3.1.0.tgz#827703e8e8857e45ac449dfc41e8706e0d6da309" + integrity sha512-yPEZV1aYaw7xFWdoEluvdwNxIJIA834HaBQaMATjNAYPwd1fRqIJ46YnuRo6+9mRRWu6nM6sJqrVVa5H6UhFuw== dependencies: ember-cli-babel "^7.26.10" - ember-cli-typescript "^4.1.0" + ember-cli-typescript "^5.1.0" ember-tracked-storage-polyfill "^1.0.0" - tracked-maps-and-sets "^3.0.2" -tracked-maps-and-sets@^3.0.1, tracked-maps-and-sets@^3.0.2: +tracked-maps-and-sets@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/tracked-maps-and-sets/-/tracked-maps-and-sets-3.0.2.tgz#6ea1b9f2a367d24f2e9905b74b24437fbce76ea6" integrity sha512-UIRcWsX1kDOcC/Q2R58weYWlw01EnmWWBwUv3okWS+zMBvsgIfYoO6veHhuNE3hgzWCEImNp46QS5CyKnw5QUA==