WIP: More work on panel content, styling and data submission logic

This commit is contained in:
Rob Harrington
2015-06-10 09:40:28 +08:00
parent a586a52c23
commit 6b35e993bd
16 changed files with 322 additions and 101 deletions

View File

@@ -0,0 +1,22 @@
angular.module("admin.enterprises").controller 'indexPanelCtrl', ($scope, Enterprises) ->
$scope.enterprise = $scope.object
$scope.saving = false
$scope.saved = ->
Enterprises.saved($scope.enterprise)
$scope.save = ->
unless $scope.saved()
$scope.saving = true
Enterprises.save($scope.enterprise).then (data) ->
$scope.saving = false
, (response) ->
$scope.saving = false
if response.status == 422 && response.data.errors?
message = 'Please resolve the following errors:\n'
for attr, msg of response.data.errors
message += "#{attr} #{msg}\n"
alert(message)
$scope.resetAttribute = (attribute) ->
Enterprises.resetAttribute($scope.enterprise, attribute)

View File

@@ -1,9 +1,14 @@
angular.module("admin.enterprises").controller 'indexProducerPanelCtrl', ($scope) ->
$scope.enterprise = angular.copy($scope.object())
$scope.persisted = angular.copy($scope.object())
$scope.attributes = ['is_primary_producer']
angular.module("admin.enterprises").controller 'indexProducerPanelCtrl', ($scope, $controller) ->
angular.extend this, $controller('indexPanelCtrl', {$scope: $scope})
$scope.saved = ->
for attribute in $scope.attributes
return false if $scope.enterprise[attribute] != $scope.persisted[attribute]
true
$scope.changeToProducer = ->
$scope.resetAttribute('sells')
$scope.resetAttribute('producer_profile_only')
$scope.enterprise.is_primary_producer = true
$scope.changeToNonProducer = ->
if $scope.enterprise.sells == 'own'
$scope.enterprise.sells = 'any'
if $scope.enterprise.producer_profile_only = true
$scope.enterprise.producer_profile_only = false
$scope.enterprise.is_primary_producer = false

View File

@@ -1,2 +1,2 @@
angular.module("admin.enterprises").controller 'indexShopPanelCtrl', ($scope) ->
$scope.enterprise = angular.copy($scope.object())
angular.module("admin.enterprises").controller 'indexShopPanelCtrl', ($scope, $controller) ->
angular.extend this, $controller('indexPanelCtrl', {$scope: $scope})

View File

