From 0e8f4d092aa4c223ca9f529118a8db5841735bfa Mon Sep 17 00:00:00 2001 From: Ming Hay Luk Date: Wed, 10 Jul 2024 12:52:50 -0700 Subject: [PATCH 1/2] Modal for adding a pipeline to a collection --- .../modal/add-to-collection/component.js | 128 ++++++++++++++++++ .../modal/add-to-collection/styles.scss | 51 +++++++ .../modal/add-to-collection/template.hbs | 72 ++++++++++ .../modal/add-to-collection/util.js | 26 ++++ app/components/collection/styles.scss | 5 + app/components/styles.scss | 2 + .../modal/add-to-collection/component-test.js | 119 ++++++++++++++++ .../modal/add-to-collection/util-test.js | 40 ++++++ 8 files changed, 443 insertions(+) create mode 100644 app/components/collection/modal/add-to-collection/component.js create mode 100644 app/components/collection/modal/add-to-collection/styles.scss create mode 100644 app/components/collection/modal/add-to-collection/template.hbs create mode 100644 app/components/collection/modal/add-to-collection/util.js create mode 100644 app/components/collection/styles.scss create mode 100644 tests/integration/components/collection/modal/add-to-collection/component-test.js create mode 100644 tests/unit/components/collection/modal/add-to-collection/util-test.js diff --git a/app/components/collection/modal/add-to-collection/component.js b/app/components/collection/modal/add-to-collection/component.js new file mode 100644 index 000000000..67085f7a8 --- /dev/null +++ b/app/components/collection/modal/add-to-collection/component.js @@ -0,0 +1,128 @@ +import Component from '@glimmer/component'; +import { service } from '@ember/service'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { createCollectionBody, getCollectionsWithoutPipeline } from './util'; + +export default class CollectionModalAddToCollectionModalComponent extends Component { + @service shuttle; + + @tracked errorMessage; + + @tracked newCollectionName = ''; + + @tracked newCollectionDescription = ''; + + @tracked selectedCollections = []; + + @tracked wasActionSuccessful = false; + + @tracked collectionsWithoutPipeline = []; + + failedToAddToCollections = []; + + constructor() { + super(...arguments); + + const collections = this.args.collections || []; + + this.collectionsWithoutPipeline = getCollectionsWithoutPipeline( + collections, + this.args.pipeline.id + ); + this.errorMessage = this.args.errorMessage; + } + + get isSubmitButtonDisabled() { + if (this.isAwaitingResponse) { + return true; + } + + return !( + this.newCollectionName.length > 0 || this.selectedCollections.length > 0 + ); + } + + get hasCollections() { + return this.collectionsWithoutPipeline.length > 0; + } + + async createCollection() { + if (this.newCollectionName.length > 0) { + await this.shuttle + .fetchFromApi( + 'post', + '/collections', + createCollectionBody( + this.newCollectionName, + this.newCollectionDescription, + this.args.pipeline.id + ) + ) + .then(() => { + this.newCollectionName = ''; + this.newCollectionDescription = ''; + }) + .catch(() => { + this.errorMessage = `Failed to create new collection: ${this.newCollectionName}`; + }); + } + } + + async addToCollections() { + const promises = []; + + if (this.selectedCollections.length > 0) { + this.failedToAddToCollections = []; + + this.selectedCollections.forEach(collection => { + promises.push( + this.shuttle + .fetchFromApi('put', `/collections/${collection.id}`, { + pipelineIds: collection.pipelineIds.concat(this.args.pipeline.id) + }) + .catch(() => { + this.failedToAddToCollections.push(collection.name); + }) + ); + }); + } + + return Promise.all(promises); + } + + @action + async submitCollections() { + this.isAwaitingResponse = true; + this.errorMessage = null; + + return new Promise(resolve => { + Promise.allSettled([ + this.createCollection(), + this.addToCollections() + ]).then(() => { + this.isAwaitingResponse = false; + + if (this.failedToAddToCollections.length > 0) { + if (this.errorMessage) { + this.errorMessage += `. Also failed to add pipeline to collections: ${this.failedToAddToCollections.join( + ', ' + )}`; + } else { + this.errorMessage = `Failed to add pipeline to collections: ${this.failedToAddToCollections.join( + ', ' + )}`; + } + } else { + this.selectedCollections.forEach(collection => { + document.getElementById( + `collection-${collection.id}` + ).disabled = true; + }); + this.selectedCollections = []; + } + resolve(); + }); + }); + } +} diff --git a/app/components/collection/modal/add-to-collection/styles.scss b/app/components/collection/modal/add-to-collection/styles.scss new file mode 100644 index 000000000..6ee58c0ef --- /dev/null +++ b/app/components/collection/modal/add-to-collection/styles.scss @@ -0,0 +1,51 @@ +@use 'screwdriver-colors' as colors; + +@mixin styles { + #add-to-collection-modal { + .modal-dialog { + max-width: 50%; + + .modal-body { + .modal-title { + font-size: 1.75rem; + padding-bottom: 0.75rem; + } + + .create-new-collection { + label { + display: flex; + justify-content: space-between; + + > div { + margin: auto; + padding-right: 0.25rem; + } + + input { + flex: 1; + border-radius: 4px; + border: 1px solid colors.$sd-text-med; + padding-left: 0.5rem; + margin-left: 0.25rem; + } + } + } + + .select-collections { + max-height: 10rem; + overflow: scroll; + + .btn-group { + display: flex; + flex-direction: column; + + > .btn { + border-radius: 3px; + margin: 0.25rem; + } + } + } + } + } + } +} diff --git a/app/components/collection/modal/add-to-collection/template.hbs b/app/components/collection/modal/add-to-collection/template.hbs new file mode 100644 index 000000000..fed23684a --- /dev/null +++ b/app/components/collection/modal/add-to-collection/template.hbs @@ -0,0 +1,72 @@ + + + {{#if this.errorMessage}} + + {{/if}} + +
+ + +
+ + {{#if this.hasCollections}} +
+ +
+ + {{#each this.collectionsWithoutPipeline as |collection|}} + + {{collection.name}} + + {{/each}} + +
+ {{/if}} + +
+ + + +
diff --git a/app/components/collection/modal/add-to-collection/util.js b/app/components/collection/modal/add-to-collection/util.js new file mode 100644 index 000000000..b743785a7 --- /dev/null +++ b/app/components/collection/modal/add-to-collection/util.js @@ -0,0 +1,26 @@ +export const getCollectionsWithoutPipeline = (collections, pipelineId) => { + const collectionsWithoutPipeline = []; + + collections.forEach(collection => { + if (!collection.pipelineIds.includes(pipelineId)) { + collectionsWithoutPipeline.push(collection); + } + }); + + collectionsWithoutPipeline.sort((a, b) => a.name.localeCompare(b.name)); + + return collectionsWithoutPipeline; +}; + +export const createCollectionBody = ( + collectionName, + collectionDescription, + pipelineId +) => { + return { + name: collectionName, + description: collectionDescription, + pipelineIds: [pipelineId], + type: 'normal' + }; +}; diff --git a/app/components/collection/styles.scss b/app/components/collection/styles.scss new file mode 100644 index 000000000..5994f35f1 --- /dev/null +++ b/app/components/collection/styles.scss @@ -0,0 +1,5 @@ +@use 'modal/add-to-collection/styles' as modal; + +@mixin styles { + @include modal.styles; +} diff --git a/app/components/styles.scss b/app/components/styles.scss index 9b42f84b5..4551fc17f 100644 --- a/app/components/styles.scss +++ b/app/components/styles.scss @@ -1,5 +1,7 @@ +@use 'collection/styles' as collection; @use 'pipeline/styles' as pipeline; @mixin styles { + @include collection.styles; @include pipeline.styles; } diff --git a/tests/integration/components/collection/modal/add-to-collection/component-test.js b/tests/integration/components/collection/modal/add-to-collection/component-test.js new file mode 100644 index 000000000..5e502b248 --- /dev/null +++ b/tests/integration/components/collection/modal/add-to-collection/component-test.js @@ -0,0 +1,119 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'screwdriver-ui/tests/helpers'; +import { click, fillIn, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module( + 'Integration | Component | collection/modal/add-to-collection', + function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + this.setProperties({ + pipeline: { id: 123 }, + closeModal: () => {} + }); + + await render( + hbs`` + ); + + assert.dom('.alert').doesNotExist(); + assert.dom('.modal-title').hasText('Add to collection(s)'); + assert.dom('.create-new-collection').exists({ count: 1 }); + assert.dom('.create-new-collection label').exists({ count: 2 }); + assert.dom('.create-new-collection input').exists({ count: 2 }); + assert.dom('.select-collections').doesNotExist(); + assert.dom('.modal-footer').exists({ count: 1 }); + assert.dom('#submit-collections').exists({ count: 1 }); + }); + + test('it renders error message if provided', async function (assert) { + this.setProperties({ + errorMessage: 'Oops, something went wrong', + pipeline: { id: 123 }, + closeModal: () => {} + }); + + await render( + hbs`` + ); + + assert.dom('.alert').exists({ count: 1 }); + }); + + test('it renders collections if they exist', async function (assert) { + this.setProperties({ + pipeline: { id: 123 }, + collections: [ + { id: 1, name: 'collection1', pipelineIds: [999] }, + { id: 2, name: 'collection2', pipelineIds: [123, 987, 456] }, + { id: 3, name: 'collection3', pipelineIds: [999, 456] } + ], + closeModal: () => {} + }); + + await render( + hbs`` + ); + + assert.dom('.select-collections').exists({ count: 1 }); + assert.dom('#collection-1').exists({ count: 1 }); + assert.dom('#collection-2').doesNotExist(); + assert.dom('#collection-3').exists({ count: 1 }); + }); + + test('it enables submit button when new collection name is input', async function (assert) { + this.setProperties({ + pipeline: { id: 123 }, + closeModal: () => {} + }); + + await render( + hbs`` + ); + + assert.dom('#submit-collections').isDisabled(); + + await fillIn('#new-collection-name-input', 'New Collection'); + + assert.dom('#submit-collections').isEnabled(); + }); + + test('it enables submit button when existing collection is selected', async function (assert) { + this.setProperties({ + pipeline: { id: 123 }, + collections: [{ id: 1, name: 'Test', pipelineIds: [] }], + closeModal: () => {} + }); + + await render( + hbs`` + ); + + assert.dom('#submit-collections').isDisabled(); + + await click('#collection-1'); + + assert.dom('#submit-collections').isEnabled(); + }); + } +); diff --git a/tests/unit/components/collection/modal/add-to-collection/util-test.js b/tests/unit/components/collection/modal/add-to-collection/util-test.js new file mode 100644 index 000000000..773516256 --- /dev/null +++ b/tests/unit/components/collection/modal/add-to-collection/util-test.js @@ -0,0 +1,40 @@ +import { module, test } from 'qunit'; +import { + createCollectionBody, + getCollectionsWithoutPipeline +} from 'screwdriver-ui/components/collection/modal/add-to-collection/util'; + +module( + 'Unit | Component | collection/modal/add-to-collection/util', + function () { + test('getCollectionsWithoutPipeline returns sorted list of collections', function (assert) { + assert.deepEqual( + getCollectionsWithoutPipeline( + [ + { id: 1, name: 'collection', pipelineIds: [] }, + { id: 2, name: 'abc', pipelineIds: [456] }, + { id: 3, name: 'something', pipelineIds: [456, 987] }, + { id: 4, name: 'xyz', pipelineIds: [456, 123, 987] }, + { id: 5, name: 'zzz', pipelineIds: [456, 987] } + ], + 123 + ), + [ + { id: 2, name: 'abc', pipelineIds: [456] }, + { id: 1, name: 'collection', pipelineIds: [] }, + { id: 3, name: 'something', pipelineIds: [456, 987] }, + { id: 5, name: 'zzz', pipelineIds: [456, 987] } + ] + ); + }); + + test('createCollectionBody creates request body', function (assert) { + assert.deepEqual(createCollectionBody('Test Collection', '', 123), { + name: 'Test Collection', + description: '', + pipelineIds: [123], + type: 'normal' + }); + }); + } +); From f5caf889f60ca45b169e34ccc2181d4449108712 Mon Sep 17 00:00:00 2001 From: Ming Hay Luk Date: Wed, 10 Jul 2024 13:40:43 -0700 Subject: [PATCH 2/2] Change button styles --- .../collection/modal/add-to-collection/styles.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/components/collection/modal/add-to-collection/styles.scss b/app/components/collection/modal/add-to-collection/styles.scss index 6ee58c0ef..210373259 100644 --- a/app/components/collection/modal/add-to-collection/styles.scss +++ b/app/components/collection/modal/add-to-collection/styles.scss @@ -1,10 +1,13 @@ @use 'screwdriver-colors' as colors; +@use 'screwdriver-button' as button; @mixin styles { #add-to-collection-modal { .modal-dialog { max-width: 50%; + @include button.styles; + .modal-body { .modal-title { font-size: 1.75rem; @@ -42,6 +45,11 @@ > .btn { border-radius: 3px; margin: 0.25rem; + + &.active { + background-color: rgba(colors.$sd-running, 0.75); + color: colors.$sd-white; + } } } }