From 3a33b0030c565b36d8cc5231ec9974a220ab4f0a Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Tue, 13 Jun 2017 15:21:20 -0700 Subject: [PATCH 01/11] Data model and services for adding products to a catalog --- .../phoenix/failures/CatalogFailures.scala | 5 +++ .../models/catalog/CatalogProduct.scala | 45 +++++++++++++++++++ .../phoenix/payloads/CatalogPayloads.scala | 2 + .../services/catalog/CatalogManager.scala | 21 ++++++++- phoenix-scala/sql/20170613151544 | 0 ...3151737__create_catalog_products_table.sql | 10 +++++ 6 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 phoenix-scala/phoenix/app/phoenix/models/catalog/CatalogProduct.scala create mode 100644 phoenix-scala/sql/20170613151544 create mode 100644 phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql diff --git a/phoenix-scala/phoenix/app/phoenix/failures/CatalogFailures.scala b/phoenix-scala/phoenix/app/phoenix/failures/CatalogFailures.scala index a837fd967e..a82a518413 100644 --- a/phoenix-scala/phoenix/app/phoenix/failures/CatalogFailures.scala +++ b/phoenix-scala/phoenix/app/phoenix/failures/CatalogFailures.scala @@ -7,4 +7,9 @@ object CatalogFailures { def apply(id: Int) = NotFoundFailure404(s"Catalog $id not found") } + + object ProductNotFoundInCatalog { + def apply(catalogId: Int, productId: Int) = + NotFoundFailure404(s"Product $productId not found in catalog $catalogId") + } } diff --git a/phoenix-scala/phoenix/app/phoenix/models/catalog/CatalogProduct.scala b/phoenix-scala/phoenix/app/phoenix/models/catalog/CatalogProduct.scala new file mode 100644 index 0000000000..60fff55fff --- /dev/null +++ b/phoenix-scala/phoenix/app/phoenix/models/catalog/CatalogProduct.scala @@ -0,0 +1,45 @@ +package phoenix.models.catalog + +import java.time.Instant + +import shapeless._ +import core.db._ +import core.db.ExPostgresDriver.api._ + +case class CatalogProduct(id: Int, + catalogId: Int, + productId: Int, + createdAt: Instant, + archivedAt: Option[Instant]) + extends FoxModel[CatalogProduct] + +object CatalogProduct { + def buildSeq(catalogId: Int, productIds: Seq[Int]): Seq[CatalogProduct] = + productIds.map( + productId ⇒ + CatalogProduct(id = 0, + catalogId = catalogId, + productId = productId, + createdAt = Instant.now, + archivedAt = None)) +} + +class CatalogProducts(tag: Tag) extends FoxTable[CatalogProduct](tag, "catalog_products") { + def id = column[Int]("id", O.PrimaryKey, O.AutoInc) + def catalogId = column[Int]("catalog_id") + def productId = column[Int]("product_id") + def createdAt = column[Instant]("created_at") + def archivedAt = column[Option[Instant]]("archived_at") + + def * = + (id, catalogId, productId, createdAt, archivedAt) <> ((CatalogProduct.apply _).tupled, CatalogProduct.unapply) +} + +object CatalogProducts + extends FoxTableQuery[CatalogProduct, CatalogProducts](new CatalogProducts(_)) + with ReturningId[CatalogProduct, CatalogProducts] { + val returningLens: Lens[CatalogProduct, Int] = lens[CatalogProduct].id + + def filterProduct(catalogId: Int, productId: Int): QuerySeq = + filter(cp ⇒ cp.catalogId === catalogId && cp.productId === productId) +} diff --git a/phoenix-scala/phoenix/app/phoenix/payloads/CatalogPayloads.scala b/phoenix-scala/phoenix/app/phoenix/payloads/CatalogPayloads.scala index 051609bc33..36334ce184 100644 --- a/phoenix-scala/phoenix/app/phoenix/payloads/CatalogPayloads.scala +++ b/phoenix-scala/phoenix/app/phoenix/payloads/CatalogPayloads.scala @@ -11,4 +11,6 @@ object CatalogPayloads { site: Option[String] = None, countryId: Option[Int] = None, defaultLanguage: Option[String] = None) + + case class AddProductsPayload(productIds: Seq[Int]) } diff --git a/phoenix-scala/phoenix/app/phoenix/services/catalog/CatalogManager.scala b/phoenix-scala/phoenix/app/phoenix/services/catalog/CatalogManager.scala index 413a9d7d23..94a84a8798 100644 --- a/phoenix-scala/phoenix/app/phoenix/services/catalog/CatalogManager.scala +++ b/phoenix-scala/phoenix/app/phoenix/services/catalog/CatalogManager.scala @@ -1,5 +1,7 @@ package phoenix.services.catalog +import java.time.Instant + import cats.data._ import cats.implicits._ import com.typesafe.scalalogging.LazyLogging @@ -10,7 +12,7 @@ import phoenix.payloads.CatalogPayloads._ import phoenix.responses.CatalogResponse._ import phoenix.utils.aliases._ import core.db._ -import phoenix.failures.CatalogFailures.CatalogNotFound +import phoenix.failures.CatalogFailures._ import phoenix.services.LogActivity object CatalogManager extends LazyLogging { @@ -38,4 +40,21 @@ object CatalogManager extends LazyLogging { response = build(catalog, country) _ ← * <~ LogActivity().withScope(ac.ctx.scope).catalogUpdated(au.model, response) } yield response + + def addProductsToCatalog( + catalogId: Int, + payload: AddProductsPayload)(implicit ec: EC, db: DB, ac: AC, au: AU): DbResultT[Unit] = + for { + catalog ← * <~ Catalogs.mustFindById404(catalogId) + _ ← * <~ CatalogProducts.createAll(CatalogProduct.buildSeq(catalog.id, payload.productIds)) + } yield () + + def removeProductFromCatalog(catalogId: Int, + productId: Int)(implicit ec: EC, db: DB, ac: AC, au: AU): DbResultT[Unit] = + for { + catalogProduct ← * <~ CatalogProducts + .filterProduct(catalogId, productId) + .mustFindOneOr(ProductNotFoundInCatalog(catalogId, productId)) + _ ← * <~ CatalogProducts.update(catalogProduct, catalogProduct.copy(archivedAt = Some(Instant.now))) + } yield () } diff --git a/phoenix-scala/sql/20170613151544 b/phoenix-scala/sql/20170613151544 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql b/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql new file mode 100644 index 0000000000..2dee965f21 --- /dev/null +++ b/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql @@ -0,0 +1,10 @@ +create table catalog_products( + id serial primary key, + catalog_id integer not null references catalogs(id) on update restrict on delete restrict, + product_id integer not null references products(form_id) on update restrict on delete restrict, + created_at generic_timestamp not null, + archived_at generic_timestamp_null +); + +create index catalogs_id_idx on catalog_products (catalog_id); +create index products_id_idx on catalog_products (product_id); From 3d80b381a2de0f9213de2cfe7f59820dad5d6524 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Tue, 13 Jun 2017 16:45:23 -0700 Subject: [PATCH 02/11] Wire up the catalog API routes --- .../phoenix/routes/admin/CatalogRoutes.scala | 16 ++++++- .../app/phoenix/services/LogActivity.scala | 16 ++++++- .../services/activity/CatalogTailored.scala | 8 ++++ .../services/catalog/CatalogManager.scala | 10 ++-- .../integration/CatalogIntegrationTest.scala | 47 +++++++++++++++++++ .../testutils/apis/PhoenixAdminApi.scala | 8 +++- ...3151737__create_catalog_products_table.sql | 2 +- 7 files changed, 100 insertions(+), 7 deletions(-) diff --git a/phoenix-scala/phoenix/app/phoenix/routes/admin/CatalogRoutes.scala b/phoenix-scala/phoenix/app/phoenix/routes/admin/CatalogRoutes.scala index ca2677a04a..c8cd9fac60 100644 --- a/phoenix-scala/phoenix/app/phoenix/routes/admin/CatalogRoutes.scala +++ b/phoenix-scala/phoenix/app/phoenix/routes/admin/CatalogRoutes.scala @@ -4,7 +4,7 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import phoenix.utils.http.JsonSupport._ import phoenix.models.account.User -import phoenix.payloads.CatalogPayloads.{CreateCatalogPayload, UpdateCatalogPayload} +import phoenix.payloads.CatalogPayloads._ import phoenix.services.Authenticator.AuthData import phoenix.services.catalog.CatalogManager import phoenix.utils.aliases._ @@ -31,6 +31,20 @@ object CatalogRoutes { mutateOrFailures { CatalogManager.updateCatalog(catalogId, payload) } + } ~ + pathPrefix("products") { + (post & pathEnd & entity(as[AddProductsPayload])) { payload ⇒ + mutateOrFailures { + CatalogManager.addProductsToCatalog(catalogId, payload) + } + } ~ + pathPrefix(IntNumber) { productId ⇒ + (delete & pathEnd) { + deleteOrFailures { + CatalogManager.removeProductFromCatalog(catalogId, productId) + } + } + } } } } diff --git a/phoenix-scala/phoenix/app/phoenix/services/LogActivity.scala b/phoenix-scala/phoenix/app/phoenix/services/LogActivity.scala index 1e0c03caad..701a28f7cc 100644 --- a/phoenix-scala/phoenix/app/phoenix/services/LogActivity.scala +++ b/phoenix-scala/phoenix/app/phoenix/services/LogActivity.scala @@ -39,7 +39,7 @@ import phoenix.responses.users.{CustomerResponse, UserResponse} import phoenix.services.carts.CartLineItemUpdater.foldQuantityPayload import phoenix.services.activity.AssignmentsTailored._ import phoenix.services.activity.CartTailored._ -import phoenix.services.activity.CatalogTailored.{CatalogCreated, CatalogUpdated} +import phoenix.services.activity.CatalogTailored._ import phoenix.services.activity.CategoryTailored._ import phoenix.services.activity.CouponsTailored._ import phoenix.services.activity.CustomerGroupsTailored._ @@ -493,6 +493,20 @@ case class LogActivity(implicit ac: AC) { )(implicit ec: EC): DbResultT[Activity] = Activities.log(CatalogUpdated(UserResponse.build(admin), catalog)) + def productsAddedToCatalog( + admin: User, + catalog: CatalogResponse.Root, + productIds: Seq[Int] + )(implicit ec: EC): DbResultT[Activity] = + Activities.log(ProductsAddedToCatalog(buildUser(admin), catalog, productIds)) + + def productRemovedFromCatalog( + admin: User, + catalogId: Int, + productId: Int + )(implicit ec: EC): DbResultT[Activity] = + Activities.log(ProductRemovedFromCatalog(buildUser(admin), catalogId, productId)) + /* Products */ def fullProductCreated(admin: Option[User], product: ProductResponse.Root, diff --git a/phoenix-scala/phoenix/app/phoenix/services/activity/CatalogTailored.scala b/phoenix-scala/phoenix/app/phoenix/services/activity/CatalogTailored.scala index e0d6e8bf5f..ae70abf766 100644 --- a/phoenix-scala/phoenix/app/phoenix/services/activity/CatalogTailored.scala +++ b/phoenix-scala/phoenix/app/phoenix/services/activity/CatalogTailored.scala @@ -11,4 +11,12 @@ object CatalogTailored { case class CatalogUpdated(admin: UserResponse, catalog: CatalogResponse.Root) extends ActivityBase[CatalogUpdated] + case class ProductsAddedToCatalog(admin: UserResponse.Root, + catalog: CatalogResponse.Root, + productIds: Seq[Int]) + extends ActivityBase[ProductsAddedToCatalog] + + case class ProductRemovedFromCatalog(admin: UserResponse.Root, catalogId: Int, productId: Int) + extends ActivityBase[ProductRemovedFromCatalog] + } diff --git a/phoenix-scala/phoenix/app/phoenix/services/catalog/CatalogManager.scala b/phoenix-scala/phoenix/app/phoenix/services/catalog/CatalogManager.scala index 94a84a8798..6ef55fa955 100644 --- a/phoenix-scala/phoenix/app/phoenix/services/catalog/CatalogManager.scala +++ b/phoenix-scala/phoenix/app/phoenix/services/catalog/CatalogManager.scala @@ -43,11 +43,14 @@ object CatalogManager extends LazyLogging { def addProductsToCatalog( catalogId: Int, - payload: AddProductsPayload)(implicit ec: EC, db: DB, ac: AC, au: AU): DbResultT[Unit] = + payload: AddProductsPayload)(implicit ec: EC, db: DB, ac: AC, au: AU): DbResultT[Root] = for { - catalog ← * <~ Catalogs.mustFindById404(catalogId) + catalog ← * <~ getCatalog(catalogId) _ ← * <~ CatalogProducts.createAll(CatalogProduct.buildSeq(catalog.id, payload.productIds)) - } yield () + _ ← * <~ LogActivity() + .withScope(ac.ctx.scope) + .productsAddedToCatalog(au.model, catalog, payload.productIds) + } yield catalog def removeProductFromCatalog(catalogId: Int, productId: Int)(implicit ec: EC, db: DB, ac: AC, au: AU): DbResultT[Unit] = @@ -56,5 +59,6 @@ object CatalogManager extends LazyLogging { .filterProduct(catalogId, productId) .mustFindOneOr(ProductNotFoundInCatalog(catalogId, productId)) _ ← * <~ CatalogProducts.update(catalogProduct, catalogProduct.copy(archivedAt = Some(Instant.now))) + _ ← * <~ LogActivity().withScope(ac.ctx.scope).productRemovedFromCatalog(au.model, catalogId, productId) } yield () } diff --git a/phoenix-scala/phoenix/test/integration/CatalogIntegrationTest.scala b/phoenix-scala/phoenix/test/integration/CatalogIntegrationTest.scala index 3fff5bfb63..d6bdd5248a 100644 --- a/phoenix-scala/phoenix/test/integration/CatalogIntegrationTest.scala +++ b/phoenix-scala/phoenix/test/integration/CatalogIntegrationTest.scala @@ -1,3 +1,5 @@ +import core.db._ +import phoenix.models.catalog._ import phoenix.payloads.CatalogPayloads._ import phoenix.responses.CatalogResponse import testutils._ @@ -72,4 +74,49 @@ class CatalogIntegrationTest } } + "POST /v1/catalogs/:id/products" - { + "succeeds in adding the product to the catalog" in new Catalog_ApiFixture with ProductSku_ApiFixture { + val payload = AddProductsPayload(productIds = Seq(product.id)) + catalogsApi(catalog.id).addProducts(payload).mustBeOk + + val catalogProduct = CatalogProducts.filterProduct(catalog.id, product.id).gimme.head + catalogProduct.archivedAt must === (None) + } + + "fails with invalid catalog" in new Catalog_ApiFixture with ProductSku_ApiFixture { + val payload = AddProductsPayload(productIds = Seq(product.id)) + catalogsApi(100).addProducts(payload).mustHaveStatus(404) + } + + "fails with invalid product" in new Catalog_ApiFixture with ProductSku_ApiFixture { + val payload = AddProductsPayload(productIds = Seq(100)) + catalogsApi(catalog.id).addProducts(payload).mustHaveStatus(400) + } + } + + "DELETE /v1/catalogs/:id/products" - { + "succeeds in removing a product to the catalog" in new Catalog_ApiFixture with ProductSku_ApiFixture { + val payload = AddProductsPayload(productIds = Seq(product.id)) + catalogsApi(catalog.id).addProducts(payload).mustBeOk + catalogsApi(catalog.id).deleteProduct(product.id) + + val catalogProduct = CatalogProducts.filterProduct(catalog.id, product.id).gimme.head + catalogProduct.archivedAt must !==(None) + } + + "fails to remove a product that was not in a catalog" in new Catalog_ApiFixture + with ProductSku_ApiFixture { + catalogsApi(catalog.id).deleteProduct(product.id).mustHaveStatus(404) + } + + "fails with invalid catalog" in new Catalog_ApiFixture with ProductSku_ApiFixture { + catalogsApi(100).deleteProduct(product.id).mustHaveStatus(404) + } + + "fails with invalid product" in new Catalog_ApiFixture with ProductSku_ApiFixture { + catalogsApi(100).deleteProduct(product.id).mustHaveStatus(404) + } + + } + } diff --git a/phoenix-scala/phoenix/test/integration/testutils/apis/PhoenixAdminApi.scala b/phoenix-scala/phoenix/test/integration/testutils/apis/PhoenixAdminApi.scala index 99c155b888..89087ae0a4 100644 --- a/phoenix-scala/phoenix/test/integration/testutils/apis/PhoenixAdminApi.scala +++ b/phoenix-scala/phoenix/test/integration/testutils/apis/PhoenixAdminApi.scala @@ -9,7 +9,7 @@ import phoenix.payloads.ActivityTrailPayloads._ import phoenix.payloads.AddressPayloads._ import phoenix.payloads.AssignmentPayloads._ import phoenix.payloads.CartPayloads._ -import phoenix.payloads.CatalogPayloads.{CreateCatalogPayload, UpdateCatalogPayload} +import phoenix.payloads.CatalogPayloads._ import phoenix.payloads.CategoryPayloads._ import phoenix.payloads.CouponPayloads._ import phoenix.payloads.CustomerGroupPayloads._ @@ -895,6 +895,12 @@ trait PhoenixAdminApi extends HttpSupport { self: FoxSuite ⇒ def update(payload: UpdateCatalogPayload)(implicit aa: TestAdminAuth): HttpResponse = PATCH(catalogPath, payload, aa.jwtCookie.some) + + def addProducts(payload: AddProductsPayload)(implicit aa: TestAdminAuth): HttpResponse = + POST(s"$catalogPath/products", payload, aa.jwtCookie.some) + + def deleteProduct(productId: Int)(implicit aa: TestAdminAuth): HttpResponse = + DELETE(s"$catalogPath/products/$productId", aa.jwtCookie.some) } object captureApi { diff --git a/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql b/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql index 2dee965f21..84cbf17cc2 100644 --- a/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql +++ b/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql @@ -1,7 +1,7 @@ create table catalog_products( id serial primary key, catalog_id integer not null references catalogs(id) on update restrict on delete restrict, - product_id integer not null references products(form_id) on update restrict on delete restrict, + product_id integer not null references object_forms(id) on update restrict on delete restrict, created_at generic_timestamp not null, archived_at generic_timestamp_null ); From 9d91e6e8355bbca5f70d0c0bc7835e166700e77e Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Tue, 13 Jun 2017 17:54:23 -0700 Subject: [PATCH 03/11] Triggers for adding the catalog to the product views --- .../R__products_catalog_view_functions.sql | 27 ++++++++++++++++++ .../sql/R__products_search_view_triggers.sql | 28 +++++++++++++++++++ ...__add_catalogs_to_products_search_view.sql | 2 ++ 3 files changed, 57 insertions(+) create mode 100644 phoenix-scala/sql/V5.20170613165711__add_catalogs_to_products_search_view.sql diff --git a/phoenix-scala/sql/R__products_catalog_view_functions.sql b/phoenix-scala/sql/R__products_catalog_view_functions.sql index fa0aa78624..d4d98e59cd 100644 --- a/phoenix-scala/sql/R__products_catalog_view_functions.sql +++ b/phoenix-scala/sql/R__products_catalog_view_functions.sql @@ -166,3 +166,30 @@ create trigger update_products_cat_from_product_sku_links_delete_trigger after delete on product_sku_links for each row execute procedure update_products_cat_from_product_sku_links_delete_fn(); + +create or replace function update_products_catalog_view_catalogs_fn() returns trigger as $$ +begin + update products_catalog_view + set + catalogs = catalogProducts.catalog_names + from (select + case when count(cp.id) = 0 + then + '[]' :: jsonb + else + jsonb_agg(c.name) + end as catalog_names + from catalogs as c + left join catalog_products as cp on (cp.catalog_id = c.id and cp.archived_at is null) + where cp.product_id = new.product_id) as catalogProducts + where products_catalog_view.product_id = new.product_id; + + return null; +end; +$$ language plpgsql; + +drop trigger if exists update_products_catalog_view_catalogs on catalog_products; +create trigger update_products_catalog_view_catalogs +after insert or update on catalog_products + for each row + execute procedure update_products_catalog_view_catalogs_fn(); diff --git a/phoenix-scala/sql/R__products_search_view_triggers.sql b/phoenix-scala/sql/R__products_search_view_triggers.sql index 2a57d2ae0e..ea677b3c5c 100644 --- a/phoenix-scala/sql/R__products_search_view_triggers.sql +++ b/phoenix-scala/sql/R__products_search_view_triggers.sql @@ -51,6 +51,27 @@ begin end; $$ language plpgsql; +create or replace function update_products_search_view_catalogs_fn() returns trigger as $$ +begin + update products_search_view + set + catalogs = catalogProducts.catalog_names + from (select + case when count(cp.id) = 0 + then + '[]' :: jsonb + else + jsonb_agg(c.name) + end as catalog_names + from catalogs as c + left join catalog_products as cp on (cp.catalog_id = c.id and cp.archived_at is null) + where cp.product_id = new.product_id) as catalogProducts + where products_search_view.product_id = new.product_id; + + return null; +end; +$$ language plpgsql; + drop trigger if exists update_products_search_view_taxonomies on product_taxon_links; create trigger update_products_search_view_taxonomies after insert or update or delete on product_taxon_links @@ -64,3 +85,10 @@ after update on taxonomy_taxon_links for each row when (old.full_path is distinct from new.full_path) execute procedure update_products_search_view_taxonomies_fn(); + +drop trigger if exists update_products_search_view_catalogs on catalog_products; +create trigger update_products_search_view_catalogs +after insert or update on catalog_products + for each row + execute procedure update_products_search_view_catalogs_fn(); + diff --git a/phoenix-scala/sql/V5.20170613165711__add_catalogs_to_products_search_view.sql b/phoenix-scala/sql/V5.20170613165711__add_catalogs_to_products_search_view.sql new file mode 100644 index 0000000000..53b0e0b837 --- /dev/null +++ b/phoenix-scala/sql/V5.20170613165711__add_catalogs_to_products_search_view.sql @@ -0,0 +1,2 @@ +alter table products_search_view add column catalogs jsonb; +alter table products_catalog_view add column catalogs jsonb; From ec376d1c675c7645130ae9d251aee7b4f7d26da9 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Wed, 14 Jun 2017 09:38:09 -0700 Subject: [PATCH 04/11] Add catalogs to green river mappings for product search and catalog views --- .../scala/consumer/elastic/mappings/ProductsCatalogView.scala | 3 ++- .../consumer/elastic/mappings/admin/ProductsSearchView.scala | 3 ++- .../sql/V5.20170613151737__create_catalog_products_table.sql | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/green-river/src/main/scala/consumer/elastic/mappings/ProductsCatalogView.scala b/green-river/src/main/scala/consumer/elastic/mappings/ProductsCatalogView.scala index 6126444410..2da7ad6e3b 100644 --- a/green-river/src/main/scala/consumer/elastic/mappings/ProductsCatalogView.scala +++ b/green-river/src/main/scala/consumer/elastic/mappings/ProductsCatalogView.scala @@ -32,7 +32,8 @@ final case class ProductsCatalogView()(implicit ec: EC) extends AvroTransformer field("taxonomies").nested( field("taxons", StringType).analyzer("upper_cased"), field("taxonomy", StringType).analyzer("upper_cased") - ) + ), + field("catalogs", StringType).index("not_analyzed") ) override def nestedFields() = List("albums", "tags", "skus", "taxonomies") diff --git a/green-river/src/main/scala/consumer/elastic/mappings/admin/ProductsSearchView.scala b/green-river/src/main/scala/consumer/elastic/mappings/admin/ProductsSearchView.scala index a9bc8b841e..8b00ea6d88 100644 --- a/green-river/src/main/scala/consumer/elastic/mappings/admin/ProductsSearchView.scala +++ b/green-river/src/main/scala/consumer/elastic/mappings/admin/ProductsSearchView.scala @@ -37,7 +37,8 @@ final case class ProductsSearchView()(implicit ec: EC) extends AvroTransformer { field("title", StringType).index("not_analyzed"), field("baseUrl", StringType).index("not_analyzed") ) - ) + ), + field("catalogs", StringType).index("not_analyzed") ) override def nestedFields() = List("albums", "skus", "tags", "taxonomies", "taxons") diff --git a/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql b/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql index 84cbf17cc2..50a6368081 100644 --- a/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql +++ b/phoenix-scala/sql/V5.20170613151737__create_catalog_products_table.sql @@ -3,7 +3,7 @@ create table catalog_products( catalog_id integer not null references catalogs(id) on update restrict on delete restrict, product_id integer not null references object_forms(id) on update restrict on delete restrict, created_at generic_timestamp not null, - archived_at generic_timestamp_null + archived_at generic_timestamp ); create index catalogs_id_idx on catalog_products (catalog_id); From f3c231339a67ac58bee3d986f212bc4f63c15010 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Wed, 14 Jun 2017 11:25:38 -0700 Subject: [PATCH 05/11] Store catalog mapping in an object for search views --- .../mappings/ProductsCatalogView.scala | 7 ++++-- .../mappings/admin/ProductsSearchView.scala | 7 ++++-- phoenix-scala/sql/20170613151544 | 0 .../R__products_catalog_view_functions.sql | 25 +++++++++++-------- .../sql/R__products_search_view_triggers.sql | 25 +++++++++++-------- 5 files changed, 38 insertions(+), 26 deletions(-) delete mode 100644 phoenix-scala/sql/20170613151544 diff --git a/green-river/src/main/scala/consumer/elastic/mappings/ProductsCatalogView.scala b/green-river/src/main/scala/consumer/elastic/mappings/ProductsCatalogView.scala index 2da7ad6e3b..6a0eea191b 100644 --- a/green-river/src/main/scala/consumer/elastic/mappings/ProductsCatalogView.scala +++ b/green-river/src/main/scala/consumer/elastic/mappings/ProductsCatalogView.scala @@ -33,8 +33,11 @@ final case class ProductsCatalogView()(implicit ec: EC) extends AvroTransformer field("taxons", StringType).analyzer("upper_cased"), field("taxonomy", StringType).analyzer("upper_cased") ), - field("catalogs", StringType).index("not_analyzed") + field("catalogs").nested( + field("id", IntegerType), + field("name", StringType).index("not_analyzed") + ) ) - override def nestedFields() = List("albums", "tags", "skus", "taxonomies") + override def nestedFields() = List("albums", "tags", "skus", "taxonomies", "catalogs") } diff --git a/green-river/src/main/scala/consumer/elastic/mappings/admin/ProductsSearchView.scala b/green-river/src/main/scala/consumer/elastic/mappings/admin/ProductsSearchView.scala index 8b00ea6d88..c82a04169c 100644 --- a/green-river/src/main/scala/consumer/elastic/mappings/admin/ProductsSearchView.scala +++ b/green-river/src/main/scala/consumer/elastic/mappings/admin/ProductsSearchView.scala @@ -38,8 +38,11 @@ final case class ProductsSearchView()(implicit ec: EC) extends AvroTransformer { field("baseUrl", StringType).index("not_analyzed") ) ), - field("catalogs", StringType).index("not_analyzed") + field("catalogs").nested( + field("id", IntegerType), + field("name", StringType).index("not_analyzed") + ) ) - override def nestedFields() = List("albums", "skus", "tags", "taxonomies", "taxons") + override def nestedFields() = List("albums", "skus", "tags", "taxonomies", "taxons", "catalogs") } diff --git a/phoenix-scala/sql/20170613151544 b/phoenix-scala/sql/20170613151544 deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/phoenix-scala/sql/R__products_catalog_view_functions.sql b/phoenix-scala/sql/R__products_catalog_view_functions.sql index d4d98e59cd..d9c6e7a1ab 100644 --- a/phoenix-scala/sql/R__products_catalog_view_functions.sql +++ b/phoenix-scala/sql/R__products_catalog_view_functions.sql @@ -171,17 +171,20 @@ create or replace function update_products_catalog_view_catalogs_fn() returns tr begin update products_catalog_view set - catalogs = catalogProducts.catalog_names - from (select - case when count(cp.id) = 0 - then - '[]' :: jsonb - else - jsonb_agg(c.name) - end as catalog_names - from catalogs as c - left join catalog_products as cp on (cp.catalog_id = c.id and cp.archived_at is null) - where cp.product_id = new.product_id) as catalogProducts + catalogs = case when catalogProducts.catalogs is null then + '[]' :: jsonb + else + catalogProducts.catalogs + end + from ( + select array_to_json(array_agg(row_to_json(cat))) :: jsonb as catalogs + from ( + select c.id, c.name + from catalogs as c + inner join catalog_products as cp on (cp.catalog_id = c.id) + where cp.archived_at is null and cp.product_id = new.product_id + ) as cat + ) as catalogProducts where products_catalog_view.product_id = new.product_id; return null; diff --git a/phoenix-scala/sql/R__products_search_view_triggers.sql b/phoenix-scala/sql/R__products_search_view_triggers.sql index ea677b3c5c..bcfdc2189d 100644 --- a/phoenix-scala/sql/R__products_search_view_triggers.sql +++ b/phoenix-scala/sql/R__products_search_view_triggers.sql @@ -55,17 +55,20 @@ create or replace function update_products_search_view_catalogs_fn() returns tri begin update products_search_view set - catalogs = catalogProducts.catalog_names - from (select - case when count(cp.id) = 0 - then - '[]' :: jsonb - else - jsonb_agg(c.name) - end as catalog_names - from catalogs as c - left join catalog_products as cp on (cp.catalog_id = c.id and cp.archived_at is null) - where cp.product_id = new.product_id) as catalogProducts + catalogs = case when catalogProducts.catalogs is null then + '[]' :: jsonb + else + catalogProducts.catalogs + end + from ( + select array_to_json(array_agg(row_to_json(cat))) :: jsonb as catalogs + from ( + select c.id, c.name + from catalogs as c + inner join catalog_products as cp on (cp.catalog_id = c.id) + where cp.archived_at is null and cp.product_id = new.product_id + ) as cat + ) as catalogProducts where products_search_view.product_id = new.product_id; return null; From f2cb57f7e82492f9840b0564e87f9725daf1b06c Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Wed, 14 Jun 2017 18:27:53 -0700 Subject: [PATCH 06/11] Fix linting errors --- ashes/src/components/catalog/catalog-page.jsx | 5 ++++- ashes/src/components/catalog/details.jsx | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ashes/src/components/catalog/catalog-page.jsx b/ashes/src/components/catalog/catalog-page.jsx index cb2c2ef86b..1615fd752d 100644 --- a/ashes/src/components/catalog/catalog-page.jsx +++ b/ashes/src/components/catalog/catalog-page.jsx @@ -84,7 +84,6 @@ class CatalogPage extends Component { componentWillReceiveProps(nextProps: Props) { if (nextProps.params.catalogId !== 'new' && nextProps.catalog) { const { name, site, countryId, defaultLanguage } = nextProps.catalog; - this.setState({ name, site, countryId, defaultLanguage }); } } @@ -163,7 +162,11 @@ class CatalogPage extends Component { if (isFetching) { return ; } +<<<<<<< f3c231339a67ac58bee3d986f212bc4f63c15010 + +======= +>>>>>>> Fix linting errors return (
diff --git a/ashes/src/components/catalog/details.jsx b/ashes/src/components/catalog/details.jsx index 2c843a2572..094e00bcf2 100644 --- a/ashes/src/components/catalog/details.jsx +++ b/ashes/src/components/catalog/details.jsx @@ -30,7 +30,11 @@ const CatalogDetails = (props: Props) => { const { defaultLanguage, name, site, countryId, countries } = props; const { onChange, onSubmit } = props; const { err } = props; +<<<<<<< f3c231339a67ac58bee3d986f212bc4f63c15010 +======= + +>>>>>>> Fix linting errors const country = _.find(countries, { 'id': countryId }); let languages = _.get(country, 'languages', []); From 578deb72aeb197ea4ca05f31d86eec39ca3d4805 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Thu, 15 Jun 2017 08:22:31 -0700 Subject: [PATCH 07/11] Display products that have been added to a catalog --- ashes/src/components/catalog/catalog-page.jsx | 16 ++- ashes/src/components/catalog/products.jsx | 118 ++++++++++++++++++ ashes/src/modules/catalog/index.js | 2 + ashes/src/modules/catalog/products-list.js | 21 ++++ ashes/src/routes/catalog.js | 6 + 5 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 ashes/src/components/catalog/products.jsx create mode 100644 ashes/src/modules/catalog/products-list.js diff --git a/ashes/src/components/catalog/catalog-page.jsx b/ashes/src/components/catalog/catalog-page.jsx index 1615fd752d..8bf55b6a6f 100644 --- a/ashes/src/components/catalog/catalog-page.jsx +++ b/ashes/src/components/catalog/catalog-page.jsx @@ -11,7 +11,7 @@ import { transitionTo, transitionToLazy } from 'browserHistory'; import PageNav from 'components/core/page-nav'; import SaveCancel from 'components/core/save-cancel'; import WaitAnimation from 'components/common/wait-animation'; -import { IndexLink } from 'components/link'; +import { IndexLink, Link } from 'components/link'; import { PageTitle } from 'components/section-title'; // data @@ -92,6 +92,15 @@ class CatalogPage extends Component { const { catalogId } = this.props.params; const params = { catalogId }; + let links = null; + if (!this.isNew) { + links = ( + + Products + + ); + } + return ( Details + {links} ); } @@ -162,11 +172,7 @@ class CatalogPage extends Component { if (isFetching) { return ; } -<<<<<<< f3c231339a67ac58bee3d986f212bc4f63c15010 - -======= ->>>>>>> Fix linting errors return (
diff --git a/ashes/src/components/catalog/products.jsx b/ashes/src/components/catalog/products.jsx new file mode 100644 index 0000000000..eec6ac6345 --- /dev/null +++ b/ashes/src/components/catalog/products.jsx @@ -0,0 +1,118 @@ +/* @flow */ + +// libs +import _ from 'lodash'; +import React, { Component } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { filterArchived } from 'elastic/archive'; +import * as dsl from 'elastic/dsl'; +import { bulkExportBulkAction, renderExportModal } from 'modules/bulk-export/helpers'; + +// actions +import { actions } from 'modules/catalog/products-list'; +/* import { addProduct, deleteProduct as unlinkProduct } from 'modules/taxons/details/taxon';*/ +import { bulkExport } from 'modules/bulk-export/bulk-export'; +import { actions as bulkActions } from 'modules/taxons/details/bulk'; + +// components +import { SectionTitle } from 'components/section-title'; +import SelectableSearchList from 'components/list-page/selectable-search-list'; +import ProductRow from 'components/products/product-row'; +import { makeTotalCounter } from 'components/list-page'; +import { ProductsAddModal } from 'components/products-add'; +import { Button } from 'components/core/button'; +import BulkActions from 'components/bulk-actions/bulk-actions'; +import BulkMessages from 'components/bulk-actions/bulk-messages'; +import { Link } from 'components/link'; + +type Props = { + params: { + catalogId: number, + }, +} + +class CatalogProducts extends Component { + props: Props; + + componentDidMount() { + const { catalogId } = this.props.params; + + this.props.actions.setExtraFilters([ + dsl.nestedTermFilter('catalogs.id', catalogId), + ]); + + this.props.actions.fetch(); + } + + get tableColumns(): Columns { + return [ + { field: 'productId', text: 'ID' }, + { field: 'image', text: 'Image', type: 'image' }, + { field: 'title', text: 'Name' }, + { field: 'skus', text: 'SKUs' }, + { field: 'state', text: 'State' }, + { field: '', render: this.unlinkButton }, + ]; + } + + renderRow(row: Product, index: number, columns: Columns, params: Object) { + const id = row.productId != null ? row.productId : 0; + const key = `taxon-product-${id}`; + + return ( + + ); + } + + addSearchFilters = (filters: Array, initial: boolean = false) => { + return this.props.actions.addSearchFilters(filterArchived(filters), initial) + }; + + render() { + const { list } = this.props; + + const searchActions = { + ...actions, + addSearchFilters: this.addSearchFilters, + }; + + return ( + id} + /> + ); + } +} + +const mapStateToProps = (state) => { + return { + list: _.get(state, 'catalogs.products', {}), + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + actions: { + ...bindActionCreators(actions, dispatch), + }, + bulkActionExport: bindActionCreators(bulkExport, dispatch), + bulkActions: bindActionCreators(bulkActions, dispatch), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(CatalogProducts); diff --git a/ashes/src/modules/catalog/index.js b/ashes/src/modules/catalog/index.js index a6ac6ac24c..eac34c0e8c 100644 --- a/ashes/src/modules/catalog/index.js +++ b/ashes/src/modules/catalog/index.js @@ -7,11 +7,13 @@ import { combineReducers } from 'redux'; import details from './details'; import list from './list'; import bulk from './bulk'; +import products from './products-list'; const catalogReducer = combineReducers({ details, list, bulk, + products, }); export default catalogReducer; diff --git a/ashes/src/modules/catalog/products-list.js b/ashes/src/modules/catalog/products-list.js new file mode 100644 index 0000000000..ae05de8bcc --- /dev/null +++ b/ashes/src/modules/catalog/products-list.js @@ -0,0 +1,21 @@ +/* @flow */ + +import makeLiveSearch from 'modules/live-search'; +import productsSearchTerms from 'modules/products/search-terms'; + +const { reducer, actions } = makeLiveSearch( + 'catalogs.products', + productsSearchTerms, + 'products_search_view/_search', + 'productsScope', + { + initialState: { sortBy: 'name' }, + rawSorts: ['name'], + skipInitialFetch: true, + } +); + +export { + reducer as default, + actions, +}; diff --git a/ashes/src/routes/catalog.js b/ashes/src/routes/catalog.js index 6a1de5326a..cc969c1259 100644 --- a/ashes/src/routes/catalog.js +++ b/ashes/src/routes/catalog.js @@ -29,6 +29,7 @@ import SkuImages from 'components/skus/images'; import CatalogListWrapper from 'components/catalog/list-wrapper'; import CatalogList from 'components/catalog/list'; import CatalogDetails from 'components/catalog/details'; +import CatalogProducts from 'components/catalog/products'; import CatalogPage from 'components/catalog/catalog-page'; const getRoutes = (jwt: Object) => { @@ -97,6 +98,11 @@ const getRoutes = (jwt: Object) => { component: CatalogPage, }, [ router.read('catalog-details', { isIndex: true, component: CatalogDetails }), + router.read('catalog-products', { + path: 'products', + title: 'Products', + component: CatalogProducts, + }), ]), ]); From 19805bfb2a0c3ff3166be307c924d0301fe0d004 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Thu, 15 Jun 2017 12:46:05 -0700 Subject: [PATCH 08/11] Wire up linking and unlinking products from the catalog --- ashes/src/components/catalog/products.jsx | 99 +++++++++++++++++++---- ashes/src/modules/catalog/details.js | 12 +++ 2 files changed, 96 insertions(+), 15 deletions(-) diff --git a/ashes/src/components/catalog/products.jsx b/ashes/src/components/catalog/products.jsx index eec6ac6345..2690cf2414 100644 --- a/ashes/src/components/catalog/products.jsx +++ b/ashes/src/components/catalog/products.jsx @@ -11,7 +11,7 @@ import { bulkExportBulkAction, renderExportModal } from 'modules/bulk-export/hel // actions import { actions } from 'modules/catalog/products-list'; -/* import { addProduct, deleteProduct as unlinkProduct } from 'modules/taxons/details/taxon';*/ +import { linkProducts, unlinkProduct } from 'modules/catalog/details'; import { bulkExport } from 'modules/bulk-export/bulk-export'; import { actions as bulkActions } from 'modules/taxons/details/bulk'; @@ -25,6 +25,7 @@ import { Button } from 'components/core/button'; import BulkActions from 'components/bulk-actions/bulk-actions'; import BulkMessages from 'components/bulk-actions/bulk-messages'; import { Link } from 'components/link'; +import Content from 'components/core/content/content'; type Props = { params: { @@ -32,9 +33,19 @@ type Props = { }, } +type State = { + modalVisible: boolean, + deletedProductId: ?number, +}; + class CatalogProducts extends Component { props: Props; + state: State = { + modalVisible: false, + deletedProductId: null, + }; + componentDidMount() { const { catalogId } = this.props.params; @@ -56,9 +67,44 @@ class CatalogProducts extends Component { ]; } + unlinkButton = (children: any, row: Product) => { + const inProgress = this.props.unlinkState.inProgress + && this.state.deletedProductId === row.productId; + + return ( + + ); + }; + + openModal = () => this.setState({ modalVisible: true }); + closeModal = () => this.setState({ modalVisible: false }); + + handleAddProduct = (product: Product) => { + const { actions, params: { catalogId } } = this.props; + actions.linkProducts(catalogId, { productIds: [product.productId] }). + then(this.props.actions.fetch); + }; + + handleUnlinkProduct = (product: Product, e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const { actions, params: { catalogId } } = this.props; + const { productId } = product; + + this.setState({ deletedProductId: productId }, () => { + actions.unlinkProduct(catalogId, productId).then(this.props.actions.fetch); + }); + }; + renderRow(row: Product, index: number, columns: Columns, params: Object) { const id = row.productId != null ? row.productId : 0; - const key = `taxon-product-${id}`; + const key = `catalog-product-${id}`; return ( _.get(state, 'catalogs.products'), actions); + return ( - id} - /> +
+ } + addTitle="Product" + onAddClick={this.openModal} + /> + id} + /> + +
); } } @@ -102,6 +167,8 @@ class CatalogProducts extends Component { const mapStateToProps = (state) => { return { list: _.get(state, 'catalogs.products', {}), + linkState: _.get(state.asyncActions, 'catalogLinkProducts', {}), + unlinkState: _.get(state.asyncActions, 'catalogUnlinkProduct', {}), }; }; @@ -109,6 +176,8 @@ const mapDispatchToProps = (dispatch) => { return { actions: { ...bindActionCreators(actions, dispatch), + linkProducts: bindActionCreators(linkProducts, dispatch), + unlinkProduct: bindActionCreators(unlinkProduct, dispatch), }, bulkActionExport: bindActionCreators(bulkExport, dispatch), bulkActions: bindActionCreators(bulkActions, dispatch), diff --git a/ashes/src/modules/catalog/details.js b/ashes/src/modules/catalog/details.js index 30fb403af8..41dfdf0225 100644 --- a/ashes/src/modules/catalog/details.js +++ b/ashes/src/modules/catalog/details.js @@ -27,9 +27,21 @@ const _updateCatalog = createAsyncActions( (id: number, payload: any) => Api.patch(`/catalogs/${id}`, payload) ); +const _linkProducts = createAsyncActions( + 'catalogLinkProducts', + (catalogId: number, payload: any) => Api.post(`/catalogs/${catalogId}/products`, payload) +); + +const _unlinkProduct = createAsyncActions( + 'catalogUnlinkProduct', + (catalogId: number, productId: number) => Api.delete(`/catalogs/${catalogId}/products/${productId}`) +); + export const fetchCatalog = _fetchCatalog.perform; export const createCatalog = _createCatalog.perform; export const updateCatalog = _updateCatalog.perform; +export const linkProducts = _linkProducts.perform; +export const unlinkProduct = _unlinkProduct.perform; const handleResponse = (state, response) => ({ ...state, catalog: response }); From 23cf9c3764832798b4cae37c27f7628c2cb93c6c Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Thu, 15 Jun 2017 13:17:24 -0700 Subject: [PATCH 09/11] A bit of styling clean up --- ashes/src/components/catalog/catalog-page.jsx | 5 +- ashes/src/components/catalog/details.jsx | 110 +++++++++--------- ashes/src/components/catalog/products.css | 7 ++ ashes/src/components/catalog/products.jsx | 5 +- ashes/src/components/core/content/content.css | 2 +- .../components/products-add/products-add.jsx | 1 + ashes/src/components/typeahead/input.jsx | 1 - 7 files changed, 71 insertions(+), 60 deletions(-) create mode 100644 ashes/src/components/catalog/products.css diff --git a/ashes/src/components/catalog/catalog-page.jsx b/ashes/src/components/catalog/catalog-page.jsx index 8bf55b6a6f..8ac10654b6 100644 --- a/ashes/src/components/catalog/catalog-page.jsx +++ b/ashes/src/components/catalog/catalog-page.jsx @@ -8,6 +8,7 @@ import { createSelector } from 'reselect'; import { transitionTo, transitionToLazy } from 'browserHistory'; // components +import Content from 'components/core/content/content'; import PageNav from 'components/core/page-nav'; import SaveCancel from 'components/core/save-cancel'; import WaitAnimation from 'components/common/wait-animation'; @@ -184,7 +185,9 @@ class CatalogPage extends Component { />
{this.localNav} - {upChildren} + + {upChildren} +
); } diff --git a/ashes/src/components/catalog/details.jsx b/ashes/src/components/catalog/details.jsx index 094e00bcf2..25d9cd97a9 100644 --- a/ashes/src/components/catalog/details.jsx +++ b/ashes/src/components/catalog/details.jsx @@ -46,62 +46,60 @@ const CatalogDetails = (props: Props) => { const languageItems = languages.map((lang) => [lang, lang]); return ( - -
- {err && ( -
- -
- )} - -
- - onChange('name', v)} - value={name} - /> - - - onChange('site', v)} - value={site} - /> - - - onChange('countryId', c)} - /> - - - onChange('defaultLanguage', l)} - /> - -
-
-
-
+
+ {err && ( +
+ +
+ )} + +
+ + onChange('name', v)} + value={name} + /> + + + onChange('site', v)} + value={site} + /> + + + onChange('countryId', c)} + /> + + + onChange('defaultLanguage', l)} + /> + +
+
+
); }; diff --git a/ashes/src/components/catalog/products.css b/ashes/src/components/catalog/products.css new file mode 100644 index 0000000000..129d1dccfa --- /dev/null +++ b/ashes/src/components/catalog/products.css @@ -0,0 +1,7 @@ +.list-container { + margin-top: -10px; +} + +.list-container :global(.fc-live-search) { + padding: 0; +} diff --git a/ashes/src/components/catalog/products.jsx b/ashes/src/components/catalog/products.jsx index 2690cf2414..86ee303ed3 100644 --- a/ashes/src/components/catalog/products.jsx +++ b/ashes/src/components/catalog/products.jsx @@ -27,6 +27,9 @@ import BulkMessages from 'components/bulk-actions/bulk-messages'; import { Link } from 'components/link'; import Content from 'components/core/content/content'; +// styles +import styles from './products.css'; + type Props = { params: { catalogId: number, @@ -132,7 +135,7 @@ class CatalogProducts extends Component { const TotalCounter = makeTotalCounter(state => _.get(state, 'catalogs.products'), actions); return ( -
+
} diff --git a/ashes/src/components/core/content/content.css b/ashes/src/components/core/content/content.css index 039d46ae3f..3f37704c34 100644 --- a/ashes/src/components/core/content/content.css +++ b/ashes/src/components/core/content/content.css @@ -1,3 +1,3 @@ .body { - padding: 20px; + margin: 40px 1.85% 40px 1.85%; } diff --git a/ashes/src/components/products-add/products-add.jsx b/ashes/src/components/products-add/products-add.jsx index fa5f79cd94..17d2f7404e 100644 --- a/ashes/src/components/products-add/products-add.jsx +++ b/ashes/src/components/products-add/products-add.jsx @@ -91,6 +91,7 @@ class ProductsAdd extends Component { className={styles.search} value={this.props.search} onChange={this.handleInputChange} + placeholder="Filter products..." autoFocus /> diff --git a/ashes/src/components/typeahead/input.jsx b/ashes/src/components/typeahead/input.jsx index 8613e2ba2b..50040426a5 100644 --- a/ashes/src/components/typeahead/input.jsx +++ b/ashes/src/components/typeahead/input.jsx @@ -23,7 +23,6 @@ const TypeaheadInput = ({ className, isFetching = false, ...rest }: Props) => { return ( - Date: Thu, 15 Jun 2017 13:23:47 -0700 Subject: [PATCH 10/11] Flow and linting errorss --- ashes/src/components/catalog/details.jsx | 5 ----- ashes/src/components/catalog/products.jsx | 10 ++++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ashes/src/components/catalog/details.jsx b/ashes/src/components/catalog/details.jsx index 25d9cd97a9..79f55be0cb 100644 --- a/ashes/src/components/catalog/details.jsx +++ b/ashes/src/components/catalog/details.jsx @@ -3,7 +3,6 @@ import _ from 'lodash'; import React from 'react'; -import Content from 'components/core/content/content'; import ContentBox from 'components/content-box/content-box'; import { Dropdown } from 'components/dropdown'; import ErrorAlerts from 'components/alerts/error-alerts'; @@ -30,11 +29,7 @@ const CatalogDetails = (props: Props) => { const { defaultLanguage, name, site, countryId, countries } = props; const { onChange, onSubmit } = props; const { err } = props; -<<<<<<< f3c231339a67ac58bee3d986f212bc4f63c15010 -======= - ->>>>>>> Fix linting errors const country = _.find(countries, { 'id': countryId }); let languages = _.get(country, 'languages', []); diff --git a/ashes/src/components/catalog/products.jsx b/ashes/src/components/catalog/products.jsx index 86ee303ed3..9ad055db18 100644 --- a/ashes/src/components/catalog/products.jsx +++ b/ashes/src/components/catalog/products.jsx @@ -34,6 +34,16 @@ type Props = { params: { catalogId: number, }, + actions: { + addSearchFilters: Function, + fetch: Function, + linkProducts: Function, + unlinkProduct: Function, + setExtraFilters: Function, + }, + list: ?Object, + linkState: Object, + unlinkState: Object, } type State = { From 8936b15c79fc015202dcd39b1c0ddfc0400fa5e8 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Thu, 15 Jun 2017 14:13:08 -0700 Subject: [PATCH 11/11] Add bulk action support --- .../components/bulk-actions/bulk-actions.jsx | 7 +- .../components/bulk-actions/bulk-messages.jsx | 10 ++- ashes/src/components/catalog/products.jsx | 82 +++++++++++++++---- 3 files changed, 76 insertions(+), 23 deletions(-) diff --git a/ashes/src/components/bulk-actions/bulk-actions.jsx b/ashes/src/components/bulk-actions/bulk-actions.jsx index 1684f5f394..a03c0b0acf 100644 --- a/ashes/src/components/bulk-actions/bulk-actions.jsx +++ b/ashes/src/components/bulk-actions/bulk-actions.jsx @@ -14,8 +14,10 @@ import { getStore } from 'lib/store-creator'; import SelectAdminsModal from '../users/select-modal'; -const mapDispatchToProps = (dispatch, {module}) => { - const {actions} = getStore(`${module}.bulk`); +const mapDispatchToProps = (dispatch, {bulkModule, module}) => { + const { actions } = bulkModule + ? getStore(bulkModule) + : getStore(`${module}.bulk`); return { bulkActions: bindActionCreators(actions, dispatch), @@ -25,6 +27,7 @@ const mapDispatchToProps = (dispatch, {module}) => { @connect(void 0, mapDispatchToProps) export default class BulkActions extends Component { static propTypes = { + bulkModule: PropTypes.string, module: PropTypes.string.isRequired, entity: PropTypes.string.isRequired, actions: PropTypes.arrayOf(PropTypes.array).isRequired, diff --git a/ashes/src/components/bulk-actions/bulk-messages.jsx b/ashes/src/components/bulk-actions/bulk-messages.jsx index 822dbd7e98..4867dc235b 100644 --- a/ashes/src/components/bulk-actions/bulk-messages.jsx +++ b/ashes/src/components/bulk-actions/bulk-messages.jsx @@ -15,6 +15,7 @@ import ErrorAlerts from '../alerts/error-alerts'; type Props = { storePath: string, + bulkModule?: string, module: string, entity: string, renderDetail: () => ReactElement, @@ -95,8 +96,11 @@ const mapState = (state, { storePath }) => ({ bulk: get(state, storePath, {}), }); -const mapActions = (dispatch, { module }) => ({ - bulkActions: bindActionCreators(getStore(`${module}.bulk`).actions, dispatch), -}); +const mapActions = (dispatch, { bulkModule, module }) => { + const { actions } = bulkModule ? getStore(bulkModule) : getStore(`${module}.bulk`); + return { + bulkActions: bindActionCreators(actions, dispatch), + }; +}; export default connect(mapState, mapActions)(BulkMessages); diff --git a/ashes/src/components/catalog/products.jsx b/ashes/src/components/catalog/products.jsx index 9ad055db18..d61a467398 100644 --- a/ashes/src/components/catalog/products.jsx +++ b/ashes/src/components/catalog/products.jsx @@ -25,7 +25,6 @@ import { Button } from 'components/core/button'; import BulkActions from 'components/bulk-actions/bulk-actions'; import BulkMessages from 'components/bulk-actions/bulk-messages'; import { Link } from 'components/link'; -import Content from 'components/core/content/content'; // styles import styles from './products.css'; @@ -41,7 +40,11 @@ type Props = { unlinkProduct: Function, setExtraFilters: Function, }, - list: ?Object, + bulkActions: { + exportByIds: Function, + }, + bulkExportAction: Function, + list: Object, linkState: Object, unlinkState: Object, } @@ -61,14 +64,20 @@ class CatalogProducts extends Component { componentDidMount() { const { catalogId } = this.props.params; - + this.props.actions.setExtraFilters([ dsl.nestedTermFilter('catalogs.id', catalogId), ]); - + this.props.actions.fetch(); } - + + get bulkActions(): Array { + return [ + bulkExportBulkAction(this.bulkExport, 'Products'), + ]; + } + get tableColumns(): Columns { return [ { field: 'productId', text: 'ID' }, @@ -80,6 +89,27 @@ class CatalogProducts extends Component { ]; } + bulkExport = (allChecked: boolean, toggledIds: Array) => { + const { exportByIds } = this.props.bulkActions; + const modalTitle = 'Products'; + const entity = 'products'; + + return renderExportModal(this.tableColumns, entity, modalTitle, exportByIds, toggledIds); + }; + + renderBulkDetails = (context: string, id: number) => { + const { list } = this.props; + const results = list.currentSearch().results.rows; + const filteredProduct = _.filter(results, (product) => product.id.toString() === id)[0]; + const productId = filteredProduct.productId; + + return ( + + Product {productId} + + ); + }; + unlinkButton = (children: any, row: Product) => { const inProgress = this.props.unlinkState.inProgress && this.state.deletedProductId === row.productId; @@ -130,7 +160,7 @@ class CatalogProducts extends Component { } addSearchFilters = (filters: Array, initial: boolean = false) => { - return this.props.actions.addSearchFilters(filterArchived(filters), initial) + return this.props.actions.addSearchFilters(filterArchived(filters), initial); }; render() { @@ -152,18 +182,34 @@ class CatalogProducts extends Component { addTitle="Product" onAddClick={this.openModal} /> - id} + + + id} /> + { linkProducts: bindActionCreators(linkProducts, dispatch), unlinkProduct: bindActionCreators(unlinkProduct, dispatch), }, - bulkActionExport: bindActionCreators(bulkExport, dispatch), + bulkExportAction: bindActionCreators(bulkExport, dispatch), bulkActions: bindActionCreators(bulkActions, dispatch), }; };