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:
Nuwan 2024-07-16 17:26:48 +05:30
parent 48335a9d9c
commit 86d77df2c9
20 changed files with 341 additions and 104 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')
});
})
});

View File

@ -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');
});

View File

@ -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,

View File

@ -16,7 +16,7 @@ const NavbarVerticalMenu = ({ routes, location }) => {
navs.push({ ...route, isOpened: true, index })
});
setNavRoutes(navs)
}, []);
}, [routes]);
const toggleOpened = (e, route) => {
e.preventDefault();

View File

@ -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);

View File

@ -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));
});
}

View File

@ -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>
);
};

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
object @app_features
extends "api_app_features/show"

View File

@ -0,0 +1,3 @@
object @app_feature
attributes :id, :feature_type, :handle, :is_enabled, :env

View File

@ -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

View File

@ -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

View File

@ -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