Skip to content

Commit

Permalink
feat: allow to setup theme base on the user
Browse files Browse the repository at this point in the history
fixes #511
  • Loading branch information
wojtek-krysiak committed Aug 8, 2020
1 parent 9b126d3 commit baa28f5
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 84 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Expand Up @@ -50,6 +50,7 @@ module.exports = {
'func-names': 'off',
'prefer-arrow-callback': 'off',
'import/no-extraneous-dependencies': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
{
Expand Down
3 changes: 3 additions & 0 deletions example-app/src/admin.options.js
Expand Up @@ -32,6 +32,9 @@ const options = {
admin: true,
app: process.env.npm_package_version,
},
branding: currentUser => ({
companyName: currentUser ? currentUser.email : 'something',
}),
locale: {
language: 'en',
translations: {
Expand Down
75 changes: 56 additions & 19 deletions src/admin-bro-options.interface.ts
Expand Up @@ -5,6 +5,7 @@ import BaseDatabase from './backend/adapters/base-database'
import { PageContext } from './backend/actions/action.interface'
import { ResourceOptions } from './backend/decorators/resource-options.interface'
import { Locale } from './locale/config'
import { CurrentAdmin } from './current-admin.interface'

/**
* AdminBroOptions
Expand Down Expand Up @@ -116,25 +117,16 @@ export default interface AdminBroOptions {
/**
* Options which are related to the ht.
*/
branding?: BrandingOptions;
branding?: BrandingOptions | BrandingOptionsFunction;
/**
* Custom assets you want to pass to AdminBro
*/
assets?: {
/**
* List to urls of custom stylesheets. You can pass your font - icons here (as an example)
*/
styles?: Array<string>;
/**
* List of urls to custom scripts. If you use some particular js
* library - you can pass its url here.
*/
scripts?: Array<string>;
};
assets?: Assets | AssetsFunction;
/**
* Indicates is bundled by AdminBro files like:
* - components.bundle.js
* - global.bundle.js
* - design-system.bundle.js
* - app.bundle.js
* should be taken from the same server as other AdminBro routes (default)
* or should be taken from an external CDN.
Expand All @@ -158,6 +150,9 @@ export default interface AdminBroOptions {
* - copy
* './node_modules/admin-bro/lib/frontend/assets/scripts/global-bundle.production.js' to
* './public/global.bundle.js'
* * - copy
* './node_modules/admin-bro/node_modules/@admin-bro/design-system/bundle.production.js' to
* './public/design-system.bundle.js'
* - host entire public folder under some domain (if you use firebase - you can host them
* with firebase hosting)
* - point {@link AdminBro.assetsCDN} to this domain
Expand Down Expand Up @@ -244,6 +239,37 @@ export default interface AdminBroOptions {

/* cspell: enable */

/**
* @memberof AdminBroOptions
* @alias Assets
*
* Optional assets (stylesheets, and javascript libraries) which can be
* appended to the HEAD of the page.
*
* you can also pass {@link AssetsFunction} instead.
*/
export type Assets = {
/**
* List to urls of custom stylesheets. You can pass your font - icons here (as an example)
*/
styles?: Array<string>;
/**
* List of urls to custom scripts. If you use some particular js
* library - you can pass its url here.
*/
scripts?: Array<string>;
}

/**
* @alias AssetsFunction
* @name AssetsFunction
* @memberof AdminBroOptions
* @returns {Assets | Promise<Assets>}
* @description
* Function returning {@link Assets}
*/
export type AssetsFunction = (admin?: CurrentAdmin) => Assets | Promise<Assets>

/**
* Version Props
* @alias VersionProps
Expand Down Expand Up @@ -273,8 +299,6 @@ export type VersionProps = {
* colors (dark theme) run:
*
* ```javascript
* const theme = require('admin-bro-theme-dark')
*
* new AdminBro({
* branding: {
* companyName: 'John Doe Family Business',
Expand Down Expand Up @@ -310,6 +334,19 @@ export type BrandingOptions = {
favicon?: string;
}

/**
* Branding Options Function
*
* function returning BrandingOptions.
*
* @alias BrandingOptionsFunction
* @memberof AdminBroOptions
* @returns {BrandingOptions | Promise<BrandingOptions>}
*/
export type BrandingOptionsFunction = (
admin?: CurrentAdmin
) => BrandingOptions | Promise<BrandingOptions>

/**
* Object describing regular page in AdminBro
*
Expand Down Expand Up @@ -342,9 +379,14 @@ export type ResourceWithOptions = {
* Function taking {@link ResourceOptions} and merging it with all other options
*
* @alias FeatureType
* @type function
* @returns {ResourceOptions}
* @memberof AdminBroOptions
*/
export type FeatureType = (
/**
* Options returned by the feature added before
*/
options: ResourceOptions
) => ResourceOptions

Expand Down Expand Up @@ -373,10 +415,5 @@ export interface AdminBroOptionsWithDefault extends AdminBroOptions {
handler?: PageHandler;
component?: string;
};
branding: BrandingOptions & Required<Pick<BrandingOptions, 'softwareBrothers' | 'companyName'>>;
assets: {
styles: Array<string>;
scripts: Array<string>;
};
pages: Record<string, AdminPage>;
}
15 changes: 0 additions & 15 deletions src/admin-bro.ts
Expand Up @@ -3,7 +3,6 @@ import * as path from 'path'
import * as fs from 'fs'
import i18n, { i18n as I18n } from 'i18next'

import slash from 'slash'
import AdminBroOptions, { AdminBroOptionsWithDefault } from './admin-bro-options.interface'
import BaseResource from './backend/adapters/base-resource'
import BaseDatabase from './backend/adapters/base-database'
Expand Down Expand Up @@ -34,15 +33,7 @@ const defaults: AdminBroOptionsWithDefault = {
loginPath: DEFAULT_PATHS.loginPath,
databases: [],
resources: [],
branding: {
companyName: 'Company',
softwareBrothers: true,
},
dashboard: {},
assets: {
styles: [],
scripts: [],
},
pages: {},
}

Expand Down Expand Up @@ -171,12 +162,6 @@ class AdminBro {
*/
this.options = _.merge({}, defaults, options)

const defaultLogo = slash(path.join(this.options.rootPath, '/frontend/assets/logo-mini.svg'))
this.options.branding = this.options.branding || {}
this.options.branding.logo = this.options.branding.logo !== undefined
? this.options.branding.logo
: defaultLogo

this.initI18n()

const { databases, resources } = this.options
Expand Down
59 changes: 59 additions & 0 deletions src/backend/utils/options-parser.ts
@@ -0,0 +1,59 @@
import merge from 'lodash/merge'
import slash from 'slash'
import path from 'path'
import AdminBro from '../../admin-bro'
import { CurrentAdmin } from '../../current-admin.interface'
import { BrandingOptions, Assets } from '../../admin-bro-options.interface'


const defaultBranding = {
companyName: 'Company',
softwareBrothers: true,
}
const defaultAssets = {
styles: [],
scripts: [],
}

export const getAssets = async (
admin: AdminBro,
currentAdmin?: CurrentAdmin,
): Promise<Assets> => {
const { assets } = admin.options || {}
const computed = typeof assets === 'function'
? await assets(currentAdmin)
: assets

return merge({}, defaultAssets, computed)
}

export const getBranding = async (
admin: AdminBro,
currentAdmin?: CurrentAdmin,
): Promise<BrandingOptions> => {
const { branding } = admin.options
const defaultLogo = slash(path.join(
admin.options.rootPath,
'/frontend/assets/logo-mini.svg',
))

const computed = typeof branding === 'function'
? await branding(currentAdmin)
: branding
const merged = merge({}, defaultBranding, computed)

// checking for undefined because logo can also be `false` or `null`
merged.logo = merged.logo !== undefined ? merged.logo : defaultLogo

return merged
}

export const getFaviconFromBranding = (branding: BrandingOptions): string => {
if (branding.favicon) {
const { favicon } = branding
const type = favicon.match(/.*\.png$/) ? 'image/png' : 'image/x-icon'
return `<link rel="shortcut icon" type="${type}" href="${favicon}" />`
}

return ''
}
67 changes: 39 additions & 28 deletions src/frontend/layout-template.spec.ts
@@ -1,66 +1,77 @@
import { expect } from 'chai'
import layoutTemplate from './layout-template'
import AdminBro from '../admin-bro'
import AdminBroOptions from '../admin-bro-options.interface'
import { BrandingOptions } from '../admin-bro-options.interface'

describe('layoutTemplate', function () {
context('AdminBro with default options and not logged in user', function () {
beforeEach(function () {
this.adminBro = new AdminBro({})
context('AdminBro with branding options set as a function', function () {
const companyName = 'Dynamic Company'
let html: string

beforeEach(async function () {
const adminBro = new AdminBro({
branding: async () => ({ companyName }),
})

html = await layoutTemplate(adminBro, undefined, '/')
})

it('renders default company name', function () {
expect(
layoutTemplate(this.adminBro, undefined, '/'),
).to.contain(this.adminBro.options.branding.companyName)
expect(html).to.contain(companyName)
})

it('links to global bundle', function () {
expect(layoutTemplate(this.adminBro, undefined, '/')).to.contain('global.bundle.js')
it('links to global bundle', async function () {
expect(html).to.contain('global.bundle.js')
})
})

describe('AdminBro with branding options given', function () {
beforeEach(function () {
this.branding = {
softwareBrothers: false,
companyName: 'Other name',
favicon: '/someImage.png',
} as AdminBroOptions['branding']
const branding = {
softwareBrothers: false,
companyName: 'Other name',
favicon: '/someImage.png',
} as BrandingOptions
let html: string

this.adminBro = new AdminBro({ branding: this.branding })
this.renderedContent = layoutTemplate(this.adminBro, undefined, '/')
beforeEach(async function () {
const adminBro = new AdminBro({ branding })

html = await layoutTemplate(adminBro, undefined, '/')
})

it('renders company name', function () {
expect(this.renderedContent).to.contain(this.branding.companyName)
expect(html).to.contain(branding.companyName)
})

it('renders favicon', function () {
expect(this.renderedContent).to.contain(
`<link rel="shortcut icon" type="image/png" href="${this.branding.favicon}" />`,
expect(html).to.contain(
`<link rel="shortcut icon" type="image/png" href="${branding.favicon}" />`,
)
})
})

context('custom styles and scripts were defined in AdminBro options', function () {
beforeEach(function () {
this.scriptUrl = 'http://somescript.com'
this.styleUrl = 'http://somestyle.com'
this.adminBro = new AdminBro({
let html: string
const scriptUrl = 'http://somescript.com'
const styleUrl = 'http://somestyle.com'

beforeEach(async function () {
const adminBro = new AdminBro({
assets: {
styles: [this.styleUrl],
scripts: [this.scriptUrl],
styles: [styleUrl],
scripts: [scriptUrl],
},
})

html = await layoutTemplate(adminBro, undefined, '/')
})

it('adds styles to the head section', function () {
expect(layoutTemplate(this.adminBro, undefined, '/')).to.contain(this.styleUrl)
expect(html).to.contain(styleUrl)
})

it('adds scripts to the body', function () {
expect(layoutTemplate(this.adminBro, undefined, '/')).to.contain(this.scriptUrl)
expect(html).to.contain(scriptUrl)
})
})
})

0 comments on commit baa28f5

Please sign in to comment.