From 2db5e8e739ab2d2d3c16d3eaeb22ef371a4af7b4 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 16 Jun 2023 11:34:46 +0200 Subject: [PATCH 1/3] styles: Normalize date input heights across browsers --- app/styles/application.module.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/styles/application.module.css b/app/styles/application.module.css index 22a29f26806..6fbd3ec022b 100644 --- a/app/styles/application.module.css +++ b/app/styles/application.module.css @@ -142,6 +142,22 @@ noscript { color: white; } +/* see https://github.com/twbs/bootstrap/pull/30269 */ +::-webkit-datetime-edit, +::-webkit-datetime-edit-fields-wrapper, +::-webkit-datetime-edit-text, +::-webkit-datetime-edit-minute, +::-webkit-datetime-edit-hour-field, +::-webkit-datetime-edit-day-field, +::-webkit-datetime-edit-month-field, +::-webkit-datetime-edit-year-field { + padding: 0; +} + +::-webkit-calendar-picker-indicator { + font-size: 0.9em +} + :global { .c-notification__icon { display: flex; From e2742b426152c24e44123b39eabd66b7f51063b7 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 16 Jun 2023 16:19:19 +0200 Subject: [PATCH 2/3] settings/tokens/new: Extract `base-input` CSS class --- app/styles/settings/tokens/new.module.css | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/styles/settings/tokens/new.module.css b/app/styles/settings/tokens/new.module.css index b31e6dd83b5..e9e9ff3b5b7 100644 --- a/app/styles/settings/tokens/new.module.css +++ b/app/styles/settings/tokens/new.module.css @@ -43,10 +43,9 @@ flex-wrap: wrap; } -.name-input { - max-width: 440px; - width: 100%; +.base-input { padding: var(--space-2xs); + background-color: white; border: 1px solid var(--gray-border); border-radius: var(--space-3xs); @@ -56,6 +55,13 @@ } } +.name-input { + composes: base-input; + + max-width: 440px; + width: 100%; +} + .scopes-list { list-style: none; padding: 0; From fa29cd1e2344cd0963746123a55938b3fc5502ef Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Mon, 19 Jun 2023 17:16:16 +0200 Subject: [PATCH 3/3] settings/tokens/new: Add "Expiration" options --- app/controllers/settings/tokens/new.js | 44 +++++++++++++- app/styles/settings/tokens/new.module.css | 20 +++++++ app/templates/settings/tokens/new.hbs | 40 +++++++++++++ tests/routes/settings/tokens/new-test.js | 73 ++++++++++++++++++++++- 4 files changed, 175 insertions(+), 2 deletions(-) diff --git a/app/controllers/settings/tokens/new.js b/app/controllers/settings/tokens/new.js index 4a9f54046b5..336a66914cf 100644 --- a/app/controllers/settings/tokens/new.js +++ b/app/controllers/settings/tokens/new.js @@ -16,6 +16,9 @@ export default class NewTokenController extends Controller { @tracked name; @tracked nameInvalid; + @tracked expirySelection; + @tracked expiryDateInput; + @tracked expiryDateInvalid; @tracked scopes; @tracked scopesInvalid; @tracked crateScopes; @@ -29,6 +32,31 @@ export default class NewTokenController extends Controller { this.reset(); } + get today() { + return new Date().toISOString().slice(0, 10); + } + + get expiryDate() { + if (this.expirySelection === 'none') return null; + if (this.expirySelection === 'custom') { + if (!this.expiryDateInput) return null; + + let now = new Date(); + let timeSuffix = now.toISOString().slice(10); + return new Date(this.expiryDateInput + timeSuffix); + } + + let date = new Date(); + date.setDate(date.getDate() + Number(this.expirySelection)); + return date; + } + + get expiryDescription() { + return this.expirySelection === 'none' + ? 'The token will never expire' + : `The token will expire on ${this.expiryDate.toLocaleDateString(undefined, { dateStyle: 'long' })}`; + } + @action isScopeSelected(id) { return this.scopes.includes(id); } @@ -46,6 +74,7 @@ export default class NewTokenController extends Controller { name, endpoint_scopes: scopes, crate_scopes: crateScopes, + expired_at: this.expiryDate, }); try { @@ -68,6 +97,9 @@ export default class NewTokenController extends Controller { reset() { this.name = ''; this.nameInvalid = false; + this.expirySelection = 'none'; + this.expiryDateInput = null; + this.expiryDateInvalid = false; this.scopes = []; this.scopesInvalid = false; this.crateScopes = TrackedArray.of(); @@ -75,16 +107,26 @@ export default class NewTokenController extends Controller { validate() { this.nameInvalid = !this.name; + this.expiryDateInvalid = this.expirySelection === 'custom' && !this.expiryDateInput; this.scopesInvalid = this.scopes.length === 0; let crateScopesValid = this.crateScopes.map(pattern => pattern.validate(false)).every(Boolean); - return !this.nameInvalid && !this.scopesInvalid && crateScopesValid; + return !this.nameInvalid && !this.expiryDateInvalid && !this.scopesInvalid && crateScopesValid; } @action resetNameValidation() { this.nameInvalid = false; } + @action updateExpirySelection(event) { + this.expiryDateInput = this.expiryDate?.toISOString().slice(0, 10); + this.expirySelection = event.target.value; + } + + @action resetExpiryDateValidation() { + this.expiryDateInvalid = false; + } + @action toggleScope(id) { this.scopes = this.scopes.includes(id) ? this.scopes.filter(it => it !== id) : [...this.scopes, id]; this.scopesInvalid = false; diff --git a/app/styles/settings/tokens/new.module.css b/app/styles/settings/tokens/new.module.css index e9e9ff3b5b7..70ba09b5c7b 100644 --- a/app/styles/settings/tokens/new.module.css +++ b/app/styles/settings/tokens/new.module.css @@ -62,6 +62,26 @@ width: 100%; } +.expiry-select { + composes: base-input; + + padding-right: var(--space-m); + background-image: url("data:image/svg+xml,"); + background-repeat: no-repeat; + background-position: calc(100% - var(--space-2xs)) center; + background-size: 10px; + appearance: none; +} + +.expiry-date-input { + composes: base-input; +} + +.expiry-description { + margin-left: var(--space-2xs); + font-size: 0.9em; +} + .scopes-list { list-style: none; padding: 0; diff --git a/app/templates/settings/tokens/new.hbs b/app/templates/settings/tokens/new.hbs index a2bde9c2713..6b64a42f0d1 100644 --- a/app/templates/settings/tokens/new.hbs +++ b/app/templates/settings/tokens/new.hbs @@ -26,6 +26,46 @@ {{/let}} +
+ {{#let (unique-id) as |id|}} + + + + {{/let}} + + {{#if (eq this.expirySelection "custom")}} + + {{else}} + + {{this.expiryDescription}} + + {{/if}} +
+
Scopes diff --git a/tests/routes/settings/tokens/new-test.js b/tests/routes/settings/tokens/new-test.js index 176d573c4fc..ea7910aae94 100644 --- a/tests/routes/settings/tokens/new-test.js +++ b/tests/routes/settings/tokens/new-test.js @@ -1,4 +1,4 @@ -import { click, currentURL, fillIn, waitFor } from '@ember/test-helpers'; +import { click, currentURL, fillIn, select, waitFor } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { defer } from 'rsvp'; @@ -60,6 +60,7 @@ module('/settings/tokens/new', function (hooks) { let token = this.server.schema.apiTokens.findBy({ name: 'token-name' }); assert.ok(Boolean(token), 'API token has been created in the backend database'); assert.strictEqual(token.name, 'token-name'); + assert.strictEqual(token.expiredAt, null); assert.strictEqual(token.crateScopes, null); assert.deepEqual(token.endpointScopes, ['publish-update']); @@ -68,6 +69,7 @@ module('/settings/tokens/new', function (hooks) { assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token); assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update'); assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').doesNotExist(); + assert.dom('[data-test-api-token="1"] [data-test-expired-at]').doesNotExist(); }); test('crate scopes', async function (assert) { @@ -138,6 +140,75 @@ module('/settings/tokens/new', function (hooks) { assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token); assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update and yank'); assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').hasText('Crates: serde-* and serde'); + assert.dom('[data-test-api-token="1"] [data-test-expired-at]').doesNotExist(); + }); + + test('token expiry', async function (assert) { + prepare(this); + + await visit('/settings/tokens/new'); + assert.strictEqual(currentURL(), '/settings/tokens/new'); + assert.dom('[data-test-expiry-description]').hasText('The token will never expire'); + + await fillIn('[data-test-name]', 'token-name'); + await select('[data-test-expiry]', '30'); + + let expiryDate = new Date('2017-12-20'); + let expectedDate = expiryDate.toLocaleDateString(undefined, { dateStyle: 'long' }); + let expectedDescription = `The token will expire on ${expectedDate}`; + assert.dom('[data-test-expiry-description]').hasText(expectedDescription); + + await click('[data-test-scope="publish-update"]'); + await click('[data-test-generate]'); + + let token = this.server.schema.apiTokens.findBy({ name: 'token-name' }); + assert.ok(Boolean(token), 'API token has been created in the backend database'); + assert.strictEqual(token.name, 'token-name'); + assert.strictEqual(token.expiredAt.slice(0, 10), '2017-12-20'); + assert.strictEqual(token.crateScopes, null); + assert.deepEqual(token.endpointScopes, ['publish-update']); + + assert.strictEqual(currentURL(), '/settings/tokens'); + assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name'); + assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token); + assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update'); + assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').doesNotExist(); + assert.dom('[data-test-api-token="1"] [data-test-expired-at]').hasText('Expires in about 1 month'); + }); + + test('token expiry with custom date', async function (assert) { + prepare(this); + + await visit('/settings/tokens/new'); + assert.strictEqual(currentURL(), '/settings/tokens/new'); + assert.dom('[data-test-expiry-description]').hasText('The token will never expire'); + + await fillIn('[data-test-name]', 'token-name'); + await select('[data-test-expiry]', 'custom'); + assert.dom('[data-test-expiry-description]').doesNotExist(); + + await click('[data-test-scope="publish-update"]'); + await click('[data-test-generate]'); + assert.dom('[data-test-expiry-date]').hasAria('invalid', 'true'); + + await fillIn('[data-test-expiry-date]', '2024-05-04'); + assert.dom('[data-test-expiry-description]').doesNotExist(); + + await click('[data-test-generate]'); + + let token = this.server.schema.apiTokens.findBy({ name: 'token-name' }); + assert.ok(Boolean(token), 'API token has been created in the backend database'); + assert.strictEqual(token.name, 'token-name'); + assert.strictEqual(token.expiredAt.slice(0, 10), '2024-05-04'); + assert.strictEqual(token.crateScopes, null); + assert.deepEqual(token.endpointScopes, ['publish-update']); + + assert.strictEqual(currentURL(), '/settings/tokens'); + assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name'); + assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token); + assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update'); + assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').doesNotExist(); + assert.dom('[data-test-api-token="1"] [data-test-expired-at]').hasText('Expires in over 6 years'); }); test('loading and error state', async function (assert) {