Report copy success via tooltip at copy button (#730)

* refactor copy button to show success with tooltip

* remove ember-cli-flash

* update expected bundlesize
This commit is contained in:
Jeldrik Hanschke 2023-11-05 17:06:27 +01:00 committed by GitHub
parent 88a51964f1
commit 147f5dace4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 733 additions and 2029 deletions

View file

@ -0,0 +1,32 @@
<div class="box poll-link cr-poll-link">
<p>{{t "poll.share.title"}}</p>
<p class="link cr-poll-link__link">
<small>
<code class="cr-poll-link__url" data-test-poll-url>{{this.pollUrl}}</code>
</small>
<CopyButton
@text={{this.pollUrl}}
@onSuccess={{this.showCopySuccessMessage}}
class="btn btn-secondary cr-poll-link__copy-btn btn-sm"
data-test-button="copy-link"
>
{{t "poll.link.copy-label"}}&nbsp;
<span
class="oi oi-clipboard"
title={{t "poll.link.copy-label"}}
aria-hidden="true"
></span>
<BsTooltip
@placement="bottom"
@triggerEvents="click"
data-test-tooltip="copied"
>
{{t "poll.link.copied"}}
</BsTooltip>
</CopyButton>
</p>
<small class="text-muted">
{{t "poll.share.notice"}}
</small>
</div>

View file

@ -0,0 +1,36 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';
import type RouterService from '@ember/routing/router-service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class SharePollUrlComponent extends Component {
@service declare router: RouterService;
@tracked shouldShowCopySuccessMessage = false;
get pollUrl() {
// RouterService.currentURL is relative to the webserver's
// directory from which Croodle is served.
const relativeUrl = this.router.currentURL;
// The URL under which Croodle is served, is not known at
// build-time. But we can construct it ourself, by parsing
// window.location.href and replacing the hash part.
const absoluteUrl = new URL(window.location.href);
absoluteUrl.hash = relativeUrl;
return absoluteUrl.toString();
}
@action
showCopySuccessMessage() {
this.shouldShowCopySuccessMessage = true;
}
}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
SharePollUrl: typeof SharePollUrlComponent;
}
}

View file

@ -1,7 +1,3 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
export default class ApplicationController extends Controller {
@service declare flashMessages: FlashMessagesService;
}
export default class ApplicationController extends Controller {}

View file