@@ -1,6 +1,8 @@
angular.module("admin.enterprises").factory 'EnterpriseResource', ($resource) ->
$resource('/admin/enterprises.json', {}, {
$resource('/admin/enterprises/:id.json', {}, {
'index':
method: 'GET'
isArray: true
'update':
method: 'PUT'
})

View File

@@ -1,16 +1,39 @@
angular.module("admin.enterprises").factory 'Enterprises', (EnterpriseResource) ->
angular.module("admin.enterprises").factory 'Enterprises', ($q, EnterpriseResource) ->
new class Enterprises
enterprises: []
enterprises_by_id: {}
pristine_by_id: {}
loaded: false
index: (params={}, callback=null) ->
EnterpriseResource.index params, (data) =>
for enterprise in data
@enterprises.push enterprise
@enterprises_by_id[enterprise.id] = enterprise
@pristine_by_id[enterprise.id] = angular.copy(enterprise)
@loaded = true
(callback || angular.noop)(@enterprises)
@enterprises
save: (enterprise) ->
deferred = $q.defer()
enterprise.$update({id: enterprise.permalink})
.then( (data) =>
@pristine_by_id[enterprise.id] = angular.copy(enterprise)
deferred.resolve(data)
).catch (response) ->
deferred.reject(response)
deferred.promise
saved: (enterprise) ->
@diff(enterprise).length == 0
diff: (enterprise) ->
changed = []
for attr, value of enterprise when not angular.equals(value, @pristine_by_id[enterprise.id][attr])
changed.push attr unless attr is "$$hashKey"
changed
resetAttribute: (enterprise, attribute) ->
enterprise[attribute] = @pristine_by_id[enterprise.id][attribute]

View File

@@ -2,7 +2,7 @@ angular.module("admin.indexUtils").directive "panelRow", (Panels, Columns) ->
restrict: "C"
templateUrl: "admin/panel.html"
scope:
object: "&"
object: "="
panels: "="
link: (scope, element, attrs) ->
scope.template = ""
@@ -34,4 +34,4 @@ angular.module("admin.indexUtils").directive "panelRow", (Panels, Columns) ->
element.hide 0, ->
scope.setSelected null
Panels.register(scope.object().id, scope)
Panels.register(scope.object.id, scope)

View File

@@ -10,32 +10,35 @@
%h3 Producer
%p Producers make yummy things to eat &/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it.
%p Being a producer does not limit an enterprise, producers can still opt to aggregate food from other enterprises and sell it through a shop on the Open Food Network.
%p Producers can also perform other functions, such as aggregating food from other enterprises and selling it through a shop on the Open Food Network.
.info{ ng: { show: "enterprise.is_primary_producer==false" } }
%h3 Non-Producer
%p Non-producers do not produce any food themselves, meaning that they cannot create their own products for sale through the Open Food Network.
%p Instead, non-producers specialise in filling the other vital roles between producer and end eater, whether it be aggregating, grading, packing, selling or delivering food.
%p Instead, non-producers specialise in linking producers to the end eater, whether it be by aggregating, grading, packing, selling or delivering food.
.omega.eight.columns
%a.button.selector{ ng: { click: 'enterprise.is_primary_producer=true', class: "{selected: enterprise.is_primary_producer==true}" } }
%a.button.selector{ ng: { click: 'changeToProducer()', class: "{selected: enterprise.is_primary_producer==true}" } }
.top
%h3 PRODUCER
%p Primary producers of food
.bottom eg. GROWERS, BAKERS, BREWERS, MAKERS
%a.button.selector{ ng: { click: 'enterprise.is_primary_producer=false', class: "{selected: enterprise.is_primary_producer==false}" } }
%a.button.selector{ ng: { click: 'changeToNonProducer()', class: "{selected: enterprise.is_primary_producer==false}" } }
.top
%h3 Non-Producer
%p All other food enterprises
.bottom eg. Grocery stores, Food co-ops, Buying groups
%a.button.update.fullwidth{ ng: { class: "{disabled: saved()}" } }
%span{ ng: {show: "saved()" } }
%a.button.update.fullwidth{ ng: { class: "{disabled: saved() && !saving, saving: saving}", click: "save()" } }
%span{ ng: {hide: "saved() || saving" } }
SAVE
%i.icon-save
%span{ ng: {show: "saved() && !saving" } }
SAVED
%i.icon-ok-sign
%span{ ng: {hide: "saved()" } }
SAVE
%i.icon-upload-alt
%span{ ng: {show: "saving" } }
SAVING
%i.icon-refresh

View File

@@ -4,65 +4,96 @@
%input{ hidden: "true", name: "sells", ng: { required: true, pattern: "/^(none|own|any)$/", value: "enterprise.sells"} }
%input{ hidden: "true", name: "producer_profile_only", ng: { required: "enterprise.is_primary_producer=='none'", disabled: "enterprise.is_primary_producer!='none'", value: "enterprise.producer_profile_only"} }
-# %table
-# %col{ width: '25%' }
-# %col{ width: '25%' }
-# %col{ width: '50%' }
-# %thead
-# %th PRODUCER?
-# %th SHOP?
-# %th COST AND DESCRIPTION
-# %tr
-# %td.selector{ rowspan: 3, ng: { click: 'enterprise.is_primary_producer=true', class: "{selected: enterprise.is_primary_producer==true}" } }
-# %h5
-# PRODUCER
-# %td.selector{ rowspan: 2, ng: { click: "enterprise.sells='none'", class: "{selected: enterprise.sells=='none'}" } }
-# %h5
-# NO SHOP
-# %td.description{ rowspan: 6 }
-# %p
-# %strong MONTHLY COST:
-# {{ "FREE" }}
-#
-# %p
-# %strong DESCRIPTION:
-# %br
-# %strong {{ enterprise.name }}
-# is a
-# %strong {{ enterprise.is_primary_producer ? "producer" : "non-producer" }}
-# with a shop that sells
-# =succeed "." do
-# %strong {{ enterprise.sells }}
-# %a.update.fullwidth.button
-# UPDATE NOW
-# %tr
-# %tr
-# %td.selector{ rowspan: 2, ng: { click: "enterprise.sells='own'", class: "{selected: enterprise.sells=='own'}" } }
-# %h5
-# PRODUCER SHOP
-# %tr
-# %td.selector{ rowspan: 3, ng: { click: 'enterprise.is_primary_producer=false', class: "{selected: enterprise.is_primary_producer==false}" } }
-# %h5
-# NON-PRODUCER
-# %tr
-# %td.selector{ rowspan: 2, ng: { click: "enterprise.sells='any'", class: "{selected: enterprise.sells=='any'}" } }
-# %h5
-# FULL SHOP
-# %tr
.row
.alpha.eight.columns
%a.button.selector{ ng: { click: "enterprise.sells='none' && enterprise.producer_profile_only=true", class: "{selected: enterprise.sells=='none' && enterprise.producer_profile_only==true}" } }
-# Non-Producer Info
.info{ ng: { show: "!enterprise.is_primary_producer && enterprise.sells=='none'" } }
%h3 Hub Profile
%p
%strong COST: ALWAYS FREE
%p A Hub Profile gives you the ability to list your enterprise on the Open Food Network. Your enterprise will be visible on the map, and will be searchable in listings.
%p Profile enterprises cannot create products, and so are unable to trade with other enterprises through the Open Food Network.
.info{ ng: { show: "!enterprise.is_primary_producer && enterprise.sells=='any'" } }
%h3 Hub Shop
%p
%strong COST: %2 OF SALES, CAPPED AT $50 PER MONTH
%p A Full Shop enables an enterprise to aggregate produce and to sell it through a shop on the Open Food Network.
%p Hubs can take many forms, whether they be a food co-op, a buying group, a veggie-box program, or a local grocery store.
%p The Open Food Network aims to support as many hub models as possible, so no matter your situation, we want to provide the tools you need to run your organisation or local food business.
-# Producer Info
.info{ ng: { show: "enterprise.is_primary_producer && enterprise.sells=='none' && enterprise.producer_profile_only==true" } }
%h3 Producer Profile
%p
%strong COST: ALWAYS FREE
%p A Producer Profile gives you the ability to list your enterprise on the Open Food Network. Your enterprise will be visible on the map, and will be searchable in listings.
%p Profile enterprises cannot create products, and so are unable to trade with other enterprises through the Open Food Network.
.info{ ng: { show: "enterprise.is_primary_producer && enterprise.sells=='none' && enterprise.producer_profile_only==false" } }
%h3 No Shop
%p
%strong COST: ALWAYS FREE
%p If you prefer to focus on producing food, and want to leave the work of selling it to someone else, you won't require a shop on the Open Food Network.
%p Producers without a shop can still market their produce through the Open Food Network by connecting and trading with existing shops.
.info{ ng: { show: "enterprise.is_primary_producer && enterprise.sells=='own' && enterprise.producer_profile_only==false" } }
%h3 Producer Shop
%p
%strong COST: %2 OF SALES, CAPPED AT $50 PER MONTH
%p A Producer Shop allows producers to offer their products for sale to customers through their very own Open Food Network shop.
%p Producer Shops may sell produce that has been grown by the producer in question, but do not allow for aggregation of produce from elsewhere.
.info{ ng: { show: "enterprise.is_primary_producer && enterprise.sells=='any' && enterprise.producer_profile_only==false" } }
%h3 Producer Hub
%p
%strong COST: %2 OF SALES, CAPPED AT $50 PER MONTH
%p Producer Hubs can take many forms, whether they be a CSA, a veggie-box program, or a food co-op with a rooftop garden.
%p The Open Food Network aims to support as many hub models as possible, so no matter your situation, we want to provide the tools you need to run your organisation or local food business.
.omega.eight.columns
%a.button.selector{ ng: { if: "!enterprise.is_primary_producer", click: "enterprise.sells='none'; enterprise.producer_profile_only=false;", class: "{selected: enterprise.sells=='none'}" } }
.top
%h3 Profile Only
%p Sell through other shops
.bottom ALWAYS FREE
%a.button.selector{ ng: { if: "!enterprise.is_primary_producer", click: "enterprise.sells='any'; enterprise.producer_profile_only=false;", class: "{selected: enterprise.sells=='any'}" } }
.top
%h3 Hub Shop
%p Sell through other shops
.bottom ALWAYS FREE
%a.button.selector{ ng: { if: "enterprise.is_primary_producer", click: "enterprise.sells='none'; enterprise.producer_profile_only=true;", class: "{selected: enterprise.sells=='none' && enterprise.producer_profile_only==true}" } }
.top
%h3 Profile Only
%p Connect through OFN
.bottom ALWAYS FREE
%a.button.selector{ ng: { click: "enterprise.sells='none' && enterprise.producer_profile_only=false", class: "{selected: enterprise.sells=='none' && enterprise.producer_profile_only==false}" } }
%a.button.selector{ ng: { if: "enterprise.is_primary_producer", click: "enterprise.sells='none'; enterprise.producer_profile_only=false;", class: "{selected: enterprise.sells=='none' && enterprise.producer_profile_only==false}" } }
.top
%h3 No Shop
%p Sell through other shops
.bottom ALWAYS FREE
%a.button.selector{ ng: { click: "enterprise.sells='own' && enterprise.producer_profile_only=false", class: "{selected: enterprise.sells=='own'}" } }
%a.button.selector{ ng: { if: "enterprise.is_primary_producer", click: "enterprise.sells='own';enterprise.producer_profile_only=false;", class: "{selected: enterprise.sells=='own'}" } }
.top
%h3 Producer Shop
%p Sell your own produce
@@ -70,31 +101,22 @@
\%2 OF SALES
%br
CAPPED AT $50 PER MONTH
%a.button.selector{ ng: { click: "enterprise.sells='any' && enterprise.producer_profile_only=false", class: "{selected: enterprise.sells=='any'}" } }
%a.button.selector{ ng: { if: "enterprise.is_primary_producer", click: "enterprise.sells='any';enterprise.producer_profile_only=false;", class: "{selected: enterprise.sells=='any'}" } }
.top
%h3 Producer Hub
%h3 Hub Shop
%p Aggregate and sell produce
.bottom
\%2 OF SALES
%br
CAPPED AT $50 PER MONTH
.omega.eight.columns
%a.button.update.fullwidth{ ng: { class: "{disabled: saved()}" } }
%span{ ng: {show: "saved()" } }
%a.button.update.fullwidth{ ng: { class: "{disabled: saved() && !saving, saving: saving}", click: "save()" } }
%span{ ng: {hide: "saved() || saving" } }
SAVE
%i.icon-save
%span{ ng: {show: "saved() && !saving" } }
SAVED
%i.icon-ok-sign
%span{ ng: {hide: "saved()" } }
SAVE
%i.icon-upload-alt
.info{ ng: { show: "enterprise.is_primary_producer==true && enterprise.sells='any'" } }
%h3 Producer Profile
%p Producers make yummy things to eat &/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it.
%p Being a producer does not limit an enterprise at all, producers can aggreagate food from other enterprises and sell it through shops on the Open Food Network.
.info{ ng: { show: "enterprise.is_primary_producer==false" } }
%h3 Non-Producer
%p
This enterprise does not produce any food itself, which means that it is probably involved in aggregating, selling and/or delivering food to the end eater.
%span{ ng: {show: "saving" } }
SAVING
%i.icon-refresh

View File

@@ -37,6 +37,13 @@
&.disabled {
background-color: #C1C1C1;
}
&.saving {
background-color: #FF9848;
i.icon-refresh {
-webkit-animation: spin 2s infinite linear;
animation: spin 2s infinite linear;
}
}
span{
i{
font-size: 1.5rem;

View File

@@ -8,9 +8,10 @@ tr.panel-row {
}
td {
border-color: #000000;
padding: 0;
.panel {
border: 3px solid black;
border: 2px solid #000000;
.row{
margin: 0px -4px;

View File

@@ -32,6 +32,24 @@ module Admin
end
end
def update
invoke_callbacks(:update, :before)
if @object.update_attributes(params[object_name])
invoke_callbacks(:update, :after)
flash[:success] = flash_message_for(@object, :successfully_updated)
respond_with(@object) do |format|
format.html { redirect_to location_after_save }
format.js { render :layout => false }
format.json { render json: @object, serializer: Api::Admin::BasicEnterpriseSerializer }
end
else
invoke_callbacks(:update, :fails)
respond_with(@object) do |format|
format.json { render json: { errors: @object.errors.messages }, status: :unprocessable_entity }
end
end
end
def set_sells
enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id])
attributes = { sells: params[:sells] }

View File

@@ -1,4 +1,4 @@
class Api::Admin::BasicEnterpriseSerializer < ActiveModel::Serializer
attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids
attributes :producer_profile_only
attributes :producer_profile_only, :permalink
end

View File

@@ -61,12 +61,12 @@
%td.producer{ ng: { show: 'columns.producer.visible' } }
%panel-toggle{ name: "producer", object: "enterprise" }
%a.button.fullwidth
%span{ bo: { bind: "enterprise.is_primary_producer" } }
%span{ ng: { bind: "enterprise.is_primary_producer" } }
%i.icon-arrow-down
%td.shop{ ng: { show: 'columns.shop.visible' } }
%panel-toggle{ name: "shop", object: "enterprise" }
%a.button.fullwidth
%span{ bo: { bind: "enterprise.sells" } }
%span{ ng: { bind: "enterprise.sells" } }
%i.icon-arrow-down
%td.status{ ng: { show: 'columns.status.visible' } }
%panel-toggle{ name: "status", object: "enterprise" }

View File

@@ -4,14 +4,12 @@ describe "EnterprisesCtrl", ->
Enterprises = null
beforeEach ->
shops = "list of shops"
module('admin.enterprises')
inject ($controller, $rootScope, _Enterprises_) ->
scope = $rootScope
Enterprises = _Enterprises_
spyOn(Enterprises, "index").andReturn "list of enterprises"
ctrl = $controller 'enterprisesCtrl', {$scope: scope, Enterprises: Enterprises, shops: shops}
ctrl = $controller 'enterprisesCtrl', {$scope: scope, Enterprises: Enterprises}
describe "setting the shop on scope", ->
it "calls Enterprises#index with the correct params", ->

View File

@@ -0,0 +1,46 @@
describe "indexPanelCtrl", ->
ctrl = null
scope = null
Enterprises = null
beforeEach ->
module('admin.enterprises')
inject ($controller, $rootScope, _Enterprises_) ->
scope = $rootScope.$new()
$rootScope.object = { some: "object" }
Enterprises = _Enterprises_
ctrl = $controller 'indexPanelCtrl', {$scope: scope, Enterprises: Enterprises}
describe "initialisation", ->
it "pulls object from the parent scope and points the 'enterprise' on the current scope to it", inject ($rootScope) ->
expect(scope.enterprise).toBe $rootScope.object
describe "saving changes on an enterprise", ->
describe "when changes have been made", ->
deferred = null
beforeEach inject ($q) ->
spyOn(scope, "saved").andReturn false
deferred = $q.defer()
spyOn(Enterprises, "save").andReturn(deferred.promise)
scope.save()
it "sets scope.saving to true", ->
expect(scope.saving).toBe true
describe "when the save is successful", ->
beforeEach inject ($rootScope) ->
deferred.resolve()
$rootScope.$digest()
it "sets scope.saving to false", ->
expect(scope.saving).toBe false
describe "when the save is unsuccessful", ->
beforeEach inject ($rootScope) ->
deferred.reject({ status: 404 })
$rootScope.$digest()
it "sets scope.saving to false", ->
expect(scope.saving).toBe false

View File

@@ -8,12 +8,13 @@ describe "Enterprises service", ->
Enterprises = _Enterprises_
EnterpriseResource = _EnterpriseResource_
$httpBackend = _$httpBackend_
$httpBackend.expectGET('/admin/enterprises.json').respond 200, [{ id: 5, name: 'Enterprise 1'}]
describe "#index", ->
result = null
beforeEach ->
$httpBackend.expectGET('/admin/enterprises.json').respond 200, [{ id: 5, name: 'Enterprise 1'}]
expect(Enterprises.loaded).toBe false
result = Enterprises.index()
$httpBackend.flush()
@@ -29,3 +30,76 @@ describe "Enterprises service", ->
it "sets @loaded to true", ->
expect(Enterprises.loaded).toBe true
describe "#save", ->
result = null
describe "success", ->
enterprise = null
resolved = false
beforeEach ->
enterprise = new EnterpriseResource( { id: 15, permalink: 'enterprise1', name: 'Enterprise 1' } )
$httpBackend.expectPUT('/admin/enterprises/enterprise1.json').respond 200, { id: 15, name: 'Enterprise 1'}
Enterprises.save(enterprise).then( -> resolved = true)
$httpBackend.flush()
it "updates the pristine copy of the enterprise", ->
# Resource results have extra properties ($then, $promise) that cause them to not
# be exactly equal to the response object provided to the expectPUT clause above.
expect(Enterprises.pristine_by_id[15]).toEqual enterprise
it "resolves the promise", ->
expect(resolved).toBe(true);
describe "failure", ->
enterprise = null
rejected = false
beforeEach ->
enterprise = new EnterpriseResource( { id: 15, permalink: 'permalink', name: 'Enterprise 1' } )
$httpBackend.expectPUT('/admin/enterprises/permalink.json').respond 422, { error: 'obj' }
Enterprises.save(enterprise).catch( -> rejected = true)
$httpBackend.flush()
it "does not update the pristine copy of the enterprise", ->
expect(Enterprises.pristine_by_id[15]).toBeUndefined()
it "rejects the promise", ->
expect(rejected).toBe(true);
describe "#saved", ->
describe "when attributes of the object have been altered", ->
beforeEach ->
spyOn(Enterprises, "diff").andReturn ["attr1", "attr2"]
it "returns false", ->
expect(Enterprises.saved({})).toBe false
describe "when attributes of the object have not been altered", ->
beforeEach ->
spyOn(Enterprises, "diff").andReturn []
it "returns false", ->
expect(Enterprises.saved({})).toBe true
describe "diff", ->
beforeEach ->
Enterprises.pristine_by_id = { 23: { id: 23, name: "ent1", is_primary_producer: true } }
it "returns a list of properties that have been altered", ->
expect(Enterprises.diff({ id: 23, name: "enterprise123", is_primary_producer: true })).toEqual ["name"]
describe "resetAttribute", ->
enterprise = { id: 23, name: "ent1", is_primary_producer: true }
beforeEach ->
Enterprises.pristine_by_id = { 23: { id: 23, name: "enterprise1", is_primary_producer: true } }
it "resets the specified value according to the pristine record", ->
Enterprises.resetAttribute(enterprise, "name")
expect(enterprise.name).toEqual "enterprise1"