mirror of
https://github.com/strapi/strapi.git
synced 2025-12-29 08:04:51 +00:00
Merge branch 'master' into chore/routing-x-forwarded
This commit is contained in:
commit
8a6c162095
@ -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.
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
> div {
|
||||
width: 30px;
|
||||
height: 100%;
|
||||
color: #0E1622;
|
||||
color: #0e1622;
|
||||
}
|
||||
> div:last-child {
|
||||
margin-left: 0;
|
||||
|
||||
@ -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 |
@ -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',
|
||||
|
||||
@ -57,7 +57,7 @@ function TableHeader({ headers, isBulkable }) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className={header.sortable ? 'sortable' : ''}>
|
||||
{header.label}
|
||||
{sortBy === header.name && (
|
||||
<Icon
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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;
|
||||
@ -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'], '')
|
||||
)
|
||||
)
|
||||
|
||||
@ -32,7 +32,6 @@ function ListLayout({
|
||||
|
||||
strapi.notification.info(`${pluginId}.notification.info.minimumFields`);
|
||||
};
|
||||
|
||||
const fieldName = displayedData[fieldToEditIndex];
|
||||
const fieldPath = ['metadatas', fieldName, 'list'];
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user