@ -8,11 +8,9 @@ import { generatePassphrase } from '../../utils/encryption';
import type RouterService from '@ember/routing/router-service';
import type { CreateSettingsRouteModel } from 'croodle/routes/create/settings';
import type IntlService from 'ember-intl/services/intl';
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
import { tracked } from '@glimmer/tracking';
export default class CreateSettings extends Controller {
@service declare flashMessages: FlashMessagesService;
@service declare intl: IntlService;
@service declare router: RouterService;

View file

@ -4,13 +4,11 @@ import { isPresent, isEmpty } from '@ember/utils';
import { action } from '@ember/object';
import { DateTime } from 'luxon';
import { tracked } from '@glimmer/tracking';
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
import type IntlService from 'ember-intl/services/intl';
import type RouterService from '@ember/routing/router-service';
import type { PollRouteModel } from 'croodle/routes/poll';
export default class PollController extends Controller {
@service declare flashMessages: FlashMessagesService;
@service declare intl: IntlService;
@service declare router: RouterService;
@ -22,15 +20,6 @@ export default class PollController extends Controller {
@tracked timezoneChoosen = false;
@tracked shouldUseLocalTimezone = false;
get pollUrl() {
// consume information, which may cause a change to the URL to ensure
// getter is invalided if needed
this.router.currentURL;
this.encryptionKey;
return window.location.href;
}
get showExpirationWarning() {
const { model: poll } = this;
const { expirationDate } = poll;
@ -66,20 +55,6 @@ export default class PollController extends Controller {
return shouldUseLocalTimezone || !poll.timezone ? undefined : poll.timezone;
}
@action
linkAction(type: 'copied' | 'selected') {
const flashMessages = this.flashMessages;
switch (type) {
case 'copied':
flashMessages.success(`poll.link.copied`);
break;
case 'selected':
flashMessages.info(`poll.link.selected`);
break;
}
}
@action
useLocalTimezone() {
this.shouldUseLocalTimezone = true;

View file

@ -32,6 +32,7 @@
@import "ember-bootstrap/input-group";
@import "ember-bootstrap/custom-forms";
@import "ember-bootstrap/spinners";
@import "ember-bootstrap/tooltip";
// Overriding bootstrap selectors with properties we cannot influence by
// changing variables.

View file

@ -14,13 +14,5 @@
</nav>
<main class="container cr-main">
<div id="messages">
{{#each this.flashMessages.queue as |flash|}}
<FlashMessage @flash={{flash}}>
{{t flash.message}}
</FlashMessage>
{{/each}}
</div>
{{outlet}}
</main>

View file

@ -49,31 +49,7 @@
</div>
</div>
<div class="col-sm-6 col-lg-6 offset-lg-1">
<div class="box poll-link cr-poll-link">
<p>{{t "poll.share.title"}}</p>
<p class="link cr-poll-link__link">
<small>
<code class="cr-poll-link__url">{{this.pollUrl}}</code>
</small>
<CopyButton
@text={{this.pollUrl}}
@onError={{fn this.linkAction "selected"}}
@onSuccess={{fn this.linkAction "copied"}}
class="btn btn-secondary cr-poll-link__copy-btn btn-sm"
data-test-button="copy-link"
>
{{t "poll.link.copy-label"}}&nbsp;
<span
class="oi oi-clipboard"
title={{t "poll.link.copy-label"}}
aria-hidden="true"
></span>
</CopyButton>
</p>
<small class="text-muted">
{{t "poll.share.notice"}}
</small>
</div>
<SharePollUrl />
</div>
</div>

View file

@ -4,12 +4,12 @@ module.exports = {
app: {
javascript: {
pattern: 'assets/*.js',
limit: '430KB',
limit: '310KB',
compression: 'gzip',
},
css: {
pattern: 'assets/*.css',
limit: '16KB',
limit: '16.5KB',
compression: 'gzip',
},
},

View file

@ -28,6 +28,7 @@ module.exports = function (defaults) {
'bs-button-group',
'bs-form',
'bs-modal',
'bs-tooltip',
],
},
'ember-cli-babel': {

2464
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -49,7 +49,7 @@
"broccoli-asset-rev": "^3.0.0",
"concurrently": "^8.2.1",
"ember-auto-import": "^2.6.3",
"ember-bootstrap": "^5.0.0",
"ember-bootstrap": "^6.0.0-2",
"ember-cli": "~5.4.0",
"ember-cli-app-version": "^6.0.1",
"ember-cli-babel": "^8.0.0",
@ -61,7 +61,6 @@
"ember-cli-content-security-policy": "^2.0.0",
"ember-cli-dependency-checker": "^3.3.2",
"ember-cli-deprecation-workflow": "^2.0.0",
"ember-cli-flash": "^4.0.0",
"ember-cli-htmlbars": "^6.3.0",
"ember-cli-inject-live-reload": "https://gitpkg.now.sh/jelhan/ember-cli-inject-live-reload/lib?39c289e20aa5bd398da6f480e416e53b2973ef9c",
"ember-cli-mirage": "^3.0.0",
@ -72,7 +71,7 @@
"ember-composable-helpers": "^5.0.0",
"ember-decorators": "^6.1.1",
"ember-fetch": "^8.1.2",
"ember-intl": "^6.0.0",
"ember-intl": "^6.1.2",
"ember-load-initializers": "^2.1.2",
"ember-math-helpers": "^4.0.0",
"ember-modifier": "^4.1.0",
@ -112,6 +111,9 @@
"typescript": "^5.2.2",
"webpack": "^5.88.2"
},
"overrides": {
"ember-element-helper": "^0.8.5"
},
"engines": {
"node": ">=20"
},

View file

@ -24,20 +24,17 @@ module('Acceptance | view poll', function (hooks) {
let pollUrl = `/poll/${poll.id}?encryptionKey=${encryptionKey}`;
await visit(pollUrl);
assert.strictEqual(
pageParticipation.url,
window.location.href,
'share link is shown',
);
assert
.dom('[data-test-poll-url]')
.containsText(
`#/poll/${poll.id}/participation?encryptionKey=${encryptionKey}`,
'share link is shown',
);
await click('.copy-btn');
/*
* Can't test if link is actually copied to clipboard due to api
* restrictions. Due to security it's not allowed to read from clipboard.
*
* Can't test if flash message is shown due to
* https://github.com/poteto/ember-cli-flash/issues/202
*/
assert
.dom('[data-test-tooltip="copied"]')
.isVisible('shows success message that URL has been copied');
});
test('shows a warning if poll is about to be expired', async function (assert) {

View file

@ -0,0 +1,29 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'croodle/tests/helpers';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon';
import { setupIntl } from 'ember-intl/test-support';
module('Integration | Component | share-poll-url', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
test('it shows poll url', async function (assert) {
sinon.stub(this.owner.lookup('service:router'), 'currentURL').value('/foo');
await render(hbs`<SharePollUrl />`);
assert.dom('[data-test-poll-url]').containsText('/foo');
});
test('user can copy the link to the clipboard', async function (assert) {
sinon.stub(this.owner.lookup('service:router'), 'currentURL').value('/foo');
const execCommandFake = sinon.stub(document, 'execCommand');
await render(hbs`<SharePollUrl />`);
await click('[data-test-button="copy-link"]');
assert.ok(execCommandFake.calledOnce);
assert.deepEqual(execCommandFake.firstCall.args, ['copy']);
});
});

View file

@ -204,9 +204,8 @@ poll:
hide: Ocultar
show: Mostra
link:
copied: 'Enllaç copiat al porta-retalls.'
copied: 'Copiat!'
copy-label: 'Copia l''enllaç al porta-retalls'
selected: 'Enllaç seleccionat. Premeu Command + C per copiar.'
modal:
timezoneDiffers:
title: 'En quines zones horàries s''han de presentar les dates?'

View file

@ -212,9 +212,8 @@ poll:
hide: Verbergen
show: Anzeigen
link:
copied: 'Link in die Zwischenablage kopiert.'
copied: 'Kopiert!'
copy-label: 'Kopiere Link in die Zwischenablage'
selected: 'Link markiert. Drücke Steuerung + C zum Kopieren.'
modal:
timezoneDiffers:
title: 'In welcher Zeitzone sollen die Daten angezeigt werden?'

View file

@ -204,9 +204,8 @@ poll:
hide: Hide
show: Show
link:
copied: 'Link copied to clipboard.'
copied: 'Copied!'
copy-label: 'Copy link to clipboard'
selected: 'Link selected. Press Command+C to copy.'
modal:
timezoneDiffers:
title: 'In which time zones should the dates be presented?'

View file

@ -206,9 +206,8 @@ poll:
hide: Esconder
show: Mostrar
link:
copied: 'Enlace copiado al portapapeles.'
copied: 'Copiado!'
copy-label: 'Copiar enlace al portapapeles'
selected: 'Enlace seleccionado. Presiona Comando+C para copiarlo.'
modal:
timezoneDiffers:
title: '¿Que zona horaria deseas utilizar para mostrar los datos?'

View file

@ -206,9 +206,8 @@ poll:
hide: Caché
show: Affiché
link:
copied: 'Lien copié dans le presse-papiers.'
copied: 'Copié!'
copy-label: 'Copier le lien dans le presse-papiers'
selected: 'Lien sélectionné. Appuyez sur Commande+C pour copier.'
modal:
timezoneDiffers:
title: 'Dans quels fuseaux horaires les dates doivent-elles être présentées ?'

View file

@ -205,9 +205,8 @@ poll:
hide: Nascondi
show: Mostra
link:
copied: 'Il link è stato copiato.'
copied: 'Copiato!'
copy-label: 'Copia il link negli appunti'
selected: 'Link selezionato. Preme Command+C per copiarlo.'
modal:
timezoneDiffers:
title: 'In quale fuso orario devono essere presentate le date?'

View file

@ -31,9 +31,8 @@ poll:
expiration-date-warning: Denne avstemmingen utløper {timeToNow} og vil slettes
etterpå.
link:
selected: Lenke valgt. Trykk Command+C for å kopiere.
copy-label: Kopier lenke til utklippstavle
copied: Lenke kopiert til utklippstavle.
copied: Kopiert
input:
showEvaluation:
show: Vis

View file

@ -1,17 +1,19 @@
import { ComponentLike } from '@glint/template';
import type FlashObject from 'ember-cli-flash/flash/object';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
FlashMessage: ComponentLike<{
BsTooltip: ComponentLike<{
Args: {
Named: {
flash: FlashObject;
placement?: 'top' | 'bottom' | 'left' | 'right';
triggerEvents?: string | string[];
visible?: boolean;
};
};
Blocks: {
default: [];
};
Element: HTMLElement;
}>;
}
}

View file

@ -5,8 +5,8 @@ declare module '@glint/environment-ember-loose/registry' {
CopyButton: ComponentLike<{
Args: {
Named: {
onError: () => void;
onSuccess: () => void;
onError?: () => void;
onSuccess?: () => void;
text: string;
};
};

View file

@ -1,20 +0,0 @@
// Source: https://github.com/adopted-ember-addons/ember-cli-flash/blob/5e9ca769ce30b168eef1a4a8e4cdf5ad0d538a8d/ember-cli-flash/declarations/flash/object.d.ts
declare module 'ember-cli-flash/flash/object' {
import EmberObject from '@ember/object';
import Evented from '@ember/object/evented';
class FlashObject extends EmberObject.extend(Evented) {
exiting: boolean;
exitTimer: number;
isExitable: boolean;
initializedTime: number;
message: string;
destroyMessage(): void;
exitMessage(): void;
preventExit(): void;
allowExit(): void;
timerTask(): void;
exitTimerTask(): void;
}
export default FlashObject;
}

View file

@ -1,46 +0,0 @@
// Source: https://github.com/adopted-ember-addons/ember-cli-flash/blob/5e9ca769ce30b168eef1a4a8e4cdf5ad0d538a8d/ember-cli-flash/declarations/services/flash-messages.d.ts
declare module 'ember-cli-flash/services/flash-messages' {
import Service from '@ember/service';
import FlashObject from 'ember-cli-flash/flash/object';
export interface MessageOptions {
type: string;
priority: number;
timeout: number;
sticky: boolean;
showProgress: boolean;
extendedTimeout: number;
destroyOnClick: boolean;
onDestroy: () => void;
[key: string]: unknown;
}
export interface CustomMessageInfo extends Partial<MessageOptions> {
message: string;
}
export interface FlashFunction {
(message: string, options?: Partial<MessageOptions>): FlashMessageService;
}
class FlashMessageService extends Service {
queue: FlashObject[];
readonly arrangedQueue: FlashObject[];
readonly isEmpty: boolean;
success: FlashFunction;
warning: FlashFunction;
info: FlashFunction;
danger: FlashFunction;
alert: FlashFunction;
secondary: FlashFunction;
add(messageInfo: CustomMessageInfo): this;
clearMessages(): this;
registerTypes(types: string[]): this;
getFlashObject(): FlashObject;
peekFirst(): FlashObject | undefined;
peekLast(): FlashObject | undefined;
readonly flashMessageDefaults: unknown;
}
export default FlashMessageService;
}