Skip to content

Commit

Permalink
Merge branch 'beta' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtek-krysiak authored Aug 30, 2020
2 parents 5df0138 + c99a6e8 commit 2c991d3
Show file tree
Hide file tree
Showing 22 changed files with 587 additions and 18 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = {
'quotes': ['error', 'single'],
'semi': ['error', 'never'],
'import/no-unresolved': 'off',
'func-names': 'off',
'no-underscore-dangle': 'off',
'guard-for-in': 'off',
'no-restricted-syntax': 'off',
Expand Down
Binary file added docs/ layout1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/layout2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/layout3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/layout4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/layout5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions example-app/src/companies/company.admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ const AdminBro = require('admin-bro')
const { Company } = require('./company.entity')
const passwordFeature = require('../features/password/password.feature')

const layout = currentAdmin => ([
['@MessageBox', {
message: `Welcome ${currentAdmin.email}`,
children: 'On this page yo can do whatever you like',
variant: 'info',
mb: 'xxl',
}],
[
'companyName',
'companySize',
'email',
'address',
],
])

/** @type {AdminBro.ResourceOptions} */
const options = {
listProperties: ['companyName', 'email', 'address', 'companySize', 'isAdmin', 'isBig'],
Expand Down Expand Up @@ -32,6 +47,7 @@ const options = {
}
},
},
show: { layout },
},
}

Expand Down
31 changes: 31 additions & 0 deletions src/backend/actions/action.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ViewHelpers from '../utils/view-helpers'
import BaseRecord from '../adapters/base-record'
import BaseResource from '../adapters/base-resource'
import ActionDecorator from '../decorators/action-decorator'
import { LayoutElement } from '../utils/layout-element-parser'
import RecordJSON from '../decorators/record-json.interface'
import { NoticeMessage } from '../../frontend/store/with-notice'
import { TranslateFunctions } from '../../utils/translate-functions.factory'
Expand Down Expand Up @@ -574,4 +575,34 @@ export default interface Action <T extends ActionResponse> {
* ```
*/
containerWidth?: string | number | Array<string | number>;
/**
* Definition for the layout. Works with the edit and show actions.
*
* With the help of {@link LayoutElement} you can put all the properties to whatever
* layout you like, without knowing React.
*
* This is an example of defining a layout
*
* const layout = [{ width: 1 / 2 }, [
* ['@H3', { children: 'Company data' }],
* 'companyName',
* 'companySize',
* ]],
* [
* ['@H3', { children: 'Contact Info' }],
* [{ flexDirection: 'row', flex: true }, [
* ['email', { pr: 'default', flexGrow: 1 }],
* ['address', { flexGrow: 1 }],
* ]],
* ],
* ]
*
* Alternatively you can pass a function taking {@link CurrentAdmin} as an argument.
* This will allow you to show/hide given property for restricted users.
*
* To see entire documentation and more examples visit {@link LayoutElement}
*
* @see LayoutElement
*/
layout?: ((currentAdmin?: CurrentAdmin) => Array<LayoutElement>) | Array<LayoutElement>;
}
9 changes: 6 additions & 3 deletions src/backend/actions/list-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ const ListAction: Action<ListActionResponse> = {
* @memberof module:ListAction
* @return {Promise<ListActionResponse>} records with metadata
*/
handler: async (request, response, data) => {
handler: async (request, response, context) => {
const { query } = request
const { sortBy, direction, filters = {} } = flat.unflatten(query || {})
const { resource } = data
const { resource } = context
let { page, perPage } = flat.unflatten(query || {})

const listProperties = resource.decorate().getListProperties()
Expand All @@ -59,6 +59,9 @@ const ListAction: Action<ListActionResponse> = {
})
const populatedRecords = await populator(records, listProperties)

// eslint-disable-next-line no-param-reassign
context.records = populatedRecords

const total = await resource.count(filter)
return {
meta: {
Expand All @@ -68,7 +71,7 @@ const ListAction: Action<ListActionResponse> = {
direction: sort.direction,
sortBy: sort.sortBy,
},
records: populatedRecords.map(r => r.toJSON(data.currentAdmin)),
records: populatedRecords.map(r => r.toJSON(context.currentAdmin)),
}
},
}
Expand Down
4 changes: 2 additions & 2 deletions src/backend/adapters/base-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,10 @@ class BaseRecord {
title: this.resource.decorate().titleOf(this),
recordActions: this.resource.decorate().recordActions(
this, currentAdmin,
).map(recordAction => recordAction.toJSON()),
).map(recordAction => recordAction.toJSON(currentAdmin)),
bulkActions: this.resource.decorate().bulkActions(
this, currentAdmin,
).map(recordAction => recordAction.toJSON()),
).map(recordAction => recordAction.toJSON(currentAdmin)),
}
}

Expand Down
19 changes: 18 additions & 1 deletion src/backend/decorators/action-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ActionJSON from './action-json.interface'
import BaseRecord from '../adapters/base-record'
import actionErrorHandler from '../services/action-error-handler'
import ForbiddenError from '../utils/forbidden-error'
import layoutElementParser, { ParsedLayoutElement, LayoutElement } from '../utils/layout-element-parser'

/**
* Decorates an action
Expand Down Expand Up @@ -289,12 +290,27 @@ class ActionDecorator {
return this.action.containerWidth
}

layout(currentAdmin?: CurrentAdmin): Array<ParsedLayoutElement> | null {
if (this.action.layout) {
let layoutConfig: Array<LayoutElement>
if (typeof this.action.layout === 'function') {
layoutConfig = this.action.layout(currentAdmin) as Array<LayoutElement>
} else {
layoutConfig = this.action.layout
}
return layoutConfig.map(element => layoutElementParser(element))
}
return null
}

/**
* Serializes action to JSON format
*
* @param {CurrentAdmin} [currentAdmin]
*
* @return {ActionJSON} serialized action
*/
toJSON(): ActionJSON {
toJSON(currentAdmin?: CurrentAdmin): ActionJSON {
const resourceId = this._resource._decorated?.id() || this._resource.id()
return {
name: this.action.name,
Expand All @@ -308,6 +324,7 @@ class ActionDecorator {
showInDrawer: !!this.action.showInDrawer,
hideActionHeader: !!this.action.hideActionHeader,
containerWidth: this.containerWidth(),
layout: this.layout(currentAdmin),
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/backend/decorators/action-json.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ParsedLayoutElement } from '../utils/layout-element-parser'

/**
* JSON representation of an {@link Action}
* @see Action
Expand Down Expand Up @@ -49,4 +51,9 @@ export default interface ActionJSON {
* Id of a resource to which given action belongs.
*/
resourceId: string;

/**
* Parsed layout passed in {@link Action#layout}
*/
layout: Array<ParsedLayoutElement> | null;
}
1 change: 1 addition & 0 deletions src/backend/decorators/resource-decorator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ describe('ResourceDecorator', function () {
'editProperties',
'showProperties',
'filterProperties',
'properties',
)
})

Expand Down
53 changes: 44 additions & 9 deletions src/backend/decorators/resource-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ResourceOptions } from './resource-options.interface'
import Action, { ActionResponse } from '../actions/action.interface'
import { CurrentAdmin } from '../../current-admin.interface'
import ResourceJSON from './resource-json.interface'
import { PropertyPlace } from './property-json.interface'
import PropertyJSON, { PropertyPlace } from './property-json.interface'
import BaseRecord from '../adapters/base-record'

/**
Expand Down Expand Up @@ -84,6 +84,26 @@ const findSubProperty = (
return null
}

/**
* Bu default all subProperties are nested as an array in root Property. This is easy for
* adapter to maintain. But in AdminBro core we need a fast way to access them by path.
*
* This function changes an array to object recursively (for nested subProperties) so they
* could be accessed via properties['path.to.sub.property']
*
* @param {PropertyDecorator} rootProperty
*
* @return {Record<PropertyDecorator>}
* @private
*/
const flatSubProperties = (rootProperty: PropertyDecorator): Record<string, PropertyJSON> => (
rootProperty.subProperties().reduce((subMemo, subProperty) => ({
...subMemo,
[subProperty.path]: subProperty.toJSON(),
...flatSubProperties(subProperty),
}), {})
)

/**
* Base decorator class which decorates the Resource.
*
Expand Down Expand Up @@ -289,20 +309,21 @@ class ResourceDecorator {
* @return {Array<PropertyDecorator>}
*/
getProperties({ where, max = 0 }: {
where: PropertyPlace;
where?: PropertyPlace;
max?: number;
}): Array<PropertyDecorator> {
const whereProperties = `${where}Properties` // like listProperties, viewProperties etc
if (this.options[whereProperties] && this.options[whereProperties].length) {
if (where && this.options[whereProperties] && this.options[whereProperties].length) {
return this.options[whereProperties].map(this.getPropertyByKey)
}

const properties = Object.keys(this.properties)
.filter(key => this.properties[key].isVisible(where))
.filter(key => !where || this.properties[key].isVisible(where))
.sort((key1, key2) => (
this.properties[key1].position()

> this.properties[key2].position() ? 1 : -1))
this.properties[key1].position() > this.properties[key2].position()
? 1
: -1
))
.map(key => this.properties[key])

if (max) {
Expand All @@ -311,6 +332,19 @@ class ResourceDecorator {
return properties
}

getFlattenProperties(): Record<string, PropertyJSON> {
return Object.keys(this.properties).reduce((memo, propertyName) => {
const property = this.properties[propertyName]

const subProperties = flatSubProperties(property)
return {
...memo,
[propertyName]: property.toJSON(),
...subProperties,
}
}, {})
}

getListProperties(): Array<PropertyDecorator> {
return this.getProperties({ where: 'list', max: DEFAULT_MAX_COLUMNS_IN_LIST })
}
Expand Down Expand Up @@ -419,8 +453,9 @@ class ResourceDecorator {
parent: this.getParent(),
href: this.getHref(currentAdmin),
titleProperty: this.titleProperty().toJSON(),
resourceActions: this.resourceActions(currentAdmin).map(ra => ra.toJSON()),
actions: Object.values(this.actions).map(action => action.toJSON()),
resourceActions: this.resourceActions(currentAdmin).map(ra => ra.toJSON(currentAdmin)),
actions: Object.values(this.actions).map(action => action.toJSON(currentAdmin)),
properties: this.getFlattenProperties(),
listProperties: this.getProperties({
where: 'list', max: DEFAULT_MAX_COLUMNS_IN_LIST,
}).map(property => property.toJSON('list')),
Expand Down
5 changes: 5 additions & 0 deletions src/backend/decorators/resource-json.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,9 @@ export default interface ResourceJSON {
* Properties which should be visible on the filter
*/
filterProperties: Array<PropertyJSON>;
/**
* Map of all properties inside the resource. It also contains nested properties.
* So this is the easies way of getting any property you like from a resource.
*/
properties: Record<string, PropertyJSON>;
}
86 changes: 86 additions & 0 deletions src/backend/utils/layout-element-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { expect } from 'chai'

import layoutElementParser, { LayoutElement } from './layout-element-parser'

describe('layoutElementParser', function () {
const propertyName = 'name'
const property2 = 'surname'
const props = { mt: 'default', ml: 'xxxl' }

it('parses regular string', function () {
expect(layoutElementParser(propertyName)).to.deep.eq({
properties: [propertyName],
props: {},
layoutElements: [],
component: 'Box',
})
})

it('parses list of strings', function () {
expect(layoutElementParser([propertyName, property2])).to.deep.eq({
properties: [propertyName, property2],
props: { },
layoutElements: [],
component: 'Box',
})
})

it('parses property and props', function () {
expect(layoutElementParser([propertyName, props])).to.deep.eq({
properties: [propertyName],
props,
layoutElements: [],
component: 'Box',
})
})

it('recursively parses and inner element as string', function () {
const innerElement: LayoutElement = ['string2', { width: 1 / 2 }]
expect(layoutElementParser([props, [innerElement]])).to.deep.eq({
properties: [],
props,
layoutElements: [layoutElementParser(innerElement)],
component: 'Box',
})
})

it('recursively parses nested objects', function () {
const nested: Array<LayoutElement> = [
['companyName', { ml: 'xxl' }],
'email',
['address', 'profilePhotoLocation'],
]
const complicatedElement: LayoutElement = [props, nested]
expect(layoutElementParser(complicatedElement)).to.deep.eq({
properties: [],
props,
layoutElements: nested.map(el => layoutElementParser(el)),
component: 'Box',
})
})

it('returns layoutElements when array is passed', function () {
const arrayElements: LayoutElement = [
['string1', { width: 1 / 2 }],
['string2', { width: 1 / 2 }],
]
expect(layoutElementParser(arrayElements)).to.deep.eq({
properties: [],
props: {},
component: 'Box',
layoutElements: arrayElements.map(innerElement => layoutElementParser(innerElement)),
})
})

it('changes the component when @ is appended', function () {
const headerProps = { children: 'Welcome my boy' }
const componentElements: LayoutElement = ['@Header', headerProps]

expect(layoutElementParser(componentElements)).to.deep.eq({
properties: [],
props: headerProps,
component: 'Header',
layoutElements: [],
})
})
})
Loading

0 comments on commit 2c991d3

Please sign in to comment.