Merge pull request #6 from kaermorchen/notifications

Notifications
This commit is contained in:
Stanislav Romanov 2022-09-14 14:43:39 +03:00 committed by GitHub
commit c88a13bd5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 538 additions and 21 deletions

View file

@ -3,7 +3,7 @@
<label
for={{@for}}
class='block font-medium mb-1 text-base-2
{{if @required "after:content-['*'] after:text-red-500"}}'
{{if @required "after:content-['*'] after:text-danger"}}'
>
{{~@label~}}
</label>

View file

@ -0,0 +1 @@
export { Close as default } from 'ember-mdi';

View file

@ -1 +1 @@
export { Heart as default } from 'ember-mdi';
export { Loading as default } from 'ember-mdi';

View file

@ -0,0 +1,9 @@
<div
{{animate
(hash width=(array '100%' '0%'))
(hash duration=@duration fill='forwards')
onFinish=@onFinish
}}
>
<div class='bg-base-5 h-1.5' ...attributes></div>
</div>

View file

@ -0,0 +1,3 @@
import Component from '@glimmer/component';
export default class ProgressBarComponent extends Component {}

View file

@ -0,0 +1,19 @@
<Toast>
<div class='inline-flex flex-shrink-0 justify-center items-center self-start'>
<Icons::Loading @spin={{true}} class='mr-4' />
</div>
<div>
{{@text}}
</div>
<button
type='button'
class='ml-auto self-start'
{{! data-dismiss-target='#toast-default' }}
aria-label='Close'
{{on 'click' @onClose}}
>
<Icons::Close />
</button>
</Toast>

View file

@ -0,0 +1,3 @@
import Component from '@glimmer/component';
export default class SuccessToastComponent extends Component {}

View file

@ -0,0 +1,43 @@
<Toast
@autoClose={{this.autoClose}}
@onClose={{@onClose}}
@closeIndicatorClass={{this.bg}}
as |toast|
>
<div class='flex items-center'>
{{#if this.icon}}
<div
class='inline-flex flex-shrink-0 justify-center items-center self-start'
>
<this.icon @spin={{this.isEnqueued}} class='mr-4 {{this.color}}' />
</div>
{{/if}}
<div>
<div class='mb-2'>Task {{this.taskType}} uid:{{this.taskUid}}</div>
<div class={{if this.task.error 'mb-2'}}>
Status:
<span class={{this.color}}>{{this.taskStatus}}</span>
</div>
{{#if this.task.error}}
Error:
<a
href={{this.task.error.link}}
class='text-danger'
target='_blank'
rel='noopener noreferrer'
>{{this.task.error.message}}</a>
{{/if}}
</div>
<button
type='button'
class='ml-auto self-start'
{{! data-dismiss-target='#toast-default' }}
aria-label='Close'
{{on 'click' toast.triggerOnClose}}
>
<Icons::Close />
</button>
</div>
</Toast>

View file

@ -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);
}
}
}

20
app/components/toast.hbs Normal file
View file

@ -0,0 +1,20 @@
{{#in-element this.toasts.destinationElement insertBefore=null}}
<div
class='absolute w-full max-w-xs border rounded bg-base-7 shadow-sm'
role='alert'
style={{this.style}}
{{did-insert this.didInsert}}
...attributes
>
<div class='p-4'>
{{yield (hash triggerOnClose=this.triggerOnClose)}}
</div>
{{#if @autoClose}}
<ProgressBar
@duration={{this.autoCloseDuration}}
@onFinish={{this.triggerOnClose}}
class={{@closeIndicatorClass}}
/>
{{/if}}
</div>
{{/in-element}}

75
app/components/toast.js Normal file
View file

@ -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);
});
}
}

View file

@ -0,0 +1,5 @@
<div ...attributes>
{{#each this.toasts.container as |Item|}}
<Item />
{{/each}}
</div>

View file

@ -0,0 +1,6 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
export default class ToastsContainerComponent extends Component {
@service toasts;
}

View file

@ -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

15
app/modifiers/animate.js Normal file
View file

@ -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();
};
}
);

46
app/services/toasts.js Normal file
View file

@ -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`<SuccessToast @text={{text}} @onClose={{onClose}} />`;
this.container = this.container.concat([wrapper]);
}
taskToast(task) {
const onClose = () => {
this.container = this.container.filter((item) => item !== wrapper);
};
const wrapper = hbs`<TaskToast @task={{task}} @onClose={{onClose}} />`;
this.container = this.container.concat([wrapper]);
}
add(toast) {
this.queue = this.queue.concat(toast);
}
remove(toast) {
this.queue = this.queue.filter((item) => item !== toast);
}
}

View file

@ -1,4 +1,6 @@
<Headers::H1 @title={{if @model.isNew 'Create new index' 'Edit'}} />
{{#if @model.isNew}}
<Headers::H1 @title='Create new index' />
{{/if}}
<form {{on 'submit' this.save}}>
{{#if @model.isNew}}

View file

@ -1 +1,3 @@
{{outlet}}
<ToastsContainer />

View file

@ -26,7 +26,7 @@
/>
{{#if this.errorMessage}}
<p class="text-red">
<p class="text-danger">
{{this.errorMessage}}
</p>
{{/if}}

View file

@ -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',

View file

@ -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": {

View file

@ -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`<ProgressBar />`);
assert.dom(this.element).hasText('');
// Template block usage:
await render(hbs`
<ProgressBar>
template block text
</ProgressBar>
`);
assert.dom(this.element).hasText('template block text');
});
});

View file

@ -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`<SuccessToast />`);
assert.dom(this.element).hasText('');
// Template block usage:
await render(hbs`
<SuccessToast>
template block text
</SuccessToast>
`);
assert.dom(this.element).hasText('template block text');
});
});

View file

@ -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`<TaskToast />`);
assert.dom(this.element).hasText('');
// Template block usage:
await render(hbs`
<TaskToast>
template block text
</TaskToast>
`);
assert.dom(this.element).hasText('template block text');
});
});

View file

@ -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`<Toast />`);
assert.dom(this.element).hasText('');
// Template block usage:
await render(hbs`
<Toast>
template block text
</Toast>
`);
assert.dom(this.element).hasText('template block text');
});
});

View file

@ -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`<ToastsContainer />`);
assert.dom(this.element).hasText('');
// Template block usage:
await render(hbs`
<ToastsContainer>
template block text
</ToastsContainer>
`);
assert.dom(this.element).hasText('template block text');
});
});

View file

@ -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);
});
});

View file

@ -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==