app features on/off
add ability to control the visibility of beta site menu items. the visibility state is stored in the back end api and the front end menu items are been shown accordingly.
This commit is contained in:
parent
48335a9d9c
commit
86d77df2c9
|
|
@ -5,3 +5,4 @@ REACT_APP_CLIENT_BASE_URL=http://www.jamkazam.local:3000
|
|||
REACT_APP_API_BASE_URL=http://www.jamkazam.local:3000/api
|
||||
REACT_APP_BITBUCKET_BUILD_NUMBER=dev
|
||||
REACT_APP_BITBUCKET_COMMIT=dev
|
||||
REACT_APP_ENV=development
|
||||
|
|
@ -5,3 +5,4 @@ REACT_APP_CLIENT_BASE_URL=http://www.jamkazam.local:3000
|
|||
REACT_APP_API_BASE_URL=http://www.jamkazam.local:3000/api
|
||||
REACT_APP_BITBUCKET_BUILD_NUMBER=dev
|
||||
REACT_APP_BITBUCKET_COMMIT=dev
|
||||
REACT_APP_ENV=development
|
||||
|
|
@ -3,3 +3,4 @@ PORT=4000
|
|||
REACT_APP_ORIGIN=jamkazam.com
|
||||
REACT_APP_CLIENT_BASE_URL=https://www.jamkazam.com
|
||||
REACT_APP_API_BASE_URL=https://www.jamkazam.com/api
|
||||
REACT_APP_ENV=production
|
||||
|
|
@ -3,3 +3,4 @@ PORT=4000
|
|||
REACT_APP_ORIGIN=staging.jamkazam.com
|
||||
REACT_APP_CLIENT_BASE_URL=https://staging.jamkazam.com
|
||||
REACT_APP_API_BASE_URL=https://staging.jamkazam.com/api
|
||||
REACT_APP_ENV=staging
|
||||
|
|
@ -1,120 +1,198 @@
|
|||
/// <reference types="cypress" />
|
||||
|
||||
describe("Top Navigation", () => {
|
||||
|
||||
describe('Top Navigation', () => {
|
||||
const showSubscribeToUpdates = () => {
|
||||
cy.contains('Keep JamKazam Improving').should('exist')
|
||||
cy.contains('Subscribe').should('exist')
|
||||
}
|
||||
cy.contains('Keep JamKazam Improving').should('exist');
|
||||
cy.contains('Subscribe').should('exist');
|
||||
};
|
||||
|
||||
const showProfileDropdown = () => {
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('exist')
|
||||
cy.contains("Peter Pan").should('exist')
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('exist');
|
||||
cy.contains('Peter Pan').should('exist');
|
||||
//cy.contains("My Profile").should('exist')
|
||||
cy.contains("Sign Out").should('exist')
|
||||
}
|
||||
cy.contains('Sign Out').should('exist');
|
||||
};
|
||||
|
||||
describe("when user has not logged in", () => {
|
||||
describe('when user has not logged in', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubUnauthenticate()
|
||||
cy.stubUnauthenticate();
|
||||
});
|
||||
|
||||
it('shows homepage', () => {
|
||||
cy.visit('/')
|
||||
cy.contains('Home').should('exist')
|
||||
showSubscribeToUpdates()
|
||||
})
|
||||
|
||||
it("not allowed to protected page", () => {
|
||||
cy.visit('/friends')
|
||||
cy.url().should('include', '/authentication/basic/login')
|
||||
cy.contains("Sign in")
|
||||
cy.get('button').should('have.text', 'Sign in')
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('not.exist')
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.contains('Home').should('exist');
|
||||
showSubscribeToUpdates();
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
describe("when user has logged in", () => {
|
||||
it('not allowed to protected page', () => {
|
||||
cy.visit('/friends');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.url().should('include', '/authentication/basic/login');
|
||||
cy.contains('Sign in');
|
||||
cy.get('button').should('have.text', 'Sign in');
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user has logged in', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate()
|
||||
cy.visit('/')
|
||||
cy.stubAuthenticate();
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
});
|
||||
|
||||
it("shows user dropdown", () => {
|
||||
showSubscribeToUpdates()
|
||||
showProfileDropdown()
|
||||
})
|
||||
it('shows user dropdown', () => {
|
||||
showSubscribeToUpdates();
|
||||
showProfileDropdown();
|
||||
});
|
||||
|
||||
it('sign out', () => {
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').contains('Peter Pan').click()
|
||||
cy.stubUnauthenticate()
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').contains('Sign Out').click()
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('not.exist')
|
||||
cy.contains("Home")
|
||||
})
|
||||
})
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]')
|
||||
.contains('Peter Pan')
|
||||
.click();
|
||||
cy.stubUnauthenticate();
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]')
|
||||
.contains('Sign Out')
|
||||
.click();
|
||||
cy.get('[data-testid=navbarTopProfileDropdown]').should('not.exist');
|
||||
cy.contains('Home');
|
||||
});
|
||||
});
|
||||
|
||||
describe('header notifications', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate()
|
||||
cy.intercept('GET', /\S+\/notifications/, { fixture: 'notifications'} )
|
||||
cy.stubAuthenticate();
|
||||
cy.intercept('GET', /\S+\/notifications/, { fixture: 'notifications' });
|
||||
cy.intercept('GET', /\S+\/profile\S+/, { fixture: 'person' });
|
||||
cy.visit('/')
|
||||
})
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
});
|
||||
|
||||
it('shows notifications', () => {
|
||||
cy.get('[data-testid=notificationDropdown]').should('not.be.visible')
|
||||
cy.get('.notification-indicator').click()
|
||||
cy.get('[data-testid=notificationDropdown]').should('be.visible')
|
||||
cy.get('[data-testid=notificationDropdown] .list-group-item').should('have.length', 3)
|
||||
cy.get('[data-testid=notificationDropdown]').contains('View all').click() //view all notifications
|
||||
cy.url().should('include', '/notifications')
|
||||
})
|
||||
})
|
||||
cy.get('[data-testid=notificationDropdown]').should('not.be.visible');
|
||||
cy.get('.notification-indicator').click();
|
||||
cy.get('[data-testid=notificationDropdown]').should('be.visible');
|
||||
cy.get('[data-testid=notificationDropdown] .list-group-item').should('have.length', 3);
|
||||
cy.get('[data-testid=notificationDropdown]')
|
||||
.contains('View all')
|
||||
.click(); //view all notifications
|
||||
cy.url().should('include', '/notifications');
|
||||
});
|
||||
});
|
||||
|
||||
describe('locale switch', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate()
|
||||
cy.visit('/')
|
||||
})
|
||||
cy.stubAuthenticate();
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
});
|
||||
|
||||
it("translate", () => {
|
||||
cy.get('.card-header').contains('Home')
|
||||
cy.get('[data-testid=langSwitch]').contains('ES').click()
|
||||
cy.get('.card-header').contains('Página de inicio')
|
||||
cy.get('.card-header').should('not.contain', 'Home')
|
||||
cy.get('[data-testid=langSwitch]').contains('EN').click()
|
||||
cy.get('.card-header').contains('Home')
|
||||
cy.get('.card-header').should('not.contain', 'Página de inicio')
|
||||
})
|
||||
})
|
||||
|
||||
describe('left side navigation', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport('macbook-13');
|
||||
cy.stubAuthenticate()
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('shows all main and sub menu items opened by default', () => {
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Sessions')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Create Session')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Browse Current Sessions')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('View Session History')
|
||||
})
|
||||
|
||||
it.only('toggles only one menu on click', () => {
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Sessions').click()
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Create Session').should('not.visible')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Browse Current Sessions').should('not.visible')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('View Session History').should('not.visible')
|
||||
//the Friends menu is not toggled
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('Friends')
|
||||
cy.get('[data-testid=verticalNavigation]' ).contains('My Friends')
|
||||
})
|
||||
})
|
||||
it('translate', () => {
|
||||
cy.get('.card-header').contains('Home');
|
||||
cy.get('[data-testid=langSwitch]')
|
||||
.contains('ES')
|
||||
.click();
|
||||
cy.get('.card-header').contains('Página de inicio');
|
||||
cy.get('.card-header').should('not.contain', 'Home');
|
||||
cy.get('[data-testid=langSwitch]')
|
||||
.contains('EN')
|
||||
.click();
|
||||
cy.get('.card-header').contains('Home');
|
||||
cy.get('.card-header').should('not.contain', 'Página de inicio');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Side Navigation', () => {
|
||||
describe('backend returns empty set of app features', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate();
|
||||
cy.viewport('macbook-13');
|
||||
});
|
||||
|
||||
it('shows all menu items', () => {
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Home');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Sessions');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Create Session');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Friends');
|
||||
});
|
||||
|
||||
it('toggles only one menu on click', () => {
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.get('[data-testid=verticalNavigation]')
|
||||
.contains('Sessions')
|
||||
.click();
|
||||
cy.get('[data-testid=verticalNavigation]')
|
||||
.contains('Create Session')
|
||||
.should('not.visible');
|
||||
cy.get('[data-testid=verticalNavigation]')
|
||||
.contains('Browse Sessions')
|
||||
.should('not.visible');
|
||||
cy.get('[data-testid=verticalNavigation]')
|
||||
.contains('Session History')
|
||||
.should('not.visible');
|
||||
//the Friends menu is not toggled
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Friends');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('My Friends');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('backend returns app features', () => {
|
||||
beforeEach(() => {
|
||||
cy.stubAuthenticate();
|
||||
cy.viewport('macbook-13');
|
||||
cy.intercept('GET', /\S+\/app_features/, {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{
|
||||
handle: '/sessions',
|
||||
is_enabled: true,
|
||||
feature_type: 'page',
|
||||
env: 'development'
|
||||
},
|
||||
{
|
||||
handle: '/sessions/new',
|
||||
is_enabled: false,
|
||||
feature_type: 'page',
|
||||
env: 'development'
|
||||
},
|
||||
{
|
||||
handle: '/sessions/history',
|
||||
is_enabled: true,
|
||||
feature_type: 'page',
|
||||
env: 'production'
|
||||
},
|
||||
{
|
||||
handle: '/friends',
|
||||
is_enabled: true,
|
||||
feature_type: 'page',
|
||||
env: 'development'
|
||||
},
|
||||
{
|
||||
handle: '/friends/my',
|
||||
is_enabled: true,
|
||||
feature_type: 'page',
|
||||
env: 'development'
|
||||
}
|
||||
]
|
||||
}).as('getAppFeatures');
|
||||
});
|
||||
|
||||
it('shows only enabled menu items', () => {
|
||||
cy.visit('/');
|
||||
cy.wait('@getAppFeatures');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Friends');
|
||||
cy.get('[data-testid=verticalNavigation]').contains('Sessions');
|
||||
cy.get('[data-testid=verticalNavigation]').should('not.include.text', 'Create Session')
|
||||
cy.get('[data-testid=verticalNavigation]').should('not.include.text', 'Session History')
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,4 +20,14 @@ import './commands'
|
|||
// require('./commands')
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
// Intercept the GET /app_features request and return an empty array
|
||||
// to simulate the backend returning an empty set of app features
|
||||
cy.intercept('GET', /\S+\/app_features/, {
|
||||
statusCode: 200,
|
||||
body: [],
|
||||
}).as('getAppFeatures');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,32 @@
|
|||
import classNames from 'classnames';
|
||||
import is from 'is_js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Collapse, Nav, Navbar } from 'reactstrap';
|
||||
import bgNavbarImg from '../../assets/img/generic/bg-navbar.png';
|
||||
import { navbarBreakPoint, topNavbarBreakpoint } from '../../config';
|
||||
import AppContext from '../../context/Context';
|
||||
import routes from '../../routes';
|
||||
//import routes from '../../routes';
|
||||
import Flex from '../common/Flex';
|
||||
import Logo from './Logo';
|
||||
import NavbarTopDropDownMenus from './NavbarTopDropDownMenus';
|
||||
import NavbarVerticalMenu from './NavbarVerticalMenu';
|
||||
//import ToggleButton from './ToggleButton';
|
||||
|
||||
import { useAppRoutes } from '../../context/AppRoutesContext';
|
||||
|
||||
|
||||
const JKNavbarVertical = ({ navbarStyle }) => {
|
||||
|
||||
const { appRoutes } = useAppRoutes();
|
||||
const [routes, setRoutes] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if(appRoutes.length > 0){
|
||||
setRoutes(appRoutes);
|
||||
}
|
||||
}, [appRoutes]);
|
||||
|
||||
const navBarRef = useRef(null);
|
||||
const {
|
||||
showBurgerMenu,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const NavbarVerticalMenu = ({ routes, location }) => {
|
|||
navs.push({ ...route, isOpened: true, index })
|
||||
});
|
||||
setNavRoutes(navs)
|
||||
}, []);
|
||||
}, [routes]);
|
||||
|
||||
const toggleOpened = (e, route) => {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import routes from '../routes';
|
||||
import { getAppFeatures } from '../helpers/rest';
|
||||
|
||||
const AppRoutesContext = React.createContext(null);
|
||||
|
||||
export const AppRoutesProvider = ({ children }) => {
|
||||
const [appRoutes, setAppRoutes] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (appRoutes.length === 0) {
|
||||
const env = process.env.REACT_APP_ENV;
|
||||
getAppFeatures(env)
|
||||
.then(response => {
|
||||
if (response) {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(features => {
|
||||
if (features.length === 0) {
|
||||
setAppRoutes(routes);
|
||||
} else {
|
||||
const _routes = routes.filter(route => {
|
||||
if(route.children && route.children.length > 0) {
|
||||
route.children = route.children.filter(child => {
|
||||
return presentInNav(features, child);
|
||||
});
|
||||
}
|
||||
return presentInNav(features, route);
|
||||
});
|
||||
setAppRoutes(_routes);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const presentInNav = (features, route) => {
|
||||
return features.find(
|
||||
feature => route.to === feature.handle && feature.is_enabled && feature.feature_type === 'page' && feature.env === process.env.REACT_APP_ENV
|
||||
);
|
||||
};
|
||||
|
||||
return <AppRoutesContext.Provider value={{ appRoutes }}>{children}</AppRoutesContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAppRoutes = () => React.useContext(AppRoutesContext);
|
||||
|
|
@ -416,3 +416,12 @@ export const addJamtrackToShoppingCart = (options = {}) => {
|
|||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export const getAppFeatures = (env) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/app_features?env=${env}`)
|
||||
.then(response => resolve(response))
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ import { NativeAppProvider } from '../context/NativeAppContext';
|
|||
|
||||
import { JKLobbyChatProvider } from '../components/sessions/JKLobbyChatContext';
|
||||
|
||||
import { AppRoutesProvider } from '../context/AppRoutesContext';
|
||||
|
||||
const DashboardLayout = ({ location }) => {
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
|
|
@ -16,13 +18,15 @@ const DashboardLayout = ({ location }) => {
|
|||
|
||||
return (
|
||||
<UserAuth path={location.pathname}>
|
||||
<BrowserQueryProvider>
|
||||
<NativeAppProvider>
|
||||
<JKLobbyChatProvider>
|
||||
<DashboardMain />
|
||||
</JKLobbyChatProvider>
|
||||
</NativeAppProvider>
|
||||
</BrowserQueryProvider>
|
||||
<AppRoutesProvider>
|
||||
<BrowserQueryProvider>
|
||||
<NativeAppProvider>
|
||||
<JKLobbyChatProvider>
|
||||
<DashboardMain />
|
||||
</JKLobbyChatProvider>
|
||||
</NativeAppProvider>
|
||||
</BrowserQueryProvider>
|
||||
</AppRoutesProvider>
|
||||
</UserAuth>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
class CreateAppFeatures < ActiveRecord::Migration
|
||||
def self.up
|
||||
execute(<<-SQL
|
||||
CREATE TABLE public.app_features (
|
||||
id character varying(64) DEFAULT public.uuid_generate_v4() PRIMARY KEY NOT NULL,
|
||||
feature_type character varying(64) NOT NULL,
|
||||
handle character varying(1024) NOT NULL,
|
||||
is_enabled boolean DEFAULT false NOT NULL,
|
||||
env character varying(16) DEFAULT 'development' NOT NULL
|
||||
);
|
||||
SQL
|
||||
)
|
||||
end
|
||||
def self.down
|
||||
execute("DROP TABLE public.app_features")
|
||||
end
|
||||
end
|
||||
|
|
@ -345,6 +345,7 @@ require "jam_ruby/models/ad_campaign"
|
|||
require "jam_ruby/models/user_asset"
|
||||
require "jam_ruby/models/user_match_email_sending"
|
||||
require "jam_ruby/models/app_interaction"
|
||||
require "jam_ruby/models/app_feature"
|
||||
|
||||
|
||||
include Jampb
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
module JamRuby
|
||||
class AppFeature < ActiveRecord::Base
|
||||
self.primary_key = 'id'
|
||||
self.table_name = 'app_features'
|
||||
|
||||
attr_accessible :feature_type, :handle, :is_enabled, :env
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
class ApiAppFeaturesController < ApiController
|
||||
before_filter :api_signed_in_user
|
||||
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
env = params[:env] || 'development'
|
||||
@app_features = AppFeature.where(env: env)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
object @app_features
|
||||
extends "api_app_features/show"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
object @app_feature
|
||||
|
||||
attributes :id, :feature_type, :handle, :is_enabled, :env
|
||||
|
|
@ -859,5 +859,9 @@ Rails.application.routes.draw do
|
|||
match 'jamblasters/pairing/login' => 'api_jamblasters#login', :via => :post
|
||||
match 'jamblasters/pairing/store' => 'api_jamblasters#store_token', :via => :post
|
||||
match 'jamblasters/pairing/pair' => 'api_jamblasters#pair', :via => :post
|
||||
|
||||
|
||||
#app features
|
||||
match '/app_features' => 'api_app_features#index', :via => :get
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe ApiAppFeaturesController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:user) { FactoryGirl.create(:user) }
|
||||
|
||||
before(:each) do
|
||||
controller.current_user = user
|
||||
end
|
||||
|
||||
describe "index" do
|
||||
it "returns app features of env" do
|
||||
FactoryGirl.create(:app_feature, env: 'production')
|
||||
get :index, env: 'production'
|
||||
response.should be_success
|
||||
json = JSON.parse(response.body, :symbolize_names => true)
|
||||
json.should have(1).item
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1125,4 +1125,12 @@ FactoryGirl.define do
|
|||
asset_type "image"
|
||||
filename "image.jpg"
|
||||
end
|
||||
|
||||
factory :app_feature, class: "JamRuby::AppFeature" do
|
||||
feature_type "page"
|
||||
handle "/"
|
||||
env "development"
|
||||
is_enabled true
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue