From 6da1200b64111d7c2e21d526071f543826c28868 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Thu, 24 Jul 2025 02:30:37 +0500 Subject: [PATCH 01/13] Refactor product routes to remove feature toggle constraints and simplify access --- config/routes/admin.rb | 19 ++++++++----------- config/routes/spree.rb | 15 +-------------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 9c3eb66324..f4efd0a64e 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -73,17 +73,14 @@ Openfoodnetwork::Application.routes.draw do post :import, on: :collection end - constraints FeatureToggleConstraint.new(:admin_style_v3) do - # This might be easier to arrange once we rename the controller to plain old "products" - post '/products/bulk_update', to: 'products_v3#bulk_update' - get '/products', to: 'products_v3#index' - # we already have DELETE admin/products/:id here - delete 'products_v3/:id', to: 'products_v3#destroy', as: 'product_destroy' - delete 'products_v3/destroy_variant/:id', to: 'products_v3#destroy_variant', as: 'destroy_variant' - post 'clone/:id', to: 'products_v3#clone', as: 'clone_product' - - resources :product_preview, only: [:show] - end + # This might be easier to arrange once we rename the controller to plain old "products" + post '/products/bulk_update', to: 'products_v3#bulk_update' + get '/products', to: 'products_v3#index' + # we already have DELETE admin/products/:id here + delete 'products_v3/:id', to: 'products_v3#destroy', as: 'product_destroy' + delete 'products_v3/destroy_variant/:id', to: 'products_v3#destroy_variant', as: 'destroy_variant' + post 'clone/:id', to: 'products_v3#clone', as: 'clone_product' + resources :product_preview, only: [:show] resources :variant_overrides do post :bulk_update, on: :collection diff --git a/config/routes/spree.rb b/config/routes/spree.rb index c6c84fddbe..be474a7383 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -50,16 +50,8 @@ Spree::Core::Engine.routes.draw do resources :users - constraints FeatureToggleConstraint.new(:admin_style_v3, negate: true) do - # Show old bulk products screen - resources :products, :index do - post :bulk_update, :on => :collection, :as => :bulk_update - end - end - - resources :products, except: :index do + resources :products do member do - get :clone get :group_buy_options get :seo end @@ -83,11 +75,6 @@ Spree::Core::Engine.routes.draw do end end - if Rails.env.development? - # duplicate old path for reference when admin_style_v3 enabled - resources :products_old, to: 'products#index', only: :index - end - get '/variants/search', :to => "variants#search", :as => :search_variants resources :properties From 44cbe55c965829b5606adbed8d312a8786bf606a Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 05:25:50 +0500 Subject: [PATCH 02/13] Update product routes and views for consistency and clarity --- app/views/admin/products_v3/_table.html.haml | 2 +- app/views/spree/admin/shared/_product_sub_menu.html.haml | 2 +- config/routes/admin.rb | 5 ++--- config/routes/spree.rb | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/views/admin/products_v3/_table.html.haml b/app/views/admin/products_v3/_table.html.haml index 5226461c94..15d7646c6c 100644 --- a/app/views/admin/products_v3/_table.html.haml +++ b/app/views/admin/products_v3/_table.html.haml @@ -1,4 +1,4 @@ -= form_with url: bulk_update_admin_products_path, method: :post, id: "products-form", += form_with url: admin_products_bulk_update_path, method: :post, id: "products-form", builder: BulkFormBuilder, html: { data: { 'turbo-frame': "_self", controller: "bulk-form", diff --git a/app/views/spree/admin/shared/_product_sub_menu.html.haml b/app/views/spree/admin/shared/_product_sub_menu.html.haml index 91fc3e6bc5..7fe1ccc3ed 100644 --- a/app/views/spree/admin/shared/_product_sub_menu.html.haml +++ b/app/views/spree/admin/shared/_product_sub_menu.html.haml @@ -1,6 +1,6 @@ - content_for :sub_menu do %ul#sub_nav.inline-menu - = tab :products, :products_v3 + = tab :products, :products_v3, url: admin_products_path = tab :properties = tab :variant_overrides, url: main_app.admin_inventory_path, match_path: '/inventory' if feature?(:inventory, spree_current_user.enterprises) = tab :import, url: main_app.admin_product_import_path, match_path: '/product_import' diff --git a/config/routes/admin.rb b/config/routes/admin.rb index f4efd0a64e..9b844fcfd0 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -74,9 +74,8 @@ Openfoodnetwork::Application.routes.draw do end # This might be easier to arrange once we rename the controller to plain old "products" - post '/products/bulk_update', to: 'products_v3#bulk_update' - get '/products', to: 'products_v3#index' - # we already have DELETE admin/products/:id here + post '/products/bulk_update', to: 'products_v3#bulk_update', as: 'products_bulk_update' + get '/products', to: 'products_v3#index', as: 'products' delete 'products_v3/:id', to: 'products_v3#destroy', as: 'product_destroy' delete 'products_v3/destroy_variant/:id', to: 'products_v3#destroy_variant', as: 'destroy_variant' post 'clone/:id', to: 'products_v3#clone', as: 'clone_product' diff --git a/config/routes/spree.rb b/config/routes/spree.rb index be474a7383..512f708b14 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -50,7 +50,7 @@ Spree::Core::Engine.routes.draw do resources :users - resources :products do + resources :products, except: [:index, :destroy] do member do get :group_buy_options get :seo From 0fa67c69fd56735705598e56d1f5e1b2ef8e1646 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 06:03:14 +0500 Subject: [PATCH 03/13] Remove bulk product update functionality Removes the bulk product update feature and its associated components: - Removes Angular-based bulk product editing controller and views - Deletes bulk product API endpoints and related controller actions - Removes product cloning and variant deletion functionality - Removes associated JavaScript tests and specs This appears to be part of a larger effort to modernize/simplify the product management interface, removing legacy Angular-based bulk editing in favor of a different approach. --- .../admin/bulk_product_update.js.coffee | 390 ------ .../resources/product_resource.js.coffee | 6 - .../admin/services/bulk_products.js.coffee | 76 -- app/controllers/api/v0/products_controller.rb | 27 - .../spree/admin/products_controller.rb | 27 - .../spree/admin/variants_controller.rb | 17 - .../spree/admin/products/index.html.haml | 14 - .../admin/products/index/_actions.html.haml | 11 - .../admin/products/index/_data.html.haml | 5 - .../admin/products/index/_filters.html.haml | 33 - .../admin/products/index/_header.html.haml | 10 - .../products/index/_indicators.html.haml | 14 - .../admin/products/index/_products.html.haml | 14 - .../products/index/_products_head.html.haml | 41 - .../index/_products_product.html.haml | 33 - .../index/_products_variant.html.haml | 39 - .../products/index/_save_button_row.html.haml | 3 - config/routes/api.rb | 1 - config/routes/spree.rb | 2 +- .../admin/bulk_product_update_spec.js.coffee | 1080 ----------------- .../services/bulk_products_spec.js.coffee | 188 --- 21 files changed, 1 insertion(+), 2030 deletions(-) delete mode 100644 app/assets/javascripts/admin/bulk_product_update.js.coffee delete mode 100644 app/assets/javascripts/admin/resources/resources/product_resource.js.coffee delete mode 100644 app/assets/javascripts/admin/services/bulk_products.js.coffee delete mode 100644 app/views/spree/admin/products/index.html.haml delete mode 100644 app/views/spree/admin/products/index/_actions.html.haml delete mode 100644 app/views/spree/admin/products/index/_data.html.haml delete mode 100644 app/views/spree/admin/products/index/_filters.html.haml delete mode 100644 app/views/spree/admin/products/index/_header.html.haml delete mode 100644 app/views/spree/admin/products/index/_indicators.html.haml delete mode 100644 app/views/spree/admin/products/index/_products.html.haml delete mode 100644 app/views/spree/admin/products/index/_products_head.html.haml delete mode 100644 app/views/spree/admin/products/index/_products_product.html.haml delete mode 100644 app/views/spree/admin/products/index/_products_variant.html.haml delete mode 100644 app/views/spree/admin/products/index/_save_button_row.html.haml delete mode 100644 spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee delete mode 100644 spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee deleted file mode 100644 index ab62e3f321..0000000000 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ /dev/null @@ -1,390 +0,0 @@ -angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $filter, $http, $window, $location, BulkProducts, DisplayProperties, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, Columns, tax_categories, RequestMonitor, SortOptions, ErrorsParser, ProductFiltersUrl) -> - $scope.StatusMessage = StatusMessage - - $scope.columns = Columns.columns - - $scope.variant_unit_options = VariantUnitManager.variantUnitOptions() - - $scope.RequestMonitor = RequestMonitor - $scope.pagination = BulkProducts.pagination - $scope.per_page_options = [ - {id: 15, name: t('js.admin.orders.index.per_page', results: 15)}, - {id: 50, name: t('js.admin.orders.index.per_page', results: 50)}, - {id: 100, name: t('js.admin.orders.index.per_page', results: 100)} - ] - - $scope.q = { - producerFilter: "" - categoryFilter: "" - importDateFilter: "" - query: "" - sorting: "" - } - - $scope.sorting = "name asc" - $scope.producers = producers - $scope.taxons = Taxons.all - $scope.tax_categories = tax_categories - $scope.page = 1 - $scope.per_page = 15 - $scope.products = BulkProducts.products - $scope.DisplayProperties = DisplayProperties - - $scope.sortOptions = SortOptions - - $scope.initialise = -> - $scope.q = ProductFiltersUrl.loadFromUrl($location.search()) - $scope.fetchProducts() - - $scope.$watchCollection '[q.query, q.producerFilter, q.categoryFilter, q.importDateFilter, per_page]', -> - $scope.page = 1 # Reset page when changing filters for new search - - $scope.changePage = (newPage) -> - $scope.page = newPage - $scope.fetchProducts() - - $scope.fetchProducts = -> - removeClearedValues() - params = { - 'q[name_cont]': $scope.q.query, - 'q[variants_supplier_id_eq]': $scope.q.producerFilter, - 'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter, - 'q[s]': $scope.sorting, - import_date: $scope.q.importDateFilter, - page: $scope.page, - per_page: $scope.per_page - } - RequestMonitor.load(BulkProducts.fetch(params).$promise).then -> - # update url with the filters used - $location.search(ProductFiltersUrl.generate($scope.q)) - $scope.resetProducts() - - removeClearedValues = -> - delete $scope.q.producerFilter if $scope.q.producerFilter == "0" - delete $scope.q.categoryFilter if $scope.q.categoryFilter == "0" - delete $scope.q.importDateFilter if $scope.q.importDateFilter == "0" - - $timeout -> - if $scope.showLatestImport - $scope.q.importDateFilter = $scope.importDates[1].id - - $scope.resetProducts = -> - DirtyProducts.clear() - StatusMessage.clear() - - $scope.updateOnHand = (product) -> - on_demand_variants = [] - if product.variants - on_demand_variants = (variant for id, variant of product.variants when variant.on_demand) - - unless product.on_demand || on_demand_variants.length > 0 - product.on_hand = $scope.onHand(product) - - - $scope.onHand = (product) -> - onHand = 0 - if product.hasOwnProperty("variants") and product.variants instanceof Object - for id, variant of product.variants - onHand = onHand + parseInt(if variant.on_hand > 0 then variant.on_hand else 0) - else - onHand = "error" - onHand - - $scope.shiftTab = (tab) -> - $scope.visibleTab.visible = false unless $scope.visibleTab == tab || $scope.visibleTab == undefined - tab.visible = !tab.visible - $scope.visibleTab = tab - - $scope.resetSelectFilters = -> - $scope.q.query = "" - $scope.q.producerFilter = "0" - $scope.q.categoryFilter = "0" - $scope.q.importDateFilter = "0" - $scope.fetchProducts() - - $scope.$watch 'sortOptions', (sort) -> - return unless sort && sort.predicate != "" - - $scope.sorting = sort.getSortingExpr(defaultDirection: "asc") - $scope.fetchProducts() - , true - - confirm_unsaved_changes = () -> - (DirtyProducts.count() > 0 and confirm(t("unsaved_changes_confirmation"))) or (DirtyProducts.count() == 0) - - editProductUrl = (product, variant) -> - "/admin/products/" + product.id + ((if variant then "/variants/" + variant.id else "")) + "/edit" - - $scope.editWarn = (product, variant) -> - if confirm_unsaved_changes() - $window.location.href = ProductFiltersUrl.buildUrl(editProductUrl(product, variant), $scope.q) - - $scope.toggleShowAllVariants = -> - showVariants = !DisplayProperties.showVariants 0 - $scope.products.forEach (product) -> - DisplayProperties.setShowVariants product.id, showVariants - DisplayProperties.setShowVariants 0, showVariants - - $scope.addVariant = (product) -> - # Set new variant category to same as last product variant category to keep compactibility with deleted variant callback to set new variant category - newVariantId = $scope.nextVariantId(); - newVariantCategoryId = product.variants[product.variants.length - 1]?.category_id - product.variants.push - id: newVariantId - unit_value: null - unit_description: null - on_demand: false - display_as: null - display_name: null - on_hand: null - price: null - tax_category_id: null - category_id: newVariantCategoryId - DisplayProperties.setShowVariants product.id, true - DirtyProducts.addVariantProperty(product.id, newVariantId, 'category_id', newVariantCategoryId) - - - $scope.nextVariantId = -> - $scope.variantIdCounter = 0 unless $scope.variantIdCounter? - $scope.variantIdCounter -= 1 - $scope.variantIdCounter - - $scope.deleteProduct = (product) -> - if confirm(t('are_you_sure')) - $http( - method: "DELETE" - url: "/api/v0/products/" + product.id - ).then (response) -> - $scope.products.splice $scope.products.indexOf(product), 1 - DirtyProducts.deleteProduct product.id - $scope.displayDirtyProducts() - - - $scope.deleteVariant = (product, variant) -> - if product.variants.length > 1 - if !$scope.variantSaved(variant) - $scope.removeVariant(product, variant) - else - if confirm(t("are_you_sure")) - $http( - method: "DELETE" - url: "/api/v0/products/" + product.id + "/variants/" + variant.id - ).then (response) -> - $scope.removeVariant(product, variant) - else - alert(t("delete_product_variant")) - - $scope.removeVariant = (product, variant) -> - product.variants.splice product.variants.indexOf(variant), 1 - DirtyProducts.deleteVariant product.id, variant.id - $scope.displayDirtyProducts() - - - $scope.cloneProduct = (product) -> - BulkProducts.cloneProduct product - - $scope.hasVariants = (product) -> - product.variants.length > 0 - - - $scope.hasUnit = (variant) -> - variant.variant_unit_with_scale? - - $scope.variantSaved = (variant) -> - variant.hasOwnProperty('id') && variant.id > 0 - - - $scope.hasOnDemandVariants = (product) -> - (variant for id, variant of product.variants when variant.on_demand).length > 0 - - - $scope.submitProducts = -> - # Pack pack $scope.products, so they will match the list returned from the server, - # then pack $scope.dirtyProducts, ensuring that the correct product info is sent to the server. - $scope.packProduct product for id, product of $scope.products - $scope.packProduct product for id, product of DirtyProducts.all() - - productsToSubmit = filterSubmitProducts(DirtyProducts.all()) - if productsToSubmit.length > 0 - $scope.updateProducts productsToSubmit # Don't submit an empty list - else - StatusMessage.display 'alert', t("products_change") - - - $scope.updateProducts = (productsToSubmit) -> - $scope.displayUpdating() - $http( - method: "POST" - url: "/admin/products/bulk_update" - data: - products: productsToSubmit - filters: - 'q[name_cont]': $scope.q.query - 'q[variants_supplier_id_eq]': $scope.q.producerFilter - 'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter - 'q[s]': $scope.sorting - import_date: $scope.q.importDateFilter - page: $scope.page - per_page: $scope.per_page - ).then((response) -> - DirtyProducts.clear() - BulkProducts.updateVariantLists(response.data.products || []) - $timeout -> $scope.displaySuccess() - ).catch (response) -> - if response.status == 400 && response.data.errors? - errorsString = ErrorsParser.toString(response.data.errors, response.status) - $scope.displayFailure t("products_update_error") + "\n" + errorsString - else - $scope.displayFailure t("products_update_error_data") + response.status - - $scope.cancel = (destination) -> - $window.location = destination - - $scope.packProduct = (product) -> - if product.variants - for id, variant of product.variants - $scope.packVariant variant - - - $scope.packVariant = (variant) -> - if variant.variant_unit_with_scale - match = variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/) - if match - variant.variant_unit = match[1] - variant.variant_unit_scale = parseFloat(match[2]) - else - variant.variant_unit = variant.variant_unit_with_scale - variant.variant_unit_scale = null - - if variant.hasOwnProperty("unit_value_with_description") - match = variant.unit_value_with_description.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/) - if match - variant.unit_value = parseFloat(match[1].replace(",", ".")) - variant.unit_value = null if isNaN(variant.unit_value) - if variant.unit_value && variant.variant_unit_scale - variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, variant.variant_unit_scale, 2)) - variant.unit_description = match[3] - - $scope.incrementLimit = -> - if $scope.limit < $scope.products.length - $scope.limit = $scope.limit + 5 - - - $scope.displayUpdating = -> - StatusMessage.display 'progress', t("saving") - - - $scope.displaySuccess = -> - StatusMessage.display 'success',t("products_changes_saved") - $scope.bulk_product_form.$setPristine() - - - $scope.displayFailure = (failMessage) -> - StatusMessage.display 'failure', t("products_update_error_msg") + " #{failMessage}" - - - $scope.displayDirtyProducts = -> - count = DirtyProducts.count() - switch count - when 0 then StatusMessage.clear() - when 1 then StatusMessage.display 'notice', t("one_product_unsaved") - else StatusMessage.display 'notice', t("products_unsaved", n: count) - - -filterSubmitProducts = (productsToFilter) -> - filteredProducts = [] - if productsToFilter instanceof Object - angular.forEach productsToFilter, (product) -> - if product.hasOwnProperty("id") - filteredProduct = {id: product.id} - filteredVariants = [] - hasUpdatableProperty = false - - if product.hasOwnProperty("variants") - angular.forEach product.variants, (variant) -> - result = filterSubmitVariant variant - filteredVariant = result.filteredVariant - variantHasUpdatableProperty = result.hasUpdatableProperty - filteredVariants.push filteredVariant if variantHasUpdatableProperty - - if product.hasOwnProperty("name") - filteredProduct.name = product.name - hasUpdatableProperty = true - if product.hasOwnProperty("price") - filteredProduct.price = product.price - hasUpdatableProperty = true - if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present - filteredProduct.on_hand = product.on_hand - hasUpdatableProperty = true - if product.hasOwnProperty("on_demand") and filteredVariants.length == 0 #only update if no variants present - filteredProduct.on_demand = product.on_demand - hasUpdatableProperty = true - if product.hasOwnProperty("inherits_properties") - filteredProduct.inherits_properties = product.inherits_properties - hasUpdatableProperty = true - if filteredVariants.length > 0 # Note that the name of the property changes to enable mass assignment of variants attributes with rails - filteredProduct.variants_attributes = filteredVariants - hasUpdatableProperty = true - filteredProducts.push filteredProduct if hasUpdatableProperty - - filteredProducts - - -filterSubmitVariant = (variant) -> - hasUpdatableProperty = false - filteredVariant = {} - if not variant.deleted_at? and variant.hasOwnProperty("id") - filteredVariant.id = variant.id unless variant.id <= 0 - if variant.hasOwnProperty("sku") - filteredVariant.sku = variant.sku - hasUpdatableProperty = true - if variant.hasOwnProperty("on_hand") - filteredVariant.on_hand = variant.on_hand - hasUpdatableProperty = true - if variant.hasOwnProperty("on_demand") - filteredVariant.on_demand = variant.on_demand - hasUpdatableProperty = true - if variant.hasOwnProperty("price") - filteredVariant.price = variant.price - hasUpdatableProperty = true - if variant.hasOwnProperty("unit_value") - filteredVariant.unit_value = variant.unit_value - hasUpdatableProperty = true - if variant.hasOwnProperty("unit_description") - filteredVariant.unit_description = variant.unit_description - hasUpdatableProperty = true - if variant.hasOwnProperty("display_name") - filteredVariant.display_name = variant.display_name - hasUpdatableProperty = true - if variant.hasOwnProperty("tax_category_id") - filteredVariant.tax_category_id = variant.tax_category_id - hasUpdatableProperty = true - if variant.hasOwnProperty("category_id") - filteredVariant.primary_taxon_id = variant.category_id - hasUpdatableProperty = true - if variant.hasOwnProperty("display_as") - filteredVariant.display_as = variant.display_as - hasUpdatableProperty = true - if variant.hasOwnProperty("producer_id") - filteredVariant.supplier_id = variant.producer_id - hasUpdatableProperty = true - if variant.hasOwnProperty("variant_unit_with_scale") - filteredVariant.variant_unit = variant.variant_unit - filteredVariant.variant_unit_scale = variant.variant_unit_scale - hasUpdatableProperty = true - if variant.hasOwnProperty("variant_unit_name") - filteredVariant.variant_unit_name = variant.variant_unit_name - hasUpdatableProperty = true - - {filteredVariant: filteredVariant, hasUpdatableProperty: hasUpdatableProperty} - - -toObjectWithIDKeys = (array) -> - object = {} - - for i of array - if array[i] instanceof Object and array[i].hasOwnProperty("id") - object[array[i].id] = angular.copy(array[i]) - object[array[i].id].variants = toObjectWithIDKeys(array[i].variants) if array[i].hasOwnProperty("variants") and array[i].variants instanceof Array - - object diff --git a/app/assets/javascripts/admin/resources/resources/product_resource.js.coffee b/app/assets/javascripts/admin/resources/resources/product_resource.js.coffee deleted file mode 100644 index 7d842d8eed..0000000000 --- a/app/assets/javascripts/admin/resources/resources/product_resource.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -angular.module("admin.resources").factory 'ProductResource', ($resource) -> - $resource('/admin/product/:id/:action.json', {}, { - 'index': - url: '/api/v0/products/bulk_products.json' - method: 'GET' - }) diff --git a/app/assets/javascripts/admin/services/bulk_products.js.coffee b/app/assets/javascripts/admin/services/bulk_products.js.coffee deleted file mode 100644 index a2823bf997..0000000000 --- a/app/assets/javascripts/admin/services/bulk_products.js.coffee +++ /dev/null @@ -1,76 +0,0 @@ -angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetcher, $http) -> - new class BulkProducts - products: [] - pagination: {} - - fetch: (params) -> - ProductResource.index params, (data) => - @products.length = 0 - @addProducts data.products - angular.extend(@pagination, data.pagination) - - cloneProduct: (product) -> - $http.post("/api/v0/products/" + product.id + "/clone").then (response) => - dataFetcher("/api/v0/products/" + response.data.id + "?template=bulk_show").then (newProduct) => - @unpackProduct newProduct - @insertProductAfter(product, newProduct) - - updateVariantLists: (serverProducts) -> - for server_product in serverProducts - product = @findProductInList(server_product.id, @products) - product.variants = server_product.variants - @loadVariantUnitValues product.variants - - find: (id) -> - @findProductInList id, @products - - findProductInList: (id, product_list) -> - products = (product for product in product_list when product.id == id) - if products.length == 0 then null else products[0] - - addProducts: (products) -> - for product in products - @unpackProduct product - @products.push product - - insertProductAfter: (product, newProduct) -> - index = @products.indexOf(product) - @products.splice(index + 1, 0, newProduct) - - unpackProduct: (product) -> - @loadVariantUnit product - - loadVariantUnit: (product) -> - @loadVariantUnitValues product.variants if product.variants - - loadVariantUnitValues: (variants) -> - for variant in variants - @loadVariantUnitValue variant - - loadVariantUnitValue: (variant) -> - variant.variant_unit_with_scale = - if variant.variant_unit && variant.variant_unit_scale && variant.variant_unit != 'items' - "#{variant.variant_unit}_#{variant.variant_unit_scale}" - else if variant.variant_unit - variant.variant_unit - else - null - - unit_value = @variantUnitValue variant - unit_value = if unit_value? then unit_value else '' - variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim() - - variantUnitValue: (variant) -> - if variant.unit_value? - if variant.variant_unit_scale - variant_unit_value = @divideAsInteger variant.unit_value, variant.variant_unit_scale - parseFloat(window.bigDecimal.round(variant_unit_value, 2)) - else - variant.unit_value - else - null - - # forces integer division to avoid javascript floating point imprecision - # using one billion as the multiplier so that it works for numbers with up to 9 decimal places - divideAsInteger: (a, b) -> - (a * 1000000000) / (b * 1000000000) diff --git a/app/controllers/api/v0/products_controller.rb b/app/controllers/api/v0/products_controller.rb index 6f95e1e64e..1ba016cc5a 100644 --- a/app/controllers/api/v0/products_controller.rb +++ b/app/controllers/api/v0/products_controller.rb @@ -38,39 +38,12 @@ module Api end end - def destroy - authorize! :delete, Spree::Product - @product = product_finder.find_product - authorize! :delete, @product - @product.destroyed_by = current_api_user - @product.destroy - head :no_content - end - - def bulk_products - @products = product_finder.bulk_products - - render_paged_products @products - end - def overridable @products = product_finder.products_for_producers render_paged_products @products, ::Api::Admin::ProductSimpleSerializer end - # POST /api/products/:product_id/clone - # - def clone - authorize! :create, Spree::Product - original_product = product_finder.find_product_to_be_cloned - authorize! :update, original_product - - @product = original_product.duplicate - - render json: @product, serializer: Api::Admin::ProductSerializer, status: :created - end - private def product_finder diff --git a/app/controllers/spree/admin/products_controller.rb b/app/controllers/spree/admin/products_controller.rb index f51ba5503e..2e036ffdb6 100644 --- a/app/controllers/spree/admin/products_controller.rb +++ b/app/controllers/spree/admin/products_controller.rb @@ -19,11 +19,6 @@ module Spree before_action :load_spree_api_key, only: [:index, :variant_overrides] before_action :strip_new_properties, only: [:create, :update] - def index - @current_user = spree_current_user - @show_latest_import = params[:latest_import] || false - end - def show session[:return_to] ||= request.referer redirect_to( action: :edit ) @@ -65,28 +60,6 @@ module Spree end end - def bulk_update - product_set = product_set_from_params - - product_set.collection.each { |p| authorize! :update, p } - - if product_set.save - redirect_to main_app.bulk_products_api_v0_products_path(bulk_index_query) - elsif product_set.errors.present? - render json: { errors: product_set.errors }, status: :bad_request - else - render body: nil, status: :internal_server_error - end - end - - def clone - @new = @product.duplicate - raise "Clone failed" unless @new.save - - flash[:success] = t('.success') - redirect_to spree.admin_products_url - end - def group_buy_options @url_filters = ::ProductFilters.new.extract(request.query_parameters) end diff --git a/app/controllers/spree/admin/variants_controller.rb b/app/controllers/spree/admin/variants_controller.rb index 7ec309ea93..83fe131045 100644 --- a/app/controllers/spree/admin/variants_controller.rb +++ b/app/controllers/spree/admin/variants_controller.rb @@ -72,25 +72,8 @@ module Spree render json: @variants, each_serializer: ::Api::Admin::VariantSerializer end - def destroy - @url_filters = ::ProductFilters.new.extract(request.query_parameters) - - @variant = Spree::Variant.find(params[:id]) - flash[:success] = delete_variant - - redirect_to spree.admin_product_variants_url(params[:product_id], @url_filters) - end - protected - def delete_variant - if VariantDeleter.new.delete(@variant) - Spree.t('notice_messages.variant_deleted') - else - Spree.t('notice_messages.variant_not_deleted') - end - end - def create_before @object.save end diff --git a/app/views/spree/admin/products/index.html.haml b/app/views/spree/admin/products/index.html.haml deleted file mode 100644 index b78c73cb4f..0000000000 --- a/app/views/spree/admin/products/index.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -= render 'spree/admin/products/index/header' -= render 'spree/admin/products/index/data' -= admin_inject_available_units - -%div{ "ng-app": 'ofn.admin', "ng-controller": 'AdminProductEditCtrl', "ng-init": 'initialise()' } - - = render 'spree/admin/products/index/filters' - %div{ 'ng-cloak' => true } - = render 'spree/admin/products/index/actions' - = render 'spree/admin/products/index/indicators' - = render 'spree/admin/products/index/products' - - %div{'ng-show' => "!RequestMonitor.loading && products.length > 0" } - = render partial: 'admin/shared/angular_pagination' diff --git a/app/views/spree/admin/products/index/_actions.html.haml b/app/views/spree/admin/products/index/_actions.html.haml deleted file mode 100644 index aea4ebeb61..0000000000 --- a/app/views/spree/admin/products/index/_actions.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -.controls.sixteen.columns.alpha{ 'ng-hide' => 'RequestMonitor.loading || products.length == 0' } - .ten.columns.alpha.index-controls - .per-page{'ng-show' => '!RequestMonitor.loading && products.length > 0'} - %input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'min-search' => 999, 'ng-model' => 'per_page', 'ng-change' => 'fetchProducts()'} - - %span.per-page-feedback - {{ 'spree.admin.orders.index.results_found' | t:{number: pagination.results} }} - {{ 'spree.admin.orders.index.viewing' | t:{start: ((pagination.page -1) * pagination.per_page) +1, end: ((pagination.page -1) * pagination.per_page) + products.length} }} - - .six.columns.omega - %columns-dropdown{ action: "#{controller_name}_#{action_name}" } diff --git a/app/views/spree/admin/products/index/_data.html.haml b/app/views/spree/admin/products/index/_data.html.haml deleted file mode 100644 index 13a6dc780e..0000000000 --- a/app/views/spree/admin/products/index/_data.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -= admin_inject_producers(@producers) -= admin_inject_taxons(@taxons) -= admin_inject_tax_categories(@tax_categories) -= admin_inject_spree_api_key(@spree_api_key) -= admin_inject_column_preferences diff --git a/app/views/spree/admin/products/index/_filters.html.haml b/app/views/spree/admin/products/index/_filters.html.haml deleted file mode 100644 index 57615eb09e..0000000000 --- a/app/views/spree/admin/products/index/_filters.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -%fieldset - %legend{align: 'center'}= t(:search) - - .filters.sixteen.columns.alpha.omega - .quick_search.three.columns.alpha - %label{ for: 'quick_filter' } - %br - %input.quick-search.fullwidth{ name: "quick_filter", type: 'text', placeholder: t('admin.quick_search'), "ng-keypress": "$event.keyCode === 13 && fetchProducts()", "ng-model": 'q.query' } - .one.columns   - .filter_select.three.columns - %label{ for: 'producer_filter' }= t 'producer' - %br - %select.fullwidth{ id: 'producer_filter', "ofn-select2-min-search": 5, "ng-model": 'q.producerFilter', "ng-options": 'producer.id as producer.name for producer in producers' } - .filter_select.three.columns - %label{ for: 'category_filter' }= t 'category' - %br - %select.fullwidth{ id: 'category_filter', "ofn-select2-min-search": 5, "ng-model": 'q.categoryFilter', "ng-options": 'taxon.id as taxon.name for taxon in taxons' } - .filter_select.three.columns - %label{ for: 'import_filter' }= t 'import_date' - %br - %select.fullwidth{ id: 'import_date_filter', "ofn-select2-min-search": 5, "ng-model": 'q.importDateFilter', "ng-init": "importDates = #{@import_dates}; showLatestImport = #{@show_latest_import}", "ng-options": 'date.id as date.name for date in importDates' } - - .filter_clear.three.columns.omega - %label{ for: 'clear_all_filters' } - %br - %input.fullwidth.red{ :type => 'button', :id => 'clear_all_filters', :value => t('admin.clear_filters'), 'ng-click' => "resetSelectFilters()" } - - .clearfix - - .actions.filter-actions - %div - %a.button.icon-search{'ng-click' => 'fetchProducts()'} - = t(:filter_results) diff --git a/app/views/spree/admin/products/index/_header.html.haml b/app/views/spree/admin/products/index/_header.html.haml deleted file mode 100644 index f3314a1a39..0000000000 --- a/app/views/spree/admin/products/index/_header.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- content_for :page_title do - = t('.title') - -- content_for :page_actions do - %div{ :class => "toolbar" } - %ul{ :class => "actions header-action-links inline-menu" } - %li#new_product_link - = button_link_to t(:new_product), new_object_url, { :icon => 'icon-plus', :id => 'admin_new_product' } - -= render partial: 'spree/admin/shared/product_sub_menu' diff --git a/app/views/spree/admin/products/index/_indicators.html.haml b/app/views/spree/admin/products/index/_indicators.html.haml deleted file mode 100644 index 2907c04074..0000000000 --- a/app/views/spree/admin/products/index/_indicators.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -.sixteen.columns.alpha#loading{ 'ng-if' => 'RequestMonitor.loading' } - = render partial: "components/admin_spinner" - %h1 - = t('.title') - -.sixteen.columns.alpha{ 'ng-show' => '!RequestMonitor.loading && products.length == 0 && q.query.length == 0' } - %h1#no_results= t('.no_products') - -.sixteen.columns.alpha{ 'ng-show' => '!RequestMonitor.loading && products.length == 0 && q.query.length != 0' } - %h1#no_results - = t('.no_results') - ' - {{q.query}} - ' diff --git a/app/views/spree/admin/products/index/_products.html.haml b/app/views/spree/admin/products/index/_products.html.haml deleted file mode 100644 index 032c783e7f..0000000000 --- a/app/views/spree/admin/products/index/_products.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -.sixteen.columns.alpha{ 'ng-hide' => 'RequestMonitor.loading || products.length == 0' } - %form{ name: 'bulk_product_form', autocomplete: "off" } - %save-bar{ dirty: "bulk_product_form.$dirty", persist: "false" } - %input.red{ type: "button", value: t(:save_changes), "ng-click": "submitProducts()", "ng-disabled": "!bulk_product_form.$dirty" } - %input{ type: "button", value: t(:close), 'ng-click' => "cancel('#{admin_products_path}')" } - - %table.index#listing_products.bulk - - = render 'spree/admin/products/index/products_head' - - %tbody{ 'ng-repeat' => 'product in products', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } - - = render 'spree/admin/products/index/products_product' - = render 'spree/admin/products/index/products_variant' diff --git a/app/views/spree/admin/products/index/_products_head.html.haml b/app/views/spree/admin/products/index/_products_head.html.haml deleted file mode 100644 index 9e6d7e7065..0000000000 --- a/app/views/spree/admin/products/index/_products_head.html.haml +++ /dev/null @@ -1,41 +0,0 @@ -%colgroup - %col.actions - %col.image{ "ng-show": 'columns.image.visible' } - %col.producer{ "ng-show": 'columns.producer.visible' } - %col.sku{ "ng-show": 'columns.sku.visible' } - %col.name{ "ng-show": 'columns.name.visible' } - %col.unit{ "ng-show": 'columns.unit.visible' } - %col.display_as{ "ng-show": 'columns.unit.visible' } - %col.price{ "ng-show": 'columns.price.visible' } - %col.on_hand{ "ng-show": 'columns.on_hand.visible' } - %col.on_demand{ "ng-show": 'columns.on_demand.visible' } - %col.category{ "ng-show": 'columns.category.visible' } - %col.tax_category{ "ng-show": 'columns.tax_category.visible' } - %col.inherits_properties{ "ng-show": 'columns.inherits_properties.visible' } - %col.import_date{ "ng-show": 'columns.import_date.visible' } - %col.actions - %col.actions - %col.actions - -%thead - %tr{ "ng-controller": "ColumnsCtrl" } - %th.left-actions - %a{ 'ng-click' => 'toggleShowAllVariants()', :style => 'color: red; cursor: pointer' } - = t(:expand_all) - %th.image{ 'ng-show' => 'columns.image.visible' } - %th.producer{ 'ng-show' => 'columns.producer.visible' }=t('admin.producer') - %th.sku{ 'ng-show' => 'columns.sku.visible' }=t('admin.sku') - %th.name{ 'ng-show' => 'columns.name.visible' } - = render partial: 'spree/admin/shared/sortable_header', locals: {column_name: 'name'} - %th.unit{ 'ng-show' => 'columns.unit.visible' }=t('.unit') - %th.display_as{ 'ng-show' => 'columns.unit.visible' }=t('.display_as') - %th.price{ 'ng-show' => 'columns.price.visible' }=t('admin.price') - %th.on_hand{ 'ng-show' => 'columns.on_hand.visible' }=t('admin.on_hand') - %th.on_demand{ 'ng-show' => 'columns.on_demand.visible' }=t('admin.on_demand?') - %th.category{ 'ng-show' => 'columns.category.visible' }=t('.category') - %th.tax_category{ 'ng-show' => 'columns.tax_category.visible' }=t('.tax_category') - %th.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' }=t('.inherits_properties?') - %th.import_date{ 'ng-show' => 'columns.import_date.visible' }=t('.import_date') - %th.actions - %th.actions - %th.actions diff --git a/app/views/spree/admin/products/index/_products_product.html.haml b/app/views/spree/admin/products/index/_products_product.html.haml deleted file mode 100644 index aa10e5e29a..0000000000 --- a/app/views/spree/admin/products/index/_products_product.html.haml +++ /dev/null @@ -1,33 +0,0 @@ -%tr.product{ :id => "p_{{product.id}}" } - %td.left-actions - %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants", 'ng-show' => 'hasVariants(product)' } - %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" } - %td.image{ 'ng-show' => 'columns.image.visible' } - %a{class: 'image-modal'} - %img{'ng-src' => '{{ product.thumb_url }}'} - %td.producer{ 'ng-show' => 'columns.producer.visible' } - %td.sku{ 'ng-show' => 'columns.sku.visible' } - %input{ 'ng-model' => "product.sku", :name => 'product_sku', 'ofn-track-product' => 'sku', :type => 'text' } - %td.name{ 'ng-show' => 'columns.name.visible' } - %input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' } - %td.unit{ 'ng-show' => 'columns.unit.visible' } - %td.display_as{ 'ng-show' => 'columns.unit.visible' } - %td.price{ 'ng-show' => 'columns.price.visible' } - %input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text', 'ng-hide' => 'hasVariants(product)' } - %td.on_hand{ 'ng-show' => 'columns.on_hand.visible' } - %span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-if' => '!hasOnDemandVariants(product) && (hasVariants(product) || product.on_demand)' } - %input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ofn-track-product' => 'on_hand', 'ng-if' => '!(hasVariants(product) || product.on_demand)', :type => 'number' } - %td.on_demand{ 'ng-show' => 'columns.on_demand.visible' } - %input.field{ 'ng-model' => 'product.on_demand', :name => 'on_demand', 'ofn-track-product' => 'on_demand', :type => 'checkbox', 'ng-hide' => 'hasVariants(product)' } - %td.category{ 'ng-if' => 'columns.category.visible' } - %td.tax_category{ 'ng-if' => 'columns.tax_category.visible' } - %td.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' } - %input{ 'ng-model' => 'product.inherits_properties', :name => 'inherits_properties', 'ofn-track-product' => 'inherits_properties', type: "checkbox" } - %td.import_date{ 'ng-show' => 'columns.import_date.visible' } - %span {{(product.import_date | date:"MMMM dd, yyyy HH:mm") || ""}} - %td.actions - %a{ 'ng-click' => 'editWarn(product)', :class => "edit-product icon-edit no-text", 'ofn-with-tip' => t(:edit) } - %td.actions - %a{ 'ng-click' => 'cloneProduct(product)', :class => "clone-product icon-copy no-text", 'ofn-with-tip' => t(:clone) } - %td.actions - %a{ 'ng-click' => 'deleteProduct(product)', :class => "delete-product icon-trash no-text", 'ofn-with-tip' => t(:remove) } diff --git a/app/views/spree/admin/products/index/_products_variant.html.haml b/app/views/spree/admin/products/index/_products_variant.html.haml deleted file mode 100644 index 4aa5e13f78..0000000000 --- a/app/views/spree/admin/products/index/_products_variant.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -%tr.variant{ :id => "v_{{variant.id}}", 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'DisplayProperties.showVariants(product.id)', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" } - %td.left-actions - %a{ :class => "variant-item icon-caret-right", 'ng-hide' => "$last" } - %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last", 'ofn-with-tip' => t('.new_variant') } - %td{ 'ng-show' => 'columns.image.visible' } - %td{ 'ng-show' => 'columns.producer.visible' } - %select.fullwidth{ "data-controller": "tom-select", 'ng-model' => 'variant.producer_id', :name => 'producer_id', 'ofn-track-variant' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' } - %td{ 'ng-show' => 'columns.sku.visible' } - %input{ 'ng-model' => "variant.sku", :name => 'variant_sku', 'ofn-track-variant' => 'sku', :type => 'text' } - %td{ 'ng-show' => 'columns.name.visible' } - %input{ 'ng-model' => 'variant.display_name', :name => 'variant_display_name', 'ofn-track-variant' => 'display_name', :type => 'text', placeholder: "{{ product.name }}" } - %td.unit_value{ 'ng-show' => 'columns.unit.visible' } - %select.no-search{ "data-controller": "tom-select", 'ng-model' => 'variant.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-variant' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' } - %input{ 'ng-model' => 'variant.unit_value_with_description', :name => 'variant_unit_value_with_description', 'ofn-track-variant' => 'unit_value_with_description', :type => 'text', :placeholder => 'value', 'ng-show' => "hasUnit(variant)" } - %input{ 'ng-model' => 'variant.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-variant' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "variant.variant_unit_with_scale == 'items'", :type => 'text' } - %td.display_as{ 'ng-show' => 'columns.unit.visible' } - %input{ 'ofn-display-as' => 'variant', 'ng-model' => 'variant.display_as', name: 'variant_display_as', 'ofn-track-variant' => 'display_as', type: 'text', placeholder: '{{ placeholder_text }}' } - %td{ 'ng-show' => 'columns.price.visible' } - %input{ 'ng-model' => 'variant.price', 'ofn-decimal' => :true, :name => 'variant_price', 'ofn-track-variant' => 'price', :type => 'text' } - %td{ 'ng-show' => 'columns.on_hand.visible' } - %input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ng-if' => '!variant.on_demand', 'ofn-track-variant' => 'on_hand', :type => 'number' } - %span{ :name => 'variant_on_hand', 'ng-if' => 'variant.on_demand' } - = t(:on_demand) - %td{ 'ng-show' => 'columns.on_demand.visible' } - %input.field{ 'ng-model' => 'variant.on_demand', :name => 'variant_on_demand', 'ofn-track-variant' => 'on_demand', :type => 'checkbox' } - %td{ 'ng-show' => 'columns.category.visible' } - %input.fullwidth{ type: 'text', id: "p{{product.id}}_category_id", 'ng-model' => 'variant.category_id', 'ofn-taxon-autocomplete' => '', 'ofn-track-variant' => 'category_id', 'multiple-selection' => 'false', placeholder: 'Category' } - %td{ 'ng-show' => 'columns.tax_category.visible' } - %select.select2{ name: 'variant_tax_category_id', "ofn-track-variant": 'tax_category_id', "ng-model": 'variant.tax_category_id', "ng-options": 'tax_category.id as tax_category.name for tax_category in tax_categories' } - %option{ value: '' }= t(:none) - %td{ 'ng-show' => 'columns.inherits_properties.visible' } - %td{ 'ng-show' => 'columns.import_date.visible' } - %span {{variant.import_date | date:"MMMM dd, yyyy HH:mm"}} - %td.actions - %a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text", 'ng-show' => "variantSaved(variant)", 'ofn-with-tip' => t(:edit) } - %td.actions - %span.icon-warning-sign{ 'ng-if' => 'variant.variant_overrides_count > 0', 'ofn-with-tip' => "{{ 'spree.admin.products.index.products_variant.variant_has_n_overrides' | t:{n: variant.variant_overrides_count} }}" } - %td.actions - %a{ 'ng-click' => 'deleteVariant(product,variant)', "ng-class" => '{disabled: product.variants.length < 2}', :class => "delete-variant icon-trash no-text", 'ofn-with-tip' => t(:remove) } diff --git a/app/views/spree/admin/products/index/_save_button_row.html.haml b/app/views/spree/admin/products/index/_save_button_row.html.haml deleted file mode 100644 index 8818bf84b3..0000000000 --- a/app/views/spree/admin/products/index/_save_button_row.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -.sixteen.columns.alpha{ 'ng-hide' => 'RequestMonitor.loading || products.length == 0', style: "margin-bottom: 10px" } - .four.columns.alpha - %input.four.columns.alpha{ :type => 'button', :value => t(:save_changes), 'ng-click' => 'submitProducts()'} diff --git a/config/routes/api.rb b/config/routes/api.rb index 30bd7cd1b1..2545554739 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -16,7 +16,6 @@ Openfoodnetwork::Application.routes.draw do namespace :v0 do resources :products do collection do - get :bulk_products get :overridable end post :clone diff --git a/config/routes/spree.rb b/config/routes/spree.rb index 512f708b14..a133ff7418 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -68,7 +68,7 @@ Spree::Core::Engine.routes.draw do end end - resources :variants do + resources :variants, except: :destroy do collection do post :update_positions end diff --git a/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee deleted file mode 100644 index 532f569d69..0000000000 --- a/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee +++ /dev/null @@ -1,1080 +0,0 @@ -describe "filtering products for submission to database", -> - it "accepts an object or an array and only returns an array", -> - expect(filterSubmitProducts([])).toEqual [] - expect(filterSubmitProducts({})).toEqual [] - expect(filterSubmitProducts(1: - id: 1 - name: "lala" - )).toEqual [ - id: 1 - name: "lala" - ] - expect(filterSubmitProducts([ - id: 1 - name: "lala" - ])).toEqual [ - id: 1 - name: "lala" - ] - expect(filterSubmitProducts(1)).toEqual [] - expect(filterSubmitProducts("2")).toEqual [] - expect(filterSubmitProducts(null)).toEqual [] - - it "only returns products which have an id property", -> - expect(filterSubmitProducts([ - { - id: 1 - name: "p1" - } - { - notanid: 2 - name: "p2" - } - ])).toEqual [ - id: 1 - name: "p1" - ] - - it "does not return a product object for products which have no propeties other than an id", -> - expect(filterSubmitProducts([ - { - id: 1 - someunwantedproperty: "something" - } - { - id: 2 - name: "p2" - } - ])).toEqual [ - id: 2 - name: "p2" - ] - - it "does not return an on_hand count when a product has variants", -> - testProduct = - id: 1 - on_hand: 5 - variants: [ - id: 1 - on_hand: 5 - price: 12.0 - ] - - expect(filterSubmitProducts([testProduct])).toEqual [ - id: 1 - variants_attributes: [ - id: 1 - on_hand: 5 - price: 12.0 - ] - ] - - it "returns variants as variants_attributes", -> - testProduct = - id: 1 - variants: [ - id: 1 - on_hand: 5 - price: 12.0 - unit_value: 250 - unit_description: "(bottle)" - ] - - expect(filterSubmitProducts([testProduct])).toEqual [ - id: 1 - variants_attributes: [ - id: 1 - on_hand: 5 - price: 12.0 - unit_value: 250 - unit_description: "(bottle)" - ] - ] - - it "ignores variants without an id, and those for which deleted_at is not null", -> - testProduct = - id: 1 - variants: [ - { - id: 1 - on_hand: 3 - price: 5.0 - } - { - on_hand: 1 - price: 15.0 - } - { - id: 2 - on_hand: 2 - deleted_at: new Date() - price: 20.0 - } - ] - - expect(filterSubmitProducts([testProduct])).toEqual [ - id: 1 - variants_attributes: [ - id: 1 - on_hand: 3 - price: 5.0 - ] - ] - - it "returns variants with a negative id without that id", -> - testProduct = - id: 1 - variants: [ - id: -1 - on_hand: 5 - price: 12.0 - unit_value: 250 - unit_description: "(bottle)" - ] - - expect(filterSubmitProducts([testProduct])).toEqual [ - id: 1 - variants_attributes: [ - on_hand: 5 - price: 12.0 - unit_value: 250 - unit_description: "(bottle)" - ] - ] - - it "does not return variants_attributes property if variants is an empty array", -> - testProduct = - id: 1 - price: 10 - variants: [] - - expect(filterSubmitProducts([testProduct])).toEqual [ - id: 1 - price: 10 - ] - - it "returns variant_unit_with_scale as variant_unit and variant_unit_scale", -> - testProduct = - id: 1 - variants: [ - id: 1 - variant_unit: 'weight' - variant_unit_scale: 1 - variant_unit_with_scale: 'weight_1' - ] - - expect(filterSubmitProducts([testProduct])).toEqual [ - id: 1 - variants_attributes: [ - id: 1 - variant_unit: 'weight' - variant_unit_scale: 1 - ] - ] - - it "returns stock properties of a product if no variant is provided", -> - testProduct = - id: 1 - name: "TestProduct" - on_hand: 0 - on_demand: false - - expect(filterSubmitProducts([testProduct])).toEqual [ - id: 1 - name: "TestProduct" - on_hand: 0 - on_demand: false - ] - - it "only returns the properties of products which ought to be updated", -> - testProduct = - id: 1 - name: "TestProduct" - description: "" - deleted_at: null - meta_keywords: null - shipping_category_id: null - created_at: null - updated_at: null - on_hand: 0 - on_demand: false - group_buy: null - group_buy_unit_size: null - variants: [ - id: 1 - on_hand: 2 - price: 10.0 - unit_value: 250 - tax_category_id: null - unit_description: "(bottle)" - display_as: "bottle" - display_name: "nothing" - producer_id: 5 - variant_unit: 'volume' - variant_unit_scale: 1 - variant_unit_name: 'loaf' - variant_unit_with_scale: 'volume_1' - ] - - expect(filterSubmitProducts([testProduct])).toEqual [ - id: 1 - name: "TestProduct" - variants_attributes: [ - id: 1 - on_hand: 2 - price: 10.0 - unit_value: 250 - tax_category_id: null - unit_description: "(bottle)" - display_as: "bottle" - display_name: "nothing" - supplier_id: 5 - variant_unit: 'volume' - variant_unit_scale: 1 - variant_unit_name: 'loaf' - ] - ] - -describe "AdminProductEditCtrl", -> - $ctrl = $scope = $timeout = $httpBackend = BulkProducts = DirtyProducts = DisplayProperties = ProductFiltersUrl = windowStub = null - - beforeEach -> - module "ofn.admin" - module ($provide)-> - $provide.value "producers", [] - $provide.value "taxons", [] - $provide.value "tax_categories", [] - $provide.value 'SpreeApiKey', 'API_KEY' - $provide.value 'columns', [] - null - module "admin.products" - module ($provide)-> - $provide.value "availableUnits", "g,kg,T,mL,L,kL" - null - - beforeEach inject((_$controller_, _$timeout_, $rootScope, _$httpBackend_, _BulkProducts_, _DirtyProducts_, _DisplayProperties_, _ProductFiltersUrl_) -> - $scope = $rootScope.$new() - $ctrl = _$controller_ - $timeout = _$timeout_ - $httpBackend = _$httpBackend_ - BulkProducts = _BulkProducts_ - DirtyProducts = _DirtyProducts_ - DisplayProperties = _DisplayProperties_ - ProductFiltersUrl = _ProductFiltersUrl_ - - # Stub the window object so we don't get redirected when href is updated - windowStub = {navigator: {userAgent: 'foo'}, location: {href: ''}} - - $ctrl "AdminProductEditCtrl", {$scope: $scope, $timeout: $timeout, $window: windowStub} - ) - - describe "loading data upon initialisation", -> - beforeEach -> - spyOn($scope, "fetchProducts").and.returnValue "nothing" - - it "gets a list of producers and then resets products with a list of data", -> - $scope.initialise() - expect($scope.fetchProducts.calls.count()).toBe 1 - - it "gets a list of products applying filters from the url", inject ($location) -> - query = 'lala' - producerFilter = 2 - categoryFilter = 5 - sorting = 'name desc' - importDateFilter = '2020-06-08' - $location.search({query: query, producerFilter: producerFilter, categoryFilter: categoryFilter, sorting: sorting, importDateFilter: importDateFilter}) - - $scope.initialise() - - expect($scope.q.query).toBe query - expect($scope.q.categoryFilter).toBe categoryFilter - expect($scope.q.sorting).toBe sorting - expect($scope.q.importDateFilter).toBe importDateFilter - - describe "fetching products", -> - $q = null - deferred = null - - beforeEach inject((_$q_) -> - $q = _$q_ - ) - - beforeEach -> - deferred = $q.defer() - deferred.resolve() - spyOn $scope, "resetProducts" - spyOn(BulkProducts, "fetch").and.returnValue deferred.promise - - it "calls resetProducts after data has been received", -> - $scope.fetchProducts() - $scope.$digest() - expect($scope.resetProducts).toHaveBeenCalled() - - it "updates url with filter after data has been received", inject ($location, $window) -> - query = 'lala' - producerFilter = 2 - categoryFilter = 5 - sorting = 'name desc' - importDateFilter = '2020-06-08' - - $scope.q.query = query - $scope.q.producerFilter = producerFilter - $scope.q.categoryFilter = categoryFilter - $scope.q.sorting = sorting - $scope.q.importDateFilter = importDateFilter - - $scope.fetchProducts() - $scope.$digest() - - encodedSorting = $window.encodeURIComponent(sorting) - encodedDate = $window.encodeURIComponent(importDateFilter) - expect($location.url()).toBe( - "?producerFilter=#{producerFilter}&categoryFilter=#{categoryFilter}&query=#{query}&sorting=#{encodedSorting}&importDateFilter=#{encodedDate}" - ) - - describe "resetting products", -> - beforeEach -> - spyOn DirtyProducts, "clear" - $scope.products = {} - $scope.resetProducts [ - { - id: 1 - name: "P1" - } - { - id: 3 - name: "P2" - } - ] - - it "resets dirtyProducts", -> - expect(DirtyProducts.clear).toHaveBeenCalled() - - describe "sorting products", -> - it "sorts products", -> - spyOn $scope, "fetchProducts" - - $scope.sortOptions.toggle('name') - $scope.$apply() - - expect($scope.sorting).toEqual 'name desc' - expect($scope.fetchProducts).toHaveBeenCalled() - - describe "updating the product on hand count", -> - it "updates when product is not available on demand", -> - spyOn($scope, "onHand").and.returnValue 123 - product = {on_demand: false} - $scope.updateOnHand(product) - expect(product.on_hand).toEqual 123 - - it "updates when product's variants are not available on demand", -> - spyOn($scope, "onHand").and.returnValue 123 - product = {on_demand: false, variants: [{on_demand: false}]} - $scope.updateOnHand(product) - expect(product.on_hand).toEqual 123 - - it "does nothing when the product is available on demand", -> - product = {on_demand: true} - $scope.updateOnHand(product) - expect(product.on_hand).toBeUndefined() - - it "does nothing when one of the variants is available on demand", -> - product = - on_demand: false - variants: [ - {on_demand: false, on_hand: 10} - {on_demand: true, on_hand: Infinity} - ] - $scope.updateOnHand(product) - expect(product.on_hand).toBeUndefined() - - - describe "getting on_hand counts when products have variants", -> - p1 = undefined - p2 = undefined - p3 = undefined - beforeEach -> - p1 = variants: - 1: - id: 1 - on_hand: 1 - - 2: - id: 2 - on_hand: 2 - - 3: - id: 3 - on_hand: 3 - - p2 = variants: - 4: - id: 4 - not_on_hand: 1 - - 5: - id: 5 - on_hand: 2 - - 6: - id: 6 - on_hand: 3 - - p3 = - not_variants: - 7: - id: 7 - on_hand: 1 - - 8: - id: 8 - on_hand: 2 - - variants: - 9: - id: 9 - on_hand: 3 - - it "sums variant on_hand properties", -> - expect($scope.onHand(p1)).toEqual 6 - - it "ignores items in variants without an on_hand property (adds 0)", -> - expect($scope.onHand(p2)).toEqual 5 - - it "ignores on_hand properties of objects in arrays which are not named 'variants' (adds 0)", -> - expect($scope.onHand(p3)).toEqual 3 - - it "returns 'error' if not given an object with a variants property that is an object", -> - expect($scope.onHand([])).toEqual "error" - expect($scope.onHand(not_variants: [])).toEqual "error" - - - describe "determining whether a product has variants that are available on demand", -> - it "returns true when at least one variant does", -> - product = - variants: [ - {on_demand: false} - {on_demand: true} - ] - expect($scope.hasOnDemandVariants(product)).toBe(true) - - it "returns false otherwise", -> - product = - variants: [ - {on_demand: false} - {on_demand: false} - ] - expect($scope.hasOnDemandVariants(product)).toBe(false) - - - describe "determining whether a product has variants", -> - it "returns true when it does", -> - product = - variants: [{id: 1}, {id: 2}] - expect($scope.hasVariants(product)).toBe(true) - - it "returns false when it does not", -> - product = - variants: [] - expect($scope.hasVariants(product)).toBe(false) - - - describe "determining whether a product has a unit", -> - it "returns true when it does", -> - variant ={variant_unit_with_scale: 'weight_1000'} - - expect($scope.hasUnit(variant)).toBe(true) - - it "returns false when its unit is undefined", -> - variant = {} - expect($scope.hasUnit(variant)).toBe(false) - - - describe "determining whether a variant has been saved", -> - it "returns true when it has a positive id", -> - variant = {id: 1} - expect($scope.variantSaved(variant)).toBe(true) - - it "returns false when it has no id", -> - variant = {} - expect($scope.variantSaved(variant)).toBe(false) - - it "returns false when it has a negative id", -> - variant = {id: -1} - expect($scope.variantSaved(variant)).toBe(false) - - - describe "submitting products to be updated", -> - describe "packing products", -> - beforeEach -> - window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["multiply"] - window.bigDecimal.multiply.and.callFake (a, b, c) -> (a * b).toFixed(c) - - it "packs each variant", -> - spyOn $scope, "packVariant" - testVariant = {id: 1} - testProduct = - id: 1 - variants: {1: testVariant} - - $scope.packProduct(testProduct) - - expect($scope.packVariant).toHaveBeenCalledWith(testVariant) - - describe "packing variants", -> - beforeEach -> - window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["multiply"] - window.bigDecimal.multiply.and.callFake (a, b, c) -> (a * b).toFixed(c) - - it "extracts variant_unit_with_scale into variant_unit and variant_unit_scale", -> - testVariant = - id: 1 - variant_unit: 'weight' - variant_unit_scale: 1 - variant_unit_with_scale: 'volume_1000' - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - id: 1 - variant_unit: 'volume' - variant_unit_scale: 1000 - variant_unit_with_scale: 'volume_1000' - - it "extracts when variant_unit_with_scale is 'items'", -> - testVariant = - id: 1 - variant_unit: 'weight' - variant_unit_scale: 1 - variant_unit_with_scale: 'items' - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - id: 1 - variant_unit: 'items' - variant_unit_scale: null - variant_unit_with_scale: 'items' - - it "extracts unit_value and unit_description from unit_value_with_description", -> - testVariant = {unit_value_with_description: "250.5 (bottle)"} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: 250.5 - unit_description: "(bottle)" - unit_value_with_description: "250.5 (bottle)" - - it "extracts into unit_value when only a number is provided", -> - testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "250.5"} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: 250.5 - unit_description: '' - unit_value_with_description: "250.5" - variant_unit_scale: 1.0 - - it "extracts into unit_description when only a string is provided", -> - testVariant = {unit_value_with_description: "Medium"} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: null - unit_description: 'Medium' - unit_value_with_description: "Medium" - - it "extracts into unit_description when a string starting with a number is provided", -> - testVariant = {unit_value_with_description: "1kg"} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: null - unit_description: '1kg' - unit_value_with_description: "1kg" - - it "sets blank values when no value provided", -> - testVariant = {unit_value_with_description: ""} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: null - unit_description: '' - unit_value_with_description: "" - - it "sets nothing when the field is undefined", -> - testVariant = {} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual({}) - - it "sets zero when the field is zero", -> - testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "0"} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: 0 - unit_description: '' - unit_value_with_description: "0" - variant_unit_scale: 1.0 - - it "converts value from chosen unit to base unit", -> - testVariant = {variant_unit_scale: 1000, unit_value_with_description: "250.5"} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: 250500 - unit_description: '' - unit_value_with_description: "250.5" - variant_unit_scale: 1000 - - it "does not convert value when using a non-scaled unit", -> - testVariant = {unit_value_with_description: "12"} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: 12 - unit_description: '' - unit_value_with_description: "12" - - it "converts unit_value into a float when a comma separated number is provided", -> - testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "250,5"} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: 250.5 - unit_description: '' - unit_value_with_description: "250,5" - variant_unit_scale: 1.0 - - it "rounds off the unit_value upto 2 decimal places", -> - testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "1234.567"} - - $scope.packVariant(testVariant) - - expect(testVariant).toEqual - unit_value: 1234.57 - unit_description: '' - unit_value_with_description: "1234.567" - variant_unit_scale: 1.0 - - - describe "filtering products", -> - beforeEach -> - spyOn $scope, "packProduct" - spyOn(window, "filterSubmitProducts").and.returnValue [ - { - id: 1 - value: 3 - } - { - id: 2 - value: 4 - } - ] - spyOn $scope, "updateProducts" - DirtyProducts.addProductProperty 1, "propName", "something" - DirtyProducts.addProductProperty 2, "propName", "something" - $scope.products = - 1: - id: 1 - 2: - id: 2 - - $scope.submitProducts() - - it "packs all products and all dirty products", -> - expect($scope.packProduct.calls.count()).toBe 4 - - it "filters returned dirty products", -> - expect(filterSubmitProducts).toHaveBeenCalledWith - 1: - id: 1 - propName: "something" - 2: - id: 2 - propName: "something" - - - it "sends dirty and filtered objects to submitProducts()", -> - expect($scope.updateProducts).toHaveBeenCalledWith [ - { - id: 1 - value: 3 - } - { - id: 2 - value: 4 - } - ] - - - describe "updating products", -> - it "submits products to be updated with a http post request to /admin/products/bulk_update", -> - $httpBackend.expectPOST("/admin/products/bulk_update").respond "list of products" - $scope.updateProducts "list of products" - $httpBackend.flush() - - it "runs displaySuccess() when post returns success", -> - spyOn $scope, "displaySuccess" - spyOn BulkProducts, "updateVariantLists" - spyOn DirtyProducts, "clear" - - $scope.bulk_product_form = jasmine.createSpyObj('bulk_product_form', ['$setPristine']) - - $scope.products = [ - { - id: 1 - name: "P1" - } - { - id: 2 - name: "P2" - } - ] - $httpBackend.expectPOST("/admin/products/bulk_update").respond 200, [ - { - id: 1 - name: "P1" - } - { - id: 2 - name: "P2" - } - ] - $scope.updateProducts "list of dirty products" - $httpBackend.flush() - $timeout.flush() - expect($scope.displaySuccess).toHaveBeenCalled() - expect($scope.bulk_product_form.$setPristine).toHaveBeenCalled - expect(DirtyProducts.clear).toHaveBeenCalled() - expect(BulkProducts.updateVariantLists).toHaveBeenCalled() - - it "runs displayFailure() when post returns an error", -> - spyOn $scope, "displayFailure" - $scope.products = "updated list of products" - $httpBackend.expectPOST("/admin/products/bulk_update").respond 500, "updated list of products" - $scope.updateProducts "updated list of products" - $httpBackend.flush() - expect($scope.displayFailure).toHaveBeenCalled() - - describe "displaying the error information when post returns 400", -> - beforeEach -> - spyOn $scope, "displayFailure" - $scope.products = "updated list of products" - - it "displays errors in an array", -> - $httpBackend.expectPOST("/admin/products/bulk_update").respond 400, { "errors": ["an error"] } - $scope.updateProducts "updated list of products" - $httpBackend.flush() - expect($scope.displayFailure).toHaveBeenCalledWith("Saving failed with the following error(s):\nan error\n") - - it "displays errors in a hash", -> - $httpBackend.expectPOST("/admin/products/bulk_update").respond 400, { "errors": { "base": ["a basic error"] } } - $scope.updateProducts "updated list of products" - $httpBackend.flush() - expect($scope.displayFailure).toHaveBeenCalledWith("Saving failed with the following error(s):\na basic error\n") - - - describe "adding variants", -> - beforeEach -> - spyOn DisplayProperties, 'setShowVariants' - - it "adds first and subsequent variants", -> - product = {id: 123, variants: [ - { - id: 1, price: 10.0, unit_value: 1, tax_category_id: null, unit_description: '1kg', - on_demand: false, on_hand: null, display_as: null, display_name: null, category_id: 2 - } - ]} - $scope.addVariant(product) - $scope.addVariant(product) - expect(product).toEqual - id: 123 - variants: [ - {id: 1, price: 10.0, unit_value: 1, tax_category_id: null, unit_description: '1kg', on_demand: false, on_hand: null, display_as: null, display_name: null, category_id: 2} - {id: -1, price: null, unit_value: null, tax_category_id: null, unit_description: null, on_demand: false, on_hand: null, display_as: null, display_name: null, category_id: 2} - {id: -2, price: null, unit_value: null, tax_category_id: null, unit_description: null, on_demand: false, on_hand: null, display_as: null, display_name: null, category_id: 2} - ] - - it "shows the variant(s)", -> - product = {id: 123, variants: []} - $scope.addVariant(product) - expect(DisplayProperties.setShowVariants).toHaveBeenCalledWith 123, true - - - describe "deleting products", -> - it "deletes products with a http delete request to /api/products/id", -> - spyOn(window, "confirm").and.returnValue true - $scope.products = [ - { - id: 9 - } - { - id: 13 - } - ] - $scope.dirtyProducts = {} - $httpBackend.expectDELETE("/api/v0/products/13").respond 200, "data" - $scope.deleteProduct $scope.products[1] - expect(window.confirm).toHaveBeenCalledWith "Are you sure?" - $httpBackend.flush() - - it "removes the specified product from both $scope.products and $scope.dirtyProducts (if it exists there)", -> - spyOn(window, "confirm").and.returnValue true - $scope.products = [ - { - id: 9 - } - { - id: 13 - } - ] - DirtyProducts.addProductProperty 9, "someProperty", "something" - DirtyProducts.addProductProperty 13, "name", "P1" - - $httpBackend.expectDELETE("/api/v0/products/13").respond 200, "data" - $scope.deleteProduct $scope.products[1] - $httpBackend.flush() - expect($scope.products).toEqual [ - id: 9 - ] - expect(DirtyProducts.all()).toEqual 9: - id: 9 - someProperty: "something" - - - - describe "deleting variants", -> - describe "when the variant is the only one left on the product", -> - it "alerts the user", -> - spyOn(window, "alert") - $scope.products = [ - {id: 1, variants: [{id: 1}]} - ] - $scope.deleteVariant $scope.products[0], $scope.products[0].variants[0] - expect(window.alert).toHaveBeenCalledWith "The last variant cannot be deleted!" - - describe "when the variant has not been saved", -> - it "removes the variant from products and dirtyProducts", -> - spyOn(window, "confirm").and.returnValue true - $scope.products = [ - {id: 1, variants: [{id: -1},{id: -2}]} - ] - DirtyProducts.addVariantProperty 1, -1, "something", "something" - DirtyProducts.addProductProperty 1, "something", "something" - $scope.deleteVariant $scope.products[0], $scope.products[0].variants[0] - expect($scope.products).toEqual([ - {id: 1, variants: [{id: -2}]} - ]) - expect(DirtyProducts.all()).toEqual - 1: { id: 1, something: 'something'} - - - describe "when the variant has been saved", -> - it "deletes variants with a http delete request to /api/products/(id)/variants/(variant_id)", -> - spyOn(window, "confirm").and.returnValue true - $scope.products = [ - { - id: 9 - variants: [{ - id: 3 - price: 12 - }, - { - id: 4 - price: 15 - } - ] - } - { - id: 13 - } - ] - $scope.dirtyProducts = {} - $httpBackend.expectDELETE("/api/v0/products/9/variants/3").respond 200, "data" - $scope.deleteVariant $scope.products[0], $scope.products[0].variants[0] - $httpBackend.flush() - - it "removes the specified variant from both the variants object and $scope.dirtyProducts (if it exists there)", -> - spyOn(window, "confirm").and.returnValue true - $scope.products = [ - { - id: 9 - variants: [ - { - id: 3 - price: 12.0 - } - { - id: 4 - price: 6.0 - } - ] - } - { - id: 13 - } - ] - DirtyProducts.addVariantProperty 9, 3, "price", 12.0 - DirtyProducts.addVariantProperty 9, 4, "price", 6.0 - DirtyProducts.addProductProperty 13, "name", "P1" - - $httpBackend.expectDELETE("/api/v0/products/9/variants/3").respond 200, "data" - $scope.deleteVariant $scope.products[0], $scope.products[0].variants[0] - $httpBackend.flush() - expect($scope.products[0].variants).toEqual [ - id: 4 - price: 6.0 - ] - expect(DirtyProducts.all()).toEqual - 9: - id: 9 - variants: - 4: - id: 4 - price: 6.0 - - 13: - id: 13 - name: "P1" - - describe "editWarn", -> - testProduct = testVariant = null - - beforeEach -> - testProduct = - id: 1 - name: "TestProduct" - description: "" - deleted_at: null - meta_keywords: null - shipping_category_id: null - created_at: null - updated_at: null - on_hand: 0 - on_demand: false - producer_id: 5 - group_buy: null - group_buy_unit_size: null - - describe 'product has variant', -> - it 'should load the edit product variant page', -> - testVariant = - id: 2 - name: "TestVariant" - - $scope.editWarn(testProduct, testVariant) - - expect(windowStub.location.href).toBe( - "/admin/products/#{testProduct.id}/variants/#{testVariant.id}/edit" - ) - - describe 'product has no variant', -> - it 'should display unsaved changes confirmation if there are any DirtyProduct', inject ($window, DirtyProducts) -> - spyOn($window, 'confirm') - spyOn(DirtyProducts, 'count').and.returnValue 2 - - $scope.editWarn(testProduct, null) - expect($window.confirm).toHaveBeenCalled() - - it 'should load the edit product page', inject -> - $scope.editWarn(testProduct, null) - - expect(windowStub.location.href).toBe( - "/admin/products/#{testProduct.id}/edit" - ) - - it 'should load edit product page including the selected filters', inject ($httpParamSerializer) -> - query = 'lala' - category = 3 - $scope.q.query = query - $scope.q.categoryFilter = category - - # use $httpParamSerializer as it will sort parameters alphabetically - expectedFilter = $httpParamSerializer({ query: query, categoryFilter: category }) - - $scope.editWarn(testProduct, null) - - expect(windowStub.location.href).toBe( - "/admin/products/#{testProduct.id}/edit?#{expectedFilter}" - ) - - describe "filtering products", -> - describe "clearing filters", -> - it "resets filter variables", -> - $scope.q.query = "lala" - $scope.q.producerFilter = "5" - $scope.q.categoryFilter = "6" - $scope.resetSelectFilters() - expect($scope.q.query).toBe "" - expect($scope.q.producerFilter).toBeUndefined - expect($scope.q.categoryFilter).toBeUndefined - - -describe "converting arrays of objects with ids to an object with ids as keys", -> - it "returns an object", -> - array = [] - expect(toObjectWithIDKeys(array)).toEqual {} - - it "adds each object in the array provided with an id to the returned object with the id as its key", -> - array = [ - { - id: 1 - } - { - id: 3 - } - ] - expect(toObjectWithIDKeys(array)).toEqual - 1: - id: 1 - - 3: - id: 3 - - - it "ignores items which are not objects and those which do not possess ids", -> - array = [ - { - id: 1 - } - "not an object" - { - notanid: 3 - } - ] - expect(toObjectWithIDKeys(array)).toEqual 1: - id: 1 - - - it "sends arrays with the key 'variants' to itself", -> - spyOn(window, "toObjectWithIDKeys").and.callThrough() - array = [ - { - id: 1 - variants: [id: 17] - } - { - id: 2 - variants: - 12: - id: 12 - } - ] - products = toObjectWithIDKeys(array) - expect(products["1"].variants).toEqual 17: - id: 17 - - expect(toObjectWithIDKeys).toHaveBeenCalledWith [id: 17] - expect(toObjectWithIDKeys).not.toHaveBeenCalledWith {12: {id: 12}} diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee deleted file mode 100644 index ea741adc35..0000000000 --- a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee +++ /dev/null @@ -1,188 +0,0 @@ -describe "BulkProducts service", -> - BulkProducts = $httpBackend = null - - beforeEach -> - module "ofn.admin" - window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["round"] - window.bigDecimal.round.and.callFake (a, b) -> a.toFixed(b) - - beforeEach inject (_BulkProducts_, _$httpBackend_) -> - BulkProducts = _BulkProducts_ - $httpBackend = _$httpBackend_ - - describe "cloning products", -> - it "clones products using a http post request to /api/products/(id)/clone", -> - BulkProducts.products = [ - id: 13 - ] - $httpBackend.expectPOST("/api/v0/products/13/clone").respond 201, - id: 17 - $httpBackend.expectGET("/api/v0/products/17?template=bulk_show").respond 200, [ - id: 17 - ] - BulkProducts.cloneProduct BulkProducts.products[0] - $httpBackend.flush() - - it "adds the product", -> - originalProduct = - id: 16 - clonedProduct = - id: 17 - - spyOn(BulkProducts, "insertProductAfter") - spyOn(BulkProducts, "unpackProduct") - BulkProducts.products = [originalProduct] - $httpBackend.expectPOST("/api/v0/products/16/clone").respond 201, clonedProduct - $httpBackend.expectGET("/api/v0/products/17?template=bulk_show").respond 200, clonedProduct - BulkProducts.cloneProduct BulkProducts.products[0] - $httpBackend.flush() - expect(BulkProducts.unpackProduct).toHaveBeenCalledWith clonedProduct - BulkProducts.unpackProduct(clonedProduct) - expect(BulkProducts.insertProductAfter).toHaveBeenCalledWith originalProduct, clonedProduct - - - describe "preparing products", -> - beforeEach -> - spyOn BulkProducts, "loadVariantUnit" - - it "calls loadVariantUnit for the product", -> - product = {id: 123} - BulkProducts.unpackProduct product - expect(BulkProducts.loadVariantUnit).toHaveBeenCalled() - - - describe "loading variant unit", -> - describe "setting product variant_unit_with_scale field", -> - it "HERE 2 sets by combining variant_unit and variant_unit_scale", -> - product = - variants:[ - id: 10 - variant_unit: "volume" - variant_unit_scale: .001 - ] - BulkProducts.loadVariantUnit product - expect(product.variants[0].variant_unit_with_scale).toEqual "volume_0.001" - - it "sets to null when variant_unit is null", -> - product = - variants: [ - {variant_unit: null, variant_unit_scale: 1000} - ] - BulkProducts.loadVariantUnit product - - expect(product.variants[0].variant_unit_with_scale).toBeNull() - - it "sets to variant_unit when variant_unit_scale is null", -> - product = - variants: [ - {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'} - ] - BulkProducts.loadVariantUnit product - expect(product.variants[0].variant_unit_with_scale).toEqual "items" - - it "sets to variant_unit when variant_unit is 'items'", -> - product = - variants: [ - {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'} - ] - BulkProducts.loadVariantUnit product - expect(product.variants[0].variant_unit_with_scale).toEqual "items" - - it "loads data for variants (excl. master)", -> - spyOn BulkProducts, "loadVariantUnitValue" - - product = - variants: [ - {id: 2, variant_unit_scale: 1.0, unit_value: 2, unit_description: '(two)'} - ] - BulkProducts.loadVariantUnitValues product.variants - - expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product.variants[0] - - describe "setting variant unit_value_with_description", -> - it "sets by combining unit_value and unit_description", -> - product = - variants: [ - {id: 1, variant_unit_scale: 1.0, unit_value: 1, unit_description: '(bottle)'} - ] - BulkProducts.loadVariantUnitValues product.variants - expect(product.variants[0]).toEqual - id: 1 - variant_unit_scale: 1.0, - variant_unit_with_scale: null, - unit_value: 1 - unit_description: '(bottle)' - unit_value_with_description: '1 (bottle)' - - it "uses unit_value when description is missing", -> - product = - variants: [ - {id: 1, variant_unit_scale: 1.0, unit_value: 1} - ] - BulkProducts.loadVariantUnitValues product.variants - expect(product.variants[0].unit_value_with_description).toEqual '1' - - it "uses unit_description when value is missing", -> - product = - variants: [ - {id: 1, variant_unit_scale: 1.0, unit_description: 'Small'} - ] - BulkProducts.loadVariantUnitValues product.variants - expect(product.variants[0].unit_value_with_description).toEqual 'Small' - - it "converts values from base value to chosen unit", -> - product = - variants: [ - id: 1, variant_unit_scale: 1000.0, unit_value: 2500 - ] - BulkProducts.loadVariantUnitValues product.variants - expect(product.variants[0].unit_value_with_description).toEqual '2.5' - - it "converts values from base value to chosen unit without breaking precision", -> - product = - variants: [ - {id: 1,variant_unit_scale: 0.001, unit_value: 0.35} - ] - BulkProducts.loadVariantUnitValues product.variants - expect(product.variants[0].unit_value_with_description).toEqual '350' - - it "displays a unit_value of zero", -> - product = - variants: [ - {id: 1, variant_unit_scale: 1.0, unit_value: 0} - ] - BulkProducts.loadVariantUnitValues product.variants - expect(product.variants[0].unit_value_with_description).toEqual '0' - - - describe "calculating the scaled unit value for a variant", -> - it "returns the scaled value when variant has a unit_value", -> - variant = {variant_unit_scale: 0.001, unit_value: 5} - expect(BulkProducts.variantUnitValue(variant)).toEqual 5000 - - it "returns the scaled value rounded off upto 2 decimal points", -> - variant = {variant_unit_scale: 28.35, unit_value: 1234.5} - expect(BulkProducts.variantUnitValue(variant)).toEqual 43.54 - - it "returns the unscaled value when the product has no scale", -> - variant = {unit_value: 5} - expect(BulkProducts.variantUnitValue(variant)).toEqual 5 - - it "returns zero when the value is zero", -> - variant = {unit_value: 0} - expect(BulkProducts.variantUnitValue(variant)).toEqual 0 - - it "returns null when the variant has no unit_value", -> - variant = {} - expect(BulkProducts.variantUnitValue(variant)).toEqual null - - - describe "fetching a product by id", -> - it "returns the product when it is present", -> - product = {id: 123} - BulkProducts.products = [product] - expect(BulkProducts.find(123)).toEqual product - - it "returns null when the product is not present", -> - BulkProducts.products = [] - expect(BulkProducts.find(123)).toBeNull() From fefd0239e6978fc77ceff3f2e68993e9c3a7b875 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 06:15:14 +0500 Subject: [PATCH 04/13] Remove unused product image controller and directive; delete product image update route --- .../controllers/product_image_controller.js.coffee | 8 -------- .../admin/products/directives/image_modal.js.coffee | 6 ------ config/routes/api.rb | 2 -- 3 files changed, 16 deletions(-) delete mode 100644 app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee delete mode 100644 app/assets/javascripts/admin/products/directives/image_modal.js.coffee diff --git a/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee deleted file mode 100644 index fb447208e2..0000000000 --- a/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee +++ /dev/null @@ -1,8 +0,0 @@ -angular.module("ofn.admin").controller "ProductImageCtrl", ($scope, ProductImageService) -> - $scope.imageUploader = ProductImageService.imageUploader - $scope.imagePreview = ProductImageService.imagePreview - - $scope.$watch 'product.image_url', (newValue, oldValue) -> - if newValue != oldValue - $scope.imagePreview = newValue - $scope.uploadModal.close() diff --git a/app/assets/javascripts/admin/products/directives/image_modal.js.coffee b/app/assets/javascripts/admin/products/directives/image_modal.js.coffee deleted file mode 100644 index 69ca1731fd..0000000000 --- a/app/assets/javascripts/admin/products/directives/image_modal.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -angular.module("ofn.admin").directive "imageModal", ($modal, ProductImageService) -> - restrict: 'C' - link: (scope, elem, attrs, ctrl) -> - elem.on "click", (ev) => - scope.uploadModal = $modal.open(templateUrl: 'admin/modals/image_upload.html', controller: ctrl, scope: scope, windowClass: 'simple-modal') - ProductImageService.configure(scope.product) diff --git a/config/routes/api.rb b/config/routes/api.rb index 2545554739..725d022b98 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -74,8 +74,6 @@ Openfoodnetwork::Application.routes.draw do resources :enterprise_fees, only: [:destroy] - post '/product_images/:product_id', to: 'product_images#update_product_image' - resources :states, :only => [:index, :show] resources :taxons, except: %i[show edit] From 025fc784a8bf79bf530a1dc2fd9d54a973f5988d Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 06:26:12 +0500 Subject: [PATCH 05/13] Refactor products_return_to_url method to remove url_filters parameter and simplify usage in views --- app/helpers/admin/products_helper.rb | 8 ++------ app/views/spree/admin/products/edit.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/helpers/admin/products_helper.rb b/app/helpers/admin/products_helper.rb index b2f7beada9..d7b8a2086f 100644 --- a/app/helpers/admin/products_helper.rb +++ b/app/helpers/admin/products_helper.rb @@ -32,12 +32,8 @@ module Admin [precised_unit_value, variant.unit_description].compact_blank.join(" ") end - def products_return_to_url(url_filters) - if feature?(:admin_style_v3, spree_current_user) - return session[:products_return_to_url] || admin_products_url - end - - "#{admin_products_path}#{url_filters.empty? ? '' : "#?#{url_filters.to_query}"}" + def products_return_to_url + session[:products_return_to_url] || admin_products_url end # if user hasn't saved any preferences on products page and there's only one producer; diff --git a/app/views/spree/admin/products/edit.html.haml b/app/views/spree/admin/products/edit.html.haml index 003a35eee6..7f6df174db 100644 --- a/app/views/spree/admin/products/edit.html.haml +++ b/app/views/spree/admin/products/edit.html.haml @@ -1,7 +1,7 @@ = admin_inject_available_units - content_for :page_actions do - %li= button_link_to t('admin.products.back_to_products_list'), products_return_to_url(@url_filters), :icon => 'icon-arrow-left' + %li= button_link_to t('admin.products.back_to_products_list'), products_return_to_url, :icon => 'icon-arrow-left' %li#new_product_link = button_link_to t(:new_product), new_object_url, { :icon => 'icon-plus', :id => 'admin_new_product' } @@ -18,6 +18,6 @@ = button t(:update), 'icon-refresh' - = button_link_to t(:cancel), products_return_to_url(@url_filters), icon: 'icon-remove' + = button_link_to t(:cancel), products_return_to_url, icon: 'icon-remove' #product-preview-modal-container From 6e055ddbdf6eb6bb107984b000bfcdbcfcb24e75 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 06:32:44 +0500 Subject: [PATCH 06/13] Remove icon parameters from admin navigation tabs for simplification --- app/helpers/spree/admin/navigation_helper.rb | 8 +------- app/views/spree/admin/shared/_tabs.html.haml | 14 +++++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/helpers/spree/admin/navigation_helper.rb b/app/helpers/spree/admin/navigation_helper.rb index 7b408e5ef5..ccb141043f 100644 --- a/app/helpers/spree/admin/navigation_helper.rb +++ b/app/helpers/spree/admin/navigation_helper.rb @@ -29,13 +29,7 @@ module Spree scope: [:admin, :tab]).capitalize css_classes = [] - - if options[:icon] && !feature?(:admin_style_v3, spree_current_user) - link = link_to_with_icon(options[:icon], titleized_label, destination_url) - css_classes << 'tab-with-icon' - else - link = link_to(titleized_label, destination_url) - end + link = link_to(titleized_label, destination_url) selected = if options[:match_path] PathChecker diff --git a/app/views/spree/admin/shared/_tabs.html.haml b/app/views/spree/admin/shared/_tabs.html.haml index 3ad1a1d53a..ebe1c422ba 100644 --- a/app/views/spree/admin/shared/_tabs.html.haml +++ b/app/views/spree/admin/shared/_tabs.html.haml @@ -1,11 +1,11 @@ -= tab :overview, label: 'dashboard', url: spree.admin_dashboard_path, icon: 'icon-dashboard' -= tab :products, :properties, :inventory, :product_import, :images, :variants, :product_properties, :group_buy_options, :seo, :products_v3, :variant_overrides, url: admin_products_path, icon: 'icon-th-large' -= tab :order_cycles, url: main_app.admin_order_cycles_path, icon: 'icon-refresh' -= tab :orders, :subscriptions, :customer_details, :adjustments, :payments, :return_authorizations, url: admin_orders_path, icon: 'icon-shopping-cart' -= tab :reports, url: main_app.admin_reports_path, icon: 'icon-file' -= tab :general_settings, :terms_of_service_files, :mail_methods, :tax_categories, :tax_rates, :tax_settings, :zones, :countries, :states, :payment_methods, :taxons, :shipping_methods, :shipping_categories, :enterprise_fees, :contents, :invoice_settings, :matomo_settings, :stripe_connect_settings, :connected_app_settings, label: 'configuration', icon: 'icon-wrench', url: edit_admin_general_settings_path += tab :overview, label: 'dashboard', url: spree.admin_dashboard_path += tab :products, :properties, :inventory, :product_import, :images, :variants, :product_properties, :group_buy_options, :seo, :products_v3, :variant_overrides, url: admin_products_path += tab :order_cycles, url: main_app.admin_order_cycles_path += tab :orders, :subscriptions, :customer_details, :adjustments, :payments, :return_authorizations, url: admin_orders_path += tab :reports, url: main_app.admin_reports_path += tab :general_settings, :terms_of_service_files, :mail_methods, :tax_categories, :tax_rates, :tax_settings, :zones, :countries, :states, :payment_methods, :taxons, :shipping_methods, :shipping_categories, :enterprise_fees, :contents, :invoice_settings, :matomo_settings, :stripe_connect_settings, :connected_app_settings, label: 'configuration', url: edit_admin_general_settings_path = tab :enterprises, :enterprise_relationships, :vouchers, :oidc_settings, url: main_app.admin_enterprises_path = tab :customers, url: main_app.admin_customers_path = tab :enterprise_groups, url: main_app.admin_enterprise_groups_path, label: 'groups' - if can? :admin, Spree::User - = tab(:users, :enterprise_roles, url: spree.admin_users_path, icon: 'icon-user') + = tab(:users, :enterprise_roles, url: spree.admin_users_path) From 188b2eb7548810a75493d8c769a4253bdd015a40 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 06:44:02 +0500 Subject: [PATCH 07/13] Simplify pagination next button by removing conditional icon rendering --- app/views/admin/shared/_stimulus_pagination.html.haml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/views/admin/shared/_stimulus_pagination.html.haml b/app/views/admin/shared/_stimulus_pagination.html.haml index ec73ecb92b..23860ead4e 100644 --- a/app/views/admin/shared/_stimulus_pagination.html.haml +++ b/app/views/admin/shared/_stimulus_pagination.html.haml @@ -22,9 +22,6 @@ - if pagy.next %button.page.next{ data: { action: 'click->search#changePage', page: pagy.next } } - - if feature?(:admin_style_v3, spree_current_user) - %i.icon-chevron-right{ data: { action: 'click->search#changePage', page: pagy.next } } - - else - != pagy_t('pagy.next') + %i.icon-chevron-right{ data: { action: 'click->search#changePage', page: pagy.next } } - else %button.page.disabled.pagination-next{disabled: "disabled"}!= pagy_t('pagy.next') From 1426b6eeb71398838272a5960cccd50783629d99 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 07:02:53 +0500 Subject: [PATCH 08/13] Remove legacy admin styles in favor of v3 styling Completes migration to the new admin v3 styling system by: - Removing conditional stylesheet inclusion in admin head - Deleting all legacy admin style files and components - Making admin-style-v3 the default and only stylesheet This change reduces maintenance overhead and simplifies the admin styling codebase by removing the old styling system that was being conditionally loaded based on feature flags. --- app/views/spree/admin/shared/_head.html.haml | 7 +- app/webpacker/css/admin/all.scss | 125 --------- .../css/admin/components/actions.scss | 31 --- .../css/admin/components/buttons.scss | 93 ------- .../css/admin/components/date-picker.scss | 20 -- .../css/admin/components/messages.scss | 76 ----- .../css/admin/components/navigation.scss | 174 ------------ .../css/admin/components/pagination.scss | 32 --- .../css/admin/components/sidebar.scss | 26 -- .../css/admin/components/tom_select.scss | 141 ---------- .../css/admin/globals/variables.scss | 156 ----------- .../plugins/flatpickr-customization.scss | 68 ----- app/webpacker/css/admin/plugins/powertip.scss | 118 -------- app/webpacker/css/admin/sections/orders.scss | 65 ----- app/webpacker/css/admin/shared/forms.scss | 261 ------------------ app/webpacker/css/admin/shared/icons.scss | 25 -- app/webpacker/css/admin/shared/layout.scss | 134 --------- app/webpacker/css/admin/shared/tables.scss | 208 -------------- .../css/admin/shared/typography.scss | 158 ----------- .../css/admin/terms_of_service_banner.scss | 27 -- app/webpacker/packs/admin-styles.scss | 2 - 21 files changed, 1 insertion(+), 1946 deletions(-) delete mode 100644 app/webpacker/css/admin/all.scss delete mode 100644 app/webpacker/css/admin/components/actions.scss delete mode 100644 app/webpacker/css/admin/components/buttons.scss delete mode 100644 app/webpacker/css/admin/components/date-picker.scss delete mode 100644 app/webpacker/css/admin/components/messages.scss delete mode 100644 app/webpacker/css/admin/components/navigation.scss delete mode 100644 app/webpacker/css/admin/components/pagination.scss delete mode 100644 app/webpacker/css/admin/components/sidebar.scss delete mode 100644 app/webpacker/css/admin/components/tom_select.scss delete mode 100644 app/webpacker/css/admin/globals/variables.scss delete mode 100644 app/webpacker/css/admin/plugins/flatpickr-customization.scss delete mode 100644 app/webpacker/css/admin/plugins/powertip.scss delete mode 100644 app/webpacker/css/admin/sections/orders.scss delete mode 100644 app/webpacker/css/admin/shared/forms.scss delete mode 100644 app/webpacker/css/admin/shared/icons.scss delete mode 100644 app/webpacker/css/admin/shared/layout.scss delete mode 100644 app/webpacker/css/admin/shared/tables.scss delete mode 100644 app/webpacker/css/admin/shared/typography.scss delete mode 100644 app/webpacker/css/admin/terms_of_service_banner.scss delete mode 100644 app/webpacker/packs/admin-styles.scss diff --git a/app/views/spree/admin/shared/_head.html.haml b/app/views/spree/admin/shared/_head.html.haml index e1b987232c..5da4963871 100644 --- a/app/views/spree/admin/shared/_head.html.haml +++ b/app/views/spree/admin/shared/_head.html.haml @@ -14,12 +14,7 @@ = " - OFN #{t(:administration)}" %link{:href => "https://fonts.googleapis.com/css?family=Open+Sans:400italic,600italic,400,600&subset=latin,cyrillic,greek,vietnamese", :rel => "stylesheet", :type => "text/css"} - -- if feature?(:admin_style_v3, spree_current_user) - = stylesheet_pack_tag 'admin-style-v3', media: "screen, print" -- else - = stylesheet_pack_tag 'admin-styles', media: "screen, print" - += stylesheet_pack_tag 'admin-style-v3', media: "screen, print" = render "layouts/bugsnag_js" - if content_for? :minimal_js diff --git a/app/webpacker/css/admin/all.scss b/app/webpacker/css/admin/all.scss deleted file mode 100644 index a63e543f90..0000000000 --- a/app/webpacker/css/admin/all.scss +++ /dev/null @@ -1,125 +0,0 @@ -@import "vendor/assets/stylesheets/normalize"; -@import "vendor/assets/stylesheets/responsive-tables"; -@import "vendor/assets/stylesheets/jquery.powertip"; -@import "~jquery-ui/themes/base/core"; -@import "~jquery-ui/themes/base/button"; -@import "~jquery-ui/themes/base/resizable"; -@import "vendor/assets/stylesheets/jquery-ui-theme"; -@import "~jquery-ui/themes/base/dialog"; -@import "../shared/ng-tags-input.min"; -@import "vendor/assets/stylesheets/select2.css.scss"; -@import "~flatpickr/dist/flatpickr"; -@import "~flatpickr/dist/themes/material_blue"; -@import "~shortcut-buttons-flatpickr/dist/themes/light"; - -@import "globals/functions"; -@import "globals/palette"; -@import "globals/variables"; -@import "globals/mixins"; - -@import "plugins/font-awesome"; - -@import "../shared/variables/layout"; -@import "../shared/variables/variables"; -@import "../shared/utilities"; -@import "shared/typography"; -@import "shared/tables"; -@import "shared/icons"; -@import "shared/forms"; -@import "shared/layout"; -@import "shared/scroll_bar"; - -@import "../shared/trix"; - -@import "plugins/flatpickr-customization"; -@import "plugins/powertip"; -@import "plugins/select2"; - -@import "sections/orders"; -@import "sections/products"; - -@import "hacks/mozilla"; -@import "hacks/opera"; -@import "hacks/ie"; - -@import "components/actions"; -@import "components/alert-box"; -@import "components/alert_row"; -@import "components/buttons"; -@import "components/date-picker"; -@import "components/dialogs"; -@import "components/input"; -@import "components/jquery_dialog"; -@import "components/messages"; -@import "components/navigation"; -@import "components/ng-cloak"; -@import "components/page_actions"; -@import "components/pagination"; -@import "components/per_page_controls"; -@import "components/product_autocomplete"; -@import "components/progress"; -@import "components/save_bar"; -@import "components/sidebar"; -@import "components/simple_modal"; -@import "components/states"; -@import "components/stripe_connect_button"; -@import "components/subscriptions_states"; -@import "components/table-filter"; -@import "components/table_loading"; -@import "components/timepicker"; -@import "components/todo"; -@import "components/tooltip"; -@import "components/wizard_progress"; - -@import "pages/enterprise_form"; -@import "pages/subscription_form"; -@import "pages/subscription_line_items"; -@import "pages/subscription_review"; - -@import "advanced_settings"; -@import "alert"; -@import "animations"; -@import "change_type_form"; -@import "connected_apps"; -@import "customers"; -@import "dashboard_item"; -@import "dashboard-single-ent"; -@import "dialog"; -@import "disabled"; -@import "dropdown"; -@import "enterprise_index_panels"; -@import "enterprises"; -@import "filters_and_controls"; -@import "grid"; -@import "icons"; -@import "index_panel_buttons"; -@import "index_panels"; -@import "modals"; -@import "offsets"; -@import "openfoodnetwork"; -@import "order_cycles"; -@import "orders"; -@import "product_import"; -@import "products"; -@import "question-mark-tooltip"; -@import "relationships"; -@import "reports"; -@import "select2"; -@import "sidebar-item"; -@import "side_menu"; -@import "tables"; -@import "tag_rules"; -@import "terms_of_service_banner"; -@import "terms_of_service_files"; -@import "validation"; -@import "variant_overrides"; -@import "welcome"; - -@import "../shared/question-mark-icon"; -@import "question-mark-tooltip"; - -@import "~tom-select/src/scss/tom-select.default"; -@import "components/tom_select"; - -@import "app/components/modal_component/modal_component"; -@import "app/webpacker/css/admin/trix.scss"; diff --git a/app/webpacker/css/admin/components/actions.scss b/app/webpacker/css/admin/components/actions.scss deleted file mode 100644 index c766a284ac..0000000000 --- a/app/webpacker/css/admin/components/actions.scss +++ /dev/null @@ -1,31 +0,0 @@ -table tbody tr { - &.highlight { - @each $action in $actions { - &.action-#{$action} td { - background-color: get-value($actions, $actions-bg-colors, $action); - border-color: get-value($actions, $actions-brd-colors, $action); - } - } - - &.action-remove td, - &.action-void td { - text-decoration: line-through; - - &.actions { - text-decoration: none; - } - } - } - - &.before-highlight { - @each $action in $actions { - &.action-#{$action} td { - border-bottom-color: get-value($actions, $actions-brd-colors, $action); - } - } - } - - td.actions { - background-color: transparent !important; - } -} diff --git a/app/webpacker/css/admin/components/buttons.scss b/app/webpacker/css/admin/components/buttons.scss deleted file mode 100644 index 1291d04e15..0000000000 --- a/app/webpacker/css/admin/components/buttons.scss +++ /dev/null @@ -1,93 +0,0 @@ -input[type="submit"], -input[type="button"]:not(.trix-button), -button:not(.plain):not(.trix-button), -.button { - position: relative; - cursor: pointer; - font-size: 85%; - @include border-radius($border-radius); - display: inline-block; - padding: 8px 15px; - border: none; - background-color: $color-btn-bg; - color: $color-btn-text; - text-transform: uppercase; - font-weight: 600 !important; - - &:before { - font-weight: normal !important; - } - - &:visited, - &:active, - &:focus { - color: $color-btn-text; - } - - &:hover { - background-color: $color-btn-hover-bg; - color: $color-btn-hover-text; - } - - &:active:focus { - box-shadow: 0 0 8px 0 darken($color-btn-hover-bg, 5) inset; - } - - &.fullwidth { - width: 100%; - text-align: center; - } - - &.secondary { - background-color: transparent; - border: 1px solid $color-btn-bg; - color: $color-btn-bg; - - &:hover, - &:active, - &:focus { - background-color: #ebf3fb; - } - - &:active:focus { - box-shadow: none; - } - } - - .badge { - position: absolute; - top: 0; - right: 0; - transform: translateY(-50%); - font-size: 10px; - text-transform: capitalize; - padding: 0px 5px; - border-radius: 3px; - - &:before { - padding: 0; - } - - &.danger { - background-color: $color-warning; - } - &.success { - background-color: $spree-green; - } - } -} - -input.red, -a.button.red, -button.red { - background-color: $color-warning; - margin-right: 5px; - color: #ffffff; -} - -a.button.red { - &:not(:hover) { - color: #fff; - background-color: $color-warning; - } -} diff --git a/app/webpacker/css/admin/components/date-picker.scss b/app/webpacker/css/admin/components/date-picker.scss deleted file mode 100644 index a35b5a970e..0000000000 --- a/app/webpacker/css/admin/components/date-picker.scss +++ /dev/null @@ -1,20 +0,0 @@ -// scss-lint:disable QualifyingElement - -input.datetimepicker { - min-width: 12.9em; -} - -.container input[readonly].flatpickr-input, -.container input[readonly].datepicker, -.container input[readonly].datetimepicker { - background-color: $white; - cursor: pointer; -} - -img.ui-datepicker-trigger { - margin-left: -1.75em; - position: absolute; - margin-top: 0.5em; -} - -// scss-lint:enable QualifyingElement diff --git a/app/webpacker/css/admin/components/messages.scss b/app/webpacker/css/admin/components/messages.scss deleted file mode 100644 index f92498a599..0000000000 --- a/app/webpacker/css/admin/components/messages.scss +++ /dev/null @@ -1,76 +0,0 @@ -.errorExplanation { - padding: 10px; - border: 1px solid very-light($color-error, 12); - background-color: very-light($color-error, 6); - border-radius: 3px; - color: $color-error; - margin-bottom: 15px; - - h2 { - font-size: 140%; - color: $color-error; - margin-bottom: 5px; - } - - p { - padding: 10px 0; - } - - ul { - list-style-position: inside; - - li { - font-weight: $font-weight-bold; - } - } -} - -.flash-container { - position: fixed; - top: 0; - left: 0; - width: 100%; - z-index: 1000; - - .flash { - padding: 18px; - text-align: center; - font-size: 120%; - color: $color-1; - font-weight: 600; - margin-top: 0; - - &.notice { - background-color: rgba($color-notice, 0.8); - } - &.success { - background-color: rgba($color-success, 0.8); - } - &.error { - background-color: rgba($color-error, 0.8); - } - - // Adjust heights to fit main layout dimension (header, navbar...) - &:nth-child(2) { - padding: 24px; - } - &:nth-child(3) { - padding: 20px; - } - - .actions { - display: none; /* avoid adding new button on old design */ - } - } -} - -.notice:not(.flash) { - padding: 1rem; - margin-bottom: 1.5rem; - background-color: $spree-light-blue; - border-radius: $border-radius; - - a { - font-weight: bold; - } -} diff --git a/app/webpacker/css/admin/components/navigation.scss b/app/webpacker/css/admin/components/navigation.scss deleted file mode 100644 index 2aeb3a8a30..0000000000 --- a/app/webpacker/css/admin/components/navigation.scss +++ /dev/null @@ -1,174 +0,0 @@ -// Navigation -//--------------------------------------------------- -.inline-menu { - margin: 0; - -webkit-margin-before: 0; - -webkit-padding-start: 0; -} - -nav.menu { - ul { - list-style: none; - - li { - a { - padding: 10px 0; - display: block; - position: relative; - text-align: left; - border: 1px solid transparent; - text-transform: uppercase; - font-weight: 600; - font-size: 90%; - } - - &.active a { - color: $color-2; - border-left-width: 0; - border-bottom-color: $color-2; - } - - &:hover a { - color: $color-2; - } - } - } -} - -.admin-login-navigation-bar { - ul { - text-align: right; - - li { - padding: 5px 0 5px 10px; - text-align: right; - font-size: 90%; - color: $color-link; - margin-top: 8px; - - &.user-logged-in-as { - width: 50%; - color: $color-body-text; - } - - &:hover { - i { - color: $color-2; - } - } - } - } -} - -#admin-menu { - background-color: $color-3; - - ul { - display: flex; - } - - li { - min-width: 90px; - flex-grow: 1; - - a { - display: block; - padding: 25px 5px; - color: $color-1 !important; - text-transform: uppercase; - position: relative; - text-align: center; - font-weight: 600; - - i { - display: inline; - } - - &:hover { - background-color: $color-2; - - &:after { - content: ""; - position: absolute; - border-left: 10px solid transparent; - border-right: 10px solid transparent; - border-top: 5px solid $color-2; - bottom: 0px; - margin-bottom: -5px; - left: 50%; - margin-left: -10px; - z-index: 1; - } - } - - span.text { - font-weight: 600; - } - } - - a::before { - font-weight: normal; - padding-top: 0; - } - - .dropdown { - width: 300px; - background-color: $color-3; - width: 200px; - z-index: 100000; - - > li { - width: 200px !important; - - a:after { - display: none; - } - } - } - - &.selected a { - @extend a, :hover; - } - } -} - -#sub-menu { - background-color: $color-2; - padding-bottom: 0; - - li { - a { - display: block; - padding: 12px 20px; - color: $color-1; - text-align: center; - text-transform: uppercase; - position: relative; - font-size: 85%; - } - - &.selected a, - a:hover { - &:after { - content: ""; - position: absolute; - border-left: 10px solid transparent; - border-right: 10px solid transparent; - border-top: 5px solid $color-2; - bottom: 0px; - margin-bottom: -5px; - left: 50%; - margin-left: -10px; - z-index: 0; - } - } - } -} - -#header figure { - margin: 0.25em 0; -} - -#login-nav { - line-height: 1.75em; -} diff --git a/app/webpacker/css/admin/components/pagination.scss b/app/webpacker/css/admin/components/pagination.scss deleted file mode 100644 index 730abe8888..0000000000 --- a/app/webpacker/css/admin/components/pagination.scss +++ /dev/null @@ -1,32 +0,0 @@ -.pagination { - text-align: center; - margin: 2em 0 1em; - padding: 10px 0; - - .page { - padding: 5px 8px; - text-align: center; - display: inline-block; - text-align: center; - - &.current { - background-color: $color-2; - border-radius: 3px; - color: $color-1; - } - } - - button { - margin: 0 0.35em; - - &.active { - background-color: darken($spree-blue, 15%); - cursor: default; - } - - &.disabled { - background-color: $color-btn-disabled-bg; - cursor: default; - } - } -} diff --git a/app/webpacker/css/admin/components/sidebar.scss b/app/webpacker/css/admin/components/sidebar.scss deleted file mode 100644 index 960de96ab0..0000000000 --- a/app/webpacker/css/admin/components/sidebar.scss +++ /dev/null @@ -1,26 +0,0 @@ -// Sidebar -//--------------------------------------------------- -#sidebar { - overflow: visible; - border-top: 1px solid $color-border; - margin-top: 17px; - - .sidebar-title { - color: $color-2; - text-transform: uppercase; - text-align: center; - font-size: 14px; - font-weight: 600; - - > span { - display: inline; - background: #fff; - padding: 5px 10px; - position: relative; - top: -14px; - - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - } -} diff --git a/app/webpacker/css/admin/components/tom_select.scss b/app/webpacker/css/admin/components/tom_select.scss deleted file mode 100644 index 0783795e6e..0000000000 --- a/app/webpacker/css/admin/components/tom_select.scss +++ /dev/null @@ -1,141 +0,0 @@ -.ts-wrapper { - min-height: initial; -} - -.ts-wrapper.multi { - .ts-control { - box-shadow: none; - border-color: $pale-blue; - - &:focus { - border-color: $spree-green; - } - - [data-value] { - text-shadow: none; - background-image: none; - background-repeat: initial; - box-shadow: none; - background-color: $spree-blue; - } - } - - .ts-control > div { - border: none; - background-color: $spree-blue; - } -} - -.ts-wrapper.plugin-remove_button .item .remove { - font-weight: bold; -} - -.ts-dropdown { - margin-top: 0; - - .option { - min-height: 2.25em; - display: block; - } -} - -.ts-wrapper.single .ts-control, -.ts-dropdown.single { - border-color: $color-tbl-border; -} - -.ts-control, -.ts-wrapper.single.input-active .ts-control { - cursor: pointer; - padding: 6px 8px; - outline: 0 !important; - min-height: 2.5em; -} - -.ts-wrapper.single .ts-control { - padding-right: 2rem; -} - -.ts-wrapper.inline, -.ts-wrapper.inline.input-active { - width: fit-content; - - .ts-control { - padding-right: 2rem; - } -} - -.ts-wrapper.single .ts-control { - box-shadow: none; - background-image: none; - background-color: $white; -} - -.ts-wrapper.primary.focus .ts-control, -.ts-wrapper.primary .ts-control { - background-color: $spree-blue; - border-color: $spree-blue; - color: $white; - - &:after { - border-color: $white transparent transparent transparent; - } -} - -.ts-wrapper .select-multiple { - cursor: pointer; -} - -.ts-wrapper.dropdown-active.primary .ts-control { - background-color: $spree-green; - border-color: $spree-green; - color: $white; - - &:after { - border-color: transparent transparent $white transparent; - } -} - -.dropdown-input-wrap { - padding: 0.2em; - position: relative; - - &:before { - @extend [class^="icon-"]; - @extend .icon-search; - - position: absolute; - opacity: 0.4; - line-height: 2em; - left: 0.75em; - } - - .dropdown-input { - outline: 0; - padding: 4px 6px; - border: 1px solid $spree-green; - border-radius: 0.3em; - padding-left: 1.75em; - } -} - -.ts-wrapper.no-search { - .dropdown-input-wrap { - display: none; - } -} - -.ts-dropdown .create:hover, -.ts-dropdown #admin-menu li.selected a.create, -#admin-menu li.selected .ts-dropdown a.create, -.ts-dropdown .option:hover, -.ts-dropdown #admin-menu li.selected a.option, -#admin-menu li.selected .ts-dropdown a.option, -.ts-dropdown .active { - background-color: $spree-blue; - color: $white; -} - -.ts-dropdown [data-selectable] .highlight { - background: rgba(149, 180, 255, 0.26); -} diff --git a/app/webpacker/css/admin/globals/variables.scss b/app/webpacker/css/admin/globals/variables.scss deleted file mode 100644 index f35788dc5f..0000000000 --- a/app/webpacker/css/admin/globals/variables.scss +++ /dev/null @@ -1,156 +0,0 @@ -// ------------------------------------------------------------- -// Variables used in all other files -//-------------------------------------------------------------- - -// Fonts -//-------------------------------------------------------------- -$base-font-family: "Open Sans", "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; - -// Colors -//-------------------------------------------------------------- - -// Body base colors -$color-body-bg: $color-1 !default; -$color-body-text: $color-4 !default; -$color-headers: $color-4 !default; -$color-link: $color-3 !default; -$color-link-hover: $color-2 !default; -$color-link-active: $color-2 !default; -$color-link-focus: $color-2 !default; -$color-link-visited: $color-3 !default; -$color-border: very-light($color-3, 12) !default; - -// Basic flash colors -$color-success: $color-2 !default; -$color-notice: $color-6 !default; -$color-warning: $color-5 !default; -$color-error: $color-5 !default; - -// Table colors -$color-tbl-odd: $color-1 !default; -$color-tbl-even: very-light($color-3, 4) !default; -$color-tbl-thead: very-light($color-3, 4) !default; -$color-tbl-border: $pale-blue !default; - -// Button colors -$color-btn-bg: $color-3 !default; -$color-btn-text: $color-1 !default; -$color-btn-hover-bg: $color-2 !default; -$color-btn-hover-text: $color-1 !default; -$color-btn-disabled-bg: $light-grey !default; - -// Actions colors -$color-action-edit-bg: very-light($color-success, 5 ) !default; -$color-action-edit-brd: very-light($color-success, 20 ) !default; -$color-action-clone-bg: very-light($color-notice, 5 ) !default; -$color-action-clone-brd: very-light($color-notice, 15 ) !default; -$color-action-remove-bg: very-light($color-error, 5 ) !default; -$color-action-remove-brd: very-light($color-error, 10 ) !default; -$color-action-void-bg: very-light($color-error, 10 ) !default; -$color-action-void-brd: very-light($color-error, 20 ) !default; -$color-action-cancel-bg: very-light($color-notice, 10 ) !default; -$color-action-cancel-brd: very-light($color-notice, 20 ) !default; -$color-action-capture-bg: very-light($color-success, 5 ) !default; -$color-action-capture-brd: very-light($color-success, 20 ) !default; -$color-action-save-bg: very-light($color-success, 5 ) !default; -$color-action-save-brd: very-light($color-success, 20 ) !default; -$color-action-mail-bg: very-light($color-success, 5 ) !default; -$color-action-mail-brd: very-light($color-success, 20 ) !default; - -// Select2 select field colors -$color-sel-bg: $color-3 !default; -$color-sel-text: $color-1 !default; -$color-sel-hover-bg: $color-2 !default; -$color-sel-hover-text: $color-1 !default; - -// Text inputs colors -$color-txt-brd: $color-border !default; -$color-txt-text: $color-3 !default; -$color-txt-hover-brd: $color-2 !default; -$vpadding-txt: 7px; -$hpadding-txt: 10px; - -// Modal colors -$color-modal-close-btn: $color-5 !default; -$color-modal-close-btn-hover: darken($color-5, 5%) !default; - -// States label colors -$color-ste-complete-bg: $color-success !default; -$color-ste-complete-text: $color-1 !default; -$color-ste-completed-bg: $color-success !default; -$color-ste-completed-text: $color-1 !default; -$color-ste-sold-bg: $color-success !default; -$color-ste-sold-text: $color-1 !default; -$color-ste-pending-bg: $color-notice !default; -$color-ste-pending-text: $color-1 !default; -$color-ste-requires_authorization-bg: $color-notice !default; -$color-ste-requires_authorization-text: $color-1 !default; -$color-ste-awaiting_return-bg: $color-notice !default; -$color-ste-awaiting_return-text: $color-1 !default; -$color-ste-returned-bg: $color-notice !default; -$color-ste-returned-text: $color-1 !default; -$color-ste-credit_owed-bg: $color-notice !default; -$color-ste-credit_owed-text: $color-1 !default; -$color-ste-paid-bg: $color-success !default; -$color-ste-paid-text: $color-1 !default; -$color-ste-shipped-bg: $color-success !default; -$color-ste-shipped-text: $color-1 !default; -$color-ste-balance_due-bg: $color-notice !default; -$color-ste-balance_due-text: $color-1 !default; -$color-ste-backorder-bg: $color-notice !default; -$color-ste-backorder-text: $color-1 !default; -$color-ste-none-bg: $color-error !default; -$color-ste-none-text: $color-1 !default; -$color-ste-ready-bg: $color-success !default; -$color-ste-ready-text: $color-1 !default; -$color-ste-void-bg: $color-error !default; -$color-ste-void-text: $color-1 !default; -$color-ste-canceled-bg: $color-error !default; -$color-ste-canceled-text: $color-1 !default; -$color-ste-address-bg: $color-error !default; -$color-ste-address-text: $color-1 !default; -$color-ste-checkout-bg: $color-notice !default; -$color-ste-checkout-text: $color-1 !default; -$color-ste-cart-bg: $color-notice !default; -$color-ste-cart-text: $color-1 !default; -$color-ste-payment-bg: $color-error !default; -$color-ste-payment-text: $color-1 !default; -$color-ste-delivery-bg: $color-success !default; -$color-ste-delivery-text: $color-1 !default; -$color-ste-confirmation-bg: $color-error !default; -$color-ste-confirmation-text: $color-1 !default; -$color-ste-active-bg: $color-success !default; -$color-ste-active-text: $color-1 !default; -$color-ste-inactive-bg: $color-notice !default; -$color-ste-inactive-text: $color-1 !default; - -// Available states -$states: completed, complete, sold, pending, awaiting_return, returned, credit_owed, paid, shipped, balance_due, backorder, checkout, cart, address, delivery, payment, confirmation, canceled, ready, void, requires_authorization, active, inactive !default; -$states-bg-colors: $color-ste-completed-bg, $color-ste-complete-bg, $color-ste-sold-bg, $color-ste-pending-bg, $color-ste-awaiting_return-bg, $color-ste-returned-bg, $color-ste-credit_owed-bg, $color-ste-paid-bg, $color-ste-shipped-bg, $color-ste-balance_due-bg, $color-ste-backorder-bg, $color-ste-checkout-bg, $color-ste-cart-bg, $color-ste-address-bg, $color-ste-delivery-bg, $color-ste-payment-bg, $color-ste-confirmation-bg, $color-ste-canceled-bg, $color-ste-ready-bg, $color-ste-void-bg, $color-ste-requires_authorization-bg, $color-ste-active-bg, $color-ste-inactive-bg !default; -$states-text-colors: $color-ste-completed-text, $color-ste-complete-text, $color-ste-sold-text, $color-ste-pending-text, $color-ste-awaiting_return-text, $color-ste-returned-text, $color-ste-credit_owed-text, $color-ste-paid-text, $color-ste-shipped-text, $color-ste-balance_due-text, $color-ste-backorder-text, $color-ste-checkout-text, $color-ste-cart-text, $color-ste-address-text, $color-ste-delivery-text, $color-ste-payment-text, $color-ste-confirmation-text, $color-ste-canceled-text, $color-ste-ready-text, $color-ste-void-text, $color-ste-requires_authorization-text, $color-ste-active-text, $color-ste-inactive-text !default; - -// Available actions -$actions: edit, clone, remove, void, capture, save, cancel, mail !default; -$actions-bg-colors: $color-action-edit-bg, $color-action-clone-bg, $color-action-remove-bg, $color-action-void-bg, $color-action-capture-bg, $color-action-save-bg, $color-action-cancel-bg, $color-action-mail-bg !default; -$actions-brd-colors: $color-action-edit-brd, $color-action-clone-brd, $color-action-remove-brd, $color-action-void-brd, $color-action-capture-brd, $color-action-save-brd, $color-action-cancel-brd, $color-action-mail-brd !default; - -// Sizes -//-------------------------------------------------------------- -$body-font-size: 13px !default; - -$h6-size: $body-font-size + 2 !default; -$h5-size: $h6-size + 2 !default; -$h4-size: $h5-size + 2 !default; -$h3-size: $h4-size + 2 !default; -$h2-size: $h3-size + 2 !default; -$h1-size: $h2-size + 2 !default; - -$border-radius: 3px !default; -$border-input: 1px solid #2e3132; // Copied over from admin_v3 variables as a temporary solution - -$font-weight-bold: 600 !default; -$font-weight-normal: 400 !default; - -// z-index -//-------------------------------------------------------------- -$tos-banner-z-index: 102; diff --git a/app/webpacker/css/admin/plugins/flatpickr-customization.scss b/app/webpacker/css/admin/plugins/flatpickr-customization.scss deleted file mode 100644 index 79bc735d4d..0000000000 --- a/app/webpacker/css/admin/plugins/flatpickr-customization.scss +++ /dev/null @@ -1,68 +0,0 @@ -$background-grey: #eceef1; -$background-blue: $color-3; - -// scss-lint:disable SelectorFormat - -.flatpickr-calendar { - border-radius: 0; - - // Disable animation - &.animate.open { - animation: none; - } - - &.arrowBottom::after { - border-top-color: $background-grey; - } - - &.arrowTop::after { - border-bottom-color: $background-blue; - } - - .flatpickr-months .flatpickr-month { - border-radius: 0; - } - - .flatpickr-months .flatpickr-month, - .flatpickr-current-month .flatpickr-monthDropdown-months { - background: $background-blue; - } - - .flatpickr-weekdays { - background: $background-blue; - - .flatpickr-weekday { - background: $background-blue; - } - } - - .flatpickr-day.selected, - .flatpickr-day.startRange, - .flatpickr-day.endRange, - .flatpickr-day.selected.inRange, - .flatpickr-day.startRange.inRange, - .flatpickr-day.endRange.inRange, - .flatpickr-day.selected:focus, - .flatpickr-day.startRange:focus, - .flatpickr-day.endRange:focus, - .flatpickr-day.selected:hover, - .flatpickr-day.startRange:hover, - .flatpickr-day.endRange:hover, - .flatpickr-day.selected.prevMonthDay, - .flatpickr-day.startRange.prevMonthDay, - .flatpickr-day.endRange.prevMonthDay, - .flatpickr-day.selected.nextMonthDay, - .flatpickr-day.startRange.nextMonthDay, - .flatpickr-day.endRange.nextMonthDay { - background: $background-blue; - border-color: $background-blue; - } -} - -// scss-lint:enable SelectorFormat - -// customization for shortcut-buttons -.shortcut-buttons-flatpickr-wrapper > .shortcut-buttons-flatpickr-buttons { - justify-content: space-between; - flex-grow: 1; -} diff --git a/app/webpacker/css/admin/plugins/powertip.scss b/app/webpacker/css/admin/plugins/powertip.scss deleted file mode 100644 index 4afc7dfddb..0000000000 --- a/app/webpacker/css/admin/plugins/powertip.scss +++ /dev/null @@ -1,118 +0,0 @@ -#powerTip { - background-color: $color-3; - padding: 5px 15px; - @include border-radius($border-radius); - - &.n:before, - &.ne:before, - &.nw:before { - border-top-width: 5px; - border-top-color: $color-3; - bottom: -5px; - } - - &.e:before { - border-right-width: 5px; - border-right-color: $color-3; - left: -5px; - } - &.s:before, - &.se:before, - &.sw:before { - border-bottom-width: 5px; - border-bottom-color: $color-3; - top: -5px; - } - &.w:before { - border-left-width: 5px; - border-left-color: $color-3; - right: -5px; - } - &.ne:before, - &.se:before { - border-right-width: 5px; - border-right-color: $color-3; - left: -5px; - } - &.nw:before, - &.sw:before { - border-left-width: 5px; - border-right-color: $color-3; - right: -5px; - } - - &.clone, - &.yellow, - &.cancel { - background-color: $color-notice; - - &.n:before, - &.ne:before, - &.nw:before { - border-top-color: $color-notice; - } - &.e:before, - &.nw:before, - &.sw:before { - border-right-color: $color-notice; - } - &.s:before, - &.se:before, - &.sw:before { - border-bottom-color: $color-notice; - } - &.w:before { - border-left-color: $color-notice; - } - } - &.edit, - &.green, - &.capture, - &.save, - &.add { - background-color: $color-success; - - &.n:before, - &.ne:before, - &.nw:before { - border-top-color: $color-success; - } - &.e:before, - &.nw:before, - &.sw:before { - border-right-color: $color-success; - } - &.s:before, - &.se:before, - &.sw:before { - border-bottom-color: $color-success; - } - &.w:before { - border-left-color: $color-success; - } - } - &.remove, - &.red, - &.void { - background-color: $color-error; - - &.n:before, - &.ne:before, - &.nw:before { - border-top-color: $color-error; - } - &.e:before, - &.nw:before, - &.sw:before { - border-right-color: $color-error; - } - &.s:before, - &.se:before, - &.sw:before { - border-bottom-color: $color-error; - } - &.w:before { - border-left-color: $color-error; - } - } -} diff --git a/app/webpacker/css/admin/sections/orders.scss b/app/webpacker/css/admin/sections/orders.scss deleted file mode 100644 index e6e45309d3..0000000000 --- a/app/webpacker/css/admin/sections/orders.scss +++ /dev/null @@ -1,65 +0,0 @@ -// Customize orders filter -.admin-orders-index-search { - select[data-placeholder="Status"] { - width: 100%; - } - - .select2-container { - width: 100% !important; - } -} - -// Order-total -.order-details-total { - text-align: center; - - .order-total { - font-size: 35px; - font-weight: 600; - color: $color-success; - } -} - -.admin-order-form-fields { - legend.stock-location { - color: $color-body-text; - - .shipment-number { - color: $color-success; - } - .stock-location-name { - color: $color-success; - } - } -} - -.insufficient-stock-items { - legend { - color: $color-error; - } - - table tr:last-child th { - border-bottom: 1px solid $color-tbl-border; - } -} - -// Customize orduct add fieldset -#add-line-item { - fieldset { - padding: 10px 0; - - .field { - margin-bottom: 0; - - input[type="text"], - input[type="number"] { - width: 100%; - } - } - .actions { - .button { - margin-top: 28px; - } - } - } -} diff --git a/app/webpacker/css/admin/shared/forms.scss b/app/webpacker/css/admin/shared/forms.scss deleted file mode 100644 index 03bee63048..0000000000 --- a/app/webpacker/css/admin/shared/forms.scss +++ /dev/null @@ -1,261 +0,0 @@ -$text-inputs: "input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel]"; - -#{$text-inputs}, -input[type="date"], -input[type="datetime"], -input[type="time"], -input[type="number"], -textarea, -fieldset { - @include border-radius($border-radius); - padding: $vpadding-txt $hpadding-txt; - border: 1px solid $color-txt-brd; - color: $color-txt-text; - font-size: 90%; - - &:focus { - outline: none; - border-color: $color-txt-hover-brd; - } - - &[disabled] { - opacity: 0.7; - } -} - -textarea { - line-height: 19px; -} - -.fullwidth { - width: 100%; -} - -label { - font-weight: 600; - text-transform: uppercase; - font-size: 85%; - display: inline; - margin-bottom: 5px; - color: $color-4; - - &.inline { - display: inline-block !important; - } - - &.block { - display: block !important; - } -} - -.label-block label { - display: block; -} - -span.info { - font-style: italic; - font-size: 85%; - color: lighten($color-body-text, 15); - display: block; - line-height: 20px; - margin: 5px 0; -} - -.field { - padding: 10px 0; - - &.checkbox { - min-height: 70px; - - input[type="checkbox"] { - display: inline-block; - width: auto; - } - - label { - cursor: pointer; - display: block; - } - } - - ul { - border-top: 1px solid $color-border; - list-style: none; - padding-top: 5px; - - li { - display: inline-block; - padding-right: 10px; - - label { - font-weight: normal; - text-transform: none; - } - &.white-space-nowrap { - white-space: nowrap; - } - } - } - - // Errors described by default form builder - .field_with_errors { - label { - color: $color-error; - } - - input { - border-color: $color-error; - } - } - // Errors described by Spree::Admin::BaseHelper - .formError { - color: $color-error; - font-style: italic; - font-size: 85%; - } -} - -fieldset { - box-shadow: none; - box-sizing: border-box; - border-color: $color-border; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - margin-left: 0; - margin-right: 0; - position: relative; - margin-bottom: 35px; - padding: 10px 0 15px 0; - background-color: transparent; - border-left: none; - border-right: none; - border-radius: 0; - - &.no-border-bottom { - border-bottom: none; - margin-bottom: 0; - } - - &.no-border-top { - border-top: none; - padding-top: 0; - } - - legend { - background-color: $color-1; - color: $color-2; - font-size: 14px; - font-weight: 600; - text-transform: uppercase; - text-align: center; - padding: 8px 15px; - margin: 0 auto; - - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - i { - color: $color-link; - } - } - - label { - color: lighten($color-body-text, 8); - } - - .filter-actions { - margin-bottom: -32px; - margin-top: 15px; - text-align: center; - - form { - display: inline-block; - } - - button, - .button, - input[type="submit"], - input[type="button"], - span.or { - @include border-radius($border-radius); - - -webkit-box-shadow: 0 0 0 15px $color-1; - -moz-box-shadow: 0 0 0 15px $color-1; - -ms-box-shadow: 0 0 0 15px $color-1; - -o-box-shadow: 0 0 0 15px $color-1; - box-shadow: 0 0 0 15px $color-1; - - &:hover { - border-color: $color-1; - } - - &:first-of-type { - margin-right: 1.25em; - } - } - - span.or { - background-color: $color-1; - border-width: 5px; - margin-left: 5px; - margin-right: 5px; - position: relative; - - -webkit-box-shadow: 0 0 0 5px $color-1; - -moz-box-shadow: 0 0 0 5px $color-1; - -ms-box-shadow: 0 0 0 5px $color-1; - -o-box-shadow: 0 0 0 5px $color-1; - box-shadow: 0 0 0 5px $color-1; - } - } - - &.labels-inline { - .field { - margin-bottom: 0; - display: table; - width: 100%; - - label, - input { - display: table-cell !important; - } - input { - width: 100%; - } - - &.checkbox { - input { - width: auto !important; - } - } - } - .actions { - padding: 0; - text-align: right; - } - } -} - -.form-buttons { - text-align: center; -} - -select { - @extend input, [type="text"]; - background-color: white; -} - -.inline-checkbox { - display: inline-flex; - align-items: center; - margin-top: 3px; - - input, - label { - cursor: pointer; - } - label { - margin: 0; - padding-left: 0.4rem; - } -} diff --git a/app/webpacker/css/admin/shared/icons.scss b/app/webpacker/css/admin/shared/icons.scss deleted file mode 100644 index 2e581b209b..0000000000 --- a/app/webpacker/css/admin/shared/icons.scss +++ /dev/null @@ -1,25 +0,0 @@ -// Some fixes for fontwesome stylesheets -[class^="icon-"], [class*=" icon-"] { - &:before { - padding-right: 5px; - } - - &.button, &.icon_link { - width: auto; - - &:before { - padding-top: 3px; - } - } -} - -.icon-email:before { @extend .icon-envelope, :before; } -.icon-resend_authorization_email:before { @extend .icon-envelope, :before; } -.icon-resume:before { @extend .icon-refresh, :before; } - -.icon-cancel:before, -.icon-void:before { @extend .icon-remove, :before; } - -.icon-capture, -.icon-capture_and_complete_order { @extend .icon-ok } -.icon-credit:before { @extend .icon-ok, :before ; } diff --git a/app/webpacker/css/admin/shared/layout.scss b/app/webpacker/css/admin/shared/layout.scss deleted file mode 100644 index 35d4c2613d..0000000000 --- a/app/webpacker/css/admin/shared/layout.scss +++ /dev/null @@ -1,134 +0,0 @@ -// Basics -//--------------------------------------------------- -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.admin { - &__section-header { - padding: 15px 0; - background-color: very-light($color-3, 4); - border-bottom: 1px solid $color-border; - - .ofn-drop-down { - border: 0; - background-color: $spree-blue; - color: $color-1; - float: none; - margin-left: 3px; - &:hover, - &.expanded { - border: 0; - color: $color-1; - } - } - - &__content { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - @media all and (min-width: $tablet_breakpoint) { - flex-wrap: nowrap; - } - } - - &__title { - width: 100%; - margin-bottom: 10px; - @media all and (min-width: $tablet_breakpoint) { - margin-bottom: 0; - } - } - - &__actions { - display: flex; - flex: 1 0 auto; - align-items: center; - list-style: none; - @media all and (min-width: $tablet_breakpoint) { - justify-content: flex-end; - } - > li { - display: flex; - margin-right: 10px; - &:empty { - display: none; - } - &:last-child { - margin-right: 0; - } - } - } - } -} - -.hidden { - display: none; -} - -.float-right { - float: right; -} - -.float-left { - float: left; -} - -.mr-0 { - margin-right: 0 !important; -} - -.ml-0 { - margin-left: 0 !important; -} - -@media print { - .print-hidden { - display: none !important; - } -} - -// Header -//--------------------------------------------------- -#header { - background-color: $color-1; - padding: 5px 0; -} - -#logo { - height: 40px; -} - -.page-title { - i { - color: $color-2; - } -} - -// Content -//--------------------------------------------------- -#content { - background-color: $color-1; - position: relative; - z-index: 0; - padding: 0; - margin-top: 15px; -} - -// Footer -//--------------------------------------------------- -#footer { - margin-top: 15px; - border-top: 1px solid $color-border; - padding: 10px 0; -} - -@media print { - header, - nav { - display: none; - } -} diff --git a/app/webpacker/css/admin/shared/tables.scss b/app/webpacker/css/admin/shared/tables.scss deleted file mode 100644 index a518f223b1..0000000000 --- a/app/webpacker/css/admin/shared/tables.scss +++ /dev/null @@ -1,208 +0,0 @@ -table { - width: 100%; - margin-bottom: 15px; - border-collapse: separate; - - th, td { - padding: 7px 5px; - border-right: 1px solid $color-border; - border-bottom: 1px solid $color-border; - vertical-align: middle; - text-overflow: ellipsis; - - img { - border: 1px solid transparent; - } - - &:first-child { - border-left: 1px solid $color-border; - } - - a { - border-bottom: 1px dotted lighten($color-link, 10); - - &:hover { - border-color: lighten($color-link-hover, 10); - } - } - - .handle { - display: block !important; - text-align: center; - padding-right: 0; - } - - &.actions { - background-color: transparent; - border: none !important; - text-align: center; - - span.text { - font-size: $body-font-size; - } - - [class*='icon-'].no-text { - font-size: 120%; - background-color: very-light($color-3); - border: 1px solid $color-border; - border-radius: 15px; - width: 29px; - height: 29px; - display: inline-block; - padding-top: 2px; - - &:before { - text-align: center !important; - width: 27px; - display: inline-block; - } - - &:hover { - border-color: transparent; - } - } - - button[class*='icon-'] { - color: $color-link; - padding: 0 !important; - } - - .icon-envelope-alt, .icon-eye-open { - color: $color-link; - padding-left: 0px; - - &:hover { - background-color: $color-3; - color: $color-1; - } - } - .icon-trash:hover, .icon-void:hover { - background-color: $color-error; - color: $color-1; - } - .icon-cancel:hover { - background-color: $color-notice; - color: $color-1; - } - .icon-edit:hover, .icon-capture:hover, .icon-capture_and_complete_order:hover, .icon-ok:hover, .icon-plus:hover, .icon-road:hover { - background-color: $color-success; - color: $color-1; - } - .icon-copy:hover { - background-color: $color-notice; - color: $color-1; - } - } - - input[type="number"], - input[type="text"] { - width: 100%; - } - - &.no-border { - border-right: none; - } - - .handle { - @extend .icon-reorder; - font-family: FontAwesome; - text-decoration: inherit; - display: inline-block; - speak: none; - cursor: move; - } - - } - - &.no-borders { - td, th { - border: none !important; - } - - } - - thead { - th { - padding: 10px; - border-top: 1px solid $color-border; - border-bottom: none; - background-color: $color-tbl-thead; - text-transform: uppercase; - font-size: 85%; - font-weight: $font-weight-bold; - } - } - - tbody { - tr { - &:first-child th, - &:first-child td { - border-top: 1px solid $color-border; - } - &.even td { - background-color: $color-tbl-even; - - img { - border: 1px solid very-light($color-3, 6); - } - } - - &:hover td { - background-color: very-light($color-3, 5); - - img { - border: 1px solid $color-border; - } - } - - &.deleted td { - background-color: very-light($color-error, 6); - border-color: very-light($color-error, 15); - } - - &.ui-sortable-placeholder td { - border: 1px solid $color-2 !important; - visibility: visible !important; - - &.actions { - background-color: transparent; - border-right: none !important; - border-top: none !important; - border-bottom: none !important; - border-left: 1px solid $color-2 !important; - } - } - - &.ui-sortable-helper { - width: 100%; - - td { - background-color: lighten($color-3, 33); - border-bottom: 1px solid $color-border; - - &.actions { - display: none; - } - } - } - } - - &.no-border-top tr:first-child td { - border-top: none; - } - - &.grand-total { - td { - border-color: $color-2 !important; - text-transform: uppercase; - font-size: 110%; - font-weight: 600; - background-color: lighten($color-2, 50); - } - .total { - background-color: $color-2; - color: $color-1; - } - } - } -} diff --git a/app/webpacker/css/admin/shared/typography.scss b/app/webpacker/css/admin/shared/typography.scss deleted file mode 100644 index 4435933458..0000000000 --- a/app/webpacker/css/admin/shared/typography.scss +++ /dev/null @@ -1,158 +0,0 @@ -// Base -//-------------------------------------------------------------- -body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; font-size: $body-font-size; } - -body { - font-family: $base-font-family; - font-size: $body-font-size; - font-weight: 400; - color: $color-body-text; - text-rendering: optimizeLegibility; -} - -hr { - border-top: 1px solid $color-border; - border-bottom: 1px solid white; - border-left: none; -} - -strong, b { - font-weight: 600; -} - -// links -//-------------------------------------------------------------- -a { - color: $color-link; - text-decoration: none; - line-height: inherit; - - &, &:hover, &:active, &:visited, &:focus { - outline: none; - } - - &:visited { - color: $color-link-visited; - } - &:focus { - color: $color-link-focus; - } - &:active { - color: $color-link-active; - } - &:hover { - color: $color-link-hover; - } -} - -// Headings -//-------------------------------------------------------------- - -h1,h2,h3,h4,h5,h6 { - font-weight: 600; - color: $color-headers; - line-height: 1.1; -} - -h1 { font-size: $h1-size; line-height: $h1-size + 6 } -h2 { font-size: $h2-size; line-height: $h1-size + 4 } -h3 { font-size: $h3-size; line-height: $h1-size + 2 } -h4 { font-size: $h4-size; line-height: $h1-size } -h5 { font-size: $h5-size; line-height: $h1-size } -h6 { font-size: $h6-size; line-height: $h1-size } - - -// Lists -//-------------------------------------------------------------- -ul { - &.inline-menu { - li { - display: inline-block; - } - } - &.fields { - list-style: none; - padding: 0; - margin: 0; - } -} - -dl { - width: 100%; - overflow: hidden; - margin: 5px 0; - color: lighten($color-body-text, 15); - - dt, dd { - float: left; - line-height: 16px; - padding: 5px; - text-align: justify; - } - - dt { - width: 40%; - font-weight: 600; - padding-left: 0; - text-transform: uppercase; - font-size: 85%; - } - - dd { - width: 60%; - padding-right: 0; - } - - dd:after { - content: ''; - clear: both; - } - -} - -// Helpers -.align-center { text-align: center } -.align-right { text-align: right } -.align-left { text-align: left } -.align-justify { text-align: justify } - -.uppercase { text-transform: uppercase } - -.green { color: $color-2 } -.blue { color: $color-3 } -.red { color: $color-5 } -.yellow { color: $color-6 } - -.no-objects-found { - text-align: center; - font-size: 120%; - text-transform: uppercase; - padding: 40px 0px; - color: lighten($color-body-text, 15); -} - -.text-normal { - font-size: 1rem; - font-weight: 300; -} - -.text-big { - font-size: 1.2rem; - font-weight: 300; -} - -.text-red { - color: $color-warning; -} - -input.text-big { - font-size: 1.1rem; -} - -.pad-top { - padding-top: 1em; -} - -.white-space-nowrap { - white-space: nowrap; -} diff --git a/app/webpacker/css/admin/terms_of_service_banner.scss b/app/webpacker/css/admin/terms_of_service_banner.scss deleted file mode 100644 index ca23f9b763..0000000000 --- a/app/webpacker/css/admin/terms_of_service_banner.scss +++ /dev/null @@ -1,27 +0,0 @@ -#banner-container { - position: fixed; - bottom: 0px; - left: 0; - width: 100%; - z-index: $tos-banner-z-index; - - .terms-of-service-banner { - padding: 18px; - text-align: center; - font-size: 120%; - color: white; - font-weight: 600; - margin-top: 0; - background-color: rgba($color-notice, 0.8); - display: flex; - - .column-left { - width: 70%; - } - - .column-right { - width: 30%; - text-align: center; - } - } -} diff --git a/app/webpacker/packs/admin-styles.scss b/app/webpacker/packs/admin-styles.scss deleted file mode 100644 index 32d33f37b6..0000000000 --- a/app/webpacker/packs/admin-styles.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import "../css/admin/all.scss"; -@import "../../../node_modules/trix/dist/trix.css"; From 1717c5376bcad4bde23e42d1d139326ce1418107 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 07:11:07 +0500 Subject: [PATCH 09/13] Remove deprecated migrations for admin style v3 activation --- ...240625024328_activate_admin_style_v3_for_new_users.rb | 5 ----- ...710013128_enable_feature_admin_style_v3_for_admins.rb | 5 ----- .../20240715103415_enable_admin_style_v3_by_default.rb | 7 ------- ...150852_activate_admin_style_v3_for_25_p_cent_users.rb | 5 ----- ...40731065321_activate_admin_style_v3_for50_pc_users.rb | 5 ----- ...40810150912_activate_admin_style_v3_for75_pc_users.rb | 9 --------- .../20240828203544_fully_enable_admin_style_v3_flag.rb | 5 ----- 7 files changed, 41 deletions(-) delete mode 100644 db/migrate/20240625024328_activate_admin_style_v3_for_new_users.rb delete mode 100644 db/migrate/20240710013128_enable_feature_admin_style_v3_for_admins.rb delete mode 100644 db/migrate/20240715103415_enable_admin_style_v3_by_default.rb delete mode 100644 db/migrate/20240718150852_activate_admin_style_v3_for_25_p_cent_users.rb delete mode 100644 db/migrate/20240731065321_activate_admin_style_v3_for50_pc_users.rb delete mode 100644 db/migrate/20240810150912_activate_admin_style_v3_for75_pc_users.rb delete mode 100644 db/migrate/20240828203544_fully_enable_admin_style_v3_flag.rb diff --git a/db/migrate/20240625024328_activate_admin_style_v3_for_new_users.rb b/db/migrate/20240625024328_activate_admin_style_v3_for_new_users.rb deleted file mode 100644 index 55fb41febd..0000000000 --- a/db/migrate/20240625024328_activate_admin_style_v3_for_new_users.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ActivateAdminStyleV3ForNewUsers < ActiveRecord::Migration[7.0] - def up - Flipper.enable_group(:admin_style_v3, :new_2024_07_03) - end -end diff --git a/db/migrate/20240710013128_enable_feature_admin_style_v3_for_admins.rb b/db/migrate/20240710013128_enable_feature_admin_style_v3_for_admins.rb deleted file mode 100644 index 9d04164719..0000000000 --- a/db/migrate/20240710013128_enable_feature_admin_style_v3_for_admins.rb +++ /dev/null @@ -1,5 +0,0 @@ -class EnableFeatureAdminStyleV3ForAdmins < ActiveRecord::Migration[7.0] - def up - Flipper.enable_group(:admin_style_v3, :admins) - end -end diff --git a/db/migrate/20240715103415_enable_admin_style_v3_by_default.rb b/db/migrate/20240715103415_enable_admin_style_v3_by_default.rb deleted file mode 100644 index 29099bcb0f..0000000000 --- a/db/migrate/20240715103415_enable_admin_style_v3_by_default.rb +++ /dev/null @@ -1,7 +0,0 @@ -class EnableAdminStyleV3ByDefault < ActiveRecord::Migration[7.0] - def up - if Rails.env.development? - Flipper.enable(:admin_style_v3) - end - end -end diff --git a/db/migrate/20240718150852_activate_admin_style_v3_for_25_p_cent_users.rb b/db/migrate/20240718150852_activate_admin_style_v3_for_25_p_cent_users.rb deleted file mode 100644 index 066a18745f..0000000000 --- a/db/migrate/20240718150852_activate_admin_style_v3_for_25_p_cent_users.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ActivateAdminStyleV3For25PCentUsers < ActiveRecord::Migration[7.0] - def up - Flipper.enable_percentage_of_actors(:admin_style_v3, 25) - end -end diff --git a/db/migrate/20240731065321_activate_admin_style_v3_for50_pc_users.rb b/db/migrate/20240731065321_activate_admin_style_v3_for50_pc_users.rb deleted file mode 100644 index 0cde1950fb..0000000000 --- a/db/migrate/20240731065321_activate_admin_style_v3_for50_pc_users.rb +++ /dev/null @@ -1,5 +0,0 @@ -class ActivateAdminStyleV3For50PcUsers < ActiveRecord::Migration[7.0] - def up - Flipper.enable_percentage_of_actors(:admin_style_v3, 50) - end -end diff --git a/db/migrate/20240810150912_activate_admin_style_v3_for75_pc_users.rb b/db/migrate/20240810150912_activate_admin_style_v3_for75_pc_users.rb deleted file mode 100644 index cc412c0b58..0000000000 --- a/db/migrate/20240810150912_activate_admin_style_v3_for75_pc_users.rb +++ /dev/null @@ -1,9 +0,0 @@ -class ActivateAdminStyleV3For75PcUsers < ActiveRecord::Migration[7.0] - def up - Flipper.enable_percentage_of_actors(:admin_style_v3, 75) - end - - def down - Flipper.enable_percentage_of_actors(:admin_style_v3, 50) - end -end diff --git a/db/migrate/20240828203544_fully_enable_admin_style_v3_flag.rb b/db/migrate/20240828203544_fully_enable_admin_style_v3_flag.rb deleted file mode 100644 index db2fd6007f..0000000000 --- a/db/migrate/20240828203544_fully_enable_admin_style_v3_flag.rb +++ /dev/null @@ -1,5 +0,0 @@ -class FullyEnableAdminStyleV3Flag < ActiveRecord::Migration[7.0] - def up - Flipper.enable(:admin_style_v3) - end -end From acfe3f658986ba3df3b4b8a742ea2fa7357169a7 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 07:20:16 +0500 Subject: [PATCH 10/13] Remove admin_style_v3 feature toggle and related conditional logic --- lib/open_food_network/feature_toggle.rb | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/lib/open_food_network/feature_toggle.rb b/lib/open_food_network/feature_toggle.rb index 604f949084..8e9932f169 100644 --- a/lib/open_food_network/feature_toggle.rb +++ b/lib/open_food_network/feature_toggle.rb @@ -8,16 +8,10 @@ module OpenFoodNetwork # module FeatureToggle def self.conditional_features - features = {} - if Rails.env.development? - features.merge!({ - "admin_style_v3" => <<~DESC, - Test the work-in-progress design updates. - DESC - }); - end + # Returns environment-specific features that are conditionally available + # Currently empty but can be used to add features based on environment - features + {} end # Please add your new feature here to appear in the Flipper UI. @@ -73,9 +67,6 @@ module OpenFoodNetwork ACTIVE_BY_DEFAULT = { # Copy features here that were activated in a migration so that new # instances, development and test environments have the feature active. - "admin_style_v3" => <<~DESC, - Test the work-in-progress design updates. - DESC }.freeze def self.setup! @@ -94,9 +85,6 @@ module OpenFoodNetwork # Checks weather a feature is enabled for any of the given actors. def self.enabled?(feature_name, *actors) - # TODO: Need to remove these checks when we fully remove the toggle from development as well - # need this check as Flipper won't recognize 'admin_style_v3' as it is removed for server envs - return true if !Rails.env.development? && feature_name == :admin_style_v3 return Flipper.enabled?(feature_name) if actors.empty? actors.any? do |actor| From 3f39d94bd30cf94d9942279fb520d77b67d49559 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 07:21:10 +0500 Subject: [PATCH 11/13] Remove conditional rendering for previous page icon in pagination --- app/views/admin/shared/_stimulus_pagination.html.haml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/views/admin/shared/_stimulus_pagination.html.haml b/app/views/admin/shared/_stimulus_pagination.html.haml index 23860ead4e..632b766653 100644 --- a/app/views/admin/shared/_stimulus_pagination.html.haml +++ b/app/views/admin/shared/_stimulus_pagination.html.haml @@ -3,10 +3,7 @@ .pagination{ "data-controller": "search" } - if pagy.prev %button.page.prev{ data: { action: 'click->search#changePage', page: pagy.prev } } - - if feature?(:admin_style_v3, spree_current_user) - %i.icon-chevron-left{ data: { action: 'click->search#changePage', page: pagy.prev } } - - else - != pagy_t('pagy.prev') + %i.icon-chevron-left{ data: { action: 'click->search#changePage', page: pagy.prev } } - else %button.page.disabled{disabled: "disabled"}!= pagy_t('pagy.prev') From df4cf4b76809fad0a7aa1f463578b22b44e4158d Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Sun, 27 Jul 2025 07:58:04 +0500 Subject: [PATCH 12/13] Fix specs --- .../spree/admin/variants_controller.rb | 17 ++ config/routes/spree.rb | 2 +- .../api/v0/products_controller_spec.rb | 176 ------------------ .../spree/admin/products_controller_spec.rb | 124 ------------ 4 files changed, 18 insertions(+), 301 deletions(-) diff --git a/app/controllers/spree/admin/variants_controller.rb b/app/controllers/spree/admin/variants_controller.rb index 83fe131045..7ec309ea93 100644 --- a/app/controllers/spree/admin/variants_controller.rb +++ b/app/controllers/spree/admin/variants_controller.rb @@ -72,8 +72,25 @@ module Spree render json: @variants, each_serializer: ::Api::Admin::VariantSerializer end + def destroy + @url_filters = ::ProductFilters.new.extract(request.query_parameters) + + @variant = Spree::Variant.find(params[:id]) + flash[:success] = delete_variant + + redirect_to spree.admin_product_variants_url(params[:product_id], @url_filters) + end + protected + def delete_variant + if VariantDeleter.new.delete(@variant) + Spree.t('notice_messages.variant_deleted') + else + Spree.t('notice_messages.variant_not_deleted') + end + end + def create_before @object.save end diff --git a/config/routes/spree.rb b/config/routes/spree.rb index a133ff7418..512f708b14 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -68,7 +68,7 @@ Spree::Core::Engine.routes.draw do end end - resources :variants, except: :destroy do + resources :variants do collection do post :update_positions end diff --git a/spec/controllers/api/v0/products_controller_spec.rb b/spec/controllers/api/v0/products_controller_spec.rb index 8150cf9332..09dab8cc70 100644 --- a/spec/controllers/api/v0/products_controller_spec.rb +++ b/spec/controllers/api/v0/products_controller_spec.rb @@ -62,33 +62,6 @@ RSpec.describe Api::V0::ProductsController do api_put :update, id: product.to_param, product: { name: "I hacked your store!" } assert_unauthorized! end - - it "cannot delete a product" do - api_delete :destroy, id: product.to_param - assert_unauthorized! - end - end - - context "as an enterprise user" do - let(:current_api_user) { supplier_enterprise_user(supplier) } - - it "can delete my product" do - expect(product.deleted_at).to be_nil - api_delete :destroy, id: product.to_param - - expect(response).to have_http_status(:no_content) - expect { product.reload }.not_to raise_error - expect(product.deleted_at).not_to be_nil - end - - it "is denied access to deleting another enterprises' product" do - expect(product_other_supplier.deleted_at).to be_nil - api_delete :destroy, id: product_other_supplier.to_param - - assert_unauthorized! - expect { product_other_supplier.reload }.not_to raise_error - expect(product_other_supplier.deleted_at).to be_nil - end end context "as an administrator" do @@ -137,155 +110,6 @@ RSpec.describe Api::V0::ProductsController do expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") expect(json_response["errors"]["name"]).to eq(["can't be blank"]) end - - it "can delete a product" do - expect(product.deleted_at).to be_nil - api_delete :destroy, id: product.to_param - - expect(response).to have_http_status(:no_content) - expect(product.reload.deleted_at).not_to be_nil - end - end - - describe '#clone' do - context 'as a normal user' do - before do - allow(current_api_user) - .to receive(:admin?).and_return(false) - end - - it 'denies access' do - spree_post :clone, product_id: product.id, format: :json - - assert_unauthorized! - end - end - - context 'as an enterprise user' do - let(:current_api_user) { supplier_enterprise_user(supplier) } - let!(:variant) { create(:variant, product_id: product.id) } - - it 'responds with a successful response' do - spree_post :clone, product_id: product.id, format: :json - - expect(response).to have_http_status(:created) - end - - it 'clones the product' do - spree_post :clone, product_id: product.id, format: :json - - expect(json_response['name']).to eq("COPY OF #{product.name}") - end - - it 'clones a product with image' do - spree_post :clone, product_id: product_with_image.id, format: :json - - expect(response).to have_http_status(:created) - expect(json_response['name']).to eq("COPY OF #{product_with_image.name}") - end - - # test cases related to bug #660: product duplication clones master variant - - # stock info - clone is set to zero - it '(does not) clone the stock info of the product' do - spree_post :clone, product_id: product.id, format: :json - expect(json_response.dig("variants", 0, "on_hand")).to eq(0) - end - - # variants: only the master variant of the product is cloned - it '(does not) clone variants from a product with several variants' do - spree_post :clone, product_id: product.id, format: :json - expect(Spree::Product.second.variants.count).not_to eq Spree::Product.first.variants.count - end - end - - context 'as an administrator' do - before do - allow(current_api_user) - .to receive(:admin?).and_return(true) - end - - it 'responds with a successful response' do - spree_post :clone, product_id: product.id, format: :json - - expect(response).to have_http_status(:created) - end - - it 'clones the product' do - spree_post :clone, product_id: product.id, format: :json - - expect(json_response['name']).to eq("COPY OF #{product.name}") - end - - it 'clones a product with image' do - spree_post :clone, product_id: product_with_image.id, format: :json - - expect(response).to have_http_status(:created) - expect(json_response['name']).to eq("COPY OF #{product_with_image.name}") - end - end - end - - describe '#bulk_products' do - context "as an enterprise user" do - let!(:taxon) { create(:taxon) } - let!(:product2) { create(:product, supplier_id: supplier.id, primary_taxon: taxon) } - let!(:product3) { create(:product, supplier_id: supplier2.id, primary_taxon: taxon) } - let!(:product4) { create(:product, supplier_id: supplier2.id) } - let(:current_api_user) { supplier_enterprise_user(supplier) } - - before { current_api_user.enterprise_roles.create(enterprise: supplier2) } - - it "returns a list of products" do - api_get :bulk_products, { page: 1, per_page: 15 }, format: :json - expect(returned_product_ids).to eq [product4.id, product3.id, product2.id, - other_product.id, product.id] - end - - it "returns pagination data" do - api_get :bulk_products, { page: 1, per_page: 15 }, format: :json - expect(json_response['pagination']).to eq "results" => 5, "pages" => 1, "page" => 1, - "per_page" => 15 - end - - it "uses defaults when page and per_page are not supplied" do - api_get :bulk_products, format: :json - expect(json_response['pagination']).to eq "results" => 5, "pages" => 1, "page" => 1, - "per_page" => 15 - end - - it "returns paginated products by page" do - api_get :bulk_products, { page: 1, per_page: 2 }, format: :json - expect(returned_product_ids).to eq [product4.id, product3.id] - - api_get :bulk_products, { page: 2, per_page: 2 }, format: :json - expect(returned_product_ids).to eq [product2.id, other_product.id] - end - - it "filters results by supplier" do - api_get :bulk_products, - { page: 1, per_page: 15, q: { variants_supplier_id_eq: supplier.id } }, - format: :json - expect(returned_product_ids).to eq [product2.id, other_product.id, product.id] - end - - it "filters results by product category" do - api_get :bulk_products, - { page: 1, per_page: 15, q: { variants_primary_taxon_id_eq: taxon.id } }, - format: :json - expect(returned_product_ids).to eq [product3.id, product2.id] - end - - it "filters results by import_date" do - product.variants.first.update_attribute :import_date, 1.day.ago - product2.variants.first.update_attribute :import_date, 2.days.ago - product3.variants.first.update_attribute :import_date, 1.day.ago - - api_get :bulk_products, { page: 1, per_page: 15, import_date: 1.day.ago.to_date.to_s }, - format: :json - expect(returned_product_ids).to eq [product3.id, product.id] - end - end end private diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb index af89e197fc..f57392aa81 100644 --- a/spec/controllers/spree/admin/products_controller_spec.rb +++ b/spec/controllers/spree/admin/products_controller_spec.rb @@ -3,130 +3,6 @@ require 'spec_helper' RSpec.describe Spree::Admin::ProductsController do - describe 'bulk_update' do - context "updating a product we do not have access to" do - let(:s_managed) { create(:enterprise) } - let(:s_unmanaged) { create(:enterprise) } - let(:product) do - create(:simple_product, supplier_id: s_unmanaged.id, name: 'Peas') - end - - before do - controller_login_as_enterprise_user [s_managed] - spree_post :bulk_update, - "products" => [{ "id" => product.id, "name" => "Pine nuts" }] - end - - it "denies access" do - expect(response).to redirect_to unauthorized_path - end - - it "does not update any product" do - expect(product.reload.name).not_to eq("Pine nuts") - end - end - - context "when changing a product's variant_unit" do - let(:producer) { create(:enterprise) } - let!(:product) do - create( - :simple_product, - supplier_id: producer.id, - variant_unit: 'items', - variant_unit_scale: nil, - variant_unit_name: 'bunches', - unit_value: nil, - unit_description: 'some description' - ) - end - - before { controller_login_as_enterprise_user([producer]) } - - it 'succeeds' do - spree_post :bulk_update, - "products" => [ - { - "id" => product.id, - "variant_unit" => "weight", - "variant_unit_scale" => 1 - } - ] - - expect(response).to have_http_status(:found) - end - - it 'does not redirect to bulk_products' do - spree_post :bulk_update, - "products" => [ - { - "id" => product.id, - "variant_unit" => "weight", - "variant_unit_scale" => 1 - } - ] - - expect(response).to redirect_to( - '/api/v0/products/bulk_products' - ) - end - end - - context 'when passing empty variants_attributes' do - let(:producer) { create(:enterprise) } - let!(:product) do - create( - :simple_product, - supplier_id: producer.id, - variant_unit: 'items', - variant_unit_scale: nil, - variant_unit_name: 'bunches', - unit_value: nil, - unit_description: 'bunches' - ) - end - let!(:another_product) do - create( - :simple_product, - supplier_id: producer.id, - variant_unit: 'weight', - variant_unit_scale: 1000, - variant_unit_name: nil - ) - end - let!(:taxon) { create(:taxon) } - - before { controller_login_as_enterprise_user([producer]) } - - it 'does not fail' do - spree_post :bulk_update, - "products" => [ - { - "id" => another_product.id, - "variants_attributes" => [{}] - }, - { - "id" => product.id, - "variants_attributes" => [ - { - "on_hand" => 2, - "price" => "5.0", - "unit_value" => 4, - "variant_unit" => "weight", - "variant_unit_scale" => "1", - "unit_description" => "", - "display_name" => "name", - "primary_taxon_id" => taxon.id, - "supplier_id" => producer.id - } - ] - } - ] - - expect(response).to have_http_status(:found) - end - end - end - context "creating a new product" do let(:supplier) { create(:supplier_enterprise) } let(:taxon) { create(:taxon) } From 75b2fe1dd472655b9e79c50e1a79ed5fd305e11c Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Fri, 1 Aug 2025 01:48:21 +0500 Subject: [PATCH 13/13] revert API removals --- app/controllers/api/v0/products_controller.rb | 27 +++ config/routes/api.rb | 3 + .../api/v0/products_controller_spec.rb | 176 ++++++++++++++++++ 3 files changed, 206 insertions(+) diff --git a/app/controllers/api/v0/products_controller.rb b/app/controllers/api/v0/products_controller.rb index 1ba016cc5a..6f95e1e64e 100644 --- a/app/controllers/api/v0/products_controller.rb +++ b/app/controllers/api/v0/products_controller.rb @@ -38,12 +38,39 @@ module Api end end + def destroy + authorize! :delete, Spree::Product + @product = product_finder.find_product + authorize! :delete, @product + @product.destroyed_by = current_api_user + @product.destroy + head :no_content + end + + def bulk_products + @products = product_finder.bulk_products + + render_paged_products @products + end + def overridable @products = product_finder.products_for_producers render_paged_products @products, ::Api::Admin::ProductSimpleSerializer end + # POST /api/products/:product_id/clone + # + def clone + authorize! :create, Spree::Product + original_product = product_finder.find_product_to_be_cloned + authorize! :update, original_product + + @product = original_product.duplicate + + render json: @product, serializer: Api::Admin::ProductSerializer, status: :created + end + private def product_finder diff --git a/config/routes/api.rb b/config/routes/api.rb index 725d022b98..30bd7cd1b1 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -16,6 +16,7 @@ Openfoodnetwork::Application.routes.draw do namespace :v0 do resources :products do collection do + get :bulk_products get :overridable end post :clone @@ -74,6 +75,8 @@ Openfoodnetwork::Application.routes.draw do resources :enterprise_fees, only: [:destroy] + post '/product_images/:product_id', to: 'product_images#update_product_image' + resources :states, :only => [:index, :show] resources :taxons, except: %i[show edit] diff --git a/spec/controllers/api/v0/products_controller_spec.rb b/spec/controllers/api/v0/products_controller_spec.rb index 09dab8cc70..8150cf9332 100644 --- a/spec/controllers/api/v0/products_controller_spec.rb +++ b/spec/controllers/api/v0/products_controller_spec.rb @@ -62,6 +62,33 @@ RSpec.describe Api::V0::ProductsController do api_put :update, id: product.to_param, product: { name: "I hacked your store!" } assert_unauthorized! end + + it "cannot delete a product" do + api_delete :destroy, id: product.to_param + assert_unauthorized! + end + end + + context "as an enterprise user" do + let(:current_api_user) { supplier_enterprise_user(supplier) } + + it "can delete my product" do + expect(product.deleted_at).to be_nil + api_delete :destroy, id: product.to_param + + expect(response).to have_http_status(:no_content) + expect { product.reload }.not_to raise_error + expect(product.deleted_at).not_to be_nil + end + + it "is denied access to deleting another enterprises' product" do + expect(product_other_supplier.deleted_at).to be_nil + api_delete :destroy, id: product_other_supplier.to_param + + assert_unauthorized! + expect { product_other_supplier.reload }.not_to raise_error + expect(product_other_supplier.deleted_at).to be_nil + end end context "as an administrator" do @@ -110,6 +137,155 @@ RSpec.describe Api::V0::ProductsController do expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.") expect(json_response["errors"]["name"]).to eq(["can't be blank"]) end + + it "can delete a product" do + expect(product.deleted_at).to be_nil + api_delete :destroy, id: product.to_param + + expect(response).to have_http_status(:no_content) + expect(product.reload.deleted_at).not_to be_nil + end + end + + describe '#clone' do + context 'as a normal user' do + before do + allow(current_api_user) + .to receive(:admin?).and_return(false) + end + + it 'denies access' do + spree_post :clone, product_id: product.id, format: :json + + assert_unauthorized! + end + end + + context 'as an enterprise user' do + let(:current_api_user) { supplier_enterprise_user(supplier) } + let!(:variant) { create(:variant, product_id: product.id) } + + it 'responds with a successful response' do + spree_post :clone, product_id: product.id, format: :json + + expect(response).to have_http_status(:created) + end + + it 'clones the product' do + spree_post :clone, product_id: product.id, format: :json + + expect(json_response['name']).to eq("COPY OF #{product.name}") + end + + it 'clones a product with image' do + spree_post :clone, product_id: product_with_image.id, format: :json + + expect(response).to have_http_status(:created) + expect(json_response['name']).to eq("COPY OF #{product_with_image.name}") + end + + # test cases related to bug #660: product duplication clones master variant + + # stock info - clone is set to zero + it '(does not) clone the stock info of the product' do + spree_post :clone, product_id: product.id, format: :json + expect(json_response.dig("variants", 0, "on_hand")).to eq(0) + end + + # variants: only the master variant of the product is cloned + it '(does not) clone variants from a product with several variants' do + spree_post :clone, product_id: product.id, format: :json + expect(Spree::Product.second.variants.count).not_to eq Spree::Product.first.variants.count + end + end + + context 'as an administrator' do + before do + allow(current_api_user) + .to receive(:admin?).and_return(true) + end + + it 'responds with a successful response' do + spree_post :clone, product_id: product.id, format: :json + + expect(response).to have_http_status(:created) + end + + it 'clones the product' do + spree_post :clone, product_id: product.id, format: :json + + expect(json_response['name']).to eq("COPY OF #{product.name}") + end + + it 'clones a product with image' do + spree_post :clone, product_id: product_with_image.id, format: :json + + expect(response).to have_http_status(:created) + expect(json_response['name']).to eq("COPY OF #{product_with_image.name}") + end + end + end + + describe '#bulk_products' do + context "as an enterprise user" do + let!(:taxon) { create(:taxon) } + let!(:product2) { create(:product, supplier_id: supplier.id, primary_taxon: taxon) } + let!(:product3) { create(:product, supplier_id: supplier2.id, primary_taxon: taxon) } + let!(:product4) { create(:product, supplier_id: supplier2.id) } + let(:current_api_user) { supplier_enterprise_user(supplier) } + + before { current_api_user.enterprise_roles.create(enterprise: supplier2) } + + it "returns a list of products" do + api_get :bulk_products, { page: 1, per_page: 15 }, format: :json + expect(returned_product_ids).to eq [product4.id, product3.id, product2.id, + other_product.id, product.id] + end + + it "returns pagination data" do + api_get :bulk_products, { page: 1, per_page: 15 }, format: :json + expect(json_response['pagination']).to eq "results" => 5, "pages" => 1, "page" => 1, + "per_page" => 15 + end + + it "uses defaults when page and per_page are not supplied" do + api_get :bulk_products, format: :json + expect(json_response['pagination']).to eq "results" => 5, "pages" => 1, "page" => 1, + "per_page" => 15 + end + + it "returns paginated products by page" do + api_get :bulk_products, { page: 1, per_page: 2 }, format: :json + expect(returned_product_ids).to eq [product4.id, product3.id] + + api_get :bulk_products, { page: 2, per_page: 2 }, format: :json + expect(returned_product_ids).to eq [product2.id, other_product.id] + end + + it "filters results by supplier" do + api_get :bulk_products, + { page: 1, per_page: 15, q: { variants_supplier_id_eq: supplier.id } }, + format: :json + expect(returned_product_ids).to eq [product2.id, other_product.id, product.id] + end + + it "filters results by product category" do + api_get :bulk_products, + { page: 1, per_page: 15, q: { variants_primary_taxon_id_eq: taxon.id } }, + format: :json + expect(returned_product_ids).to eq [product3.id, product2.id] + end + + it "filters results by import_date" do + product.variants.first.update_attribute :import_date, 1.day.ago + product2.variants.first.update_attribute :import_date, 2.days.ago + product3.variants.first.update_attribute :import_date, 1.day.ago + + api_get :bulk_products, { page: 1, per_page: 15, import_date: 1.day.ago.to_date.to_s }, + format: :json + expect(returned_product_ids).to eq [product3.id, product.id] + end + end end private