Merge branch 'master' into chore/routing-x-forwarded

This commit is contained in:
Alexandre BODIN 2019-09-16 23:07:54 +02:00 committed by GitHub
commit 8a6c162095
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 378 additions and 44 deletions

View File

@ -492,7 +492,7 @@ module.exports = {
Query: {
post: {
description: 'Return a single post',
policy: ['plugins.users-permissions.isAuthenticated', 'isOwner'], // Apply the 'isAuthenticated' policy of the `Users & Permissions` plugin, then the 'isOwner' policy before executing the resolver.
policies: ['plugins.users-permissions.isAuthenticated', 'isOwner'], // Apply the 'isAuthenticated' policy of the `Users & Permissions` plugin, then the 'isOwner' policy before executing the resolver.
},
posts: {
description: 'Return a list of posts', // Add a description to the query.
@ -504,7 +504,7 @@ module.exports = {
},
postsByTags: {
description: 'Return the posts published by the author',
resolverOf: 'Post.findByTags', // Will apply the same policy on the custom resolver than the controller's action `findByTags`.
resolverOf: 'Post.findByTags', // Will apply the same policy on the custom resolver as the controller's action `findByTags`.
resolver: (obj, options, ctx) => {
// ctx is the context of the Koa request.
await strapi.controllers.posts.findByTags(ctx);
@ -516,7 +516,7 @@ module.exports = {
Mutation: {
attachPostToAuthor: {
description: 'Attach a post to an author',
policy: ['plugins.users-permissions.isAuthenticated', 'isOwner'],
policies: ['plugins.users-permissions.isAuthenticated', 'isOwner'],
resolver: 'Post.attachToAuthor'
}
}
@ -677,7 +677,7 @@ module.exports = {
Query: {
posts: {
description: 'Return a list of posts',
policy: [
policies: [
'plugins.users-permissions.isAuthenticated',
'isOwner',
'global.logging',
@ -687,7 +687,10 @@ module.exports = {
Mutation: {
createPost: {
description: 'Create a new post',
policy: ['plugins.users-permissions.isAuthenticated', 'global.logging'],
policies: [
'plugins.users-permissions.isAuthenticated',
'global.logging',
],
},
},
},
@ -782,7 +785,7 @@ module.exports = {
Query: {
posts: {
description: 'Return a list of posts by author',
resolverOf: 'Post.find', // Will apply the same policy on the custom resolver than the controller's action `find` located in `Post.js`.
resolverOf: 'Post.find', // Will apply the same policy on the custom resolver as the controller's action `find` located in `Post.js`.
resolver: (obj, options, context) => {
// You can return a raw JSON object or a promise.

View File

@ -5,7 +5,7 @@
> div {
width: 30px;
height: 100%;
color: #0E1622;
color: #0e1622;
}
> div:last-child {
margin-left: 0;

View File

@ -0,0 +1 @@
<svg width="35" height="35" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="#FAFAFB" width="35" height="35" rx="17.5"/><text font-family="Lato-Medium, Lato" font-size="12" font-weight="400" fill="#838383"><tspan x="6" y="22">N/A</tspan></text></g></svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@ -1,4 +1,4 @@
import React, { memo } from 'react';
import React, { memo, useCallback } from 'react';
import { withRouter } from 'react-router';
import PropTypes from 'prop-types';
import { get, isEmpty, isNull, isObject, toLower, toString } from 'lodash';
@ -7,6 +7,7 @@ import { IcoContainer } from 'strapi-helper-plugin';
import { useListView } from '../../contexts/ListView';
import CustomInputCheckbox from '../CustomInputCheckbox';
import MediaPreviewList from '../MediaPreviewList';
import { ActionContainer, Truncate, Truncated } from './styledComponents';
@ -46,6 +47,10 @@ const getDisplayedValue = (type, value, name) => {
}
case 'password':
return '••••••••';
case 'media':
case 'file':
case 'files':
return value;
default:
return '-';
}
@ -59,6 +64,15 @@ function Row({ goTo, isBulkable, row, headers }) {
schema,
} = useListView();
const memoizedDisplayedValue = useCallback(
name => {
const type = get(schema, ['attributes', name, 'type'], 'string');
return getDisplayedValue(type, row[name], name);
},
[row, schema]
);
return (
<>
{isBulkable && (
@ -76,20 +90,21 @@ function Row({ goTo, isBulkable, row, headers }) {
{headers.map(header => {
return (
<td key={header.name}>
<Truncate>
<Truncated>
{getDisplayedValue(
get(schema, ['attributes', header.name, 'type'], 'string'),
row[header.name],
header.name
)}
</Truncated>
</Truncate>
{get(schema, ['attributes', header.name, 'type']) !== 'media' ? (
<Truncate>
<Truncated>{memoizedDisplayedValue(header.name)}</Truncated>
</Truncate>
) : (
<MediaPreviewList
files={memoizedDisplayedValue(header.name)}
></MediaPreviewList>
)}
</td>
);
})}
<ActionContainer>
<IcoContainer
style={{ minWidth: 'inherit', width: '100%' }}
icons={[
{
icoType: 'pencil',

View File

@ -57,7 +57,7 @@ function TableHeader({ headers, isBulkable }) {
}
}}
>
<span>
<span className={header.sortable ? 'sortable' : ''}>
{header.label}
{sortBy === header.name && (
<Icon

View File

@ -3,7 +3,6 @@ import styled, { css } from 'styled-components';
const Table = styled.table`
border-radius: 3px;
border-collapse: initial;
overflow: hidden;
box-shadow: 0 2px 4px #e3e9f3;
table-layout: fixed;
margin-bottom: 0;
@ -33,7 +32,9 @@ const Thead = styled.thead`
vertical-align: middle !important;
> span {
position: relative;
cursor: pointer;
&.sortable {
cursor: pointer;
}
}
}
${({ isBulkable }) => {

View File

@ -0,0 +1,171 @@
import styled, { css } from 'styled-components';
const sizes = {
small: '35px',
big: '127px',
margin: '20px',
};
const max = 4;
const StyledMediaPreviewList = styled.div`
position: relative;
height: ${sizes.small};
> div {
position: absolute;
top: 0;
${createCSS()};
}
`;
function createCSS() {
let styles = '';
for (let i = 0; i <= max; i += 1) {
styles += `
&:nth-of-type(${i}) {
left: calc( ${sizes.margin} * ${i - 1});
z-index: ${i};
}
`;
}
return css`
${styles}
`;
}
const MediaPreviewItem = styled.div`
width: ${sizes.small};
height: ${sizes.small};
div {
width: 100%;
height: 100%;
overflow: hidden;
border-radius: calc(${sizes.small} / 2);
background-color: #f3f3f4;
border: 1px solid #f3f3f4;
}
&.hoverable {
:hover {
z-index: ${max + 1};
}
}
`;
const MediaPreviewFile = styled(MediaPreviewItem)`
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
div {
position: relative;
background-color: #f3f3f4;
color: #333740;
text-align: center;
line-height: ${sizes.small};
border: 1px solid white;
span {
display: block;
padding: 0 3px;
text-transform: uppercase;
font-size: 10px;
font-weight: 600;
}
i {
position: absolute;
left: 1px;
top: -1px;
font-size: 15px;
width: 100%;
height: 100%;
&:before {
width: 100%;
height: 100%;
padding: 10px;
line-height: 35px;
background: #f3f3f4;
}
}
}
div + span {
display: none;
position: absolute;
left: 120%;
bottom: -10px;
display: none;
max-width: 150px;
color: #333740;
}
&.hoverable {
:hover {
div + span {
display: block;
}
}
}
`;
const MediaPreviewText = styled(MediaPreviewItem)`
div {
font-size: 13px;
color: #333740;
text-align: center;
line-height: ${sizes.small};
font-weight: 600;
}
`;
const MediaPreviewImage = styled(MediaPreviewItem)`
img {
display: block;
object-fit: cover;
background-color: #fafafb;
}
div {
position: relative;
&::before {
content: '-';
position: absolute;
width: 100%;
height: 100%;
background: white;
color: transparent;
opacity: 0;
}
img {
width: 100%;
height: 100%;
}
}
div + img {
display: none;
width: ${sizes.big};
height: ${sizes.big};
border-radius: calc(${sizes.big} / 2);
margin-top: calc(-${sizes.big} - ${sizes.small} - 5px);
margin-left: calc((-${sizes.big} + ${sizes.small}) / 2);
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.05);
}
&.hoverable {
:hover {
div {
&::before {
opacity: 0.6;
}
}
div + img {
display: block;
}
}
}
`;
export {
MediaPreviewFile,
MediaPreviewImage,
MediaPreviewItem,
MediaPreviewText,
StyledMediaPreviewList,
};

View File

@ -0,0 +1,105 @@
import React from 'react';
import PropTypes from 'prop-types';
import { isArray, includes, isEmpty } from 'lodash';
import DefaultIcon from '../../assets/images/media/na.svg';
import {
StyledMediaPreviewList,
MediaPreviewFile,
MediaPreviewImage,
MediaPreviewItem,
MediaPreviewText,
} from './StyledMediaPreviewList';
function MediaPreviewList({ hoverable, files }) {
const getFileType = fileName => fileName.split('.').slice(-1)[0];
const getSrc = fileURL =>
fileURL.startsWith('/') ? `${strapi.backendURL}${fileURL}` : fileURL;
const renderImage = image => {
const { name, size, url } = image;
if (size > 2000) {
return renderFile(image);
}
return (
<MediaPreviewImage className={hoverable ? 'hoverable' : ''}>
<div>
<img src={getSrc(url)} alt={`${name}`} />
</div>
<img src={getSrc(url)} alt={`${name}`} />
</MediaPreviewImage>
);
};
const renderFile = file => {
const { mime, name } = file;
const fileType = includes(mime, 'image') ? 'image' : getFileType(name);
return (
<MediaPreviewFile className={hoverable ? 'hoverable' : ''}>
<div>
<span>{fileType}</span>
<i className={`fa fa-file-${fileType}-o`} />
</div>
<span>{name}</span>
</MediaPreviewFile>
);
};
const renderItem = file => {
const { mime } = file;
return (
<React.Fragment key={JSON.stringify(file)}>
{includes(mime, 'image') ? renderImage(file) : renderFile(file)}
</React.Fragment>
);
};
const renderText = count => {
return (
<MediaPreviewText>
<div>
<span>+{count}</span>
</div>
</MediaPreviewText>
);
};
const renderMultipleItems = files => {
return files.map((file, index) => {
return (
<React.Fragment key={JSON.stringify(file)}>
{index === 3 && files.length - 4 > 0
? renderText(files.length - 4)
: renderItem(file)}
</React.Fragment>
);
});
};
return !!files && !isEmpty(files) ? (
<StyledMediaPreviewList>
{!isArray(files) ? renderItem(files) : renderMultipleItems(files)}
</StyledMediaPreviewList>
) : (
<MediaPreviewItem>
<img src={DefaultIcon} alt="default" />
</MediaPreviewItem>
);
}
MediaPreviewList.defaultProps = {
hoverable: true,
files: null,
};
MediaPreviewList.propTypes = {
hoverable: PropTypes.bool,
files: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
};
export default MediaPreviewList;

View File

@ -132,7 +132,7 @@ function ListView({
Object.keys(getMetaDatas())
.filter(
key =>
!['json', 'group', 'relation', 'media', 'richtext'].includes(
!['json', 'group', 'relation', 'richtext'].includes(
get(layouts, [slug, 'schema', 'attributes', key, 'type'], '')
)
)

View File

@ -32,7 +32,6 @@ function ListLayout({
strapi.notification.info(`${pluginId}.notification.info.minimumFields`);
};
const fieldName = displayedData[fieldToEditIndex];
const fieldPath = ['metadatas', fieldName, 'list'];

View File

@ -214,7 +214,7 @@ function SettingViewModel({
.filter(key => {
const type = get(attributes, [key, 'type'], '');
return !['json', 'relation', 'group', 'media'].includes(type) && !!type;
return !['json', 'relation', 'group'].includes(type) && !!type;
})
.filter(field => {
return !getListDisplayedFields().includes(field);

View File

@ -3,9 +3,19 @@
const _ = require('lodash');
const NON_SORTABLES = ['group', 'json', 'relation', 'media'];
const NON_LISTABLES = ['group', 'json', 'relation', 'password'];
const isListable = (schema, name) => {
return isSortable(schema, name) && schema.attributes[name].type != 'password';
if (!_.has(schema.attributes, name)) {
return false;
}
const attribute = schema.attributes[name];
if (NON_LISTABLES.includes(attribute.type)) {
return false;
}
return true;
};
const isSortable = (schema, name) => {

View File

@ -19,11 +19,15 @@ module.exports = {
* @return Promise or Error.
*/
composeMutationResolver: function(_schema, plugin, name, action) {
composeMutationResolver: function({ _schema, plugin, name, action }) {
// Extract custom resolver or type description.
const { resolver: handler = {} } = _schema;
const queryName = `${action}${_.capitalize(name)}`;
let queryName = `${action}${_.capitalize(name)}`;
if (_.has(handler, `Mutation.${action}`)) {
queryName = action;
}
// Retrieve policies.
const policies = _.get(handler, `Mutation.${queryName}.policies`, []);
@ -155,7 +159,7 @@ module.exports = {
}
if (strapi.plugins['users-permissions']) {
policies.push('plugins.users-permissions.permissions');
policies.unshift('plugins.users-permissions.permissions');
}
// Populate policies.

View File

@ -69,7 +69,7 @@ module.exports = {
* @return Promise or Error.
*/
composeQueryResolver: function(_schema, plugin, name, isSingular) {
composeQueryResolver: function({ _schema, plugin, name, isSingular }) {
const params = {
model: name,
};
@ -236,7 +236,7 @@ module.exports = {
}
if (strapi.plugins['users-permissions']) {
policies.push('plugins.users-permissions.permissions');
policies.unshift('plugins.users-permissions.permissions');
}
// Populate policies.

View File

@ -326,11 +326,21 @@ const buildShadowCRUD = (models, plugin) => {
const queries = {
singular:
_.get(resolver, `Query.${singularName}`) !== false
? Query.composeQueryResolver(_schema, plugin, name, true)
? Query.composeQueryResolver({
_schema,
plugin,
name,
isSingular: true,
})
: null,
plural:
_.get(resolver, `Query.${pluralName}`) !== false
? Query.composeQueryResolver(_schema, plugin, name, false)
? Query.composeQueryResolver({
_schema,
plugin,
name,
isSingular: false,
})
: null,
};
@ -376,15 +386,30 @@ const buildShadowCRUD = (models, plugin) => {
const mutations = {
create:
_.get(resolver, `Mutation.create${capitalizedName}`) !== false
? Mutation.composeMutationResolver(_schema, plugin, name, 'create')
? Mutation.composeMutationResolver({
_schema,
plugin,
name,
action: 'create',
})
: null,
update:
_.get(resolver, `Mutation.update${capitalizedName}`) !== false
? Mutation.composeMutationResolver(_schema, plugin, name, 'update')
? Mutation.composeMutationResolver({
_schema,
plugin,
name,
action: 'update',
})
: null,
delete:
_.get(resolver, `Mutation.delete${capitalizedName}`) !== false
? Mutation.composeMutationResolver(_schema, plugin, name, 'delete')
? Mutation.composeMutationResolver({
_schema,
plugin,
name,
action: 'delete',
})
: null,
};

View File

@ -228,22 +228,22 @@ const schemaBuilder = {
const [name, action] = acc[type][resolver].split('.');
const normalizedName = _.toLower(name);
acc[type][resolver] = Mutation.composeMutationResolver(
strapi.plugins.graphql.config._schema.graphql,
acc[type][resolver] = Mutation.composeMutationResolver({
_schema: strapi.plugins.graphql.config._schema.graphql,
plugin,
normalizedName,
action
);
name: normalizedName,
action,
});
break;
}
case 'Query':
default:
acc[type][resolver] = Query.composeQueryResolver(
strapi.plugins.graphql.config._schema.graphql,
acc[type][resolver] = Query.composeQueryResolver({
_schema: strapi.plugins.graphql.config._schema.graphql,
plugin,
resolver,
'force' // Avoid singular/pluralize and force query name.
);
name: resolver,
isSingular: 'force', // Avoid singular/pluralize and force query name.
});
break;
}
}